From 73f58683039ac39241c67b9138fe1793c3aa65d9 Mon Sep 17 00:00:00 2001 From: Leonardo Di Giovanna Date: Tue, 29 Oct 2024 14:57:30 +0100 Subject: [PATCH 1/6] chore(decl/log): add more log context in test and resource builders Signed-off-by: Leonardo Di Giovanna Co-authored-by: Aldo Lacuku --- pkg/test/builder/builder.go | 11 +++++++---- pkg/test/resource/builder/builder.go | 4 ++-- pkg/test/resource/clientserver/clientserver.go | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pkg/test/builder/builder.go b/pkg/test/builder/builder.go index 9c03a662..2d5e9d30 100644 --- a/pkg/test/builder/builder.go +++ b/pkg/test/builder/builder.go @@ -56,17 +56,20 @@ func New(resourceBuilder resource.Builder, stepBuilder step.Builder) (test.Build func (b *builder) Build(logger logr.Logger, testDesc *loader.Test) (test.Test, error) { // Create a unique test script from "before" and "after" scripts. - testScript := shell.New(logger, testDesc.BeforeScript, testDesc.AfterScript) + scriptLogger := logger.WithName("script") + testScript := shell.New(scriptLogger, testDesc.BeforeScript, testDesc.AfterScript) // Build test resources. resourcesNum := len(testDesc.Resources) testResources := make([]resource.Resource, 0, resourcesNum) for resourceIndex := 0; resourceIndex < resourcesNum; resourceIndex++ { rawResource := &testDesc.Resources[resourceIndex] - logger := logger.WithValues("resourceIndex", resourceIndex) - testResource, err := b.resourceBuilder.Build(logger, rawResource) + resourceName := rawResource.Name + resourceLogger := logger.WithName("resource").WithValues("resourceName", resourceName, + "resourceIndex", resourceIndex) + testResource, err := b.resourceBuilder.Build(resourceLogger, rawResource) if err != nil { - return nil, &test.ResourceBuildError{ResourceName: rawResource.Name, ResourceIndex: resourceIndex, Err: err} + return nil, &test.ResourceBuildError{ResourceName: resourceName, ResourceIndex: resourceIndex, Err: err} } testResources = append(testResources, testResource) diff --git a/pkg/test/resource/builder/builder.go b/pkg/test/resource/builder/builder.go index acd975bb..28b60cdf 100644 --- a/pkg/test/resource/builder/builder.go +++ b/pkg/test/resource/builder/builder.go @@ -47,7 +47,7 @@ func New() (resource.Builder, error) { func (b *builder) Build(logger logr.Logger, testResource *loader.TestResource) (resource.Resource, error) { resourceType := testResource.Type resourceName := testResource.Name - logger = logger.WithValues("resourceType", resourceType, "resourceName", resourceName) + logger = logger.WithValues("resourceType", resourceType) switch resourceType { case loader.TestResourceTypeClientServer: clientServerSpec, ok := testResource.Spec.(*loader.TestResourceClientServerSpec) @@ -79,7 +79,7 @@ func (b *builder) Build(logger logr.Logger, testResource *loader.TestResource) ( func (b *builder) buildFD(logger logr.Logger, resourceName string, fdSpec *loader.TestResourceFDSpec) (resource.Resource, error) { subtype := fdSpec.Subtype - logger = logger.WithValues("resourceSubtype", subtype) + logger = logger.WithValues("fdSubtype", subtype) switch subtype { case loader.TestResourceFDSubtypeFile: subSpec, ok := fdSpec.Spec.(*loader.TestResourceFDFileSpec) diff --git a/pkg/test/resource/clientserver/clientserver.go b/pkg/test/resource/clientserver/clientserver.go index ee661998..2c20059c 100644 --- a/pkg/test/resource/clientserver/clientserver.go +++ b/pkg/test/resource/clientserver/clientserver.go @@ -614,10 +614,10 @@ func (cs *clientServer) Destroy(_ context.Context) error { // Close any open FD. for fd := range cs.openFDs { - cs.logger.V(1).Info("Closing FD", "fd", fd) if err := unix.Close(fd); err != nil { cs.logger.Error(err, "Error closing FD", "fd", fd) } + cs.logger.V(1).Info("Closed FD", "fd", fd) } cs.openFDs = make(map[int]struct{}) cs.fields.Client.FD = -1 From e33e3c446dfc8f254f821aee6cf6df15f002b0ae Mon Sep 17 00:00:00 2001 From: Leonardo Di Giovanna Date: Tue, 29 Oct 2024 15:26:34 +0100 Subject: [PATCH 2/6] feat(decl/loader): add support for test configuration serialization Signed-off-by: Leonardo Di Giovanna Co-authored-by: Aldo Lacuku --- pkg/test/loader/loader.go | 153 ++++++++++++++++++++++++++++++++++---- 1 file changed, 139 insertions(+), 14 deletions(-) diff --git a/pkg/test/loader/loader.go b/pkg/test/loader/loader.go index 778bf9b1..e98d9d1b 100644 --- a/pkg/test/loader/loader.go +++ b/pkg/test/loader/loader.go @@ -55,6 +55,16 @@ type Configuration struct { Tests []Test `yaml:"tests" validate:"min=1,unique=Name,dive"` } +// Write writes the configuration to the provided writer. +func (c *Configuration) Write(w io.Writer) error { + enc := yaml.NewEncoder(w) + if err := enc.Encode(c); err != nil { + return fmt.Errorf("error encoding configuration: %w", err) + } + + return nil +} + // validate validates the current configuration. func (c *Configuration) validate() error { // Register custom validations and validate configuration @@ -118,12 +128,12 @@ func validateRuleName(fl validator.FieldLevel) bool { type Test struct { Rule string `yaml:"rule" validate:"rule_name"` Name string `yaml:"name" validate:"required"` - Description *string `yaml:"description" validate:"omitempty,min=1"` + Description *string `yaml:"description,omitempty" validate:"omitempty,min=1"` Runner TestRunnerType `yaml:"runner" validate:"-"` - Context *TestContext `yaml:"context"` - BeforeScript *string `yaml:"before" validate:"omitempty,min=1"` - AfterScript *string `yaml:"after" validate:"omitempty,min=1"` - Resources []TestResource `yaml:"resources" validate:"omitempty,unique=Name,dive"` + Context *TestContext `yaml:"context,omitempty" validate:"omitempty"` + BeforeScript *string `yaml:"before,omitempty" validate:"omitempty,min=1"` + AfterScript *string `yaml:"after,omitempty" validate:"omitempty,min=1"` + Resources []TestResource `yaml:"resources,omitempty" validate:"omitempty,unique=Name,dive"` Steps []TestStep `yaml:"steps" validate:"min=1,unique=Name,dive"` ExpectedOutput TestExpectedOutput `yaml:"expectedOutput"` } @@ -155,14 +165,14 @@ func (r *TestRunnerType) UnmarshalYAML(node *yaml.Node) error { // TestContext contains information regarding the running context of a test. type TestContext struct { - Container *ContainerContext `yaml:"container"` - Processes []ProcessContext `yaml:"processes" validate:"-"` + Container *ContainerContext `yaml:"container,omitempty"` + Processes []ProcessContext `yaml:"processes,omitempty" validate:"omitempty,dive"` } // ContainerContext contains information regarding the container instance that will run a test. type ContainerContext struct { Image string `yaml:"image" validate:"required"` - Name *string `yaml:"name" validate:"omitempty,min=1"` + Name *string `yaml:"name,omitempty" validate:"omitempty,min=1"` } // ProcessContext contains information regarding the process that will run a test, or information about one of its @@ -209,6 +219,96 @@ func (r *TestResource) UnmarshalYAML(node *yaml.Node) error { return nil } +// MarshalYAML returns an inner representation of the TestResource instance that is used, in place of the instance, to +// marshal the content. +// TODO: this method should be implemented with a pointer receiver but unfortunately, the yaml.v3 library is only able +// to call it if it is implemented with a value receiver. Uniform the receivers once the library is replaced. +func (r TestResource) MarshalYAML() (interface{}, error) { + switch resourceType := r.Type; resourceType { + case TestResourceTypeClientServer: + return struct { + Type TestResourceType `yaml:"type"` + Name string `yaml:"name"` + Spec *TestResourceClientServerSpec `yaml:"spec,inline"` + }{Type: resourceType, Name: r.Name, Spec: r.Spec.(*TestResourceClientServerSpec)}, nil + case TestResourceTypeFD: + return r.marshalFD() + default: + return nil, fmt.Errorf("unknown test resource type %q", resourceType) + } +} + +// marshalFD returns an inner representation of the fd test resource instance that is used, in place of the instance, to +// marshal the content. +// TODO: this function contains a lot of repetitions for TestResource common fields. However, it is not possible to +// provide an addition MarshalYAML method for TestResourceFDSpec, as it will not be called by the library if the Spec +// field specify "inline" (as it should be in our case). Take care of replace this with a more elegant solution once +// yaml.v3 is replaced. +func (r *TestResource) marshalFD() (interface{}, error) { + spec := r.Spec.(*TestResourceFDSpec) + subSpec := spec.Spec + switch subtype := spec.Subtype; subtype { + case TestResourceFDSubtypeFile: + return struct { + Type TestResourceType `yaml:"type"` + Name string `yaml:"name"` + Subtype TestResourceFDSubtype `yaml:"subtype"` + Spec *TestResourceFDFileSpec `yaml:"subspec,inline"` + }{Type: r.Type, Name: r.Name, Subtype: subtype, Spec: subSpec.(*TestResourceFDFileSpec)}, nil + case TestResourceFDSubtypeDirectory: + return struct { + Type TestResourceType `yaml:"type"` + Name string `yaml:"name"` + Subtype TestResourceFDSubtype `yaml:"subtype"` + Spec *TestResourceFDDirectorySpec `yaml:"subspec,inline"` + }{Type: r.Type, Name: r.Name, Subtype: subtype, Spec: subSpec.(*TestResourceFDDirectorySpec)}, nil + case TestResourceFDSubtypePipe: + return struct { + Type TestResourceType `yaml:"type"` + Name string `yaml:"name"` + Subtype TestResourceFDSubtype `yaml:"subtype"` + Spec *TestResourceFDPipeSpec `yaml:"subspec,inline"` + }{Type: r.Type, Name: r.Name, Subtype: subtype, Spec: subSpec.(*TestResourceFDPipeSpec)}, nil + case TestResourceFDSubtypeEvent: + return struct { + Type TestResourceType `yaml:"type"` + Name string `yaml:"name"` + Subtype TestResourceFDSubtype `yaml:"subtype"` + Spec *TestResourceFDEventSpec `yaml:"subspec,inline"` + }{Type: r.Type, Name: r.Name, Subtype: subtype, Spec: subSpec.(*TestResourceFDEventSpec)}, nil + case TestResourceFDSubtypeSignal: + return struct { + Type TestResourceType `yaml:"type"` + Name string `yaml:"name"` + Subtype TestResourceFDSubtype `yaml:"subtype"` + Spec *TestResourceFDSignalSpec `yaml:"subspec,inline"` + }{Type: r.Type, Name: r.Name, Subtype: subtype, Spec: subSpec.(*TestResourceFDSignalSpec)}, nil + case TestResourceFDSubtypeEpoll: + return struct { + Type TestResourceType `yaml:"type"` + Name string `yaml:"name"` + Subtype TestResourceFDSubtype `yaml:"subtype"` + Spec *TestResourceFDEpollSpec `yaml:"subspec,inline"` + }{Type: r.Type, Name: r.Name, Subtype: subtype, Spec: subSpec.(*TestResourceFDEpollSpec)}, nil + case TestResourceFDSubtypeInotify: + return struct { + Type TestResourceType `yaml:"type"` + Name string `yaml:"name"` + Subtype TestResourceFDSubtype `yaml:"subtype"` + Spec *TestResourceFDInotifySpec `yaml:"subspec,inline"` + }{Type: r.Type, Name: r.Name, Subtype: subtype, Spec: subSpec.(*TestResourceFDInotifySpec)}, nil + case TestResourceFDSubtypeMem: + return struct { + Type TestResourceType `yaml:"type"` + Name string `yaml:"name"` + Subtype TestResourceFDSubtype `yaml:"subtype"` + Spec *TestResourceFDMemSpec `yaml:"subspec,inline"` + }{Type: r.Type, Name: r.Name, Subtype: subtype, Spec: subSpec.(*TestResourceFDMemSpec)}, nil + default: + return nil, fmt.Errorf("unknown fd test resource subtype %q", subtype) + } +} + // TestResourceType is the type of test resource. type TestResourceType string @@ -451,6 +551,31 @@ func (s *TestStep) UnmarshalYAML(node *yaml.Node) error { return nil } +// MarshalYAML returns an inner representation of the TestStep instance that is used, in place of the instance, to +// marshal the content. +// TODO: this method should be implemented with a pointer receiver but unfortunately, the yaml.v3 library is only able +// to call it if it is implemented with a value receiver. Uniform the receivers once the library is replaced. +func (s TestStep) MarshalYAML() (interface{}, error) { + switch stepType := s.Type; stepType { + case TestStepTypeSyscall: + spec := s.Spec.(*TestStepSyscallSpec) + args := make(map[string]string, len(spec.Args)+len(s.FieldBindings)) + for arg, argValue := range spec.Args { + args[arg] = argValue + } + for _, fieldBinding := range s.FieldBindings { + args[fieldBinding.LocalField] = fmt.Sprintf("${%s.%s}", fieldBinding.SrcStep, fieldBinding.SrcField) + } + return struct { + Type TestStepType `yaml:"type"` + Name string `yaml:"name"` + Spec *TestStepSyscallSpec `yaml:"spec,inline"` + }{Type: stepType, Name: s.Name, Spec: &TestStepSyscallSpec{Syscall: spec.Syscall, Args: args}}, nil + default: + return nil, fmt.Errorf("unknown test step type %q", stepType) + } +} + // TestStepType is the type of test step. type TestStepType string @@ -587,10 +712,10 @@ func (s *SyscallName) UnmarshalYAML(node *yaml.Node) error { // TestExpectedOutput is the expected output for a test. type TestExpectedOutput struct { - Source *string `yaml:"source" validate:"-"` - Time *string `yaml:"time" validate:"-"` - Hostname *string `yaml:"hostname" validate:"-"` - Priority *string `yaml:"priority" validate:"-"` - Output *string `yaml:"output" validate:"-"` - OutputFields map[string]string `yaml:"outputFields" validate:"-"` + Source *string `yaml:"source,omitempty" validate:"omitempty,min=1"` + Time *string `yaml:"time,omitempty" validate:"omitempty,min=1"` + Hostname *string `yaml:"hostname,omitempty" validate:"omitempty,min=1"` + Priority *string `yaml:"priority,omitempty" validate:"omitempty,min=1"` + Output *string `yaml:"output,omitempty" validate:"omitempty,min=1"` + OutputFields map[string]string `yaml:"outputFields,omitempty" validate:"omitempty,min=1"` } From 9c3c740b92f6532123b38f59ebf9fa050e8418be Mon Sep 17 00:00:00 2001 From: Leonardo Di Giovanna Date: Wed, 30 Oct 2024 09:27:24 +0100 Subject: [PATCH 3/6] feat(decl/loader): complete process context definition Signed-off-by: Leonardo Di Giovanna Co-authored-by: Aldo Lacuku --- pkg/test/loader/loader.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pkg/test/loader/loader.go b/pkg/test/loader/loader.go index e98d9d1b..f5855c19 100644 --- a/pkg/test/loader/loader.go +++ b/pkg/test/loader/loader.go @@ -178,7 +178,19 @@ type ContainerContext struct { // ProcessContext contains information regarding the process that will run a test, or information about one of its // ancestors. type ProcessContext struct { - Name string `yaml:"name" validate:"required"` + // ExePath is the executable path. + ExePath string `yaml:"exePath" validate:"required"` + // Args is a string containing the space-separated list of command line arguments. If a single argument contains + // spaces, the entire argument must be quoted in order to not be considered as multiple arguments. If omitted or + // empty, it defaults to "". + Args *string `yaml:"args,omitempty" validate:"omitempty,min=1"` + // Exe is the argument in position 0 (a.k.a. argv[0]) of the process. If omitted or empty, it defaults to Name if + // this is specified; otherwise, it defaults to filepath.Base(ExePath). + Exe *string `yaml:"exe,omitempty" validate:"omitempty,min=1"` + // Name is the process name. If omitted or empty, it defaults to filepath.Base(ExePath). + Name *string `yaml:"name,omitempty" validate:"omitempty,min=1"` + // Env is the set of environment variables that must be provided to the process (in addition to the default ones). + Env map[string]string `yaml:"env,omitempty" validate:"omitempty,min=1"` } // TestResource describes a test resource. From 9ef438e56391d028d9642146235031e882c1cae9 Mon Sep 17 00:00:00 2001 From: Leonardo Di Giovanna Date: Wed, 30 Oct 2024 09:29:30 +0100 Subject: [PATCH 4/6] feat(decl/runners): add runners and runners builder declarations Signed-off-by: Leonardo Di Giovanna Co-authored-by: Aldo Lacuku --- pkg/test/runner/doc.go | 17 +++++++++++++ pkg/test/runner/runner.go | 53 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 pkg/test/runner/doc.go create mode 100644 pkg/test/runner/runner.go diff --git a/pkg/test/runner/doc.go b/pkg/test/runner/doc.go new file mode 100644 index 00000000..ba200917 --- /dev/null +++ b/pkg/test/runner/doc.go @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2024 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 runner provides the definition of a Runner and a runner Builder. +package runner diff --git a/pkg/test/runner/runner.go b/pkg/test/runner/runner.go new file mode 100644 index 00000000..1518d15c --- /dev/null +++ b/pkg/test/runner/runner.go @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2024 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 runner + +import ( + "context" + + "github.com/go-logr/logr" + + "github.com/falcosecurity/event-generator/pkg/test/loader" +) + +// Runner allows to run a test. +type Runner interface { + // Run runs the provided test. + Run(ctx context.Context, testIndex int, test *loader.Test) error +} + +// Builder allows to build a new test runner. +type Builder interface { + // Build builds a new test runner using the provided description. + Build(description *Description) (Runner, error) +} + +// Description contains information to build a new test runner. +type Description struct { + // Logger is the test runner logger. + Logger logr.Logger + // Type is the test runner type. + Type loader.TestRunnerType + // Environ is a list of strings representing the environment, in the form "key=value". + Environ []string + // TestConfigEnvKey is the key identifying the environment variable used to store the serialized test configuration. + TestConfigEnvKey string + // ProcIDEnvKey is the key identifying the environment variable used to store the process identifier in the form + // "test,child". + ProcIDEnvKey string + // ProcID is the current process ID. + ProcID string +} From 734a26c7b2126991e261f51cf4ec95d9a08515d4 Mon Sep 17 00:00:00 2001 From: Leonardo Di Giovanna Date: Wed, 30 Oct 2024 09:35:29 +0100 Subject: [PATCH 5/6] feat(decl/runners): add host runner Signed-off-by: Leonardo Di Giovanna Co-authored-by: Aldo Lacuku --- pkg/test/runner/host/doc.go | 17 +++ pkg/test/runner/host/host.go | 285 +++++++++++++++++++++++++++++++++++ 2 files changed, 302 insertions(+) create mode 100644 pkg/test/runner/host/doc.go create mode 100644 pkg/test/runner/host/host.go diff --git a/pkg/test/runner/host/doc.go b/pkg/test/runner/host/doc.go new file mode 100644 index 00000000..b0dab985 --- /dev/null +++ b/pkg/test/runner/host/doc.go @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2024 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 host provides an implementation of runner.Runner enabling test execution on the host system. +package host diff --git a/pkg/test/runner/host/host.go b/pkg/test/runner/host/host.go new file mode 100644 index 00000000..376b33af --- /dev/null +++ b/pkg/test/runner/host/host.go @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2024 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 host + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strconv" + "strings" + + "github.com/go-logr/logr" + + "github.com/falcosecurity/event-generator/pkg/test" + "github.com/falcosecurity/event-generator/pkg/test/loader" + "github.com/falcosecurity/event-generator/pkg/test/runner" +) + +// hostRunner is an implementation of runner.Runner enabling test execution on the host system. +type hostRunner struct { + // logger is the test runner logger. + logger logr.Logger + // environ is a list of strings representing the environment, in the form "key=value". + environ []string + // testBuilder is the builder used to build a test. + testBuilder test.Builder + // testConfigEnvKey is the key identifying the environment variable used to store the serialized test configuration. + testConfigEnvKey string + // procIDEnvKey is the key identifying the environment variable used to store the process identifier in the form + // "test,child". + procIDEnvKey string + // procID is the current process ID. + procID string +} + +// Verify that hostRunner implements runner.Runner interface. +var _ runner.Runner = (*hostRunner)(nil) + +// New creates a new host runner. +func New(logger logr.Logger, testBuilder test.Builder, environ []string, testConfigEnvKey, procIDEnvKey, + procID string) (runner.Runner, error) { + if testBuilder == nil { + return nil, fmt.Errorf("test builder must not be nil") + } + + if testConfigEnvKey == "" { + return nil, fmt.Errorf("testConfigEnvKey must not be empty") + } + + if procIDEnvKey == "" { + return nil, fmt.Errorf("procIDEnvKey must not be empty") + } + + r := &hostRunner{ + logger: logger, + testBuilder: testBuilder, + environ: environ, + testConfigEnvKey: testConfigEnvKey, + procIDEnvKey: procIDEnvKey, + procID: procID, + } + return r, nil +} + +func (r *hostRunner) Run(ctx context.Context, testIndex int, testDesc *loader.Test) error { + // Delegate to child process if we are not at the end of the chain. + if testDesc.Context != nil && len(testDesc.Context.Processes) != 0 { + if err := r.delegateToChild(ctx, testIndex, testDesc); err != nil { + return fmt.Errorf("error delegating to child process: %w", err) + } + + return nil + } + + // Build test. + testLogger := r.logger.WithName("test").WithValues("testName", testDesc.Name, + "testIndex", testIndex) + testInstance, err := r.testBuilder.Build(testLogger, testDesc) + if err != nil { + return fmt.Errorf("error building test: %w", err) + } + + // Run test. + if err := testInstance.Run(ctx); err != nil { + return fmt.Errorf("error running test: %w", err) + } + + return nil +} + +// delegateToChild delegates the execution of the test to a child process, created and tuned as per test specification. +func (r *hostRunner) delegateToChild(ctx context.Context, testIndex int, testDesc *loader.Test) error { + firstProcess := popFirstProcessContext(testDesc.Context) + + realExePath := firstProcess.ExePath + + // If the user provides a process name, we must run the executable through a symbolic link having the provided name + // and pointing to the real executable path. + exePath := realExePath + if name := firstProcess.Name; name != nil { + exePath = filepath.Join(filepath.Dir(realExePath), *name) + } + + // If the user provides the "exe" field, set the argument 0 of the new process to its value; otherwise defaults it + // to the last segment of the executable path. + arg0 := filepath.Base(exePath) + if exe := firstProcess.Exe; exe != nil { + arg0 = *exe + } + + // Evaluate process arguments. + procArgs := splitArgs(firstProcess.Args) + + // Evaluate process environment variables. + procEnv, err := r.buildEnv(testIndex, testDesc, firstProcess.Env) + if err != nil { + return fmt.Errorf("error building process environment variables set: %w", err) + } + + // Setup process command. + cmd := exec.CommandContext(ctx, exePath, procArgs...) //nolint:gosec // Disable G204 + cmd.Args[0] = arg0 + cmd.Env = procEnv + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // Create a hard link to the current process executable as specified by the user in the "ExePath" field. + currentExePath, err := getCurrentExePath() + if err != nil { + return fmt.Errorf("error retrieving the current process executable path: %w", err) + } + + if err := os.Link(currentExePath, realExePath); err != nil { + return fmt.Errorf("error creating process executable: %w", err) + } + defer func() { + if err := os.Remove(realExePath); err != nil { + r.logger.Error(err, "Error deleting process executable", "path", realExePath) + } + }() + + // If the user specified a custom process name, we will run the executable through a symbolic link, so create it. + if realExePath != exePath { + if err := os.Symlink(realExePath, exePath); err != nil { + return fmt.Errorf("error creating symlink %q to process executable %q: %w", exePath, realExePath, + err) + } + defer func() { + if err := os.Remove(exePath); err != nil { + r.logger.Error(err, "Error deleting symlink to process executable", "symlink", exePath) + } + }() + } + + // Run the child process and wait for it. + if err := cmd.Start(); err != nil { + return fmt.Errorf("error starting process: %w", err) + } + + if err := cmd.Wait(); err != nil { + return fmt.Errorf("error waiting for process: %w", err) + } + + return nil +} + +// popFirstProcessContext removes and returns the first process context from the provided testContext. +func popFirstProcessContext(testContext *loader.TestContext) *loader.ProcessContext { + processes := testContext.Processes + firstProcess := processes[0] + testContext.Processes = processes[1:] + return &firstProcess +} + +// splittingArgsRegex allows to split space-separated arguments, keeping together space-separated words under the same +// single- or double-quoted group. +var splittingArgsRegex = regexp.MustCompile(`"([^"]+)"|'([^']+)'|(\S+)`) + +// splitArgs splits the provided space-separated arguments. If a group composed of space-separated words must be +// considered as a single argument, it must be single- or double-quoted. +func splitArgs(args *string) []string { + if args == nil { + return nil + } + + matches := splittingArgsRegex.FindAllStringSubmatch(*args, -1) + splittedArgs := make([]string, len(matches)) + for matchIndex, match := range matches { + // match[1] is for double quotes, match[2] for single quotes, match[3] for unquoted. + if match[1] != "" { //nolint:gocritic // Rewrite this as switch statement worsens readability. + splittedArgs[matchIndex] = match[1] + } else if match[2] != "" { + splittedArgs[matchIndex] = match[2] + } else if match[3] != "" { + splittedArgs[matchIndex] = match[3] + } + } + return splittedArgs +} + +func (r *hostRunner) buildEnv(testIndex int, testDesc *loader.Test, userEnv map[string]string) ([]string, error) { + // Use the current process environment variable as base. + env := r.environ + + // Add the user-provided environment variable. + for key, value := range userEnv { + env = append(env, fmt.Sprintf("%s=%s", key, value)) + } + + // Set test config environment variable to the serialized test configuration. + config, err := marshalTestConfig(testDesc) + if err != nil { + return nil, fmt.Errorf("error serializing new test configuration: %w", err) + } + env = append(env, buildEnvVar(r.testConfigEnvKey, config)) + + // Set process ID environment variable. + procID, err := r.buildProcID(testIndex) + if err != nil { + return nil, fmt.Errorf("error building process ID: %w", err) + } + env = append(env, buildEnvVar(r.procIDEnvKey, procID)) + + return env, nil +} + +// buildEnvVar creates an environment variable string in the form "=". +func buildEnvVar(envKey, envValue string) string { + return fmt.Sprintf("%s=%s", envKey, envValue) +} + +// marshalTestConfig returns the serialized content of a test configuration object containing only the provided test. +func marshalTestConfig(testDesc *loader.Test) (string, error) { + conf := &loader.Configuration{Tests: []loader.Test{*testDesc}} + sb := &strings.Builder{} + if err := conf.Write(sb); err != nil { + return "", err + } + + return sb.String(), nil +} + +// buildProcID builds a process ID. If the current process ID is not defined, it uses the provided testIndex to create a +// new one; otherwise, given the process ID in the form testName,child, it returns +// testName,child. +func (r *hostRunner) buildProcID(testIndex int) (string, error) { + procID := r.procID + if procID == "" { + return fmt.Sprintf("test%d,child0", testIndex), nil + } + + idParts := strings.Split(procID, ",") + if len(idParts) != 2 { + return "", fmt.Errorf("cannot parse process ID") + } + + testName, procName := idParts[0], idParts[1] + childIndex, err := strconv.Atoi(strings.TrimPrefix(procName, "child")) + if err != nil { + return "", fmt.Errorf("error parsing process name in process ID %q: %w", procName, err) + } + + return fmt.Sprintf("%s,child%d", testName, childIndex+1), nil +} + +// getCurrentExePath retrieves the current process executable path. +func getCurrentExePath() (string, error) { + return os.Readlink(fmt.Sprintf("/proc/%d/exe", os.Getpid())) +} From 96c8d08e249c58ee0fed3d7870eb46ae7f323437 Mon Sep 17 00:00:00 2001 From: Leonardo Di Giovanna Date: Wed, 30 Oct 2024 09:38:23 +0100 Subject: [PATCH 6/6] feat(decl/runners): add runner builder Signed-off-by: Leonardo Di Giovanna Co-authored-by: Aldo Lacuku --- pkg/test/runner/builder/builder.go | 62 ++++++++++++++++++++++++++++++ pkg/test/runner/builder/doc.go | 17 ++++++++ 2 files changed, 79 insertions(+) create mode 100644 pkg/test/runner/builder/builder.go create mode 100644 pkg/test/runner/builder/doc.go diff --git a/pkg/test/runner/builder/builder.go b/pkg/test/runner/builder/builder.go new file mode 100644 index 00000000..5b6c21f2 --- /dev/null +++ b/pkg/test/runner/builder/builder.go @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2024 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 builder + +import ( + "fmt" + + "github.com/falcosecurity/event-generator/pkg/test" + "github.com/falcosecurity/event-generator/pkg/test/loader" + "github.com/falcosecurity/event-generator/pkg/test/runner" + "github.com/falcosecurity/event-generator/pkg/test/runner/host" +) + +// builder is an implementation of runner.Builder. +type builder struct { + // testBuilder is the builder used to build a test. + testBuilder test.Builder +} + +// Verify that builder implements runner.Builder interface. +var _ runner.Builder = (*builder)(nil) + +// New creates a new builder. +func New(testBuilder test.Builder) (runner.Builder, error) { + if testBuilder == nil { + return nil, fmt.Errorf("test builder must not be nil") + } + + b := &builder{testBuilder: testBuilder} + return b, nil +} + +func (b *builder) Build(description *runner.Description) (runner.Runner, error) { + runnerType := description.Type + logger := description.Logger.WithValues("runnerType", runnerType) + switch runnerType { + case loader.TestRunnerTypeHost: + return host.New( + logger, + b.testBuilder, + description.Environ, + description.TestConfigEnvKey, + description.ProcIDEnvKey, + description.ProcID, + ) + default: + return nil, fmt.Errorf("unknown test runner type %q", runnerType) + } +} diff --git a/pkg/test/runner/builder/doc.go b/pkg/test/runner/builder/doc.go new file mode 100644 index 00000000..4a20464d --- /dev/null +++ b/pkg/test/runner/builder/doc.go @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2024 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 builder provides an implementation of runner.Builder. +package builder