feat: support --mount=type=secret,id=foo,env=bar
Allow secrets specified with mount to result in env variable set as an alternative to mounting a file. This matches Docker behaviour: https://docs.docker.com/reference/dockerfile/#example-mount-as-environment-variable Signed-off-by: Adam Eijdenberg <adam@continusec.com>
This commit is contained in:
parent
50d9584c1e
commit
3a2513cc84
|
@ -109,6 +109,17 @@ type Secret struct {
|
|||
SourceType string
|
||||
}
|
||||
|
||||
func (s Secret) ResolveValue() ([]byte, error) {
|
||||
switch s.SourceType {
|
||||
case "env":
|
||||
return []byte(os.Getenv(s.Source)), nil
|
||||
case "file":
|
||||
return os.ReadFile(s.Source)
|
||||
default:
|
||||
return nil, errors.New("invalid secret type")
|
||||
}
|
||||
}
|
||||
|
||||
// BuildOutputOptions contains the the outcome of parsing the value of a build --output flag
|
||||
type BuildOutputOption struct {
|
||||
Path string // Only valid if !IsStdout
|
||||
|
|
|
@ -967,6 +967,10 @@ The location of the secret in the container can be overridden using the
|
|||
|
||||
`RUN --mount=type=secret,id=mysecret,target=/run/secrets/myothersecret cat /run/secrets/myothersecret`
|
||||
|
||||
The secret may alternatively be exposed as an environment variable to the process:
|
||||
|
||||
`RUN --mount=type=secret,id=mysecret,env=FOO sh -c 'echo "Hello $FOO"'`
|
||||
|
||||
Note: changing the contents of secret files will not trigger a rebuild of layers that use said secrets.
|
||||
|
||||
**--security-opt**=[]
|
||||
|
|
2
run.go
2
run.go
|
@ -203,6 +203,8 @@ type runMountArtifacts struct {
|
|||
TargetLocks []*lockfile.LockFile
|
||||
// Intermediate mount points, which should be Unmount()ed and Removed()d
|
||||
IntermediateMounts []string
|
||||
// Environment variables that should be set for RUN that contain secrets, each is name=value form
|
||||
SecretEnvVars []string
|
||||
}
|
||||
|
||||
// RunMountInfo are the available run mounts for this run
|
||||
|
|
130
run_common.go
130
run_common.go
|
@ -1433,6 +1433,12 @@ func (b *Builder) setupMounts(mountPoint string, spec *specs.Spec, bundlePath st
|
|||
mounts = append(mounts, mount)
|
||||
}
|
||||
|
||||
// Some mounts require env vars to be set, do these here
|
||||
spec.Process.Env = append(spec.Process.Env, mountArtifacts.SecretEnvVars...)
|
||||
if mountArtifacts.SSHAuthSock != "" {
|
||||
spec.Process.Env = append(spec.Process.Env, "SSH_AUTH_SOCK="+mountArtifacts.SSHAuthSock)
|
||||
}
|
||||
|
||||
// Set the list in the spec.
|
||||
spec.Mounts = mounts
|
||||
succeeded = true
|
||||
|
@ -1522,6 +1528,7 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
|
|||
intermediateMounts := make([]string, 0, len(mounts))
|
||||
finalMounts := make([]specs.Mount, 0, len(mounts))
|
||||
agents := make([]*sshagent.AgentServer, 0, len(mounts))
|
||||
var secretEnvVars []string
|
||||
defaultSSHSock := ""
|
||||
targetLocks := []*lockfile.LockFile{}
|
||||
var overlayDirs []string
|
||||
|
@ -1561,11 +1568,7 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
|
|||
}
|
||||
}()
|
||||
for _, mount := range mounts {
|
||||
var mountSpec *specs.Mount
|
||||
var err error
|
||||
var envFile, image, bundleMountsDir, overlayDir, intermediateMount string
|
||||
var agent *sshagent.AgentServer
|
||||
var tl *lockfile.LockFile
|
||||
var bundleMountsDir string
|
||||
|
||||
tokens := strings.Split(mount, ",")
|
||||
|
||||
|
@ -1583,18 +1586,21 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
|
|||
}
|
||||
switch mountType {
|
||||
case "secret":
|
||||
mountSpec, envFile, err = b.getSecretMount(tokens, sources.Secrets, idMaps, sources.WorkDir)
|
||||
mountOrEnvSpec, err := b.getSecretMount(tokens, sources.Secrets, idMaps, sources.WorkDir)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if mountSpec != nil {
|
||||
finalMounts = append(finalMounts, *mountSpec)
|
||||
if envFile != "" {
|
||||
tmpFiles = append(tmpFiles, envFile)
|
||||
}
|
||||
if mountOrEnvSpec.Mount != nil {
|
||||
finalMounts = append(finalMounts, *mountOrEnvSpec.Mount)
|
||||
}
|
||||
if mountOrEnvSpec.EnvFile != "" {
|
||||
tmpFiles = append(tmpFiles, mountOrEnvSpec.EnvFile)
|
||||
}
|
||||
if mountOrEnvSpec.EnvVariable != "" {
|
||||
secretEnvVars = append(secretEnvVars, mountOrEnvSpec.EnvVariable)
|
||||
}
|
||||
case "ssh":
|
||||
mountSpec, agent, err = b.getSSHMount(tokens, len(agents), sources.SSHSources, idMaps)
|
||||
mountSpec, agent, err := b.getSSHMount(tokens, len(agents), sources.SSHSources, idMaps)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
@ -1607,11 +1613,12 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
|
|||
}
|
||||
case define.TypeBind:
|
||||
if bundleMountsDir == "" {
|
||||
var err error
|
||||
if bundleMountsDir, err = os.MkdirTemp(bundlePath, "mounts"); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
mountSpec, image, intermediateMount, overlayDir, err = b.getBindMount(tokens, sources.SystemContext, sources.ContextDir, sources.StageMountPoints, idMaps, sources.WorkDir, bundleMountsDir)
|
||||
mountSpec, image, intermediateMount, overlayDir, err := b.getBindMount(tokens, sources.SystemContext, sources.ContextDir, sources.StageMountPoints, idMaps, sources.WorkDir, bundleMountsDir)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
@ -1626,18 +1633,19 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
|
|||
}
|
||||
finalMounts = append(finalMounts, *mountSpec)
|
||||
case "tmpfs":
|
||||
mountSpec, err = b.getTmpfsMount(tokens, idMaps, sources.WorkDir)
|
||||
mountSpec, err := b.getTmpfsMount(tokens, idMaps, sources.WorkDir)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
finalMounts = append(finalMounts, *mountSpec)
|
||||
case "cache":
|
||||
if bundleMountsDir == "" {
|
||||
var err error
|
||||
if bundleMountsDir, err = os.MkdirTemp(bundlePath, "mounts"); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
mountSpec, image, intermediateMount, overlayDir, tl, err = b.getCacheMount(tokens, sources.SystemContext, sources.StageMountPoints, idMaps, sources.WorkDir, bundleMountsDir)
|
||||
mountSpec, image, intermediateMount, overlayDir, tl, err := b.getCacheMount(tokens, sources.SystemContext, sources.StageMountPoints, idMaps, sources.WorkDir, bundleMountsDir)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
@ -1666,6 +1674,7 @@ func (b *Builder) runSetupRunMounts(bundlePath string, mounts []string, sources
|
|||
SSHAuthSock: defaultSSHSock,
|
||||
TargetLocks: targetLocks,
|
||||
IntermediateMounts: intermediateMounts,
|
||||
SecretEnvVars: secretEnvVars,
|
||||
}
|
||||
return finalMounts, artifacts, nil
|
||||
}
|
||||
|
@ -1731,13 +1740,17 @@ func (b *Builder) getTmpfsMount(tokens []string, idMaps IDMaps, workDir string)
|
|||
return &volumes[0], nil
|
||||
}
|
||||
|
||||
func (b *Builder) getSecretMount(tokens []string, secrets map[string]define.Secret, idMaps IDMaps, workdir string) (_ *specs.Mount, _ string, retErr error) {
|
||||
errInvalidSyntax := errors.New("secret should have syntax id=id[,target=path,required=bool,mode=uint,uid=uint,gid=uint")
|
||||
func (b *Builder) getSecretMount(tokens []string, secrets map[string]define.Secret, idMaps IDMaps, workdir string) (rv struct {
|
||||
Mount *specs.Mount // set if mount created
|
||||
EnvFile string // set if caller mount created from temp created env file
|
||||
EnvVariable string // set if caller should add to env variable list
|
||||
}, retErr error,
|
||||
) {
|
||||
errInvalidSyntax := errors.New("secret should have syntax id=id[,target=path,required=bool,mode=uint,uid=uint,gid=uint,env=dstVarName")
|
||||
if len(tokens) == 0 {
|
||||
return nil, "", errInvalidSyntax
|
||||
return rv, errInvalidSyntax
|
||||
}
|
||||
var err error
|
||||
var id, target string
|
||||
var id, target, env string
|
||||
var required bool
|
||||
var uid, gid uint32
|
||||
var mode uint32 = 0o400
|
||||
|
@ -1757,110 +1770,123 @@ func (b *Builder) getSecretMount(tokens []string, secrets map[string]define.Secr
|
|||
case "required":
|
||||
required = true
|
||||
if len(kv) > 1 {
|
||||
var err error
|
||||
required, err = strconv.ParseBool(kv[1])
|
||||
if err != nil {
|
||||
return nil, "", errInvalidSyntax
|
||||
return rv, errInvalidSyntax
|
||||
}
|
||||
}
|
||||
case "mode":
|
||||
mode64, err := strconv.ParseUint(kv[1], 8, 32)
|
||||
if err != nil {
|
||||
return nil, "", errInvalidSyntax
|
||||
return rv, errInvalidSyntax
|
||||
}
|
||||
mode = uint32(mode64)
|
||||
case "uid":
|
||||
uid64, err := strconv.ParseUint(kv[1], 10, 32)
|
||||
if err != nil {
|
||||
return nil, "", errInvalidSyntax
|
||||
return rv, errInvalidSyntax
|
||||
}
|
||||
uid = uint32(uid64)
|
||||
case "gid":
|
||||
gid64, err := strconv.ParseUint(kv[1], 10, 32)
|
||||
if err != nil {
|
||||
return nil, "", errInvalidSyntax
|
||||
return rv, errInvalidSyntax
|
||||
}
|
||||
gid = uint32(gid64)
|
||||
case "env":
|
||||
if kv[1] == "" {
|
||||
return rv, errInvalidSyntax
|
||||
}
|
||||
env = kv[1]
|
||||
default:
|
||||
return nil, "", errInvalidSyntax
|
||||
return rv, errInvalidSyntax
|
||||
}
|
||||
}
|
||||
|
||||
if id == "" {
|
||||
return nil, "", errInvalidSyntax
|
||||
return rv, errInvalidSyntax
|
||||
}
|
||||
|
||||
// first fetch the secret data
|
||||
secr, ok := secrets[id]
|
||||
if !ok {
|
||||
if required {
|
||||
return rv, fmt.Errorf("secret required but no secret with id %q found", id)
|
||||
}
|
||||
return rv, nil
|
||||
}
|
||||
data, err := secr.ResolveValue()
|
||||
if err != nil {
|
||||
return rv, err
|
||||
}
|
||||
|
||||
// if env is set, then we can return now
|
||||
if env != "" {
|
||||
rv.EnvVariable = env + "=" + string(data)
|
||||
return rv, nil
|
||||
}
|
||||
// else we fallback to default behaviour of creating mount
|
||||
|
||||
// Default location for secrets is /run/secrets/id
|
||||
if target == "" {
|
||||
target = "/run/secrets/" + id
|
||||
}
|
||||
|
||||
secr, ok := secrets[id]
|
||||
if !ok {
|
||||
if required {
|
||||
return nil, "", fmt.Errorf("secret required but no secret with id %q found", id)
|
||||
}
|
||||
return nil, "", nil
|
||||
}
|
||||
var data []byte
|
||||
var envFile string
|
||||
var ctrFileOnHost string
|
||||
|
||||
switch secr.SourceType {
|
||||
case "env":
|
||||
data = []byte(os.Getenv(secr.Source))
|
||||
tmpFile, err := os.CreateTemp(tmpdir.GetTempDir(), "buildah*")
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return rv, err
|
||||
}
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
os.Remove(tmpFile.Name())
|
||||
}
|
||||
}()
|
||||
envFile = tmpFile.Name()
|
||||
rv.EnvFile = tmpFile.Name()
|
||||
ctrFileOnHost = tmpFile.Name()
|
||||
case "file":
|
||||
containerWorkingDir, err := b.store.ContainerDirectory(b.ContainerID)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
data, err = os.ReadFile(secr.Source)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return rv, err
|
||||
}
|
||||
ctrFileOnHost = filepath.Join(containerWorkingDir, "secrets", digest.FromString(id).Encoded()[:16])
|
||||
default:
|
||||
return nil, "", errors.New("invalid source secret type")
|
||||
return rv, errors.New("invalid source secret type")
|
||||
}
|
||||
|
||||
// Copy secrets to container working dir (or tmp dir if it's an env), since we need to chmod,
|
||||
// chown and relabel it for the container user and we don't want to mess with the original file
|
||||
if err := os.MkdirAll(filepath.Dir(ctrFileOnHost), 0o755); err != nil {
|
||||
return nil, "", err
|
||||
return rv, err
|
||||
}
|
||||
if err := os.WriteFile(ctrFileOnHost, data, 0o644); err != nil {
|
||||
return nil, "", err
|
||||
return rv, err
|
||||
}
|
||||
|
||||
if err := relabel(ctrFileOnHost, b.MountLabel, false); err != nil {
|
||||
return nil, "", err
|
||||
return rv, err
|
||||
}
|
||||
hostUID, hostGID, err := util.GetHostIDs(idMaps.uidmap, idMaps.gidmap, uid, gid)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return rv, err
|
||||
}
|
||||
if err := os.Lchown(ctrFileOnHost, int(hostUID), int(hostGID)); err != nil {
|
||||
return nil, "", err
|
||||
return rv, err
|
||||
}
|
||||
if err := os.Chmod(ctrFileOnHost, os.FileMode(mode)); err != nil {
|
||||
return nil, "", err
|
||||
return rv, err
|
||||
}
|
||||
newMount := specs.Mount{
|
||||
rv.Mount = &specs.Mount{
|
||||
Destination: target,
|
||||
Type: define.TypeBind,
|
||||
Source: ctrFileOnHost,
|
||||
Options: append(define.BindOptions, "rprivate", "ro"),
|
||||
}
|
||||
return &newMount, envFile, nil
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
// getSSHMount parses the --mount type=ssh flag in the Containerfile, checks if there's an ssh source provided, and creates and starts an ssh-agent to be forwarded into the container
|
||||
|
|
|
@ -276,10 +276,6 @@ func (b *Builder) Run(command []string, options RunOptions) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("resolving mountpoints for container %q: %w", b.ContainerID, err)
|
||||
}
|
||||
if runArtifacts.SSHAuthSock != "" {
|
||||
sshenv := "SSH_AUTH_SOCK=" + runArtifacts.SSHAuthSock
|
||||
spec.Process.Env = append(spec.Process.Env, sshenv)
|
||||
}
|
||||
|
||||
// following run was called from `buildah run`
|
||||
// and some images were mounted for this run
|
||||
|
|
|
@ -513,10 +513,6 @@ rootless=%d
|
|||
if err != nil {
|
||||
return fmt.Errorf("resolving mountpoints for container %q: %w", b.ContainerID, err)
|
||||
}
|
||||
if runArtifacts.SSHAuthSock != "" {
|
||||
sshenv := "SSH_AUTH_SOCK=" + runArtifacts.SSHAuthSock
|
||||
spec.Process.Env = append(spec.Process.Env, sshenv)
|
||||
}
|
||||
|
||||
// Create any mount points that we need that aren't already present in
|
||||
// the rootfs.
|
||||
|
|
|
@ -8888,3 +8888,15 @@ _EOF
|
|||
run_buildah --root=${TEST_SCRATCH_DIR}/newroot --storage-opt=imagestore=${TEST_SCRATCH_DIR}/root build --pull=never ${contextdir}
|
||||
run_buildah --root=${TEST_SCRATCH_DIR}/newroot --storage-opt=imagestore=${TEST_SCRATCH_DIR}/root build --pull=never --squash ${contextdir}
|
||||
}
|
||||
|
||||
@test "use-secret-to-env-variable" {
|
||||
local outpath="${TEST_SCRATCH_DIR}/timestamp-after-secret.tar"
|
||||
|
||||
BAR=baz run_buildah build --secret id=mysecret,env=BAR -f <(printf "FROM alpine\nRUN --mount=type=secret,id=mysecret,env=FOO,required sh -c 'echo "'"Hello $FOO"'"'") --tag=oci-archive:${outpath}.a --timestamp 0
|
||||
expect_output --substring "Hello baz"
|
||||
BAR=boz run_buildah build --secret id=mysecret,env=BAR -f <(printf "FROM alpine\nRUN --mount=type=secret,id=mysecret,env=FOO,required sh -c 'echo "'"Hello $FOO"'"'") --tag=oci-archive:${outpath}.b --timestamp 0
|
||||
expect_output --substring "Hello boz"
|
||||
|
||||
# even though different secret was passed to each(baz vs boz), we expect the same result, ie should not affect build history
|
||||
diff "${outpath}.a" "${outpath}.b"
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue