Skip to content

Commit 0108788

Browse files
committed
volumes: support for api-socket volume type
The `api-socket` volume type allows an easy path to run containers that need access to the Docker API socket with a simple configuration on the service definition: service: foo: ... volumes: - type: api-socket The bind mount and credentials are automatically injected using existing service infrastructure. Some work is still needed to make this a bit more robust. Signed-off-by: Stephen Day <stephen.day@docker.com>
1 parent 51907d9 commit 0108788

File tree

2 files changed

+84
-13
lines changed

2 files changed

+84
-13
lines changed

Diff for: pkg/compose/create.go

+53-10
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/docker/compose/v2/pkg/prompt"
3838
"github.com/docker/compose/v2/pkg/utils"
3939
"github.com/docker/docker/api/types/blkiodev"
40+
"github.com/docker/cli/cli/config/configfile"
4041
"github.com/docker/docker/api/types/container"
4142
"github.com/docker/docker/api/types/filters"
4243
"github.com/docker/docker/api/types/mount"
@@ -50,6 +51,11 @@ import (
5051
cdi "tags.cncf.io/container-device-interface/pkg/parser"
5152
)
5253

54+
const (
55+
secretsDir = "/run/secrets/"
56+
dockerConfigPathInContainer = secretsDir+"/docker/config.json"
57+
)
58+
5359
type createOptions struct {
5460
AutoRemove bool
5561
AttachStdin bool
@@ -241,6 +247,11 @@ func (s *composeService) getCreateConfigs(ctx context.Context,
241247
proxyConfig := types.MappingWithEquals(s.configFile().ParseProxyConfig(s.apiClient().DaemonHost(), nil))
242248
env := proxyConfig.OverrideBy(service.Environment)
243249

250+
if hasAPISocket(service) {
251+
// Inject the Docker API socket credentials path in the environment.
252+
env = append(env, "DOCKER_CONFIG="+path.Dir(dockerConfigPathInContainer))
253+
}
254+
244255
var mainNwName string
245256
var mainNw *types.ServiceNetworkConfig
246257
if len(service.Networks) > 0 {
@@ -371,6 +382,14 @@ func (s *composeService) getCreateConfigs(ctx context.Context,
371382
return cfgs, nil
372383
}
373384

385+
func hasAPISocket(service types.ServiceConfig) bool {
386+
for _, v := range service.Volumes {
387+
if v.Type == "api-socket" {
388+
return true
389+
}
390+
}
391+
}
392+
374393
// prepareContainerMACAddress handles the service-level mac_address field and the newer mac_address field added to service
375394
// network config. This newer field is only compatible with the Engine API v1.44 (and onwards), and this API version
376395
// also deprecates the container-wide mac_address field. Thus, this method will validate service config and mutate the
@@ -1001,7 +1020,7 @@ func (s *composeService) buildContainerMountOptions(ctx context.Context, p types
10011020
}
10021021
}
10031022

1004-
mounts, err := fillBindMounts(p, service, mounts)
1023+
mounts, err := s.fillBindMounts(p, service, mounts)
10051024
if err != nil {
10061025
return nil, err
10071026
}
@@ -1013,9 +1032,9 @@ func (s *composeService) buildContainerMountOptions(ctx context.Context, p types
10131032
return values, nil
10141033
}
10151034

1016-
func fillBindMounts(p types.Project, s types.ServiceConfig, m map[string]mount.Mount) (map[string]mount.Mount, error) {
1035+
func (s *composeService) fillBindMounts(p types.Project, service types.ServiceConfig, m map[string]mount.Mount) (map[string]mount.Mount, error) {
10171036
for _, v := range s.Volumes {
1018-
bindMount, err := buildMount(p, v)
1037+
bindMount, err := s.buildMount(p, v)
10191038
if err != nil {
10201039
return nil, err
10211040
}
@@ -1096,11 +1115,13 @@ func buildContainerConfigMounts(p types.Project, s types.ServiceConfig) ([]mount
10961115
return values, nil
10971116
}
10981117

1099-
func buildContainerSecretMounts(p types.Project, s types.ServiceConfig) ([]mount.Mount, error) {
1118+
func (s *composeService) buildContainerSecretMounts(p types.Project, service types.ServiceConfig) ([]mount.Mount, error) {
11001119
mounts := map[string]mount.Mount{}
11011120

1102-
secretsDir := "/run/secrets/"
1103-
for _, secret := range s.Secrets {
1121+
var secrets []types.ServiceSecretConfig
1122+
1123+
1124+
for _, secret := range secrets {
11041125
target := secret.Target
11051126
if secret.Target == "" {
11061127
target = secretsDir + secret.Source
@@ -1171,7 +1192,7 @@ func isWindowsAbs(p string) bool {
11711192
return false
11721193
}
11731194

1174-
func buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount.Mount, error) {
1195+
func (s *composeService) buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount.Mount, error) {
11751196
source := volume.Source
11761197
// on windows, filepath.IsAbs(source) is false for unix style abs path like /var/run/docker.sock.
11771198
// do not replace these with filepath.Abs(source) that will include a default drive.
@@ -1192,14 +1213,33 @@ func buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount.
11921213
}
11931214
}
11941215

1216+
vtype := mount.Type(volume.Type)
1217+
if volume.Type == "api-socket" { // TODO: use VolumeTypeAPISocket when compose-go PR merged
1218+
vtype = mount.TypeBind // rewrite to a bind mount
1219+
1220+
if volume.Source == "" {
1221+
socketPath := s.dockerCli.DockerEndpoint().Host
1222+
if !strings.HasPrefix(socket, "unix://") {
1223+
return "", fmt.Errorf("flag --use-api-socket can only be used with unix sockets: docker endpoint %s incompatible", socket)
1224+
}
1225+
socket = strings.TrimPrefix(socket, "unix://") // should we confirm absolute path?
1226+
volume.Source = socketPath
1227+
}
1228+
1229+
if volume.Target == "" {
1230+
volume.Target = "/var/run/docker.sock"
1231+
}
1232+
}
1233+
11951234
bind, vol, tmpfs, img := buildMountOptions(volume)
11961235

11971236
if bind != nil {
1198-
volume.Type = types.VolumeTypeBind
1237+
vtype = mount.TypeBind
11991238
}
12001239

1240+
12011241
return mount.Mount{
1202-
Type: mount.Type(volume.Type),
1242+
Type: vtype,
12031243
Source: source,
12041244
Target: volume.Target,
12051245
ReadOnly: volume.ReadOnly,
@@ -1212,7 +1252,7 @@ func buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount.
12121252
}
12131253

12141254
func buildMountOptions(volume types.ServiceVolumeConfig) (*mount.BindOptions, *mount.VolumeOptions, *mount.TmpfsOptions, *mount.ImageOptions) {
1215-
if volume.Type != types.VolumeTypeBind && volume.Bind != nil {
1255+
if (volume.Type != types.VolumeTypeBind && volume.Type != "api-socket" && volume.Bind != nil { // TODO: use VolumeTypeAPISocket when compose-go PR merged
12161256
logrus.Warnf("mount of type `%s` should not define `bind` option", volume.Type)
12171257
}
12181258
if volume.Type != types.VolumeTypeVolume && volume.Volume != nil {
@@ -1228,6 +1268,8 @@ func buildMountOptions(volume types.ServiceVolumeConfig) (*mount.BindOptions, *m
12281268
switch volume.Type {
12291269
case "bind":
12301270
return buildBindOption(volume.Bind), nil, nil, nil
1271+
case "api-socket":
1272+
return nil, nil, nil, nil // defer to defaults when binding the API socket
12311273
case "volume":
12321274
return nil, buildVolumeOptions(volume.Volume), nil, nil
12331275
case "tmpfs":
@@ -1242,6 +1284,7 @@ func buildBindOption(bind *types.ServiceVolumeBind) *mount.BindOptions {
12421284
if bind == nil {
12431285
return nil
12441286
}
1287+
12451288
opts := &mount.BindOptions{
12461289
Propagation: mount.Propagation(bind.Propagation),
12471290
CreateMountpoint: bind.CreateHostPath,

Diff for: pkg/compose/secrets.go

+31-3
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,39 @@ import (
2525
"time"
2626

2727
"github.com/compose-spec/compose-go/v2/types"
28+
"github.com/docker/cli/cli/config/configfile"
2829
"github.com/docker/docker/api/types/container"
2930
)
3031

3132
func (s *composeService) injectSecrets(ctx context.Context, project *types.Project, service types.ServiceConfig, id string) error {
32-
for _, config := range service.Secrets {
33+
if hasAPISocket(service) {
34+
// Generate a "secret" for the authconfig for the API socket.
35+
creds, err := s.dockerCli.ConfigFile().GetAllCredentials()
36+
if err != nil {
37+
return fmt.Errorf("resolving credentials failed: %w", err)
38+
}
39+
40+
// Create a new config file with just the auth.
41+
newConfig := &configfile.ConfigFile{
42+
AuthConfigs: creds,
43+
}
44+
var configBuf bytes.Buffer
45+
if err := newConfig.SaveToWriter(&configBuf); err != nil {
46+
return fmt.Errorf("saving creds for API socket: %w", err)
47+
}
48+
49+
mode := types.FileMode(0o400)
50+
b, err := createTar(configBuf.String(), types.FileReferenceConfig{
51+
Target: dockerConfigPathInContainer,
52+
Mode: &mode,
53+
})
54+
55+
if err = s.apiClient().CopyToContainer(ctx, id, "/", &b, container.CopyToContainerOptions{}); err != nil {
56+
return fmt.Errorf("copying creds for API socket: %w", err)
57+
}
58+
}
59+
60+
for _, config := range secrets {
3361
file := project.Secrets[config.Source]
3462
if file.Environment == "" {
3563
continue
@@ -40,9 +68,9 @@ func (s *composeService) injectSecrets(ctx context.Context, project *types.Proje
4068
}
4169

4270
if config.Target == "" {
43-
config.Target = "/run/secrets/" + config.Source
71+
config.Target = secretsDir + config.Source
4472
} else if !isAbsTarget(config.Target) {
45-
config.Target = "/run/secrets/" + config.Target
73+
config.Target = secretsDir + config.Target
4674
}
4775

4876
content := file.Content

0 commit comments

Comments
 (0)