github.com/cspotcode/docker-cli@v20.10.0-rc1.0.20201201121459-3faad7acc5b8+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/docker/cli/cli"
    12  	"github.com/docker/cli/cli/command"
    13  	"github.com/docker/cli/cli/command/image"
    14  	"github.com/docker/cli/opts"
    15  	"github.com/docker/distribution/reference"
    16  	"github.com/docker/docker/api/types"
    17  	"github.com/docker/docker/api/types/container"
    18  	"github.com/docker/docker/api/types/versions"
    19  	apiclient "github.com/docker/docker/client"
    20  	"github.com/docker/docker/pkg/jsonmessage"
    21  	"github.com/docker/docker/registry"
    22  	specs "github.com/opencontainers/image-spec/specs-go/v1"
    23  	"github.com/pkg/errors"
    24  	"github.com/spf13/cobra"
    25  	"github.com/spf13/pflag"
    26  )
    27  
    28  // Pull constants
    29  const (
    30  	PullImageAlways  = "always"
    31  	PullImageMissing = "missing" // Default (matches previous behavior)
    32  	PullImageNever   = "never"
    33  )
    34  
    35  type createOptions struct {
    36  	name      string
    37  	platform  string
    38  	untrusted bool
    39  	pull      string // alway, missing, never
    40  }
    41  
    42  // NewCreateCommand creates a new cobra.Command for `docker create`
    43  func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
    44  	var opts createOptions
    45  	var copts *containerOptions
    46  
    47  	cmd := &cobra.Command{
    48  		Use:   "create [OPTIONS] IMAGE [COMMAND] [ARG...]",
    49  		Short: "Create a new container",
    50  		Args:  cli.RequiresMinArgs(1),
    51  		RunE: func(cmd *cobra.Command, args []string) error {
    52  			copts.Image = args[0]
    53  			if len(args) > 1 {
    54  				copts.Args = args[1:]
    55  			}
    56  			return runCreate(dockerCli, cmd.Flags(), &opts, copts)
    57  		},
    58  	}
    59  
    60  	flags := cmd.Flags()
    61  	flags.SetInterspersed(false)
    62  
    63  	flags.StringVar(&opts.name, "name", "", "Assign a name to the container")
    64  	flags.StringVar(&opts.pull, "pull", PullImageMissing,
    65  		`Pull image before creating ("`+PullImageAlways+`"|"`+PullImageMissing+`"|"`+PullImageNever+`")`)
    66  
    67  	// Add an explicit help that doesn't have a `-h` to prevent the conflict
    68  	// with hostname
    69  	flags.Bool("help", false, "Print usage")
    70  
    71  	command.AddPlatformFlag(flags, &opts.platform)
    72  	command.AddTrustVerificationFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled())
    73  	copts = addFlags(flags)
    74  	return cmd
    75  }
    76  
    77  func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, options *createOptions, copts *containerOptions) error {
    78  	proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetAll()))
    79  	newEnv := []string{}
    80  	for k, v := range proxyConfig {
    81  		if v == nil {
    82  			newEnv = append(newEnv, k)
    83  		} else {
    84  			newEnv = append(newEnv, fmt.Sprintf("%s=%s", k, *v))
    85  		}
    86  	}
    87  	copts.env = *opts.NewListOptsRef(&newEnv, nil)
    88  	containerConfig, err := parse(flags, copts, dockerCli.ServerInfo().OSType)
    89  	if err != nil {
    90  		reportError(dockerCli.Err(), "create", err.Error(), true)
    91  		return cli.StatusError{StatusCode: 125}
    92  	}
    93  	if err = validateAPIVersion(containerConfig, dockerCli.Client().ClientVersion()); err != nil {
    94  		reportError(dockerCli.Err(), "create", err.Error(), true)
    95  		return cli.StatusError{StatusCode: 125}
    96  	}
    97  	response, err := createContainer(context.Background(), dockerCli, containerConfig, options)
    98  	if err != nil {
    99  		return err
   100  	}
   101  	fmt.Fprintln(dockerCli.Out(), response.ID)
   102  	return nil
   103  }
   104  
   105  func pullImage(ctx context.Context, dockerCli command.Cli, image string, platform string, out io.Writer) error {
   106  	ref, err := reference.ParseNormalizedNamed(image)
   107  	if err != nil {
   108  		return err
   109  	}
   110  
   111  	// Resolve the Repository name from fqn to RepositoryInfo
   112  	repoInfo, err := registry.ParseRepositoryInfo(ref)
   113  	if err != nil {
   114  		return err
   115  	}
   116  
   117  	authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index)
   118  	encodedAuth, err := command.EncodeAuthToBase64(authConfig)
   119  	if err != nil {
   120  		return err
   121  	}
   122  
   123  	options := types.ImageCreateOptions{
   124  		RegistryAuth: encodedAuth,
   125  		Platform:     platform,
   126  	}
   127  
   128  	responseBody, err := dockerCli.Client().ImageCreate(ctx, image, options)
   129  	if err != nil {
   130  		return err
   131  	}
   132  	defer responseBody.Close()
   133  
   134  	return jsonmessage.DisplayJSONMessagesStream(
   135  		responseBody,
   136  		out,
   137  		dockerCli.Out().FD(),
   138  		dockerCli.Out().IsTerminal(),
   139  		nil)
   140  }
   141  
   142  type cidFile struct {
   143  	path    string
   144  	file    *os.File
   145  	written bool
   146  }
   147  
   148  func (cid *cidFile) Close() error {
   149  	if cid.file == nil {
   150  		return nil
   151  	}
   152  	cid.file.Close()
   153  
   154  	if cid.written {
   155  		return nil
   156  	}
   157  	if err := os.Remove(cid.path); err != nil {
   158  		return errors.Errorf("failed to remove the CID file '%s': %s \n", cid.path, err)
   159  	}
   160  
   161  	return nil
   162  }
   163  
   164  func (cid *cidFile) Write(id string) error {
   165  	if cid.file == nil {
   166  		return nil
   167  	}
   168  	if _, err := cid.file.Write([]byte(id)); err != nil {
   169  		return errors.Errorf("Failed to write the container ID to the file: %s", err)
   170  	}
   171  	cid.written = true
   172  	return nil
   173  }
   174  
   175  func newCIDFile(path string) (*cidFile, error) {
   176  	if path == "" {
   177  		return &cidFile{}, nil
   178  	}
   179  	if _, err := os.Stat(path); err == nil {
   180  		return nil, errors.Errorf("Container ID file found, make sure the other container isn't running or delete %s", path)
   181  	}
   182  
   183  	f, err := os.Create(path)
   184  	if err != nil {
   185  		return nil, errors.Errorf("Failed to create the container ID file: %s", err)
   186  	}
   187  
   188  	return &cidFile{path: path, file: f}, nil
   189  }
   190  
   191  // nolint: gocyclo
   192  func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig *containerConfig, opts *createOptions) (*container.ContainerCreateCreatedBody, error) {
   193  	config := containerConfig.Config
   194  	hostConfig := containerConfig.HostConfig
   195  	networkingConfig := containerConfig.NetworkingConfig
   196  	stderr := dockerCli.Err()
   197  
   198  	warnOnOomKillDisable(*hostConfig, stderr)
   199  	warnOnLocalhostDNS(*hostConfig, stderr)
   200  
   201  	var (
   202  		trustedRef reference.Canonical
   203  		namedRef   reference.Named
   204  	)
   205  
   206  	containerIDFile, err := newCIDFile(hostConfig.ContainerIDFile)
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  	defer containerIDFile.Close()
   211  
   212  	ref, err := reference.ParseAnyReference(config.Image)
   213  	if err != nil {
   214  		return nil, err
   215  	}
   216  	if named, ok := ref.(reference.Named); ok {
   217  		namedRef = reference.TagNameOnly(named)
   218  
   219  		if taggedRef, ok := namedRef.(reference.NamedTagged); ok && !opts.untrusted {
   220  			var err error
   221  			trustedRef, err = image.TrustedReference(ctx, dockerCli, taggedRef, nil)
   222  			if err != nil {
   223  				return nil, err
   224  			}
   225  			config.Image = reference.FamiliarString(trustedRef)
   226  		}
   227  	}
   228  
   229  	pullAndTagImage := func() error {
   230  		if err := pullImage(ctx, dockerCli, config.Image, opts.platform, stderr); err != nil {
   231  			return err
   232  		}
   233  		if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
   234  			return image.TagTrusted(ctx, dockerCli, trustedRef, taggedRef)
   235  		}
   236  		return nil
   237  	}
   238  
   239  	var platform *specs.Platform
   240  	// Engine API version 1.41 first introduced the option to specify platform on
   241  	// create. It will produce an error if you try to set a platform on older API
   242  	// versions, so check the API version here to maintain backwards
   243  	// compatibility for CLI users.
   244  	if opts.platform != "" && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.41") {
   245  		p, err := platforms.Parse(opts.platform)
   246  		if err != nil {
   247  			return nil, errors.Wrap(err, "error parsing specified platform")
   248  		}
   249  		platform = &p
   250  	}
   251  
   252  	if opts.pull == PullImageAlways {
   253  		if err := pullAndTagImage(); err != nil {
   254  			return nil, err
   255  		}
   256  	}
   257  
   258  	response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, opts.name)
   259  	if err != nil {
   260  		// Pull image if it does not exist locally and we have the PullImageMissing option. Default behavior.
   261  		if apiclient.IsErrNotFound(err) && namedRef != nil && opts.pull == PullImageMissing {
   262  			// we don't want to write to stdout anything apart from container.ID
   263  			fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
   264  			if err := pullAndTagImage(); err != nil {
   265  				return nil, err
   266  			}
   267  
   268  			var retryErr error
   269  			response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, opts.name)
   270  			if retryErr != nil {
   271  				return nil, retryErr
   272  			}
   273  		} else {
   274  			return nil, err
   275  		}
   276  	}
   277  
   278  	for _, warning := range response.Warnings {
   279  		fmt.Fprintf(stderr, "WARNING: %s\n", warning)
   280  	}
   281  	err = containerIDFile.Write(response.ID)
   282  	return &response, 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  }