Skip to content
Closed
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
78b6c7d
Add validation for data stream template paths
teresaromero Sep 26, 2025
6b5db09
Add new testdata packages for stream template validation
teresaromero Sep 26, 2025
78ebe46
Add validation for stream templates and corresponding tests
teresaromero Sep 26, 2025
f09e0d6
Add validation for input and integration policy template paths with c…
teresaromero Sep 26, 2025
84cf7b1
Fix policy template validation to support nested inputs and improve e…
teresaromero Sep 26, 2025
45f462b
Add testdata for input and integration policy templates
teresaromero Sep 26, 2025
cd6c408
Add tests for input and integration policy template validation, inclu…
teresaromero Sep 26, 2025
bc144d4
Format imports with goimports
teresaromero Sep 26, 2025
edbba40
Move validation test from spec to validator. Move testdata to test/pa…
teresaromero Sep 26, 2025
5045d28
fix test maifest and spec example to align to validation path
teresaromero Sep 29, 2025
423793f
Refactor validateDataStreamManifestTemplates to simplify parameters b…
teresaromero Sep 29, 2025
fb191e2
Improve error messaging in template path validation by using %w for w…
teresaromero Sep 29, 2025
526c665
Update error messages in template path validation tests for clarity a…
teresaromero Sep 29, 2025
a78f6c2
Add validation for template_path in policy templates and data streams
teresaromero Sep 29, 2025
4fa3db6
Add validation for `template_path` in policy templates and data strea…
teresaromero Sep 30, 2025
364d708
Refactor template path handling in validation functions to use filepa…
teresaromero Sep 30, 2025
6e35950
Update tests to use filepath
teresaromero Sep 30, 2025
0d3e22c
filepath join at tests
teresaromero Sep 30, 2025
9dca430
Refactor tests to use os and filepath for directory and file handling…
teresaromero Sep 30, 2025
704aef2
Reorder import statements
teresaromero Sep 30, 2025
801b990
Refactor error handling in template validation functions to use prede…
teresaromero Oct 1, 2025
741ffb0
Merge branch 'main' into 703-stream-yml-hbs-exists
teresaromero Oct 1, 2025
8f47f3c
remove changelog template_path quotes
teresaromero Oct 1, 2025
9d3e1dd
Comment out validation rules and test cases for stream and input poli…
teresaromero Oct 1, 2025
316dbd3
Refactor path handling in validation functions to use path.Join inste…
teresaromero Oct 1, 2025
13f5ae8
Uncomment validation rules and test cases for stream and input policy…
teresaromero Oct 1, 2025
cfa7087
Fix changelog entry order
teresaromero Oct 1, 2025
19bf9da
Add validation for default template paths in data stream tests
teresaromero Oct 3, 2025
731a589
Rename validation function for agent input template paths and update …
teresaromero Oct 3, 2025
807f407
Add stream template files for data stream agent validation tests
teresaromero Oct 3, 2025
b2be7dd
Refactor error handling in validateAgentInputTemplatePath to differen…
teresaromero Oct 3, 2025
0882440
Change specs with required fields and json patch
teresaromero Oct 3, 2025
a6379bf
Update validation rules and manifest files for version 3.6.0 complian…
teresaromero Oct 3, 2025
bfe466b
revert changes on old test packages
teresaromero Oct 3, 2025
53ccaf3
recover deleted file
teresaromero Oct 3, 2025
f1369eb
Revert "revert changes on old test packages"
teresaromero Oct 3, 2025
d0dd5d8
revert version limit
teresaromero Oct 6, 2025
6ee279c
fix test cases
teresaromero Oct 6, 2025
4da74b3
fix testdata with template_paths
teresaromero Oct 7, 2025
5d1ac9c
Revert "fix testdata with template_paths"
teresaromero Oct 7, 2025
616761c
Revert "fix test cases"
teresaromero Oct 7, 2025
7b474a7
revert required field at integrations, fix required validation for in…
teresaromero Oct 7, 2025
818eff5
change streams template_path validation, use walkdir with endsWith logic
teresaromero Oct 8, 2025
86fec86
Merge branch 'main' into 703-stream-yml-hbs-exists
teresaromero Oct 14, 2025
84d2161
changelog move template_path validation entry to 3.5.1-next
teresaromero Oct 15, 2025
a84f9c7
Rename defaultTemplatePath to defaultStreamTemplatePath for clarity
teresaromero Oct 15, 2025
fd34e79
Change fs.SkipDir to fs.SkipAll to stop directory traversal once a ma…
teresaromero Oct 15, 2025
11efa5d
Fix error handling in validateDataStreamManifestTemplates for directo…
teresaromero Oct 15, 2025
8ec5de0
Update link in comment to point to the correct commit in Kibana repos…
teresaromero Oct 15, 2025
a3b56e5
Refactor policy template validation: consolidate ValidateStreamTempla…
teresaromero Oct 16, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// 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 (
"errors"
"io/fs"
"path"
"strings"

"gopkg.in/yaml.v3"

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

const (
defaultTemplatePath = "stream.yml.hbs"
)

var (
errFailedToReadManifest = errors.New("failed to read manifest")
errFailedToParseManifest = errors.New("failed to parse manifest")
errTemplateNotFound = errors.New("template file not found")
)

// ValidateStreamTemplates validates that all referenced template_path files exist for data streams
func ValidateStreamTemplates(fsys fspath.FS) specerrors.ValidationErrors {
var errs specerrors.ValidationErrors

dataStreams, err := listDataStreams(fsys)
if err != nil {
return specerrors.ValidationErrors{specerrors.NewStructuredError(err, specerrors.UnassignedCode)}
}

for _, dataStream := range dataStreams {
streamErrs := validateDataStreamManifestTemplates(fsys, dataStream)
errs = append(errs, streamErrs...)
}

return errs
}

func validateDataStreamManifestTemplates(fsys fspath.FS, dataStreamName string) specerrors.ValidationErrors {
var errs specerrors.ValidationErrors

manifestPath := path.Join("data_stream", dataStreamName, "manifest.yml")
data, err := fs.ReadFile(fsys, manifestPath)
if err != nil {
return specerrors.ValidationErrors{specerrors.NewStructuredErrorf("file \"%s\" is invalid: %w", fsys.Path(manifestPath), errFailedToReadManifest)}
}

var manifest struct {
Streams []struct {
Input string `yaml:"input"`
TemplatePath string `yaml:"template_path"`
} `yaml:"streams"`
}

err = yaml.Unmarshal(data, &manifest)
if err != nil {
return specerrors.ValidationErrors{specerrors.NewStructuredErrorf("file \"%s\" is invalid: %w", fsys.Path(manifestPath), errFailedToParseManifest)}
}

for _, stream := range manifest.Streams {
streamPath := stream.TemplatePath
if stream.TemplatePath == "" {
// When no template_path is specified, it defaults to "stream.yml.hbs"
streamPath = defaultTemplatePath
}

// Walk through the "data_stream/<dataStreamName>/agent/stream" directory
// This mirrors the logic in fleet where the assets are filtered based on the template_path
// https://github.com/elastic/kibana/blob/main/x-pack/platform/plugins/shared/fleet/server/services/package_policy.ts#L3317
streamDir := path.Join("data_stream", dataStreamName, "agent", "stream")
found := false
fs.WalkDir(fsys, streamDir, func(filePath string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if !d.IsDir() && path.Base(filePath) != "" && strings.HasSuffix(filePath, streamPath) {
found = true
return fs.SkipDir // Stop walking once found
}
return nil
})
if !found {
if stream.TemplatePath == "" {
errs = append(errs, specerrors.NewStructuredErrorf(
"file \"%s\" is invalid: stream \"%s\" is missing template file \"%s\": %w",
fsys.Path(manifestPath), stream.Input, streamPath, errTemplateNotFound))
continue
}
errs = append(errs, specerrors.NewStructuredErrorf(
"file \"%s\" is invalid: stream \"%s\" references template_path \"%s\": %w",
fsys.Path(manifestPath), stream.Input, streamPath, errTemplateNotFound))
continue
}
}

return errs
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// 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"
"testing"

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

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

func TestValidateStreamTemplates(t *testing.T) {

t.Run("valid_data_stream_with_templates", func(t *testing.T) {
d := t.TempDir()
err := os.MkdirAll(filepath.Join(d, "data_stream", "test", "agent", "stream"), 0o755)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(d, "data_stream", "test", "manifest.yml"), []byte(`
streams:
- input: udp
template_path: udp.yml.hbs
title: Test UDP
description: Test UDP stream
`), 0o644)
require.NoError(t, err)

err = os.WriteFile(filepath.Join(d, "data_stream", "test", "agent", "stream", "udp.yml.hbs"), []byte("# UDP template"), 0o644)
require.NoError(t, err)

errs := ValidateStreamTemplates(fspath.DirFS(d))
require.Empty(t, errs, "expected no validation errors")
})

t.Run("valid_data_stream_with_default_templates", func(t *testing.T) {
d := t.TempDir()
err := os.MkdirAll(filepath.Join(d, "data_stream", "test", "agent", "stream"), 0o755)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(d, "data_stream", "test", "manifest.yml"), []byte(`
streams:
- input: udp
title: Test UDP
description: Test UDP stream
`), 0o644)
require.NoError(t, err)

err = os.WriteFile(filepath.Join(d, "data_stream", "test", "agent", "stream", "stream.yml.hbs"), []byte("# UDP template"), 0o644)
require.NoError(t, err)

errs := ValidateStreamTemplates(fspath.DirFS(d))
require.Empty(t, errs, "expected no validation errors")

})

t.Run("valid_data_stream_with_default_templates_endsWith_stream", func(t *testing.T) {
d := t.TempDir()
err := os.MkdirAll(filepath.Join(d, "data_stream", "test", "agent", "stream"), 0o755)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(d, "data_stream", "test", "manifest.yml"), []byte(`
streams:
- input: udp
title: Test UDP
description: Test UDP stream
`), 0o644)
require.NoError(t, err)

err = os.WriteFile(filepath.Join(d, "data_stream", "test", "agent", "stream", "filestream.yml.hbs"), []byte("# UDP template"), 0o644)
require.NoError(t, err)

errs := ValidateStreamTemplates(fspath.DirFS(d))
require.Empty(t, errs, "expected no validation errors")

})

t.Run("err_read_manifest", func(t *testing.T) {
d := t.TempDir()
err := os.MkdirAll(filepath.Join(d, "data_stream", "test", "agent", "stream"), 0o755)
require.NoError(t, err)

errs := ValidateStreamTemplates(fspath.DirFS(d))
require.NotEmpty(t, errs, "expected validation errors")
assert.Len(t, errs, 1)
assert.ErrorIs(t, errs[0], errFailedToReadManifest)
})

t.Run("err_parse_manifest", func(t *testing.T) {
d := t.TempDir()
err := os.MkdirAll(filepath.Join(d, "data_stream", "test", "agent", "stream"), 0o755)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(d, "data_stream", "test", "manifest.yml"), []byte(`
streams:
- input: udp
template_path: udp.yml.hbs
title: Test UDP
description: Test UDP stream
`), 0o644)
require.NoError(t, err)

err = os.WriteFile(filepath.Join(d, "data_stream", "test", "agent", "stream", "udp.yml.hbs"), []byte("# UDP template"), 0o644)
require.NoError(t, err)

errs := ValidateStreamTemplates(fspath.DirFS(d))
require.NotEmpty(t, errs, "expected validation errors")
assert.Len(t, errs, 1)
assert.ErrorIs(t, errs[0], errFailedToParseManifest)

})

t.Run("err_missing_template_file", func(t *testing.T) {
d := t.TempDir()
err := os.MkdirAll(filepath.Join(d, "data_stream", "test", "agent", "stream"), 0o755)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(d, "data_stream", "test", "manifest.yml"), []byte(`
streams:
- input: udp
template_path: missing.yml.hbs
title: Test UDP
description: Test UDP stream
`), 0o644)
require.NoError(t, err)

errs := ValidateStreamTemplates(fspath.DirFS(d))
require.NotEmpty(t, errs, "expected validation errors")
assert.Len(t, errs, 1)
assert.ErrorIs(t, errs[0], errTemplateNotFound)
assert.ErrorContains(t, errs[0], "stream \"udp\" references template_path \"missing.yml.hbs\": template file not found")
})

t.Run("err_missing_default_template_file", func(t *testing.T) {
d := t.TempDir()
err := os.MkdirAll(filepath.Join(d, "data_stream", "test", "agent", "stream"), 0o755)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(d, "data_stream", "test", "manifest.yml"), []byte(`
streams:
- input: udp
title: Test UDP
description: Test UDP stream
`), 0o644)
require.NoError(t, err)

errs := ValidateStreamTemplates(fspath.DirFS(d))
require.NotEmpty(t, errs, "expected validation errors")
assert.Len(t, errs, 1)
assert.ErrorIs(t, errs[0], errTemplateNotFound)
assert.ErrorContains(t, errs[0], "stream \"udp\" is missing template file \"stream.yml.hbs\": template file not found")
})

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// 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 (
"errors"
"fmt"
"io/fs"
"os"
"path"

"gopkg.in/yaml.v3"

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

// ValidateInputPolicyTemplates validates that all referenced template_path files exist for integration and input policy templates
func ValidateInputPolicyTemplates(fsys fspath.FS) specerrors.ValidationErrors {
var errs specerrors.ValidationErrors

manifestPath := "manifest.yml"

data, err := fs.ReadFile(fsys, manifestPath)
if err != nil {
return specerrors.ValidationErrors{specerrors.NewStructuredErrorf("file \"%s\" is invalid: %ww", fsys.Path(manifestPath), errFailedToReadManifest)}
}

var manifest struct { // package manifest
Type string `yaml:"type"` // integration or input

PolicyTemplates []struct {
Name string `yaml:"name"`
TemplatePath string `yaml:"template_path"` // input type packages require this field
Inputs []struct {
Title string `yaml:"title"`
TemplatePath string `yaml:"template_path"` // optional for integration packages
} `yaml:"inputs"`
} `yaml:"policy_templates"`
}

err = yaml.Unmarshal(data, &manifest)
if err != nil {
return specerrors.ValidationErrors{specerrors.NewStructuredErrorf("file \"%s\" is invalid: %w", fsys.Path(manifestPath), errFailedToParseManifest)}
}

for _, policyTemplate := range manifest.PolicyTemplates {
switch manifest.Type {
case "integration":
for _, input := range policyTemplate.Inputs {
if input.TemplatePath == "" {
continue // template_path is optional
}
Copy link
Contributor

Choose a reason for hiding this comment

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

So, in this case if there is not template_path defined, does fleet looks for any default input template ?
I was wondering if it should be added some other validation here or not.

Copy link
Author

Choose a reason for hiding this comment

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

from what i have investigated, when an integration has inputs within its policy_template, the template_paths that are not explicitly described there, are taken from the data_streams.

https://github.com/elastic/kibana/blob/main/x-pack/platform/plugins/shared/fleet/server/services/epm/packages/assets.ts#L12

there is some logic i don't fully understand but my guess is that from the assetMap, it takes the data stream name from the package manifest inputs and looks for the data stream associated to it. Perhaps the validation to be done at package spec should be that, this inputs need to have a data_stream associated to be able to "skip" describing the template?

err := validateAgentInputTemplatePath(fsys, input.TemplatePath)
if err != nil {
errs = append(errs, specerrors.NewStructuredErrorf(
"file \"%s\" is invalid: policy template \"%s\" references template_path \"%s\": %w",
fsys.Path(manifestPath), policyTemplate.Name, input.TemplatePath, err))
}
}

case "input":
if policyTemplate.TemplatePath == "" {
errs = append(errs, specerrors.NewStructuredErrorf(
"file \"%s\" is invalid: policy template \"%s\" is missing required field \"template_path\"",
fsys.Path(manifestPath), policyTemplate.Name))
continue
}
err := validateAgentInputTemplatePath(fsys, policyTemplate.TemplatePath)
if err != nil {
errs = append(errs, specerrors.NewStructuredErrorf(
"file \"%s\" is invalid: policy template \"%s\" references template_path \"%s\": %w",
fsys.Path(manifestPath), policyTemplate.Name, policyTemplate.TemplatePath, err))
}
}
}

return errs
}

func validateAgentInputTemplatePath(fsys fspath.FS, tmplPath string) error {
templatePath := path.Join("agent", "input", tmplPath)
_, err := fs.Stat(fsys, templatePath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return errTemplateNotFound
}
return fmt.Errorf("failed to stat template file %s: %w", fsys.Path(templatePath), err)
}

return nil
}
Loading