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): add support for running tests in containers #244

Open
wants to merge 8 commits into
base: declarative-testing
Choose a base branch
from
13 changes: 8 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
FROM golang:1.23.1-alpine3.20 AS builder
FROM golang:1.23.1-bookworm AS builder

LABEL maintainer="[email protected]"

RUN apk add --no-cache make bash
RUN apt-get -y update && apt-get install -y make bash && apt clean -y && rm -rf /var/lib/apt/lists/*

WORKDIR /event-generator

Expand All @@ -11,10 +11,13 @@ COPY . .
RUN make


FROM alpine:3.20
FROM debian:bookworm-slim

RUN apk add --no-cache sudo polkit libcap e2fsprogs-extra openssh nmap netcat-openbsd wget curl
RUN apt-get -y update && \
apt-get -y install policykit-1 libcap-dev e2fsprogs openssh-client openssh-server nmap netcat-openbsd wget \
curl && \
apt clean -y && rm -rf /var/lib/apt/lists/*

COPY --from=builder /event-generator/event-generator /bin/event-generator
COPY --from=builder --chmod=0755 /event-generator/event-generator /bin/event-generator

ENTRYPOINT ["/bin/event-generator"]
65 changes: 46 additions & 19 deletions cmd/declarative/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import (
"time"

"github.com/spf13/cobra"
"github.com/thediveo/enumflag"

"github.com/falcosecurity/event-generator/pkg/container/builder"
)

const (
Expand All @@ -31,8 +34,8 @@ const (
DescriptionFlagName = "description"
// TestIDFlagName is the name of the flag allowing to specify the test identifier.
TestIDFlagName = "test-id"
// ProcLabelFlagName is the name of the flag allowing to specify a process label.
ProcLabelFlagName = "proc-label"
// LabelsFlagName is the name of the flag allowing to specify labels.
LabelsFlagName = "labels"
// TimeoutFlagName is the name of the flag allowing to specify the test timeout.
TimeoutFlagName = "timeout"
)
Expand All @@ -48,8 +51,8 @@ type Config struct {
DescriptionEnvKey string
// TestIDEnvKey is the environment variable key corresponding to TestIDFlagName.
TestIDEnvKey string
// ProcLabelEnvKey is the environment variable key corresponding to ProcLabelFlagName.
ProcLabelEnvKey string
// LabelsEnvKey is the environment variable key corresponding to LabelsFlagName.
LabelsEnvKey string
// TimeoutEnvKey is the environment variable key corresponding to TimeoutFlagName.
TimeoutEnvKey string

Expand All @@ -65,14 +68,29 @@ type Config struct {
// - the root process has no test ID
// - the processes in the process chain but the last have the test ID in the form testIDIgnorePrefix<testUID>
// - the last process in the process chain has the test ID in the form <testUID>
// A process having a test ID in the form <testUID> (i.e.: the leaf process) is the only one that is monitored.
TestID string
// ProcLabel is the process label in the form test<testIndex>.child<childIndex>. It is used for logging purposes
// and to potentially generate the child process label.
ProcLabel string
// TestsTimeout is the maximal duration of the tests. If running tests lasts more than TestsTimeout, the execution
// of all pending tasks is canceled.
TestsTimeout time.Duration
// ContainerRuntimeUnixSocketPath is the unix socket path of the local container runtime.
ContainerRuntimeUnixSocketPath string
// ContainerBaseImageName is the event-generator base image to generate new containers.
ContainerBaseImageName string
// ContainerImagePullPolicy is container image pull policy.
ContainerImagePullPolicy builder.ImagePullPolicy
//
// Hidden flags
//
// A process having a test ID in the form <testUID> (i.e.: the leaf process) is the only one that is monitored.
TestID string
// Labels is the string containing the comma-separated list of labels in the form <labelX>=<labelXValue>. It is used
// for logging purposes and to potentially generate the child process/container labels.
Labels string
}

var containerImagePullPolicies = map[builder.ImagePullPolicy][]string{
builder.ImagePullPolicyAlways: {"always"},
builder.ImagePullPolicyNever: {"never"},
builder.ImagePullPolicyIfNotPresent: {"ifnotpresent"},
}

// New creates a new config linked to the provided command.
Expand All @@ -83,7 +101,7 @@ func New(cmd *cobra.Command, declarativeEnvKey, envKeysPrefix string) *Config {
DescriptionFileEnvKey: envKeyFromFlagName(envKeysPrefix, DescriptionFileFlagName),
DescriptionEnvKey: envKeyFromFlagName(envKeysPrefix, DescriptionFlagName),
TestIDEnvKey: envKeyFromFlagName(envKeysPrefix, TestIDFlagName),
ProcLabelEnvKey: envKeyFromFlagName(envKeysPrefix, ProcLabelFlagName),
LabelsEnvKey: envKeyFromFlagName(envKeysPrefix, LabelsFlagName),
TimeoutEnvKey: envKeyFromFlagName(envKeysPrefix, TimeoutFlagName),
}
commonConf.initFlags(cmd)
Expand All @@ -99,19 +117,28 @@ func (c *Config) initFlags(cmd *cobra.Command) {
flags.StringVarP(&c.TestsDescription, DescriptionFlagName, "d", "",
"The YAML-formatted tests description string specifying the tests to be run")
cmd.MarkFlagsMutuallyExclusive(DescriptionFileFlagName, DescriptionFlagName)
flags.DurationVarP(&c.TestsTimeout, TimeoutFlagName, "t", time.Minute,
"The maximal duration of the tests. If running tests lasts more than testsTimeout, the execution of "+
"all pending tasks is canceled")

// Container runtime flags.
flags.StringVar(&c.ContainerRuntimeUnixSocketPath, "container-runtime-unix-socket",
"/run/containerd/containerd.sock", "The unix socket path of the local container runtime")
flags.StringVar(&c.ContainerBaseImageName, "container-base-image",
"docker.io/falcosecurity/event-generator:latest", "The event-generator base image to generate new containers")
flags.Var(enumflag.New(&c.ContainerImagePullPolicy, "container-image-pull-policy", containerImagePullPolicies,
enumflag.EnumCaseInsensitive), "container-image-pull-policy",
"The container image pull policy; can be 'always', 'never' or 'ifnotpresent'")

// Hidden flags.
flags.StringVar(&c.TestID, TestIDFlagName, "",
"(used during process chain building) The test identifier in the form <ignorePrefix><testUID>. It is "+
"used to propagate the test UID to child processes in the process chain")
flags.StringVar(&c.ProcLabel, ProcLabelFlagName, "",
"(used during process chain building) The process label in the form test<testIndex>,child<childIndex>. "+
"It is used for logging purposes and to potentially generate the child process label")
"used to propagate the test UID to child processes/container in the process chain")
flags.StringVar(&c.Labels, LabelsFlagName, "",
"(used during process chain building) The list of comma-separated labels in the form <labelX>=<labelXValue>. "+
"It is used for logging purposes and to potentially generate the child process/container labels")
_ = flags.MarkHidden(TestIDFlagName)
_ = flags.MarkHidden(ProcLabelFlagName)

flags.DurationVarP(&c.TestsTimeout, TimeoutFlagName, "t", time.Minute,
"The maximal duration of the tests. If running tests lasts more than testsTimeout, the execution of "+
"all pending tasks is canceled")
_ = flags.MarkHidden(LabelsFlagName)
}

// envKeyFromFlagName converts the provided flag name into the corresponding environment variable key.
Expand Down
105 changes: 47 additions & 58 deletions cmd/declarative/test/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
Expand All @@ -35,6 +33,8 @@ import (

"github.com/falcosecurity/event-generator/cmd/declarative/config"
"github.com/falcosecurity/event-generator/pkg/alert/retriever/grpcretriever"
containerbuilder "github.com/falcosecurity/event-generator/pkg/container/builder"
"github.com/falcosecurity/event-generator/pkg/label"
"github.com/falcosecurity/event-generator/pkg/test"
testbuilder "github.com/falcosecurity/event-generator/pkg/test/builder"
"github.com/falcosecurity/event-generator/pkg/test/loader"
Expand Down Expand Up @@ -180,17 +180,13 @@ func (cw *CommandWrapper) run(cmd *cobra.Command, _ []string) {
testID := cw.TestID
isRootProcess := testID == ""

procLabelInfo, err := parseProcLabel(cw.ProcLabel)
labels, err := label.ParseSet(cw.Labels)
if err != nil {
logger.Error(err, "Error parsing process label")
logger.Error(err, "Error parsing labels")
exitAndCancel()
}

if procLabelInfo == nil {
logger = logger.WithName("root")
} else {
logger = logger.WithName(procLabelInfo.testName).WithName(procLabelInfo.childName)
}
logger = enrichLogger(logger, labels)

description, err := loadTestsDescription(logger, cw.TestsDescriptionFile, cw.TestsDescription)
if err != nil {
Expand All @@ -217,7 +213,18 @@ func (cw *CommandWrapper) run(cmd *cobra.Command, _ []string) {
exitAndCancel()
}

runnerBuilder, err := runnerbuilder.New(testBuilder)
containerBuilderOptions := []containerbuilder.Option{
containerbuilder.WithUnixSocketPath(cw.ContainerRuntimeUnixSocketPath),
containerbuilder.WithBaseImageName(cw.ContainerBaseImageName),
containerbuilder.WithBaseImagePullPolicy(cw.ContainerImagePullPolicy),
}
containerBuilder, err := containerbuilder.New(containerBuilderOptions...)
if err != nil {
logger.Error(err, "Error creating container builder")
exitAndCancel()
}

runnerBuilder, err := runnerbuilder.New(testBuilder, containerBuilder)
if err != nil {
logger.Error(err, "Error creating runner builder")
exitAndCancel()
Expand All @@ -244,15 +251,21 @@ func (cw *CommandWrapper) run(cmd *cobra.Command, _ []string) {
// Prepare parameters shared by runners.
runnerLogger := logger.WithName("runner")
runnerEnviron := cw.buildRunnerEnviron(cmd)
var runnerProcLabel string
if procLabelInfo != nil {
runnerProcLabel = fmt.Sprintf("%s,%s", procLabelInfo.testName, procLabelInfo.childName)
var runnerLabels *label.Set
if labels != nil {
runnerLabels = labels.Clone()
}

// Build and run the tests.
for testIndex := range description.Tests {
testDesc := &description.Tests[testIndex]

// If this process belongs to a test process chain, override the logged test index in order to match its
// absolute index among all the test descriptions.
if labels != nil {
testIndex = labels.TestIndex
}

logger := logger.WithValues("testName", testDesc.Name, "testIndex", testIndex)

var testUID uuid.UUID
Expand All @@ -266,7 +279,7 @@ func (cw *CommandWrapper) run(cmd *cobra.Command, _ []string) {
if testDesc.Context == nil {
testDesc.Context = &loader.TestContext{}
}
if len(testDesc.Context.Processes) == 0 {
if len(testDesc.Context.Processes) == 0 && testDesc.Context.Container == nil {
testDesc.Context.Processes = []loader.ProcessContext{{}}
}
} else {
Expand All @@ -280,28 +293,23 @@ func (cw *CommandWrapper) run(cmd *cobra.Command, _ []string) {
}

logger = logger.WithValues("testUid", testUID)
runnerLogger := runnerLogger.WithValues("testName", testDesc.Name, "testIndex", testIndex, "testUid", testUID)

runnerDescription := &runner.Description{
Environ: runnerEnviron,
TestDescriptionEnvKey: cw.DescriptionEnvKey,
TestDescriptionFileEnvKey: cw.DescriptionFileEnvKey,
TestIDEnvKey: cw.TestIDEnvKey,
TestIDIgnorePrefix: testIDIgnorePrefix,
ProcLabelEnvKey: cw.ProcLabelEnvKey,
ProcLabel: runnerProcLabel,
LabelsEnvKey: cw.LabelsEnvKey,
Labels: runnerLabels,
}
runnerInstance, err := runnerBuilder.Build(testDesc.Runner, runnerLogger, runnerDescription)
if err != nil {
logger.Error(err, "Error creating runner")
exitAndCancel()
}

// If this process belongs to a test process chain, override the logged test index in order to match its
// absolute index among all the test descriptions.
if !isRootProcess {
testIndex = procLabelInfo.testIndex
}

logger.Info("Starting test execution...")

if err := runnerInstance.Run(ctx, testID, testIndex, testDesc); err != nil {
Expand Down Expand Up @@ -340,45 +348,26 @@ func (cw *CommandWrapper) run(cmd *cobra.Command, _ []string) {
testerWaitGroup.Wait()
}

var (
// procLabelRegex defines the process label format and allows to extract the embedded test and child indexes.
procLabelRegex = regexp.MustCompile(`^test(\d+),child(\d+)$`)
errProcLabelRegex = fmt.Errorf("process label must comply with %q regex", procLabelRegex.String())
)

// processLabelInfo contains information regarding the process label.
type processLabelInfo struct {
testName string
testIndex int
childName string
childIndex int
}

// parseProcLabel parses the process label and returns information on it.
func parseProcLabel(procLabel string) (*processLabelInfo, error) {
if procLabel == "" {
return nil, nil
// enrichLogger creates a new logger, starting from the provided one, with the information extracted from the provided
// labels.
func enrichLogger(logger logr.Logger, labels *label.Set) logr.Logger {
if labels == nil {
return logger.WithName("root")
}

match := procLabelRegex.FindStringSubmatch(procLabel)
if match == nil {
return nil, errProcLabelRegex
}

// No errors can occur, since we have already verified through regex that they are numbers.
testIndex, _ := strconv.Atoi(match[1])
childIndex, _ := strconv.Atoi(match[2])

parts := strings.Split(procLabel, ",")

procLabelInfo := &processLabelInfo{
testName: parts[0],
testIndex: testIndex,
childName: parts[1],
childIndex: childIndex,
testName := fmt.Sprintf("test%d", labels.TestIndex)
logger = logger.WithName(testName)
if labels.IsContainer {
logger = logger.WithName("cont")
if imageName := labels.ImageName; imageName != "" {
logger = logger.WithValues("imageName", imageName)
}
if containerName := labels.ContainerName; containerName != "" {
logger = logger.WithValues("containerName", containerName)
}
}

return procLabelInfo, nil
procName := fmt.Sprintf("proc%d", labels.ProcIndex)
return logger.WithName(procName)
}

// loadTestsDescription loads the YAML tests description from a different source, depending on the content of the
Expand Down
Loading