1/*
2   Copyright The containerd Authors.
3
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7
8       http://www.apache.org/licenses/LICENSE-2.0
9
10   Unless required by applicable law or agreed to in writing, software
11   distributed under the License is distributed on an "AS IS" BASIS,
12   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   See the License for the specific language governing permissions and
14   limitations under the License.
15*/
16
17package images
18
19import (
20	"fmt"
21	"time"
22
23	"github.com/containerd/containerd"
24	"github.com/containerd/containerd/cmd/ctr/commands"
25	"github.com/containerd/containerd/cmd/ctr/commands/content"
26	"github.com/containerd/containerd/images"
27	"github.com/containerd/containerd/log"
28	"github.com/containerd/containerd/platforms"
29	"github.com/opencontainers/image-spec/identity"
30	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
31	"github.com/pkg/errors"
32	"github.com/urfave/cli"
33)
34
35var pullCommand = cli.Command{
36	Name:      "pull",
37	Usage:     "pull an image from a remote",
38	ArgsUsage: "[flags] <ref>",
39	Description: `Fetch and prepare an image for use in containerd.
40
41After pulling an image, it should be ready to use the same reference in a run
42command. As part of this process, we do the following:
43
441. Fetch all resources into containerd.
452. Prepare the snapshot filesystem with the pulled resources.
463. Register metadata for the image.
47`,
48	Flags: append(append(commands.RegistryFlags, append(commands.SnapshotterFlags, commands.LabelFlag)...),
49		cli.StringSliceFlag{
50			Name:  "platform",
51			Usage: "Pull content from a specific platform",
52			Value: &cli.StringSlice{},
53		},
54		cli.BoolFlag{
55			Name:  "all-platforms",
56			Usage: "pull content and metadata from all platforms",
57		},
58		cli.BoolFlag{
59			Name:  "all-metadata",
60			Usage: "Pull metadata for all platforms",
61		},
62		cli.BoolFlag{
63			Name:  "print-chainid",
64			Usage: "Print the resulting image's chain ID",
65		},
66		cli.IntFlag{
67			Name:  "max-concurrent-downloads",
68			Usage: "Set the max concurrent downloads for each pull",
69		},
70	),
71	Action: func(context *cli.Context) error {
72		var (
73			ref = context.Args().First()
74		)
75		if ref == "" {
76			return fmt.Errorf("please provide an image reference to pull")
77		}
78
79		client, ctx, cancel, err := commands.NewClient(context)
80		if err != nil {
81			return err
82		}
83		defer cancel()
84
85		ctx, done, err := client.WithLease(ctx)
86		if err != nil {
87			return err
88		}
89		defer done(ctx)
90
91		config, err := content.NewFetchConfig(ctx, context)
92		if err != nil {
93			return err
94		}
95
96		img, err := content.Fetch(ctx, client, ref, config)
97		if err != nil {
98			return err
99		}
100
101		log.G(ctx).WithField("image", ref).Debug("unpacking")
102
103		// TODO: Show unpack status
104
105		var p []ocispec.Platform
106		if context.Bool("all-platforms") {
107			p, err = images.Platforms(ctx, client.ContentStore(), img.Target)
108			if err != nil {
109				return errors.Wrap(err, "unable to resolve image platforms")
110			}
111		} else {
112			for _, s := range context.StringSlice("platform") {
113				ps, err := platforms.Parse(s)
114				if err != nil {
115					return errors.Wrapf(err, "unable to parse platform %s", s)
116				}
117				p = append(p, ps)
118			}
119		}
120		if len(p) == 0 {
121			p = append(p, platforms.DefaultSpec())
122		}
123
124		start := time.Now()
125		for _, platform := range p {
126			fmt.Printf("unpacking %s %s...\n", platforms.Format(platform), img.Target.Digest)
127			i := containerd.NewImageWithPlatform(client, img, platforms.Only(platform))
128			err = i.Unpack(ctx, context.String("snapshotter"))
129			if err != nil {
130				return err
131			}
132			if context.Bool("print-chainid") {
133				diffIDs, err := i.RootFS(ctx)
134				if err != nil {
135					return err
136				}
137				chainID := identity.ChainID(diffIDs).String()
138				fmt.Printf("image chain ID: %s\n", chainID)
139			}
140		}
141		fmt.Printf("done: %s\t\n", time.Since(start))
142		return nil
143	},
144}
145