Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions code/go/internal/specschema/folder_item_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ func (s *ItemSpec) DevelopmentFolder() bool {
return s.itemSpec.DevelopmentFolder
}

// ValidationMode returns the mode in which this item is valid: "source",
// "build", or "" (both modes).
func (s *ItemSpec) ValidationMode() string {
return s.itemSpec.ValidationMode
}

// AllowLink returns true if the item allows links.
func (s *ItemSpec) AllowLink() bool {
return s.itemSpec.AllowLink
Expand Down Expand Up @@ -142,6 +148,10 @@ type folderItemSpec struct {
DevelopmentFolder bool `json:"developmentFolder" yaml:"developmentFolder"`
AllowLink bool `json:"allowLink" yaml:"allowLink"`

// ValidationMode restricts the item to a specific validation mode: "source",
// "build", or "" (both modes).
ValidationMode string `json:"validationMode" yaml:"validationMode"`

// As it is required to be inline both in yaml and json, this struct must be public embedded field
SpecLimits `yaml:",inline"`

Expand Down
4 changes: 4 additions & 0 deletions code/go/internal/specschema/folder_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ func (l *FolderSpecLoader) loadContents(s *folderItemSpec, fs fs.FS, specPath st
return fmt.Errorf("item [%s] visibility is expected to be private or public, not [%s]", path.Join(specPath, content.Name), content.Visibility)
}

if vm := content.ValidationMode; vm != "" && vm != spectypes.ValidationModeSource && vm != spectypes.ValidationModeBuild {
return fmt.Errorf("item [%s] validationMode is expected to be %q or %q, not [%s]", path.Join(specPath, content.Name), spectypes.ValidationModeSource, spectypes.ValidationModeBuild, vm)
}

// All folders inside a development folder are too.
if s.DevelopmentFolder {
content.DevelopmentFolder = true
Expand Down
12 changes: 12 additions & 0 deletions code/go/internal/spectypes/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ const (

// ItemTypeFolder is the type of an item that represents a folder.
ItemTypeFolder = "folder"

// ValidationModeSource marks an item as valid only in source packages (not in built packages).
// The string value must match validator.SourceMode.
ValidationModeSource = "source"

// ValidationModeBuild marks an item as valid only in built packages (not in source packages).
// The string value must match validator.BuildMode.
ValidationModeBuild = "build"
)

// LimitsSpec contain the specifications related to limits.
Expand Down Expand Up @@ -57,6 +65,10 @@ type ItemSpec interface {
// DevelopmentFolder returns true if the item is inside a development folder.
DevelopmentFolder() bool

// ValidationMode returns the mode in which this item is valid: "source",
// "build", or "" (both modes).
ValidationMode() string

// AllowLink returns true if the item allows links.
AllowLink() bool

Expand Down
9 changes: 9 additions & 0 deletions code/go/internal/validator/folder_item_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ import (
"github.com/elastic/package-spec/v3/code/go/pkg/specerrors"
)

// itemForbiddenInMode returns true if the item is forbidden in the given mode.
// LegacyMode is never restricted.
func itemForbiddenInMode(spec spectypes.ItemSpec, mode Mode) bool {
if mode == LegacyMode {
return false
}
return spec.ValidationMode() != "" && spec.ValidationMode() != string(mode)
}

func matchingFileExists(spec spectypes.ItemSpec, files []fs.DirEntry) (bool, error) {
if spec.Name() != "" {
for _, file := range files {
Expand Down
46 changes: 37 additions & 9 deletions code/go/internal/validator/folder_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,23 @@ type validator struct {
pkg *packages.Package
folderPath string
warningsAsErrors bool
mode Mode

totalSize spectypes.FileSize
totalContents int
}

func newValidator(spec spectypes.ItemSpec, pkg *packages.Package, warningsAsErrors bool) *validator {
return newValidatorForPath(spec, pkg, ".", warningsAsErrors)
func newValidator(spec spectypes.ItemSpec, pkg *packages.Package, warningsAsErrors bool, mode Mode) *validator {
return newValidatorForPath(spec, pkg, ".", warningsAsErrors, mode)
}

func newValidatorForPath(spec spectypes.ItemSpec, pkg *packages.Package, folderPath string, warningsAsErrors bool) *validator {
func newValidatorForPath(spec spectypes.ItemSpec, pkg *packages.Package, folderPath string, warningsAsErrors bool, mode Mode) *validator {
return &validator{
spec: spec,
pkg: pkg,
folderPath: folderPath,
warningsAsErrors: warningsAsErrors,
mode: mode,
}
}

Expand Down Expand Up @@ -83,6 +85,15 @@ func (v *validator) Validate() specerrors.ValidationErrors {

for _, file := range files {
fileName := file.Name()

if isLink, _ := checkLink(fileName); v.mode == BuildMode && isLink {
errs = append(errs, specerrors.NewStructuredErrorf(
"file %q: .link files are not allowed in built packages",
v.pkg.Path(path.Join(v.folderPath, fileName)),
))
continue
}

itemSpec, err := v.findItemSpec(fileName)
if err != nil {
errs = append(errs, specerrors.NewStructuredError(err, specerrors.UnassignedCode))
Expand Down Expand Up @@ -119,8 +130,18 @@ func (v *validator) Validate() specerrors.ValidationErrors {
continue
}

if itemForbiddenInMode(itemSpec, v.mode) {
errs = append(errs, specerrors.NewStructuredErrorf(
"file %q: %s-only folder is not allowed in %s packages",
v.pkg.Path(path.Join(v.folderPath, fileName)),
itemSpec.ValidationMode(),
string(v.mode),
))
continue
}

subFolderPath := path.Join(v.folderPath, fileName)
itemValidator := newValidatorForPath(itemSpec, v.pkg, subFolderPath, v.warningsAsErrors)
itemValidator := newValidatorForPath(itemSpec, v.pkg, subFolderPath, v.warningsAsErrors, v.mode)
subErrs := itemValidator.Validate()
if len(subErrs) > 0 {
errs = append(errs, subErrs...)
Expand All @@ -139,6 +160,16 @@ func (v *validator) Validate() specerrors.ValidationErrors {
continue
}

if itemForbiddenInMode(itemSpec, v.mode) {
errs = append(errs, specerrors.NewStructuredErrorf(
"file %q: %s-only file is not allowed in %s packages",
v.pkg.Path(path.Join(v.folderPath, fileName)),
itemSpec.ValidationMode(),
string(v.mode),
))
continue
}

itemPath := path.Join(v.folderPath, file.Name())
itemValidationErrs := validateFile(itemSpec, v.pkg, itemPath)
for _, ive := range itemValidationErrs {
Expand Down Expand Up @@ -231,9 +262,6 @@ func (v *validator) findItemSpec(folderItemName string) (spectypes.ItemSpec, err
// checkLink checks if an item is a link and returns the item name without the
// ".link" suffix if it is a link.
func checkLink(itemName string) (bool, string) {
const linkExtension = ".link"
if strings.HasSuffix(itemName, linkExtension) {
return true, strings.TrimSuffix(itemName, linkExtension)
}
return false, itemName
stripped, isLink := strings.CutSuffix(itemName, ".link")
return isLink, stripped
}
2 changes: 2 additions & 0 deletions code/go/internal/validator/modes.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ const (
LegacyMode Mode = "legacy"
// SourceMode validates a package as a checked-out source tree: linked files
// are resolved transparently and source-only rules are enforced.
// The string value must match spectypes.ValidationModeSource.
SourceMode Mode = "source"
// BuildMode validates a package as a built artifact: linked files are
// unconditionally blocked and build-only rules are enforced.
// The string value must match spectypes.ValidationModeBuild.
BuildMode Mode = "build"
)

Expand Down
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,9 @@ 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 {
// Only validate template files that are explicitly declared; if none are set there
// is nothing to check (composable inputs without overlay templates source them from
// the dependency package, which is absent from the source tree).

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.

Thanks for making these comments more concise 👍

if len(input.TemplatePaths) > 0 {
for _, tp := range input.TemplatePaths {
if err := validateAgentInputTemplatePath(fsys, tp); err != nil {
Expand Down Expand Up @@ -141,6 +145,11 @@ func validateAllDataStreamStreamTemplates(fsys fspath.FS, dsMap map[string]dataS
dsManifestPath := path.Join(dsDir, "manifest.yml")
manifest := dsMap[dsDir]
for _, s := range manifest.Streams {
// 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
}
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
Loading