Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e1a0cb2
Update .gitignore to include .scratch directory
teresaromero May 27, 2026
08356fa
Add internal Mode scaffolding for validation modes
teresaromero May 27, 2026
135f03a
Add public constructor API with mode-aware validators (task 02)
teresaromero May 27, 2026
9d454d5
Add source mode: tag source-only rules and reject _embedded_ecs (task…
teresaromero May 27, 2026
50cc670
Add build mode: _dev/ rejection + good_built fixture (task 04)
teresaromero May 27, 2026
d8a368f
Add build mode: .link file rejection (task 05)
teresaromero May 27, 2026
138fa63
Add build mode: external: ecs field rejection (task 06)
teresaromero May 27, 2026
d827f09
Add build mode: stream input materialization rejection (task 07)
teresaromero May 27, 2026
2eda0ed
Add changelog entry for validation modes (task 08)
teresaromero May 27, 2026
6ac4740
Fix listDataStreams to skip non-directory entries under data_stream/
teresaromero May 27, 2026
3c23271
Fix bad_built fixture names and mid-name .link test case (review B1, B2)
teresaromero May 27, 2026
9827ff2
Fix medium-priority review items (S2, T1, T2)
teresaromero May 27, 2026
d6573c2
Fix correctness and cleanup issues found in code review
teresaromero May 28, 2026
2a2f388
Update changelog link to PR #1175
teresaromero May 28, 2026
14cc75c
Fix lint: add godoc comments to exported modes package symbols
teresaromero May 28, 2026
9c3e702
Add eager path/fs validation to NewFromPath and NewFromFS constructors
teresaromero May 28, 2026
460165a
Skip template validation for composable streams with no local templates
teresaromero May 28, 2026
7242f16
Skip template validation for composable policy template inputs with n…
teresaromero May 28, 2026
411b8f8
Allow partial var definitions on composable streams and policy templa…
teresaromero May 28, 2026
498dbf2
Restrict SVR00010 (input qualifier) to build mode only
teresaromero May 28, 2026
440ecd0
Enhance validation error handling and mode checks
teresaromero May 29, 2026
703e088
Address PR review comments
teresaromero May 29, 2026
a960b91
Use package-relative path in manifest read error
teresaromero May 29, 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ fuzz
/build/
.vscode/
.DS_Store

.scratch/
7 changes: 7 additions & 0 deletions code/go/internal/validator/folder_item_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
package validator

import (
"errors"
"fmt"
"io/fs"
"regexp"

"github.com/elastic/package-spec/v3/code/go/internal/linkedfiles"
"github.com/elastic/package-spec/v3/code/go/internal/spectypes"
"github.com/elastic/package-spec/v3/code/go/pkg/specerrors"
)
Expand Down Expand Up @@ -40,6 +42,11 @@ func matchingFileExists(spec spectypes.ItemSpec, files []fs.DirEntry) (bool, err
func validateFile(spec spectypes.ItemSpec, fsys fs.FS, itemPath string) specerrors.ValidationErrors {
err := validateMaxSize(fsys, itemPath, spec)
if err != nil {
if errors.Is(err, linkedfiles.ErrUnsupportedLinkFile) {
// .link files in build mode: skip syntactic checks here; the semantic
// ValidateNoLinkFiles rule emits a clean validation error for them.
return nil
}
return specerrors.ValidationErrors{specerrors.NewStructuredError(err, specerrors.UnassignedCode)}
}
if mediaType := spec.ContentMediaType(); mediaType != nil {
Expand Down
24 changes: 24 additions & 0 deletions code/go/internal/validator/modes/modes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// 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 mode used when validating a package.
type Mode string

// Validation modes. The public API re-exports these as validator.Mode* constants.
const (
Legacy Mode = "legacy"

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.

We originally discussed that legacy was functionally equivalent to source mode, but then there was some additional discussion about maybe keeping it for some time until we deprecate it and it collapses into source by default. Just wanted to see what the plan was here and if we really even need legacy at all if theyre functionally eq.

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've kept it for the shake of migrating. I felt it was safer to have all options available when using it on elastic-package. We could have a followup cleanup issue to remove it.

Source Mode = "source"
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
}
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