From 1aaabab323916e0215d0d5bdc3675f512f96c39f Mon Sep 17 00:00:00 2001 From: a-palchikov Date: Mon, 5 Aug 2024 23:00:29 +0200 Subject: [PATCH] Implements frontend side of #2122. This adds the syntax to Dockerfile frontend. I purposedly chose to use a simple format for this as it's likely going to be debated. As implemented, the following format is supported: ``` RUN --mount=type=secret,id=MYSECRET,env ``` or, more explicitly: ``` RUN --mount=type=secret,id=MYSECRET,env=true ``` will mount the secret with id MYSECRET as a new environment variable with the same name. Using 'target', it's possible to create a different environment variable: ``` RUN --mount=type=secret,id=mysecret,target=MY_SECRET,env ``` will mount 'mysecret' secret as MY_SECRET environment variable. Any suggestions on making it more ergonomic are welcome. Signed-off-by: a-palchikov --- .../dockerfile2llb/convert_secrets.go | 6 ++ .../dockerfile/dockerfile_secrets_test.go | 62 +++++++++++++++++++ .../instructions/commands_runmount.go | 12 +++- 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/frontend/dockerfile/dockerfile2llb/convert_secrets.go b/frontend/dockerfile/dockerfile2llb/convert_secrets.go index ced2bff1b070c..7af5462f5dda0 100644 --- a/frontend/dockerfile/dockerfile2llb/convert_secrets.go +++ b/frontend/dockerfile/dockerfile2llb/convert_secrets.go @@ -39,6 +39,12 @@ func dispatchSecret(d *dispatchState, m *instructions.Mount, loc []parser.Range) if !m.Required { opts = append(opts, llb.SecretOptional) } + if m.SecretAsEnv { + if m.Target == "" { + target = path.Base(id) + } + opts = append(opts, llb.SecretAsEnv(true)) + } if m.UID != nil || m.GID != nil || m.Mode != nil { var uid, gid, mode int diff --git a/frontend/dockerfile/dockerfile_secrets_test.go b/frontend/dockerfile/dockerfile_secrets_test.go index fd72d9b053b12..b25c526cdd229 100644 --- a/frontend/dockerfile/dockerfile_secrets_test.go +++ b/frontend/dockerfile/dockerfile_secrets_test.go @@ -16,6 +16,8 @@ import ( var secretsTests = integration.TestFuncs( testSecretFileParams, testSecretRequiredWithoutValue, + testSecretAsEnviron, + testSecretAsEnvironWithOverride, ) func init() { @@ -80,3 +82,63 @@ RUN --mount=type=secret,required,id=mysecret foo require.Error(t, err) require.Contains(t, err.Error(), "secret mysecret: not found") } + +func testSecretAsEnviron(t *testing.T, sb integration.Sandbox) { + integration.SkipOnPlatform(t, "windows") + f := getFrontend(t, sb) + + dockerfile := []byte(` +FROM busybox +RUN --mount=type=secret,id=mysecret,env=true [ "$mysecret" == "pw" ] || false +`) + + dir := integration.Tmpdir( + t, + fstest.CreateFile("Dockerfile", dockerfile, 0600), + ) + + c, err := client.New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + _, err = f.Solve(sb.Context(), c, client.SolveOpt{ + LocalMounts: map[string]fsutil.FS{ + dockerui.DefaultLocalNameDockerfile: dir, + dockerui.DefaultLocalNameContext: dir, + }, + Session: []session.Attachable{secretsprovider.FromMap(map[string][]byte{ + "mysecret": []byte("pw"), + })}, + }, nil) + require.NoError(t, err) +} + +func testSecretAsEnvironWithOverride(t *testing.T, sb integration.Sandbox) { + integration.SkipOnPlatform(t, "windows") + f := getFrontend(t, sb) + + dockerfile := []byte(` +FROM busybox +RUN --mount=type=secret,id=mysecret,target=MY_SECRET,env [ "$MY_SECRET" == "pw" ] || false +`) + + dir := integration.Tmpdir( + t, + fstest.CreateFile("Dockerfile", dockerfile, 0600), + ) + + c, err := client.New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + _, err = f.Solve(sb.Context(), c, client.SolveOpt{ + LocalMounts: map[string]fsutil.FS{ + dockerui.DefaultLocalNameDockerfile: dir, + dockerui.DefaultLocalNameContext: dir, + }, + Session: []session.Attachable{secretsprovider.FromMap(map[string][]byte{ + "mysecret": []byte("pw"), + })}, + }, nil) + require.NoError(t, err) +} diff --git a/frontend/dockerfile/instructions/commands_runmount.go b/frontend/dockerfile/instructions/commands_runmount.go index 0a368238a0ab8..7ca2bbbffc1ec 100644 --- a/frontend/dockerfile/instructions/commands_runmount.go +++ b/frontend/dockerfile/instructions/commands_runmount.go @@ -122,6 +122,7 @@ type Mount struct { CacheID string CacheSharing ShareMode Required bool + SecretAsEnv bool Mode *uint64 UID *uint64 GID *uint64 @@ -154,6 +155,9 @@ func parseMount(val string, expander SingleWordExpander) (*Mount, error) { m.ReadOnly = false roAuto = false continue + case "env": + m.SecretAsEnv = true + continue case "required": if m.Type == MountTypeSecret || m.Type == MountTypeSSH { m.Required = true @@ -252,9 +256,15 @@ func parseMount(val string, expander SingleWordExpander) (*Mount, error) { return nil, errors.Errorf("invalid value %s for gid", value) } m.GID = &gid + case "env": + env, err := strconv.ParseBool(value) + if err != nil { + return nil, errors.Errorf("invalid value for %q: %q", key, value) + } + m.SecretAsEnv = env default: allKeys := []string{ - "type", "from", "source", "target", "readonly", "id", "sharing", "required", "mode", "uid", "gid", "src", "dst", "ro", "rw", "readwrite", + "type", "from", "source", "target", "readonly", "id", "sharing", "required", "mode", "uid", "gid", "src", "dst", "ro", "rw", "readwrite", "env", } return nil, suggest.WrapError(errors.Errorf("unexpected key '%s' in '%s'", key, field), key, allKeys, true) }