diff --git a/cli/command/engine/activate.go b/cli/command/engine/activate.go index a40438a13f..e37048f194 100644 --- a/cli/command/engine/activate.go +++ b/cli/command/engine/activate.go @@ -12,7 +12,6 @@ import ( "github.com/docker/licensing/model" "github.com/pkg/errors" "github.com/spf13/cobra" - "golang.org/x/sys/unix" ) type activateOptions struct { @@ -68,7 +67,7 @@ https://hub.docker.com/ then specify the file with the '--license' flag. } func runActivate(cli command.Cli, options activateOptions) error { - if unix.Geteuid() != 0 { + if !isRoot() { return errors.New("must be privileged to activate engine") } ctx := context.Background() @@ -108,12 +107,17 @@ func runActivate(cli command.Cli, options activateOptions) error { EngineVersion: options.version, } - return client.ActivateEngine(ctx, opts, cli.Out(), authConfig, + err = client.ActivateEngine(ctx, opts, cli.Out(), authConfig, func(ctx context.Context) error { client := cli.Client() _, err := client.Ping(ctx) return err }) + if err != nil { + return err + } + fmt.Fprintln(cli.Out(), "To complete the activation, please restart docker with 'systemctl restart docker'") + return nil } func getLicenses(ctx context.Context, authConfig *types.AuthConfig, cli command.Cli, options activateOptions) (*model.IssuedLicense, error) { diff --git a/cli/command/engine/activate_test.go b/cli/command/engine/activate_test.go index 6936161eef..6fe552c7da 100644 --- a/cli/command/engine/activate_test.go +++ b/cli/command/engine/activate_test.go @@ -14,6 +14,7 @@ func TestActivateNoContainerd(t *testing.T) { return nil, fmt.Errorf("some error") }, ) + isRoot = func() bool { return true } cmd := newActivateCommand(testCli) cmd.Flags().Set("license", "invalidpath") cmd.SilenceUsage = true @@ -28,6 +29,7 @@ func TestActivateBadLicense(t *testing.T) { return &fakeContainerizedEngineClient{}, nil }, ) + isRoot = func() bool { return true } cmd := newActivateCommand(testCli) cmd.SilenceUsage = true cmd.SilenceErrors = true diff --git a/cli/command/engine/activate_unix.go b/cli/command/engine/activate_unix.go new file mode 100644 index 0000000000..ed4777ae2e --- /dev/null +++ b/cli/command/engine/activate_unix.go @@ -0,0 +1,13 @@ +// +build !windows + +package engine + +import ( + "golang.org/x/sys/unix" +) + +var ( + isRoot = func() bool { + return unix.Geteuid() == 0 + } +) diff --git a/cli/command/engine/activate_windows.go b/cli/command/engine/activate_windows.go new file mode 100644 index 0000000000..35a4e88a36 --- /dev/null +++ b/cli/command/engine/activate_windows.go @@ -0,0 +1,9 @@ +// +build windows + +package engine + +var ( + isRoot = func() bool { + return true + } +) diff --git a/cli/command/engine/check.go b/cli/command/engine/check.go index 86d4243dca..b4d92cfa5e 100644 --- a/cli/command/engine/check.go +++ b/cli/command/engine/check.go @@ -1,14 +1,16 @@ package engine import ( + "context" "fmt" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/formatter" + "github.com/docker/cli/internal/versions" clitypes "github.com/docker/cli/types" "github.com/pkg/errors" "github.com/spf13/cobra" - "golang.org/x/sys/unix" ) const ( @@ -37,7 +39,7 @@ func newCheckForUpdatesCommand(dockerCli command.Cli) *cobra.Command { }, } flags := cmd.Flags() - flags.StringVar(&options.registryPrefix, "registry-prefix", "", "Override the existing location where engine images are pulled") + flags.StringVar(&options.registryPrefix, "registry-prefix", "docker.io/store/docker", "Override the existing location where engine images are pulled") flags.BoolVar(&options.downgrades, "downgrades", false, "Report downgrades (default omits older versions)") flags.BoolVar(&options.preReleases, "pre-releases", false, "Include pre-release versions") flags.BoolVar(&options.upgrades, "upgrades", true, "Report available upgrades") @@ -49,70 +51,67 @@ func newCheckForUpdatesCommand(dockerCli command.Cli) *cobra.Command { } func runCheck(dockerCli command.Cli, options checkOptions) error { - if unix.Geteuid() != 0 { + if !isRoot() { return errors.New("must be privileged to activate engine") } + ctx := context.Background() + client := dockerCli.Client() + serverVersion, err := client.ServerVersion(ctx) + if err != nil { + return err + } - /* - ctx := context.Background() - client, err := dockerCli.NewContainerizedEngineClient(options.sockPath) - if err != nil { - return errors.Wrap(err, "unable to access local containerd") - } - defer client.Close() - versions, err := client.GetEngineVersions(ctx, dockerCli.RegistryClient(false), currentVersion, imageName) - if err != nil { - return err - } + availVersions, err := versions.GetEngineVersions(ctx, dockerCli.RegistryClient(false), options.registryPrefix, serverVersion) + if err != nil { + return err + } - availUpdates := []clitypes.Update{ - {Type: "current", Version: currentVersion}, - } - if len(versions.Patches) > 0 { - availUpdates = append(availUpdates, - processVersions( - currentVersion, - "patch", - options.preReleases, - versions.Patches)...) - } - if options.upgrades { - availUpdates = append(availUpdates, - processVersions( - currentVersion, - "upgrade", - options.preReleases, - versions.Upgrades)...) - } - if options.downgrades { - availUpdates = append(availUpdates, - processVersions( - currentVersion, - "downgrade", - options.preReleases, - versions.Downgrades)...) - } + availUpdates := []clitypes.Update{ + {Type: "current", Version: serverVersion.Version}, + } + if len(availVersions.Patches) > 0 { + availUpdates = append(availUpdates, + processVersions( + serverVersion.Version, + "patch", + options.preReleases, + availVersions.Patches)...) + } + if options.upgrades { + availUpdates = append(availUpdates, + processVersions( + serverVersion.Version, + "upgrade", + options.preReleases, + availVersions.Upgrades)...) + } + if options.downgrades { + availUpdates = append(availUpdates, + processVersions( + serverVersion.Version, + "downgrade", + options.preReleases, + availVersions.Downgrades)...) + } - format := options.format - if len(format) == 0 { - format = formatter.TableFormatKey - } + format := options.format + if len(format) == 0 { + format = formatter.TableFormatKey + } - updatesCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewUpdatesFormat(format, options.quiet), - Trunc: false, - } - return formatter.UpdatesWrite(updatesCtx, availUpdates) - */ - return nil + updatesCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.NewUpdatesFormat(format, options.quiet), + Trunc: false, + } + return formatter.UpdatesWrite(updatesCtx, availUpdates) } func processVersions(currentVersion, verType string, includePrerelease bool, - versions []clitypes.DockerVersion) []clitypes.Update { + availVersions []clitypes.DockerVersion) []clitypes.Update { availUpdates := []clitypes.Update{} - for _, ver := range versions { + for _, ver := range availVersions { if !includePrerelease && ver.Prerelease() != "" { continue } diff --git a/cli/command/engine/check_test.go b/cli/command/engine/check_test.go index 14c6eddb27..89450e67f9 100644 --- a/cli/command/engine/check_test.go +++ b/cli/command/engine/check_test.go @@ -5,11 +5,13 @@ import ( "fmt" "testing" - registryclient "github.com/docker/cli/cli/registry/client" + manifesttypes "github.com/docker/cli/cli/manifest/types" "github.com/docker/cli/internal/test" - clitypes "github.com/docker/cli/types" + "github.com/docker/distribution" + "github.com/docker/distribution/reference" + "github.com/docker/docker/api/types" "github.com/docker/docker/client" - ver "github.com/hashicorp/go-version" + "github.com/opencontainers/go-digest" "gotest.tools/assert" "gotest.tools/golden" ) @@ -18,126 +20,87 @@ var ( testCli = test.NewFakeCli(&client.Client{}) ) -func TestCheckForUpdatesNoContainerd(t *testing.T) { - testCli.SetContainerizedEngineClient( - func(string) (clitypes.ContainerizedClient, error) { - return nil, fmt.Errorf("some error") - }, - ) - cmd := newCheckForUpdatesCommand(testCli) - cmd.SilenceUsage = true - cmd.SilenceErrors = true - err := cmd.Execute() - assert.ErrorContains(t, err, "unable to access local containerd") +type verClient struct { + client.Client + ver types.Version + verErr error +} + +func (c *verClient) ServerVersion(ctx context.Context) (types.Version, error) { + return c.ver, c.verErr +} + +type testRegistryClient struct { + tags []string +} + +func (c testRegistryClient) GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) { + return manifesttypes.ImageManifest{}, nil +} +func (c testRegistryClient) GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) { + return nil, nil +} +func (c testRegistryClient) MountBlob(ctx context.Context, source reference.Canonical, target reference.Named) error { + return nil +} + +func (c testRegistryClient) PutManifest(ctx context.Context, ref reference.Named, manifest distribution.Manifest) (digest.Digest, error) { + return "", nil +} +func (c testRegistryClient) GetTags(ctx context.Context, ref reference.Named) ([]string, error) { + return c.tags, nil } func TestCheckForUpdatesNoCurrentVersion(t *testing.T) { - retErr := fmt.Errorf("some failure") - getCurrentEngineVersionFunc := func(ctx context.Context) (clitypes.EngineInitOptions, error) { - return clitypes.EngineInitOptions{}, retErr - } - testCli.SetContainerizedEngineClient( - func(string) (clitypes.ContainerizedClient, error) { - return &fakeContainerizedEngineClient{ - getCurrentEngineVersionFunc: getCurrentEngineVersionFunc, - }, nil - }, - ) - cmd := newCheckForUpdatesCommand(testCli) + isRoot = func() bool { return true } + c := test.NewFakeCli(&verClient{client.Client{}, types.Version{}, nil}) + c.SetRegistryClient(testRegistryClient{}) + cmd := newCheckForUpdatesCommand(c) cmd.SilenceUsage = true cmd.SilenceErrors = true err := cmd.Execute() - assert.Assert(t, err == retErr) -} - -func TestCheckForUpdatesGetEngineVersionsFail(t *testing.T) { - retErr := fmt.Errorf("some failure") - getEngineVersionsFunc := func(ctx context.Context, - registryClient registryclient.RegistryClient, - currentVersion, imageName string) (clitypes.AvailableVersions, error) { - return clitypes.AvailableVersions{}, retErr - } - testCli.SetContainerizedEngineClient( - func(string) (clitypes.ContainerizedClient, error) { - return &fakeContainerizedEngineClient{ - getEngineVersionsFunc: getEngineVersionsFunc, - }, nil - }, - ) - cmd := newCheckForUpdatesCommand(testCli) - cmd.SilenceUsage = true - cmd.SilenceErrors = true - err := cmd.Execute() - assert.Assert(t, err == retErr) + assert.ErrorContains(t, err, "alformed version") } func TestCheckForUpdatesGetEngineVersionsHappy(t *testing.T) { - getCurrentEngineVersionFunc := func(ctx context.Context) (clitypes.EngineInitOptions, error) { - return clitypes.EngineInitOptions{ - EngineImage: "current engine", - EngineVersion: "1.1.0", - }, nil - } - getEngineVersionsFunc := func(ctx context.Context, - registryClient registryclient.RegistryClient, - currentVersion, imageName string) (clitypes.AvailableVersions, error) { - return clitypes.AvailableVersions{ - Downgrades: parseVersions(t, "1.0.1", "1.0.2", "1.0.3-beta1"), - Patches: parseVersions(t, "1.1.1", "1.1.2", "1.1.3-beta1"), - Upgrades: parseVersions(t, "1.2.0", "2.0.0", "2.1.0-beta1"), - }, nil - } - testCli.SetContainerizedEngineClient( - func(string) (clitypes.ContainerizedClient, error) { - return &fakeContainerizedEngineClient{ - getEngineVersionsFunc: getEngineVersionsFunc, - getCurrentEngineVersionFunc: getCurrentEngineVersionFunc, - }, nil - }, - ) - cmd := newCheckForUpdatesCommand(testCli) + c := test.NewFakeCli(&verClient{client.Client{}, types.Version{Version: "1.1.0"}, nil}) + c.SetRegistryClient(testRegistryClient{[]string{ + "1.0.1", "1.0.2", "1.0.3-beta1", + "1.1.1", "1.1.2", "1.1.3-beta1", + "1.2.0", "2.0.0", "2.1.0-beta1", + }}) + isRoot = func() bool { return true } + cmd := newCheckForUpdatesCommand(c) cmd.Flags().Set("pre-releases", "true") cmd.Flags().Set("downgrades", "true") + cmd.SilenceUsage = true + cmd.SilenceErrors = true err := cmd.Execute() assert.NilError(t, err) - golden.Assert(t, testCli.OutBuffer().String(), "check-all.golden") + golden.Assert(t, c.OutBuffer().String(), "check-all.golden") - testCli.OutBuffer().Reset() + c.OutBuffer().Reset() cmd.Flags().Set("pre-releases", "false") cmd.Flags().Set("downgrades", "true") err = cmd.Execute() assert.NilError(t, err) - fmt.Println(testCli.OutBuffer().String()) - golden.Assert(t, testCli.OutBuffer().String(), "check-no-prerelease.golden") + fmt.Println(c.OutBuffer().String()) + golden.Assert(t, c.OutBuffer().String(), "check-no-prerelease.golden") - testCli.OutBuffer().Reset() + c.OutBuffer().Reset() cmd.Flags().Set("pre-releases", "false") cmd.Flags().Set("downgrades", "false") err = cmd.Execute() assert.NilError(t, err) - fmt.Println(testCli.OutBuffer().String()) - golden.Assert(t, testCli.OutBuffer().String(), "check-no-downgrades.golden") + fmt.Println(c.OutBuffer().String()) + golden.Assert(t, c.OutBuffer().String(), "check-no-downgrades.golden") - testCli.OutBuffer().Reset() + c.OutBuffer().Reset() cmd.Flags().Set("pre-releases", "false") cmd.Flags().Set("downgrades", "false") cmd.Flags().Set("upgrades", "false") err = cmd.Execute() assert.NilError(t, err) - fmt.Println(testCli.OutBuffer().String()) - golden.Assert(t, testCli.OutBuffer().String(), "check-patches-only.golden") -} - -func makeVersion(t *testing.T, tag string) clitypes.DockerVersion { - v, err := ver.NewVersion(tag) - assert.NilError(t, err) - return clitypes.DockerVersion{Version: *v, Tag: tag} -} - -func parseVersions(t *testing.T, tags ...string) []clitypes.DockerVersion { - ret := make([]clitypes.DockerVersion, len(tags)) - for i, tag := range tags { - ret[i] = makeVersion(t, tag) - } - return ret + fmt.Println(c.OutBuffer().String()) + golden.Assert(t, c.OutBuffer().String(), "check-patches-only.golden") } diff --git a/cli/command/engine/cmd_test.go b/cli/command/engine/cmd_test.go index 9378f0aa63..30639cbf3c 100644 --- a/cli/command/engine/cmd_test.go +++ b/cli/command/engine/cmd_test.go @@ -10,5 +10,5 @@ func TestNewEngineCommand(t *testing.T) { cmd := NewEngineCommand(testCli) subcommands := cmd.Commands() - assert.Assert(t, len(subcommands) == 5) + assert.Assert(t, len(subcommands) == 3) } diff --git a/cli/command/engine/init.go b/cli/command/engine/init.go index 9d1ff3e34e..f29001d086 100644 --- a/cli/command/engine/init.go +++ b/cli/command/engine/init.go @@ -1,62 +1,10 @@ package engine import ( - "context" - - "github.com/docker/cli/cli" - "github.com/docker/cli/cli/command" clitypes "github.com/docker/cli/types" - "github.com/pkg/errors" - "github.com/spf13/cobra" ) type extendedEngineInitOptions struct { clitypes.EngineInitOptions sockPath string } - -func newInitCommand(dockerCli command.Cli) *cobra.Command { - var options extendedEngineInitOptions - - cmd := &cobra.Command{ - Use: "init [OPTIONS]", - Short: "Initialize a local engine", - Long: `This command will initialize a local engine running on containerd. - -Configuration of the engine is managed through the daemon.json configuration -file on the host and may be pre-created before running the 'init' command. -`, - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runInit(dockerCli, options) - }, - Annotations: map[string]string{"experimentalCLI": ""}, - } - flags := cmd.Flags() - flags.StringVar(&options.EngineVersion, "version", cli.Version, "Specify engine version") - flags.StringVar(&options.EngineImage, "engine-image", clitypes.CommunityEngineImage, "Specify engine image") - flags.StringVar(&options.RegistryPrefix, "registry-prefix", "docker.io/docker", "Override the default location where engine images are pulled") - flags.StringVar(&options.ConfigFile, "config-file", "/etc/docker/daemon.json", "Specify the location of the daemon configuration file on the host") - flags.StringVar(&options.sockPath, "containerd", "", "override default location of containerd endpoint") - - return cmd -} - -func runInit(dockerCli command.Cli, options extendedEngineInitOptions) error { - ctx := context.Background() - client, err := dockerCli.NewContainerizedEngineClient(options.sockPath) - if err != nil { - return errors.Wrap(err, "unable to access local containerd") - } - defer client.Close() - authConfig, err := getRegistryAuth(dockerCli, options.RegistryPrefix) - if err != nil { - return err - } - return client.InitEngine(ctx, options.EngineInitOptions, dockerCli.Out(), authConfig, - func(ctx context.Context) error { - client := dockerCli.Client() - _, err := client.Ping(ctx) - return err - }) -} diff --git a/cli/command/engine/init_test.go b/cli/command/engine/init_test.go deleted file mode 100644 index 5d6c91b1f4..0000000000 --- a/cli/command/engine/init_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package engine - -import ( - "fmt" - "testing" - - clitypes "github.com/docker/cli/types" - "gotest.tools/assert" -) - -func TestInitNoContainerd(t *testing.T) { - testCli.SetContainerizedEngineClient( - func(string) (clitypes.ContainerizedClient, error) { - return nil, fmt.Errorf("some error") - }, - ) - cmd := newInitCommand(testCli) - cmd.SilenceUsage = true - cmd.SilenceErrors = true - err := cmd.Execute() - assert.ErrorContains(t, err, "unable to access local containerd") -} - -func TestInitHappy(t *testing.T) { - testCli.SetContainerizedEngineClient( - func(string) (clitypes.ContainerizedClient, error) { - return &fakeContainerizedEngineClient{}, nil - }, - ) - cmd := newInitCommand(testCli) - err := cmd.Execute() - assert.NilError(t, err) -} diff --git a/cli/command/engine/update.go b/cli/command/engine/update.go index 73f3638629..0709f314e6 100644 --- a/cli/command/engine/update.go +++ b/cli/command/engine/update.go @@ -8,7 +8,6 @@ import ( "github.com/docker/cli/cli/command" "github.com/pkg/errors" "github.com/spf13/cobra" - "golang.org/x/sys/unix" ) func newUpdateCommand(dockerCli command.Cli) *cobra.Command { @@ -33,7 +32,7 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command { } func runUpdate(dockerCli command.Cli, options extendedEngineInitOptions) error { - if unix.Geteuid() != 0 { + if !isRoot() { return errors.New("must be privileged to activate engine") } ctx := context.Background() @@ -43,11 +42,8 @@ func runUpdate(dockerCli command.Cli, options extendedEngineInitOptions) error { } defer client.Close() if options.EngineImage == "" || options.RegistryPrefix == "" { - if options.EngineImage == "" { - options.EngineImage = "docker/engine-community" - } if options.RegistryPrefix == "" { - options.RegistryPrefix = "docker.io" + options.RegistryPrefix = "docker.io/store/docker" } } authConfig, err := getRegistryAuth(dockerCli, options.RegistryPrefix) @@ -63,6 +59,6 @@ func runUpdate(dockerCli command.Cli, options extendedEngineInitOptions) error { }); err != nil { return err } - fmt.Fprintln(dockerCli.Out(), "Success! The docker engine is now running.") + fmt.Fprintln(dockerCli.Out(), "To complete the update, please restart docker with 'systemctl restart docker'") return nil } diff --git a/docker.Makefile b/docker.Makefile index 4955be6088..69135e97a6 100644 --- a/docker.Makefile +++ b/docker.Makefile @@ -105,7 +105,7 @@ shellcheck: build_shell_validate_image ## run shellcheck validation docker run -ti --rm $(ENVVARS) $(MOUNTS) $(VALIDATE_IMAGE_NAME) make shellcheck .PHONY: test-e2e ## run e2e tests -test-e2e: test-e2e-non-experimental test-e2e-experimental test-e2e-containerized +test-e2e: test-e2e-non-experimental test-e2e-experimental .PHONY: test-e2e-experimental test-e2e-experimental: build_e2e_image @@ -115,14 +115,6 @@ test-e2e-experimental: build_e2e_image test-e2e-non-experimental: build_e2e_image docker run --rm -v /var/run/docker.sock:/var/run/docker.sock $(E2E_IMAGE_NAME) -.PHONY: test-e2e-containerized -test-e2e-containerized: build_e2e_image - docker run --rm --privileged \ - -v /var/lib/docker \ - -v /var/lib/containerd \ - -v /lib/modules:/lib/modules \ - $(E2E_IMAGE_NAME) /go/src/github.com/docker/cli/scripts/test/engine/entry - .PHONY: help help: ## print this help @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) diff --git a/e2eengine/utils.go b/e2eengine/utils.go index 3f109f73db..9139adeda9 100644 --- a/e2eengine/utils.go +++ b/e2eengine/utils.go @@ -38,9 +38,13 @@ func CleanupEngine(t *testing.T) error { return err } // TODO Consider nuking the docker dir too so there's no cached content between test cases - err = client.RemoveEngine(ctx) - if err != nil { - t.Logf("Failed to remove engine: %s", err) - } + + // TODO - this needs refactoring still to actually work properly + /* + err = client.RemoveEngine(ctx) + if err != nil { + t.Logf("Failed to remove engine: %s", err) + } + */ return err } diff --git a/internal/containerizedengine/client_test.go b/internal/containerizedengine/client_test.go index cc5c5770b2..62216c99dc 100644 --- a/internal/containerizedengine/client_test.go +++ b/internal/containerizedengine/client_test.go @@ -24,6 +24,8 @@ type ( getImageFunc func(ctx context.Context, ref string) (containerd.Image, error) contentStoreFunc func() content.Store containerServiceFunc func() containers.Store + installFunc func(context.Context, containerd.Image, ...containerd.InstallOpts) error + versionFunc func(ctx context.Context) (containerd.Version, error) } fakeContainer struct { idFunc func() string @@ -109,6 +111,18 @@ func (w *fakeContainerdClient) ContainerService() containers.Store { func (w *fakeContainerdClient) Close() error { return nil } +func (w *fakeContainerdClient) Install(ctx context.Context, image containerd.Image, args ...containerd.InstallOpts) error { + if w.installFunc != nil { + return w.installFunc(ctx, image, args...) + } + return nil +} +func (w *fakeContainerdClient) Version(ctx context.Context) (containerd.Version, error) { + if w.versionFunc != nil { + return w.versionFunc(ctx) + } + return containerd.Version{}, nil +} func (c *fakeContainer) ID() string { if c.idFunc != nil { diff --git a/internal/containerizedengine/types.go b/internal/containerizedengine/types.go index 8268e1e4fa..a0695ee93d 100644 --- a/internal/containerizedengine/types.go +++ b/internal/containerizedengine/types.go @@ -64,6 +64,12 @@ var ( NoNewPrivileges: false, }, } + + // RuntimeMetadataName is the name of the runtime metadata file + RuntimeMetadataName = "distribution_based_engine" + + // ReleaseNotePrefix is where to point users to for release notes + ReleaseNotePrefix = "https://docs.docker.com/releasenotes" ) type baseClient struct { @@ -80,4 +86,12 @@ type containerdClient interface { ContentStore() content.Store ContainerService() containers.Store Install(context.Context, containerd.Image, ...containerd.InstallOpts) error + Version(ctx context.Context) (containerd.Version, error) +} + +// RuntimeMetadata holds platform information about the daemon +type RuntimeMetadata struct { + Platform string `json:"platform"` + ContainerdMinVersion string `json:"containerd_min_version"` + Runtime string `json:"runtime"` } diff --git a/internal/containerizedengine/update.go b/internal/containerizedengine/update.go index d4626b96b6..bb551555eb 100644 --- a/internal/containerizedengine/update.go +++ b/internal/containerizedengine/update.go @@ -2,13 +2,22 @@ package containerizedengine import ( "context" + "encoding/json" "fmt" + "io/ioutil" + "path/filepath" + "strings" "github.com/containerd/containerd" + "github.com/containerd/containerd/content" "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/images" "github.com/containerd/containerd/namespaces" clitypes "github.com/docker/cli/types" + "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" + ver "github.com/hashicorp/go-version" + "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" ) @@ -34,6 +43,20 @@ func (c *baseClient) DoUpdate(ctx context.Context, opts clitypes.EngineInitOptio return fmt.Errorf("please pick the version you want to update to") } + localMetadata, err := c.GetCurrentRuntimeMetadata(ctx, "") + if err == nil { + if opts.EngineImage == "" { + if strings.Contains(strings.ToLower(localMetadata.Platform), "enterprise") { + opts.EngineImage = "engine-enterprise" + } else { + opts.EngineImage = "engine-community" + } + } + } + if opts.EngineImage == "" { + return fmt.Errorf("please pick the engine image to update with (engine-community or engine-enterprise)") + } + imageName := fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, opts.EngineImage, opts.EngineVersion) // Look for desired image @@ -48,5 +71,137 @@ func (c *baseClient) DoUpdate(ctx context.Context, opts clitypes.EngineInitOptio return errors.Wrapf(err, "unable to check for image %s", imageName) } } - return c.cclient.Install(ctx, image, containerd.WithInstallReplace, containerd.WithInstallPath("/usr")) + + // Make sure we're safe to proceed + newMetadata, err := c.PreflightCheck(ctx, image) + if err != nil { + return err + } + // Grab current metadata for comparison purposes + if localMetadata != nil { + if localMetadata.Platform != newMetadata.Platform { + fmt.Fprintf(out, "\nNotice: you have switched to \"%s\". Please refer to %s for update instructions.\n\n", newMetadata.Platform, c.GetReleaseNotesURL(imageName)) + } + } + + err = c.cclient.Install(ctx, image, containerd.WithInstallReplace, containerd.WithInstallPath("/usr")) + if err != nil { + return err + } + + return c.WriteRuntimeMetadata(ctx, "", newMetadata) +} + +var defaultDockerRoot = "/var/lib/docker" + +// GetCurrentRuntimeMetadata loads the current daemon runtime metadata information from the local host +func (c *baseClient) GetCurrentRuntimeMetadata(_ context.Context, dockerRoot string) (*RuntimeMetadata, error) { + if dockerRoot == "" { + dockerRoot = defaultDockerRoot + } + filename := filepath.Join(dockerRoot, RuntimeMetadataName+".json") + + data, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + var res RuntimeMetadata + err = json.Unmarshal(data, &res) + if err != nil { + return nil, errors.Wrapf(err, "malformed runtime metadata file %s", filename) + } + return &res, nil +} + +func (c *baseClient) WriteRuntimeMetadata(_ context.Context, dockerRoot string, metadata *RuntimeMetadata) error { + if dockerRoot == "" { + dockerRoot = defaultDockerRoot + } + filename := filepath.Join(dockerRoot, RuntimeMetadataName+".json") + + data, err := json.Marshal(metadata) + if err != nil { + return err + } + + return ioutil.WriteFile(filename, data, 0644) +} + +// PreflightCheck verifies the specified image is compatible with the local system before proceeding to update/activate +// If things look good, the RuntimeMetadata for the new image is returned and can be written out to the host +func (c *baseClient) PreflightCheck(ctx context.Context, image containerd.Image) (*RuntimeMetadata, error) { + var metadata RuntimeMetadata + ic, err := image.Config(ctx) + if err != nil { + return nil, err + } + var ( + ociimage v1.Image + config v1.ImageConfig + ) + switch ic.MediaType { + case v1.MediaTypeImageConfig, images.MediaTypeDockerSchema2Config: + p, err := content.ReadBlob(ctx, image.ContentStore(), ic) + if err != nil { + return nil, err + } + + if err := json.Unmarshal(p, &ociimage); err != nil { + return nil, err + } + config = ociimage.Config + default: + return nil, fmt.Errorf("unknown image %s config media type %s", image.Name(), ic.MediaType) + } + + metadataString, ok := config.Labels[RuntimeMetadataName] + if !ok { + return nil, fmt.Errorf("image %s does not contain runtime metadata label %s", image.Name(), RuntimeMetadataName) + } + err = json.Unmarshal([]byte(metadataString), &metadata) + if err != nil { + return nil, errors.Wrapf(err, "malformed runtime metadata file in %s", image.Name()) + } + + // Current CLI only supports host install runtime + if metadata.Runtime != "host_install" { + return nil, fmt.Errorf("unsupported runtime: %s\nPlease consult the release notes at %s for upgrade instructions", metadata.Runtime, c.GetReleaseNotesURL(image.Name())) + } + + // Verify local containerd is new enough + localVersion, err := c.cclient.Version(ctx) + if err != nil { + return nil, err + } + if metadata.ContainerdMinVersion != "" { + lv, err := ver.NewVersion(localVersion.Version) + if err != nil { + return nil, err + } + mv, err := ver.NewVersion(metadata.ContainerdMinVersion) + if err != nil { + return nil, err + } + if lv.LessThan(mv) { + return nil, fmt.Errorf("local containerd is too old: %s - this engine version requires %s or newer.\nPlease consult the release notes at %s for upgrade instructions", + localVersion.Version, metadata.ContainerdMinVersion, c.GetReleaseNotesURL(image.Name())) + } + } // If omitted on metadata, no hard dependency on containerd version beyond 18.09 baseline + + // All checks look OK, proceed with update + return &metadata, nil +} + +// GetReleaseNotesURL returns a release notes url +// If the image name does not contain a version tag, the base release notes URL is returned +func (c *baseClient) GetReleaseNotesURL(imageName string) string { + versionTag := "" + distributionRef, err := reference.ParseNormalizedNamed(imageName) + if err == nil { + taggedRef, ok := distributionRef.(reference.NamedTagged) + if ok { + versionTag = taggedRef.Tag() + } + } + return fmt.Sprintf("%s/%s", ReleaseNotePrefix, versionTag) } diff --git a/internal/containerizedengine/update_test.go b/internal/containerizedengine/update_test.go index cb1e06dd68..9f25b1311c 100644 --- a/internal/containerizedengine/update_test.go +++ b/internal/containerizedengine/update_test.go @@ -4,6 +4,9 @@ import ( "bytes" "context" "fmt" + "io/ioutil" + "os" + "path/filepath" "testing" "github.com/containerd/containerd" @@ -12,162 +15,20 @@ import ( "github.com/docker/cli/cli/command" clitypes "github.com/docker/cli/types" "github.com/docker/docker/api/types" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "gotest.tools/assert" ) -func TestGetCurrentEngineVersionHappy(t *testing.T) { - ctx := context.Background() - image := &fakeImage{ - nameFunc: func() string { - return "acme.com/dockermirror/" + clitypes.CommunityEngineImage + ":engineversion" - }, - } - container := &fakeContainer{ - imageFunc: func(context.Context) (containerd.Image, error) { - return image, nil - }, - } - client := baseClient{ - cclient: &fakeContainerdClient{ - containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) { - return []containerd.Container{container}, nil - }, - }, - } - - opts, err := client.GetCurrentEngineVersion(ctx) - assert.NilError(t, err) - assert.Equal(t, opts.EngineImage, clitypes.CommunityEngineImage) - assert.Equal(t, opts.RegistryPrefix, "acme.com/dockermirror") - assert.Equal(t, opts.EngineVersion, "engineversion") -} - -func TestGetCurrentEngineVersionEnterpriseHappy(t *testing.T) { - ctx := context.Background() - image := &fakeImage{ - nameFunc: func() string { - return "docker.io/docker/" + clitypes.EnterpriseEngineImage + ":engineversion" - }, - } - container := &fakeContainer{ - imageFunc: func(context.Context) (containerd.Image, error) { - return image, nil - }, - } - client := baseClient{ - cclient: &fakeContainerdClient{ - containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) { - return []containerd.Container{container}, nil - }, - }, - } - - opts, err := client.GetCurrentEngineVersion(ctx) - assert.NilError(t, err) - assert.Equal(t, opts.EngineImage, clitypes.EnterpriseEngineImage) - assert.Equal(t, opts.EngineVersion, "engineversion") - assert.Equal(t, opts.RegistryPrefix, "docker.io/docker") -} - -func TestGetCurrentEngineVersionNoEngine(t *testing.T) { - ctx := context.Background() - client := baseClient{ - cclient: &fakeContainerdClient{ - containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) { - return []containerd.Container{}, nil - }, - }, - } - - _, err := client.GetCurrentEngineVersion(ctx) - assert.ErrorContains(t, err, "failed to find existing engine") -} - -func TestGetCurrentEngineVersionMiscEngineError(t *testing.T) { - ctx := context.Background() - expectedError := fmt.Errorf("some container lookup error") - client := baseClient{ - cclient: &fakeContainerdClient{ - containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) { - return nil, expectedError - }, - }, - } - - _, err := client.GetCurrentEngineVersion(ctx) - assert.Assert(t, err == expectedError) -} - -func TestGetCurrentEngineVersionImageFailure(t *testing.T) { - ctx := context.Background() - container := &fakeContainer{ - imageFunc: func(context.Context) (containerd.Image, error) { - return nil, fmt.Errorf("container image failure") - }, - } - client := baseClient{ - cclient: &fakeContainerdClient{ - containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) { - return []containerd.Container{container}, nil - }, - }, - } - - _, err := client.GetCurrentEngineVersion(ctx) - assert.ErrorContains(t, err, "container image failure") -} - -func TestGetCurrentEngineVersionMalformed(t *testing.T) { - ctx := context.Background() - image := &fakeImage{ - nameFunc: func() string { - return "imagename" - }, - } - container := &fakeContainer{ - imageFunc: func(context.Context) (containerd.Image, error) { - return image, nil - }, - } - client := baseClient{ - cclient: &fakeContainerdClient{ - containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) { - return []containerd.Container{container}, nil - }, - }, - } - - _, err := client.GetCurrentEngineVersion(ctx) - assert.Assert(t, err == ErrEngineImageMissingTag) -} - -func TestActivateNoEngine(t *testing.T) { - ctx := context.Background() - client := baseClient{ - cclient: &fakeContainerdClient{ - containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) { - return []containerd.Container{}, nil - }, - }, - } - opts := clitypes.EngineInitOptions{ - EngineVersion: "engineversiongoeshere", - RegistryPrefix: "registryprefixgoeshere", - ConfigFile: "/tmp/configfilegoeshere", - EngineImage: clitypes.EnterpriseEngineImage, - } - - err := client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy) - assert.ErrorContains(t, err, "unable to find") -} - -func TestActivateNoChange(t *testing.T) { +func TestActivateConfigFailure(t *testing.T) { ctx := context.Background() registryPrefix := "registryprefixgoeshere" image := &fakeImage{ nameFunc: func() string { return registryPrefix + "/" + clitypes.EnterpriseEngineImage + ":engineversion" }, + configFunc: func(ctx context.Context) (ocispec.Descriptor, error) { + return ocispec.Descriptor{}, fmt.Errorf("config lookup failure") + }, } container := &fakeContainer{ imageFunc: func(context.Context) (containerd.Image, error) { @@ -185,6 +46,9 @@ func TestActivateNoChange(t *testing.T) { containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) { return []containerd.Container{container}, nil }, + getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) { + return image, nil + }, }, } opts := clitypes.EngineInitOptions{ @@ -195,7 +59,7 @@ func TestActivateNoChange(t *testing.T) { } err := client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy) - assert.NilError(t, err) + assert.ErrorContains(t, err, "config lookup failure") } func TestActivateDoUpdateFail(t *testing.T) { @@ -292,30 +156,112 @@ func TestDoUpdatePullFail(t *testing.T) { assert.ErrorContains(t, err, "pull failure") } -func TestDoUpdateEngineMissing(t *testing.T) { +func TestActivateDoUpdateVerifyImageName(t *testing.T) { ctx := context.Background() + registryPrefix := "registryprefixgoeshere" + image := &fakeImage{ + nameFunc: func() string { + return registryPrefix + "/ce-engine:engineversion" + }, + } + container := &fakeContainer{ + imageFunc: func(context.Context) (containerd.Image, error) { + return image, nil + }, + } + requestedImage := "unset" + client := baseClient{ + cclient: &fakeContainerdClient{ + containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) { + return []containerd.Container{container}, nil + }, + getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) { + requestedImage = ref + return nil, fmt.Errorf("something went wrong") + + }, + }, + } opts := clitypes.EngineInitOptions{ EngineVersion: "engineversiongoeshere", RegistryPrefix: "registryprefixgoeshere", ConfigFile: "/tmp/configfilegoeshere", - EngineImage: "testnamegoeshere", + //EngineImage: clitypes.EnterpriseEngineImage, } - image := &fakeImage{ - nameFunc: func() string { - return "imagenamehere" - }, - } - client := baseClient{ - cclient: &fakeContainerdClient{ - getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) { - return image, nil - }, - containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) { - return []containerd.Container{}, nil - }, - }, - } - err := client.DoUpdate(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy) - assert.ErrorContains(t, err, "unable to find existing engine") + tmpdir, err := ioutil.TempDir("", "docker-root") + assert.NilError(t, err) + defer os.RemoveAll(tmpdir) + defaultDockerRoot = tmpdir + metadata := RuntimeMetadata{Platform: "platformgoeshere"} + err = client.WriteRuntimeMetadata(ctx, tmpdir, &metadata) + assert.NilError(t, err) + + err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy) + assert.ErrorContains(t, err, "check for image") + assert.ErrorContains(t, err, "something went wrong") + expectedImage := fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, "engine-community", opts.EngineVersion) + assert.Assert(t, requestedImage == expectedImage, "%s != %s", requestedImage, expectedImage) + + // Redo with enterprise set + metadata = RuntimeMetadata{Platform: "Docker Engine - Enterprise"} + err = client.WriteRuntimeMetadata(ctx, tmpdir, &metadata) + assert.NilError(t, err) + + err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy) + assert.ErrorContains(t, err, "check for image") + assert.ErrorContains(t, err, "something went wrong") + expectedImage = fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, "engine-enterprise", opts.EngineVersion) + assert.Assert(t, requestedImage == expectedImage, "%s != %s", requestedImage, expectedImage) +} + +func TestGetCurrentRuntimeMetadataNotPresent(t *testing.T) { + ctx := context.Background() + tmpdir, err := ioutil.TempDir("", "docker-root") + assert.NilError(t, err) + defer os.RemoveAll(tmpdir) + client := baseClient{} + _, err = client.GetCurrentRuntimeMetadata(ctx, tmpdir) + assert.ErrorType(t, err, os.IsNotExist) +} + +func TestGetCurrentRuntimeMetadataBadJson(t *testing.T) { + ctx := context.Background() + tmpdir, err := ioutil.TempDir("", "docker-root") + assert.NilError(t, err) + defer os.RemoveAll(tmpdir) + filename := filepath.Join(tmpdir, RuntimeMetadataName+".json") + err = ioutil.WriteFile(filename, []byte("not json"), 0644) + assert.NilError(t, err) + client := baseClient{} + _, err = client.GetCurrentRuntimeMetadata(ctx, tmpdir) + assert.ErrorContains(t, err, "malformed runtime metadata file") +} + +func TestGetCurrentRuntimeMetadataHappyPath(t *testing.T) { + ctx := context.Background() + tmpdir, err := ioutil.TempDir("", "docker-root") + assert.NilError(t, err) + defer os.RemoveAll(tmpdir) + client := baseClient{} + metadata := RuntimeMetadata{Platform: "platformgoeshere"} + err = client.WriteRuntimeMetadata(ctx, tmpdir, &metadata) + assert.NilError(t, err) + + res, err := client.GetCurrentRuntimeMetadata(ctx, tmpdir) + assert.NilError(t, err) + assert.Equal(t, res.Platform, "platformgoeshere") +} + +func TestGetReleaseNotesURL(t *testing.T) { + client := baseClient{} + imageName := "bogus image name #$%&@!" + url := client.GetReleaseNotesURL(imageName) + assert.Equal(t, url, ReleaseNotePrefix+"/") + imageName = "foo.bar/valid/repowithouttag" + url = client.GetReleaseNotesURL(imageName) + assert.Equal(t, url, ReleaseNotePrefix+"/") + imageName = "foo.bar/valid/repowithouttag:tag123" + url = client.GetReleaseNotesURL(imageName) + assert.Equal(t, url, ReleaseNotePrefix+"/tag123") } diff --git a/internal/containerizedengine/versions.go b/internal/versions/versions.go similarity index 69% rename from internal/containerizedengine/versions.go rename to internal/versions/versions.go index 63ac4a2dc2..27c4557c67 100644 --- a/internal/containerizedengine/versions.go +++ b/internal/versions/versions.go @@ -1,19 +1,23 @@ -package containerizedengine +package versions import ( "context" + "path" "sort" + "strings" registryclient "github.com/docker/cli/cli/registry/client" clitypes "github.com/docker/cli/types" "github.com/docker/distribution/reference" + "github.com/docker/docker/api/types" ver "github.com/hashicorp/go-version" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) // GetEngineVersions reports the versions of the engine that are available -func (c *baseClient) GetEngineVersions(ctx context.Context, registryClient registryclient.RegistryClient, currentVersion, imageName string) (clitypes.AvailableVersions, error) { +func GetEngineVersions(ctx context.Context, registryClient registryclient.RegistryClient, registryPrefix string, serverVersion types.Version) (clitypes.AvailableVersions, error) { + imageName := getEngineImage(registryPrefix, serverVersion) imageRef, err := reference.ParseNormalizedNamed(imageName) if err != nil { return clitypes.AvailableVersions{}, err @@ -24,7 +28,23 @@ func (c *baseClient) GetEngineVersions(ctx context.Context, registryClient regis return clitypes.AvailableVersions{}, err } - return parseTags(tags, currentVersion) + return parseTags(tags, serverVersion.Version) +} + +func getEngineImage(registryPrefix string, serverVersion types.Version) string { + communityImage := "engine-community" + enterpriseImage := "engine-enterprise" + platform := strings.ToLower(serverVersion.Platform.Name) + if platform != "" { + if strings.Contains(platform, "enterprise") { + return path.Join(registryPrefix, enterpriseImage) + } + return path.Join(registryPrefix, communityImage) + } + if strings.Contains(serverVersion.Version, "ee") { + return path.Join(registryPrefix, enterpriseImage) + } + return path.Join(registryPrefix, communityImage) } func parseTags(tags []string, currentVersion string) (clitypes.AvailableVersions, error) { diff --git a/internal/containerizedengine/versions_test.go b/internal/versions/versions_test.go similarity index 91% rename from internal/containerizedengine/versions_test.go rename to internal/versions/versions_test.go index eec782471b..1828ad0249 100644 --- a/internal/containerizedengine/versions_test.go +++ b/internal/versions/versions_test.go @@ -1,19 +1,19 @@ -package containerizedengine +package versions import ( "context" "testing" + "github.com/docker/docker/api/types" "gotest.tools/assert" ) func TestGetEngineVersionsBadImage(t *testing.T) { ctx := context.Background() - client := baseClient{} - currentVersion := "currentversiongoeshere" - imageName := "this is an illegal image $%^&" - _, err := client.GetEngineVersions(ctx, nil, currentVersion, imageName) + registryPrefix := "this is an illegal image $%^&" + currentVersion := types.Version{Version: "currentversiongoeshere"} + _, err := GetEngineVersions(ctx, nil, registryPrefix, currentVersion) assert.ErrorContains(t, err, "invalid reference format") } diff --git a/types/types.go b/types/types.go index cec01377dd..72d9ab314d 100644 --- a/types/types.go +++ b/types/types.go @@ -4,7 +4,6 @@ import ( "context" "io" - registryclient "github.com/docker/cli/cli/registry/client" "github.com/docker/docker/api/types" ver "github.com/hashicorp/go-version" ) @@ -36,7 +35,6 @@ type ContainerizedClient interface { out OutStream, authConfig *types.AuthConfig, healthfn func(context.Context) error) error - GetEngineVersions(ctx context.Context, registryClient registryclient.RegistryClient, currentVersion, imageName string) (AvailableVersions, error) } // EngineInitOptions contains the configuration settings