1package containerizedengine
2
3import (
4	"context"
5	"encoding/json"
6	"fmt"
7	"strings"
8
9	"github.com/containerd/containerd"
10	"github.com/containerd/containerd/content"
11	"github.com/containerd/containerd/errdefs"
12	"github.com/containerd/containerd/images"
13	"github.com/containerd/containerd/namespaces"
14	"github.com/docker/cli/internal/versions"
15	clitypes "github.com/docker/cli/types"
16	"github.com/docker/distribution/reference"
17	"github.com/docker/docker/api/types"
18	ver "github.com/hashicorp/go-version"
19	"github.com/opencontainers/image-spec/specs-go/v1"
20	"github.com/pkg/errors"
21)
22
23// ActivateEngine will switch the image from the CE to EE image
24func (c *baseClient) ActivateEngine(ctx context.Context, opts clitypes.EngineInitOptions, out clitypes.OutStream,
25	authConfig *types.AuthConfig) error {
26
27	// If the user didn't specify an image, determine the correct enterprise image to use
28	if opts.EngineImage == "" {
29		localMetadata, err := versions.GetCurrentRuntimeMetadata(opts.RuntimeMetadataDir)
30		if err != nil {
31			return errors.Wrap(err, "unable to determine the installed engine version. Specify which engine image to update with --engine-image")
32		}
33
34		engineImage := localMetadata.EngineImage
35		if engineImage == clitypes.EnterpriseEngineImage || engineImage == clitypes.CommunityEngineImage {
36			opts.EngineImage = clitypes.EnterpriseEngineImage
37		} else {
38			// Chop off the standard prefix and retain any trailing OS specific image details
39			// e.g., engine-community-dm -> engine-enterprise-dm
40			engineImage = strings.TrimPrefix(engineImage, clitypes.EnterpriseEngineImage)
41			engineImage = strings.TrimPrefix(engineImage, clitypes.CommunityEngineImage)
42			opts.EngineImage = clitypes.EnterpriseEngineImage + engineImage
43		}
44	}
45
46	ctx = namespaces.WithNamespace(ctx, engineNamespace)
47	return c.DoUpdate(ctx, opts, out, authConfig)
48}
49
50// DoUpdate performs the underlying engine update
51func (c *baseClient) DoUpdate(ctx context.Context, opts clitypes.EngineInitOptions, out clitypes.OutStream,
52	authConfig *types.AuthConfig) error {
53
54	ctx = namespaces.WithNamespace(ctx, engineNamespace)
55	if opts.EngineVersion == "" {
56		// TODO - Future enhancement: This could be improved to be
57		// smart about figuring out the latest patch rev for the
58		// current engine version and automatically apply it so users
59		// could stay in sync by simply having a scheduled
60		// `docker engine update`
61		return fmt.Errorf("pick the version you want to update to with --version")
62	}
63	var localMetadata *clitypes.RuntimeMetadata
64	if opts.EngineImage == "" {
65		var err error
66		localMetadata, err = versions.GetCurrentRuntimeMetadata(opts.RuntimeMetadataDir)
67		if err != nil {
68			return errors.Wrap(err, "unable to determine the installed engine version. Specify which engine image to update with --engine-image set to 'engine-community' or 'engine-enterprise'")
69		}
70		opts.EngineImage = localMetadata.EngineImage
71	}
72
73	imageName := fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, opts.EngineImage, opts.EngineVersion)
74
75	// Look for desired image
76	image, err := c.cclient.GetImage(ctx, imageName)
77	if err != nil {
78		if errdefs.IsNotFound(err) {
79			image, err = c.pullWithAuth(ctx, imageName, out, authConfig)
80			if err != nil {
81				return errors.Wrapf(err, "unable to pull image %s", imageName)
82			}
83		} else {
84			return errors.Wrapf(err, "unable to check for image %s", imageName)
85		}
86	}
87
88	// Make sure we're safe to proceed
89	newMetadata, err := c.PreflightCheck(ctx, image)
90	if err != nil {
91		return err
92	}
93	if localMetadata != nil {
94		if localMetadata.Platform != newMetadata.Platform {
95			fmt.Fprintf(out, "\nNotice: you have switched to \"%s\".  Refer to %s for update instructions.\n\n", newMetadata.Platform, getReleaseNotesURL(imageName))
96		}
97	}
98
99	if err := c.cclient.Install(ctx, image, containerd.WithInstallReplace, containerd.WithInstallPath("/usr")); err != nil {
100		return err
101	}
102
103	return versions.WriteRuntimeMetadata(opts.RuntimeMetadataDir, newMetadata)
104}
105
106// PreflightCheck verifies the specified image is compatible with the local system before proceeding to update/activate
107// If things look good, the RuntimeMetadata for the new image is returned and can be written out to the host
108func (c *baseClient) PreflightCheck(ctx context.Context, image containerd.Image) (*clitypes.RuntimeMetadata, error) {
109	var metadata clitypes.RuntimeMetadata
110	ic, err := image.Config(ctx)
111	if err != nil {
112		return nil, err
113	}
114	var (
115		ociimage v1.Image
116		config   v1.ImageConfig
117	)
118	switch ic.MediaType {
119	case v1.MediaTypeImageConfig, images.MediaTypeDockerSchema2Config:
120		p, err := content.ReadBlob(ctx, image.ContentStore(), ic)
121		if err != nil {
122			return nil, err
123		}
124
125		if err := json.Unmarshal(p, &ociimage); err != nil {
126			return nil, err
127		}
128		config = ociimage.Config
129	default:
130		return nil, fmt.Errorf("unknown image %s config media type %s", image.Name(), ic.MediaType)
131	}
132
133	metadataString, ok := config.Labels["com.docker."+clitypes.RuntimeMetadataName]
134	if !ok {
135		return nil, fmt.Errorf("image %s does not contain runtime metadata label %s", image.Name(), clitypes.RuntimeMetadataName)
136	}
137	err = json.Unmarshal([]byte(metadataString), &metadata)
138	if err != nil {
139		return nil, errors.Wrapf(err, "malformed runtime metadata file in %s", image.Name())
140	}
141
142	// Current CLI only supports host install runtime
143	if metadata.Runtime != "host_install" {
144		return nil, fmt.Errorf("unsupported daemon image: %s\nConsult the release notes at %s for upgrade instructions", metadata.Runtime, getReleaseNotesURL(image.Name()))
145	}
146
147	// Verify local containerd is new enough
148	localVersion, err := c.cclient.Version(ctx)
149	if err != nil {
150		return nil, err
151	}
152	if metadata.ContainerdMinVersion != "" {
153		lv, err := ver.NewVersion(localVersion.Version)
154		if err != nil {
155			return nil, err
156		}
157		mv, err := ver.NewVersion(metadata.ContainerdMinVersion)
158		if err != nil {
159			return nil, err
160		}
161		if lv.LessThan(mv) {
162			return nil, fmt.Errorf("local containerd is too old: %s - this engine version requires %s or newer.\nConsult the release notes at %s for upgrade instructions",
163				localVersion.Version, metadata.ContainerdMinVersion, getReleaseNotesURL(image.Name()))
164		}
165	} // If omitted on metadata, no hard dependency on containerd version beyond 18.09 baseline
166
167	// All checks look OK, proceed with update
168	return &metadata, nil
169}
170
171// getReleaseNotesURL returns a release notes url
172// If the image name does not contain a version tag, the base release notes URL is returned
173func getReleaseNotesURL(imageName string) string {
174	versionTag := ""
175	distributionRef, err := reference.ParseNormalizedNamed(imageName)
176	if err == nil {
177		taggedRef, ok := distributionRef.(reference.NamedTagged)
178		if ok {
179			versionTag = taggedRef.Tag()
180		}
181	}
182	return fmt.Sprintf("%s?%s", clitypes.ReleaseNotePrefix, versionTag)
183}
184