From 9a6144abb1d94383421a0be8450e560d8cb77c90 Mon Sep 17 00:00:00 2001 From: Leonardo Di Giovanna Date: Fri, 17 Jul 2026 12:09:34 +0200 Subject: [PATCH 1/2] docs(suite/run): update stale command description Signed-off-by: Leonardo Di Giovanna --- cmd/suite/run/run.go | 6 +++--- cmd/suite/test/test.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/suite/run/run.go b/cmd/suite/run/run.go index 161fc586..7cbe17dd 100644 --- a/cmd/suite/run/run.go +++ b/cmd/suite/run/run.go @@ -27,8 +27,8 @@ import ( const ( longDescriptionPrefaceTemplate = `%s. It is possible to provide the YAML description in multiple ways. The order of evaluation is the following: -1) If the --%s= flag is provided the description is read from the file at -2) If the --%s= flag is provided, the description is read from the string +1) If --%s= and/or --%s= flags are/is provided, it is read from the file(s) at and/or contained in +2) If the --%s= flag is provided, it is read from the string 3) Otherwise, it is read from standard input` longDescriptionHeading = "Run test(s) specified via a YAML description" warningMessage = `Warning: @@ -38,7 +38,7 @@ It is possible to provide the YAML description in multiple ways. The order of ev var ( longDescriptionPreface = fmt.Sprintf(longDescriptionPrefaceTemplate, longDescriptionHeading, - config.DescriptionFileFlagName, config.DescriptionFlagName) + config.DescriptionFileFlagName, config.DescriptionDirFlagName, config.DescriptionFlagName) longDescription = fmt.Sprintf("%s\n\n%s", longDescriptionPreface, warningMessage) ) diff --git a/cmd/suite/test/test.go b/cmd/suite/test/test.go index 5d36ccc4..2b11aa07 100644 --- a/cmd/suite/test/test.go +++ b/cmd/suite/test/test.go @@ -63,8 +63,8 @@ const ( const ( longDescriptionPrefaceTemplate = `%s. It is possible to provide the YAML description in multiple ways. The order of evaluation is the following: -1) If --%s= and/or --%s= flags are/is provided, the description is read from the file at -2) If the --%s= flag is provided, the description is read from the string +1) If --%s= and/or --%s= flags are/is provided, it is read from the file(s) at and/or contained in +2) If the --%s= flag is provided, it is read from the string 3) Otherwise, it is read from standard input` longDescriptionHeading = "Run test(s) specified via a YAML description and verify that they produce the expected outcomes" warningMessage = `Warning: From e7863437d37dc63465d184f51f775bafb799bfff Mon Sep 17 00:00:00 2001 From: Leonardo Di Giovanna Date: Mon, 20 Jul 2026 12:36:43 +0200 Subject: [PATCH 2/2] feat(suite): add support for `render` sub-command The `suite render` sub-command allows to render the test(s) YAML description and display it to output. The rendering process instantiates any template present in the YAML description. The order and the way sources are evaluated is identical to the one the `suite test` command offers. For each YAML description source, a separate YAML document is written to output; each of these documents is annotated with a comment indicating the source name. Signed-off-by: Leonardo Di Giovanna --- cmd/suite/render/doc.go | 18 +++ cmd/suite/render/render.go | 247 +++++++++++++++++++++++++++++++++++++ cmd/suite/suite.go | 3 + cmd/suite/test/test.go | 2 +- 4 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 cmd/suite/render/doc.go create mode 100644 cmd/suite/render/render.go diff --git a/cmd/suite/render/doc.go b/cmd/suite/render/doc.go new file mode 100644 index 00000000..a6f79b35 --- /dev/null +++ b/cmd/suite/render/doc.go @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 The Falco Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package render provides the implementation of the "render" command. This command lets users render the test(s) YAML +// description and display it to output. +package render diff --git a/cmd/suite/render/render.go b/cmd/suite/render/render.go new file mode 100644 index 00000000..c011f953 --- /dev/null +++ b/cmd/suite/render/render.go @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 The Falco Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package render + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/go-logr/logr" + "github.com/spf13/cobra" + + "github.com/falcosecurity/event-generator/cmd/suite/config" + "github.com/falcosecurity/event-generator/pkg/envvar" + "github.com/falcosecurity/event-generator/pkg/test/loader" +) + +const ( + longDescriptionPrefaceTemplate = `%s. +The rendering process instantiates any template present in the YAML description. +It is possible to provide the YAML description in multiple ways. The order of evaluation is the following: +1) If --%s= and/or --%s= flags are/is provided, it is read from the file(s) at and/or contained in +2) If the --%s= flag is provided, it is read from the string +3) Otherwise, it is read from standard input +For each YAML description source, a separate YAML document is written to output. Each document is annotated with a comment indicating the source name. +` + longDescriptionHeading = "Render the test(s) YAML description and display it to output" +) + +var ( + longDescription = fmt.Sprintf(longDescriptionPrefaceTemplate, longDescriptionHeading, + config.DescriptionFileFlagName, config.DescriptionDirFlagName, config.DescriptionFlagName) +) + +// CommandWrapper wraps the command and stores the associated flag values. +type CommandWrapper struct { + Command *cobra.Command + envKeysPrefix string + suiteEnvKey string + + // Flags + // + // testsDescriptionFiles is the list of pathnames of files containing the YAML tests descriptions. If + // testsDescription is provided, this is empty. + testsDescriptionFiles []string + // testsDescriptionDirs is the list of pathnames of directories containing the YAML tests description files. If + // testsDescription is provided, this is empty. + testsDescriptionDirs []string + // testsDescription is the YAML tests description. If testsDescriptionFiles or testsDescriptionDirs are provided, + // this is empty. + testsDescription string +} + +// New creates a new render command. +func New(suiteEnvKey, envKeysPrefix string) *CommandWrapper { + cw := &CommandWrapper{envKeysPrefix: envKeysPrefix, suiteEnvKey: suiteEnvKey} + c := &cobra.Command{ + Use: "render", + Short: longDescriptionHeading, + Long: longDescription, + DisableAutoGenTag: true, + Run: cw.run, + } + cw.Command = c + cw.initCommandFlags() + return cw +} + +// initCommandFlags initializes the command's flags. +func (cw *CommandWrapper) initCommandFlags() { + cmd := cw.Command + flags := cmd.Flags() + + flags.StringSliceVarP(&cw.testsDescriptionFiles, config.DescriptionFileFlagName, "f", nil, + "The pathnames of tests description YAML files specifying the tests to be rendered. Multiple pathnames can be "+ + "specified as a comma-separated list. The flag can be specified multiple times. Pathnames are evaluated "+ + "in order of appearance") + flags.StringSliceVarP(&cw.testsDescriptionDirs, config.DescriptionDirFlagName, "d", nil, + "The pathnames of directories containing tests description YAML files specifying the tests to be rendered. "+ + "Sub-directories of the provided pathnames are not recursively loaded. Only files with YAML extensions "+ + "are loaded. Multiple pathnames can be specified as a comma-separated list. The flag can be specified "+ + "multiple times. Pathnames are evaluated in order of appearance") + flags.StringVar(&cw.testsDescription, config.DescriptionFlagName, "", + "The YAML-formatted tests description string specifying the tests to be rendered") + cmd.MarkFlagsMutuallyExclusive(config.DescriptionFileFlagName, config.DescriptionFlagName) + cmd.MarkFlagsMutuallyExclusive(config.DescriptionDirFlagName, config.DescriptionFlagName) +} + +func (cw *CommandWrapper) run(cmd *cobra.Command, _ []string) { + ctx := cmd.Context() + logger, err := logr.FromContext(ctx) + if err != nil { + panic(fmt.Sprintf("logger unconfigured: %v", err)) + } + + logger = logger.WithName("main") + + // Note: keep the following in sync with the corresponding code in "test" package. + reservedEnvKeyPrefixes := []string{envvar.KeyFromFlagName(cw.envKeysPrefix, "")} + reservedEnvKeys := []string{cw.suiteEnvKey} + testDescs, err := loadTests(logger, cw.testsDescriptionFiles, cw.testsDescriptionDirs, cw.testsDescription, + reservedEnvKeyPrefixes, reservedEnvKeys) + if err != nil { + logger.Error(err, "Error loading tests") + os.Exit(1) + } + + w := os.Stdout + for _, testDesc := range testDescs { + source := testDesc.source + logger := logger.WithValues("source", source) + header := fmt.Sprintf("---\n# source: %s\n", source) + if _, err := w.WriteString(header); err != nil { + logger.Error(err, "Error writing tests description header") + os.Exit(1) + } + + if err := testDesc.desc.Write(w); err != nil { + logger.Error(err, "Error writing tests description") + os.Exit(1) + } + } +} + +type sourcedDescription struct { + source string + desc *loader.Description +} + +// loadTests loads tests description(s) from a different source, depending on the content of the provided values: +// - if the provided descriptionFilePaths or descriptionDirPaths are not empty, they are loaded both from the +// specified files (if any) and from the YAML files (if any) in the specified directories (if any); +// - if the provided description is not empty, they are loaded from its content; +// - otherwise, they are loaded from standard input. +func loadTests(logger logr.Logger, descriptionFilePaths, descriptionDirPaths []string, description string, + reservedEnvKeyPrefixes, reservedEnvKeys []string) ([]*sourcedDescription, error) { + descLoader := loader.New(reservedEnvKeyPrefixes, reservedEnvKeys) + + // Load from the specified files or directories. + if len(descriptionFilePaths) > 0 || len(descriptionDirPaths) > 0 { + var testsDescs []*sourcedDescription + for _, descriptionDirPath := range descriptionDirPaths { + descs, err := loadTestsFromDescriptionDir(logger, descLoader, descriptionDirPath) + if err != nil { + return nil, fmt.Errorf("error loading description directory %q: %w", descriptionDirPath, err) + } + testsDescs = append(testsDescs, descs...) + } + + for _, descriptionFilePath := range descriptionFilePaths { + desc, err := loadTestsFromDescriptionFile(logger, descLoader, descriptionFilePath) + if err != nil { + return nil, fmt.Errorf("error loading description file %q: %w", descriptionFilePath, err) + } + testsDescs = append(testsDescs, desc) + } + + return testsDescs, nil + } + + // Load from the provided description string. + if description != "" { + desc, err := descLoader.Load(strings.NewReader(description)) + if err != nil { + return nil, fmt.Errorf("error loading from description flag: %w", err) + } + return []*sourcedDescription{{source: "", desc: desc}}, nil + } + + // Load from standard input. + desc, err := descLoader.Load(os.Stdin) + if err != nil { + return nil, fmt.Errorf("error loading from stdin: %w", err) + } + return []*sourcedDescription{{source: "", desc: desc}}, nil +} + +// loadTestsFromDescriptionDir loads tests descriptions from YAML files inside the directory at the provided path and +// returns them. +func loadTestsFromDescriptionDir(logger logr.Logger, descLoader *loader.Loader, + descriptionDirPath string) ([]*sourcedDescription, error) { + descriptionDirPath = filepath.Clean(descriptionDirPath) + dirEntries, err := os.ReadDir(descriptionDirPath) + if err != nil { + return nil, fmt.Errorf("error reading entries in directory %q: %w", descriptionDirPath, err) + } + + testsDescs := make([]*sourcedDescription, 0, len(dirEntries)) + + for _, dirEntry := range dirEntries { + if dirEntry.IsDir() { + continue + } + + name := dirEntry.Name() + if !strings.HasSuffix(name, ".yaml") { + continue + } + + descriptionFilePath := filepath.Join(descriptionDirPath, name) + testsDesc, err := loadTestsFromDescriptionFile(logger, descLoader, descriptionFilePath) + if err != nil { + return nil, fmt.Errorf("error loading description file %q: %w", name, err) + } + + testsDescs = append(testsDescs, testsDesc) + } + + return testsDescs, nil +} + +// loadTestsFromDescriptionFile loads tests description from the file at the provided path and returns it. The +// associated name is the "cleaned" version of the provided path (see filepath.Clean). +func loadTestsFromDescriptionFile(logger logr.Logger, descLoader *loader.Loader, + descriptionFilePath string) (*sourcedDescription, error) { + descriptionFilePath = filepath.Clean(descriptionFilePath) + descriptionFile, err := os.Open(descriptionFilePath) + if err != nil { + return nil, fmt.Errorf("error opening file path %q: %w", descriptionFilePath, err) + } + defer func() { + if err := descriptionFile.Close(); err != nil { + logger.Error(err, "Error closing description file", "path", descriptionFilePath) + } + }() + + desc, err := descLoader.Load(descriptionFile) + if err != nil { + return nil, err + } + + return &sourcedDescription{source: descriptionFilePath, desc: desc}, nil +} diff --git a/cmd/suite/suite.go b/cmd/suite/suite.go index a3503ce0..2cc3fa4c 100644 --- a/cmd/suite/suite.go +++ b/cmd/suite/suite.go @@ -20,6 +20,7 @@ import ( "github.com/falcosecurity/event-generator/cmd/suite/config" "github.com/falcosecurity/event-generator/cmd/suite/explain" + "github.com/falcosecurity/event-generator/cmd/suite/render" "github.com/falcosecurity/event-generator/cmd/suite/run" "github.com/falcosecurity/event-generator/cmd/suite/test" "github.com/falcosecurity/event-generator/cmd/suite/verify" @@ -40,9 +41,11 @@ func New(suiteEnvKey, envKeysPrefix string) *cobra.Command { testCmd := test.New(commonConf, false).Command explainCmd := explain.New().Command verifyCmd := verify.New(suiteEnvKey, envKeysPrefix).Command + renderCmd := render.New(suiteEnvKey, envKeysPrefix).Command c.AddCommand(runCmd) c.AddCommand(testCmd) c.AddCommand(explainCmd) c.AddCommand(verifyCmd) + c.AddCommand(renderCmd) return c } diff --git a/cmd/suite/test/test.go b/cmd/suite/test/test.go index 2b11aa07..0838868d 100644 --- a/cmd/suite/test/test.go +++ b/cmd/suite/test/test.go @@ -173,7 +173,7 @@ func (cw *CommandWrapper) run(cmd *cobra.Command, _ []string) { isRootProcess := cw.TestID == "" verifyExpectedOutcome := isRootProcess && !cw.skipOutcomeVerification - // Note: keep the following in sync with the corresponding code in "verify" package. + // Note: keep the following in sync with the corresponding code in "verify" and "render" package. reservedEnvKeyPrefixes := []string{envvar.KeyFromFlagName(cw.EnvKeysPrefix, "")} reservedEnvKeys := []string{cw.SuiteEnvKey} testSuites, err := LoadSuites(logger, !verifyExpectedOutcome, cw.TestsDescriptionFiles, cw.TestsDescriptionDirs,