Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions pkg/compose/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,17 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
return "", err
}

err = s.injectSecrets(ctx, project, service, created.ID)
ctr, err := s.apiClient().ContainerInspect(ctx, created.ID)
if err != nil {
return "", err
}

err = s.injectSecrets(ctx, project, service, ctr.ID)
if err != nil {
return created.ID, err
}

err = s.injectConfigs(ctx, project, service, created.ID)
err = s.injectConfigs(ctx, project, service, ctr.ID)
return created.ID, err
}

Expand Down
152 changes: 105 additions & 47 deletions pkg/compose/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,88 +22,146 @@ import (
"context"
"fmt"
"strconv"
"strings"
"time"

"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/docker/api/types/container"
)

type mountType string

const (
secretMount mountType = "secret"
configMount mountType = "config"
)

func (s *composeService) injectSecrets(ctx context.Context, project *types.Project, service types.ServiceConfig, id string) error {
for _, config := range service.Secrets {
file := project.Secrets[config.Source]
if file.Environment == "" {
return s.injectFileReferences(ctx, project, service, id, secretMount)
}

func (s *composeService) injectConfigs(ctx context.Context, project *types.Project, service types.ServiceConfig, id string) error {
return s.injectFileReferences(ctx, project, service, id, configMount)
}

func (s *composeService) injectFileReferences(ctx context.Context, project *types.Project, service types.ServiceConfig, id string, mountType mountType) error {
mounts, sources := s.getFilesAndMap(project, service, mountType)
var ctrConfig *container.Config

for _, mount := range mounts {
content, err := s.resolveFileContent(project, sources[mount.Source], mountType)
if err != nil {
return err
}
if content == "" {
continue
}

if service.ReadOnly {
return fmt.Errorf("cannot create secret %q in read-only service %s: `file` is the sole supported option", file.Name, service.Name)
return fmt.Errorf("cannot create %s %q in read-only service %s: `file` is the sole supported option", mountType, sources[mount.Source].Name, service.Name)
}

if config.Target == "" {
config.Target = "/run/secrets/" + config.Source
} else if !isAbsTarget(config.Target) {
config.Target = "/run/secrets/" + config.Target
}
s.setDefaultTarget(&mount, mountType)

content := file.Content
if content == "" {
env, ok := project.Environment[file.Environment]
if !ok {
return fmt.Errorf("environment variable %q required by secret %q is not set", file.Environment, file.Name)
}
content = env
}
b, err := createTar(content, types.FileReferenceConfig(config))
ctrConfig, err = s.setFileOwnership(ctx, id, &mount, ctrConfig)
if err != nil {
return err
}

err = s.apiClient().CopyToContainer(ctx, id, "/", &b, container.CopyToContainerOptions{
CopyUIDGID: config.UID != "" || config.GID != "",
})
if err != nil {
if err := s.copyFileToContainer(ctx, id, content, mount); err != nil {
return err
}
}
return nil
}

func (s *composeService) injectConfigs(ctx context.Context, project *types.Project, service types.ServiceConfig, id string) error {
for _, config := range service.Configs {
file := project.Configs[config.Source]
content := file.Content
if file.Environment != "" {
env, ok := project.Environment[file.Environment]
if !ok {
return fmt.Errorf("environment variable %q required by config %q is not set", file.Environment, file.Name)
}
content = env
func (s *composeService) getFilesAndMap(project *types.Project, service types.ServiceConfig, mountType mountType) ([]types.FileReferenceConfig, map[string]types.FileObjectConfig) {
var files []types.FileReferenceConfig
var fileMap map[string]types.FileObjectConfig

switch mountType {
case secretMount:
files = make([]types.FileReferenceConfig, len(service.Secrets))
for i, config := range service.Secrets {
files[i] = types.FileReferenceConfig(config)
}
if content == "" {
continue
fileMap = make(map[string]types.FileObjectConfig)
for k, v := range project.Secrets {
fileMap[k] = types.FileObjectConfig(v)
}

if service.ReadOnly {
return fmt.Errorf("cannot create config %q in read-only service %s: `file` is the sole supported option", file.Name, service.Name)
case configMount:
files = make([]types.FileReferenceConfig, len(service.Configs))
for i, config := range service.Configs {
files[i] = types.FileReferenceConfig(config)
}
fileMap = make(map[string]types.FileObjectConfig)
for k, v := range project.Configs {
fileMap[k] = types.FileObjectConfig(v)
}
}
return files, fileMap
}

if config.Target == "" {
config.Target = "/" + config.Source
func (s *composeService) resolveFileContent(project *types.Project, source types.FileObjectConfig, mountType mountType) (string, error) {
if source.Content != "" {
// inlined, or already resolved by include
return source.Content, nil
}
if source.Environment != "" {
env, ok := project.Environment[source.Environment]
if !ok {
return "", fmt.Errorf("environment variable %q required by %s %q is not set", source.Environment, mountType, source.Name)
}
return env, nil
}
return "", nil
}

b, err := createTar(content, types.FileReferenceConfig(config))
if err != nil {
return err
func (s *composeService) setDefaultTarget(file *types.FileReferenceConfig, mountType mountType) {
if file.Target == "" {
if mountType == secretMount {
file.Target = "/run/secrets/" + file.Source
} else {
file.Target = "/" + file.Source
}
} else if mountType == secretMount && !isAbsTarget(file.Target) {
file.Target = "/run/secrets/" + file.Target
}
}

func (s *composeService) setFileOwnership(ctx context.Context, id string, file *types.FileReferenceConfig, ctrConfig *container.Config) (*container.Config, error) {
if file.UID != "" || file.GID != "" {
return ctrConfig, nil
}

err = s.apiClient().CopyToContainer(ctx, id, "/", &b, container.CopyToContainerOptions{
CopyUIDGID: config.UID != "" || config.GID != "",
})
if ctrConfig == nil {
ctr, err := s.apiClient().ContainerInspect(ctx, id)
if err != nil {
return err
return nil, err
}
ctrConfig = ctr.Config
}
return nil

parts := strings.Split(ctrConfig.User, ":")
if len(parts) > 0 {
file.UID = parts[0]
}
if len(parts) > 1 {
file.GID = parts[1]
}

return ctrConfig, nil
}

func (s *composeService) copyFileToContainer(ctx context.Context, id, content string, file types.FileReferenceConfig) error {
b, err := createTar(content, file)
if err != nil {
return err
}

return s.apiClient().CopyToContainer(ctx, id, "/", &b, container.CopyToContainerOptions{
CopyUIDGID: true,
})
}

func createTar(env string, config types.FileReferenceConfig) (bytes.Buffer, error) {
Expand Down
17 changes: 17 additions & 0 deletions pkg/e2e/fixtures/env-secret/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@ services:
mode: 0440
command: cat /run/secrets/bar

bar:
image: alpine
user: "1005"
secrets:
- source: secret
target: bar
command: cat /run/secrets/bar

zot:
image: alpine
user: "1005:1005"
secrets:
- source: secret
target: bar
command: cat /run/secrets/bar


secrets:
secret:
environment: SECRET
Expand Down
23 changes: 23 additions & 0 deletions pkg/e2e/secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package e2e

import (
"strings"
"testing"

"gotest.tools/v3/icmd"
Expand All @@ -40,6 +41,28 @@ func TestSecretFromEnv(t *testing.T) {
})
res.Assert(t, icmd.Expected{Out: "-r--r----- 1 1005 1005"})
})
t.Run("secret uid from user", func(t *testing.T) {
res := c.RunDockerCmd(t, "version", "--format", "{{ .Server.Version }}")
if strings.HasPrefix(res.Stdout(), "27.") {
t.Skip("USER uid:gid is not supported")
}
res = icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/env-secret/compose.yaml", "run", "bar", "ls", "-al", "/var/run/secrets/bar"),
func(cmd *icmd.Cmd) {
cmd.Env = append(cmd.Env, "SECRET=BAR")
})
res.Assert(t, icmd.Expected{Out: "-r--r--r-- 1 1005 root"})
})
t.Run("secret uid:gid from user", func(t *testing.T) {
res := c.RunDockerCmd(t, "version", "--format", "{{ .Server.Version }}")
if strings.HasPrefix(res.Stdout(), "27.") {
t.Skip("USER uid:gid is not supported")
}
res = icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/env-secret/compose.yaml", "run", "zot", "ls", "-al", "/var/run/secrets/bar"),
func(cmd *icmd.Cmd) {
cmd.Env = append(cmd.Env, "SECRET=BAR")
})
res.Assert(t, icmd.Expected{Out: "-r--r--r-- 1 1005 1005"})
})
}

func TestSecretFromInclude(t *testing.T) {
Expand Down