-
Notifications
You must be signed in to change notification settings - Fork 88
Handlebars template static validation and documentation #1030
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
Changes from all commits
d6d204e
ea6d4bd
67389f6
97a32b2
f5854ea
32ebc29
e76c19a
49c9c5a
86281bb
3e48130
32ab54d
bc7eee5
0f1ae93
d4898fe
d6ee0cd
ab1a541
0d298e7
e699759
31644e8
62679a5
17b18db
bd2358e
79bde15
667ed7c
70547b9
b896c68
75e87b6
76d1835
5b236f1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| // 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" | ||
| "os" | ||
| "path" | ||
|
|
||
| "github.com/aymerick/raymond" | ||
|
|
||
| "github.com/elastic/package-spec/v3/code/go/internal/fspath" | ||
| "github.com/elastic/package-spec/v3/code/go/internal/linkedfiles" | ||
| "github.com/elastic/package-spec/v3/code/go/pkg/specerrors" | ||
| ) | ||
|
|
||
| var ( | ||
| errInvalidHandlebarsTemplate = errors.New("invalid handlebars template") | ||
| ) | ||
|
|
||
| // ValidateStaticHandlebarsFiles validates all Handlebars (.hbs) files in the package filesystem. | ||
| // It returns a list of validation errors if any Handlebars files are invalid. | ||
| // hbs are located in both the package root and data stream directories under the agent folder. | ||
| func ValidateStaticHandlebarsFiles(fsys fspath.FS) specerrors.ValidationErrors { | ||
| var errs specerrors.ValidationErrors | ||
|
|
||
| // template files are placed at /agent/input directory or | ||
| // at the datastream /agent/stream directory | ||
| inputDir := path.Join("agent", "input") | ||
| if inputErrs := validateTemplateDir(fsys, inputDir); inputErrs != nil { | ||
| errs = append(errs, inputErrs...) | ||
| } | ||
|
|
||
| datastreamEntries, err := fs.ReadDir(fsys, "data_stream") | ||
| if err != nil && !errors.Is(err, fs.ErrNotExist) { | ||
| return specerrors.ValidationErrors{ | ||
| specerrors.NewStructuredErrorf("error reading data_stream directory: %w", err), | ||
| } | ||
| } | ||
| for _, dsEntry := range datastreamEntries { | ||
| if !dsEntry.IsDir() { | ||
| continue | ||
| } | ||
| streamDir := path.Join("data_stream", dsEntry.Name(), "agent", "stream") | ||
| dsErrs := validateTemplateDir(fsys, streamDir) | ||
| if dsErrs != nil { | ||
| errs = append(errs, dsErrs...) | ||
| } | ||
| } | ||
|
|
||
| return errs | ||
| } | ||
|
|
||
| // validateTemplateDir validates all Handlebars files in the given directory. | ||
| func validateTemplateDir(fsys fspath.FS, dir string) specerrors.ValidationErrors { | ||
| entries, err := fs.ReadDir(fsys, dir) | ||
| if err != nil && !errors.Is(err, fs.ErrNotExist) { | ||
| return specerrors.ValidationErrors{ | ||
| specerrors.NewStructuredErrorf("error trying to read :%s", dir), | ||
| } | ||
| } | ||
| var errs specerrors.ValidationErrors | ||
| for _, entry := range entries { | ||
| if path.Ext(entry.Name()) == ".hbs" { | ||
| err := validateStaticHandlebarsEntry(fsys, dir, entry.Name()) | ||
| if err != nil { | ||
| errs = append(errs, specerrors.NewStructuredErrorf("%w: error validating %s: %w", errInvalidHandlebarsTemplate, path.Join(dir, entry.Name()), err)) | ||
| } | ||
| continue | ||
| } | ||
| if path.Ext(entry.Name()) == ".link" { | ||
| linkFilePath := path.Join(dir, entry.Name()) | ||
| linkFile, err := linkedfiles.NewLinkedFile(fsys.Path(linkFilePath)) | ||
| if err != nil { | ||
| errs = append(errs, specerrors.NewStructuredErrorf("error reading linked file %s: %w", linkFilePath, err)) | ||
| continue | ||
| } | ||
| err = validateStaticHandlebarsEntry(fsys, dir, linkFile.IncludedFilePath) | ||
| if err != nil { | ||
| errs = append(errs, specerrors.NewStructuredErrorf("%w: error validating %s: %w", errInvalidHandlebarsTemplate, path.Join(dir, linkFile.IncludedFilePath), err)) | ||
| } | ||
| } | ||
| } | ||
| return errs | ||
| } | ||
|
|
||
| // validateStaticHandlebarsEntry validates a single Handlebars file located at filePath. | ||
| // it parses the file using the raymond library to check for syntax errors. | ||
| func validateStaticHandlebarsEntry(fsys fspath.FS, dir, entryName string) error { | ||
| if entryName == "" { | ||
| return nil | ||
| } | ||
|
|
||
| var content []byte | ||
| var err error | ||
|
|
||
| // First try to read from filesystem (works for regular files and files within zip) | ||
| filePath := path.Join(dir, entryName) | ||
| if content, err = fs.ReadFile(fsys, filePath); err != nil { | ||
| if !errors.Is(err, fs.ErrInvalid) { | ||
| return err | ||
| } | ||
| // If fs.ReadFile fails (likely due to linked file path outside filesystem boundary), | ||
| // fall back to absolute path approach like linkedfiles.FS does | ||
| absolutePath := fsys.Path(filePath) | ||
| if content, err = os.ReadFile(absolutePath); err != nil { | ||
| return err | ||
| } | ||
| } | ||
|
|
||
| // Parse from content string instead of file path | ||
| _, err = raymond.Parse(string(content)) | ||
| return err | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| // 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" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/elastic/package-spec/v3/code/go/internal/fspath" | ||
| ) | ||
|
|
||
| func TestValidateTemplateDir(t *testing.T) { | ||
| t.Run("empty directory", func(t *testing.T) { | ||
| tmpDir := t.TempDir() | ||
| pkgDir := path.Join(tmpDir, "package") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit. Most paths in these tests are native paths, they should be joined with |
||
| err := os.MkdirAll(pkgDir, 0o755) | ||
| require.NoError(t, err) | ||
|
|
||
| templateDir := path.Join(pkgDir, "agent", "input") | ||
| err = os.MkdirAll(templateDir, 0o755) | ||
| require.NoError(t, err) | ||
|
|
||
| fsys := fspath.DirFS(pkgDir) | ||
| errs := validateTemplateDir(fsys, path.Join("agent", "input")) | ||
| require.Empty(t, errs) | ||
|
|
||
| }) | ||
| t.Run("valid handlebars file", func(t *testing.T) { | ||
| tmpDir := t.TempDir() | ||
| pkgDir := path.Join(tmpDir, "package") | ||
| err := os.MkdirAll(pkgDir, 0o755) | ||
| require.NoError(t, err) | ||
|
|
||
| templateDir := path.Join(pkgDir, "agent", "input") | ||
| err = os.MkdirAll(templateDir, 0o755) | ||
| require.NoError(t, err) | ||
| hbsFilePath := path.Join(templateDir, "template.hbs") | ||
| hbsContent := `{{#if condition}}Valid Handlebars{{/if}}` | ||
| err = os.WriteFile(hbsFilePath, []byte(hbsContent), 0o644) | ||
| require.NoError(t, err) | ||
|
|
||
| fsys := fspath.DirFS(pkgDir) | ||
| errs := validateTemplateDir(fsys, path.Join("agent", "input")) | ||
| require.Empty(t, errs) | ||
| }) | ||
|
|
||
| t.Run("valid handlebars file template with multiline", func(t *testing.T) { | ||
| tmpDir := t.TempDir() | ||
| pkgDir := path.Join(tmpDir, "package") | ||
| err := os.MkdirAll(pkgDir, 0o755) | ||
| require.NoError(t, err) | ||
|
|
||
| templateDir := path.Join(pkgDir, "agent", "input") | ||
| err = os.MkdirAll(templateDir, 0o755) | ||
| require.NoError(t, err) | ||
| hbsFilePath := path.Join(templateDir, "template.hbs") | ||
| hbsContent := `audit_rules: "# Session data audit rules\n-a always,exit -F arch=b64 -S execve,execveat -k exec\n-a always,exit -F arch=b64 -S exit_group\n-a always,exit -F arch=b64 -S setsid\n{{escape_multiline_string audit_rules}}"` | ||
| err = os.WriteFile(hbsFilePath, []byte(hbsContent), 0o644) | ||
| require.NoError(t, err) | ||
|
|
||
| fsys := fspath.DirFS(pkgDir) | ||
| errs := validateTemplateDir(fsys, path.Join("agent", "input")) | ||
| require.Empty(t, errs) | ||
| }) | ||
| t.Run("invalid handlebars file", func(t *testing.T) { | ||
| tmpDir := t.TempDir() | ||
| pkgDir := path.Join(tmpDir, "package") | ||
| err := os.MkdirAll(pkgDir, 0o755) | ||
| require.NoError(t, err) | ||
|
|
||
| templateDir := path.Join(pkgDir, "agent", "input") | ||
| err = os.MkdirAll(templateDir, 0o755) | ||
| require.NoError(t, err) | ||
| hbsFilePath := path.Join(templateDir, "template.hbs") | ||
| hbsContent := `{{#if condition}}Valid Handlebars` | ||
| err = os.WriteFile(hbsFilePath, []byte(hbsContent), 0o644) | ||
| require.NoError(t, err) | ||
|
|
||
| fsys := fspath.DirFS(pkgDir) | ||
| errs := validateTemplateDir(fsys, path.Join("agent", "input")) | ||
| require.NotEmpty(t, errs) | ||
| assert.Len(t, errs, 1) | ||
| }) | ||
| t.Run("valid linked handlebars file", func(t *testing.T) { | ||
| tmpDir := t.TempDir() | ||
| pkgDir := path.Join(tmpDir, "package") | ||
| err := os.MkdirAll(pkgDir, 0o755) | ||
| require.NoError(t, err) | ||
|
|
||
| pkgDirLinked := path.Join(tmpDir, "linked") | ||
| err = os.MkdirAll(pkgDirLinked, 0o755) | ||
| require.NoError(t, err) | ||
| linkedHbsFilePath := path.Join(pkgDirLinked, "linked_template.hbs") | ||
| linkedHbsContent := `{{#if condition}}Valid Linked Handlebars{{/if}}` | ||
| err = os.WriteFile(linkedHbsFilePath, []byte(linkedHbsContent), 0o644) | ||
| require.NoError(t, err) | ||
|
|
||
| templateDir := path.Join(pkgDir, "agent", "input") | ||
| err = os.MkdirAll(templateDir, 0o755) | ||
| require.NoError(t, err) | ||
| hbsFilePath := path.Join(templateDir, "template.hbs.link") | ||
| hbsContent := `../../../linked/linked_template.hbs` | ||
| err = os.WriteFile(hbsFilePath, []byte(hbsContent), 0o644) | ||
| require.NoError(t, err) | ||
|
|
||
| fsys := fspath.DirFS(pkgDir) | ||
| errs := validateTemplateDir(fsys, path.Join("agent", "input")) | ||
| require.Empty(t, errs) | ||
|
|
||
| }) | ||
| t.Run("invalid linked handlebars file", func(t *testing.T) { | ||
| tmpDir := t.TempDir() | ||
| pkgDir := path.Join(tmpDir, "package") | ||
| err := os.MkdirAll(pkgDir, 0o755) | ||
| require.NoError(t, err) | ||
|
|
||
| pkgDirLinked := path.Join(tmpDir, "linked") | ||
| err = os.MkdirAll(pkgDirLinked, 0o755) | ||
| require.NoError(t, err) | ||
| linkedHbsFilePath := path.Join(pkgDirLinked, "linked_template.hbs") | ||
| linkedHbsContent := `{{#if condition}}Valid Linked Handlebars` | ||
| err = os.WriteFile(linkedHbsFilePath, []byte(linkedHbsContent), 0o644) | ||
| require.NoError(t, err) | ||
|
|
||
| templateDir := path.Join(pkgDir, "agent", "input") | ||
| err = os.MkdirAll(templateDir, 0o755) | ||
| require.NoError(t, err) | ||
| hbsFilePath := path.Join(templateDir, "template.hbs.link") | ||
| hbsContent := `../../../linked/linked_template.hbs` | ||
| err = os.WriteFile(hbsFilePath, []byte(hbsContent), 0o644) | ||
| require.NoError(t, err) | ||
|
|
||
| fsys := fspath.DirFS(pkgDir) | ||
| errs := validateTemplateDir(fsys, path.Join("agent", "input")) | ||
| require.NotEmpty(t, errs) | ||
| assert.Len(t, errs, 1) | ||
| }) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -225,6 +225,7 @@ func (s Spec) rules(pkgType string, rootSpec spectypes.ItemSpec) validationRules | |
| {fn: semantic.ValidateMinimumAgentVersion}, | ||
| {fn: semantic.ValidateIntegrationPolicyTemplates, types: []string{"integration"}}, | ||
| {fn: semantic.ValidatePipelineTags, types: []string{"integration"}, since: semver.MustParse("3.6.0")}, | ||
| {fn: semantic.ValidateStaticHandlebarsFiles, types: []string{"integration", "input"}}, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you consider to place the validation in the spec? It could have been done by using a content media type and something like the But using a semantic validator LGTM too in any case.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did not. Thanks for this suggestion, i see now that adding this to the spec, perhaps with a custom type i think this approach could be an improvement |
||
| } | ||
|
|
||
| var validationRules validationRules | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the comment, is this already done by the linkedfiles.FS ? If so, maybe it could be removed from here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this refers to the logic at
getLinkedFileChecksumwhere the file content is read to get the checksum. If the file is not within fsysfs.ReadFilefails, as is not in its scope. If this happens, we useos.ReadFile(like ingetLinkedFileChecksum) to read the file with the absolute path of it