Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c2b6ed7
Update .gitignore to include .scratch directory
teresaromero May 27, 2026
1166296
Add internal Mode scaffolding for validation modes
teresaromero May 27, 2026
892c661
Add public constructor API with mode-aware validators (task 02)
teresaromero May 27, 2026
a5c8cfc
Add eager path/fs validation to NewFromPath and NewFromFS constructors
teresaromero May 28, 2026
30cfd76
Fix API: NewFromZip drops mode param, NewFromFS ModeSource uses linke…
teresaromero Jun 1, 2026
d482f31
Fix lint: add godoc comments to exported modes package symbols
teresaromero Jun 1, 2026
2769edc
Add mode.Valid() guard in NewSpec and improve Validate() closer error…
teresaromero Jun 1, 2026
324c4a0
Fix TestLegacyPreservation_FromZip to match NewFromZip signature change
teresaromero Jun 1, 2026
d999602
Add support for mode-aware constructors and validation APIs in changelog
teresaromero Jun 2, 2026
fe83a0a
Address PR review: fix API semantics, tests, and remove private newFr…
teresaromero Jun 2, 2026
491658c
Fix TestNewFromZip_ConstructorSucceeds file handle leak on Windows
teresaromero Jun 2, 2026
15750f5
Update .gitignore to remove .scratch directory entry
teresaromero Jun 3, 2026
fcaf8c2
Refactor Validator constructors to remove Option parameter
teresaromero Jun 3, 2026
f4e266e
refactor POV on modes validation
teresaromero Jun 3, 2026
e9d0013
remove unused public mode
teresaromero Jun 3, 2026
cbd3b7c
Improve documentation for validation API and modes
teresaromero Jun 3, 2026
0f275c0
Add unit tests for mode validation
teresaromero Jun 3, 2026
d1efd03
Add integration tests for link file behavior across validation modes
teresaromero Jun 3, 2026
4233331
Remove unused test case for package validation without links in TestL…
teresaromero Jun 3, 2026
da2a325
Remove wrapping around specFn
teresaromero Jun 4, 2026
8539209
Restore Option C Validator API with mode-embedded FS and fix link blo…
teresaromero Jun 4, 2026
b0049ff
Improve godoc comments for validation modes API
teresaromero Jun 4, 2026
d4508b1
Add copyright notice to modes.go file
teresaromero Jun 4, 2026
5d48e9d
Add build/source mode semantic validations and composable package sup…
teresaromero Jun 1, 2026
da153df
Fix stale NewFromZip comment and bad_built_missing_input README
teresaromero Jun 2, 2026
14145a5
Remove unnecessary ErrUnsupportedLinkFile suppression from validateFile
teresaromero Jun 2, 2026
e5b5d98
Update validation rules to include Legacy mode for integration inputs…
teresaromero Jun 3, 2026
c6f4749
Add godoc comment to helper
teresaromero Jun 3, 2026
6f8a1e8
Add build mode integration tests for orphaned test fixtures
teresaromero Jun 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions code/go/internal/validator/modes/modes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package modes

// Mode represents the validation context: which semantic rules run and how
// linked (.link) files are handled during package validation.
type Mode string

const (
// Legacy preserves the original validation behavior: linked files are
// resolved transparently and no rules are mode-gated.
Legacy Mode = "legacy"
// Source validates a package as a checked-out source tree: linked files
// are resolved transparently and source-only rules are enforced.
Source Mode = "source"
// Build validates a package as a built artifact: linked files are
// unconditionally blocked and build-only rules are enforced.
Build Mode = "build"
)

// Valid reports whether m is a recognised validation mode.
func (m Mode) Valid() bool {
switch m {
case Legacy, Source, Build:
return true
}
return false
}
45 changes: 45 additions & 0 deletions code/go/internal/validator/modes/modes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package modes

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestValid(t *testing.T) {
tests := map[string]struct {
mode Mode
valid bool
}{
"valid": {
mode: Legacy,
valid: true,
},
"invalid": {
mode: Mode("invalid"),
valid: false,
},
"source": {
mode: Source,
valid: true,
},
"build": {
mode: Build,
valid: true,
},
"": {
mode: Mode(""),
valid: false,
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
assert.Equal(t, test.valid, test.mode.Valid(), "mode %s should be %v", test.mode, test.valid)
})
}
}
8 changes: 5 additions & 3 deletions code/go/internal/validator/semantic/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,9 +352,11 @@ func listDataStreams(fsys fspath.FS) ([]string, error) {
return nil, fmt.Errorf("can't list data streams directory: %w", err)
}

list := make([]string, len(dataStreams))
for i, dataStream := range dataStreams {
list[i] = dataStream.Name()
var list []string
for _, dataStream := range dataStreams {
if dataStream.IsDir() {
list = append(list, dataStream.Name())
}
}
return list, nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func ValidateDatastreamPackageCategories(fsys fspath.FS) specerrors.ValidationEr
specerrors.NewStructuredErrorf("file \"%s\" is invalid: %w", fsys.Path(manifestPath), err)}
}

if pkgType != packageTypeIntegration {
if pkgType != integrationPackageType {
return nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func ValidateIntegrationInputsDeprecation(fsys fspath.FS) specerrors.ValidationE
specerrors.NewStructuredErrorf("file \"%s\" is invalid: %w", fsys.Path(manifestPath), err)}
}
// skip if not an integration package
if m.Type != packageTypeIntegration {
if m.Type != integrationPackageType {
return nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ import (

const (
defaultStreamTemplatePath = "stream.yml.hbs"
packageTypeIntegration = "integration"
)

type policyTemplateInput struct {
Type string `yaml:"type"`
Package string `yaml:"package"`
TemplatePath string `yaml:"template_path"`
TemplatePaths []string `yaml:"template_paths"`
}
Expand All @@ -41,6 +41,7 @@ type integrationPackageManifest struct { // package manifest

type stream struct {
Input string `yaml:"input"`
Package string `yaml:"package"`
TemplatePath string `yaml:"template_path"`
TemplatePaths []string `yaml:"template_paths"`
}
Expand Down Expand Up @@ -78,7 +79,7 @@ func ValidateIntegrationPolicyTemplates(fsys fspath.FS) specerrors.ValidationErr
specerrors.NewStructuredErrorf("file \"%s\" is invalid: %w", fsys.Path(manifestPath), errFailedToParseManifest)}
}

if manifest.Type != packageTypeIntegration {
if manifest.Type != integrationPackageType {
return nil
}

Expand Down Expand Up @@ -110,6 +111,14 @@ func ValidateIntegrationPolicyTemplates(fsys fspath.FS) specerrors.ValidationErr
// under agent/input when template_paths or template_path is set (Fleet: template_paths first).
func validateIntegrationPolicyTemplateInputs(fsys fspath.FS, policyTemplate integrationPolicyTemplate) error {
for _, input := range policyTemplate.Inputs {
// Composable inputs reference an input package via 'package:'. When no
// explicit template_path/template_paths is set, all templates come from
// the dependency and are only present after build. Skip those.
// If the composable input defines its own template_path or template_paths
// (overlay templates that live in the source package), those are validated.
if input.Package != "" && input.TemplatePath == "" && len(input.TemplatePaths) == 0 {
continue
}
if len(input.TemplatePaths) > 0 {
for _, tp := range input.TemplatePaths {
if err := validateAgentInputTemplatePath(fsys, tp); err != nil {
Expand Down Expand Up @@ -141,6 +150,17 @@ func validateAllDataStreamStreamTemplates(fsys fspath.FS, dsMap map[string]dataS
dsManifestPath := path.Join(dsDir, "manifest.yml")
manifest := dsMap[dsDir]
for _, s := range manifest.Streams {
// Composable streams reference an input package via 'package:'. When
// no explicit template_path/template_paths is set on the stream, all
// templates come from the dependency and are only present after build.
// Skip those — ValidateStreamInputMaterialized enforces that 'package:'
// is replaced by 'input:' in build mode.
// However, if the composable stream defines its own template_path or
// template_paths, those files must exist in the source package and are
// validated here.
if s.Package != "" && s.TemplatePath == "" && len(s.TemplatePaths) == 0 {
continue
}
if err := validateSingleDataStreamStreamTemplates(fsys, dsDir, s); err != nil {
errs = append(errs, specerrors.NewStructuredErrorf(
"file \"%s\" is invalid: data stream \"%s\" stream input %q: %w",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,65 @@ streams:
errs := ValidateIntegrationPolicyTemplates(fspath.DirFS(d))
require.Empty(t, errs)
})

t.Run("composable stream with no explicit templates skips validation", func(t *testing.T) {
d := t.TempDir()
writeMinimalIntegrationManifest(t, d)
// No agent/stream directory — templates come entirely from the input package.
err := os.MkdirAll(filepath.Join(d, "data_stream", "logs"), 0o755)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(d, "data_stream", "logs", "manifest.yml"), []byte(`
streams:
- package: some_input_package
title: Composable
description: d
`), 0o644)
require.NoError(t, err)

errs := ValidateIntegrationPolicyTemplates(fspath.DirFS(d))
require.Empty(t, errs)
})

t.Run("composable stream with explicit template_paths validates those files", func(t *testing.T) {
d := t.TempDir()
writeMinimalIntegrationManifest(t, d)
err := os.MkdirAll(filepath.Join(d, "data_stream", "logs", "agent", "stream"), 0o755)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(d, "data_stream", "logs", "agent", "stream", "overlay.yml.hbs"), []byte(`x`), 0o644)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(d, "data_stream", "logs", "manifest.yml"), []byte(`
streams:
- package: some_input_package
title: Composable with overlay
description: d
template_paths:
- overlay.yml.hbs
`), 0o644)
require.NoError(t, err)

errs := ValidateIntegrationPolicyTemplates(fspath.DirFS(d))
require.Empty(t, errs)
})

t.Run("composable stream with explicit template_paths fails when file missing", func(t *testing.T) {
d := t.TempDir()
writeMinimalIntegrationManifest(t, d)
err := os.MkdirAll(filepath.Join(d, "data_stream", "logs", "agent", "stream"), 0o755)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(d, "data_stream", "logs", "manifest.yml"), []byte(`
streams:
- package: some_input_package
title: Composable with overlay
description: d
template_paths:
- missing.yml.hbs
`), 0o644)
require.NoError(t, err)

errs := ValidateIntegrationPolicyTemplates(fspath.DirFS(d))
require.Len(t, errs, 1)
require.Contains(t, errs[0].Error(), "template file not found")
})
}
func TestValidateIntegrationPolicyTemplates_NonIntegrationType(t *testing.T) {
d := t.TempDir()
Expand Down Expand Up @@ -323,6 +382,70 @@ streams:
errs := ValidateIntegrationPolicyTemplates(fspath.DirFS(d))
require.Empty(t, errs)
}
func TestValidateIntegrationPolicyTemplates_ComposableInputs(t *testing.T) {
t.Run("composable input with no templates skips validation", func(t *testing.T) {
d := t.TempDir()
err := os.WriteFile(filepath.Join(d, "manifest.yml"), []byte(`
type: integration
policy_templates:
- name: pt
inputs:
- package: some_input_package
title: Composable
description: d
`), 0o644)
require.NoError(t, err)

errs := ValidateIntegrationPolicyTemplates(fspath.DirFS(d))
require.Empty(t, errs)
})

t.Run("composable input with explicit template_paths validates those files", func(t *testing.T) {
d := t.TempDir()
err := os.MkdirAll(filepath.Join(d, "agent", "input"), 0o755)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(d, "agent", "input", "overlay.yml.hbs"), []byte(`x`), 0o644)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(d, "manifest.yml"), []byte(`
type: integration
policy_templates:
- name: pt
inputs:
- package: some_input_package
title: Composable with overlay
description: d
template_paths:
- overlay.yml.hbs
`), 0o644)
require.NoError(t, err)

errs := ValidateIntegrationPolicyTemplates(fspath.DirFS(d))
require.Empty(t, errs)
})

t.Run("composable input with explicit template_paths fails when file missing", func(t *testing.T) {
d := t.TempDir()
err := os.MkdirAll(filepath.Join(d, "agent", "input"), 0o755)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(d, "manifest.yml"), []byte(`
type: integration
policy_templates:
- name: pt
inputs:
- package: some_input_package
title: Composable with overlay
description: d
template_paths:
- missing.yml.hbs
`), 0o644)
require.NoError(t, err)

errs := ValidateIntegrationPolicyTemplates(fspath.DirFS(d))
require.Len(t, errs, 1)
require.Contains(t, errs[0].Error(), "template file not found")
})
}

func TestFindPathAtDirectory(t *testing.T) {
d := t.TempDir()

Expand Down
39 changes: 39 additions & 0 deletions code/go/internal/validator/semantic/validate_no_dev_folder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package semantic

import (
"io/fs"

"github.com/elastic/package-spec/v3/code/go/internal/fspath"
"github.com/elastic/package-spec/v3/code/go/pkg/specerrors"
)

// ValidateNoDevFolder errors for any _dev/ directory found in the package.
// _dev/ is a source-only artifact used during development (tests, deploy
// configs, build manifests). It must not appear in a built package that is
// validated with ModeBuild.
func ValidateNoDevFolder(fsys fspath.FS) specerrors.ValidationErrors {
var errs specerrors.ValidationErrors
walkErr := fs.WalkDir(fsys, ".", func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() && d.Name() == "_dev" {
errs = append(errs, specerrors.NewStructuredErrorf(
"file %q: _dev directory is not allowed in built packages",
fsys.Path(p),
))
// Skip the subtree to avoid generating child errors for each
// file inside the _dev directory.
return fs.SkipDir
}
return nil
})
if walkErr != nil {
errs = append(errs, specerrors.NewStructuredError(walkErr, specerrors.UnassignedCode))
}
return errs
}
Loading