create.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. package container
  2. import (
  3. "archive/tar"
  4. "bytes"
  5. "context"
  6. "fmt"
  7. "io"
  8. "net/netip"
  9. "os"
  10. "path"
  11. "strings"
  12. "github.com/containerd/platforms"
  13. "github.com/distribution/reference"
  14. "github.com/docker/cli/cli"
  15. "github.com/docker/cli/cli/command"
  16. "github.com/docker/cli/cli/command/completion"
  17. "github.com/docker/cli/cli/command/image"
  18. "github.com/docker/cli/cli/config/configfile"
  19. "github.com/docker/cli/cli/config/types"
  20. "github.com/docker/cli/cli/internal/jsonstream"
  21. "github.com/docker/cli/cli/streams"
  22. "github.com/docker/cli/cli/trust"
  23. "github.com/docker/cli/opts"
  24. "github.com/docker/docker/api/types/container"
  25. imagetypes "github.com/docker/docker/api/types/image"
  26. "github.com/docker/docker/api/types/mount"
  27. "github.com/docker/docker/api/types/versions"
  28. "github.com/docker/docker/client"
  29. "github.com/docker/docker/errdefs"
  30. specs "github.com/opencontainers/image-spec/specs-go/v1"
  31. "github.com/pkg/errors"
  32. "github.com/spf13/cobra"
  33. "github.com/spf13/pflag"
  34. )
  35. // Pull constants
  36. const (
  37. PullImageAlways = "always"
  38. PullImageMissing = "missing" // Default (matches previous behavior)
  39. PullImageNever = "never"
  40. )
  41. type createOptions struct {
  42. name string
  43. platform string
  44. untrusted bool
  45. pull string // always, missing, never
  46. quiet bool
  47. useAPISocket bool
  48. }
  49. // NewCreateCommand creates a new cobra.Command for `docker create`
  50. func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
  51. var options createOptions
  52. var copts *containerOptions
  53. cmd := &cobra.Command{
  54. Use: "create [OPTIONS] IMAGE [COMMAND] [ARG...]",
  55. Short: "Create a new container",
  56. Args: cli.RequiresMinArgs(1),
  57. RunE: func(cmd *cobra.Command, args []string) error {
  58. copts.Image = args[0]
  59. if len(args) > 1 {
  60. copts.Args = args[1:]
  61. }
  62. return runCreate(cmd.Context(), dockerCli, cmd.Flags(), &options, copts)
  63. },
  64. Annotations: map[string]string{
  65. "aliases": "docker container create, docker create",
  66. },
  67. ValidArgsFunction: completion.ImageNames(dockerCli, -1),
  68. }
  69. flags := cmd.Flags()
  70. flags.SetInterspersed(false)
  71. flags.StringVar(&options.name, "name", "", "Assign a name to the container")
  72. flags.StringVar(&options.pull, "pull", PullImageMissing, `Pull image before creating ("`+PullImageAlways+`", "|`+PullImageMissing+`", "`+PullImageNever+`")`)
  73. flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the pull output")
  74. flags.BoolVarP(&options.useAPISocket, "use-api-socket", "", false, "Bind mount Docker API socket and required auth")
  75. flags.SetAnnotation("use-api-socket", "experimentalCLI", nil) // Marks flag as experimental for now.
  76. // Add an explicit help that doesn't have a `-h` to prevent the conflict
  77. // with hostname
  78. flags.Bool("help", false, "Print usage")
  79. command.AddPlatformFlag(flags, &options.platform)
  80. command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled())
  81. copts = addFlags(flags)
  82. addCompletions(cmd, dockerCli)
  83. flags.VisitAll(func(flag *pflag.Flag) {
  84. // Set a default completion function if none was set. We don't look
  85. // up if it does already have one set, because Cobra does this for
  86. // us, and returns an error (which we ignore for this reason).
  87. _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete)
  88. })
  89. return cmd
  90. }
  91. func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, options *createOptions, copts *containerOptions) error {
  92. if err := validatePullOpt(options.pull); err != nil {
  93. return cli.StatusError{
  94. Status: withHelp(err, "create").Error(),
  95. StatusCode: 125,
  96. }
  97. }
  98. proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetAll()))
  99. newEnv := []string{}
  100. for k, v := range proxyConfig {
  101. if v == nil {
  102. newEnv = append(newEnv, k)
  103. } else {
  104. newEnv = append(newEnv, k+"="+*v)
  105. }
  106. }
  107. copts.env = *opts.NewListOptsRef(&newEnv, nil)
  108. containerCfg, err := parse(flags, copts, dockerCli.ServerInfo().OSType)
  109. if err != nil {
  110. return cli.StatusError{
  111. Status: withHelp(err, "create").Error(),
  112. StatusCode: 125,
  113. }
  114. }
  115. id, err := createContainer(ctx, dockerCli, containerCfg, options)
  116. if err != nil {
  117. return err
  118. }
  119. _, _ = fmt.Fprintln(dockerCli.Out(), id)
  120. return nil
  121. }
  122. // FIXME(thaJeztah): this is the only code-path that uses APIClient.ImageCreate. Rewrite this to use the regular "pull" code (or vice-versa).
  123. func pullImage(ctx context.Context, dockerCli command.Cli, img string, options *createOptions) error {
  124. encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCli.ConfigFile(), img)
  125. if err != nil {
  126. return err
  127. }
  128. responseBody, err := dockerCli.Client().ImageCreate(ctx, img, imagetypes.CreateOptions{
  129. RegistryAuth: encodedAuth,
  130. Platform: options.platform,
  131. })
  132. if err != nil {
  133. return err
  134. }
  135. defer responseBody.Close()
  136. out := dockerCli.Err()
  137. if options.quiet {
  138. out = streams.NewOut(io.Discard)
  139. }
  140. return jsonstream.Display(ctx, responseBody, out)
  141. }
  142. type cidFile struct {
  143. path string
  144. file *os.File
  145. written bool
  146. }
  147. func (cid *cidFile) Close() error {
  148. if cid.file == nil {
  149. return nil
  150. }
  151. cid.file.Close()
  152. if cid.written {
  153. return nil
  154. }
  155. if err := os.Remove(cid.path); err != nil {
  156. return errors.Wrapf(err, "failed to remove the CID file '%s'", cid.path)
  157. }
  158. return nil
  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. func newCIDFile(cidPath string) (*cidFile, error) {
  171. if cidPath == "" {
  172. return &cidFile{}, nil
  173. }
  174. if _, err := os.Stat(cidPath); err == nil {
  175. return nil, errors.Errorf("container ID file found, make sure the other container isn't running or delete %s", cidPath)
  176. }
  177. f, err := os.Create(cidPath)
  178. if err != nil {
  179. return nil, errors.Wrap(err, "failed to create the container ID file")
  180. }
  181. return &cidFile{path: cidPath, file: f}, nil
  182. }
  183. //nolint:gocyclo
  184. func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *containerConfig, options *createOptions) (containerID string, err error) {
  185. config := containerCfg.Config
  186. hostConfig := containerCfg.HostConfig
  187. networkingConfig := containerCfg.NetworkingConfig
  188. var (
  189. trustedRef reference.Canonical
  190. namedRef reference.Named
  191. )
  192. containerIDFile, err := newCIDFile(hostConfig.ContainerIDFile)
  193. if err != nil {
  194. return "", err
  195. }
  196. defer containerIDFile.Close()
  197. ref, err := reference.ParseAnyReference(config.Image)
  198. if err != nil {
  199. return "", err
  200. }
  201. if named, ok := ref.(reference.Named); ok {
  202. namedRef = reference.TagNameOnly(named)
  203. if taggedRef, ok := namedRef.(reference.NamedTagged); ok && !options.untrusted {
  204. var err error
  205. trustedRef, err = image.TrustedReference(ctx, dockerCli, taggedRef)
  206. if err != nil {
  207. return "", err
  208. }
  209. config.Image = reference.FamiliarString(trustedRef)
  210. }
  211. }
  212. pullAndTagImage := func() error {
  213. if err := pullImage(ctx, dockerCli, config.Image, options); err != nil {
  214. return err
  215. }
  216. if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
  217. return trust.TagTrusted(ctx, dockerCli.Client(), dockerCli.Err(), trustedRef, taggedRef)
  218. }
  219. return nil
  220. }
  221. const dockerConfigPathInContainer = "/run/secrets/docker/config.json"
  222. var apiSocketCreds map[string]types.AuthConfig
  223. if options.useAPISocket {
  224. // We'll create two new mounts to handle this flag:
  225. // 1. Mount the actual docker socket.
  226. // 2. A synthezised ~/.docker/config.json with resolved tokens.
  227. socket := dockerCli.DockerEndpoint().Host
  228. if !strings.HasPrefix(socket, "unix://") {
  229. return "", fmt.Errorf("flag --use-api-socket can only be used with unix sockets: docker endpoint %s incompatible", socket)
  230. }
  231. socket = strings.TrimPrefix(socket, "unix://") // should we confirm absolute path?
  232. containerCfg.HostConfig.Mounts = append(containerCfg.HostConfig.Mounts, mount.Mount{
  233. Type: mount.TypeBind,
  234. Source: socket,
  235. Target: "/var/run/docker.sock",
  236. BindOptions: &mount.BindOptions{},
  237. })
  238. /*
  239. Ideally, we'd like to copy the config into a tmpfs but unfortunately,
  240. the mounts won't be in place until we start the container. This can
  241. leave around the config if the container doesn't get deleted.
  242. We are using the most compose-secret-compatible approach,
  243. which is implemented at
  244. https://github.com/docker/compose/blob/main/pkg/compose/convergence.go#L737
  245. // Prepare a tmpfs mount for our credentials so they go away after the
  246. // container exits. We'll copy into this mount after the container is
  247. // created.
  248. containerCfg.HostConfig.Mounts = append(containerCfg.HostConfig.Mounts, mount.Mount{
  249. Type: mount.TypeTmpfs,
  250. Target: "/docker/",
  251. TmpfsOptions: &mount.TmpfsOptions{
  252. SizeBytes: 1 << 20, // only need a small partition
  253. Mode: 0o600,
  254. },
  255. })
  256. */
  257. var envvarPresent bool
  258. for _, envvar := range containerCfg.Config.Env {
  259. if strings.HasPrefix(envvar, "DOCKER_CONFIG=") {
  260. envvarPresent = true
  261. }
  262. }
  263. // If the DOCKER_CONFIG env var is already present, we assume the client knows
  264. // what they're doing and don't inject the creds.
  265. if !envvarPresent {
  266. // Resolve this here for later, ensuring we error our before we create the container.
  267. creds, err := dockerCli.ConfigFile().GetAllCredentials()
  268. if err != nil {
  269. return "", fmt.Errorf("resolving credentials failed: %w", err)
  270. }
  271. if len(creds) > 0 {
  272. // Set our special little location for the config file.
  273. containerCfg.Config.Env = append(containerCfg.Config.Env, "DOCKER_CONFIG="+path.Dir(dockerConfigPathInContainer))
  274. apiSocketCreds = creds // inject these after container creation.
  275. }
  276. }
  277. }
  278. var platform *specs.Platform
  279. // Engine API version 1.41 first introduced the option to specify platform on
  280. // create. It will produce an error if you try to set a platform on older API
  281. // versions, so check the API version here to maintain backwards
  282. // compatibility for CLI users.
  283. if options.platform != "" && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.41") {
  284. p, err := platforms.Parse(options.platform)
  285. if err != nil {
  286. return "", errors.Wrap(errdefs.InvalidParameter(err), "error parsing specified platform")
  287. }
  288. platform = &p
  289. }
  290. if options.pull == PullImageAlways {
  291. if err := pullAndTagImage(); err != nil {
  292. return "", err
  293. }
  294. }
  295. hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.Out().GetTtySize()
  296. response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name)
  297. if err != nil {
  298. // Pull image if it does not exist locally and we have the PullImageMissing option. Default behavior.
  299. if errdefs.IsNotFound(err) && namedRef != nil && options.pull == PullImageMissing {
  300. if !options.quiet {
  301. // we don't want to write to stdout anything apart from container.ID
  302. _, _ = fmt.Fprintf(dockerCli.Err(), "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
  303. }
  304. if err := pullAndTagImage(); err != nil {
  305. return "", err
  306. }
  307. var retryErr error
  308. response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name)
  309. if retryErr != nil {
  310. return "", retryErr
  311. }
  312. } else {
  313. return "", err
  314. }
  315. }
  316. if warn := localhostDNSWarning(*hostConfig); warn != "" {
  317. response.Warnings = append(response.Warnings, warn)
  318. }
  319. containerID = response.ID
  320. for _, w := range response.Warnings {
  321. _, _ = fmt.Fprintln(dockerCli.Err(), "WARNING:", w)
  322. }
  323. err = containerIDFile.Write(containerID)
  324. if options.useAPISocket && len(apiSocketCreds) > 0 {
  325. // Create a new config file with just the auth.
  326. newConfig := &configfile.ConfigFile{
  327. AuthConfigs: apiSocketCreds,
  328. }
  329. if err := copyDockerConfigIntoContainer(ctx, dockerCli.Client(), containerID, dockerConfigPathInContainer, newConfig); err != nil {
  330. return "", fmt.Errorf("injecting docker config.json into container failed: %w", err)
  331. }
  332. }
  333. return containerID, err
  334. }
  335. // check the DNS settings passed via --dns against localhost regexp to warn if
  336. // they are trying to set a DNS to a localhost address.
  337. //
  338. // TODO(thaJeztah): move this to the daemon, which can make a better call if it will work or not (depending on networking mode).
  339. func localhostDNSWarning(hostConfig container.HostConfig) string {
  340. for _, dnsIP := range hostConfig.DNS {
  341. if addr, err := netip.ParseAddr(dnsIP); err == nil && addr.IsLoopback() {
  342. return fmt.Sprintf("Localhost DNS (%s) may fail in containers.", addr)
  343. }
  344. }
  345. return ""
  346. }
  347. func validatePullOpt(val string) error {
  348. switch val {
  349. case PullImageAlways, PullImageMissing, PullImageNever, "":
  350. // valid option, but nothing to do yet
  351. return nil
  352. default:
  353. return fmt.Errorf(
  354. "invalid pull option: '%s': must be one of %q, %q or %q",
  355. val,
  356. PullImageAlways,
  357. PullImageMissing,
  358. PullImageNever,
  359. )
  360. }
  361. }
  362. // copyDockerConfigIntoContainer takes the client configuration and copies it
  363. // into the container.
  364. //
  365. // The path should be an absolute path in the container, commonly
  366. // /root/.docker/config.json.
  367. func copyDockerConfigIntoContainer(ctx context.Context, dockerAPI client.APIClient, containerID string, configPath string, config *configfile.ConfigFile) error {
  368. var configBuf bytes.Buffer
  369. if err := config.SaveToWriter(&configBuf); err != nil {
  370. return fmt.Errorf("saving creds: %w", err)
  371. }
  372. // We don't need to get super fancy with the tar creation.
  373. var tarBuf bytes.Buffer
  374. tarWriter := tar.NewWriter(&tarBuf)
  375. tarWriter.WriteHeader(&tar.Header{
  376. Name: configPath,
  377. Size: int64(configBuf.Len()),
  378. Mode: 0o600,
  379. })
  380. if _, err := io.Copy(tarWriter, &configBuf); err != nil {
  381. return fmt.Errorf("writing config to tar file for config copy: %w", err)
  382. }
  383. if err := tarWriter.Close(); err != nil {
  384. return fmt.Errorf("closing tar for config copy failed: %w", err)
  385. }
  386. if err := dockerAPI.CopyToContainer(ctx, containerID, "/",
  387. &tarBuf, container.CopyToContainerOptions{}); err != nil {
  388. return fmt.Errorf("copying config.json into container failed: %w", err)
  389. }
  390. return nil
  391. }