mirror of
https://github.com/docker/cli.git
synced 2025-04-18 19:24:03 +03:00
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 <stephen.day@docker.com>
This commit is contained in:
parent
1adc1583a7
commit
1a502e91c9
@ -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,17 @@ 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/config/types"
|
||||
"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 +43,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
|
||||
useAPISocket bool
|
||||
}
|
||||
|
||||
// NewCreateCommand creates a new cobra.Command for `docker create`
|
||||
@ -70,6 +79,8 @@ 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.useAPISocket, "use-api-socket", "", false, "Bind mount Docker API socket and required auth")
|
||||
flags.SetAnnotation("use-api-socket", "experimentalCLI", nil) // Marks flag as experimental for now.
|
||||
|
||||
// Add an explicit help that doesn't have a `-h` to prevent the conflict
|
||||
// with hostname
|
||||
@ -179,20 +190,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 +250,73 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
return nil
|
||||
}
|
||||
|
||||
const dockerConfigPathInContainer = "/run/secrets/docker/config.json"
|
||||
var apiSocketCreds map[string]types.AuthConfig
|
||||
|
||||
if options.useAPISocket {
|
||||
// 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-api-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,
|
||||
},
|
||||
})
|
||||
*/
|
||||
|
||||
var envvarPresent bool
|
||||
for _, envvar := range containerCfg.Config.Env {
|
||||
if strings.HasPrefix(envvar, "DOCKER_CONFIG=") {
|
||||
envvarPresent = true
|
||||
}
|
||||
}
|
||||
|
||||
// If the DOCKER_CONFIG env var is already present, we assume the client knows
|
||||
// what they're doing and don't inject the creds.
|
||||
if !envvarPresent {
|
||||
// Set our special little location for the config file.
|
||||
containerCfg.Config.Env = append(containerCfg.Config.Env,
|
||||
"DOCKER_CONFIG="+path.Dir(dockerConfigPathInContainer))
|
||||
|
||||
// Resolve this here for later, ensuring we error our before we create the container.
|
||||
creds, err := dockerCli.ConfigFile().GetAllCredentials()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolving credentials failed: %w", err)
|
||||
}
|
||||
apiSocketCreds = creds // inject these after container creation.
|
||||
}
|
||||
}
|
||||
|
||||
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 +364,25 @@ 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.useAPISocket && apiSocketCreds != nil {
|
||||
// Create a new config file with just the auth.
|
||||
newConfig := &configfile.ConfigFile{
|
||||
AuthConfigs: apiSocketCreds,
|
||||
}
|
||||
|
||||
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 +413,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
|
||||
}
|
||||
|
@ -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.useAPISocket, "use-api-socket", "", false, "Bind mount Docker API socket and required auth")
|
||||
|
||||
// Add an explicit help that doesn't have a `-h` to prevent the conflict
|
||||
// with hostname
|
||||
|
@ -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-api-socket` | `bool` | | Bind mount Docker API socket and required auth |
|
||||
| `-u`, `--user` | `string` | | Username or UID (format: <name\|uid>[:<group\|gid>]) |
|
||||
| `--userns` | `string` | | User namespace to use |
|
||||
| `--uts` | `string` | | UTS namespace to use |
|
||||
|
@ -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-api-socket` | `bool` | | Bind mount Docker API socket and required auth |
|
||||
| `-u`, `--user` | `string` | | Username or UID (format: <name\|uid>[:<group\|gid>]) |
|
||||
| [`--userns`](#userns) | `string` | | User namespace to use |
|
||||
| [`--uts`](#uts) | `string` | | UTS namespace to use |
|
||||
|
@ -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-api-socket` | `bool` | | Bind mount Docker API socket and required auth |
|
||||
| `-u`, `--user` | `string` | | Username or UID (format: <name\|uid>[:<group\|gid>]) |
|
||||
| `--userns` | `string` | | User namespace to use |
|
||||
| `--uts` | `string` | | UTS namespace to use |
|
||||
|
@ -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-api-socket` | `bool` | | Bind mount Docker API socket and required auth |
|
||||
| `-u`, `--user` | `string` | | Username or UID (format: <name\|uid>[:<group\|gid>]) |
|
||||
| `--userns` | `string` | | User namespace to use |
|
||||
| `--uts` | `string` | | UTS namespace to use |
|
||||
|
Loading…
x
Reference in New Issue
Block a user