diff --git a/pkg/compose/create.go b/pkg/compose/create.go index 5c9d13436fc..726e0e5aba3 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -50,6 +50,11 @@ import ( cdi "tags.cncf.io/container-device-interface/pkg/parser" ) +const ( + secretsDir = "/run/secrets/" + dockerConfigPathInContainer = secretsDir + "/docker/config.json" +) + type createOptions struct { AutoRemove bool AttachStdin bool @@ -241,6 +246,12 @@ func (s *composeService) getCreateConfigs(ctx context.Context, proxyConfig := types.MappingWithEquals(s.configFile().ParseProxyConfig(s.apiClient().DaemonHost(), nil)) env := proxyConfig.OverrideBy(service.Environment) + if hasAPISocket(service) { + // Inject the Docker API socket credentials path in the environment. + p := dockerConfigPathInContainer + env["DOCKER_CONFIG"] = &p + } + var mainNwName string var mainNw *types.ServiceNetworkConfig if len(service.Networks) > 0 { @@ -371,6 +382,16 @@ func (s *composeService) getCreateConfigs(ctx context.Context, return cfgs, nil } +func hasAPISocket(service types.ServiceConfig) bool { + for _, v := range service.Volumes { + if v.Type == "api-socket" { + return true + } + } + + return false +} + // prepareContainerMACAddress handles the service-level mac_address field and the newer mac_address field added to service // network config. This newer field is only compatible with the Engine API v1.44 (and onwards), and this API version // also deprecates the container-wide mac_address field. Thus, this method will validate service config and mutate the @@ -1001,7 +1022,7 @@ func (s *composeService) buildContainerMountOptions(ctx context.Context, p types } } - mounts, err := fillBindMounts(p, service, mounts) + mounts, err := s.fillBindMounts(p, service, mounts) if err != nil { return nil, err } @@ -1013,16 +1034,16 @@ func (s *composeService) buildContainerMountOptions(ctx context.Context, p types return values, nil } -func fillBindMounts(p types.Project, s types.ServiceConfig, m map[string]mount.Mount) (map[string]mount.Mount, error) { - for _, v := range s.Volumes { - bindMount, err := buildMount(p, v) +func (s *composeService) fillBindMounts(p types.Project, service types.ServiceConfig, m map[string]mount.Mount) (map[string]mount.Mount, error) { + for _, v := range service.Volumes { + bindMount, err := s.buildMount(p, v) if err != nil { return nil, err } m[bindMount.Target] = bindMount } - secrets, err := buildContainerSecretMounts(p, s) + secrets, err := s.buildContainerSecretMounts(p, service) if err != nil { return nil, err } @@ -1033,7 +1054,7 @@ func fillBindMounts(p types.Project, s types.ServiceConfig, m map[string]mount.M m[s.Target] = s } - configs, err := buildContainerConfigMounts(p, s) + configs, err := s.buildContainerConfigMounts(p, service) if err != nil { return nil, err } @@ -1046,11 +1067,11 @@ func fillBindMounts(p types.Project, s types.ServiceConfig, m map[string]mount.M return m, nil } -func buildContainerConfigMounts(p types.Project, s types.ServiceConfig) ([]mount.Mount, error) { +func (s *composeService) buildContainerConfigMounts(p types.Project, service types.ServiceConfig) ([]mount.Mount, error) { mounts := map[string]mount.Mount{} configsBaseDir := "/" - for _, config := range s.Configs { + for _, config := range service.Configs { target := config.Target if config.Target == "" { target = configsBaseDir + config.Source @@ -1078,7 +1099,7 @@ func buildContainerConfigMounts(p types.Project, s types.ServiceConfig) ([]mount logrus.Warn("config `uid`, `gid` and `mode` are not supported, they will be ignored") } - bindMount, err := buildMount(p, types.ServiceVolumeConfig{ + bindMount, err := s.buildMount(p, types.ServiceVolumeConfig{ Type: types.VolumeTypeBind, Source: definedConfig.File, Target: target, @@ -1096,11 +1117,10 @@ func buildContainerConfigMounts(p types.Project, s types.ServiceConfig) ([]mount return values, nil } -func buildContainerSecretMounts(p types.Project, s types.ServiceConfig) ([]mount.Mount, error) { +func (s *composeService) buildContainerSecretMounts(p types.Project, service types.ServiceConfig) ([]mount.Mount, error) { mounts := map[string]mount.Mount{} - secretsDir := "/run/secrets/" - for _, secret := range s.Secrets { + for _, secret := range service.Secrets { target := secret.Target if secret.Target == "" { target = secretsDir + secret.Source @@ -1132,7 +1152,7 @@ func buildContainerSecretMounts(p types.Project, s types.ServiceConfig) ([]mount logrus.Warnf("secret file %s does not exist", definedSecret.Name) } - mnt, err := buildMount(p, types.ServiceVolumeConfig{ + mnt, err := s.buildMount(p, types.ServiceVolumeConfig{ Type: types.VolumeTypeBind, Source: definedSecret.File, Target: target, @@ -1171,7 +1191,7 @@ func isWindowsAbs(p string) bool { return false } -func buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount.Mount, error) { +func (s *composeService) buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount.Mount, error) { source := volume.Source // on windows, filepath.IsAbs(source) is false for unix style abs path like /var/run/docker.sock. // do not replace these with filepath.Abs(source) that will include a default drive. @@ -1192,14 +1212,32 @@ func buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount. } } + vtype := mount.Type(volume.Type) + if volume.Type == "api-socket" { // TODO: use VolumeTypeAPISocket when compose-go PR merged + vtype = mount.TypeBind // rewrite to a bind mount + + if volume.Source == "" { + socketPath := s.dockerCli.DockerEndpoint().Host + if !strings.HasPrefix(socketPath, "unix://") { + return mount.Mount{}, fmt.Errorf("flag --use-api-socket can only be used with unix sockets: docker endpoint %s incompatible", socketPath) + } + socketPath = strings.TrimPrefix(socketPath, "unix://") // should we confirm absolute path? + volume.Source = socketPath + } + + if volume.Target == "" { + volume.Target = "/var/run/docker.sock" + } + } + bind, vol, tmpfs, img := buildMountOptions(volume) if bind != nil { - volume.Type = types.VolumeTypeBind + vtype = mount.TypeBind } return mount.Mount{ - Type: mount.Type(volume.Type), + Type: vtype, Source: source, Target: volume.Target, ReadOnly: volume.ReadOnly, @@ -1212,7 +1250,7 @@ func buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount. } func buildMountOptions(volume types.ServiceVolumeConfig) (*mount.BindOptions, *mount.VolumeOptions, *mount.TmpfsOptions, *mount.ImageOptions) { - if volume.Type != types.VolumeTypeBind && volume.Bind != nil { + if volume.Type != types.VolumeTypeBind && volume.Type != "api-socket" && volume.Bind != nil { // TODO: use VolumeTypeAPISocket when compose-go PR merged logrus.Warnf("mount of type `%s` should not define `bind` option", volume.Type) } if volume.Type != types.VolumeTypeVolume && volume.Volume != nil { @@ -1228,6 +1266,8 @@ func buildMountOptions(volume types.ServiceVolumeConfig) (*mount.BindOptions, *m switch volume.Type { case "bind": return buildBindOption(volume.Bind), nil, nil, nil + case "api-socket": + return nil, nil, nil, nil // defer to defaults when binding the API socket case "volume": return nil, buildVolumeOptions(volume.Volume), nil, nil case "tmpfs": @@ -1242,6 +1282,7 @@ func buildBindOption(bind *types.ServiceVolumeBind) *mount.BindOptions { if bind == nil { return nil } + opts := &mount.BindOptions{ Propagation: mount.Propagation(bind.Propagation), CreateMountpoint: bind.CreateHostPath, diff --git a/pkg/compose/secrets.go b/pkg/compose/secrets.go index e8064cca8b2..eb6e314fd71 100644 --- a/pkg/compose/secrets.go +++ b/pkg/compose/secrets.go @@ -25,10 +25,38 @@ import ( "time" "github.com/compose-spec/compose-go/v2/types" + "github.com/docker/cli/cli/config/configfile" "github.com/docker/docker/api/types/container" ) func (s *composeService) injectSecrets(ctx context.Context, project *types.Project, service types.ServiceConfig, id string) error { + if hasAPISocket(service) { + // Generate a "secret" for the authconfig for the API socket. + creds, err := s.dockerCli.ConfigFile().GetAllCredentials() + if err != nil { + return fmt.Errorf("resolving credentials failed: %w", err) + } + + // Create a new config file with just the auth. + newConfig := &configfile.ConfigFile{ + AuthConfigs: creds, + } + var configBuf bytes.Buffer + if err := newConfig.SaveToWriter(&configBuf); err != nil { + return fmt.Errorf("saving creds for API socket: %w", err) + } + + mode := types.FileMode(0o400) + b, err := createTar(configBuf.String(), types.FileReferenceConfig{ + Target: dockerConfigPathInContainer, + Mode: &mode, + }) + + if err = s.apiClient().CopyToContainer(ctx, id, "/", &b, container.CopyToContainerOptions{}); err != nil { + return fmt.Errorf("copying creds for API socket: %w", err) + } + } + for _, config := range service.Secrets { file := project.Secrets[config.Source] if file.Environment == "" { @@ -40,9 +68,9 @@ func (s *composeService) injectSecrets(ctx context.Context, project *types.Proje } if config.Target == "" { - config.Target = "/run/secrets/" + config.Source + config.Target = secretsDir + config.Source } else if !isAbsTarget(config.Target) { - config.Target = "/run/secrets/" + config.Target + config.Target = secretsDir + config.Target } content := file.Content