From c12e23a4c1f2811d06f4ba60229963b9b009bfa9 Mon Sep 17 00:00:00 2001 From: Daniel Hiltgen Date: Fri, 28 Sep 2018 14:06:28 -0700 Subject: [PATCH] Refine how metadata dir is handled This is a follow up PR to #1381 to address some of the review comments we didn't get to. Signed-off-by: Daniel Hiltgen --- cli/command/engine/activate.go | 19 +- cli/command/engine/activate_test.go | 2 +- cli/command/engine/check.go | 4 +- cli/command/engine/check_test.go | 18 +- cli/command/engine/update.go | 3 +- cli/command/engine/update_test.go | 9 +- internal/containerizedengine/types.go | 11 - internal/containerizedengine/update.go | 90 +++------ internal/containerizedengine/update_test.go | 213 +++++++++++--------- internal/versions/versions.go | 80 +++++--- internal/versions/versions_test.go | 47 ++++- types/types.go | 21 +- 12 files changed, 305 insertions(+), 212 deletions(-) diff --git a/cli/command/engine/activate.go b/cli/command/engine/activate.go index 8868931f43..a2f409c637 100644 --- a/cli/command/engine/activate.go +++ b/cli/command/engine/activate.go @@ -3,6 +3,7 @@ package engine import ( "context" "fmt" + "strings" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/formatter" @@ -57,7 +58,7 @@ https://hub.docker.com/ then specify the file with the '--license' flag. flags.StringVar(&options.licenseFile, "license", "", "License File") flags.StringVar(&options.version, "version", "", "Specify engine version (default is to use currently running version)") flags.StringVar(&options.registryPrefix, "registry-prefix", clitypes.RegistryPrefix, "Override the default location where engine images are pulled") - flags.StringVar(&options.image, "engine-image", clitypes.EnterpriseEngineImage, "Specify engine image") + flags.StringVar(&options.image, "engine-image", "", "Specify engine image") flags.StringVar(&options.format, "format", "", "Pretty-print licenses using a Go template") flags.BoolVar(&options.displayOnly, "display-only", false, "only display the available licenses and exit") flags.BoolVar(&options.quiet, "quiet", false, "Only display available licenses by ID") @@ -102,10 +103,24 @@ func runActivate(cli command.Cli, options activateOptions) error { if options.displayOnly { return nil } - if err = licenseutils.ApplyLicense(ctx, cli.Client(), license); err != nil { + dclient := cli.Client() + if err = licenseutils.ApplyLicense(ctx, dclient, license); err != nil { return err } + // Short circuit if the user didn't specify a version and we're already running enterprise + if options.version == "" { + serverVersion, err := dclient.ServerVersion(ctx) + if err != nil { + return err + } + if strings.Contains(strings.ToLower(serverVersion.Platform.Name), "enterprise") { + fmt.Fprintln(cli.Out(), "Successfully activated engine license on existing enterprise engine.") + return nil + } + options.version = serverVersion.Version + } + opts := clitypes.EngineInitOptions{ RegistryPrefix: options.registryPrefix, EngineImage: options.image, diff --git a/cli/command/engine/activate_test.go b/cli/command/engine/activate_test.go index 9924e49032..d930847e3a 100644 --- a/cli/command/engine/activate_test.go +++ b/cli/command/engine/activate_test.go @@ -53,7 +53,7 @@ func TestActivateExpiredLicenseDryRun(t *testing.T) { defer dir.Remove() filename := dir.Join("docker.lic") isRoot = func() bool { return true } - c := test.NewFakeCli(&verClient{client.Client{}, types.Version{}, nil}) + c := test.NewFakeCli(&verClient{client.Client{}, types.Version{}, nil, types.Info{}, nil}) c.SetContainerizedEngineClient( func(string) (clitypes.ContainerizedClient, error) { return &fakeContainerizedEngineClient{}, nil diff --git a/cli/command/engine/check.go b/cli/command/engine/check.go index 587d46adb9..2a41392117 100644 --- a/cli/command/engine/check.go +++ b/cli/command/engine/check.go @@ -16,6 +16,7 @@ import ( type checkOptions struct { registryPrefix string preReleases bool + engineImage string downgrades bool upgrades bool format string @@ -38,6 +39,7 @@ func newCheckForUpdatesCommand(dockerCli command.Cli) *cobra.Command { flags.StringVar(&options.registryPrefix, "registry-prefix", clitypes.RegistryPrefix, "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.StringVar(&options.engineImage, "engine-image", "", "Specify engine image (default uses the same image as currently running)") flags.BoolVar(&options.upgrades, "upgrades", true, "Report available upgrades") flags.StringVar(&options.format, "format", "", "Pretty-print updates using a Go template") flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display available versions") @@ -57,7 +59,7 @@ func runCheck(dockerCli command.Cli, options checkOptions) error { return err } - availVersions, err := versions.GetEngineVersions(ctx, dockerCli.RegistryClient(false), options.registryPrefix, serverVersion) + availVersions, err := versions.GetEngineVersions(ctx, dockerCli.RegistryClient(false), options.registryPrefix, options.engineImage, serverVersion.Version) if err != nil { return err } diff --git a/cli/command/engine/check_test.go b/cli/command/engine/check_test.go index 89450e67f9..d1bfd933fc 100644 --- a/cli/command/engine/check_test.go +++ b/cli/command/engine/check_test.go @@ -22,14 +22,20 @@ var ( type verClient struct { client.Client - ver types.Version - verErr error + ver types.Version + verErr error + info types.Info + infoErr error } func (c *verClient) ServerVersion(ctx context.Context) (types.Version, error) { return c.ver, c.verErr } +func (c *verClient) Info(ctx context.Context) (types.Info, error) { + return c.info, c.infoErr +} + type testRegistryClient struct { tags []string } @@ -53,26 +59,28 @@ func (c testRegistryClient) GetTags(ctx context.Context, ref reference.Named) ([ func TestCheckForUpdatesNoCurrentVersion(t *testing.T) { isRoot = func() bool { return true } - c := test.NewFakeCli(&verClient{client.Client{}, types.Version{}, nil}) + c := test.NewFakeCli(&verClient{client.Client{}, types.Version{}, nil, types.Info{}, nil}) c.SetRegistryClient(testRegistryClient{}) cmd := newCheckForUpdatesCommand(c) cmd.SilenceUsage = true cmd.SilenceErrors = true err := cmd.Execute() - assert.ErrorContains(t, err, "alformed version") + assert.ErrorContains(t, err, "no such file or directory") } func TestCheckForUpdatesGetEngineVersionsHappy(t *testing.T) { - c := test.NewFakeCli(&verClient{client.Client{}, types.Version{Version: "1.1.0"}, nil}) + c := test.NewFakeCli(&verClient{client.Client{}, types.Version{Version: "1.1.0"}, nil, types.Info{ServerVersion: "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.Flags().Set("engine-image", "engine-community") cmd.SilenceUsage = true cmd.SilenceErrors = true err := cmd.Execute() diff --git a/cli/command/engine/update.go b/cli/command/engine/update.go index 041ef0d763..e9079d97c2 100644 --- a/cli/command/engine/update.go +++ b/cli/command/engine/update.go @@ -25,7 +25,7 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command { flags := cmd.Flags() flags.StringVar(&options.EngineVersion, "version", "", "Specify engine version") - flags.StringVar(&options.EngineImage, "engine-image", "", "Specify engine image") + flags.StringVar(&options.EngineImage, "engine-image", "", "Specify engine image (default uses the same image as currently running)") flags.StringVar(&options.RegistryPrefix, "registry-prefix", clitypes.RegistryPrefix, "Override the current location where engine images are pulled") flags.StringVar(&options.sockPath, "containerd", "", "override default location of containerd endpoint") @@ -46,7 +46,6 @@ func runUpdate(dockerCli command.Cli, options extendedEngineInitOptions) error { if err != nil { return err } - if err := client.DoUpdate(ctx, options.EngineInitOptions, dockerCli.Out(), authConfig, func(ctx context.Context) error { client := dockerCli.Client() diff --git a/cli/command/engine/update_test.go b/cli/command/engine/update_test.go index 99dcbdc8ef..641b8458c7 100644 --- a/cli/command/engine/update_test.go +++ b/cli/command/engine/update_test.go @@ -4,7 +4,10 @@ import ( "fmt" "testing" + "github.com/docker/cli/internal/test" clitypes "github.com/docker/cli/types" + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" "gotest.tools/assert" ) @@ -22,14 +25,16 @@ func TestUpdateNoContainerd(t *testing.T) { } func TestUpdateHappy(t *testing.T) { - testCli.SetContainerizedEngineClient( + c := test.NewFakeCli(&verClient{client.Client{}, types.Version{Version: "1.1.0"}, nil, types.Info{ServerVersion: "1.1.0"}, nil}) + c.SetContainerizedEngineClient( func(string) (clitypes.ContainerizedClient, error) { return &fakeContainerizedEngineClient{}, nil }, ) - cmd := newUpdateCommand(testCli) + cmd := newUpdateCommand(c) cmd.Flags().Set("registry-prefix", clitypes.RegistryPrefix) cmd.Flags().Set("version", "someversion") + cmd.Flags().Set("engine-image", "someimage") err := cmd.Execute() assert.NilError(t, err) } diff --git a/internal/containerizedengine/types.go b/internal/containerizedengine/types.go index 017e9e7ed4..6fd5c5bb02 100644 --- a/internal/containerizedengine/types.go +++ b/internal/containerizedengine/types.go @@ -12,10 +12,6 @@ import ( const ( containerdSockPath = "/run/containerd/containerd.sock" engineNamespace = "com.docker" - - // runtimeMetadataName is the name of the runtime metadata file - // When stored as a label on the container it is prefixed by "com.docker." - runtimeMetadataName = "distribution_based_engine" ) var ( @@ -51,10 +47,3 @@ type containerdClient interface { 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 0d5bab225e..4b029165f3 100644 --- a/internal/containerizedengine/update.go +++ b/internal/containerizedengine/update.go @@ -4,9 +4,6 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" - "os" - "path/filepath" "strings" "github.com/containerd/containerd" @@ -14,6 +11,7 @@ import ( "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/images" "github.com/containerd/containerd/namespaces" + "github.com/docker/cli/internal/versions" clitypes "github.com/docker/cli/types" "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" @@ -26,6 +24,25 @@ import ( func (c *baseClient) ActivateEngine(ctx context.Context, opts clitypes.EngineInitOptions, out clitypes.OutStream, authConfig *types.AuthConfig, healthfn func(context.Context) error) error { + // If the user didn't specify an image, determine the correct enterprise image to use + if opts.EngineImage == "" { + localMetadata, err := versions.GetCurrentRuntimeMetadata(opts.RuntimeMetadataDir) + if err != nil { + return errors.Wrap(err, "unable to determine the installed engine version. Specify which engine image to update with --engine-image") + } + + engineImage := localMetadata.EngineImage + if engineImage == clitypes.EnterpriseEngineImage || engineImage == clitypes.CommunityEngineImage { + opts.EngineImage = clitypes.EnterpriseEngineImage + } else { + // Chop off the standard prefix and retain any trailing OS specific image details + // e.g., engine-community-dm -> engine-enterprise-dm + engineImage = strings.TrimPrefix(engineImage, clitypes.EnterpriseEngineImage) + engineImage = strings.TrimPrefix(engineImage, clitypes.CommunityEngineImage) + opts.EngineImage = clitypes.EnterpriseEngineImage + engineImage + } + } + ctx = namespaces.WithNamespace(ctx, engineNamespace) return c.DoUpdate(ctx, opts, out, authConfig, healthfn) } @@ -43,19 +60,14 @@ func (c *baseClient) DoUpdate(ctx context.Context, opts clitypes.EngineInitOptio // `docker engine update` return fmt.Errorf("pick the version you want to update to with --version") } - - localMetadata, err := c.GetCurrentRuntimeMetadata(ctx, "") - if err == nil { - if opts.EngineImage == "" { - if strings.Contains(strings.ToLower(localMetadata.Platform), "community") { - opts.EngineImage = clitypes.CommunityEngineImage - } else { - opts.EngineImage = clitypes.EnterpriseEngineImage - } - } - } + var localMetadata *clitypes.RuntimeMetadata if opts.EngineImage == "" { - return fmt.Errorf("unable to determine the installed engine version. Specify which engine image to update with --engine-image set to 'engine-community' or 'engine-enterprise'") + var err error + localMetadata, err = versions.GetCurrentRuntimeMetadata(opts.RuntimeMetadataDir) + if err != nil { + 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'") + } + opts.EngineImage = localMetadata.EngineImage } imageName := fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, opts.EngineImage, opts.EngineVersion) @@ -78,7 +90,6 @@ func (c *baseClient) DoUpdate(ctx context.Context, opts clitypes.EngineInitOptio 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\". Refer to %s for update instructions.\n\n", newMetadata.Platform, getReleaseNotesURL(imageName)) @@ -89,50 +100,13 @@ func (c *baseClient) DoUpdate(ctx context.Context, opts clitypes.EngineInitOptio return err } - return c.WriteRuntimeMetadata("", 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 -} - -// WriteRuntimeMetadata stores the metadata on the local system -func (c *baseClient) WriteRuntimeMetadata(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 - } - - os.Remove(filename) - return ioutil.WriteFile(filename, data, 0644) + return versions.WriteRuntimeMetadata(opts.RuntimeMetadataDir, newMetadata) } // 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 +func (c *baseClient) PreflightCheck(ctx context.Context, image containerd.Image) (*clitypes.RuntimeMetadata, error) { + var metadata clitypes.RuntimeMetadata ic, err := image.Config(ctx) if err != nil { return nil, err @@ -156,9 +130,9 @@ func (c *baseClient) PreflightCheck(ctx context.Context, image containerd.Image) return nil, fmt.Errorf("unknown image %s config media type %s", image.Name(), ic.MediaType) } - metadataString, ok := config.Labels["com.docker."+runtimeMetadataName] + metadataString, ok := config.Labels["com.docker."+clitypes.RuntimeMetadataName] if !ok { - return nil, fmt.Errorf("image %s does not contain runtime metadata label %s", image.Name(), runtimeMetadataName) + return nil, fmt.Errorf("image %s does not contain runtime metadata label %s", image.Name(), clitypes.RuntimeMetadataName) } err = json.Unmarshal([]byte(metadataString), &metadata) if err != nil { diff --git a/internal/containerizedengine/update_test.go b/internal/containerizedengine/update_test.go index 75615df271..47299a5efa 100644 --- a/internal/containerizedengine/update_test.go +++ b/internal/containerizedengine/update_test.go @@ -6,13 +6,13 @@ import ( "fmt" "io/ioutil" "os" - "path/filepath" "testing" "github.com/containerd/containerd" "github.com/containerd/containerd/cio" "github.com/containerd/containerd/errdefs" "github.com/docker/cli/cli/command" + "github.com/docker/cli/internal/versions" clitypes "github.com/docker/cli/types" "github.com/docker/docker/api/types" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -23,6 +23,51 @@ func healthfnHappy(ctx context.Context) error { return nil } +func TestActivateImagePermutations(t *testing.T) { + ctx := context.Background() + lookedup := "not called yet" + expectedError := fmt.Errorf("expected error") + client := baseClient{ + cclient: &fakeContainerdClient{ + getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) { + lookedup = ref + return nil, expectedError + }, + }, + } + tmpdir, err := ioutil.TempDir("", "enginedir") + assert.NilError(t, err) + defer os.RemoveAll(tmpdir) + metadata := clitypes.RuntimeMetadata{EngineImage: clitypes.EnterpriseEngineImage} + err = versions.WriteRuntimeMetadata(tmpdir, &metadata) + assert.NilError(t, err) + + opts := clitypes.EngineInitOptions{ + EngineVersion: "engineversiongoeshere", + RegistryPrefix: "registryprefixgoeshere", + ConfigFile: "/tmp/configfilegoeshere", + RuntimeMetadataDir: tmpdir, + } + + err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy) + assert.ErrorContains(t, err, expectedError.Error()) + assert.Equal(t, lookedup, fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, clitypes.EnterpriseEngineImage, opts.EngineVersion)) + + metadata = clitypes.RuntimeMetadata{EngineImage: clitypes.CommunityEngineImage} + err = versions.WriteRuntimeMetadata(tmpdir, &metadata) + assert.NilError(t, err) + err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy) + assert.ErrorContains(t, err, expectedError.Error()) + assert.Equal(t, lookedup, fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, clitypes.EnterpriseEngineImage, opts.EngineVersion)) + + metadata = clitypes.RuntimeMetadata{EngineImage: clitypes.CommunityEngineImage + "-dm"} + err = versions.WriteRuntimeMetadata(tmpdir, &metadata) + assert.NilError(t, err) + err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy) + assert.ErrorContains(t, err, expectedError.Error()) + assert.Equal(t, lookedup, fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, clitypes.EnterpriseEngineImage+"-dm", opts.EngineVersion)) +} + func TestActivateConfigFailure(t *testing.T) { ctx := context.Background() registryPrefix := "registryprefixgoeshere" @@ -55,14 +100,21 @@ func TestActivateConfigFailure(t *testing.T) { }, }, } + tmpdir, err := ioutil.TempDir("", "engindir") + assert.NilError(t, err) + defer os.RemoveAll(tmpdir) + metadata := clitypes.RuntimeMetadata{EngineImage: clitypes.CommunityEngineImage} + err = versions.WriteRuntimeMetadata(tmpdir, &metadata) + assert.NilError(t, err) opts := clitypes.EngineInitOptions{ - EngineVersion: "engineversiongoeshere", - RegistryPrefix: "registryprefixgoeshere", - ConfigFile: "/tmp/configfilegoeshere", - EngineImage: clitypes.EnterpriseEngineImage, + EngineVersion: "engineversiongoeshere", + RegistryPrefix: "registryprefixgoeshere", + ConfigFile: "/tmp/configfilegoeshere", + EngineImage: clitypes.EnterpriseEngineImage, + RuntimeMetadataDir: tmpdir, } - err := client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy) + err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy) assert.ErrorContains(t, err, "config lookup failure") } @@ -90,38 +142,60 @@ func TestActivateDoUpdateFail(t *testing.T) { }, }, } + tmpdir, err := ioutil.TempDir("", "enginedir") + assert.NilError(t, err) + defer os.RemoveAll(tmpdir) + metadata := clitypes.RuntimeMetadata{EngineImage: clitypes.CommunityEngineImage} + err = versions.WriteRuntimeMetadata(tmpdir, &metadata) + assert.NilError(t, err) opts := clitypes.EngineInitOptions{ - EngineVersion: "engineversiongoeshere", - RegistryPrefix: "registryprefixgoeshere", - ConfigFile: "/tmp/configfilegoeshere", - EngineImage: clitypes.EnterpriseEngineImage, + EngineVersion: "engineversiongoeshere", + RegistryPrefix: "registryprefixgoeshere", + ConfigFile: "/tmp/configfilegoeshere", + EngineImage: clitypes.EnterpriseEngineImage, + RuntimeMetadataDir: tmpdir, } - err := client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy) + 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") } func TestDoUpdateNoVersion(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "enginedir") + assert.NilError(t, err) + defer os.RemoveAll(tmpdir) + metadata := clitypes.RuntimeMetadata{EngineImage: clitypes.EnterpriseEngineImage} + err = versions.WriteRuntimeMetadata(tmpdir, &metadata) + assert.NilError(t, err) ctx := context.Background() opts := clitypes.EngineInitOptions{ - EngineVersion: "", - RegistryPrefix: "registryprefixgoeshere", - ConfigFile: "/tmp/configfilegoeshere", - EngineImage: clitypes.EnterpriseEngineImage, + EngineVersion: "", + RegistryPrefix: "registryprefixgoeshere", + ConfigFile: "/tmp/configfilegoeshere", + EngineImage: clitypes.EnterpriseEngineImage, + RuntimeMetadataDir: tmpdir, } + client := baseClient{} - err := client.DoUpdate(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy) + err = client.DoUpdate(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy) assert.ErrorContains(t, err, "pick the version you") } func TestDoUpdateImageMiscError(t *testing.T) { ctx := context.Background() + tmpdir, err := ioutil.TempDir("", "enginedir") + assert.NilError(t, err) + defer os.RemoveAll(tmpdir) + metadata := clitypes.RuntimeMetadata{EngineImage: clitypes.EnterpriseEngineImage} + err = versions.WriteRuntimeMetadata(tmpdir, &metadata) + assert.NilError(t, err) opts := clitypes.EngineInitOptions{ - EngineVersion: "engineversiongoeshere", - RegistryPrefix: "registryprefixgoeshere", - ConfigFile: "/tmp/configfilegoeshere", - EngineImage: "testnamegoeshere", + EngineVersion: "engineversiongoeshere", + RegistryPrefix: "registryprefixgoeshere", + ConfigFile: "/tmp/configfilegoeshere", + EngineImage: "testnamegoeshere", + RuntimeMetadataDir: tmpdir, } client := baseClient{ cclient: &fakeContainerdClient{ @@ -131,18 +205,26 @@ func TestDoUpdateImageMiscError(t *testing.T) { }, }, } - err := client.DoUpdate(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy) + + err = client.DoUpdate(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy) assert.ErrorContains(t, err, "check for image") assert.ErrorContains(t, err, "something went wrong") } func TestDoUpdatePullFail(t *testing.T) { ctx := context.Background() + tmpdir, err := ioutil.TempDir("", "enginedir") + assert.NilError(t, err) + defer os.RemoveAll(tmpdir) + metadata := clitypes.RuntimeMetadata{EngineImage: clitypes.EnterpriseEngineImage} + err = versions.WriteRuntimeMetadata(tmpdir, &metadata) + assert.NilError(t, err) opts := clitypes.EngineInitOptions{ - EngineVersion: "engineversiongoeshere", - RegistryPrefix: "registryprefixgoeshere", - ConfigFile: "/tmp/configfilegoeshere", - EngineImage: "testnamegoeshere", + EngineVersion: "engineversiongoeshere", + RegistryPrefix: "registryprefixgoeshere", + ConfigFile: "/tmp/configfilegoeshere", + EngineImage: "testnamegoeshere", + RuntimeMetadataDir: tmpdir, } client := baseClient{ cclient: &fakeContainerdClient{ @@ -155,7 +237,8 @@ func TestDoUpdatePullFail(t *testing.T) { }, }, } - err := client.DoUpdate(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy) + + err = client.DoUpdate(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy) assert.ErrorContains(t, err, "unable to pull") assert.ErrorContains(t, err, "pull failure") } @@ -186,78 +269,26 @@ func TestActivateDoUpdateVerifyImageName(t *testing.T) { }, }, } + tmpdir, err := ioutil.TempDir("", "enginedir") + assert.NilError(t, err) + defer os.RemoveAll(tmpdir) + metadata := clitypes.RuntimeMetadata{EngineImage: clitypes.EnterpriseEngineImage} + err = versions.WriteRuntimeMetadata(tmpdir, &metadata) + assert.NilError(t, err) + opts := clitypes.EngineInitOptions{ - EngineVersion: "engineversiongoeshere", - RegistryPrefix: "registryprefixgoeshere", - ConfigFile: "/tmp/configfilegoeshere", + EngineVersion: "engineversiongoeshere", + RegistryPrefix: "registryprefixgoeshere", + EngineImage: "testnamegoeshere", + ConfigFile: "/tmp/configfilegoeshere", + RuntimeMetadataDir: tmpdir, } - tmpdir, err := ioutil.TempDir("", "docker-root") - assert.NilError(t, err) - defer os.RemoveAll(tmpdir) - tmpDockerRoot := defaultDockerRoot - defaultDockerRoot = tmpdir - defer func() { - defaultDockerRoot = tmpDockerRoot - }() - metadata := RuntimeMetadata{Platform: "platformgoeshere"} - err = client.WriteRuntimeMetadata(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) + expectedImage := fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, opts.EngineImage, opts.EngineVersion) assert.Assert(t, requestedImage == expectedImage, "%s != %s", requestedImage, expectedImage) - - // Redo with enterprise set - metadata = RuntimeMetadata{Platform: "Docker Engine - Enterprise"} - err = client.WriteRuntimeMetadata(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(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) { diff --git a/internal/versions/versions.go b/internal/versions/versions.go index 160d00c55c..9e83bb371a 100644 --- a/internal/versions/versions.go +++ b/internal/versions/versions.go @@ -2,23 +2,38 @@ package versions import ( "context" + "encoding/json" + "io/ioutil" + "os" "path" + "path/filepath" "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" ) +const ( + // defaultRuntimeMetadataDir is the location where the metadata file is stored + defaultRuntimeMetadataDir = "/var/lib/docker-engine" +) + // GetEngineVersions reports the versions of the engine that are available -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) +func GetEngineVersions(ctx context.Context, registryClient registryclient.RegistryClient, registryPrefix, imageName, versionString string) (clitypes.AvailableVersions, error) { + + if imageName == "" { + var err error + localMetadata, err := GetCurrentRuntimeMetadata("") + if err != nil { + return clitypes.AvailableVersions{}, err + } + imageName = localMetadata.EngineImage + } + imageRef, err := reference.ParseNormalizedNamed(path.Join(registryPrefix, imageName)) if err != nil { return clitypes.AvailableVersions{}, err } @@ -28,25 +43,7 @@ func GetEngineVersions(ctx context.Context, registryClient registryclient.Regist return clitypes.AvailableVersions{}, err } - return parseTags(tags, serverVersion.Version) -} - -func getEngineImage(registryPrefix string, serverVersion types.Version) string { - platform := strings.ToLower(serverVersion.Platform.Name) - if platform != "" { - if strings.Contains(platform, "enterprise") { - return path.Join(registryPrefix, clitypes.EnterpriseEngineImage) - } - return path.Join(registryPrefix, clitypes.CommunityEngineImage) - } - - // TODO This check is only applicable for early 18.09 builds that had some packaging bugs - // and can be removed once we're no longer testing with them - if strings.Contains(serverVersion.Version, "ee") { - return path.Join(registryPrefix, clitypes.EnterpriseEngineImage) - } - - return path.Join(registryPrefix, clitypes.CommunityEngineImage) + return parseTags(tags, versionString) } func parseTags(tags []string, currentVersion string) (clitypes.AvailableVersions, error) { @@ -93,3 +90,38 @@ func parseTags(tags []string, currentVersion string) (clitypes.AvailableVersions ret.Upgrades = upgrades return ret, nil } + +// GetCurrentRuntimeMetadata loads the current daemon runtime metadata information from the local host +func GetCurrentRuntimeMetadata(metadataDir string) (*clitypes.RuntimeMetadata, error) { + if metadataDir == "" { + metadataDir = defaultRuntimeMetadataDir + } + filename := filepath.Join(metadataDir, clitypes.RuntimeMetadataName+".json") + + data, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + var res clitypes.RuntimeMetadata + err = json.Unmarshal(data, &res) + if err != nil { + return nil, errors.Wrapf(err, "malformed runtime metadata file %s", filename) + } + return &res, nil +} + +// WriteRuntimeMetadata stores the metadata on the local system +func WriteRuntimeMetadata(metadataDir string, metadata *clitypes.RuntimeMetadata) error { + if metadataDir == "" { + metadataDir = defaultRuntimeMetadataDir + } + filename := filepath.Join(metadataDir, clitypes.RuntimeMetadataName+".json") + + data, err := json.Marshal(metadata) + if err != nil { + return err + } + + os.Remove(filename) + return ioutil.WriteFile(filename, data, 0644) +} diff --git a/internal/versions/versions_test.go b/internal/versions/versions_test.go index 1828ad0249..3469d8bbc3 100644 --- a/internal/versions/versions_test.go +++ b/internal/versions/versions_test.go @@ -1,22 +1,15 @@ package versions import ( - "context" + "io/ioutil" + "os" + "path/filepath" "testing" - "github.com/docker/docker/api/types" + clitypes "github.com/docker/cli/types" "gotest.tools/assert" ) -func TestGetEngineVersionsBadImage(t *testing.T) { - ctx := context.Background() - - registryPrefix := "this is an illegal image $%^&" - currentVersion := types.Version{Version: "currentversiongoeshere"} - _, err := GetEngineVersions(ctx, nil, registryPrefix, currentVersion) - assert.ErrorContains(t, err, "invalid reference format") -} - func TestParseTagsSimple(t *testing.T) { tags := []string{"1.0.0", "1.1.2", "1.1.1", "1.2.2"} currentVersion := "1.1.0" @@ -78,3 +71,35 @@ func TestParseBadCurrent2(t *testing.T) { _, err := parseTags(tags, currentVersion) assert.ErrorContains(t, err, "failed to parse existing") } + +func TestGetCurrentRuntimeMetadataNotPresent(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "docker-root") + assert.NilError(t, err) + defer os.RemoveAll(tmpdir) + _, err = GetCurrentRuntimeMetadata(tmpdir) + assert.ErrorType(t, err, os.IsNotExist) +} + +func TestGetCurrentRuntimeMetadataBadJson(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "docker-root") + assert.NilError(t, err) + defer os.RemoveAll(tmpdir) + filename := filepath.Join(tmpdir, clitypes.RuntimeMetadataName+".json") + err = ioutil.WriteFile(filename, []byte("not json"), 0644) + assert.NilError(t, err) + _, err = GetCurrentRuntimeMetadata(tmpdir) + assert.ErrorContains(t, err, "malformed runtime metadata file") +} + +func TestGetCurrentRuntimeMetadataHappyPath(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "docker-root") + assert.NilError(t, err) + defer os.RemoveAll(tmpdir) + metadata := clitypes.RuntimeMetadata{Platform: "platformgoeshere"} + err = WriteRuntimeMetadata(tmpdir, &metadata) + assert.NilError(t, err) + + res, err := GetCurrentRuntimeMetadata(tmpdir) + assert.NilError(t, err) + assert.Equal(t, res.Platform, "platformgoeshere") +} diff --git a/types/types.go b/types/types.go index 61cbcd4b55..b10a01b001 100644 --- a/types/types.go +++ b/types/types.go @@ -20,6 +20,10 @@ const ( // ReleaseNotePrefix is where to point users to for release notes ReleaseNotePrefix = "https://docs.docker.com/releasenotes" + + // RuntimeMetadataName is the name of the runtime metadata file + // When stored as a label on the container it is prefixed by "com.docker." + RuntimeMetadataName = "distribution_based_engine" ) // ContainerizedClient can be used to manage the lifecycle of @@ -41,10 +45,11 @@ type ContainerizedClient interface { // EngineInitOptions contains the configuration settings // use during initialization of a containerized docker engine type EngineInitOptions struct { - RegistryPrefix string - EngineImage string - EngineVersion string - ConfigFile string + RegistryPrefix string + EngineImage string + EngineVersion string + ConfigFile string + RuntimeMetadataDir string } // AvailableVersions groups the available versions which were discovered @@ -75,3 +80,11 @@ type OutStream interface { FD() uintptr IsTerminal() bool } + +// RuntimeMetadata holds platform information about the daemon +type RuntimeMetadata struct { + Platform string `json:"platform"` + ContainerdMinVersion string `json:"containerd_min_version"` + Runtime string `json:"runtime"` + EngineImage string `json:"engine_image"` +}