-
Notifications
You must be signed in to change notification settings - Fork 87
Add validation for template_path at integration packages
#1002
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
teresaromero
merged 11 commits into
elastic:main
from
teresaromero:703-integration-packages-template-path-validation
Oct 29, 2025
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
005ec1c
Add validation for integration policy template paths and correspondin…
teresaromero 5e49719
Add integration policy template validation and update template path i…
teresaromero e01db84
fix error messages and added validator test cases
teresaromero cea8452
Add integration packages validation for template paths in changelog
teresaromero 516c095
Enhance template path validation to accommodate legacy cases and upda…
teresaromero 9ee4b1a
Refactor policyTemplateInput struct by removing unnecessary Streams f…
teresaromero 3e3e674
Revert "Enhance template path validation to accommodate legacy cases …
teresaromero 45f317d
Refactor template validation logic to simplify error handling and rem…
teresaromero 370d8b9
Remove unused error variable for clarity in integration policy templa…
teresaromero c5799ce
Merge branch 'main' into 703-integration-packages-template-path-valid…
teresaromero 66f2f56
Add fallback validation to lookup prefixed template pathes
teresaromero File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
170 changes: 170 additions & 0 deletions
170
code/go/internal/validator/semantic/validate_integration_policy_template_path.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| // 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" | ||
| "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" | ||
| ) | ||
|
|
||
| const ( | ||
| defaultStreamTemplatePath = "stream.yml.hbs" | ||
| packageTypeIntegration = "integration" | ||
| ) | ||
|
|
||
| type policyTemplateInput struct { | ||
| Type string `yaml:"type"` | ||
| TemplatePath string `yaml:"template_path"` // optional for integration packages | ||
| } | ||
|
|
||
| type integrationPolicyTemplate struct { | ||
| Name string `yaml:"name"` | ||
| Inputs []policyTemplateInput `yaml:"inputs"` | ||
| } | ||
|
|
||
| type integrationPackageManifest struct { // package manifest | ||
| Type string `yaml:"type"` // integration or input | ||
| PolicyTemplates []integrationPolicyTemplate `yaml:"policy_templates"` | ||
| } | ||
|
|
||
| type stream struct { | ||
| Input string `yaml:"input"` | ||
| TemplatePath string `yaml:"template_path"` | ||
| } | ||
|
|
||
| type dataStreamManifest struct { | ||
| Streams []stream `yaml:"streams"` | ||
| } | ||
|
|
||
| // ValidateIntegrationPolicyTemplates validates the template_path fields at the policy template level for integration type packages | ||
| func ValidateIntegrationPolicyTemplates(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 integrationPackageManifest | ||
| err = yaml.Unmarshal(data, &manifest) | ||
| if err != nil { | ||
| return specerrors.ValidationErrors{ | ||
| specerrors.NewStructuredErrorf("file \"%s\" is invalid: %w", fsys.Path(manifestPath), errFailedToParseManifest)} | ||
| } | ||
|
|
||
| // only validate integration type packages | ||
| if manifest.Type != packageTypeIntegration { | ||
| return nil | ||
| } | ||
|
|
||
| // read at once all data stream manifests | ||
| dataStreamsManifestMap, err := readDataStreamsManifests(fsys) | ||
| if err != nil { | ||
| return specerrors.ValidationErrors{ | ||
| specerrors.NewStructuredErrorf("file \"%s\" is invalid: %w", fsys.Path(manifestPath), err)} | ||
| } | ||
|
|
||
| for _, policyTemplate := range manifest.PolicyTemplates { | ||
| err = validateIntegrationPackagePolicyTemplate(fsys, policyTemplate, dataStreamsManifestMap) | ||
| if err != nil { | ||
| errs = append(errs, specerrors.NewStructuredErrorf( | ||
| "file \"%s\" is invalid: policy template \"%s\" references input template_path: %w", | ||
| fsys.Path(manifestPath), policyTemplate.Name, err)) | ||
| } | ||
| } | ||
|
|
||
| return errs | ||
| } | ||
|
|
||
| // validateIntegrationPackagePolicyTemplate validates the template_path fields at the policy template level for integration type packages | ||
| func validateIntegrationPackagePolicyTemplate(fsys fspath.FS, policyTemplate integrationPolicyTemplate, dsManifestMap map[string]dataStreamManifest) error { | ||
| for _, input := range policyTemplate.Inputs { | ||
| if input.TemplatePath != "" { | ||
| // validate the provided template_path file exists | ||
| err := validateAgentInputTemplatePath(fsys, input.TemplatePath) | ||
| if err != nil { | ||
| return fmt.Errorf("error validating input \"%s\": %w", input.Type, err) | ||
| } | ||
| continue | ||
| } | ||
|
|
||
| err := validateInputWithStreams(fsys, input.Type, dsManifestMap) | ||
| if err != nil { | ||
| return fmt.Errorf("error validating input from streams \"%s\": %w", input.Type, err) | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // readDataStreamsManifests reads all data stream manifests and returns a map of data stream directory to its manifest relevant content | ||
| func readDataStreamsManifests(fsys fspath.FS) (map[string]dataStreamManifest, error) { | ||
| // map of data stream directory to its manifest | ||
| dsManifestMap := make(map[string]dataStreamManifest, 0) | ||
|
|
||
| dsManifests, err := fs.Glob(fsys, "data_stream/*/manifest.yml") | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| for _, file := range dsManifests { | ||
| data, err := fs.ReadFile(fsys, file) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| var m dataStreamManifest | ||
| err = yaml.Unmarshal(data, &m) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| dsDir := path.Dir(file) | ||
| dsManifestMap[dsDir] = m | ||
| } | ||
|
|
||
| return dsManifestMap, nil | ||
| } | ||
|
|
||
| // validateInputWithStreams validates that for the given input type, the streams of each dataset related to it have valid template_path files | ||
| // an input is related to a data_stream if any of its streams has the same input type as input | ||
| func validateInputWithStreams(fsys fspath.FS, input string, dsMap map[string]dataStreamManifest) error { | ||
| for dsDir, manifest := range dsMap { | ||
| for _, stream := range manifest.Streams { | ||
| // only consider streams that match the input type of the policy template | ||
| if stream.Input != input { | ||
| continue | ||
| } | ||
| // if template_path is not set at the stream level, default to "stream.yml.hbs" | ||
| if stream.TemplatePath == "" { | ||
| stream.TemplatePath = defaultStreamTemplatePath | ||
| } | ||
|
|
||
| _, err := fs.ReadFile(fsys, path.Join(dsDir, "agent", "stream", stream.TemplatePath)) | ||
| if err != nil { | ||
| if errors.Is(err, fs.ErrNotExist) { | ||
| // fallback to glob pattern matching in case the default template path is customized with a prefix | ||
| matches, err := fs.Glob(fsys, path.Join(dsDir, "agent", "stream", "*"+stream.TemplatePath)) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if len(matches) == 0 { | ||
| return errTemplateNotFound | ||
| } | ||
| continue | ||
| } | ||
| return err | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
181 changes: 181 additions & 0 deletions
181
code/go/internal/validator/semantic/validate_integration_policy_template_path_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| // 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" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/elastic/package-spec/v3/code/go/internal/fspath" | ||
| ) | ||
|
|
||
| func TestReadDataStreamsManifests(t *testing.T) { | ||
|
|
||
| d := t.TempDir() | ||
|
|
||
| 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: | ||
| - input: nginx/access | ||
| template_path: stream.yml.hbs | ||
| - input: nginx/error | ||
| template_path: error_stream.yml.hbs | ||
| `), 0o644) | ||
| require.NoError(t, err) | ||
|
|
||
| err = os.MkdirAll(filepath.Join(d, "data_stream", "logs", "nested"), 0o755) | ||
| require.NoError(t, err) | ||
| err = os.WriteFile(filepath.Join(d, "data_stream", "logs", "nested", "manifest.yml"), []byte(` | ||
| streams: | ||
| - input: nginx/access | ||
| template_path: stream.yml.hbs | ||
| - input: nginx/error | ||
| template_path: error_stream.yml.hbs | ||
| `), 0o644) | ||
| require.NoError(t, err) | ||
|
|
||
| dataStreamsManifestMap, err := readDataStreamsManifests(fspath.DirFS(d)) | ||
| require.NoError(t, err) | ||
| // only the top-level manifest.yml should be read | ||
| require.Len(t, dataStreamsManifestMap, 1) | ||
|
|
||
| mapKey := filepath.ToSlash(path.Join("data_stream", "logs")) | ||
| require.NotEmpty(t, dataStreamsManifestMap[mapKey]) | ||
| logsManifest := dataStreamsManifestMap[mapKey] | ||
| require.Len(t, logsManifest.Streams, 2) | ||
| require.Equal(t, "nginx/access", logsManifest.Streams[0].Input) | ||
| require.Equal(t, "stream.yml.hbs", logsManifest.Streams[0].TemplatePath) | ||
| require.Equal(t, "nginx/error", logsManifest.Streams[1].Input) | ||
| require.Equal(t, "error_stream.yml.hbs", logsManifest.Streams[1].TemplatePath) | ||
| } | ||
|
|
||
| func TestValidateInputWithStreams(t *testing.T) { | ||
| d := t.TempDir() | ||
| 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", "access.yml.hbs"), []byte(`access stream template`), 0o644) | ||
| require.NoError(t, err) | ||
|
|
||
| dsMap := make(map[string]dataStreamManifest) | ||
| dsMap[filepath.ToSlash(path.Join("data_stream", "logs"))] = dataStreamManifest{ | ||
| Streams: []stream{ | ||
| { | ||
| Input: "nginx/access", | ||
| TemplatePath: "access.yml.hbs", | ||
| }, | ||
| { | ||
| Input: "nginx/error", | ||
| TemplatePath: "error_stream.yml.hbs", | ||
| }, | ||
| { | ||
| Input: "nginx/other", | ||
| }, | ||
| { | ||
| Input: "prefix/stream", | ||
| }, | ||
| }, | ||
| } | ||
| t.Run("valid input with existing template_path", func(t *testing.T) { | ||
| err = validateInputWithStreams(fspath.DirFS(d), "nginx/access", dsMap) | ||
| require.NoError(t, err) | ||
| }) | ||
|
|
||
| t.Run("input with non-existing template_path", func(t *testing.T) { | ||
| err = validateInputWithStreams(fspath.DirFS(d), "nginx/error", dsMap) | ||
| require.ErrorIs(t, errTemplateNotFound, err) | ||
| }) | ||
|
|
||
| t.Run("valid input with default template_path", func(t *testing.T) { | ||
| err = os.WriteFile(filepath.Join(d, "data_stream", "logs", "agent", "stream", "stream.yml.hbs"), []byte(`default stream template`), 0o644) | ||
| require.NoError(t, err) | ||
| defer os.Remove(filepath.Join(d, "data_stream", "logs", "agent", "stream", "stream.yml.hbs")) | ||
|
|
||
| err = validateInputWithStreams(fspath.DirFS(d), "nginx/other", dsMap) | ||
| require.NoError(t, err) | ||
| }) | ||
|
|
||
| t.Run("valid input with default prefixed template_path", func(t *testing.T) { | ||
| err = os.WriteFile(filepath.Join(d, "data_stream", "logs", "agent", "stream", "prefixstream.yml.hbs"), []byte(`access stream template`), 0o644) | ||
| require.NoError(t, err) | ||
| defer os.Remove(filepath.Join(d, "data_stream", "logs", "agent", "stream", "prefixstream.yml.hbs")) | ||
|
|
||
| err = validateInputWithStreams(fspath.DirFS(d), "prefix/stream", dsMap) | ||
| require.NoError(t, err) | ||
| }) | ||
|
|
||
| } | ||
| func TestValidateIntegrationPolicyTemplates_NonIntegrationType(t *testing.T) { | ||
| d := t.TempDir() | ||
| // write a manifest with a non-integration type | ||
| err := os.WriteFile(filepath.Join(d, "manifest.yml"), []byte(`type: input`), 0o644) | ||
| require.NoError(t, err) | ||
|
|
||
| errs := ValidateIntegrationPolicyTemplates(fspath.DirFS(d)) | ||
| require.Nil(t, errs) | ||
| } | ||
|
|
||
| func TestValidateIntegrationPolicyTemplates_IntegrationValidTemplates(t *testing.T) { | ||
| d := t.TempDir() | ||
|
|
||
| // manifest: integration with a policy template referencing nginx/access (no template_path at policy level) | ||
| err := os.WriteFile(filepath.Join(d, "manifest.yml"), []byte(` | ||
| type: integration | ||
| policy_templates: | ||
| - name: pt1 | ||
| inputs: | ||
| - type: nginx/access | ||
| `), 0o644) | ||
| require.NoError(t, err) | ||
|
|
||
| // data stream manifest providing the stream for nginx/access with a specific template | ||
| 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: | ||
| - input: nginx/access | ||
| template_path: access.yml.hbs | ||
| `), 0o644) | ||
| require.NoError(t, err) | ||
| // write the actual template file referenced by the stream | ||
| err = os.WriteFile(filepath.Join(d, "data_stream", "logs", "agent", "stream", "access.yml.hbs"), []byte("template"), 0o644) | ||
| require.NoError(t, err) | ||
|
|
||
| errs := ValidateIntegrationPolicyTemplates(fspath.DirFS(d)) | ||
| require.Empty(t, errs) | ||
| } | ||
|
|
||
| func TestValidateIntegrationPolicyTemplates_DefaultTemplate(t *testing.T) { | ||
| d := t.TempDir() | ||
|
|
||
| // manifest: integration with a policy template referencing an input that does not exist in any data stream | ||
| err := os.WriteFile(filepath.Join(d, "manifest.yml"), []byte(` | ||
| type: integration | ||
| policy_templates: | ||
| - name: pt2 | ||
| inputs: | ||
| - type: nginx/access | ||
| `), 0o644) | ||
| require.NoError(t, err) | ||
|
|
||
| // create a data stream that does NOT include the referenced input | ||
| 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: | ||
| - input: nginx/access | ||
| `), 0o644) | ||
| require.NoError(t, err) | ||
| // write the default template file for the existing stream | ||
| err = os.WriteFile(filepath.Join(d, "data_stream", "logs", "agent", "stream", "stream.yml.hbs"), []byte("template"), 0o644) | ||
| require.NoError(t, err) | ||
|
|
||
| errs := ValidateIntegrationPolicyTemplates(fspath.DirFS(d)) | ||
| require.Empty(t, errs) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.