From 10c45ad19ba63713a4530d63b4a83ec217c3cd83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emanuel=20Skrenkovic=CC=81?= Date: Sat, 10 Feb 2024 12:22:29 +0100 Subject: [PATCH] feat: Add support for build-secrets Enables passing of build-secrets through the 'secrets' block inside 'build'. The feature is only available when using Buildkit. --- docs/resources/image.md | 14 ++++++ go.mod | 1 + go.sum | 2 + internal/provider/resource_docker_image.go | 30 ++++++++++++ .../provider/resource_docker_image_funcs.go | 43 ++++++++++++++++- .../provider/resource_docker_image_test.go | 47 +++++++++++++++++++ .../testDockerImageBuildSecrets.tf | 18 +++++++ 7 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 testdata/resources/docker_image/testDockerImageBuildSecrets.tf diff --git a/docs/resources/image.md b/docs/resources/image.md index 50d33909f..e3d4c33c2 100644 --- a/docs/resources/image.md +++ b/docs/resources/image.md @@ -132,6 +132,7 @@ Optional: - `pull_parent` (Boolean) Attempt to pull the image even if an older image exists locally - `remote_context` (String) A Git repository URI or HTTP/HTTPS context URI - `remove` (Boolean) Remove intermediate containers after a successful build. Defaults to `true`. +- `secrets` (Block List) Set build-time secrets (see [below for nested schema](#nestedblock--build--secrets)) - `security_opt` (List of String) The security options - `session_id` (String) Set an ID for the build session - `shm_size` (Number) Size of /dev/shm in bytes. The size must be greater than 0 @@ -160,6 +161,19 @@ Optional: - `user_name` (String) the registry user name + +### Nested Schema for `build.secrets` + +Required: + +- `id` (String) ID of the secret. By default, secrets are mounted to /run/secrets/ + +Optional: + +- `env` (String) Environment variable source of the secret +- `src` (String) File source of the secret + + ### Nested Schema for `build.ulimit` diff --git a/go.mod b/go.mod index 01f18d686..6c923cce7 100644 --- a/go.mod +++ b/go.mod @@ -218,6 +218,7 @@ require ( github.com/timonwong/loggercheck v0.9.3 // indirect github.com/tomarrell/wrapcheck/v2 v2.6.2 // indirect github.com/tommy-muehle/go-mnd/v2 v2.5.0 // indirect + github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect github.com/ultraware/funlen v0.0.3 // indirect github.com/ultraware/whitespace v0.0.5 // indirect github.com/uudashr/gocognit v1.0.6 // indirect diff --git a/go.sum b/go.sum index 2436c9b4a..b9419a333 100644 --- a/go.sum +++ b/go.sum @@ -1158,6 +1158,8 @@ github.com/tomarrell/wrapcheck/v2 v2.6.2 h1:3dI6YNcrJTQ/CJQ6M/DUkc0gnqYSIk6o0rCh github.com/tomarrell/wrapcheck/v2 v2.6.2/go.mod h1:ao7l5p0aOlUNJKI0qVwB4Yjlqutd0IvAB9Rdwyilxvg= github.com/tommy-muehle/go-mnd/v2 v2.5.0 h1:iAj0a8e6+dXSL7Liq0aXPox36FiN1dBbjA6lt9fl65s= github.com/tommy-muehle/go-mnd/v2 v2.5.0/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= +github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0= +github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ultraware/funlen v0.0.3 h1:5ylVWm8wsNwH5aWo9438pwvsK0QiqVuUrt9bn7S/iLA= github.com/ultraware/funlen v0.0.3/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= diff --git a/internal/provider/resource_docker_image.go b/internal/provider/resource_docker_image.go index 2652b1038..cca12043b 100644 --- a/internal/provider/resource_docker_image.go +++ b/internal/provider/resource_docker_image.go @@ -97,6 +97,36 @@ func resourceDockerImage() *schema.Resource { }, ForceNew: true, }, + "secrets": { + Type: schema.TypeList, + Description: "Set build-time secrets", + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Description: "ID of the secret. By default, secrets are mounted to /run/secrets/", + Optional: false, + Required: true, + ForceNew: true, + }, + "src": { + Type: schema.TypeString, + Description: "File source of the secret", + Optional: true, + Required: false, + ForceNew: true, + }, + "env": { + Type: schema.TypeString, + Description: "Environment variable source of the secret", + Optional: true, + Required: false, + ForceNew: true, + }, + }, + }, + }, "label": { Type: schema.TypeMap, Description: "Set metadata for an image", diff --git a/internal/provider/resource_docker_image_funcs.go b/internal/provider/resource_docker_image_funcs.go index 5c15e5746..7ef1194d3 100644 --- a/internal/provider/resource_docker_image_funcs.go +++ b/internal/provider/resource_docker_image_funcs.go @@ -24,6 +24,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/mitchellh/go-homedir" "github.com/moby/buildkit/session" + "github.com/moby/buildkit/session/secrets/secretsprovider" "github.com/pkg/errors" ) @@ -333,7 +334,22 @@ func buildDockerImage(ctx context.Context, rawBuild map[string]interface{}, imag buildContext := rawBuild["context"].(string) - enableBuildKitIfSupported(ctx, client, &buildOptions) + buildKitSession := enableBuildKitIfSupported(ctx, client, &buildOptions) + + // If Buildkit is enabled, try to parse and use secrets if present. + if buildKitSession != nil { + if secretsRaw, secretsDefined := rawBuild["secrets"]; secretsDefined { + parsedSecrets := parseBuildSecrets(secretsRaw) + + store, err := secretsprovider.NewStore(parsedSecrets) + if err != nil { + return err + } + + provider := secretsprovider.NewSecretProvider(store) + buildKitSession.Allow(provider) + } + } buildCtx, relDockerfile, err := prepareBuildContext(buildContext, buildOptions.Dockerfile) if err != nil { @@ -355,7 +371,11 @@ func buildDockerImage(ctx context.Context, rawBuild map[string]interface{}, imag return nil } -func enableBuildKitIfSupported(ctx context.Context, client *client.Client, buildOptions *types.ImageBuildOptions) { +func enableBuildKitIfSupported( + ctx context.Context, + client *client.Client, + buildOptions *types.ImageBuildOptions, +) *session.Session { dockerClientVersion := client.ClientVersion() log.Printf("[DEBUG] DockerClientVersion: %v, minBuildKitDockerVersion: %v\n", dockerClientVersion, minBuildkitDockerVersion) if versions.GreaterThanOrEqualTo(dockerClientVersion, minBuildkitDockerVersion) { @@ -369,8 +389,10 @@ func enableBuildKitIfSupported(ctx context.Context, client *client.Client, build defer s.Close() buildOptions.SessionID = s.ID() buildOptions.Version = types.BuilderBuildKit + return s } else { buildOptions.Version = types.BuilderV1 + return nil } } @@ -452,3 +474,20 @@ func decodeBuildMessages(response types.ImageBuildResponse) (string, error) { return buf.String(), buildErr } + +func parseBuildSecrets(secretsRaw interface{}) []secretsprovider.Source { + options := secretsRaw.([]interface{}) + + secrets := make([]secretsprovider.Source, len(options)) + for i, option := range options { + secretRaw := option.(map[string]interface{}) + source := secretsprovider.Source{ + ID: secretRaw["id"].(string), + FilePath: secretRaw["src"].(string), + Env: secretRaw["env"].(string), + } + secrets[i] = source + } + + return secrets +} diff --git a/internal/provider/resource_docker_image_test.go b/internal/provider/resource_docker_image_test.go index f2284ef58..6aa0a053d 100644 --- a/internal/provider/resource_docker_image_test.go +++ b/internal/provider/resource_docker_image_test.go @@ -449,6 +449,53 @@ func TestAccDockerImage_build(t *testing.T) { }) } +func TestAccDockerImageSecrets_build(t *testing.T) { + const testDockerFileWithSecret = ` + FROM python:3-stretch + + WORKDIR /app + + ARG test_arg + + RUN echo ${test_arg} > test_arg.txt + + RUN --mount=type=secret,id=TEST_SECRET_SRC \ + --mount=type=secret,id=TEST_SECRET_ENV \ + apt-get update -qq` + + ctx := context.Background() + wd, _ := os.Getwd() + dfPath := filepath.Join(wd, "Dockerfile") + if err := ioutil.WriteFile(dfPath, []byte(testDockerFileWithSecret), 0o644); err != nil { + t.Fatalf("failed to create a Dockerfile %s for test: %+v", dfPath, err) + } + defer os.Remove(dfPath) + + const secretContent = "THIS IS A SECRET" + sPath := filepath.Join(wd, "secret") + if err := ioutil.WriteFile(sPath, []byte(secretContent), 0o644); err != nil { + t.Fatalf("failed to create a secret file %s for test: %+v", sPath, err) + } + + defer os.Remove(sPath) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: func(state *terraform.State) error { + return testAccDockerImageDestroy(ctx, state) + }, + Steps: []resource.TestStep{ + { + Config: loadTestConfiguration(t, RESOURCE, "docker_image", "testDockerImageBuildSecrets"), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("docker_image.test", "name", contentDigestRegexp), + ), + }, + }, + }) +} + const testDockerFileExample = ` FROM python:3-bookworm diff --git a/testdata/resources/docker_image/testDockerImageBuildSecrets.tf b/testdata/resources/docker_image/testDockerImageBuildSecrets.tf new file mode 100644 index 000000000..0fb34c927 --- /dev/null +++ b/testdata/resources/docker_image/testDockerImageBuildSecrets.tf @@ -0,0 +1,18 @@ +resource "docker_image" "test" { + name = "ubuntu:11" + build { + context = "." + dockerfile = "Dockerfile" + force_remove = true + + secrets { + id = "TEST_SECRET_SRC" + src = "./secret" + } + + secrets { + id = "TEST_SECRET_ENV" + env = "PATH" + } + } +}