Skip to content
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

feat(decl/runners): add host runner and test serialization support #236

Merged
merged 6 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 7 additions & 4 deletions pkg/test/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
167 changes: 152 additions & 15 deletions pkg/test/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"`
}
Expand Down Expand Up @@ -155,20 +165,32 @@ 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
// 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.
Expand Down Expand Up @@ -209,6 +231,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

Expand Down Expand Up @@ -451,6 +563,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

Expand Down Expand Up @@ -587,10 +724,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"`
}
4 changes: 2 additions & 2 deletions pkg/test/resource/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/test/resource/clientserver/clientserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 62 additions & 0 deletions pkg/test/runner/builder/builder.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
17 changes: 17 additions & 0 deletions pkg/test/runner/builder/doc.go
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions pkg/test/runner/doc.go
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions pkg/test/runner/host/doc.go
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading