From c633ae4e0632edf94dc3bd02cf2da0081463d51d Mon Sep 17 00:00:00 2001 From: Easton Crupper <65553218+ecrupper@users.noreply.github.com> Date: Wed, 11 Sep 2024 10:23:04 -0400 Subject: [PATCH] feat: container outputs and dynamic environments (#591) * init commit * yep * more work * status reporting * add testing * remove assemble build priv image test and fix compose * k8s sub + no sub secret injection during createstep * address some linter comments * linter overlord * fix error handling in runtime for outputs ctn * correct pull policy and update secret masking * move polling in stages to account for needs key * update polling calls and add environment vars for outputs paths * gci imports and splitN * use hashicorp envparse over toMap --- cmd/vela-worker/exec.go | 5 + cmd/vela-worker/run.go | 13 + docker-compose.yml | 1 + executor/flags.go | 6 + executor/linux/build.go | 164 ++-- executor/linux/build_test.go | 841 +----------------- executor/linux/linux.go | 5 +- executor/linux/opts.go | 11 + executor/linux/outputs.go | 150 ++++ executor/linux/outputs_test.go | 469 ++++++++++ executor/linux/secret.go | 2 +- executor/linux/service.go | 16 + executor/linux/service_test.go | 16 + executor/linux/stage.go | 37 + executor/linux/stage_test.go | 1 + executor/linux/step.go | 48 +- executor/linux/step_test.go | 24 +- executor/setup.go | 3 + go.mod | 3 +- go.sum | 6 +- internal/image/image.go | 56 +- internal/image/image_test.go | 58 +- internal/step/environment.go | 2 + runtime/docker/container.go | 68 +- runtime/docker/container_test.go | 45 + runtime/docker/image.go | 14 +- runtime/engine.go | 3 + runtime/kubernetes/container.go | 17 +- .../clientset/versioned/clientset.go | 3 +- .../versioned/fake/clientset_generated.go | 7 +- .../clientset/versioned/fake/register.go | 3 +- .../clientset/versioned/scheme/register.go | 3 +- .../fake/fake_pipelinepodstemplate.go | 3 +- .../vela/v1alpha1/fake/fake_vela_client.go | 3 +- .../vela/v1alpha1/pipelinepodstemplate.go | 5 +- .../typed/vela/v1alpha1/vela_client.go | 3 +- 36 files changed, 1086 insertions(+), 1028 deletions(-) create mode 100644 executor/linux/outputs.go create mode 100644 executor/linux/outputs_test.go diff --git a/cmd/vela-worker/exec.go b/cmd/vela-worker/exec.go index a1568d77..99a319ea 100644 --- a/cmd/vela-worker/exec.go +++ b/cmd/vela-worker/exec.go @@ -5,6 +5,7 @@ package main import ( "context" "encoding/json" + "fmt" "net/http" "sync" "time" @@ -145,6 +146,9 @@ func (w *Worker) exec(index int, config *api.Worker) error { break } + // set the outputs container ID + w.Config.Executor.OutputCtn.ID = fmt.Sprintf("outputs_%s", p.ID) + // create logger with extra metadata // // https://pkg.go.dev/github.com/sirupsen/logrus#WithFields @@ -236,6 +240,7 @@ func (w *Worker) exec(index int, config *api.Worker) error { Build: item.Build, Pipeline: p.Sanitize(w.Config.Runtime.Driver), Version: v.Semantic(), + OutputCtn: w.Config.Executor.OutputCtn, }) // add the executor to the worker diff --git a/cmd/vela-worker/run.go b/cmd/vela-worker/run.go index 20ba16a4..a0ff3cef 100644 --- a/cmd/vela-worker/run.go +++ b/cmd/vela-worker/run.go @@ -14,6 +14,8 @@ import ( api "github.com/go-vela/server/api/types" "github.com/go-vela/server/queue" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/pipeline" "github.com/go-vela/worker/executor" "github.com/go-vela/worker/runtime" ) @@ -73,6 +75,16 @@ func run(c *cli.Context) error { return fmt.Errorf("unable to parse address: %w", err) } + outputsCtn := new(pipeline.Container) + if len(c.String("executor.outputs-image")) > 0 { + outputsCtn = &pipeline.Container{ + Detach: true, + Image: c.String("executor.outputs-image"), + Environment: make(map[string]string), + Pull: constants.PullNotPresent, + } + } + // create the worker w := &Worker{ // worker configuration @@ -94,6 +106,7 @@ func run(c *cli.Context) error { MaxLogSize: c.Uint("executor.max_log_size"), LogStreamingTimeout: c.Duration("executor.log_streaming_timeout"), EnforceTrustedRepos: c.Bool("executor.enforce-trusted-repos"), + OutputCtn: outputsCtn, }, // logger configuration Logger: &Logger{ diff --git a/docker-compose.yml b/docker-compose.yml index d756b38d..851544d1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,6 +33,7 @@ services: VELA_SERVER_SECRET: 'zB7mrKDTZqNeNTD8z47yG4DHywspAh' WORKER_ADDR: 'http://worker:8080' WORKER_CHECK_IN: 2m + VELA_EXECUTOR_OUTPUTS_IMAGE: 'alpine:latest' restart: always ports: - "8081:8080" diff --git a/executor/flags.go b/executor/flags.go index 9850c322..946a7b2f 100644 --- a/executor/flags.go +++ b/executor/flags.go @@ -43,4 +43,10 @@ var Flags = []cli.Flag{ Usage: "enforce trusted repo restrictions for privileged images", Value: true, }, + &cli.StringFlag{ + EnvVars: []string{"VELA_EXECUTOR_OUTPUTS_IMAGE", "EXECUTOR_OUTPUTS_IMAGE"}, + FilePath: "/vela/executor/outputs_image", + Name: "executor.outputs-image", + Usage: "image used for the outputs container sidecar", + }, } diff --git a/executor/linux/build.go b/executor/linux/build.go index ed87ed95..67dd3c95 100644 --- a/executor/linux/build.go +++ b/executor/linux/build.go @@ -215,7 +215,7 @@ func (c *client) PlanBuild(ctx context.Context) error { // AssembleBuild prepares the containers within a build for execution. // -//nolint:gocyclo,funlen // ignore cyclomatic complexity and function length due to comments and logging messages +//nolint:funlen // consider abstracting parts here but for now this is fine func (c *client) AssembleBuild(ctx context.Context) error { // defer taking a snapshot of the build // @@ -329,9 +329,10 @@ func (c *client) AssembleBuild(ctx context.Context) error { continue } + c.Logger.Infof("creating %s step", s.Name) + _log.AppendData([]byte(fmt.Sprintf("> Preparing step image %s...\n", s.Image))) - c.Logger.Infof("creating %s step", s.Name) // create the step c.err = c.CreateStep(ctx, s) if c.err != nil { @@ -364,6 +365,18 @@ func (c *client) AssembleBuild(ctx context.Context) error { continue } + // verify secret image is allowed to run + if c.enforceTrustedRepos { + priv, err := image.IsPrivilegedImage(s.Origin.Image, c.privilegedImages) + if err != nil { + return err + } + + if priv && !c.build.GetRepo().GetTrusted() { + return fmt.Errorf("attempting to use privileged image (%s) as untrusted repo", s.Origin.Image) + } + } + c.Logger.Infof("creating %s secret", s.Origin.Name) // create the service c.err = c.secret.create(ctx, s.Origin) @@ -384,92 +397,11 @@ func (c *client) AssembleBuild(ctx context.Context) error { // https://pkg.go.dev/github.com/go-vela/types/library#Log.AppendData _log.AppendData(image) } - // enforce repo.trusted is set for pipelines containing privileged images - // if not enforced, allow all that exist in the list of runtime privileged images - // this configuration is set as an executor flag - if c.enforceTrustedRepos { - // group steps services stages and secret origins together - containers := c.pipeline.Steps - - containers = append(containers, c.pipeline.Services...) - for _, stage := range c.pipeline.Stages { - containers = append(containers, stage.Steps...) - } - - for _, secret := range c.pipeline.Secrets { - containers = append(containers, secret.Origin) - } - - // assume no privileged images are in use - containsPrivilegedImages := false - privImages := []string{} - - // verify all pipeline containers - for _, container := range containers { - // TODO: remove hardcoded reference - if container.Image == "#init" { - continue - } - - // skip over non-plugin secrets origins - if container.Empty() { - continue - } - - c.Logger.Infof("verifying privileges for container %s", container.Name) - - // update the init log with image info - // - // https://pkg.go.dev/github.com/go-vela/types/library#Log.AppendData - _log.AppendData([]byte(fmt.Sprintf("Verifying privileges for image %s...\n", container.Image))) - - for _, pattern := range c.privilegedImages { - // check if image matches privileged pattern - privileged, err := image.IsPrivilegedImage(container.Image, pattern) - if err != nil { - // wrap the error - c.err = fmt.Errorf("unable to verify privileges for image %s: %w", container.Image, err) - - // update the init log with image info - // - // https://pkg.go.dev/github.com/go-vela/types/library#Log.AppendData - _log.AppendData([]byte(fmt.Sprintf("ERROR: %s\n", c.err.Error()))) - - // return error and destroy the build - // ignore checking more images - return c.err - } - - if privileged { - // pipeline contains at least one privileged image - containsPrivilegedImages = privileged - - privImages = append(privImages, container.Image) - } - } - - // update the init log with image info - // - // https://pkg.go.dev/github.com/go-vela/types/library#Log.AppendData - _log.AppendData([]byte(fmt.Sprintf("Privileges verified for image %s\n", container.Image))) - } - - localBool := c.build.GetRepo().GetTrusted() - - // ensure pipelines containing privileged images are only permitted to run by trusted repos - if (containsPrivilegedImages) && !localBool { - // update error including privileged image - c.err = fmt.Errorf("unable to assemble build. pipeline contains privileged images and repo is not trusted. privileged image: %v", privImages) - - // update the init log with image info - // - // https://pkg.go.dev/github.com/go-vela/types/library#Log.AppendData - _log.AppendData([]byte(fmt.Sprintf("ERROR: %s\n", c.err.Error()))) - - // return error and destroy the build - return c.err - } + // create outputs container with a timeout equal to the repo timeout + c.err = c.outputs.create(ctx, c.OutputCtn, (int64(60) * c.build.GetRepo().GetTimeout())) + if c.err != nil { + return fmt.Errorf("unable to create outputs container: %w", c.err) } // inspect the runtime build (eg a kubernetes pod) for the pipeline @@ -502,6 +434,8 @@ func (c *client) AssembleBuild(ctx context.Context) error { } // ExecBuild runs a pipeline for a build. +// +//nolint:funlen // there is a lot going on here and will probably always be long func (c *client) ExecBuild(ctx context.Context) error { defer func() { // Exec* calls are responsible for sending StreamRequest messages. @@ -515,6 +449,18 @@ func (c *client) ExecBuild(ctx context.Context) error { build.Upload(c.build, c.Vela, c.err, c.Logger) }() + // output maps for dynamic environment variables captured from volume + var opEnv, maskEnv map[string]string + + // fire up output container to run with the build + c.Logger.Infof("creating outputs container %s", c.OutputCtn.ID) + + // execute outputs container + c.err = c.outputs.exec(ctx, c.OutputCtn) + if c.err != nil { + return fmt.Errorf("unable to exec outputs container: %w", c.err) + } + c.Logger.Info("executing secret images") // execute the secret c.err = c.secret.exec(ctx, &c.pipeline.Secrets) @@ -571,6 +517,42 @@ func (c *client) ExecBuild(ctx context.Context) error { return fmt.Errorf("unable to plan step: %w", c.err) } + // poll outputs + opEnv, maskEnv, c.err = c.outputs.poll(ctx, c.OutputCtn) + if c.err != nil { + return fmt.Errorf("unable to exec outputs container: %w", c.err) + } + + // merge env from outputs + // + //nolint:errcheck // only errors with empty environment input, which does not matter here + _step.MergeEnv(opEnv) + + // merge env from masked outputs + // + //nolint:errcheck // only errors with empty environment input, which does not matter here + _step.MergeEnv(maskEnv) + + // add masked outputs to secret map so they can be masked in logs + for key := range maskEnv { + sec := &pipeline.StepSecret{ + Target: key, + } + _step.Secrets = append(_step.Secrets, sec) + } + + // perform any substitution on dynamic variables + err = _step.Substitute() + if err != nil { + return err + } + + // inject no-substitution secrets for container + err = injectSecrets(_step, c.NoSubSecrets) + if err != nil { + return err + } + c.Logger.Infof("executing %s step", _step.Name) // execute the step c.err = c.ExecStep(ctx, _step) @@ -706,6 +688,8 @@ func (c *client) StreamBuild(ctx context.Context) error { // loadLazySecrets is a helper function that injects secrets // into the container right before execution, rather than // during build planning. It is only available for the Docker runtime. +// +//nolint:funlen // explanation takes up a lot of lines func loadLazySecrets(c *client, _step *pipeline.Container) error { _log := new(library.Log) @@ -941,6 +925,12 @@ func (c *client) DestroyBuild(ctx context.Context) error { } } + // destroy output container + err = c.outputs.destroy(ctx, c.OutputCtn) + if err != nil { + c.Logger.Errorf("unable to destroy output container: %v", err) + } + c.Logger.Info("deleting volume") // remove the runtime volume for the pipeline err = c.Runtime.RemoveVolume(ctx, c.pipeline) diff --git a/executor/linux/build_test.go b/executor/linux/build_test.go index aac6bd28..9c758239 100644 --- a/executor/linux/build_test.go +++ b/executor/linux/build_test.go @@ -216,836 +216,6 @@ func TestLinux_CreateBuild(t *testing.T) { } } -func TestLinux_AssembleBuild_EnforceTrustedRepos(t *testing.T) { - // setup types - set := flag.NewFlagSet("test", 0) - set.String("clone-image", "target/vela-git:latest", "doc") - compiler, _ := native.FromCLIContext(cli.NewContext(nil, set, nil)) - - _build := testBuild() - - // setting mock build for testing dynamic environment tags - _buildWithMessageAlpine := testBuild() - _buildWithMessageAlpine.SetMessage("alpine") - - // test repo is not trusted by default - _untrustedRepo := testRepo() - - // to be matched with the image used by testdata/build/steps/basic.yml - _privilegedImagesStepsPipeline := []string{"alpine"} - // to be matched with the image used by testdata/build/services/basic.yml - _privilegedImagesServicesPipeline := []string{"postgres"} - // to be matched with the image used by testdata/build/stages/basic.yml - _privilegedImagesStagesPipeline := []string{"alpine"} - // create trusted repo - _trustedRepo := testRepo() - _trustedRepo.SetTrusted(true) - - gin.SetMode(gin.TestMode) - - s := httptest.NewServer(server.FakeHandler()) - - _client, err := vela.NewClient(s.URL, "", nil) - if err != nil { - t.Errorf("unable to create Vela API client: %v", err) - } - - tests := []struct { - name string - failure bool - runtime string - build *api.Build - repo *api.Repo - pipeline string - privilegedImages []string - enforceTrustedRepos bool - }{ - { - name: "docker-enforce trusted repos enabled: privileged steps pipeline with trusted repo", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/steps/basic.yml", - privilegedImages: _privilegedImagesStepsPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: privileged steps pipeline with untrusted repo", - failure: true, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/steps/basic.yml", - privilegedImages: _privilegedImagesStepsPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: non-privileged steps pipeline with trusted repo", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/steps/basic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: non-privileged steps pipeline with untrusted repo", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/steps/basic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos disabled: privileged steps pipeline with trusted repo", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/steps/basic.yml", - privilegedImages: _privilegedImagesStepsPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: privileged steps pipeline with untrusted repo", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/steps/basic.yml", - privilegedImages: _privilegedImagesStepsPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: non-privileged steps pipeline with trusted repo", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/steps/basic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: non-privileged steps pipeline with untrusted repo", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/steps/basic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos enabled: privileged steps pipeline with trusted repo and dynamic image:tag", - failure: false, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _trustedRepo, - pipeline: "testdata/build/steps/img_environmentdynamic.yml", - privilegedImages: _privilegedImagesStepsPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: privileged steps pipeline with untrusted repo and dynamic image:tag", - failure: true, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _untrustedRepo, - pipeline: "testdata/build/steps/img_environmentdynamic.yml", - privilegedImages: _privilegedImagesStepsPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: non-privileged steps pipeline with trusted repo and dynamic image:tag", - failure: false, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _trustedRepo, - pipeline: "testdata/build/steps/img_environmentdynamic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: non-privileged steps pipeline with untrusted repo and dynamic image:tag", - failure: false, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _untrustedRepo, - pipeline: "testdata/build/steps/img_environmentdynamic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos disabled: privileged steps pipeline with trusted repo and dynamic image:tag", - failure: false, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _trustedRepo, - pipeline: "testdata/build/steps/img_environmentdynamic.yml", - privilegedImages: _privilegedImagesStepsPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: privileged steps pipeline with untrusted repo and dynamic image:tag", - failure: false, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _untrustedRepo, - pipeline: "testdata/build/steps/img_environmentdynamic.yml", - privilegedImages: _privilegedImagesStepsPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: non-privileged steps pipeline with trusted repo and dynamic image:tag", - failure: false, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _trustedRepo, - pipeline: "testdata/build/steps/img_environmentdynamic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: non-privileged steps pipeline with untrusted repo and dynamic image:tag", - failure: false, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _untrustedRepo, - pipeline: "testdata/build/steps/img_environmentdynamic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos enabled: privileged services pipeline with trusted repo", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/services/basic.yml", - privilegedImages: _privilegedImagesServicesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: privileged services pipeline with untrusted repo", - failure: true, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/services/basic.yml", - privilegedImages: _privilegedImagesServicesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: non-privileged services pipeline with trusted repo", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/services/basic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: non-privileged services pipeline with untrusted repo", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/services/basic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos disabled: privileged services pipeline with trusted repo", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/services/basic.yml", - privilegedImages: _privilegedImagesServicesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: privileged services pipeline with untrusted repo", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/services/basic.yml", - privilegedImages: _privilegedImagesServicesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: non-privileged services pipeline with trusted repo", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/services/basic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: non-privileged services pipeline with untrusted repo", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/services/basic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos enabled: privileged services pipeline with trusted repo and dynamic image:tag", - failure: false, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _trustedRepo, - pipeline: "testdata/build/services/img_environmentdynamic.yml", - privilegedImages: _privilegedImagesServicesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: privileged services pipeline with untrusted repo and dynamic image:tag", - failure: true, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _untrustedRepo, - pipeline: "testdata/build/services/img_environmentdynamic.yml", - privilegedImages: _privilegedImagesServicesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: non-privileged services pipeline with trusted repo and dynamic image:tag", - failure: false, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _trustedRepo, - pipeline: "testdata/build/services/img_environmentdynamic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: non-privileged services pipeline with untrusted repo and dynamic image:tag", - failure: false, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _untrustedRepo, - pipeline: "testdata/build/services/img_environmentdynamic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos disabled: privileged services pipeline with trusted repo and dynamic image:tag", - failure: false, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _trustedRepo, - pipeline: "testdata/build/services/img_environmentdynamic.yml", - privilegedImages: _privilegedImagesServicesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: privileged services pipeline with untrusted repo and dynamic image:tag", - failure: false, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _untrustedRepo, - pipeline: "testdata/build/services/img_environmentdynamic.yml", - privilegedImages: _privilegedImagesServicesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: non-privileged services pipeline with trusted repo and dynamic image:tag", - failure: false, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _trustedRepo, - pipeline: "testdata/build/services/img_environmentdynamic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: non-privileged services pipeline with untrusted repo and dynamic image:tag", - failure: false, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _untrustedRepo, - pipeline: "testdata/build/services/img_environmentdynamic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos enabled: privileged stages pipeline with trusted repo", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/stages/basic.yml", - privilegedImages: _privilegedImagesStagesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: privileged stages pipeline with untrusted repo", - failure: true, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/stages/basic.yml", - privilegedImages: _privilegedImagesStagesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: non-privileged stages pipeline with trusted repo", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/stages/basic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: non-privileged stages pipeline with untrusted repo", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/stages/basic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos disabled: privileged stages pipeline with trusted repo", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/stages/basic.yml", - privilegedImages: _privilegedImagesStagesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: privileged stages pipeline with untrusted repo", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/stages/basic.yml", - privilegedImages: _privilegedImagesStagesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: non-privileged stages pipeline with trusted repo", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/stages/basic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: non-privileged stages pipeline with untrusted repo", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/stages/basic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos enabled: privileged stages pipeline with trusted repo and dynamic image:tag", - failure: false, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _trustedRepo, - pipeline: "testdata/build/stages/img_environmentdynamic.yml", - privilegedImages: _privilegedImagesStagesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: privileged stages pipeline with untrusted repo and dynamic image:tag", - failure: true, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _untrustedRepo, - pipeline: "testdata/build/stages/img_environmentdynamic.yml", - privilegedImages: _privilegedImagesStagesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: non-privileged stages pipeline with trusted repo and dynamic image:tag", - failure: false, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _trustedRepo, - pipeline: "testdata/build/stages/img_environmentdynamic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: non-privileged stages pipeline with untrusted repo and dynamic image:tag", - failure: false, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _untrustedRepo, - pipeline: "testdata/build/stages/img_environmentdynamic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos disabled: privileged stages pipeline with trusted repo and dynamic image:tag", - failure: false, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _trustedRepo, - pipeline: "testdata/build/stages/img_environmentdynamic.yml", - privilegedImages: _privilegedImagesStagesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: privileged stages pipeline with untrusted repo and dynamic image:tag", - failure: false, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _untrustedRepo, - pipeline: "testdata/build/stages/img_environmentdynamic.yml", - privilegedImages: _privilegedImagesStagesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: non-privileged stages pipeline with trusted repo and dynamic image:tag", - failure: false, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _trustedRepo, - pipeline: "testdata/build/stages/img_environmentdynamic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: non-privileged stages pipeline with untrusted repo and dynamic image:tag", - failure: false, - runtime: constants.DriverDocker, - build: _buildWithMessageAlpine, - repo: _untrustedRepo, - pipeline: "testdata/build/stages/img_environmentdynamic.yml", - privilegedImages: []string{}, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos enabled: privileged steps pipeline with trusted repo and init step name", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/steps/name_init.yml", - privilegedImages: _privilegedImagesStepsPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: privileged steps pipeline with untrusted repo and init step name", - failure: true, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/steps/name_init.yml", - privilegedImages: _privilegedImagesStepsPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: non-privileged steps pipeline with trusted repo and init step name", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/steps/name_init.yml", - privilegedImages: _privilegedImagesStepsPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: non-privileged steps pipeline with untrusted repo and init step name", - failure: true, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/steps/name_init.yml", - privilegedImages: _privilegedImagesStepsPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos disabled: privileged steps pipeline with trusted repo and init step name", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/steps/name_init.yml", - privilegedImages: _privilegedImagesStepsPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: privileged steps pipeline with untrusted repo and init step name", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/steps/name_init.yml", - privilegedImages: _privilegedImagesStepsPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: non-privileged steps pipeline with trusted repo and init step name", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/steps/name_init.yml", - privilegedImages: _privilegedImagesStepsPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: non-privileged steps pipeline with untrusted repo and init step name", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/steps/name_init.yml", - privilegedImages: _privilegedImagesStepsPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos enabled: privileged stages pipeline with trusted repo and init step name", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/stages/name_init.yml", - privilegedImages: _privilegedImagesStagesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: privileged stages pipeline with untrusted repo and init step name", - failure: true, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/stages/name_init.yml", - privilegedImages: _privilegedImagesStagesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: non-privileged stages pipeline with trusted repo and init step name", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/stages/name_init.yml", - privilegedImages: _privilegedImagesStagesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: non-privileged stages pipeline with untrusted repo and init step name", - failure: true, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/stages/name_init.yml", - privilegedImages: _privilegedImagesStagesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos disabled: privileged stages pipeline with trusted repo and init step name", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/stages/name_init.yml", - privilegedImages: _privilegedImagesStagesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: privileged stages pipeline with untrusted repo and init step name", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/stages/name_init.yml", - privilegedImages: _privilegedImagesStagesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: non-privileged stages pipeline with trusted repo and init step name", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/stages/name_init.yml", - privilegedImages: _privilegedImagesStagesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: non-privileged stages pipeline with untrusted repo and init step name", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/stages/name_init.yml", - privilegedImages: _privilegedImagesStagesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos enabled: privileged services pipeline with trusted repo and init service name", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/services/name_init.yml", - privilegedImages: _privilegedImagesServicesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: privileged services pipeline with untrusted repo and init service name", - failure: true, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/services/name_init.yml", - privilegedImages: _privilegedImagesServicesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: non-privileged services pipeline with trusted repo and init service name", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/services/name_init.yml", - privilegedImages: _privilegedImagesServicesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos enabled: non-privileged services pipeline with untrusted repo and init service name", - failure: true, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/services/name_init.yml", - privilegedImages: _privilegedImagesServicesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: true, - }, - { - name: "docker-enforce trusted repos disabled: privileged services pipeline with trusted repo and init service name", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/services/name_init.yml", - privilegedImages: _privilegedImagesServicesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: privileged services pipeline with untrusted repo and init service name", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/services/name_init.yml", - privilegedImages: _privilegedImagesServicesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: non-privileged services pipeline with trusted repo and init service name", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _trustedRepo, - pipeline: "testdata/build/services/name_init.yml", - privilegedImages: _privilegedImagesServicesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - { - name: "docker-enforce trusted repos disabled: non-privileged services pipeline with untrusted repo and init service name", - failure: false, - runtime: constants.DriverDocker, - build: _build, - repo: _untrustedRepo, - pipeline: "testdata/build/services/name_init.yml", - privilegedImages: _privilegedImagesServicesPipeline, // this matches the image from test.pipeline - enforceTrustedRepos: false, - }, - } - - // run test - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - // ensure custom test repos are in the executor engine - test.build.SetRepo(test.repo) - - _pipeline, _, err := compiler. - Duplicate(). - WithBuild(test.build). - WithRepo(test.repo). - Compile(test.pipeline) - if err != nil { - t.Errorf("unable to compile pipeline %s: %v", test.pipeline, err) - } - - var _runtime runtime.Engine - - switch test.runtime { - case constants.DriverDocker: - _runtime, err = docker.NewMock() - if err != nil { - t.Errorf("unable to create docker runtime engine: %v", err) - } - } - - _engine, err := New( - WithBuild(test.build), - WithPipeline(_pipeline), - WithRuntime(_runtime), - WithVelaClient(_client), - WithPrivilegedImages(test.privilegedImages), - WithEnforceTrustedRepos(test.enforceTrustedRepos), - ) - if err != nil { - t.Errorf("unable to create executor engine: %v", err) - } - - err = _engine.CreateBuild(context.Background()) - if err != nil { - t.Errorf("CreateBuild returned err: %v", err) - } - - // override mock handler PUT build update - // used for dynamic substitute testing - _engine.build.SetMessage(test.build.GetMessage()) - _engine.build.SetRepo(test.repo) - - err = _engine.AssembleBuild(context.Background()) - - if test.failure { - if err == nil { - t.Errorf("AssembleBuild should have returned err") - } - - return // continue to next test - } - - if err != nil { - t.Errorf("AssembleBuild returned err: %v", err) - } - }) - } -} - func TestLinux_PlanBuild(t *testing.T) { // setup types set := flag.NewFlagSet("test", 0) @@ -1467,6 +637,7 @@ func TestLinux_AssembleBuild(t *testing.T) { WithPipeline(_pipeline), WithRuntime(_runtime), WithVelaClient(_client), + WithOutputCtn(testOutputsCtn()), withStreamRequests(streamRequests), ) if err != nil { @@ -1708,6 +879,7 @@ func TestLinux_ExecBuild(t *testing.T) { WithPipeline(_pipeline), WithRuntime(_runtime), WithVelaClient(_client), + WithOutputCtn(testOutputsCtn()), withStreamRequests(streamRequests), ) if err != nil { @@ -2618,6 +1790,7 @@ func TestLinux_DestroyBuild(t *testing.T) { WithPipeline(_pipeline), WithRuntime(_runtime), WithVelaClient(_client), + WithOutputCtn(testOutputsCtn()), ) if err != nil { t.Errorf("unable to create %s executor engine: %v", test.name, err) @@ -2682,3 +1855,11 @@ func TestLinux_DestroyBuild(t *testing.T) { }) } } + +func testOutputsCtn() *pipeline.Container { + return &pipeline.Container{ + ID: "outputs_test", + Environment: make(map[string]string), + Detach: true, + } +} diff --git a/executor/linux/linux.go b/executor/linux/linux.go index 6bd422af..d2c1bfd2 100644 --- a/executor/linux/linux.go +++ b/executor/linux/linux.go @@ -29,9 +29,11 @@ type ( NoSubSecrets map[string]*library.Secret Hostname string Version string + OutputCtn *pipeline.Container // clients for build actions - secret *secretSvc + secret *secretSvc + outputs *outputSvc // private fields init *pipeline.Container @@ -122,6 +124,7 @@ func New(opts ...Opt) (*client, error) { // instantiate all client services c.secret = &secretSvc{client: c} + c.outputs = &outputSvc{client: c} return c, nil } diff --git a/executor/linux/opts.go b/executor/linux/opts.go index ea130992..caf6e7a4 100644 --- a/executor/linux/opts.go +++ b/executor/linux/opts.go @@ -185,6 +185,17 @@ func WithVersion(version string) Opt { } } +func WithOutputCtn(ctn *pipeline.Container) Opt { + return func(c *client) error { + c.Logger.Trace("configuring output container in linux executor client") + + // set the version in the client + c.OutputCtn = ctn + + return nil + } +} + // withStreamRequests sets the streamRequests channel in the executor client for Linux // (primarily used for tests). func withStreamRequests(s chan message.StreamRequest) Opt { diff --git a/executor/linux/outputs.go b/executor/linux/outputs.go new file mode 100644 index 00000000..00c02db5 --- /dev/null +++ b/executor/linux/outputs.go @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: Apache-2.0 + +package linux + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + + envparse "github.com/hashicorp/go-envparse" + "github.com/sirupsen/logrus" + + "github.com/go-vela/types/pipeline" +) + +// outputSvc handles communication with the outputs container during the build. +type outputSvc svc + +// create configures the outputs container for execution. +func (o *outputSvc) create(ctx context.Context, ctn *pipeline.Container, timeout int64) error { + // exit if outputs container has not been configured + if len(ctn.Image) == 0 { + return nil + } + + // set up outputs logger + logger := o.client.Logger.WithField("outputs", "outputs") + + // Encode script content to Base64 + script := base64.StdEncoding.EncodeToString( + []byte(fmt.Sprintf("mkdir /vela/outputs\nsleep %d\n", timeout)), + ) + + // set the entrypoint for the ctn + ctn.Entrypoint = []string{"/bin/sh", "-c"} + + // set the commands for the ctn + ctn.Commands = []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"} + + // set the environment variables for the ctn + ctn.Environment["HOME"] = "/root" + ctn.Environment["SHELL"] = "/bin/sh" + ctn.Environment["VELA_BUILD_SCRIPT"] = script + + logger.Debug("setting up outputs container") + // setup the runtime container + err := o.client.Runtime.SetupContainer(ctx, ctn) + if err != nil { + return err + } + + return nil +} + +// destroy cleans up outputs container after execution. +func (o *outputSvc) destroy(ctx context.Context, ctn *pipeline.Container) error { + // exit if outputs container has not been configured + if len(ctn.Image) == 0 { + return nil + } + + // update engine logger with secret metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus#Entry.WithField + logger := o.client.Logger.WithField("outputs", ctn.Name) + + logger.Debug("inspecting outputs container") + // inspect the runtime container + err := o.client.Runtime.InspectContainer(ctx, ctn) + if err != nil { + return err + } + + logger.Debug("removing outputs container") + // remove the runtime container + err = o.client.Runtime.RemoveContainer(ctx, ctn) + if err != nil { + return err + } + + return nil +} + +// exec runs the outputs sidecar container for a pipeline. +func (o *outputSvc) exec(ctx context.Context, _outputs *pipeline.Container) error { + // exit if outputs container has not been configured + if len(_outputs.Image) == 0 { + return nil + } + + logrus.Debug("running outputs container") + // run the runtime container + err := o.client.Runtime.RunContainer(ctx, _outputs, o.client.pipeline) + if err != nil { + return err + } + + logrus.Debug("inspecting outputs container") + // inspect the runtime container + err = o.client.Runtime.InspectContainer(ctx, _outputs) + if err != nil { + return err + } + + return nil +} + +// poll tails the output for sidecar container. +func (o *outputSvc) poll(ctx context.Context, ctn *pipeline.Container) (map[string]string, map[string]string, error) { + // exit if outputs container has not been configured + if len(ctn.Image) == 0 { + return nil, nil, nil + } + + // update engine logger with outputs metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus#Entry.WithField + logger := o.client.Logger.WithField("outputs", ctn.Name) + + logger.Debug("tailing container") + + // grab outputs + outputBytes, err := o.client.Runtime.PollOutputsContainer(ctx, ctn, "/vela/outputs/.env") + if err != nil { + return nil, nil, err + } + + reader := bytes.NewReader(outputBytes) + + outputMap, err := envparse.Parse(reader) + if err != nil { + logger.Debugf("unable to parse output map: %v", err) + } + + // grab masked outputs + maskedBytes, err := o.client.Runtime.PollOutputsContainer(ctx, ctn, "/vela/outputs/masked.env") + if err != nil { + return nil, nil, err + } + + reader = bytes.NewReader(maskedBytes) + + maskMap, err := envparse.Parse(reader) + if err != nil { + logger.Debugf("unable to parse masked output map: %v", err) + } + + return outputMap, maskMap, nil +} diff --git a/executor/linux/outputs_test.go b/executor/linux/outputs_test.go new file mode 100644 index 00000000..db0a3174 --- /dev/null +++ b/executor/linux/outputs_test.go @@ -0,0 +1,469 @@ +// SPDX-License-Identifier: Apache-2.0 + +package linux + +import ( + "context" + "flag" + "net/http/httptest" + "os" + "testing" + + "github.com/gin-gonic/gin" + "github.com/urfave/cli/v2" + + "github.com/go-vela/sdk-go/vela" + "github.com/go-vela/server/compiler/native" + "github.com/go-vela/server/mock/server" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/go-vela/types/pipeline" + "github.com/go-vela/worker/internal/message" + "github.com/go-vela/worker/runtime" + "github.com/go-vela/worker/runtime/docker" +) + +func TestLinux_Outputs_create(t *testing.T) { + // setup types + _build := testBuild() + _steps := testSteps(constants.DriverDocker) + + gin.SetMode(gin.TestMode) + + s := httptest.NewServer(server.FakeHandler()) + + _client, err := vela.NewClient(s.URL, "", nil) + if err != nil { + t.Errorf("unable to create Vela API client: %v", err) + } + + _docker, err := docker.NewMock() + if err != nil { + t.Errorf("unable to create docker runtime engine: %v", err) + } + + // setup tests + tests := []struct { + name string + failure bool + runtime runtime.Engine + container *pipeline.Container + }{ + { + name: "good image tag", + failure: false, + runtime: _docker, + container: &pipeline.Container{ + ID: "outputs_github_octocat_1", + Directory: "/vela/src/vcs.company.com/github/octocat", + Environment: map[string]string{"FOO": "bar"}, + Image: "alpine:latest", + Name: "outputs", + Number: 1, + Pull: "not_present", + }, + }, + { + name: "notfound image tag", + failure: true, + runtime: _docker, + container: &pipeline.Container{ + ID: "outputs_github_octocat_1", + Directory: "/vela/src/vcs.company.com/github/octocat", + Environment: map[string]string{"FOO": "bar"}, + Image: "alpine:notfound", + Name: "outputs", + Number: 1, + Pull: "not_present", + }, + }, + { + name: "not supplied image tag", + failure: false, + runtime: _docker, + container: &pipeline.Container{ + ID: "outputs_github_octocat_1", + Directory: "/vela/src/vcs.company.com/github/octocat", + Environment: map[string]string{"FOO": "bar"}, + Image: "", + Name: "outputs", + Number: 1, + Pull: "not_present", + }, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _engine, err := New( + WithBuild(_build), + WithPipeline(_steps), + WithRuntime(test.runtime), + WithVelaClient(_client), + WithOutputCtn(test.container), + ) + if err != nil { + t.Errorf("unable to create %s executor engine: %v", test.name, err) + } + + err = _engine.outputs.create(context.Background(), test.container, 30) + + if test.failure { + if err == nil { + t.Errorf("%s create should have returned err", test.name) + } + + return // continue to next test + } + + if err != nil { + t.Errorf("%s create returned err: %v", test.name, err) + } + }) + } +} + +func TestLinux_Outputs_delete(t *testing.T) { + // setup types + _build := testBuild() + _dockerSteps := testSteps(constants.DriverDocker) + + gin.SetMode(gin.TestMode) + + s := httptest.NewServer(server.FakeHandler()) + + _client, err := vela.NewClient(s.URL, "", nil) + if err != nil { + t.Errorf("unable to create Vela API client: %v", err) + } + + _docker, err := docker.NewMock() + if err != nil { + t.Errorf("unable to create docker runtime engine: %v", err) + } + + _step := new(library.Step) + _step.SetName("clone") + _step.SetNumber(2) + _step.SetStatus(constants.StatusPending) + + // setup tests + tests := []struct { + name string + failure bool + runtime runtime.Engine + container *pipeline.Container + step *library.Step + steps *pipeline.Build + }{ + { + name: "docker-running container-empty step", + failure: false, + runtime: _docker, + container: &pipeline.Container{ + ID: "outputs_github_octocat_1", + Directory: "/vela/src/vcs.company.com/github/octocat", + Environment: map[string]string{"FOO": "bar"}, + Image: "alpine:latest", + Name: "outputs", + Number: 1, + Pull: "always", + }, + step: new(library.Step), + steps: _dockerSteps, + }, + { + name: "docker-running container-pending step", + failure: false, + runtime: _docker, + container: &pipeline.Container{ + ID: "outputs_github_octocat_1", + Directory: "/vela/src/vcs.company.com/github/octocat", + Environment: map[string]string{"FOO": "bar"}, + Image: "alpine:latest", + Name: "outputs", + Number: 2, + Pull: "always", + }, + step: _step, + steps: _dockerSteps, + }, + { + name: "docker-inspecting container failure due to invalid container id", + failure: true, + runtime: _docker, + container: &pipeline.Container{ + ID: "outputs_github_octocat_1_notfound", + Directory: "/vela/src/vcs.company.com/github/octocat", + Environment: map[string]string{"FOO": "bar"}, + Image: "alpine:latest", + Name: "notfound", + Number: 2, + Pull: "always", + }, + step: new(library.Step), + steps: _dockerSteps, + }, + { + name: "docker-removing container failure", + failure: true, + runtime: _docker, + container: &pipeline.Container{ + ID: "outputs_github_octocat_1_ignorenotfound", + Directory: "/vela/src/vcs.company.com/github/octocat", + Environment: map[string]string{"FOO": "bar"}, + Image: "alpine:latest", + Name: "ignorenotfound", + Number: 2, + Pull: "always", + }, + step: new(library.Step), + steps: _dockerSteps, + }, + { + name: "no outputs image provided", + failure: false, + runtime: _docker, + container: &pipeline.Container{ + ID: "outputs_github_octocat_1", + Directory: "/vela/src/vcs.company.com/github/octocat", + Environment: map[string]string{"FOO": "bar"}, + Image: "", + Name: "outputs", + Number: 2, + Pull: "always", + }, + step: _step, + steps: _dockerSteps, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _engine, err := New( + WithBuild(_build), + WithPipeline(test.steps), + WithRuntime(test.runtime), + WithVelaClient(_client), + WithOutputCtn(test.container), + ) + if err != nil { + t.Errorf("unable to create %s executor engine: %v", test.name, err) + } + + // add init container info to client + _ = _engine.CreateBuild(context.Background()) + + _engine.steps.Store(test.container.ID, test.step) + + err = _engine.outputs.destroy(context.Background(), test.container) + + if test.failure { + if err == nil { + t.Errorf("%s destroy should have returned err", test.name) + } + + return // continue to next test + } + + if err != nil { + t.Errorf("%s destroy returned err: %v", test.name, err) + } + }) + } +} + +func TestLinux_Outputs_exec(t *testing.T) { + // setup types + set := flag.NewFlagSet("test", 0) + set.String("clone-image", "target/vela-git:latest", "doc") + compiler, _ := native.FromCLIContext(cli.NewContext(nil, set, nil)) + + _build := testBuild() + + gin.SetMode(gin.TestMode) + + s := httptest.NewServer(server.FakeHandler()) + + _client, err := vela.NewClient(s.URL, "", nil) + if err != nil { + t.Errorf("unable to create Vela API client: %v", err) + } + + streamRequests, done := message.MockStreamRequestsWithCancel(context.Background()) + defer done() + + // setup tests + tests := []struct { + name string + failure bool + runtime string + pipeline string + }{ + { + name: "basic pipeline", + failure: false, + runtime: constants.DriverDocker, + pipeline: "testdata/build/steps/basic.yml", + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + file, _ := os.ReadFile(test.pipeline) + + p, _, err := compiler. + Duplicate(). + WithBuild(_build). + WithRepo(_build.GetRepo()). + WithUser(_build.GetRepo().GetOwner()). + Compile(file) + if err != nil { + t.Errorf("unable to compile pipeline %s: %v", test.pipeline, err) + } + + // Docker uses _ while Kubernetes uses - + p = p.Sanitize(test.runtime) + + var _runtime runtime.Engine + + _runtime, err = docker.NewMock() + if err != nil { + t.Errorf("unable to create docker runtime engine: %v", err) + } + + outputsCtn := &pipeline.Container{ + ID: "outputs_github_octocat_1", + Directory: "/vela/src/vcs.company.com/github/octocat", + Environment: map[string]string{"FOO": "bar"}, + Image: "alpine:latest", + Name: "outputs", + Number: 2, + Pull: "always", + } + + _engine, err := New( + WithBuild(_build), + WithPipeline(p), + WithRuntime(_runtime), + WithVelaClient(_client), + withStreamRequests(streamRequests), + WithOutputCtn(outputsCtn), + ) + if err != nil { + t.Errorf("unable to create %s executor engine: %v", test.name, err) + } + + _engine.build.SetStatus(constants.StatusSuccess) + + // add init container info to client + _ = _engine.CreateBuild(context.Background()) + + err = _engine.outputs.exec(context.Background(), outputsCtn) + + if test.failure { + if err == nil { + t.Errorf("%s exec should have returned err", test.name) + } + + return // continue to next test + } + + if err != nil { + t.Errorf("%s exec returned err: %v", test.name, err) + } + }) + } +} + +func TestLinux_Outputs_poll(t *testing.T) { + // setup types + _build := testBuild() + _steps := testSteps(constants.DriverDocker) + + gin.SetMode(gin.TestMode) + + s := httptest.NewServer(server.FakeHandler()) + + _client, err := vela.NewClient(s.URL, "", nil) + if err != nil { + t.Errorf("unable to create Vela API client: %v", err) + } + + _docker, err := docker.NewMock() + if err != nil { + t.Errorf("unable to create docker runtime engine: %v", err) + } + + // setup tests + tests := []struct { + name string + failure bool + runtime runtime.Engine + container *pipeline.Container + }{ + { + name: "succeeds", + failure: false, + runtime: _docker, + container: &pipeline.Container{ + ID: "outputs_github_octocat_1", + Directory: "/home/github/octocat", + Environment: map[string]string{"FOO": "bar"}, + Image: "alpine:latest", + Name: "outputs", + Number: 1, + Pull: "always", + }, + }, + { + name: "no outputs image provided", + failure: false, + runtime: _docker, + container: &pipeline.Container{ + ID: "outputs_github_octocat_1_notfound", + Directory: "/vela/src/vcs.company.com/github/octocat", + Environment: map[string]string{"FOO": "bar"}, + Image: "", + Name: "notfound", + Number: 2, + Pull: "always", + }, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _engine, err := New( + WithBuild(_build), + WithPipeline(_steps), + WithRuntime(test.runtime), + WithVelaClient(_client), + ) + if err != nil { + t.Errorf("unable to create %s executor engine: %v", test.name, err) + } + + // add init container info to client + _ = _engine.CreateBuild(context.Background()) + + _, _, err = _engine.outputs.poll(context.Background(), test.container) + + if test.failure { + if err == nil { + t.Errorf("%s poll should have returned err", test.name) + } + + return // continue to next test + } + + if err != nil { + t.Errorf("%s poll returned err: %v", test.name, err) + } + }) + } +} diff --git a/executor/linux/secret.go b/executor/linux/secret.go index e87c1883..ed343298 100644 --- a/executor/linux/secret.go +++ b/executor/linux/secret.go @@ -349,7 +349,7 @@ func injectSecrets(ctn *pipeline.Container, m map[string]*library.Secret) error logrus.Tracef("matching secret %s to container %s", _secret.Source, ctn.Name) // ensure the secret matches with the container if s.Match(ctn) { - ctn.Environment[strings.ToUpper(_secret.Target)] = s.GetValue() + ctn.Environment[_secret.Target] = s.GetValue() } } diff --git a/executor/linux/service.go b/executor/linux/service.go index 504a9fda..96704c62 100644 --- a/executor/linux/service.go +++ b/executor/linux/service.go @@ -13,6 +13,7 @@ import ( "github.com/go-vela/types/constants" "github.com/go-vela/types/library" "github.com/go-vela/types/pipeline" + "github.com/go-vela/worker/internal/image" "github.com/go-vela/worker/internal/message" "github.com/go-vela/worker/internal/service" ) @@ -141,6 +142,21 @@ func (c *client) ExecService(ctx context.Context, ctn *pipeline.Container) error // https://pkg.go.dev/github.com/go-vela/worker/internal/service#Snapshot defer func() { service.Snapshot(ctn, c.build, c.Vela, c.Logger, _service) }() + // verify service is allowed to run + if c.enforceTrustedRepos { + priv, err := image.IsPrivilegedImage(ctn.Image, c.privilegedImages) + if err != nil { + return err + } + + if priv && !c.build.GetRepo().GetTrusted() { + _service.SetStatus(constants.StatusError) + _service.SetError("attempting to use privileged image as untrusted repo") + + return fmt.Errorf("attempting to use privileged image (%s) as untrusted repo", ctn.Image) + } + } + logger.Debug("running container") // run the runtime container err = c.Runtime.RunContainer(ctx, ctn, c.pipeline) diff --git a/executor/linux/service_test.go b/executor/linux/service_test.go index 6787c391..9ed98a0e 100644 --- a/executor/linux/service_test.go +++ b/executor/linux/service_test.go @@ -404,6 +404,20 @@ func TestLinux_ExecService(t *testing.T) { runtime: _kubernetes, container: new(pipeline.Container), }, + { + name: "privileged image", + failure: true, + runtime: _docker, + container: &pipeline.Container{ + ID: "step_github_octocat_1_echo", + Directory: "/vela/src/github.com/github/octocat", + Environment: map[string]string{"FOO": "bar"}, + Image: "target/vela-docker", + Name: "echo", + Number: 1, + Pull: "not_present", + }, + }, } // run tests @@ -415,6 +429,8 @@ func TestLinux_ExecService(t *testing.T) { WithRuntime(test.runtime), WithVelaClient(_client), withStreamRequests(streamRequests), + WithEnforceTrustedRepos(true), + WithPrivilegedImages([]string{"target/vela-docker"}), ) if err != nil { t.Errorf("unable to create %s executor engine: %v", test.name, err) diff --git a/executor/linux/stage.go b/executor/linux/stage.go index 83dacf0c..e0549f43 100644 --- a/executor/linux/stage.go +++ b/executor/linux/stage.go @@ -164,6 +164,43 @@ func (c *client) ExecStage(ctx context.Context, s *pipeline.Stage, m *sync.Map) return fmt.Errorf("unable to plan step %s: %w", _step.Name, err) } + // poll outputs + opEnv, maskEnv, err := c.outputs.poll(ctx, c.OutputCtn) + if c.err != nil { + return fmt.Errorf("unable to exec outputs container: %w", err) + } + + // merge env from outputs + // + //nolint:errcheck // only errors with empty environment input, which does not matter here + _step.MergeEnv(opEnv) + + // merge env from masked outputs + // + //nolint:errcheck // only errors with empty environment input, which does not matter here + _step.MergeEnv(maskEnv) + + // add masked outputs to secret map so they can be masked in logs + for key := range maskEnv { + sec := &pipeline.StepSecret{ + Target: key, + } + _step.Secrets = append(_step.Secrets, sec) + } + + // perform any substitution on dynamic variables + err = _step.Substitute() + if err != nil { + return err + } + + c.Logger.Debug("injecting non-substituted secrets") + // inject no-substitution secrets for container + err = injectSecrets(_step, c.NoSubSecrets) + if err != nil { + return err + } + logger.Infof("executing %s step", _step.Name) // execute the step err = c.ExecStep(ctx, _step) diff --git a/executor/linux/stage_test.go b/executor/linux/stage_test.go index f7a19656..4bb7ba5f 100644 --- a/executor/linux/stage_test.go +++ b/executor/linux/stage_test.go @@ -576,6 +576,7 @@ func TestLinux_ExecStage(t *testing.T) { WithPipeline(new(pipeline.Build)), WithRuntime(test.runtime), WithVelaClient(_client), + WithOutputCtn(testOutputsCtn()), withStreamRequests(streamRequests), ) if err != nil { diff --git a/executor/linux/step.go b/executor/linux/step.go index 391b4cc0..88186dbc 100644 --- a/executor/linux/step.go +++ b/executor/linux/step.go @@ -15,6 +15,7 @@ import ( "github.com/go-vela/types/constants" "github.com/go-vela/types/library" "github.com/go-vela/types/pipeline" + "github.com/go-vela/worker/internal/image" "github.com/go-vela/worker/internal/message" "github.com/go-vela/worker/internal/step" ) @@ -57,20 +58,23 @@ func (c *client) CreateStep(ctx context.Context, ctn *pipeline.Container) error return err } - logger.Debug("substituting container configuration") - // substitute container configuration - // - // https://pkg.go.dev/github.com/go-vela/types/pipeline#Container.Substitute - err = ctn.Substitute() - if err != nil { - return fmt.Errorf("unable to substitute container configuration") - } + // K8s runtime needs to substitute + inject prior to reaching ExecBuild (no outputs) + if c.Runtime.Driver() == constants.DriverKubernetes { + logger.Debug("substituting container configuration") + // substitute container configuration + // + // https://pkg.go.dev/github.com/go-vela/types/pipeline#Container.Substitute + err = ctn.Substitute() + if err != nil { + return fmt.Errorf("unable to substitute container configuration") + } - logger.Debug("injecting non-substituted secrets") - // inject no-substitution secrets for container - err = injectSecrets(ctn, c.NoSubSecrets) - if err != nil { - return err + logger.Debug("injecting non-substituted secrets") + // inject no-substitution secrets for container + err = injectSecrets(ctn, c.NoSubSecrets) + if err != nil { + return err + } } return nil @@ -168,7 +172,23 @@ func (c *client) ExecStep(ctx context.Context, ctn *pipeline.Container) error { // https://pkg.go.dev/github.com/go-vela/worker/internal/step#Snapshot defer func() { step.Snapshot(ctn, c.build, c.Vela, c.Logger, _step) }() + // verify step is allowed to run + if c.enforceTrustedRepos { + priv, err := image.IsPrivilegedImage(ctn.Image, c.privilegedImages) + if err != nil { + return err + } + + if priv && !c.build.GetRepo().GetTrusted() { + _step.SetStatus(constants.StatusError) + _step.SetError("attempting to use privileged image as untrusted repo") + + return fmt.Errorf("attempting to use privileged image (%s) as untrusted repo", ctn.Image) + } + } + logger.Debug("running container") + // run the runtime container err = c.Runtime.RunContainer(ctx, ctn, c.pipeline) if err != nil { @@ -423,7 +443,7 @@ func getSecretValues(ctn *pipeline.Container) []string { // gather secrets' values from the environment map for masking for _, secret := range ctn.Secrets { // capture secret from environment - s, ok := ctn.Environment[strings.ToUpper(secret.Target)] + s, ok := ctn.Environment[secret.Target] if !ok { continue } diff --git a/executor/linux/step_test.go b/executor/linux/step_test.go index 96c936e6..2435afb9 100644 --- a/executor/linux/step_test.go +++ b/executor/linux/step_test.go @@ -470,6 +470,20 @@ func TestLinux_ExecStep(t *testing.T) { runtime: _kubernetes, container: new(pipeline.Container), }, + { + name: "privileged image", + failure: true, + runtime: _docker, + container: &pipeline.Container{ + ID: "step_github_octocat_1_echo", + Directory: "/vela/src/github.com/github/octocat", + Environment: map[string]string{"FOO": "bar"}, + Image: "target/vela-docker", + Name: "echo", + Number: 1, + Pull: "not_present", + }, + }, } // run tests @@ -481,6 +495,8 @@ func TestLinux_ExecStep(t *testing.T) { WithRuntime(test.runtime), WithVelaClient(_client), withStreamRequests(streamRequests), + WithPrivilegedImages([]string{"target/vela-docker"}), + WithEnforceTrustedRepos(true), ) if err != nil { t.Errorf("unable to create %s executor engine: %v", test.name, err) @@ -873,11 +889,11 @@ func TestLinux_getSecretValues(t *testing.T) { Secrets: pipeline.StepSecretSlice{ { Source: "someSource", - Target: "secret_username", + Target: "SECRET_USERNAME", }, { Source: "someOtherSource", - Target: "secret_password", + Target: "SECRET_PASSWORD", }, { Source: "disallowedSecret", @@ -904,11 +920,11 @@ func TestLinux_getSecretValues(t *testing.T) { Secrets: pipeline.StepSecretSlice{ { Source: "someSource", - Target: "secret_username", + Target: "SECRET_USERNAME", }, { Source: "someOtherSource", - Target: "secret_password", + Target: "SECRET_PASSWORD", }, }, }, diff --git a/executor/setup.go b/executor/setup.go index 6c1a8503..745097e7 100644 --- a/executor/setup.go +++ b/executor/setup.go @@ -50,6 +50,8 @@ type Setup struct { // engine used for creating runtime resources Runtime runtime.Engine + OutputCtn *pipeline.Container + // Vela Resource Configuration // resource for storing build information in Vela @@ -88,6 +90,7 @@ func (s *Setup) Linux() (Engine, error) { linux.WithVelaClient(s.Client), linux.WithVersion(s.Version), linux.WithLogger(s.Logger), + linux.WithOutputCtn(s.OutputCtn), ) } diff --git a/go.mod b/go.mod index 67eb174e..f27744e5 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/gin-gonic/gin v1.10.0 github.com/go-vela/sdk-go v0.24.0 github.com/go-vela/server v0.24.1 - github.com/go-vela/types v0.24.0 + github.com/go-vela/types v0.24.1-0.20240813201820-772b29b91a5e github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/go-cmp v0.6.0 github.com/joho/godotenv v1.5.1 @@ -77,6 +77,7 @@ require ( github.com/goware/urlx v0.3.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-envparse v0.1.0 github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/huandu/xstrings v1.3.3 // indirect diff --git a/go.sum b/go.sum index 932849e7..51e75d6f 100644 --- a/go.sum +++ b/go.sum @@ -107,8 +107,8 @@ github.com/go-vela/sdk-go v0.24.0 h1:QmwcF8h/Fq1mwbE8mdvqyTmXM2Z3sE0dLQLEMc7HA4w github.com/go-vela/sdk-go v0.24.0/go.mod h1:TmJI0KOt/KweLy0HE4JGpKYDPsbhW4sdl2AY/tWP0tY= github.com/go-vela/server v0.24.1 h1:iM5REZBh6oHD0nxEH4O6dkUWNhY3MNrWBLNWGUUwcP8= github.com/go-vela/server v0.24.1/go.mod h1:jCnJPiyaRLcdy1u5fKIf7BqsbYAbVMjjI7dlyxZovME= -github.com/go-vela/types v0.24.0 h1:KkkiXxw3uHckh/foyadmLY1YnLw6vhZbz9XwqONCj6o= -github.com/go-vela/types v0.24.0/go.mod h1:YWj6BIapl9Kbj4yHq/fp8jltXdGiwD/gTy1ez32Rzag= +github.com/go-vela/types v0.24.1-0.20240813201820-772b29b91a5e h1:6OHP0aQ2SKwNyL5Q+qYo+Yts6MD3PwmkqWKGoH3AlTI= +github.com/go-vela/types v0.24.1-0.20240813201820-772b29b91a5e/go.mod h1:YWj6BIapl9Kbj4yHq/fp8jltXdGiwD/gTy1ez32Rzag= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -147,6 +147,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-envparse v0.1.0 h1:bE++6bhIsNCPLvgDZkYqo3nA+/PFI51pkrHdmPSDFPY= +github.com/hashicorp/go-envparse v0.1.0/go.mod h1:OHheN1GoygLlAkTlXLXvAdnXdZxy8JUweQ1rAXx1xnc= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= diff --git a/internal/image/image.go b/internal/image/image.go index ee0c3015..e2d5b353 100644 --- a/internal/image/image.go +++ b/internal/image/image.go @@ -52,34 +52,40 @@ func ParseWithError(_image string) (string, error) { // IsPrivilegedImage digests the provided image with a // privileged pattern to see if the image meets the criteria // needed to allow a Docker Socket mount. -func IsPrivilegedImage(image, privileged string) (bool, error) { - // parse the image provided into a - // named, fully qualified reference - // - // https://pkg.go.dev/github.com/distribution/reference#ParseAnyReference - _refImg, err := reference.ParseAnyReference(image) - if err != nil { - return false, err - } +func IsPrivilegedImage(image string, privilegedSet []string) (bool, error) { + for _, pattern := range privilegedSet { + // parse the image provided into a + // named, fully qualified reference + // + // https://pkg.go.dev/github.com/distribution/reference#ParseAnyReference + _refImg, err := reference.ParseAnyReference(image) + if err != nil { + return false, err + } - // ensure we have the canonical form of the named reference - // - // https://pkg.go.dev/github.com/distribution/reference#ParseNamed - _canonical, err := reference.ParseNamed(_refImg.String()) - if err != nil { - return false, err - } + // ensure we have the canonical form of the named reference + // + // https://pkg.go.dev/github.com/distribution/reference#ParseNamed + _canonical, err := reference.ParseNamed(_refImg.String()) + if err != nil { + return false, err + } - // add default tag "latest" when tag does not exist - _refImg = reference.TagNameOnly(_canonical) + // add default tag "latest" when tag does not exist + _refImg = reference.TagNameOnly(_canonical) - // check if the image matches the privileged pattern - // - // https://pkg.go.dev/github.com/distribution/reference#FamiliarMatch - match, err := reference.FamiliarMatch(privileged, _refImg) - if err != nil { - return false, err + // check if the image matches the privileged pattern + // + // https://pkg.go.dev/github.com/distribution/reference#FamiliarMatch + match, err := reference.FamiliarMatch(pattern, _refImg) + if err != nil { + return false, err + } + + if match { + return match, nil + } } - return match, nil + return false, nil } diff --git a/internal/image/image_test.go b/internal/image/image_test.go index d38d4f87..e76ffc81 100644 --- a/internal/image/image_test.go +++ b/internal/image/image_test.go @@ -154,52 +154,52 @@ func TestImage_ParseWithError(t *testing.T) { func TestImage_IsPrivilegedImage(t *testing.T) { // setup tests tests := []struct { - name string - image string - pattern string - want bool + name string + image string + patterns []string + want bool }{ { - name: "test privileged image without tag", - image: "docker.company.com/foo/bar", - pattern: "docker.company.com/foo/bar", - want: true, + name: "test privileged image without tag", + image: "docker.company.com/foo/bar", + patterns: []string{"docker.company.com/foo/bar"}, + want: true, }, { - name: "test privileged image with tag", - image: "docker.company.com/foo/bar:v0.1.0", - pattern: "docker.company.com/foo/bar", - want: true, + name: "test privileged image with tag", + image: "docker.company.com/foo/bar:v0.1.0", + patterns: []string{"docker.company.com/foo/bar"}, + want: true, }, { - name: "test privileged image with tag", - image: "docker.company.com/foo/bar", - pattern: "docker.company.com/foo/bar:v0.1.0", - want: false, + name: "test privileged image with tag", + image: "docker.company.com/foo/bar", + patterns: []string{"docker.company.com/foo/bar:v0.1.0"}, + want: false, }, { - name: "test privileged with bad image", - image: "!@#$%^&*()", - pattern: "docker.company.com/foo/bar", - want: false, + name: "test privileged with bad image", + image: "!@#$%^&*()", + patterns: []string{"docker.company.com/foo/bar"}, + want: false, }, { - name: "test privileged with bad pattern", - image: "docker.company.com/foo/bar", - pattern: "!@#$%^&*()", - want: false, + name: "test privileged with bad pattern", + image: "docker.company.com/foo/bar", + patterns: []string{"!@#$%^&*()", "docker.company.com/foo/baz"}, + want: false, }, { - name: "test privileged with on extended path image", - image: "docker.company.com/foo/bar", - pattern: "docker.company.com/foo", - want: false, + name: "test privileged with on extended path image", + image: "docker.company.com/foo/bar", + patterns: []string{"docker.company.com/foo", "docker.company.com/fab"}, + want: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got, _ := IsPrivilegedImage(test.image, test.pattern) + got, _ := IsPrivilegedImage(test.image, test.patterns) if got != test.want { t.Errorf("IsPrivilegedImage is %v want %v", got, test.want) } diff --git a/internal/step/environment.go b/internal/step/environment.go index ea1a9669..7c996276 100644 --- a/internal/step/environment.go +++ b/internal/step/environment.go @@ -41,6 +41,8 @@ func Environment(c *pipeline.Container, b *api.Build, s *library.Step, version, c.Environment["VELA_RUNTIME"] = b.GetRuntime() c.Environment["VELA_VERSION"] = version c.Environment["VELA_ID_TOKEN_REQUEST_TOKEN"] = reqToken + c.Environment["VELA_OUTPUTS"] = "/vela/outputs/.env" + c.Environment["VELA_MASKED_OUTPUTS"] = "/vela/outputs/masked.env" // populate environment variables from build library // diff --git a/runtime/docker/container.go b/runtime/docker/container.go index f9c5bb66..0beec9e0 100644 --- a/runtime/docker/container.go +++ b/runtime/docker/container.go @@ -3,11 +3,13 @@ package docker import ( + "bytes" "context" "fmt" "io" "strings" + "github.com/docker/docker/api/types" dockerContainerTypes "github.com/docker/docker/api/types/container" docker "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" @@ -136,21 +138,19 @@ func (c *client) RunContainer(ctx context.Context, ctn *pipeline.Container, b *p } // check if the image is allowed to run privileged - for _, pattern := range c.config.Images { - privileged, err := image.IsPrivilegedImage(ctn.Image, pattern) - if err != nil { - return err - } + privileged, err := image.IsPrivilegedImage(ctn.Image, c.config.Images) + if err != nil { + return err + } - if privileged { - hostConf.Privileged = true - } + if privileged { + hostConf.Privileged = true } // send API call to create the container // - // https://pkg.go.dev/github.com/docker/docker/client#Client.ContainerCreate - _, err := c.Docker.ContainerCreate( + // https://godoc.org/github.com/docker/docker/client#Client.ContainerCreate + _, err = c.Docker.ContainerCreate( ctx, containerConf, hostConf, @@ -294,6 +294,54 @@ func (c *client) WaitContainer(ctx context.Context, ctn *pipeline.Container) err return nil } +// PollOutputsContainer captures the `cat` response for a given path in the Docker volume. +func (c *client) PollOutputsContainer(ctx context.Context, ctn *pipeline.Container, path string) ([]byte, error) { + if len(ctn.Image) == 0 { + return nil, nil + } + + execConfig := types.ExecConfig{ + Tty: true, + Cmd: []string{"sh", "-c", fmt.Sprintf("cat %s", path)}, + AttachStderr: true, + AttachStdout: true, + } + + responseExec, err := c.Docker.ContainerExecCreate(ctx, ctn.ID, execConfig) + if err != nil { + return nil, err + } + + hijackedResponse, err := c.Docker.ContainerExecAttach(ctx, responseExec.ID, types.ExecStartCheck{}) + if err != nil { + return nil, err + } + + defer func() { + if hijackedResponse.Conn != nil { + hijackedResponse.Close() + } + }() + + outputStdout := new(bytes.Buffer) + outputStderr := new(bytes.Buffer) + + if hijackedResponse.Reader != nil { + _, err := stdcopy.StdCopy(outputStdout, outputStderr, hijackedResponse.Reader) + if err != nil { + c.Logger.Errorf("unable to copy logs for container: %v", err) + } + } + + if outputStderr.Len() > 0 { + return nil, fmt.Errorf("error: %s", outputStderr.String()) + } + + data := outputStdout.Bytes() + + return data, nil +} + // ctnConfig is a helper function to // generate the container config. func ctnConfig(ctn *pipeline.Container) *dockerContainerTypes.Config { diff --git a/runtime/docker/container_test.go b/runtime/docker/container_test.go index ada72242..82e35cda 100644 --- a/runtime/docker/container_test.go +++ b/runtime/docker/container_test.go @@ -397,6 +397,51 @@ func TestDocker_TailContainer(t *testing.T) { } } +func TestDocker_PollOutputsContainer(t *testing.T) { + // setup Docker + _engine, err := NewMock() + if err != nil { + t.Errorf("unable to create runtime engine: %v", err) + } + + // setup tests + tests := []struct { + name string + failure bool + container *pipeline.Container + }{ + { + name: "outputs container", + failure: false, + container: _container, + }, + { + name: "no provided outputs container", + failure: false, + container: new(pipeline.Container), + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err = _engine.PollOutputsContainer(context.Background(), test.container, "") + + if test.failure { + if err == nil { + t.Errorf("PollOutputsContainer should have returned err") + } + + return // continue to next test + } + + if err != nil { + t.Errorf("PollOutputs returned err: %v", err) + } + }) + } +} + func TestDocker_WaitContainer(t *testing.T) { // setup Docker _engine, err := NewMock() diff --git a/runtime/docker/image.go b/runtime/docker/image.go index e20c5b52..4d4f3ede 100644 --- a/runtime/docker/image.go +++ b/runtime/docker/image.go @@ -70,6 +70,13 @@ func (c *client) InspectImage(ctx context.Context, ctn *pipeline.Container) ([]b fmt.Sprintf("$ docker image inspect %s\n", ctn.Image), ) + // check if the container pull policy is on start + if strings.EqualFold(ctn.Pull, constants.PullOnStart) || strings.EqualFold(ctn.Pull, constants.PullNever) { + return []byte( + fmt.Sprintf("skipped for container %s due to pull policy %s\n", ctn.ID, ctn.Pull), + ), nil + } + // parse image from container // // https://pkg.go.dev/github.com/go-vela/worker/internal/image#ParseWithError @@ -78,13 +85,6 @@ func (c *client) InspectImage(ctx context.Context, ctn *pipeline.Container) ([]b return output, err } - // check if the container pull policy is on start - if strings.EqualFold(ctn.Pull, constants.PullOnStart) { - return []byte( - fmt.Sprintf("skipped for container %s due to pull policy %s\n", ctn.ID, ctn.Pull), - ), nil - } - // send API call to inspect the image // // https://pkg.go.dev/github.com/docker/docker/client#Client.ImageInspectWithRaw diff --git a/runtime/engine.go b/runtime/engine.go index 79f3a37a..97ed9c49 100644 --- a/runtime/engine.go +++ b/runtime/engine.go @@ -43,6 +43,9 @@ type Engine interface { // InspectContainer defines a function that inspects // the pipeline container. InspectContainer(context.Context, *pipeline.Container) error + // PollOutputsContainer defines a function that captures + // file contents from the outputs container. + PollOutputsContainer(context.Context, *pipeline.Container, string) ([]byte, error) // RemoveContainer defines a function that deletes // (kill, remove) the pipeline container. RemoveContainer(context.Context, *pipeline.Container) error diff --git a/runtime/kubernetes/container.go b/runtime/kubernetes/container.go index 5c7bd47f..55c14364 100644 --- a/runtime/kubernetes/container.go +++ b/runtime/kubernetes/container.go @@ -69,6 +69,12 @@ func (c *client) RemoveContainer(ctx context.Context, ctn *pipeline.Container) e return nil } +func (c *client) PollOutputsContainer(ctx context.Context, ctn *pipeline.Container, path string) ([]byte, error) { + c.Logger.Tracef("no-op: removing container %s", ctn.ID) + + return nil, nil +} + // RunContainer creates and starts the pipeline container. func (c *client) RunContainer(ctx context.Context, ctn *pipeline.Container, _ *pipeline.Build) error { c.Logger.Tracef("running container %s", ctn.ID) @@ -158,15 +164,14 @@ func (c *client) SetupContainer(ctx context.Context, ctn *pipeline.Container) er container.VolumeMounts = volumeMounts // check if the image is allowed to run privileged - for _, pattern := range c.config.Images { - privileged, err := image.IsPrivilegedImage(ctn.Image, pattern) - if err != nil { - return err - } - container.SecurityContext.Privileged = &privileged + privileged, err := image.IsPrivilegedImage(ctn.Image, c.config.Images) + if err != nil { + return err } + container.SecurityContext.Privileged = &privileged + if c.PipelinePodTemplate != nil && c.PipelinePodTemplate.Spec.Container != nil { securityContext := c.PipelinePodTemplate.Spec.Container.SecurityContext diff --git a/runtime/kubernetes/generated/clientset/versioned/clientset.go b/runtime/kubernetes/generated/clientset/versioned/clientset.go index 539e0a9c..a06a9d90 100644 --- a/runtime/kubernetes/generated/clientset/versioned/clientset.go +++ b/runtime/kubernetes/generated/clientset/versioned/clientset.go @@ -8,10 +8,11 @@ import ( "fmt" "net/http" - velav1alpha1 "github.com/go-vela/worker/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1" discovery "k8s.io/client-go/discovery" rest "k8s.io/client-go/rest" flowcontrol "k8s.io/client-go/util/flowcontrol" + + velav1alpha1 "github.com/go-vela/worker/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1" ) type Interface interface { diff --git a/runtime/kubernetes/generated/clientset/versioned/fake/clientset_generated.go b/runtime/kubernetes/generated/clientset/versioned/fake/clientset_generated.go index 2a3563ee..225ee62c 100644 --- a/runtime/kubernetes/generated/clientset/versioned/fake/clientset_generated.go +++ b/runtime/kubernetes/generated/clientset/versioned/fake/clientset_generated.go @@ -5,14 +5,15 @@ package fake import ( - clientset "github.com/go-vela/worker/runtime/kubernetes/generated/clientset/versioned" - velav1alpha1 "github.com/go-vela/worker/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1" - fakevelav1alpha1 "github.com/go-vela/worker/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1/fake" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/discovery" fakediscovery "k8s.io/client-go/discovery/fake" "k8s.io/client-go/testing" + + clientset "github.com/go-vela/worker/runtime/kubernetes/generated/clientset/versioned" + velav1alpha1 "github.com/go-vela/worker/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1" + fakevelav1alpha1 "github.com/go-vela/worker/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1/fake" ) // NewSimpleClientset returns a clientset that will respond with the provided objects. diff --git a/runtime/kubernetes/generated/clientset/versioned/fake/register.go b/runtime/kubernetes/generated/clientset/versioned/fake/register.go index 124cfe21..f137c592 100644 --- a/runtime/kubernetes/generated/clientset/versioned/fake/register.go +++ b/runtime/kubernetes/generated/clientset/versioned/fake/register.go @@ -5,12 +5,13 @@ package fake import ( - velav1alpha1 "github.com/go-vela/worker/runtime/kubernetes/apis/vela/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" serializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + + velav1alpha1 "github.com/go-vela/worker/runtime/kubernetes/apis/vela/v1alpha1" ) var scheme = runtime.NewScheme() diff --git a/runtime/kubernetes/generated/clientset/versioned/scheme/register.go b/runtime/kubernetes/generated/clientset/versioned/scheme/register.go index 83678554..37c62a56 100644 --- a/runtime/kubernetes/generated/clientset/versioned/scheme/register.go +++ b/runtime/kubernetes/generated/clientset/versioned/scheme/register.go @@ -5,12 +5,13 @@ package scheme import ( - velav1alpha1 "github.com/go-vela/worker/runtime/kubernetes/apis/vela/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" serializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + + velav1alpha1 "github.com/go-vela/worker/runtime/kubernetes/apis/vela/v1alpha1" ) var Scheme = runtime.NewScheme() diff --git a/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1/fake/fake_pipelinepodstemplate.go b/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1/fake/fake_pipelinepodstemplate.go index 771a36ce..c6365861 100644 --- a/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1/fake/fake_pipelinepodstemplate.go +++ b/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1/fake/fake_pipelinepodstemplate.go @@ -7,13 +7,14 @@ package fake import ( "context" - v1alpha1 "github.com/go-vela/worker/runtime/kubernetes/apis/vela/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" labels "k8s.io/apimachinery/pkg/labels" schema "k8s.io/apimachinery/pkg/runtime/schema" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" testing "k8s.io/client-go/testing" + + v1alpha1 "github.com/go-vela/worker/runtime/kubernetes/apis/vela/v1alpha1" ) // FakePipelinePodsTemplates implements PipelinePodsTemplateInterface diff --git a/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1/fake/fake_vela_client.go b/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1/fake/fake_vela_client.go index ca035652..d5695a91 100644 --- a/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1/fake/fake_vela_client.go +++ b/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1/fake/fake_vela_client.go @@ -5,9 +5,10 @@ package fake import ( - v1alpha1 "github.com/go-vela/worker/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1" rest "k8s.io/client-go/rest" testing "k8s.io/client-go/testing" + + v1alpha1 "github.com/go-vela/worker/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1" ) type FakeVelaV1alpha1 struct { diff --git a/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1/pipelinepodstemplate.go b/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1/pipelinepodstemplate.go index b2edaee6..3b271f8f 100644 --- a/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1/pipelinepodstemplate.go +++ b/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1/pipelinepodstemplate.go @@ -8,12 +8,13 @@ import ( "context" "time" - v1alpha1 "github.com/go-vela/worker/runtime/kubernetes/apis/vela/v1alpha1" - scheme "github.com/go-vela/worker/runtime/kubernetes/generated/clientset/versioned/scheme" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" rest "k8s.io/client-go/rest" + + v1alpha1 "github.com/go-vela/worker/runtime/kubernetes/apis/vela/v1alpha1" + scheme "github.com/go-vela/worker/runtime/kubernetes/generated/clientset/versioned/scheme" ) // PipelinePodsTemplatesGetter has a method to return a PipelinePodsTemplateInterface. diff --git a/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1/vela_client.go b/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1/vela_client.go index fc128214..a7004432 100644 --- a/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1/vela_client.go +++ b/runtime/kubernetes/generated/clientset/versioned/typed/vela/v1alpha1/vela_client.go @@ -7,9 +7,10 @@ package v1alpha1 import ( "net/http" + rest "k8s.io/client-go/rest" + v1alpha1 "github.com/go-vela/worker/runtime/kubernetes/apis/vela/v1alpha1" "github.com/go-vela/worker/runtime/kubernetes/generated/clientset/versioned/scheme" - rest "k8s.io/client-go/rest" ) type VelaV1alpha1Interface interface {