github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/container/create.go (about)

     1  package container
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"regexp"
     9  
    10  	"github.com/containerd/containerd/platforms"
    11  	"github.com/distribution/reference"
    12  	"github.com/docker/cli/cli"
    13  	"github.com/docker/cli/cli/command"
    14  	"github.com/docker/cli/cli/command/completion"
    15  	"github.com/docker/cli/cli/command/image"
    16  	"github.com/docker/cli/cli/streams"
    17  	"github.com/docker/cli/opts"
    18  	"github.com/docker/docker/api/types/container"
    19  	imagetypes "github.com/docker/docker/api/types/image"
    20  	"github.com/docker/docker/api/types/versions"
    21  	"github.com/docker/docker/errdefs"
    22  	"github.com/docker/docker/pkg/jsonmessage"
    23  	specs "github.com/opencontainers/image-spec/specs-go/v1"
    24  	"github.com/pkg/errors"
    25  	"github.com/spf13/cobra"
    26  	"github.com/spf13/pflag"
    27  )
    28  
    29  // Pull constants
    30  const (
    31  	PullImageAlways  = "always"
    32  	PullImageMissing = "missing" // Default (matches previous behavior)
    33  	PullImageNever   = "never"
    34  )
    35  
    36  type createOptions struct {
    37  	name      string
    38  	platform  string
    39  	untrusted bool
    40  	pull      string // always, missing, never
    41  	quiet     bool
    42  }
    43  
    44  // NewCreateCommand creates a new cobra.Command for `docker create`
    45  func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
    46  	var options createOptions
    47  	var copts *containerOptions
    48  
    49  	cmd := &cobra.Command{
    50  		Use:   "create [OPTIONS] IMAGE [COMMAND] [ARG...]",
    51  		Short: "Create a new container",
    52  		Args:  cli.RequiresMinArgs(1),
    53  		RunE: func(cmd *cobra.Command, args []string) error {
    54  			copts.Image = args[0]
    55  			if len(args) > 1 {
    56  				copts.Args = args[1:]
    57  			}
    58  			return runCreate(cmd.Context(), dockerCli, cmd.Flags(), &options, copts)
    59  		},
    60  		Annotations: map[string]string{
    61  			"aliases": "docker container create, docker create",
    62  		},
    63  		ValidArgsFunction: completion.ImageNames(dockerCli),
    64  	}
    65  
    66  	flags := cmd.Flags()
    67  	flags.SetInterspersed(false)
    68  
    69  	flags.StringVar(&options.name, "name", "", "Assign a name to the container")
    70  	flags.StringVar(&options.pull, "pull", PullImageMissing, `Pull image before creating ("`+PullImageAlways+`", "|`+PullImageMissing+`", "`+PullImageNever+`")`)
    71  	flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the pull output")
    72  
    73  	// Add an explicit help that doesn't have a `-h` to prevent the conflict
    74  	// with hostname
    75  	flags.Bool("help", false, "Print usage")
    76  
    77  	command.AddPlatformFlag(flags, &options.platform)
    78  	command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled())
    79  	copts = addFlags(flags)
    80  	return cmd
    81  }
    82  
    83  func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, options *createOptions, copts *containerOptions) error {
    84  	if err := validatePullOpt(options.pull); err != nil {
    85  		reportError(dockerCli.Err(), "create", err.Error(), true)
    86  		return cli.StatusError{StatusCode: 125}
    87  	}
    88  	proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetAll()))
    89  	newEnv := []string{}
    90  	for k, v := range proxyConfig {
    91  		if v == nil {
    92  			newEnv = append(newEnv, k)
    93  		} else {
    94  			newEnv = append(newEnv, k+"="+*v)
    95  		}
    96  	}
    97  	copts.env = *opts.NewListOptsRef(&newEnv, nil)
    98  	containerCfg, err := parse(flags, copts, dockerCli.ServerInfo().OSType)
    99  	if err != nil {
   100  		reportError(dockerCli.Err(), "create", err.Error(), true)
   101  		return cli.StatusError{StatusCode: 125}
   102  	}
   103  	if err = validateAPIVersion(containerCfg, dockerCli.Client().ClientVersion()); err != nil {
   104  		reportError(dockerCli.Err(), "create", err.Error(), true)
   105  		return cli.StatusError{StatusCode: 125}
   106  	}
   107  	id, err := createContainer(ctx, dockerCli, containerCfg, options)
   108  	if err != nil {
   109  		return err
   110  	}
   111  	_, _ = fmt.Fprintln(dockerCli.Out(), id)
   112  	return nil
   113  }
   114  
   115  // FIXME(thaJeztah): this is the only code-path that uses APIClient.ImageCreate. Rewrite this to use the regular "pull" code (or vice-versa).
   116  func pullImage(ctx context.Context, dockerCli command.Cli, img string, options *createOptions) error {
   117  	encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCli.ConfigFile(), img)
   118  	if err != nil {
   119  		return err
   120  	}
   121  
   122  	responseBody, err := dockerCli.Client().ImageCreate(ctx, img, imagetypes.CreateOptions{
   123  		RegistryAuth: encodedAuth,
   124  		Platform:     options.platform,
   125  	})
   126  	if err != nil {
   127  		return err
   128  	}
   129  	defer responseBody.Close()
   130  
   131  	out := dockerCli.Err()
   132  	if options.quiet {
   133  		out = io.Discard
   134  	}
   135  	return jsonmessage.DisplayJSONMessagesToStream(responseBody, streams.NewOut(out), nil)
   136  }
   137  
   138  type cidFile struct {
   139  	path    string
   140  	file    *os.File
   141  	written bool
   142  }
   143  
   144  func (cid *cidFile) Close() error {
   145  	if cid.file == nil {
   146  		return nil
   147  	}
   148  	cid.file.Close()
   149  
   150  	if cid.written {
   151  		return nil
   152  	}
   153  	if err := os.Remove(cid.path); err != nil {
   154  		return errors.Wrapf(err, "failed to remove the CID file '%s'", cid.path)
   155  	}
   156  
   157  	return nil
   158  }
   159  
   160  func (cid *cidFile) Write(id string) error {
   161  	if cid.file == nil {
   162  		return nil
   163  	}
   164  	if _, err := cid.file.Write([]byte(id)); err != nil {
   165  		return errors.Wrap(err, "failed to write the container ID to the file")
   166  	}
   167  	cid.written = true
   168  	return nil
   169  }
   170  
   171  func newCIDFile(path string) (*cidFile, error) {
   172  	if path == "" {
   173  		return &cidFile{}, nil
   174  	}
   175  	if _, err := os.Stat(path); err == nil {
   176  		return nil, errors.Errorf("container ID file found, make sure the other container isn't running or delete %s", path)
   177  	}
   178  
   179  	f, err := os.Create(path)
   180  	if err != nil {
   181  		return nil, errors.Wrap(err, "failed to create the container ID file")
   182  	}
   183  
   184  	return &cidFile{path: path, file: f}, nil
   185  }
   186  
   187  //nolint:gocyclo
   188  func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *containerConfig, options *createOptions) (containerID string, err error) {
   189  	config := containerCfg.Config
   190  	hostConfig := containerCfg.HostConfig
   191  	networkingConfig := containerCfg.NetworkingConfig
   192  
   193  	warnOnOomKillDisable(*hostConfig, dockerCli.Err())
   194  	warnOnLocalhostDNS(*hostConfig, dockerCli.Err())
   195  
   196  	var (
   197  		trustedRef reference.Canonical
   198  		namedRef   reference.Named
   199  	)
   200  
   201  	containerIDFile, err := newCIDFile(hostConfig.ContainerIDFile)
   202  	if err != nil {
   203  		return "", err
   204  	}
   205  	defer containerIDFile.Close()
   206  
   207  	ref, err := reference.ParseAnyReference(config.Image)
   208  	if err != nil {
   209  		return "", err
   210  	}
   211  	if named, ok := ref.(reference.Named); ok {
   212  		namedRef = reference.TagNameOnly(named)
   213  
   214  		if taggedRef, ok := namedRef.(reference.NamedTagged); ok && !options.untrusted {
   215  			var err error
   216  			trustedRef, err = image.TrustedReference(ctx, dockerCli, taggedRef)
   217  			if err != nil {
   218  				return "", err
   219  			}
   220  			config.Image = reference.FamiliarString(trustedRef)
   221  		}
   222  	}
   223  
   224  	pullAndTagImage := func() error {
   225  		if err := pullImage(ctx, dockerCli, config.Image, options); err != nil {
   226  			return err
   227  		}
   228  		if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
   229  			return image.TagTrusted(ctx, dockerCli, trustedRef, taggedRef)
   230  		}
   231  		return nil
   232  	}
   233  
   234  	var platform *specs.Platform
   235  	// Engine API version 1.41 first introduced the option to specify platform on
   236  	// create. It will produce an error if you try to set a platform on older API
   237  	// versions, so check the API version here to maintain backwards
   238  	// compatibility for CLI users.
   239  	if options.platform != "" && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.41") {
   240  		p, err := platforms.Parse(options.platform)
   241  		if err != nil {
   242  			return "", errors.Wrap(err, "error parsing specified platform")
   243  		}
   244  		platform = &p
   245  	}
   246  
   247  	if options.pull == PullImageAlways {
   248  		if err := pullAndTagImage(); err != nil {
   249  			return "", err
   250  		}
   251  	}
   252  
   253  	hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.Out().GetTtySize()
   254  
   255  	response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name)
   256  	if err != nil {
   257  		// Pull image if it does not exist locally and we have the PullImageMissing option. Default behavior.
   258  		if errdefs.IsNotFound(err) && namedRef != nil && options.pull == PullImageMissing {
   259  			if !options.quiet {
   260  				// we don't want to write to stdout anything apart from container.ID
   261  				fmt.Fprintf(dockerCli.Err(), "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
   262  			}
   263  
   264  			if err := pullAndTagImage(); err != nil {
   265  				return "", err
   266  			}
   267  
   268  			var retryErr error
   269  			response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name)
   270  			if retryErr != nil {
   271  				return "", retryErr
   272  			}
   273  		} else {
   274  			return "", err
   275  		}
   276  	}
   277  
   278  	for _, w := range response.Warnings {
   279  		_, _ = fmt.Fprintf(dockerCli.Err(), "WARNING: %s\n", w)
   280  	}
   281  	err = containerIDFile.Write(response.ID)
   282  	return response.ID, err
   283  }
   284  
   285  func warnOnOomKillDisable(hostConfig container.HostConfig, stderr io.Writer) {
   286  	if hostConfig.OomKillDisable != nil && *hostConfig.OomKillDisable && hostConfig.Memory == 0 {
   287  		fmt.Fprintln(stderr, "WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.")
   288  	}
   289  }
   290  
   291  // check the DNS settings passed via --dns against localhost regexp to warn if
   292  // they are trying to set a DNS to a localhost address
   293  func warnOnLocalhostDNS(hostConfig container.HostConfig, stderr io.Writer) {
   294  	for _, dnsIP := range hostConfig.DNS {
   295  		if isLocalhost(dnsIP) {
   296  			fmt.Fprintf(stderr, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP)
   297  			return
   298  		}
   299  	}
   300  }
   301  
   302  // IPLocalhost is a regex pattern for IPv4 or IPv6 loopback range.
   303  const ipLocalhost = `((127\.([0-9]{1,3}\.){2}[0-9]{1,3})|(::1)$)`
   304  
   305  var localhostIPRegexp = regexp.MustCompile(ipLocalhost)
   306  
   307  // IsLocalhost returns true if ip matches the localhost IP regular expression.
   308  // Used for determining if nameserver settings are being passed which are
   309  // localhost addresses
   310  func isLocalhost(ip string) bool {
   311  	return localhostIPRegexp.MatchString(ip)
   312  }
   313  
   314  func validatePullOpt(val string) error {
   315  	switch val {
   316  	case PullImageAlways, PullImageMissing, PullImageNever, "":
   317  		// valid option, but nothing to do yet
   318  		return nil
   319  	default:
   320  		return fmt.Errorf(
   321  			"invalid pull option: '%s': must be one of %q, %q or %q",
   322  			val,
   323  			PullImageAlways,
   324  			PullImageMissing,
   325  			PullImageNever,
   326  		)
   327  	}
   328  }