From 0e11384cb04c060d3dfd2eec12048882a8da8495 Mon Sep 17 00:00:00 2001 From: andoks Date: Wed, 25 Sep 2024 11:59:47 +0200 Subject: [PATCH] Add support for configs.file's and secrets.file's on remote docker hosts Copy configs.file's and secrets.file's instead of bind-mounting them to make it possible to use file configs when working with remote docker hosts (like setting DOCKER_HOST to a ssh address or setting docker context) Includes support for config.files and secrets.files as directories. Note that file.Content as source of secrets is denied elsewhere with the error "validating docker-compose.yml: secrets.content_secret Additional property content is not allowed" implements: #11867 --- pkg/compose/create.go | 128 -------------------------- pkg/compose/secrets.go | 204 +++++++++++++++++++++++++++++++---------- 2 files changed, 156 insertions(+), 176 deletions(-) diff --git a/pkg/compose/create.go b/pkg/compose/create.go index f4178d6af7..00925d2b01 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -897,137 +897,9 @@ func fillBindMounts(p types.Project, s types.ServiceConfig, m map[string]mount.M m[bindMount.Target] = bindMount } - secrets, err := buildContainerSecretMounts(p, s) - if err != nil { - return nil, err - } - for _, s := range secrets { - if _, found := m[s.Target]; found { - continue - } - m[s.Target] = s - } - - configs, err := buildContainerConfigMounts(p, s) - if err != nil { - return nil, err - } - for _, c := range configs { - if _, found := m[c.Target]; found { - continue - } - m[c.Target] = c - } return m, nil } -func buildContainerConfigMounts(p types.Project, s types.ServiceConfig) ([]mount.Mount, error) { - var mounts = map[string]mount.Mount{} - - configsBaseDir := "/" - for _, config := range s.Configs { - target := config.Target - if config.Target == "" { - target = configsBaseDir + config.Source - } else if !isAbsTarget(config.Target) { - target = configsBaseDir + config.Target - } - - if config.UID != "" || config.GID != "" || config.Mode != nil { - logrus.Warn("config `uid`, `gid` and `mode` are not supported, they will be ignored") - } - - definedConfig := p.Configs[config.Source] - if definedConfig.External { - return nil, fmt.Errorf("unsupported external config %s", definedConfig.Name) - } - - if definedConfig.Driver != "" { - return nil, errors.New("Docker Compose does not support configs.*.driver") - } - if definedConfig.TemplateDriver != "" { - return nil, errors.New("Docker Compose does not support configs.*.template_driver") - } - - if definedConfig.Environment != "" || definedConfig.Content != "" { - continue - } - - bindMount, err := buildMount(p, types.ServiceVolumeConfig{ - Type: types.VolumeTypeBind, - Source: definedConfig.File, - Target: target, - ReadOnly: true, - }) - if err != nil { - return nil, err - } - mounts[target] = bindMount - } - values := make([]mount.Mount, 0, len(mounts)) - for _, v := range mounts { - values = append(values, v) - } - return values, nil -} - -func buildContainerSecretMounts(p types.Project, s types.ServiceConfig) ([]mount.Mount, error) { - var mounts = map[string]mount.Mount{} - - secretsDir := "/run/secrets/" - for _, secret := range s.Secrets { - target := secret.Target - if secret.Target == "" { - target = secretsDir + secret.Source - } else if !isAbsTarget(secret.Target) { - target = secretsDir + secret.Target - } - - if secret.UID != "" || secret.GID != "" || secret.Mode != nil { - logrus.Warn("secrets `uid`, `gid` and `mode` are not supported, they will be ignored") - } - - definedSecret := p.Secrets[secret.Source] - if definedSecret.External { - return nil, fmt.Errorf("unsupported external secret %s", definedSecret.Name) - } - - if definedSecret.Driver != "" { - return nil, errors.New("Docker Compose does not support secrets.*.driver") - } - if definedSecret.TemplateDriver != "" { - return nil, errors.New("Docker Compose does not support secrets.*.template_driver") - } - - if definedSecret.Environment != "" { - continue - } - - if _, err := os.Stat(definedSecret.File); os.IsNotExist(err) { - logrus.Warnf("secret file %s does not exist", definedSecret.Name) - } - - mnt, err := buildMount(p, types.ServiceVolumeConfig{ - Type: types.VolumeTypeBind, - Source: definedSecret.File, - Target: target, - ReadOnly: true, - Bind: &types.ServiceVolumeBind{ - CreateHostPath: false, - }, - }) - if err != nil { - return nil, err - } - mounts[target] = mnt - } - values := make([]mount.Mount, 0, len(mounts)) - for _, v := range mounts { - values = append(values, v) - } - return values, nil -} - func isAbsTarget(p string) bool { return isUnixAbs(p) || isWindowsAbs(p) } diff --git a/pkg/compose/secrets.go b/pkg/compose/secrets.go index 4ba49eed44..6b8431c963 100644 --- a/pkg/compose/secrets.go +++ b/pkg/compose/secrets.go @@ -21,36 +21,49 @@ import ( "bytes" "context" "fmt" + "io" + "io/fs" + "os" + "path/filepath" "strconv" "time" "github.com/compose-spec/compose-go/v2/types" - "github.com/docker/docker/api/types/container" + moby "github.com/docker/docker/api/types/container" ) 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 == "" { - continue - } - if config.Target == "" { config.Target = "/run/secrets/" + config.Source } else if !isAbsTarget(config.Target) { config.Target = "/run/secrets/" + config.Target } - env, ok := project.Environment[file.Environment] - if !ok { - return fmt.Errorf("environment variable %q required by file %q is not set", file.Environment, file.Name) + file := project.Secrets[config.Source] + var tarArchive bytes.Buffer + var err error + switch { + case file.File != "": + tarArchive, err = createTarArchiveOf(file.File, types.FileReferenceConfig(config)) + case file.Environment != "": + env, ok := project.Environment[file.Environment] + if !ok { + return fmt.Errorf("environment variable %q required by file %q is not set", file.Environment, file.Name) + } + tarArchive, err = createTarredFileOf(env, types.FileReferenceConfig(config)) } - b, err := createTar(env, types.FileReferenceConfig(config)) + if err != nil { return err } - err = s.apiClient().CopyToContainer(ctx, id, "/", &b, container.CopyToContainerOptions{ + // secret was handled elsewhere (e.g it was external) + if tarArchive.Len() == 0 { + continue + } + + err = s.apiClient().CopyToContainer(ctx, id, "/", &tarArchive, moby.CopyToContainerOptions{ CopyUIDGID: config.UID != "" || config.GID != "", }) if err != nil { @@ -62,29 +75,36 @@ func (s *composeService) injectSecrets(ctx context.Context, project *types.Proje func (s *composeService) injectConfigs(ctx context.Context, project *types.Project, service types.ServiceConfig, id string) error { for _, config := range service.Configs { + if config.Target == "" { + config.Target = "/" + config.Source + } + file := project.Configs[config.Source] - content := file.Content - if file.Environment != "" { + var tarArchive bytes.Buffer + var err error + switch { + case file.File != "": + tarArchive, err = createTarArchiveOf(file.File, types.FileReferenceConfig(config)) + case file.Content != "": + tarArchive, err = createTarredFileOf(file.Content, types.FileReferenceConfig(config)) + case file.Environment != "": env, ok := project.Environment[file.Environment] if !ok { return fmt.Errorf("environment variable %q required by file %q is not set", file.Environment, file.Name) } - content = env - } - if content == "" { - continue - } - - if config.Target == "" { - config.Target = "/" + config.Source + tarArchive, err = createTarredFileOf(env, types.FileReferenceConfig(config)) } - b, err := createTar(content, types.FileReferenceConfig(config)) if err != nil { return err } - err = s.apiClient().CopyToContainer(ctx, id, "/", &b, container.CopyToContainerOptions{ + // config was handled elsewhere (e.g it was external) + if tarArchive.Len() == 0 { + continue + } + + err = s.apiClient().CopyToContainer(ctx, id, "/", &tarArchive, moby.CopyToContainerOptions{ CopyUIDGID: config.UID != "" || config.GID != "", }) if err != nil { @@ -94,47 +114,135 @@ func (s *composeService) injectConfigs(ctx context.Context, project *types.Proje return nil } -func createTar(env string, config types.FileReferenceConfig) (bytes.Buffer, error) { - value := []byte(env) +func createTarredFileOf(value string, config types.FileReferenceConfig) (bytes.Buffer, error) { + mode, uid, gid, err := makeTarFileEntryParams(config) + if err != nil { + return bytes.Buffer{}, fmt.Errorf("failed parsing target file parameters") + } + b := bytes.Buffer{} tarWriter := tar.NewWriter(&b) - mode := uint32(0o444) + valueAsBytes := []byte(value) + header := &tar.Header{ + Name: config.Target, + Size: int64(len(valueAsBytes)), + Mode: mode, + ModTime: time.Now(), + Uid: uid, + Gid: gid, + } + err = tarWriter.WriteHeader(header) + if err != nil { + return bytes.Buffer{}, err + } + _, err = tarWriter.Write(valueAsBytes) + if err != nil { + return bytes.Buffer{}, err + } + err = tarWriter.Close() + return b, err +} + +func createTarArchiveOf(path string, config types.FileReferenceConfig) (bytes.Buffer, error) { + // need to treat files and directories differently + fi, err := os.Stat(path) + if err != nil { + return bytes.Buffer{}, err + } + + // if path is not directory, try to treat it as a file by reading its value + if !fi.IsDir() { + buf, err := os.ReadFile(path) + if err == nil { + return createTarredFileOf(string(buf), config) + } + } + + mode, uid, gid, err := makeTarFileEntryParams(config) + if err != nil { + return bytes.Buffer{}, fmt.Errorf("failed parsing target file parameters") + } + + subdir := os.DirFS(path) + b := bytes.Buffer{} + tarWriter := tar.NewWriter(&b) + + // build the tar by walking instead of using archive/tar.Writer.AddFS to be able to adjust mode, gid and uid + err = fs.WalkDir(subdir, ".", func(filePath string, d fs.DirEntry, err error) error { + header := &tar.Header{ + Name: filepath.Join(config.Target, filePath), + Mode: mode, + ModTime: time.Now(), + Uid: uid, + Gid: gid, + } + + if d.IsDir() { + // tar requires that directory headers ends with a slash + header.Name = header.Name + "/" + err = tarWriter.WriteHeader(header) + if err != nil { + return fmt.Errorf("failed writing tar header of directory %v while walking diretory structure, error was: %w", filePath, err) + } + } else { + f, err := subdir.Open(filePath) + if err != nil { + return err + } + defer f.Close() + + valueAsBytes, err := io.ReadAll(f) + if err != nil { + return fmt.Errorf("failed reading file %v for to send to container, error was: %w", filePath, err) + } + + header.Size = int64(len(valueAsBytes)) + err = tarWriter.WriteHeader(header) + if err != nil { + return fmt.Errorf("failed writing tar header for file %v while walking diretory structure, error was: %w", filePath, err) + } + + _, err = tarWriter.Write(valueAsBytes) + if err != nil { + return fmt.Errorf("failed writing file content of %v into tar archive while walking directory structure, error was: %w", filePath, err) + } + } + + return nil + }) + + if err != nil { + return bytes.Buffer{}, fmt.Errorf("failed building tar archive while walking config directory structure, error was: %w", err) + } + + err = tarWriter.Close() + if err != nil { + return bytes.Buffer{}, fmt.Errorf("failed closing tar archive after writing, error was: %w", err) + } + + return b, err +} + +func makeTarFileEntryParams(config types.FileReferenceConfig) (mode int64, uid, gid int, err error) { + mode = 0o444 if config.Mode != nil { - mode = *config.Mode + mode = int64(*config.Mode) } - var uid, gid int if config.UID != "" { v, err := strconv.Atoi(config.UID) if err != nil { - return b, err + return 0, 0, 0, err } uid = v } if config.GID != "" { v, err := strconv.Atoi(config.GID) if err != nil { - return b, err + return 0, 0, 0, err } gid = v } - header := &tar.Header{ - Name: config.Target, - Size: int64(len(value)), - Mode: int64(mode), - ModTime: time.Now(), - Uid: uid, - Gid: gid, - } - err := tarWriter.WriteHeader(header) - if err != nil { - return bytes.Buffer{}, err - } - _, err = tarWriter.Write(value) - if err != nil { - return bytes.Buffer{}, err - } - err = tarWriter.Close() - return b, err + return mode, uid, gid, nil }