Skip to content
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
51 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
8d441f1
Move modes into internal validator pkg
teresaromero Jun 5, 2026
15cc91e
Refactor validator API to streamline mode handling
teresaromero Jun 5, 2026
cc5e273
Remove modes.go file and consolidate mode definitions in validator.go
teresaromero Jun 5, 2026
2a3fef8
Add comprehensive tests for ValidateFromFS and ValidateFromZip methods
teresaromero Jun 5, 2026
d8a2a6c
Refactor validation mode constants for clarity and consistency
teresaromero Jun 5, 2026
ebac0c7
Refactor validator instantiation to unify creation method
teresaromero Jun 5, 2026
d895e49
Add logging for validation mode in technical preview
teresaromero Jun 5, 2026
6fd467f
Merge branch '549-validation-api' of github.com:teresaromero/package-…
teresaromero Jun 15, 2026
2abdb49
Add structured error handling for validation mode warnings
teresaromero Jun 15, 2026
ab9ad08
Merge branch '549-validation-api' of github.com:teresaromero/package-…
teresaromero Jun 16, 2026
73326a2
Add build/source mode semantic validations and composable package sup…
teresaromero Jun 1, 2026
9345254
Fix stale NewFromZip comment and bad_built_missing_input README
teresaromero Jun 2, 2026
cc99311
Remove unnecessary ErrUnsupportedLinkFile suppression from validateFile
teresaromero Jun 2, 2026
0c34f3f
Update validation rules to include Legacy mode for integration inputs…
teresaromero Jun 3, 2026
7b72caf
Add godoc comment to helper
teresaromero Jun 3, 2026
561b526
Fix fixture isolation, add e2e source-mode tests, and update changelo…
teresaromero Jun 16, 2026
8ccac7b
Merge bad_built_with_dev and bad_built_with_link into bad_built_fs_ar…
teresaromero Jun 16, 2026
148aebf
Refactor TestBuildModeValidation to follow file test-table conventions
teresaromero Jun 16, 2026
8c60420
Remove redundant package guard from validateIntegrationPolicyTemplate…
teresaromero Jun 17, 2026
a52f51a
Address PR nits: rename Materialized→Bundled, simplify comment, move …
teresaromero Jun 17, 2026
5b78784
Remove test/packages/build_mode now that fixtures live in test/built_…
teresaromero Jun 17, 2026
44f5383
Change ValidateNoExternalEcs to ValidateNoExternalFields
teresaromero Jun 17, 2026
b66a1f3
Enforce _dev/ source-only constraint via spec instead of semantic walk
teresaromero Jun 17, 2026
4206c50
Enforce .link source-only constraint via folder validator instead of …
teresaromero Jun 17, 2026
dedc2ff
Remove redundant missing-input check from ValidateStreamInputBundled
teresaromero Jun 17, 2026
c29ab54
Update TestValidateNoExternalFields to ensure consistent error expect…
teresaromero Jun 18, 2026
2fdf404
Replace sourceOnly bool with validationMode string in item spec
teresaromero Jun 18, 2026
1219cd3
Fix validationMode gaps: enum validation, file enforcement, consisten…
teresaromero Jun 18, 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
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())
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the motivation of this change? Can we have anything under dataStreamDir that is not a directory?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by the spec definition there are only directories on the datastream dir, but, fs.ReadDir is gathering directories and files, so i thought it was interesting to add this guard at this layer to ensure it. based on the docs https://pkg.go.dev/io/fs#DirEntry the direntry interface could be one or the other...

}
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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this exception needed?

I see two options for that:

  • We don't validate template paths on source packages. Then this validator is never run with input.Package != "".
  • We validate template paths on source packages. Then the value of input.Package should not matter.

Also, unit tests pass without this code.

Suggested change
// 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
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i removed the check. i prefer the validation runs on source and build. source is run on elastic-package lint - build - install; if there is any missing file we will notice before getting to the bundle.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit. A bit too verbose.

Suggested change
// 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.
// Don't validate template paths if they use a package as input
// and they don't define any template.

if s.Package != "" && s.TemplatePath == "" && len(s.TemplatePaths) == 0 {
continue
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case I guess we need to maintain this condition if we want to validate templates at source packages. This case is different because a default template is expected if none is defined, and for composable packages it would be fine to don't have one in the source.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, i am keeping this as the source validation is mantained

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There would be any way to define this in the spec?

Maybe folder spec should be also aware of the validation mode, and it should not accept files marked with some "source" marker.

And we could reuse it for link files, or other files that should be there only in source packages.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implemented this from the spec, both dev and .link files are checked based on mode on the spec. i was suggested to use the visibility: private setting which was shared with all _dev folder placements, but i thought this might have more semantical use so i created a sourceOnly boolean property for this case... should we use visbility field instead? i have mixed feelings as i think visibility could mean a broader scope...

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
}
87 changes: 87 additions & 0 deletions code/go/internal/validator/semantic/validate_no_dev_folder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// 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 (
"os"
"path/filepath"
"strings"
"testing"

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

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

func TestValidateNoDevFolder(t *testing.T) {
tests := []struct {
name string
dirs []string // directories to create relative to the temp root
expectErrors bool
errorContains []string
}{
{
name: "no _dev directory",
dirs: []string{"data_stream/foo/fields"},
expectErrors: false,
},
{
name: "_dev at package root",
dirs: []string{"_dev/build"},
expectErrors: true,
errorContains: []string{"_dev", "_dev directory is not allowed in built packages"},
},
{
name: "_dev inside data_stream",
dirs: []string{"data_stream/foo/_dev/test"},
expectErrors: true,
errorContains: []string{"_dev", "_dev directory is not allowed in built packages"},
},
{
name: "multiple _dev directories",
dirs: []string{
"_dev/build",
"data_stream/foo/_dev/test",
},
expectErrors: true,
errorContains: []string{"_dev directory is not allowed in built packages"},
},
{
name: "directory named _devtools is not rejected",
dirs: []string{"_devtools"},
expectErrors: false,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
tempDir := t.TempDir()

for _, d := range tc.dirs {
require.NoError(t, os.MkdirAll(filepath.Join(tempDir, d), 0o755))
}

fsys := fspath.DirFS(tempDir)
errs := ValidateNoDevFolder(fsys)

if !tc.expectErrors {
assert.Nil(t, errs, "expected no errors but got: %v", errs)
return
}

require.NotNil(t, errs, "expected validation errors but got none")
var sb strings.Builder
for _, e := range errs {
sb.WriteString(e.Error())
sb.WriteString("\n")
}
combined := sb.String()
for _, substr := range tc.errorContains {
assert.Contains(t, combined, substr)
}
})
}
}
Loading