Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d6d204e
Add Handlebars template validation and corresponding tests
teresaromero Nov 28, 2025
ea6d4bd
add validator test cases with bad packages
teresaromero Nov 28, 2025
67389f6
Add validation for Handlebars template files in input and integration…
teresaromero Nov 28, 2025
97a32b2
Refactor Handlebars definition specifications for clarity and consist…
teresaromero Nov 28, 2025
f5854ea
Update changelog with link
teresaromero Nov 28, 2025
32ebc29
Fix variable naming for handlebars template validation error
teresaromero Nov 28, 2025
e76c19a
Fix syntax error in sql_query definition across multiple Handlebars t…
teresaromero Nov 28, 2025
49c9c5a
Reorder import statements for consistency in validate_hbs_templates.go
teresaromero Nov 28, 2025
86281bb
update compliace gomod
teresaromero Nov 28, 2025
3e48130
Refactor Handlebars validation logic to include linked files
teresaromero Nov 28, 2025
32ab54d
Add bad integration Handlebars templates with linked files
teresaromero Nov 28, 2025
bc7eee5
Reorder import statements for consistency in validate_hbs_templates_t…
teresaromero Nov 28, 2025
0f1ae93
use path instead of filepath
teresaromero Dec 1, 2025
d4898fe
Replace filepath.Join with path.Join in TestValidateHandlebarsFiles f…
teresaromero Dec 1, 2025
d6ee0cd
fix validateHandlebarsEntry to read file content from filesystem or a…
teresaromero Dec 1, 2025
ab1a541
Reorder logrus dependency in go.mod for consistency
teresaromero Dec 3, 2025
0d298e7
Improve error messages in Handlebars validation for clarity
teresaromero Dec 3, 2025
e699759
Add link to Fleet implementation link for available Handlebars helper…
teresaromero Dec 3, 2025
31644e8
Refactor Handlebars validation to return structured errors and update…
teresaromero Dec 3, 2025
62679a5
Merge branch 'main' of github.com:elastic/package-spec into 21-handle…
teresaromero Dec 3, 2025
17b18db
append errors instead of return
teresaromero Dec 3, 2025
bd2358e
Update Handlebars documentation to include available meta variables a…
teresaromero Dec 5, 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
82 changes: 82 additions & 0 deletions code/go/internal/validator/semantic/validate_hbs_templates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// 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"
"strings"

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

var (
errInvalidHandlebarsTemplate = errors.New("invalid handlebars template")
)

// ValidateHandlebarsFiles 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 ValidateHandlebarsFiles(fsys fspath.FS) specerrors.ValidationErrors {
entries, err := getHandlebarsFiles(fsys)
if err != nil {
return specerrors.ValidationErrors{
specerrors.NewStructuredErrorf(
"error finding Handlebars files: %w", err,
),
}
}
if len(entries) == 0 {
return nil
}

var validationErrors specerrors.ValidationErrors
for _, entry := range entries {
if !strings.HasSuffix(entry, ".hbs") {
continue
}

filePath := fsys.Path(entry)
err := validateFile(filePath)
if err != nil {
validationErrors = append(validationErrors, specerrors.NewStructuredErrorf(
"%w: file %s: %w", errInvalidHandlebarsTemplate, entry, err,
))
}
}

return validationErrors
}

// validateFile validates a single Handlebars file located at filePath.
// it parses the file using the raymond library to check for syntax errors.
func validateFile(filePath string) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd rename this to be more specific, what about validateHandlebarTemplateFile or validateHandlebarTemplate?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yup, i've gone around this one :D as the "parent" function is very similar... i will think around this

if filePath == "" {
return nil
}
_, err := raymond.ParseFile(filePath)
return err
}

// getHandlebarsFiles returns all Handlebars (.hbs) files in the package filesystem.
// It searches in both the package root and data stream directories under the agent folder.
func getHandlebarsFiles(fsys fspath.FS) ([]string, error) {
entries := make([]string, 0)
pkgEntries, err := fs.Glob(fsys, "agent/**/*.hbs")
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, err
}
Copy link
Contributor

Choose a reason for hiding this comment

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

According to the spec, this files could also be links.
Would this catch even the links thanks to the filesystem ? Or is it needed to add a special case for link files?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i've updated the PR to include the linked files. For this i've changed the aproach of glob and used nested readDirs to get into the files.
i've added tests and test packages to check this works as expected with the links 👍🏻

entries = append(entries, pkgEntries...)

dataStreamEntries, err := fs.Glob(fsys, "data_stream/*/agent/**/*.hbs")
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, err
}
entries = append(entries, dataStreamEntries...)

return entries, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// 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"
)

func TestValidateFile(t *testing.T) {

t.Run("no handlebars files", func(t *testing.T) {
err := validateFile("")
assert.NoError(t, err)
})

t.Run("valid handlebars files", func(t *testing.T) {
tmp := t.TempDir()

filePath := filepath.Join(tmp, "template.yml.hbs")
err := os.WriteFile(filePath, []byte("{{#if foo}}hello{{/if}}"), 0o644)
require.NoError(t, err)

errs := validateFile(filePath)
assert.Empty(t, errs)
})

t.Run("invalid handlebars files", func(t *testing.T) {
tmp := t.TempDir()

filePath := filepath.Join(tmp, "bad.hbs")
// Unclosed block should produce a parse error.
err := os.WriteFile(filePath, []byte("{{#if foo}}no end"), 0o644)
require.NoError(t, err)

err = validateFile(filePath)
require.Error(t, err)
})

}
1 change: 1 addition & 0 deletions code/go/internal/validator/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ func (s Spec) rules(pkgType string, rootSpec spectypes.ItemSpec) validationRules
{fn: semantic.ValidateInputPackagesPolicyTemplates, types: []string{"input"}},
{fn: semantic.ValidateMinimumAgentVersion},
{fn: semantic.ValidateIntegrationPolicyTemplates, types: []string{"integration"}},
{fn: semantic.ValidateHandlebarsFiles, types: []string{"integration", "input"}},
}

var validationRules validationRules
Expand Down
22 changes: 22 additions & 0 deletions code/go/pkg/validator/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,28 @@ func TestLinksAreBlocked(t *testing.T) {
t.Error("links should not be allowed in package")
}

func TestValidateHandlebarsFiles(t *testing.T) {
tests := map[string]string{
"bad_input_hbs": "invalid handlebars template: file agent/input/input.yml.hbs: Parse error on line 10:\nExpecting OpenEndBlock, got: 'EOF'",
"bad_integration_hbs": "invalid handlebars template: file data_stream/foo/agent/stream/filestream.yml.hbs: Parse error on line 43:\nExpecting OpenEndBlock, got: 'EOF'",
}

for pkgName, expectedErrorMessage := range tests {
t.Run(pkgName, func(t *testing.T) {
errs := ValidateFromPath(filepath.Join("..", "..", "..", "..", "test", "packages", pkgName))
require.Error(t, errs)
vErrs, ok := errs.(specerrors.ValidationErrors)
require.True(t, ok)

var errMessages []string
for _, vErr := range vErrs {
errMessages = append(errMessages, vErr.Error())
}
require.Contains(t, errMessages, expectedErrorMessage)
})
}
}

func requireErrorMessage(t *testing.T, pkgName string, invalidItemsPerFolder map[string][]string, expectedErrorMessage string) {
pkgRootPath := filepath.Join("..", "..", "..", "..", "test", "packages", pkgName)

Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/evanphx/json-patch/v5 v5.9.11
github.com/go-viper/mapstructure/v2 v2.4.0
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901
github.com/mailgun/raymond/v2 v2.0.48
github.com/otiai10/copy v1.14.1
github.com/stretchr/testify v1.11.1
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415
Expand All @@ -24,6 +25,8 @@ require (
honnef.co/go/tools v0.6.1
)

require github.com/sirupsen/logrus v1.8.1 // indirect

require (
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
github.com/PaesslerAG/gval v1.0.0 // indirect
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw=
github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
Expand All @@ -58,10 +60,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
Expand All @@ -86,6 +92,7 @@ golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
Expand All @@ -106,6 +113,9 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/gotestsum v1.13.0 h1:+Lh454O9mu9AMG1APV4o0y7oDYKyik/3kBOiCqiEpRo=
Expand Down
3 changes: 3 additions & 0 deletions spec/changelog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
- description: Require defining agent version constraints in input and integration packages.
type: breaking-change
link: https://github.com/elastic/package-spec/pull/999
- description: Handlebars template files are now validated in input and integration packages.
type: enhancement
link: https://github.com/elastic/package-spec/pull/1030
- version: 3.5.4
changes:
- description: Fix rule matching processor for event.original handling.
Expand Down
31 changes: 20 additions & 11 deletions spec/integration/agent/spec.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
spec:
additionalContents: false
contents:
- description: Folder containing input definitions
type: folder
name: input
required: true
additionalContents: false
contents:
- description: Config template file for inputs defined in the policy_templates section of the top level manifest
type: file
sizeLimit: 2MB
pattern: '^.+\.yml\.hbs$'
- description: Folder containing input definitions
type: folder
name: input
required: true
allowLink: true
additionalContents: false
contents:
- description: |
Config template file for inputs defined in the policy_templates section of the top level manifest.
The template should use standard Handlebars syntax (e.g., `{{vars.key}}`, `{{#if vars.condition}}`, `{{#each vars.items}}`)
and must compile to valid YAML.
Available Handlebars helpers include:
- `contains` (checks if item is in array/string),
- `escape_string` (wraps string in single quotes and escapes them),
- `escape_multiline_string` (escapes multiline strings without wrapping),
- `to_json` (converts object to JSON string), and
- `url_encode` (URI encodes string).
Comment on lines +14 to +19
Copy link
Contributor

Choose a reason for hiding this comment

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

These helpers are defined in Fleet (Kibana), it could be added a reference here in the description that the full list of helpers is in Fleet Kibana.

At least, I've found that they are defined here:
https://github.com/elastic/kibana/blob/70749d9216d7a5de6ce1e7a028d153d5390ad3ac/x-pack/platform/plugins/shared/fleet/server/services/epm/agent/agent.ts#L226
But not sure about adding this link since it could be outdated in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i took a list from that file and created the text with it, i will add the permlink to the file as a warning that these list can be updated so they can check the fleet code also

type: file
sizeLimit: 2MB
pattern: '^.+\.yml\.hbs$'
required: true
allowLink: true
31 changes: 20 additions & 11 deletions spec/integration/data_stream/agent/spec.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
spec:
additionalContents: false
contents:
- description: Folder containing input definitions
type: folder
name: stream
required: true
additionalContents: false
contents:
- description: Config template file for inputs defined in the policy_templates section of the top level manifest
type: file
sizeLimit: 2MB
pattern: '^.+\.yml\.hbs$'
- description: Folder containing input definitions
type: folder
name: stream
required: true
allowLink: true
additionalContents: false
contents:
- description: |
Config template file for inputs defined in the policy_templates section of the top level manifest.
The template should use standard Handlebars syntax (e.g., `{{vars.key}}`, `{{#if vars.condition}}`, `{{#each vars.items}}`)
and must compile to valid YAML.
Available Handlebars helpers include:
Copy link
Member

Choose a reason for hiding this comment

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

Can you please document the metadata variables that are available in templates too. See elastic/kibana#241140

Copy link
Contributor Author

Choose a reason for hiding this comment

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

udpated the pr bd2358e with more docs on variables

- `contains` (checks if item is in array/string),
- `escape_string` (wraps string in single quotes and escapes them),
- `escape_multiline_string` (escapes multiline strings without wrapping),
- `to_json` (converts object to JSON string), and
- `url_encode` (URI encodes string).
type: file
sizeLimit: 2MB
pattern: '^.+\.yml\.hbs$'
required: true
allowLink: true
Loading