diff --git a/cli/command/container/create.go b/cli/command/container/create.go index b23f1963be8e..e2ccb9bd4cdb 100644 --- a/cli/command/container/create.go +++ b/cli/command/container/create.go @@ -1,11 +1,15 @@ package container import ( + "archive/tar" + "bytes" "context" "fmt" "io" "net/netip" "os" + "path" + "strings" "github.com/containerd/platforms" "github.com/distribution/reference" @@ -13,13 +17,16 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/image" + "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/internal/jsonstream" "github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/trust" "github.com/docker/cli/opts" "github.com/docker/docker/api/types/container" imagetypes "github.com/docker/docker/api/types/image" + "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/versions" + "github.com/docker/docker/client" "github.com/docker/docker/errdefs" specs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" @@ -35,11 +42,12 @@ const ( ) type createOptions struct { - name string - platform string - untrusted bool - pull string // always, missing, never - quiet bool + name string + platform string + untrusted bool + pull string // always, missing, never + quiet bool + useDockerSocket bool } // NewCreateCommand creates a new cobra.Command for `docker create` @@ -70,6 +78,7 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command { flags.StringVar(&options.name, "name", "", "Assign a name to the container") flags.StringVar(&options.pull, "pull", PullImageMissing, `Pull image before creating ("`+PullImageAlways+`", "|`+PullImageMissing+`", "`+PullImageNever+`")`) flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the pull output") + flags.BoolVarP(&options.useDockerSocket, "use-docker-socket", "", false, "Bind mount docker socket and required auth") // Add an explicit help that doesn't have a `-h` to prevent the conflict // with hostname @@ -179,20 +188,20 @@ func (cid *cidFile) Write(id string) error { return nil } -func newCIDFile(path string) (*cidFile, error) { - if path == "" { +func newCIDFile(cidPath string) (*cidFile, error) { + if cidPath == "" { return &cidFile{}, nil } - if _, err := os.Stat(path); err == nil { - return nil, errors.Errorf("container ID file found, make sure the other container isn't running or delete %s", path) + if _, err := os.Stat(cidPath); err == nil { + return nil, errors.Errorf("container ID file found, make sure the other container isn't running or delete %s", cidPath) } - f, err := os.Create(path) + f, err := os.Create(cidPath) if err != nil { return nil, errors.Wrap(err, "failed to create the container ID file") } - return &cidFile{path: path, file: f}, nil + return &cidFile{path: cidPath, file: f}, nil } //nolint:gocyclo @@ -239,6 +248,53 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c return nil } + const dockerConfigPathInContainer = "/run/secrets/docker/config.json" + if options.useDockerSocket { + // We'll create two new mounts to handle this flag: + // 1. Mount the actual docker socket. + // 2. A synthezised ~/.docker/config.json with resolved tokens. + + socket := dockerCli.DockerEndpoint().Host + if !strings.HasPrefix(socket, "unix://") { + return "", fmt.Errorf("flag --use-docker-socket can only be used with unix sockets: docker endpoint %s incompatible", socket) + } + socket = strings.TrimPrefix(socket, "unix://") // should we confirm absolute path? + + containerCfg.HostConfig.Mounts = append(containerCfg.HostConfig.Mounts, mount.Mount{ + Type: mount.TypeBind, + Source: socket, + Target: "/var/run/docker.sock", + BindOptions: &mount.BindOptions{}, + }) + + /* + + Ideally, we'd like to copy the config into a tmpfs but unfortunately, + the mounts won't be in place until we start the container. This can + leave around the config if the container doesn't get deleted. + + We are using the most compose-secret-compatible approach, + which is implemented at + https://github.com/docker/compose/blob/main/pkg/compose/convergence.go#L737 + + // Prepare a tmpfs mount for our credentials so they go away after the + // container exits. We'll copy into this mount after the container is + // created. + containerCfg.HostConfig.Mounts = append(containerCfg.HostConfig.Mounts, mount.Mount{ + Type: mount.TypeTmpfs, + Target: "/docker/", + TmpfsOptions: &mount.TmpfsOptions{ + SizeBytes: 1 << 20, // only need a small partition + Mode: 0o600, + }, + }) + */ + + // Set our special little location for the config file. + containerCfg.Config.Env = append(containerCfg.Config.Env, + "DOCKER_CONFIG="+path.Dir(dockerConfigPathInContainer)) + } + var platform *specs.Platform // Engine API version 1.41 first introduced the option to specify platform on // create. It will produce an error if you try to set a platform on older API @@ -286,11 +342,30 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c if warn := localhostDNSWarning(*hostConfig); warn != "" { response.Warnings = append(response.Warnings, warn) } + + containerID = response.ID for _, w := range response.Warnings { _, _ = fmt.Fprintln(dockerCli.Err(), "WARNING:", w) } - err = containerIDFile.Write(response.ID) - return response.ID, err + err = containerIDFile.Write(containerID) + + if options.useDockerSocket { + creds, err := 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, + } + + if err := copyDockerConfigIntoContainer(ctx, dockerCli.Client(), containerID, dockerConfigPathInContainer, newConfig); err != nil { + return "", fmt.Errorf("injecting docker config.json into container failed: %w", err) + } + } + + return containerID, err } // check the DNS settings passed via --dns against localhost regexp to warn if @@ -321,3 +396,39 @@ func validatePullOpt(val string) error { ) } } + +// copyDockerConfigIntoContainer takes the client configuration and copies it +// into the container. +// +// The path should be an absolute path in the container, commonly +// /root/.docker/config.json. +func copyDockerConfigIntoContainer(ctx context.Context, dockerAPI client.APIClient, containerID string, configPath string, config *configfile.ConfigFile) error { + var configBuf bytes.Buffer + if err := config.SaveToWriter(&configBuf); err != nil { + return fmt.Errorf("saving creds: %w", err) + } + + // We don't need to get super fancy with the tar creation. + var tarBuf bytes.Buffer + tarWriter := tar.NewWriter(&tarBuf) + tarWriter.WriteHeader(&tar.Header{ + Name: configPath, + Size: int64(configBuf.Len()), + Mode: 0o600, + }) + + if _, err := io.Copy(tarWriter, &configBuf); err != nil { + return fmt.Errorf("writing config to tar file for config copy: %w", err) + } + + if err := tarWriter.Close(); err != nil { + return fmt.Errorf("closing tar for config copy failed: %w", err) + } + + if err := dockerAPI.CopyToContainer(ctx, containerID, "/", + &tarBuf, container.CopyToContainerOptions{}); err != nil { + return fmt.Errorf("copying config.json into container failed: %w", err) + } + + return nil +} diff --git a/cli/command/container/run.go b/cli/command/container/run.go index 5c541a0ddef4..b67a448a1582 100644 --- a/cli/command/container/run.go +++ b/cli/command/container/run.go @@ -60,6 +60,7 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command { flags.StringVar(&options.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container") flags.StringVar(&options.pull, "pull", PullImageMissing, `Pull image before running ("`+PullImageAlways+`", "`+PullImageMissing+`", "`+PullImageNever+`")`) flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the pull output") + flags.BoolVarP(&options.createOptions.useDockerSocket, "use-docker-socket", "", false, "Bind mount docker socket and required auth") // Add an explicit help that doesn't have a `-h` to prevent the conflict // with hostname diff --git a/docs/reference/commandline/container_create.md b/docs/reference/commandline/container_create.md index a2394eab28c2..117dc627c7e6 100644 --- a/docs/reference/commandline/container_create.md +++ b/docs/reference/commandline/container_create.md @@ -104,6 +104,7 @@ Create a new container | `--tmpfs` | `list` | | Mount a tmpfs directory | | `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY | | `--ulimit` | `ulimit` | | Ulimit options | +| `--use-docker-socket` | `bool` | | Bind mount docker socket and required auth | | `-u`, `--user` | `string` | | Username or UID (format: [:]) | | `--userns` | `string` | | User namespace to use | | `--uts` | `string` | | UTS namespace to use | diff --git a/docs/reference/commandline/container_run.md b/docs/reference/commandline/container_run.md index 4ac2ebee3a44..c07eda15ab14 100644 --- a/docs/reference/commandline/container_run.md +++ b/docs/reference/commandline/container_run.md @@ -107,6 +107,7 @@ Create and run a new container from an image | [`--tmpfs`](#tmpfs) | `list` | | Mount a tmpfs directory | | [`-t`](#tty), [`--tty`](#tty) | `bool` | | Allocate a pseudo-TTY | | [`--ulimit`](#ulimit) | `ulimit` | | Ulimit options | +| `--use-docker-socket` | `bool` | | Bind mount docker socket and required auth | | `-u`, `--user` | `string` | | Username or UID (format: [:]) | | [`--userns`](#userns) | `string` | | User namespace to use | | [`--uts`](#uts) | `string` | | UTS namespace to use | diff --git a/docs/reference/commandline/create.md b/docs/reference/commandline/create.md index 59488a5404af..a694e9024d54 100644 --- a/docs/reference/commandline/create.md +++ b/docs/reference/commandline/create.md @@ -104,6 +104,7 @@ Create a new container | `--tmpfs` | `list` | | Mount a tmpfs directory | | `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY | | `--ulimit` | `ulimit` | | Ulimit options | +| `--use-docker-socket` | `bool` | | Bind mount docker socket and required auth | | `-u`, `--user` | `string` | | Username or UID (format: [:]) | | `--userns` | `string` | | User namespace to use | | `--uts` | `string` | | UTS namespace to use | diff --git a/docs/reference/commandline/run.md b/docs/reference/commandline/run.md index f9e0fbfcf26a..ff618869e1cc 100644 --- a/docs/reference/commandline/run.md +++ b/docs/reference/commandline/run.md @@ -107,6 +107,7 @@ Create and run a new container from an image | `--tmpfs` | `list` | | Mount a tmpfs directory | | `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY | | `--ulimit` | `ulimit` | | Ulimit options | +| `--use-docker-socket` | `bool` | | Bind mount docker socket and required auth | | `-u`, `--user` | `string` | | Username or UID (format: [:]) | | `--userns` | `string` | | User namespace to use | | `--uts` | `string` | | UTS namespace to use |