11package container
22
33import (
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
3745type 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+ }
0 commit comments