Skip to content

Commit 1a502e9

Browse files
committed
run: flag to include the Docker API socket
Adds a flag to the create and run command, `--use-api-socket`, that can be used to start a container with the correctly configured parameters to ensure that accessing the docker socket will work with out managing bind mounts and authentication injection. The implementation in this PR resolves the tokens for the current credential set in the client and then copies it into a container at the well know location of /run/secrets/docker/config.json, setting DOCKER_CONFIG to ensure it is resolved by existing tooling. We use a compose-compatible secret location with the hope that the CLI and compose can work together seamlessly. The bind mount for the socket is resolved from the current context, erroring out if the flag is set and the provided socket is not a unix socket. There are a few drawbacks to this approach but it resolves a long standing pain point. We'll continue to develop this as we understand more use cases but it is marked as experimental for now. Signed-off-by: Stephen Day <[email protected]>
1 parent 1adc158 commit 1a502e9

File tree

6 files changed

+146
-13
lines changed

6 files changed

+146
-13
lines changed

cli/command/container/create.go

Lines changed: 141 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,33 @@
11
package container
22

33
import (
4+
"archive/tar"
5+
"bytes"
46
"context"
57
"fmt"
68
"io"
79
"net/netip"
810
"os"
11+
"path"
12+
"strings"
913

1014
"github.com/containerd/platforms"
1115
"github.com/distribution/reference"
1216
"github.com/docker/cli/cli"
1317
"github.com/docker/cli/cli/command"
1418
"github.com/docker/cli/cli/command/completion"
1519
"github.com/docker/cli/cli/command/image"
20+
"github.com/docker/cli/cli/config/configfile"
21+
"github.com/docker/cli/cli/config/types"
1622
"github.com/docker/cli/cli/internal/jsonstream"
1723
"github.com/docker/cli/cli/streams"
1824
"github.com/docker/cli/cli/trust"
1925
"github.com/docker/cli/opts"
2026
"github.com/docker/docker/api/types/container"
2127
imagetypes "github.com/docker/docker/api/types/image"
28+
"github.com/docker/docker/api/types/mount"
2229
"github.com/docker/docker/api/types/versions"
30+
"github.com/docker/docker/client"
2331
"github.com/docker/docker/errdefs"
2432
specs "github.com/opencontainers/image-spec/specs-go/v1"
2533
"github.com/pkg/errors"
@@ -35,11 +43,12 @@ const (
3543
)
3644

3745
type createOptions struct {
38-
name string
39-
platform string
40-
untrusted bool
41-
pull string // always, missing, never
42-
quiet bool
46+
name string
47+
platform string
48+
untrusted bool
49+
pull string // always, missing, never
50+
quiet bool
51+
useAPISocket bool
4352
}
4453

4554
// NewCreateCommand creates a new cobra.Command for `docker create`
@@ -70,6 +79,8 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
7079
flags.StringVar(&options.name, "name", "", "Assign a name to the container")
7180
flags.StringVar(&options.pull, "pull", PullImageMissing, `Pull image before creating ("`+PullImageAlways+`", "|`+PullImageMissing+`", "`+PullImageNever+`")`)
7281
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the pull output")
82+
flags.BoolVarP(&options.useAPISocket, "use-api-socket", "", false, "Bind mount Docker API socket and required auth")
83+
flags.SetAnnotation("use-api-socket", "experimentalCLI", nil) // Marks flag as experimental for now.
7384

7485
// Add an explicit help that doesn't have a `-h` to prevent the conflict
7586
// with hostname
@@ -179,20 +190,20 @@ func (cid *cidFile) Write(id string) error {
179190
return nil
180191
}
181192

182-
func newCIDFile(path string) (*cidFile, error) {
183-
if path == "" {
193+
func newCIDFile(cidPath string) (*cidFile, error) {
194+
if cidPath == "" {
184195
return &cidFile{}, nil
185196
}
186-
if _, err := os.Stat(path); err == nil {
187-
return nil, errors.Errorf("container ID file found, make sure the other container isn't running or delete %s", path)
197+
if _, err := os.Stat(cidPath); err == nil {
198+
return nil, errors.Errorf("container ID file found, make sure the other container isn't running or delete %s", cidPath)
188199
}
189200

190-
f, err := os.Create(path)
201+
f, err := os.Create(cidPath)
191202
if err != nil {
192203
return nil, errors.Wrap(err, "failed to create the container ID file")
193204
}
194205

195-
return &cidFile{path: path, file: f}, nil
206+
return &cidFile{path: cidPath, file: f}, nil
196207
}
197208

198209
//nolint:gocyclo
@@ -239,6 +250,73 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
239250
return nil
240251
}
241252

253+
const dockerConfigPathInContainer = "/run/secrets/docker/config.json"
254+
var apiSocketCreds map[string]types.AuthConfig
255+
256+
if options.useAPISocket {
257+
// We'll create two new mounts to handle this flag:
258+
// 1. Mount the actual docker socket.
259+
// 2. A synthezised ~/.docker/config.json with resolved tokens.
260+
261+
socket := dockerCli.DockerEndpoint().Host
262+
if !strings.HasPrefix(socket, "unix://") {
263+
return "", fmt.Errorf("flag --use-api-socket can only be used with unix sockets: docker endpoint %s incompatible", socket)
264+
}
265+
socket = strings.TrimPrefix(socket, "unix://") // should we confirm absolute path?
266+
267+
containerCfg.HostConfig.Mounts = append(containerCfg.HostConfig.Mounts, mount.Mount{
268+
Type: mount.TypeBind,
269+
Source: socket,
270+
Target: "/var/run/docker.sock",
271+
BindOptions: &mount.BindOptions{},
272+
})
273+
274+
/*
275+
276+
Ideally, we'd like to copy the config into a tmpfs but unfortunately,
277+
the mounts won't be in place until we start the container. This can
278+
leave around the config if the container doesn't get deleted.
279+
280+
We are using the most compose-secret-compatible approach,
281+
which is implemented at
282+
https://github.com/docker/compose/blob/main/pkg/compose/convergence.go#L737
283+
284+
// Prepare a tmpfs mount for our credentials so they go away after the
285+
// container exits. We'll copy into this mount after the container is
286+
// created.
287+
containerCfg.HostConfig.Mounts = append(containerCfg.HostConfig.Mounts, mount.Mount{
288+
Type: mount.TypeTmpfs,
289+
Target: "/docker/",
290+
TmpfsOptions: &mount.TmpfsOptions{
291+
SizeBytes: 1 << 20, // only need a small partition
292+
Mode: 0o600,
293+
},
294+
})
295+
*/
296+
297+
var envvarPresent bool
298+
for _, envvar := range containerCfg.Config.Env {
299+
if strings.HasPrefix(envvar, "DOCKER_CONFIG=") {
300+
envvarPresent = true
301+
}
302+
}
303+
304+
// If the DOCKER_CONFIG env var is already present, we assume the client knows
305+
// what they're doing and don't inject the creds.
306+
if !envvarPresent {
307+
// Set our special little location for the config file.
308+
containerCfg.Config.Env = append(containerCfg.Config.Env,
309+
"DOCKER_CONFIG="+path.Dir(dockerConfigPathInContainer))
310+
311+
// Resolve this here for later, ensuring we error our before we create the container.
312+
creds, err := dockerCli.ConfigFile().GetAllCredentials()
313+
if err != nil {
314+
return "", fmt.Errorf("resolving credentials failed: %w", err)
315+
}
316+
apiSocketCreds = creds // inject these after container creation.
317+
}
318+
}
319+
242320
var platform *specs.Platform
243321
// Engine API version 1.41 first introduced the option to specify platform on
244322
// create. It will produce an error if you try to set a platform on older API
@@ -286,11 +364,25 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
286364
if warn := localhostDNSWarning(*hostConfig); warn != "" {
287365
response.Warnings = append(response.Warnings, warn)
288366
}
367+
368+
containerID = response.ID
289369
for _, w := range response.Warnings {
290370
_, _ = fmt.Fprintln(dockerCli.Err(), "WARNING:", w)
291371
}
292-
err = containerIDFile.Write(response.ID)
293-
return response.ID, err
372+
err = containerIDFile.Write(containerID)
373+
374+
if options.useAPISocket && apiSocketCreds != nil {
375+
// Create a new config file with just the auth.
376+
newConfig := &configfile.ConfigFile{
377+
AuthConfigs: apiSocketCreds,
378+
}
379+
380+
if err := copyDockerConfigIntoContainer(ctx, dockerCli.Client(), containerID, dockerConfigPathInContainer, newConfig); err != nil {
381+
return "", fmt.Errorf("injecting docker config.json into container failed: %w", err)
382+
}
383+
}
384+
385+
return containerID, err
294386
}
295387

296388
// check the DNS settings passed via --dns against localhost regexp to warn if
@@ -321,3 +413,39 @@ func validatePullOpt(val string) error {
321413
)
322414
}
323415
}
416+
417+
// copyDockerConfigIntoContainer takes the client configuration and copies it
418+
// into the container.
419+
//
420+
// The path should be an absolute path in the container, commonly
421+
// /root/.docker/config.json.
422+
func copyDockerConfigIntoContainer(ctx context.Context, dockerAPI client.APIClient, containerID string, configPath string, config *configfile.ConfigFile) error {
423+
var configBuf bytes.Buffer
424+
if err := config.SaveToWriter(&configBuf); err != nil {
425+
return fmt.Errorf("saving creds: %w", err)
426+
}
427+
428+
// We don't need to get super fancy with the tar creation.
429+
var tarBuf bytes.Buffer
430+
tarWriter := tar.NewWriter(&tarBuf)
431+
tarWriter.WriteHeader(&tar.Header{
432+
Name: configPath,
433+
Size: int64(configBuf.Len()),
434+
Mode: 0o600,
435+
})
436+
437+
if _, err := io.Copy(tarWriter, &configBuf); err != nil {
438+
return fmt.Errorf("writing config to tar file for config copy: %w", err)
439+
}
440+
441+
if err := tarWriter.Close(); err != nil {
442+
return fmt.Errorf("closing tar for config copy failed: %w", err)
443+
}
444+
445+
if err := dockerAPI.CopyToContainer(ctx, containerID, "/",
446+
&tarBuf, container.CopyToContainerOptions{}); err != nil {
447+
return fmt.Errorf("copying config.json into container failed: %w", err)
448+
}
449+
450+
return nil
451+
}

cli/command/container/run.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command {
6060
flags.StringVar(&options.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
6161
flags.StringVar(&options.pull, "pull", PullImageMissing, `Pull image before running ("`+PullImageAlways+`", "`+PullImageMissing+`", "`+PullImageNever+`")`)
6262
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the pull output")
63+
flags.BoolVarP(&options.createOptions.useAPISocket, "use-api-socket", "", false, "Bind mount Docker API socket and required auth")
6364

6465
// Add an explicit help that doesn't have a `-h` to prevent the conflict
6566
// with hostname

docs/reference/commandline/container_create.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ Create a new container
104104
| `--tmpfs` | `list` | | Mount a tmpfs directory |
105105
| `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY |
106106
| `--ulimit` | `ulimit` | | Ulimit options |
107+
| `--use-api-socket` | `bool` | | Bind mount Docker API socket and required auth |
107108
| `-u`, `--user` | `string` | | Username or UID (format: <name\|uid>[:<group\|gid>]) |
108109
| `--userns` | `string` | | User namespace to use |
109110
| `--uts` | `string` | | UTS namespace to use |

docs/reference/commandline/container_run.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ Create and run a new container from an image
107107
| [`--tmpfs`](#tmpfs) | `list` | | Mount a tmpfs directory |
108108
| [`-t`](#tty), [`--tty`](#tty) | `bool` | | Allocate a pseudo-TTY |
109109
| [`--ulimit`](#ulimit) | `ulimit` | | Ulimit options |
110+
| `--use-api-socket` | `bool` | | Bind mount Docker API socket and required auth |
110111
| `-u`, `--user` | `string` | | Username or UID (format: <name\|uid>[:<group\|gid>]) |
111112
| [`--userns`](#userns) | `string` | | User namespace to use |
112113
| [`--uts`](#uts) | `string` | | UTS namespace to use |

docs/reference/commandline/create.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ Create a new container
104104
| `--tmpfs` | `list` | | Mount a tmpfs directory |
105105
| `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY |
106106
| `--ulimit` | `ulimit` | | Ulimit options |
107+
| `--use-api-socket` | `bool` | | Bind mount Docker API socket and required auth |
107108
| `-u`, `--user` | `string` | | Username or UID (format: <name\|uid>[:<group\|gid>]) |
108109
| `--userns` | `string` | | User namespace to use |
109110
| `--uts` | `string` | | UTS namespace to use |

docs/reference/commandline/run.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ Create and run a new container from an image
107107
| `--tmpfs` | `list` | | Mount a tmpfs directory |
108108
| `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY |
109109
| `--ulimit` | `ulimit` | | Ulimit options |
110+
| `--use-api-socket` | `bool` | | Bind mount Docker API socket and required auth |
110111
| `-u`, `--user` | `string` | | Username or UID (format: <name\|uid>[:<group\|gid>]) |
111112
| `--userns` | `string` | | User namespace to use |
112113
| `--uts` | `string` | | UTS namespace to use |

0 commit comments

Comments
 (0)