From c9a1bf12ce112bd0496cff128416f1dd2b4aa907 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Wed, 16 Oct 2024 17:33:39 +0000 Subject: [PATCH 01/13] chore(envbuilder.go): prove the concept of build secret environment variables --- envbuilder.go | 38 +++++++++++++++++++------ examples/buildsecrets/Dockerfile | 3 ++ examples/buildsecrets/demonstrate.sh | 17 +++++++++++ examples/buildsecrets/devcontainer.json | 5 ++++ 4 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 examples/buildsecrets/Dockerfile create mode 100755 examples/buildsecrets/demonstrate.sh create mode 100644 examples/buildsecrets/devcontainer.json diff --git a/envbuilder.go b/envbuilder.go index 75829c2f..7c3796d1 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -37,6 +37,7 @@ import ( "github.com/coder/envbuilder/internal/ebutil" "github.com/coder/envbuilder/internal/workingdir" "github.com/coder/envbuilder/log" + "github.com/coder/serpent" "github.com/containerd/platforms" "github.com/distribution/distribution/v3/configuration" "github.com/distribution/distribution/v3/registry/handlers" @@ -411,6 +412,10 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro }) } + magicTempDir := workingdir.At(buildParams.BuildContext, workingdir.TempDir) + if err := opts.Filesystem.MkdirAll(magicTempDir.Path(), 0o755); err != nil { + return fmt.Errorf("create magic temp dir in build context: %w", err) + } // In order to allow 'resuming' envbuilder, embed the binary into the image // if it is being pushed. // As these files will be owned by root, it is considerate to clean up @@ -427,10 +432,6 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro if err := util.AddAllowedPathToDefaultIgnoreList(workingDir.Features()); err != nil { return fmt.Errorf("add features to ignore list: %w", err) } - magicTempDir := workingdir.At(buildParams.BuildContext, workingdir.TempDir) - if err := opts.Filesystem.MkdirAll(magicTempDir.Path(), 0o755); err != nil { - return fmt.Errorf("create magic temp dir in build context: %w", err) - } // Add the magic directives that embed the binary into the built image. buildParams.DockerfileContent += workingdir.Directives @@ -525,10 +526,10 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro if val, ok := os.LookupEnv("KANIKO_REGISTRY_MIRROR"); ok { registryMirror = strings.Split(val, ";") } - var destinations []string - if opts.CacheRepo != "" { - destinations = append(destinations, opts.CacheRepo) - } + var destinations = []string{"image"} + // if opts.CacheRepo != "" { + // destinations = append(destinations, opts.CacheRepo) + // } kOpts := &config.KanikoOptions{ // Boilerplate! CustomPlatform: platforms.Format(platforms.Normalize(platforms.DefaultSpec())), @@ -538,6 +539,7 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro RunStderr: stderrWriter, Destinations: destinations, NoPush: !opts.PushImage || len(destinations) == 0, + TarPath: filepath.Join(magicTempDir.Path(), "image.tar"), CacheRunLayers: true, CacheCopyLayers: true, ForceBuildMetadata: opts.PushImage, // Force layers with no changes to be cached, required for cache probing. @@ -573,13 +575,31 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro Reproducible: opts.PushImage, } + buildSecrets := serpent.ParseEnviron(os.Environ(), "ENVBUILDER_SECRET_") + if len(buildSecrets) > 0 { + secretsPath := filepath.Join(magicTempDir.Path(), "secrets") + opts.Logger(log.LevelDebug, "writing secrets to %q", secretsPath) + os.Mkdir(secretsPath, 0o755) + for _, secret := range buildSecrets { + secretPath := filepath.Join(magicTempDir.Path(), "secrets", secret.Name) + if err := os.WriteFile(secretPath, []byte(secret.Value), 0o644); err != nil { + return nil, fmt.Errorf("write secret %q: %w", secret.Name, err) + } + } + } + defer func() { + if err := os.RemoveAll(filepath.Join(magicTempDir.Path(), "secrets")); err != nil { + opts.Logger(log.LevelWarn, "failed to clean up secrets: %s", err) + } + }() + endStage := startStage("🏗️ Building image...") image, err := executor.DoBuild(kOpts) if err != nil { return nil, xerrors.Errorf("do build: %w", err) } endStage("🏗️ Built image!") - if opts.PushImage { + if opts.PushImage || true { endStage = startStage("🏗️ Pushing image...") if err := executor.DoPush(image, kOpts); err != nil { return nil, xerrors.Errorf("do push: %w", err) diff --git a/examples/buildsecrets/Dockerfile b/examples/buildsecrets/Dockerfile new file mode 100644 index 00000000..2ca23821 --- /dev/null +++ b/examples/buildsecrets/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine:latest + +RUN cat /workspace/.envbuilder.tmp/secrets/FOO | sha256sum > /secret_hash.txt \ No newline at end of file diff --git a/examples/buildsecrets/demonstrate.sh b/examples/buildsecrets/demonstrate.sh new file mode 100755 index 00000000..382d9ff9 --- /dev/null +++ b/examples/buildsecrets/demonstrate.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +./../../scripts/build.sh --base=build-secrets-envbuilder +docker run -it --rm \ + -e ENVBUILDER_INIT_SCRIPT='/bin/sh' \ + -e ENVBUILDER_WORKSPACE_FOLDER=/workspace \ + -e ENVBUILDER_SECRET_FOO='this is a secret' \ + -v $PWD:/workspace \ + build-secrets-envbuilder:latest + +# This script will drop you into a shell inside an envbuilder built alpine container. +# Notice that the secret that was set above is nowhere to be found. Yet, it's sha256 is +# present in /secret_hash.txt. This is a demonstration of how secrets can be passed to +# the build process without being exposed in the final running container. If you'd like to +# dig deeper, you can extract the built image, which is inside /workspace/.envbuilder.tmp. +# It's only there for demonstration purposes. We would not keep the tmp dir there +# in the final PR. \ No newline at end of file diff --git a/examples/buildsecrets/devcontainer.json b/examples/buildsecrets/devcontainer.json new file mode 100644 index 00000000..1933fd86 --- /dev/null +++ b/examples/buildsecrets/devcontainer.json @@ -0,0 +1,5 @@ +{ + "build": { + "dockerfile": "Dockerfile" + } +} \ No newline at end of file From 56924ed46ff65e137c5eaf1b65c252dac9d2300f Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Fri, 18 Oct 2024 14:58:14 +0000 Subject: [PATCH 02/13] feat(devcontainer): add support for build-secrets --- devcontainer/devcontainer.go | 6 ++-- examples/buildsecrets/Dockerfile | 9 ++++- examples/buildsecrets/demonstrate.sh | 4 +-- go.mod | 27 +++++++-------- go.sum | 52 ++++++++++++++-------------- scripts/Dockerfile | 2 +- 6 files changed, 53 insertions(+), 47 deletions(-) diff --git a/devcontainer/devcontainer.go b/devcontainer/devcontainer.go index 6135c0ef..0bf7cc35 100644 --- a/devcontainer/devcontainer.go +++ b/devcontainer/devcontainer.go @@ -400,11 +400,11 @@ func ImageFromDockerfile(dockerfileContent string) (name.Reference, error) { arg = strings.TrimSpace(arg) if strings.Contains(arg, "=") { parts := strings.SplitN(arg, "=", 2) - key, err := lexer.ProcessWord(parts[0], args) + key, _, err := lexer.ProcessWord(parts[0], shell.EnvsFromSlice(args)) if err != nil { return nil, fmt.Errorf("processing %q: %w", line, err) } - val, err := lexer.ProcessWord(parts[1], args) + val, _, err := lexer.ProcessWord(parts[1], shell.EnvsFromSlice(args)) if err != nil { return nil, fmt.Errorf("processing %q: %w", line, err) } @@ -421,7 +421,7 @@ func ImageFromDockerfile(dockerfileContent string) (name.Reference, error) { if imageRef == "" { return nil, fmt.Errorf("no FROM directive found") } - imageRef, err := lexer.ProcessWord(imageRef, args) + imageRef, _, err := lexer.ProcessWord(imageRef, shell.EnvsFromSlice(args)) if err != nil { return nil, fmt.Errorf("processing %q: %w", imageRef, err) } diff --git a/examples/buildsecrets/Dockerfile b/examples/buildsecrets/Dockerfile index 2ca23821..559f70a6 100644 --- a/examples/buildsecrets/Dockerfile +++ b/examples/buildsecrets/Dockerfile @@ -1,3 +1,10 @@ FROM alpine:latest -RUN cat /workspace/.envbuilder.tmp/secrets/FOO | sha256sum > /secret_hash.txt \ No newline at end of file +# Show that we can use secrets as envs +RUN --mount=type=secret,id=FOO,env=FOO echo $FOO > /foo_env.txt +# Show that we can use secrets as files in the default location +RUN --mount=type=secret,id=FOO cat /run/secrets/FOO > /foo_from_default_file.txt +# Show that we can use secrets as files in a custom location +RUN --mount=type=secret,id=FOO,target=/foo_custom_secret_location.txt cat /foo_custom_secret_location.txt > /foo_from_custom_file.txt +# Show that secrets are only available to the commands that mount them (bar.txt should be empty) +RUN --mount=type=secret,id=BAR,env=BAR echo $FOO > /bar.txt \ No newline at end of file diff --git a/examples/buildsecrets/demonstrate.sh b/examples/buildsecrets/demonstrate.sh index 382d9ff9..3786271e 100755 --- a/examples/buildsecrets/demonstrate.sh +++ b/examples/buildsecrets/demonstrate.sh @@ -4,9 +4,9 @@ docker run -it --rm \ -e ENVBUILDER_INIT_SCRIPT='/bin/sh' \ -e ENVBUILDER_WORKSPACE_FOLDER=/workspace \ - -e ENVBUILDER_SECRET_FOO='this is a secret' \ + -e KANIKO_SECRET_FOO='okay, new secret' \ -v $PWD:/workspace \ - build-secrets-envbuilder:latest + build-secrets-envbuilder:latest # This script will drop you into a shell inside an envbuilder built alpine container. # Notice that the secret that was set above is nowhere to be found. Yet, it's sha256 is diff --git a/go.mod b/go.mod index 9fa1d696..e30077f3 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.22.4 // There are a few options we need added to Kaniko! // See: https://github.com/GoogleContainerTools/kaniko/compare/main...coder:kaniko:main -replace github.com/GoogleContainerTools/kaniko => github.com/coder/kaniko v0.0.0-20240925122543-caa18967f374 +replace github.com/GoogleContainerTools/kaniko => github.com/coder/kaniko v0.0.0-20241018143344-245c70c3786d // Required to import codersdk due to gvisor dependency. replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20240702054557-aa558fbe5374 @@ -19,8 +19,8 @@ require ( github.com/coder/serpent v0.7.0 github.com/containerd/platforms v0.2.1 github.com/distribution/distribution/v3 v3.0.0-alpha.1 - github.com/docker/cli v27.2.0+incompatible - github.com/docker/docker v26.1.5+incompatible + github.com/docker/cli v27.2.1+incompatible + github.com/docker/docker v27.2.1+incompatible github.com/fatih/color v1.17.0 github.com/gliderlabs/ssh v0.3.7 github.com/go-git/go-billy/v5 v5.5.0 @@ -31,7 +31,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/mattn/go-isatty v0.0.20 - github.com/moby/buildkit v0.13.1 + github.com/moby/buildkit v0.16.0 github.com/otiai10/copy v1.14.0 github.com/prometheus/procfs v0.15.1 github.com/sirupsen/logrus v1.9.3 @@ -100,14 +100,12 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/charmbracelet/lipgloss v0.8.0 // indirect github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect - github.com/cilium/ebpf v0.12.3 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 // indirect github.com/coder/quartz v0.1.0 // indirect github.com/coder/terraform-provider-coder v0.23.0 // indirect github.com/containerd/cgroups v1.1.0 // indirect - github.com/containerd/cgroups/v3 v3.0.2 // indirect - github.com/containerd/containerd v1.7.19 // indirect + github.com/containerd/containerd v1.7.21 // indirect github.com/containerd/containerd/api v1.7.19 // indirect github.com/containerd/continuity v0.4.3 // indirect github.com/containerd/errdefs v0.1.0 // indirect @@ -115,10 +113,9 @@ require ( github.com/containerd/log v0.1.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect github.com/containerd/ttrpc v1.2.5 // indirect - github.com/containerd/typeurl/v2 v2.1.1 // indirect + github.com/containerd/typeurl/v2 v2.2.0 // indirect github.com/coreos/go-iptables v0.6.0 // indirect github.com/coreos/go-oidc/v3 v3.10.0 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect @@ -151,7 +148,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.2 // indirect github.com/google/nftables v0.2.0 // indirect - github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b // indirect + github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect github.com/gorilla/handlers v1.5.1 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect @@ -164,7 +161,7 @@ require ( github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/golang-lru/arc/v2 v2.0.5 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl/v2 v2.21.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-plugin-go v0.12.0 // indirect @@ -202,11 +199,12 @@ require ( github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/swarmkit/v2 v2.0.0-20230315203717-e28e8ba9bc83 // indirect github.com/moby/sys/mount v0.3.3 // indirect - github.com/moby/sys/mountinfo v0.7.1 // indirect + github.com/moby/sys/mountinfo v0.7.2 // indirect github.com/moby/sys/sequential v0.5.0 // indirect - github.com/moby/sys/signal v0.7.0 // indirect + github.com/moby/sys/signal v0.7.1 // indirect github.com/moby/sys/symlink v0.2.0 // indirect - github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/sys/user v0.3.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/muesli/reflow v0.3.0 // indirect @@ -245,6 +243,7 @@ require ( github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272 // indirect github.com/tcnksm/go-httpstat v0.2.0 // indirect github.com/tinylib/msgp v1.1.8 // indirect + github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 // indirect github.com/twpayne/go-vfs/v5 v5.0.4 // indirect github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a // indirect github.com/valyala/fasthttp v1.55.0 // indirect diff --git a/go.sum b/go.sum index 07dc01db..ee3b56a3 100644 --- a/go.sum +++ b/go.sum @@ -171,8 +171,8 @@ github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoC github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352 h1:L/EjCuZxs5tOcqqCaASj/nu65TRYEFcTt8qRQfHZXX0= github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352/go.mod h1:P1KoQSgnKEAG6Mnd3YlGzAophty+yKA9VV48LpfNRvo= -github.com/coder/kaniko v0.0.0-20240925122543-caa18967f374 h1:/cyXf0vTSwFh7evQqeWHXXl14aRfC4CsNIYxOenJytQ= -github.com/coder/kaniko v0.0.0-20240925122543-caa18967f374/go.mod h1:XoTDIhNF0Ll4tLmRYdOn31udU9w5zFrY2PME/crSRCA= +github.com/coder/kaniko v0.0.0-20241018143344-245c70c3786d h1:7H66IJmnOq6EZcT1WCRFi4gcntgGImL5xzzHJINk25E= +github.com/coder/kaniko v0.0.0-20241018143344-245c70c3786d/go.mod h1:L4Z5mwSAQ0z7OMbuwdYVG8X80+EVvyf5INYgzN4OkpA= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc= github.com/coder/quartz v0.1.0 h1:cLL+0g5l7xTf6ordRnUMMiZtRE8Sq5LxpghS63vEXrQ= @@ -187,10 +187,8 @@ github.com/coder/terraform-provider-coder v0.23.0 h1:DuNLWxhnGlXyG0g+OCAZRI6xd8+ github.com/coder/terraform-provider-coder v0.23.0/go.mod h1:wMun9UZ9HT2CzF6qPPBup1odzBpVUc0/xSFoXgdI3tk= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= -github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0= -github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE= -github.com/containerd/containerd v1.7.19 h1:/xQ4XRJ0tamDkdzrrBAUy/LE5nCcxFKdBm4EcPrSMEE= -github.com/containerd/containerd v1.7.19/go.mod h1:h4FtNYUUMB4Phr6v+xG89RYKj9XccvbNSCKjdufCrkc= +github.com/containerd/containerd v1.7.21 h1:USGXRK1eOC/SX0L195YgxTHb0a00anxajOzgfN0qrCA= +github.com/containerd/containerd v1.7.21/go.mod h1:e3Jz1rYRUZ2Lt51YrH9Rz0zPyJBOlSvB3ghr2jbVD8g= github.com/containerd/containerd/api v1.7.19 h1:VWbJL+8Ap4Ju2mx9c9qS1uFSB1OVYr5JJrW2yT5vFoA= github.com/containerd/containerd/api v1.7.19/go.mod h1:fwGavl3LNwAV5ilJ0sbrABL44AQxmNjDRcwheXDb6Ig= github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= @@ -207,15 +205,13 @@ github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk= github.com/containerd/ttrpc v1.2.5 h1:IFckT1EFQoFBMG4c3sMdT8EP3/aKfumK1msY+Ze4oLU= github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= -github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= -github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= +github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso= +github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g= github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk= github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU= github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -236,12 +232,12 @@ github.com/distribution/distribution/v3 v3.0.0-alpha.1 h1:jn7I1gvjOvmLztH1+1cLiU github.com/distribution/distribution/v3 v3.0.0-alpha.1/go.mod h1:LCp4JZp1ZalYg0W/TN05jarCQu+h4w7xc7ZfQF4Y/cY= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v27.2.0+incompatible h1:yHD1QEB1/0vr5eBNpu8tncu8gWxg8EydFPOSKHzXSMM= -github.com/docker/cli v27.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v27.2.1+incompatible h1:U5BPtiD0viUzjGAjV1p0MGB8eVA3L3cbIrnyWmSJI70= +github.com/docker/cli v27.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v26.1.5+incompatible h1:NEAxTwEjxV6VbBMBoGG3zPqbiJosIApZjxlbrG9q3/g= -github.com/docker/docker v26.1.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.2.1+incompatible h1:fQdiLfW7VLscyoeYEBz7/J8soYFDZV1u6VW6gJEjNMI= +github.com/docker/docker v27.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= @@ -394,8 +390,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/nftables v0.2.0 h1:PbJwaBmbVLzpeldoeUKGkE2RjstrjPKMl6oLrfEJ6/8= github.com/google/nftables v0.2.0/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= -github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b h1:h9U78+dx9a4BKdQkBBos92HalKpaGKHrp+3Uo6yTodo= -github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -439,8 +435,8 @@ github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iP github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= -github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= -github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hc-install v0.7.0 h1:Uu9edVqjKQxxuD28mR5TikkKDd/p55S8vzPC1659aBk= github.com/hashicorp/hc-install v0.7.0/go.mod h1:ELmmzZlGnEcqoUMKUuykHaPCIR1sYLYX+KSggWSKZuA= github.com/hashicorp/hcl/v2 v2.21.0 h1:lve4q/o/2rqwYOgUg3y3V2YPyD1/zkCLGjIV74Jit14= @@ -556,8 +552,8 @@ github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374 github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/buildkit v0.13.1 h1:L8afOFhPq2RPJJSr/VyzbufwID7jquZVB7oFHbPRcPE= -github.com/moby/buildkit v0.13.1/go.mod h1:aNmNQKLBFYAOFuzQjR3VA27/FijlvtBD1pjNwTSN37k= +github.com/moby/buildkit v0.16.0 h1:wOVBj1o5YNVad/txPQNXUXdelm7Hs/i0PUFjzbK0VKE= +github.com/moby/buildkit v0.16.0/go.mod h1:Xqx/5GlrqE1yIRORk0NSCVDFpQAU1WjlT6KHYZdisIQ= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= @@ -569,16 +565,18 @@ github.com/moby/swarmkit/v2 v2.0.0-20230315203717-e28e8ba9bc83/go.mod h1:GvjR7mC github.com/moby/sys/mount v0.3.3 h1:fX1SVkXFJ47XWDoeFW4Sq7PdQJnV2QIDZAqjNqgEjUs= github.com/moby/sys/mount v0.3.3/go.mod h1:PBaEorSNTLG5t/+4EgukEQVlAvVEc6ZjTySwKdqp5K0= github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= -github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g= -github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= -github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI= -github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= +github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0= +github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8= github.com/moby/sys/symlink v0.2.0 h1:tk1rOM+Ljp0nFmfOIBtlV3rTDlWOwFRhjEeAhZB0nZc= github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs= -github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= -github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -729,6 +727,8 @@ github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ0 github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= +github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 h1:7I5c2Ig/5FgqkYOh/N87NzoyI9U15qUPXhDD8uCupv8= +github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twpayne/go-vfs/v5 v5.0.4 h1:/ne3h+rW7f5YOyOFguz+3ztfUwzOLR0Vts3y0mMAitg= diff --git a/scripts/Dockerfile b/scripts/Dockerfile index 6259407b..a8ba6941 100644 --- a/scripts/Dockerfile +++ b/scripts/Dockerfile @@ -3,6 +3,6 @@ ARG TARGETARCH COPY envbuilder-${TARGETARCH} /.envbuilder/bin/envbuilder -ENV KANIKO_DIR /.envbuilder +ENV KANIKO_DIR=/.envbuilder ENTRYPOINT ["/.envbuilder/bin/envbuilder"] From 141607707149e65e2a6c9b396cdb46dd5cc998a7 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 24 Oct 2024 08:54:35 +0000 Subject: [PATCH 03/13] feat(devcontainer): add support for build-secrets --- envbuilder.go | 31 ++----- examples/buildsecrets/Dockerfile | 10 -- examples/buildsecrets/demonstrate.sh | 17 ---- examples/buildsecrets/devcontainer.json | 5 - go.mod | 4 +- go.sum | 8 +- integration/integration_test.go | 63 +++++++++++++ options/secrets.go | 45 +++++++++ options/secrets_test.go | 116 ++++++++++++++++++++++++ 9 files changed, 239 insertions(+), 60 deletions(-) delete mode 100644 examples/buildsecrets/Dockerfile delete mode 100755 examples/buildsecrets/demonstrate.sh delete mode 100644 examples/buildsecrets/devcontainer.json create mode 100644 options/secrets.go create mode 100644 options/secrets_test.go diff --git a/envbuilder.go b/envbuilder.go index 7c3796d1..60f8cffe 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -37,7 +37,6 @@ import ( "github.com/coder/envbuilder/internal/ebutil" "github.com/coder/envbuilder/internal/workingdir" "github.com/coder/envbuilder/log" - "github.com/coder/serpent" "github.com/containerd/platforms" "github.com/distribution/distribution/v3/configuration" "github.com/distribution/distribution/v3/registry/handlers" @@ -527,9 +526,14 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro registryMirror = strings.Split(val, ";") } var destinations = []string{"image"} - // if opts.CacheRepo != "" { - // destinations = append(destinations, opts.CacheRepo) - // } + if opts.CacheRepo != "" { + destinations = append(destinations, opts.CacheRepo) + } + + buildSecrets := options.GetBuildSecrets(os.Environ()) + // Ensure that build secrets do not make it into the runtime environment or the setup script: + options.ClearBuildSecrets() + kOpts := &config.KanikoOptions{ // Boilerplate! CustomPlatform: platforms.Format(platforms.Normalize(platforms.DefaultSpec())), @@ -555,6 +559,7 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro }, ForceUnpack: true, BuildArgs: buildParams.BuildArgs, + BuildSecrets: buildSecrets, CacheRepo: opts.CacheRepo, Cache: opts.CacheRepo != "" || opts.BaseImageCacheDir != "", DockerfilePath: buildParams.DockerfilePath, @@ -575,24 +580,6 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro Reproducible: opts.PushImage, } - buildSecrets := serpent.ParseEnviron(os.Environ(), "ENVBUILDER_SECRET_") - if len(buildSecrets) > 0 { - secretsPath := filepath.Join(magicTempDir.Path(), "secrets") - opts.Logger(log.LevelDebug, "writing secrets to %q", secretsPath) - os.Mkdir(secretsPath, 0o755) - for _, secret := range buildSecrets { - secretPath := filepath.Join(magicTempDir.Path(), "secrets", secret.Name) - if err := os.WriteFile(secretPath, []byte(secret.Value), 0o644); err != nil { - return nil, fmt.Errorf("write secret %q: %w", secret.Name, err) - } - } - } - defer func() { - if err := os.RemoveAll(filepath.Join(magicTempDir.Path(), "secrets")); err != nil { - opts.Logger(log.LevelWarn, "failed to clean up secrets: %s", err) - } - }() - endStage := startStage("🏗️ Building image...") image, err := executor.DoBuild(kOpts) if err != nil { diff --git a/examples/buildsecrets/Dockerfile b/examples/buildsecrets/Dockerfile deleted file mode 100644 index 559f70a6..00000000 --- a/examples/buildsecrets/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM alpine:latest - -# Show that we can use secrets as envs -RUN --mount=type=secret,id=FOO,env=FOO echo $FOO > /foo_env.txt -# Show that we can use secrets as files in the default location -RUN --mount=type=secret,id=FOO cat /run/secrets/FOO > /foo_from_default_file.txt -# Show that we can use secrets as files in a custom location -RUN --mount=type=secret,id=FOO,target=/foo_custom_secret_location.txt cat /foo_custom_secret_location.txt > /foo_from_custom_file.txt -# Show that secrets are only available to the commands that mount them (bar.txt should be empty) -RUN --mount=type=secret,id=BAR,env=BAR echo $FOO > /bar.txt \ No newline at end of file diff --git a/examples/buildsecrets/demonstrate.sh b/examples/buildsecrets/demonstrate.sh deleted file mode 100755 index 3786271e..00000000 --- a/examples/buildsecrets/demonstrate.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -./../../scripts/build.sh --base=build-secrets-envbuilder -docker run -it --rm \ - -e ENVBUILDER_INIT_SCRIPT='/bin/sh' \ - -e ENVBUILDER_WORKSPACE_FOLDER=/workspace \ - -e KANIKO_SECRET_FOO='okay, new secret' \ - -v $PWD:/workspace \ - build-secrets-envbuilder:latest - -# This script will drop you into a shell inside an envbuilder built alpine container. -# Notice that the secret that was set above is nowhere to be found. Yet, it's sha256 is -# present in /secret_hash.txt. This is a demonstration of how secrets can be passed to -# the build process without being exposed in the final running container. If you'd like to -# dig deeper, you can extract the built image, which is inside /workspace/.envbuilder.tmp. -# It's only there for demonstration purposes. We would not keep the tmp dir there -# in the final PR. \ No newline at end of file diff --git a/examples/buildsecrets/devcontainer.json b/examples/buildsecrets/devcontainer.json deleted file mode 100644 index 1933fd86..00000000 --- a/examples/buildsecrets/devcontainer.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "build": { - "dockerfile": "Dockerfile" - } -} \ No newline at end of file diff --git a/go.mod b/go.mod index e30077f3..fdacce83 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.22.4 // There are a few options we need added to Kaniko! // See: https://github.com/GoogleContainerTools/kaniko/compare/main...coder:kaniko:main -replace github.com/GoogleContainerTools/kaniko => github.com/coder/kaniko v0.0.0-20241018143344-245c70c3786d +replace github.com/GoogleContainerTools/kaniko => github.com/coder/kaniko v0.0.0-20241024065816-8f144a699d23 // Required to import codersdk due to gvisor dependency. replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20240702054557-aa558fbe5374 @@ -20,7 +20,7 @@ require ( github.com/containerd/platforms v0.2.1 github.com/distribution/distribution/v3 v3.0.0-alpha.1 github.com/docker/cli v27.2.1+incompatible - github.com/docker/docker v27.2.1+incompatible + github.com/docker/docker v27.3.1+incompatible github.com/fatih/color v1.17.0 github.com/gliderlabs/ssh v0.3.7 github.com/go-git/go-billy/v5 v5.5.0 diff --git a/go.sum b/go.sum index ee3b56a3..be5c024d 100644 --- a/go.sum +++ b/go.sum @@ -171,8 +171,8 @@ github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoC github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352 h1:L/EjCuZxs5tOcqqCaASj/nu65TRYEFcTt8qRQfHZXX0= github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352/go.mod h1:P1KoQSgnKEAG6Mnd3YlGzAophty+yKA9VV48LpfNRvo= -github.com/coder/kaniko v0.0.0-20241018143344-245c70c3786d h1:7H66IJmnOq6EZcT1WCRFi4gcntgGImL5xzzHJINk25E= -github.com/coder/kaniko v0.0.0-20241018143344-245c70c3786d/go.mod h1:L4Z5mwSAQ0z7OMbuwdYVG8X80+EVvyf5INYgzN4OkpA= +github.com/coder/kaniko v0.0.0-20241024065816-8f144a699d23 h1:cBCxME1UiClC5EFaniW2B4xX2gbTsTlOrH6X4SO9pWY= +github.com/coder/kaniko v0.0.0-20241024065816-8f144a699d23/go.mod h1:3rM/KOQ4LgF8mE+O1P6pLDa/E57mzxIxNdUOMKi1qpg= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc= github.com/coder/quartz v0.1.0 h1:cLL+0g5l7xTf6ordRnUMMiZtRE8Sq5LxpghS63vEXrQ= @@ -236,8 +236,8 @@ github.com/docker/cli v27.2.1+incompatible h1:U5BPtiD0viUzjGAjV1p0MGB8eVA3L3cbIr github.com/docker/cli v27.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v27.2.1+incompatible h1:fQdiLfW7VLscyoeYEBz7/J8soYFDZV1u6VW6gJEjNMI= -github.com/docker/docker v27.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= diff --git a/integration/integration_test.go b/integration/integration_test.go index deb21ef6..3d854ba3 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -897,6 +897,69 @@ func TestUnsetOptionsEnv(t *testing.T) { } } +func TestUnsetSecretEnvs(t *testing.T) { + t.Parallel() + + // Ensures that a Git repository with a devcontainer.json is cloned and built. + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ + ".devcontainer/devcontainer.json": `{ + "name": "Test", + "build": { + "dockerfile": "Dockerfile" + }, + }`, + ".devcontainer/Dockerfile": "FROM " + testImageAlpine + "\nENV FROM_DOCKERFILE=foo", + }, + }) + ctr, err := runEnvbuilder(t, runOpts{env: []string{ + envbuilderEnv("GIT_URL", srv.URL), + envbuilderEnv("GIT_PASSWORD", "supersecret"), + options.EnvWithBuildSecretPrefix("FOO", "foo"), + envbuilderEnv("INIT_SCRIPT", "env > /root/env.txt && sleep infinity"), + }}) + require.NoError(t, err) + + output := execContainer(t, ctr, "cat /root/env.txt") + envsAvailableToInitScript := strings.Split(strings.TrimSpace(output), "\n") + + leftoverBuildSecrets := options.GetBuildSecrets(envsAvailableToInitScript) + require.Empty(t, leftoverBuildSecrets, "build secrets should not be available to init script") +} + +func TestBuildSecrets(t *testing.T) { + t.Parallel() + + buildSecretVal := "foo" + + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ + ".devcontainer/devcontainer.json": `{ + "name": "Test", + "build": { + "dockerfile": "Dockerfile" + }, + }`, + ".devcontainer/Dockerfile": "FROM " + testImageAlpine + + "\nRUN --mount=type=secret,id=FOO cat /run/secrets/FOO > /foo_from_file" + + "\nRUN --mount=type=secret,id=FOO,env=FOO echo $FOO > /foo_from_env", + }, + }) + + ctr, err := runEnvbuilder(t, runOpts{env: []string{ + envbuilderEnv("GIT_URL", srv.URL), + envbuilderEnv("GIT_PASSWORD", "supersecret"), + options.EnvWithBuildSecretPrefix("FOO", buildSecretVal), + }}) + require.NoError(t, err) + + output := execContainer(t, ctr, "cat /foo_from_file") + require.Equal(t, buildSecretVal, strings.TrimSpace(output)) + + output = execContainer(t, ctr, "cat /foo_from_env") + require.Equal(t, buildSecretVal, strings.TrimSpace(output)) +} + func TestLifecycleScripts(t *testing.T) { t.Parallel() diff --git a/options/secrets.go b/options/secrets.go new file mode 100644 index 00000000..fd07fbcc --- /dev/null +++ b/options/secrets.go @@ -0,0 +1,45 @@ +package options + +import ( + "fmt" + "os" + + "github.com/coder/serpent" +) + +var buildSecretPrefix = fmt.Sprintf("%sBUILD_SECRET_", envPrefix) + +func EnvWithBuildSecretPrefix(secretName, secretValue string) string { + return fmt.Sprintf("%s%s=%s", buildSecretPrefix, secretName, secretValue) +} + +// GetBuildSecrets sources build secrets from the OS environment. +// +// In a normal docker build, build secrets would be passed in via the +// `docker build --secret` flag. envbuilder is more analogous to a +// `docker run` that just happens to build its own container. It doesn't have +// access to the `--secret` flag. As an alternative, we source these from the +// envbuilder process environment. +func GetBuildSecrets(environ []string) []string { + buildSecrets := serpent.ParseEnviron(environ, buildSecretPrefix).ToOS() + return buildSecrets +} + +// ClearBuildSecrets unsets all build secrets from the process environment. +// NOTE: This does not remove them from /proc/self/environ. They are still visible +// there unless execve(2) is called. +// +// Unlike runtime secrets in the devcontainer spec or orchestration systems like +// Kubernetes, build secrets should not be available at run time. envbuilder blurs +// the line between build time and run time by transitioning from one to the other +// within the same process in the same container. +// +// These build secrets should not make it into the runtime environment of the runtime +// container init process. It is therefore useful to unset build secret environment +// variables to ensure they aren't accidentally passed into the exec call. +func ClearBuildSecrets() { + buildSecrets := serpent.ParseEnviron(os.Environ(), buildSecretPrefix) + for _, secret := range buildSecrets { + os.Unsetenv(buildSecretPrefix + secret.Name) + } +} diff --git a/options/secrets_test.go b/options/secrets_test.go new file mode 100644 index 00000000..290945d8 --- /dev/null +++ b/options/secrets_test.go @@ -0,0 +1,116 @@ +package options_test + +import ( + "os" + "testing" + + "github.com/coder/envbuilder/options" + "github.com/stretchr/testify/assert" +) + +func TestGetBuildSecrets(t *testing.T) { + t.Parallel() + + t.Run("no secrets", func(t *testing.T) { + t.Parallel() + + // given + os.Clearenv() + + // when + secrets := options.GetBuildSecrets() + + // then + assert.Empty(t, secrets) + }) + + t.Run("single secret", func(t *testing.T) { + t.Parallel() + + // given + os.Clearenv() + os.Setenv("ENVBUILDER_BUILD_SECRET_FOO", "bar") + + // when + secrets := options.GetBuildSecrets() + + // then + assert.Equal(t, []string{"FOO=bar"}, secrets) + }) + + t.Run("multiple secrets", func(t *testing.T) { + t.Parallel() + + // given + os.Clearenv() + os.Setenv("ENVBUILDER_BUILD_SECRET_FOO", "bar") + os.Setenv("ENVBUILDER_BUILD_SECRET_BAZ", "qux") + + // when + secrets := options.GetBuildSecrets() + + // then + assert.ElementsMatch(t, []string{"FOO=bar", "BAZ=qux"}, secrets) + }) +} + +func TestClearBuildSecrets(t *testing.T) { + t.Parallel() + + t.Run("no secrets", func(t *testing.T) { + t.Parallel() + + // given + os.Clearenv() + + // when + options.ClearBuildSecrets() + + // then + assert.Empty(t, os.Environ()) + }) + + t.Run("single secret", func(t *testing.T) { + t.Parallel() + + // given + os.Clearenv() + os.Setenv("ENVBUILDER_BUILD_SECRET_FOO", "bar") + + // when + options.ClearBuildSecrets() + + // then + assert.Empty(t, os.Environ()) + }) + + t.Run("multiple secrets", func(t *testing.T) { + t.Parallel() + + // given + os.Clearenv() + os.Setenv("ENVBUILDER_BUILD_SECRET_FOO", "bar") + os.Setenv("ENVBUILDER_BUILD_SECRET_BAZ", "qux") + + // when + options.ClearBuildSecrets() + + // then + assert.Empty(t, os.Environ()) + }) + + t.Run("only build secrets are cleared", func(t *testing.T) { + t.Parallel() + + // given + os.Clearenv() + os.Setenv("ENVBUILDER_BUILD_SECRET_FOO", "foo") + os.Setenv("FOO", "foo") + + // when + options.ClearBuildSecrets() + + // then + assert.Equal(t, []string{"FOO=foo"}, os.Environ()) + }) +} From 0b4ea02eee7bd5ea2ea59113fbbe6c32156e145c Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 24 Oct 2024 10:27:43 +0000 Subject: [PATCH 04/13] fix(envbuilder.go): fix self review notes and linting --- envbuilder.go | 13 ++++++------- options/secrets_test.go | 6 +++--- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/envbuilder.go b/envbuilder.go index 60f8cffe..9dc3dc88 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -411,10 +411,6 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro }) } - magicTempDir := workingdir.At(buildParams.BuildContext, workingdir.TempDir) - if err := opts.Filesystem.MkdirAll(magicTempDir.Path(), 0o755); err != nil { - return fmt.Errorf("create magic temp dir in build context: %w", err) - } // In order to allow 'resuming' envbuilder, embed the binary into the image // if it is being pushed. // As these files will be owned by root, it is considerate to clean up @@ -431,6 +427,10 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro if err := util.AddAllowedPathToDefaultIgnoreList(workingDir.Features()); err != nil { return fmt.Errorf("add features to ignore list: %w", err) } + magicTempDir := workingdir.At(buildParams.BuildContext, workingdir.TempDir) + if err := opts.Filesystem.MkdirAll(magicTempDir.Path(), 0o755); err != nil { + return fmt.Errorf("create magic temp dir in build context: %w", err) + } // Add the magic directives that embed the binary into the built image. buildParams.DockerfileContent += workingdir.Directives @@ -525,7 +525,7 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro if val, ok := os.LookupEnv("KANIKO_REGISTRY_MIRROR"); ok { registryMirror = strings.Split(val, ";") } - var destinations = []string{"image"} + destinations := []string{} if opts.CacheRepo != "" { destinations = append(destinations, opts.CacheRepo) } @@ -543,7 +543,6 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro RunStderr: stderrWriter, Destinations: destinations, NoPush: !opts.PushImage || len(destinations) == 0, - TarPath: filepath.Join(magicTempDir.Path(), "image.tar"), CacheRunLayers: true, CacheCopyLayers: true, ForceBuildMetadata: opts.PushImage, // Force layers with no changes to be cached, required for cache probing. @@ -586,7 +585,7 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro return nil, xerrors.Errorf("do build: %w", err) } endStage("🏗️ Built image!") - if opts.PushImage || true { + if opts.PushImage { endStage = startStage("🏗️ Pushing image...") if err := executor.DoPush(image, kOpts); err != nil { return nil, xerrors.Errorf("do push: %w", err) diff --git a/options/secrets_test.go b/options/secrets_test.go index 290945d8..e5f7b7c8 100644 --- a/options/secrets_test.go +++ b/options/secrets_test.go @@ -18,7 +18,7 @@ func TestGetBuildSecrets(t *testing.T) { os.Clearenv() // when - secrets := options.GetBuildSecrets() + secrets := options.GetBuildSecrets(os.Environ()) // then assert.Empty(t, secrets) @@ -32,7 +32,7 @@ func TestGetBuildSecrets(t *testing.T) { os.Setenv("ENVBUILDER_BUILD_SECRET_FOO", "bar") // when - secrets := options.GetBuildSecrets() + secrets := options.GetBuildSecrets(os.Environ()) // then assert.Equal(t, []string{"FOO=bar"}, secrets) @@ -47,7 +47,7 @@ func TestGetBuildSecrets(t *testing.T) { os.Setenv("ENVBUILDER_BUILD_SECRET_BAZ", "qux") // when - secrets := options.GetBuildSecrets() + secrets := options.GetBuildSecrets(os.Environ()) // then assert.ElementsMatch(t, []string{"FOO=bar", "BAZ=qux"}, secrets) From 9227192d10c4a8751376a7c1f3575dfd3eeb19c6 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 24 Oct 2024 10:32:51 +0000 Subject: [PATCH 05/13] fix(envbuilder.go): fix linting --- integration/integration_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/integration/integration_test.go b/integration/integration_test.go index 3d854ba3..db8c776e 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -34,7 +34,6 @@ import ( "github.com/coder/envbuilder/testutil/registrytest" clitypes "github.com/docker/cli/cli/config/types" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/image" @@ -2376,14 +2375,14 @@ func execContainer(t *testing.T, containerID, command string) string { cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) require.NoError(t, err) defer cli.Close() - execConfig := types.ExecConfig{ + execConfig := container.ExecOptions{ AttachStdout: true, AttachStderr: true, Cmd: []string{"/bin/sh", "-c", command}, } execID, err := cli.ContainerExecCreate(ctx, containerID, execConfig) require.NoError(t, err) - resp, err := cli.ContainerExecAttach(ctx, execID.ID, types.ExecStartCheck{}) + resp, err := cli.ContainerExecAttach(ctx, execID.ID, container.ExecAttachOptions{}) require.NoError(t, err) defer resp.Close() var buf bytes.Buffer From 7fff300bee34c813891f183c1939da5836302244 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Fri, 25 Oct 2024 14:42:45 +0000 Subject: [PATCH 06/13] fix(envbuilder.go): add unit tests for build secret filtering --- envbuilder.go | 2 +- options/secrets.go | 6 +- options/secrets_test.go | 232 +++++++++++++++++++++++----------------- 3 files changed, 137 insertions(+), 103 deletions(-) diff --git a/envbuilder.go b/envbuilder.go index 3f7dc117..dc98567e 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -532,7 +532,7 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro buildSecrets := options.GetBuildSecrets(os.Environ()) // Ensure that build secrets do not make it into the runtime environment or the setup script: - options.ClearBuildSecrets() + options.ClearBuildSecretsFromProcessEnvironment() kOpts := &config.KanikoOptions{ // Boilerplate! diff --git a/options/secrets.go b/options/secrets.go index fd07fbcc..bcaaa837 100644 --- a/options/secrets.go +++ b/options/secrets.go @@ -13,7 +13,7 @@ func EnvWithBuildSecretPrefix(secretName, secretValue string) string { return fmt.Sprintf("%s%s=%s", buildSecretPrefix, secretName, secretValue) } -// GetBuildSecrets sources build secrets from the OS environment. +// GetBuildSecrets sources build secrets from the given environment. // // In a normal docker build, build secrets would be passed in via the // `docker build --secret` flag. envbuilder is more analogous to a @@ -25,7 +25,7 @@ func GetBuildSecrets(environ []string) []string { return buildSecrets } -// ClearBuildSecrets unsets all build secrets from the process environment. +// ClearBuildSecretsFromProcessEnvironment unsets all build secrets from the process environment. // NOTE: This does not remove them from /proc/self/environ. They are still visible // there unless execve(2) is called. // @@ -37,7 +37,7 @@ func GetBuildSecrets(environ []string) []string { // These build secrets should not make it into the runtime environment of the runtime // container init process. It is therefore useful to unset build secret environment // variables to ensure they aren't accidentally passed into the exec call. -func ClearBuildSecrets() { +func ClearBuildSecretsFromProcessEnvironment() { buildSecrets := serpent.ParseEnviron(os.Environ(), buildSecretPrefix) for _, secret := range buildSecrets { os.Unsetenv(buildSecretPrefix + secret.Name) diff --git a/options/secrets_test.go b/options/secrets_test.go index e5f7b7c8..5b797976 100644 --- a/options/secrets_test.go +++ b/options/secrets_test.go @@ -2,115 +2,149 @@ package options_test import ( "os" + "strings" "testing" "github.com/coder/envbuilder/options" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGetBuildSecrets(t *testing.T) { - t.Parallel() - - t.Run("no secrets", func(t *testing.T) { - t.Parallel() - - // given - os.Clearenv() - - // when - secrets := options.GetBuildSecrets(os.Environ()) - - // then - assert.Empty(t, secrets) - }) - - t.Run("single secret", func(t *testing.T) { - t.Parallel() - - // given - os.Clearenv() - os.Setenv("ENVBUILDER_BUILD_SECRET_FOO", "bar") - - // when - secrets := options.GetBuildSecrets(os.Environ()) - - // then - assert.Equal(t, []string{"FOO=bar"}, secrets) - }) - - t.Run("multiple secrets", func(t *testing.T) { - t.Parallel() - - // given - os.Clearenv() - os.Setenv("ENVBUILDER_BUILD_SECRET_FOO", "bar") - os.Setenv("ENVBUILDER_BUILD_SECRET_BAZ", "qux") - - // when - secrets := options.GetBuildSecrets(os.Environ()) - - // then - assert.ElementsMatch(t, []string{"FOO=bar", "BAZ=qux"}, secrets) - }) + tests := []struct { + name string + envVars map[string]string + expectedSecrets []string + }{ + { + name: "no secrets set", + envVars: map[string]string{}, + expectedSecrets: []string{}, + }, + { + name: "single secret", + envVars: map[string]string{ + "ENVBUILDER_BUILD_SECRET_FOO": "bar", + }, + expectedSecrets: []string{"FOO=bar"}, + }, + { + name: "multiple secrets", + envVars: map[string]string{ + "ENVBUILDER_BUILD_SECRET_FOO": "bar", + "NOT_A_SECRET": "baz", + "ENVBUILDER_BUILD_SECRET_BAZ": "qux", + }, + expectedSecrets: []string{"FOO=bar", "BAZ=qux"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + envSnapshot := snapshotEnv() + t.Cleanup(func() { assertEnvUnchanged(t, envSnapshot) }) + + options.ClearBuildSecretsFromProcessEnvironment() + require.Empty(t, options.GetBuildSecrets(os.Environ())) + + // Set environment variables for the test case + for key, value := range tt.envVars { + t.Setenv(key, value) + } + + // when + secrets := options.GetBuildSecrets(os.Environ()) + + // then + assert.ElementsMatch(t, tt.expectedSecrets, secrets) + }) + } } func TestClearBuildSecrets(t *testing.T) { - t.Parallel() - - t.Run("no secrets", func(t *testing.T) { - t.Parallel() - - // given - os.Clearenv() - - // when - options.ClearBuildSecrets() - - // then - assert.Empty(t, os.Environ()) - }) - - t.Run("single secret", func(t *testing.T) { - t.Parallel() - - // given - os.Clearenv() - os.Setenv("ENVBUILDER_BUILD_SECRET_FOO", "bar") - - // when - options.ClearBuildSecrets() - - // then - assert.Empty(t, os.Environ()) - }) - - t.Run("multiple secrets", func(t *testing.T) { - t.Parallel() - - // given - os.Clearenv() - os.Setenv("ENVBUILDER_BUILD_SECRET_FOO", "bar") - os.Setenv("ENVBUILDER_BUILD_SECRET_BAZ", "qux") - - // when - options.ClearBuildSecrets() - - // then - assert.Empty(t, os.Environ()) - }) - - t.Run("only build secrets are cleared", func(t *testing.T) { - t.Parallel() - - // given - os.Clearenv() - os.Setenv("ENVBUILDER_BUILD_SECRET_FOO", "foo") - os.Setenv("FOO", "foo") + tests := []struct { + name string + initialEnvVars map[string]string + expectedSecretsBeforeClear []string + expectedSecretsAfterClear []string + expectedEnvironAfterClear []string + }{ + { + name: "single secret", + initialEnvVars: map[string]string{ + "ENVBUILDER_BUILD_SECRET_FOO": "bar", + }, + expectedSecretsBeforeClear: []string{"FOO=bar"}, + expectedSecretsAfterClear: []string{}, + }, + { + name: "multiple secrets", + initialEnvVars: map[string]string{ + "ENVBUILDER_BUILD_SECRET_FOO": "bar", + "ENVBUILDER_BUILD_SECRET_BAZ": "qux", + }, + expectedSecretsBeforeClear: []string{"FOO=bar", "BAZ=qux"}, + expectedSecretsAfterClear: []string{}, + }, + { + name: "only build secrets are cleared", + initialEnvVars: map[string]string{ + "ENVBUILDER_BUILD_SECRET_FOO": "foo", + "BAR": "bar", + }, + expectedSecretsBeforeClear: []string{"FOO=foo"}, + expectedSecretsAfterClear: []string{}, + expectedEnvironAfterClear: []string{"BAR=bar"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + envSnapshot := snapshotEnv() + t.Cleanup(func() { assertEnvUnchanged(t, envSnapshot) }) + + // Set environment variables for the test case + for key, value := range tt.initialEnvVars { + t.Setenv(key, value) + } + + // Verify secrets before clearing + secrets := options.GetBuildSecrets(os.Environ()) + assert.ElementsMatch(t, tt.expectedSecretsBeforeClear, secrets) + + // Clear the secrets + options.ClearBuildSecretsFromProcessEnvironment() + + // Verify secrets after clearing + environ := os.Environ() + secrets = options.GetBuildSecrets(environ) + assert.ElementsMatch(t, tt.expectedSecretsAfterClear, secrets) + for _, env := range tt.expectedSecretsAfterClear { + assert.Contains(t, environ, env) + } + }) + } +} - // when - options.ClearBuildSecrets() +func snapshotEnv() map[string]string { + envSnapshot := make(map[string]string) + for _, envVar := range os.Environ() { + parts := strings.SplitN(envVar, "=", 2) + envSnapshot[parts[0]] = parts[1] + } + return envSnapshot +} - // then - assert.Equal(t, []string{"FOO=foo"}, os.Environ()) - }) +func assertEnvUnchanged(t *testing.T, snapshot map[string]string) { + for key, expectedValue := range snapshot { + currentValue, exists := os.LookupEnv(key) + assert.True(t, exists, "expected environment variable %s to be set", key) + assert.Equal(t, expectedValue, currentValue, "expected environment variable %s to be unchanged", key) + } + for _, envVar := range os.Environ() { + key := strings.SplitN(envVar, "=", 2)[0] + if _, exists := snapshot[key]; !exists { + assert.Fail(t, "unexpected environment variable set", "variable %s was set during the test", key) + } + } } From c112f2597d13dc67a0be2c82a9b64bbfaffa158d Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Fri, 25 Oct 2024 14:53:53 +0000 Subject: [PATCH 07/13] fix(integration): add integration tests for build secrets --- integration/integration_test.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/integration/integration_test.go b/integration/integration_test.go index db8c776e..f3700ed2 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -940,8 +940,15 @@ func TestBuildSecrets(t *testing.T) { }, }`, ".devcontainer/Dockerfile": "FROM " + testImageAlpine + + // Test whether build secrets are written to the default location "\nRUN --mount=type=secret,id=FOO cat /run/secrets/FOO > /foo_from_file" + - "\nRUN --mount=type=secret,id=FOO,env=FOO echo $FOO > /foo_from_env", + // Test whether: + // * build secrets are written to env + // * build secrets are written to a custom target + // * build secrets are both written to env and target if both are specified + "\nRUN --mount=type=secret,id=FOO,env=FOO,target=/etc/foo echo $FOO > /foo_from_env && cat /etc/foo > /foo_from_custom_target" + + // Test what happens when you specify the same secret twice + "\nRUN --mount=type=secret,id=FOO,env=FOO --mount=type=secret,id=FOO,target=/etc/foo echo $FOO > /duplicate_foo_from_env && cat /etc/foo > /duplicate_foo_from_custom_target", }, }) @@ -957,6 +964,15 @@ func TestBuildSecrets(t *testing.T) { output = execContainer(t, ctr, "cat /foo_from_env") require.Equal(t, buildSecretVal, strings.TrimSpace(output)) + + output = execContainer(t, ctr, "cat /foo_from_custom_target") + require.Equal(t, buildSecretVal, strings.TrimSpace(output)) + + output = execContainer(t, ctr, "cat /duplicate_foo_from_env") + require.Equal(t, buildSecretVal, strings.TrimSpace(output)) + + output = execContainer(t, ctr, "cat /duplicate_foo_from_custom_target") + require.Equal(t, buildSecretVal, strings.TrimSpace(output)) } func TestLifecycleScripts(t *testing.T) { From dbf78792068065ca5d78d0aea9e49df9ae043a90 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Mon, 28 Oct 2024 06:04:57 +0000 Subject: [PATCH 08/13] fix(integration): fix a test case for duplicate secret mounts --- go.mod | 2 +- go.sum | 4 ++-- integration/integration_test.go | 13 +++++-------- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index fdacce83..c0ad4129 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.22.4 // There are a few options we need added to Kaniko! // See: https://github.com/GoogleContainerTools/kaniko/compare/main...coder:kaniko:main -replace github.com/GoogleContainerTools/kaniko => github.com/coder/kaniko v0.0.0-20241024065816-8f144a699d23 +replace github.com/GoogleContainerTools/kaniko => github.com/coder/kaniko v0.0.0-20241028054616-350cbb820e05 // Required to import codersdk due to gvisor dependency. replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20240702054557-aa558fbe5374 diff --git a/go.sum b/go.sum index be5c024d..b117caa4 100644 --- a/go.sum +++ b/go.sum @@ -171,8 +171,8 @@ github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoC github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352 h1:L/EjCuZxs5tOcqqCaASj/nu65TRYEFcTt8qRQfHZXX0= github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352/go.mod h1:P1KoQSgnKEAG6Mnd3YlGzAophty+yKA9VV48LpfNRvo= -github.com/coder/kaniko v0.0.0-20241024065816-8f144a699d23 h1:cBCxME1UiClC5EFaniW2B4xX2gbTsTlOrH6X4SO9pWY= -github.com/coder/kaniko v0.0.0-20241024065816-8f144a699d23/go.mod h1:3rM/KOQ4LgF8mE+O1P6pLDa/E57mzxIxNdUOMKi1qpg= +github.com/coder/kaniko v0.0.0-20241028054616-350cbb820e05 h1:KZc6vG/WnSWG8RtUevGrCdZbF7XJaaZ32ocig6sZLQk= +github.com/coder/kaniko v0.0.0-20241028054616-350cbb820e05/go.mod h1:3rM/KOQ4LgF8mE+O1P6pLDa/E57mzxIxNdUOMKi1qpg= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc= github.com/coder/quartz v0.1.0 h1:cLL+0g5l7xTf6ordRnUMMiZtRE8Sq5LxpghS63vEXrQ= diff --git a/integration/integration_test.go b/integration/integration_test.go index f3700ed2..da6f28ee 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -948,7 +948,7 @@ func TestBuildSecrets(t *testing.T) { // * build secrets are both written to env and target if both are specified "\nRUN --mount=type=secret,id=FOO,env=FOO,target=/etc/foo echo $FOO > /foo_from_env && cat /etc/foo > /foo_from_custom_target" + // Test what happens when you specify the same secret twice - "\nRUN --mount=type=secret,id=FOO,env=FOO --mount=type=secret,id=FOO,target=/etc/foo echo $FOO > /duplicate_foo_from_env && cat /etc/foo > /duplicate_foo_from_custom_target", + "\nRUN --mount=type=secret,id=FOO,target=/etc/duplicate_foo --mount=type=secret,id=FOO,target=/etc/duplicate_foo cat /etc/foo > /duplicate_foo_from_custom_target", }, }) @@ -960,19 +960,16 @@ func TestBuildSecrets(t *testing.T) { require.NoError(t, err) output := execContainer(t, ctr, "cat /foo_from_file") - require.Equal(t, buildSecretVal, strings.TrimSpace(output)) + assert.Equal(t, buildSecretVal, strings.TrimSpace(output)) output = execContainer(t, ctr, "cat /foo_from_env") - require.Equal(t, buildSecretVal, strings.TrimSpace(output)) + assert.Equal(t, buildSecretVal, strings.TrimSpace(output)) output = execContainer(t, ctr, "cat /foo_from_custom_target") - require.Equal(t, buildSecretVal, strings.TrimSpace(output)) - - output = execContainer(t, ctr, "cat /duplicate_foo_from_env") - require.Equal(t, buildSecretVal, strings.TrimSpace(output)) + assert.Equal(t, buildSecretVal, strings.TrimSpace(output)) output = execContainer(t, ctr, "cat /duplicate_foo_from_custom_target") - require.Equal(t, buildSecretVal, strings.TrimSpace(output)) + assert.Equal(t, buildSecretVal, strings.TrimSpace(output)) } func TestLifecycleScripts(t *testing.T) { From 58ae81c9226ba6e3d51cad3e54bebc1acf02614a Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Mon, 28 Oct 2024 06:10:16 +0000 Subject: [PATCH 09/13] fix(integration): fix a test case for duplicate secret mounts --- integration/integration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/integration_test.go b/integration/integration_test.go index da6f28ee..b27c23dc 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -948,7 +948,7 @@ func TestBuildSecrets(t *testing.T) { // * build secrets are both written to env and target if both are specified "\nRUN --mount=type=secret,id=FOO,env=FOO,target=/etc/foo echo $FOO > /foo_from_env && cat /etc/foo > /foo_from_custom_target" + // Test what happens when you specify the same secret twice - "\nRUN --mount=type=secret,id=FOO,target=/etc/duplicate_foo --mount=type=secret,id=FOO,target=/etc/duplicate_foo cat /etc/foo > /duplicate_foo_from_custom_target", + "\nRUN --mount=type=secret,id=FOO,target=/etc/duplicate_foo --mount=type=secret,id=FOO,target=/etc/duplicate_foo cat /etc/duplicate_foo > /duplicate_foo_from_custom_target", }, }) From e1a0a59949daa9ccbc06aceebaa371e4f526b34e Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Mon, 28 Oct 2024 13:34:34 +0000 Subject: [PATCH 10/13] fix(options): further prevent flakes in options testing --- options/secrets_test.go | 40 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/options/secrets_test.go b/options/secrets_test.go index 5b797976..843798ee 100644 --- a/options/secrets_test.go +++ b/options/secrets_test.go @@ -41,8 +41,8 @@ func TestGetBuildSecrets(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - envSnapshot := snapshotEnv() - t.Cleanup(func() { assertEnvUnchanged(t, envSnapshot) }) + preserveEnv(t) + os.Clearenv() options.ClearBuildSecretsFromProcessEnvironment() require.Empty(t, options.GetBuildSecrets(os.Environ())) @@ -66,7 +66,6 @@ func TestClearBuildSecrets(t *testing.T) { name string initialEnvVars map[string]string expectedSecretsBeforeClear []string - expectedSecretsAfterClear []string expectedEnvironAfterClear []string }{ { @@ -75,7 +74,6 @@ func TestClearBuildSecrets(t *testing.T) { "ENVBUILDER_BUILD_SECRET_FOO": "bar", }, expectedSecretsBeforeClear: []string{"FOO=bar"}, - expectedSecretsAfterClear: []string{}, }, { name: "multiple secrets", @@ -84,7 +82,6 @@ func TestClearBuildSecrets(t *testing.T) { "ENVBUILDER_BUILD_SECRET_BAZ": "qux", }, expectedSecretsBeforeClear: []string{"FOO=bar", "BAZ=qux"}, - expectedSecretsAfterClear: []string{}, }, { name: "only build secrets are cleared", @@ -93,15 +90,14 @@ func TestClearBuildSecrets(t *testing.T) { "BAR": "bar", }, expectedSecretsBeforeClear: []string{"FOO=foo"}, - expectedSecretsAfterClear: []string{}, expectedEnvironAfterClear: []string{"BAR=bar"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - envSnapshot := snapshotEnv() - t.Cleanup(func() { assertEnvUnchanged(t, envSnapshot) }) + preserveEnv(t) + os.Clearenv() // Set environment variables for the test case for key, value := range tt.initialEnvVars { @@ -118,33 +114,23 @@ func TestClearBuildSecrets(t *testing.T) { // Verify secrets after clearing environ := os.Environ() secrets = options.GetBuildSecrets(environ) - assert.ElementsMatch(t, tt.expectedSecretsAfterClear, secrets) - for _, env := range tt.expectedSecretsAfterClear { - assert.Contains(t, environ, env) - } + assert.Empty(t, secrets) }) } } -func snapshotEnv() map[string]string { +// preserveEnv takes a snapshot of the current process environment and restores it after the current +// test to ensure that we don't cause flakes by modifying the environment for other tests. +func preserveEnv(t *testing.T) { envSnapshot := make(map[string]string) for _, envVar := range os.Environ() { parts := strings.SplitN(envVar, "=", 2) envSnapshot[parts[0]] = parts[1] } - return envSnapshot -} - -func assertEnvUnchanged(t *testing.T, snapshot map[string]string) { - for key, expectedValue := range snapshot { - currentValue, exists := os.LookupEnv(key) - assert.True(t, exists, "expected environment variable %s to be set", key) - assert.Equal(t, expectedValue, currentValue, "expected environment variable %s to be unchanged", key) - } - for _, envVar := range os.Environ() { - key := strings.SplitN(envVar, "=", 2)[0] - if _, exists := snapshot[key]; !exists { - assert.Fail(t, "unexpected environment variable set", "variable %s was set during the test", key) + t.Cleanup(func() { + os.Clearenv() + for key, value := range envSnapshot { + os.Setenv(key, value) } - } + }) } From 3d9aac25c805c128aaafd1933d1bad5616db7944 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Tue, 29 Oct 2024 06:14:23 +0000 Subject: [PATCH 11/13] fix(envbuilder.go): properly declare an empty slice --- envbuilder.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envbuilder.go b/envbuilder.go index dc98567e..14fd004e 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -525,7 +525,7 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro if val, ok := os.LookupEnv("KANIKO_REGISTRY_MIRROR"); ok { registryMirror = strings.Split(val, ";") } - destinations := []string{} + var destinations []string if opts.CacheRepo != "" { destinations = append(destinations, opts.CacheRepo) } From 39fe438581ef83462b426fe989b0bc64cd5fa4d1 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Tue, 29 Oct 2024 07:40:28 +0000 Subject: [PATCH 12/13] fix(envbuilder.go): document an exported function --- options/secrets.go | 1 + 1 file changed, 1 insertion(+) diff --git a/options/secrets.go b/options/secrets.go index bcaaa837..9896fb36 100644 --- a/options/secrets.go +++ b/options/secrets.go @@ -9,6 +9,7 @@ import ( var buildSecretPrefix = fmt.Sprintf("%sBUILD_SECRET_", envPrefix) +// EnvWithBuildSecretPrefix returns a string in the format of a build secret environment variable. func EnvWithBuildSecretPrefix(secretName, secretValue string) string { return fmt.Sprintf("%s%s=%s", buildSecretPrefix, secretName, secretValue) } From b0ee4018ce1b4cbc283279d7d4140218e378a34a Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Tue, 29 Oct 2024 07:56:25 +0000 Subject: [PATCH 13/13] fix(options): add a note to some tests that cannot be run in parallel --- options/secrets_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/options/secrets_test.go b/options/secrets_test.go index 843798ee..69193d95 100644 --- a/options/secrets_test.go +++ b/options/secrets_test.go @@ -11,6 +11,7 @@ import ( ) func TestGetBuildSecrets(t *testing.T) { + // This test cannot be run in parallel, because it needs to modify the OS environment tests := []struct { name string envVars map[string]string @@ -62,6 +63,7 @@ func TestGetBuildSecrets(t *testing.T) { } func TestClearBuildSecrets(t *testing.T) { + // This test cannot be run in parallel, because it needs to modify the OS environment tests := []struct { name string initialEnvVars map[string]string