Skip to content

Commit

Permalink
Merge pull request #5215 from a-palchikov/dockerfile/secretasenv
Browse files Browse the repository at this point in the history
frontend: allow mounting secret environment variables
  • Loading branch information
tonistiigi authored Aug 21, 2024
2 parents 24421f1 + 96f746b commit 53d77f7
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 18 deletions.
53 changes: 43 additions & 10 deletions client/llb/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"net"
"sort"
"strings"

"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/system"
Expand Down Expand Up @@ -290,7 +291,7 @@ func (e *ExecOp) Marshal(ctx context.Context, c *Constraints) (digest.Digest, []
if len(e.secrets) > 0 {
addCap(&e.constraints, pb.CapExecMountSecret)
for _, s := range e.secrets {
if s.IsEnv {
if s.Env != nil {
addCap(&e.constraints, pb.CapExecSecretEnv)
break
}
Expand Down Expand Up @@ -388,16 +389,17 @@ func (e *ExecOp) Marshal(ctx context.Context, c *Constraints) (digest.Digest, []
}

for _, s := range e.secrets {
if s.IsEnv {
if s.Env != nil {
peo.Secretenv = append(peo.Secretenv, &pb.SecretEnv{
ID: s.ID,
Name: s.Target,
Name: *s.Env,
Optional: s.Optional,
})
} else {
}
if s.Target != nil {
pm := &pb.Mount{
Input: pb.Empty,
Dest: s.Target,
Dest: *s.Target,
MountType: pb.MountType_SECRET,
SecretOpt: &pb.SecretOpt{
ID: s.ID,
Expand Down Expand Up @@ -680,7 +682,19 @@ type SSHInfo struct {
// AddSecret is a RunOption that adds a secret to the exec.
func AddSecret(dest string, opts ...SecretOption) RunOption {
return runOptionFunc(func(ei *ExecInfo) {
s := &SecretInfo{ID: dest, Target: dest, Mode: 0400}
s := &SecretInfo{ID: dest, Target: &dest, Mode: 0400}
for _, opt := range opts {
opt.SetSecretOption(s)
}
ei.Secrets = append(ei.Secrets, *s)
})
}

// AddSecretWithDest is a RunOption that adds a secret to the exec
// with an optional destination.
func AddSecretWithDest(src string, dest *string, opts ...SecretOption) RunOption {
return runOptionFunc(func(ei *ExecInfo) {
s := &SecretInfo{ID: src, Target: dest, Mode: 0400}
for _, opt := range opts {
opt.SetSecretOption(s)
}
Expand All @@ -699,13 +713,15 @@ func (fn secretOptionFunc) SetSecretOption(si *SecretInfo) {
}

type SecretInfo struct {
ID string
Target string
ID string
// Target optionally specifies the target for the secret mount
Target *string
// Env optionally names the environment variable for the secret
Env *string
Mode int
UID int
GID int
Optional bool
IsEnv bool
}

var SecretOptional = secretOptionFunc(func(si *SecretInfo) {
Expand All @@ -721,7 +737,24 @@ func SecretID(id string) SecretOption {
// SecretAsEnv defines if the secret should be added as an environment variable
func SecretAsEnv(v bool) SecretOption {
return secretOptionFunc(func(si *SecretInfo) {
si.IsEnv = v
if !v {
si.Env = nil
return
}
if si.Target == nil {
return
}
target := strings.Clone(*si.Target)
si.Env = &target
si.Target = nil
})
}

// SecretAsEnvName defines if the secret should be added as an environment variable
// with the specified name
func SecretAsEnvName(v string) SecretOption {
return secretOptionFunc(func(si *SecretInfo) {
si.Env = &v
})
}

Expand Down
19 changes: 15 additions & 4 deletions frontend/dockerfile/dockerfile2llb/convert_secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,17 @@ func dispatchSecret(d *dispatchState, m *instructions.Mount, loc []parser.Range)
id = path.Base(m.Target)
}

target := m.Target
if target == "" {
target = "/run/secrets/" + path.Base(id)
var target *string
if m.Target != "" {
target = &m.Target
}

if m.Env == nil {
dest := m.Target
if dest == "" {
dest = "/run/secrets/" + path.Base(id)
}
target = &dest
}

if _, ok := d.outline.secrets[id]; !ok {
Expand All @@ -39,6 +47,9 @@ func dispatchSecret(d *dispatchState, m *instructions.Mount, loc []parser.Range)
if !m.Required {
opts = append(opts, llb.SecretOptional)
}
if m.Env != nil {
opts = append(opts, llb.SecretAsEnvName(*m.Env))
}

if m.UID != nil || m.GID != nil || m.Mode != nil {
var uid, gid, mode int
Expand All @@ -56,5 +67,5 @@ func dispatchSecret(d *dispatchState, m *instructions.Mount, loc []parser.Range)
opts = append(opts, llb.SecretFileOpt(uid, gid, mode))
}

return llb.AddSecret(target, opts...), nil
return llb.AddSecretWithDest(id, target, opts...), nil
}
62 changes: 62 additions & 0 deletions frontend/dockerfile/dockerfile_secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
var secretsTests = integration.TestFuncs(
testSecretFileParams,
testSecretRequiredWithoutValue,
testSecretAsEnviron,
testSecretAsEnvironWithFileMount,
)

func init() {
Expand Down Expand Up @@ -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=SECRET_ENV [ "$SECRET_ENV" == "pw" ] && [ ! -f /run/secrets/mysecret ] || 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 testSecretAsEnvironWithFileMount(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=/run/secrets/secret,env=SECRET_ENV [ "$SECRET_ENV" == "pw" ] && [ -f /run/secrets/secret ] || 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)
}
13 changes: 9 additions & 4 deletions frontend/dockerfile/instructions/commands_runmount.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,12 @@ type Mount struct {
CacheID string
CacheSharing ShareMode
Required bool
Mode *uint64
UID *uint64
GID *uint64
// Env optionally specifies the name of the environment variable for a secret.
// A pointer to an empty value uses the default
Env *string
Mode *uint64
UID *uint64
GID *uint64
}

func parseMount(val string, expander SingleWordExpander) (*Mount, error) {
Expand Down Expand Up @@ -252,9 +255,11 @@ func parseMount(val string, expander SingleWordExpander) (*Mount, error) {
return nil, errors.Errorf("invalid value %s for gid", value)
}
m.GID = &gid
case "env":
m.Env = &value
default:
allKeys := []string{
"type", "from", "source", "target", "readonly", "id", "sharing", "required", "size", "mode", "uid", "gid", "src", "dst", "destination", "ro", "rw", "readwrite",
"type", "from", "source", "target", "readonly", "id", "sharing", "required", "size", "mode", "uid", "gid", "src", "dst", "destination", "ro", "rw", "readwrite", "env",
}
return nil, suggest.WrapError(errors.Errorf("unexpected key '%s' in '%s'", key, field), key, allKeys, true)
}
Expand Down

0 comments on commit 53d77f7

Please sign in to comment.