From c3605e497b19315e82fee5a63876a1e93519c96d Mon Sep 17 00:00:00 2001 From: Daniel Hiltgen Date: Mon, 10 Sep 2018 14:31:56 -0700 Subject: [PATCH 1/4] Vendor bump of licensing lib Signed-off-by: Daniel Hiltgen Upstream-commit: 41910b6d68e3ccb79d322188e516e0ef3ccf1304 Component: cli --- components/cli/vendor.conf | 2 +- .../github.com/docker/licensing/client.go | 6 +++++ .../docker/licensing/model/subscriptions.go | 2 +- .../github.com/docker/licensing/storage.go | 27 ++++++++++++++----- 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/components/cli/vendor.conf b/components/cli/vendor.conf index 7a84ad4fdd..91932df6cc 100755 --- a/components/cli/vendor.conf +++ b/components/cli/vendor.conf @@ -22,7 +22,7 @@ github.com/docker/go-events 9461782956ad83b30282bf90e31fa6a70c255ba9 github.com/docker/go-metrics d466d4f6fd960e01820085bd7e1a24426ee7ef18 github.com/docker/go-units 47565b4f722fb6ceae66b95f853feed578a4a51c # v0.3.3 github.com/docker/libtrust 9cbd2a1374f46905c68a4eb3694a130610adc62a -github.com/docker/licensing 5c4c7b4 +github.com/docker/licensing f2eae57157a06681b024f1690923d03e414179a0 github.com/docker/swarmkit cfa742c8abe6f8e922f6e4e920153c408e7d9c3b github.com/flynn-archive/go-shlex 3f9db97f856818214da2e1057f8ad84803971cff github.com/ghodss/yaml 0ca9ea5df5451ffdf184b4428c902747c2c11cd7 # v1.0.0 diff --git a/components/cli/vendor/github.com/docker/licensing/client.go b/components/cli/vendor/github.com/docker/licensing/client.go index 8cf9d888a8..62a2cd524d 100644 --- a/components/cli/vendor/github.com/docker/licensing/client.go +++ b/components/cli/vendor/github.com/docker/licensing/client.go @@ -1,6 +1,7 @@ package licensing import ( + "bytes" "context" "encoding/base64" "encoding/json" @@ -34,6 +35,7 @@ type Client interface { ParseLicense(license []byte) (parsedLicense *model.IssuedLicense, err error) StoreLicense(ctx context.Context, dclnt WrappedDockerClient, licenses *model.IssuedLicense, localRootDir string) error LoadLocalLicense(ctx context.Context, dclnt WrappedDockerClient) (*model.Subscription, error) + SummarizeLicense(res *model.CheckResponse, keyID string) *model.Subscription } func (c *client) LoginViaAuth(ctx context.Context, username, password string) (string, error) { @@ -185,6 +187,10 @@ func (c *client) DownloadLicenseFromHub(ctx context.Context, authToken, subscrip func (c *client) ParseLicense(license []byte) (*model.IssuedLicense, error) { parsedLicense := &model.IssuedLicense{} + // The file may contain a leading BOM, which will choke the + // json deserializer. + license = bytes.Trim(license, "\xef\xbb\xbf") + if err := json.Unmarshal(license, &parsedLicense); err != nil { return nil, errors.WithMessage(err, "failed to parse license") } diff --git a/components/cli/vendor/github.com/docker/licensing/model/subscriptions.go b/components/cli/vendor/github.com/docker/licensing/model/subscriptions.go index 7e5f883368..95dadb8d48 100644 --- a/components/cli/vendor/github.com/docker/licensing/model/subscriptions.go +++ b/components/cli/vendor/github.com/docker/licensing/model/subscriptions.go @@ -34,7 +34,7 @@ type Subscription struct { } func (s *Subscription) String() string { - storeURL := "https://store.docker.com" + storeURL := "https://docker.com/licensing" var nameMsg, expirationMsg, statusMsg string switch s.State { diff --git a/components/cli/vendor/github.com/docker/licensing/storage.go b/components/cli/vendor/github.com/docker/licensing/storage.go index d4fe01c379..0358fad521 100644 --- a/components/cli/vendor/github.com/docker/licensing/storage.go +++ b/components/cli/vendor/github.com/docker/licensing/storage.go @@ -87,18 +87,23 @@ func (c *client) LoadLocalLicense(ctx context.Context, clnt WrappedDockerClient) licenseData, err = readLicenseFromHost(ctx, info.DockerRootDir) } else { // Load the latest license index - latestVersion, err := getLatestNamedConfig(clnt, licenseNamePrefix) + var latestVersion int + latestVersion, err = getLatestNamedConfig(clnt, licenseNamePrefix) if err != nil { if strings.Contains(err.Error(), "not a swarm manager.") { return nil, ErrWorkerNode } return nil, fmt.Errorf("unable to get latest license version: %s", err) } - cfg, _, err := clnt.ConfigInspectWithRaw(ctx, fmt.Sprintf("%s-%d", licenseNamePrefix, latestVersion)) - if err != nil { - return nil, fmt.Errorf("unable to load license from swarm config: %s", err) + if latestVersion >= 0 { + cfg, _, err := clnt.ConfigInspectWithRaw(ctx, fmt.Sprintf("%s-%d", licenseNamePrefix, latestVersion)) + if err != nil { + return nil, fmt.Errorf("unable to load license from swarm config: %s", err) + } + licenseData = cfg.Spec.Data + } else { + licenseData, err = readLicenseFromHost(ctx, info.DockerRootDir) } - licenseData = cfg.Spec.Data } if err != nil { if os.IsNotExist(err) { @@ -115,6 +120,10 @@ func (c *client) LoadLocalLicense(ctx context.Context, clnt WrappedDockerClient) if err != nil { return nil, err } + return checkResponseToSubscription(checkResponse, parsedLicense.KeyID), nil +} + +func checkResponseToSubscription(checkResponse *model.CheckResponse, keyID string) *model.Subscription { // TODO - this translation still needs some work // Primary missing piece is how to distinguish from basic, vs std/advanced @@ -144,7 +153,7 @@ func (c *client) LoadLocalLicense(ctx context.Context, clnt WrappedDockerClient) // Translate the legacy structure into the new Subscription fields return &model.Subscription{ // Name - ID: parsedLicense.KeyID, // This is not actually the same, but is unique + ID: keyID, // This is not actually the same, but is unique // DockerID ProductID: productID, ProductRatePlan: ratePlan, @@ -159,7 +168,11 @@ func (c *client) LoadLocalLicense(ctx context.Context, clnt WrappedDockerClient) Value: checkResponse.MaxEngines, }, }, - }, nil + } +} + +func (c *client) SummarizeLicense(checkResponse *model.CheckResponse, keyID string) *model.Subscription { + return checkResponseToSubscription(checkResponse, keyID) } // getLatestNamedConfig looks for versioned instances of configs with the From cb4c2a9e5ed758c3df39059da5fbbf40abd26ebe Mon Sep 17 00:00:00 2001 From: Daniel Hiltgen Date: Mon, 10 Sep 2018 14:31:45 -0700 Subject: [PATCH 2/4] Expose licensing details before loading Help the user understand which license they're about to load in case they have multiple licenses they need to figure out. Signed-off-by: Daniel Hiltgen Upstream-commit: 5a97a93ae10218557d737d06d1fd6c95bd690b5d Component: cli --- components/cli/cli/command/engine/activate.go | 11 +++-- .../cli/cli/command/engine/activate_test.go | 43 +++++++++++++++++-- .../expired-license-display-only.golden | 1 + .../cli/internal/licenseutils/client_test.go | 8 ++++ components/cli/internal/licenseutils/utils.go | 41 ++++++++++++------ 5 files changed, 84 insertions(+), 20 deletions(-) create mode 100644 components/cli/cli/command/engine/testdata/expired-license-display-only.golden diff --git a/components/cli/cli/command/engine/activate.go b/components/cli/cli/command/engine/activate.go index ba98eed72d..8868931f43 100644 --- a/components/cli/cli/command/engine/activate.go +++ b/components/cli/cli/command/engine/activate.go @@ -89,14 +89,19 @@ func runActivate(cli command.Cli, options activateOptions) error { if license, err = getLicenses(ctx, authConfig, cli, options); err != nil { return err } - if options.displayOnly { - return nil - } } else { if license, err = licenseutils.LoadLocalIssuedLicense(ctx, options.licenseFile); err != nil { return err } } + summary, err := licenseutils.GetLicenseSummary(ctx, *license) + if err != nil { + return err + } + fmt.Fprintf(cli.Out(), "License: %s\n", summary) + if options.displayOnly { + return nil + } if err = licenseutils.ApplyLicense(ctx, cli.Client(), license); err != nil { return err } diff --git a/components/cli/cli/command/engine/activate_test.go b/components/cli/cli/command/engine/activate_test.go index 6fe552c7da..843b64b5aa 100644 --- a/components/cli/cli/command/engine/activate_test.go +++ b/components/cli/cli/command/engine/activate_test.go @@ -2,15 +2,27 @@ package engine import ( "fmt" + "io/ioutil" + "os" + "path/filepath" "testing" - "github.com/docker/cli/types" + "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" + "gotest.tools/golden" +) + +const ( + // nolint: lll + expiredLicense = `{"key_id":"irlYm3b9fdD8hMUXjazF39im7VQSSbAm9tfHK8cKUxJt","private_key":"aH5tTRDAVJpCRS2CRetTQVXIKgWUPfoCHODhDvNPvAbz","authorization":"ewogICAicGF5bG9hZCI6ICJleUpsZUhCcGNtRjBhVzl1SWpvaU1qQXhPQzB3TXkweE9GUXdOem93TURvd01Gb2lMQ0owYjJ0bGJpSTZJbkZtTVMxMlVtRmtialp5YjFaMldXdHJlVXN4VFdKMGNGUmpXR1ozVjA4MVRWZFFTM2cwUnpJd2NIYzlJaXdpYldGNFJXNW5hVzVsY3lJNk1Td2ljMk5oYm01cGJtZEZibUZpYkdWa0lqcDBjblZsTENKc2FXTmxibk5sVkhsd1pTSTZJazltWm14cGJtVWlMQ0owYVdWeUlqb2lVSEp2WkhWamRHbHZiaUo5IiwKICAgInNpZ25hdHVyZXMiOiBbCiAgICAgIHsKICAgICAgICAgImhlYWRlciI6IHsKICAgICAgICAgICAgImp3ayI6IHsKICAgICAgICAgICAgICAgImUiOiAiQVFBQiIsCiAgICAgICAgICAgICAgICJrZXlJRCI6ICJKN0xEOjY3VlI6TDVIWjpVN0JBOjJPNEc6NEFMMzpPRjJOOkpIR0I6RUZUSDo1Q1ZROk1GRU86QUVJVCIsCiAgICAgICAgICAgICAgICJraWQiOiAiSjdMRDo2N1ZSOkw1SFo6VTdCQToyTzRHOjRBTDM6T0YyTjpKSEdCOkVGVEg6NUNWUTpNRkVPOkFFSVQiLAogICAgICAgICAgICAgICAia3R5IjogIlJTQSIsCiAgICAgICAgICAgICAgICJuIjogInlkSXktbFU3bzdQY2VZLTQtcy1DUTVPRWdDeUY4Q3hJY1FJV3VLODRwSWlaY2lZNjczMHlDWW53TFNLVGx3LVU2VUNfUVJlV1Jpb01OTkU1RHM1VFlFWGJHRzZvbG0ycWRXYkJ3Y0NnLTJVVUhfT2NCOVd1UDZnUlBIcE1GTXN4RHpXd3ZheThKVXVIZ1lVTFVwbTFJdi1tcTdscDVuUV9SeHJUMEtaUkFRVFlMRU1FZkd3bTNoTU9fZ2VMUFMtaGdLUHRJSGxrZzZfV2NveFRHb0tQNzlkX3dhSFl4R05sN1doU25laUJTeGJwYlFBS2syMWxnNzk4WGI3dlp5RUFURE1yUlI5TWVFNkFkajVISnBZM0NveVJBUENtYUtHUkNLNHVvWlNvSXUwaEZWbEtVUHliYncwMDBHTy13YTJLTjhVd2dJSW0waTVJMXVXOUdrcTR6akJ5NXpoZ3F1VVhiRzliV1BBT1lycTVRYTgxRHhHY0JsSnlIWUFwLUREUEU5VEdnNHpZbVhqSm54WnFIRWR1R3FkZXZaOFhNSTB1a2ZrR0lJMTR3VU9pTUlJSXJYbEVjQmZfNDZJOGdRV0R6eHljWmVfSkdYLUxBdWF5WHJ5clVGZWhWTlVkWlVsOXdYTmFKQi1rYUNxejVRd2FSOTNzR3ctUVNmdEQwTnZMZTdDeU9ILUU2dmc2U3RfTmVUdmd2OFluaENpWElsWjhIT2ZJd05lN3RFRl9VY3o1T2JQeWttM3R5bHJOVWp0MFZ5QW10dGFjVkkyaUdpaGNVUHJtazRsVklaN1ZEX0xTVy1pN3lvU3VydHBzUFhjZTJwS0RJbzMwbEpHaE9fM0tVbWwyU1VaQ3F6SjF5RW1LcHlzSDVIRFc5Y3NJRkNBM2RlQWpmWlV2TjdVIgogICAgICAgICAgICB9LAogICAgICAgICAgICAiYWxnIjogIlJTMjU2IgogICAgICAgICB9LAogICAgICAgICAic2lnbmF0dXJlIjogIm5saTZIdzRrbW5KcTBSUmRXaGVfbkhZS2VJLVpKenM1U0d5SUpDakh1dWtnVzhBYklpVzFZYWJJR2NqWUt0QTY4dWN6T1hyUXZreGxWQXJLSlgzMDJzN0RpbzcxTlNPRzJVcnhsSjlibDFpd0F3a3ZyTEQ2T0p5MGxGLVg4WnRabXhPVmNQZmwzcmJwZFQ0dnlnWTdNcU1QRXdmb0IxTmlWZDYyZ1cxU2NSREZZcWw3R0FVaFVKNkp4QU15VzVaOXl5YVE0NV8wd0RMUk5mRjA5YWNXeVowTjRxVS1hZjhrUTZUUWZUX05ERzNCR3pRb2V3cHlEajRiMFBHb0diOFhLdDlwekpFdEdxM3lQM25VMFFBbk90a2gwTnZac1l1UFcyUnhDT3lRNEYzVlR3UkF2eF9HSTZrMVRpYmlKNnByUWluUy16Sjh6RE8zUjBuakE3OFBwNXcxcVpaUE9BdmtzZFNSYzJDcVMtcWhpTmF5YUhOVHpVNnpyOXlOZHR2S0o1QjNST0FmNUtjYXNiWURjTnVpeXBUNk90LUtqQ2I1dmYtWVpnc2FRNzJBdFBhSU4yeUpNREZHbmEwM0hpSjMxcTJRUlp5eTZrd3RYaGtwcDhTdEdIcHYxSWRaV09SVWttb0g5SFBzSGk4SExRLTZlM0tEY2x1RUQyMTNpZnljaVhtN0YzdHdaTTNHeDd1UXR1SldHaUlTZ2Z0QW9lVjZfUmI2VThkMmZxNzZuWHYxak5nckRRcE5waEZFd2tCdGRtZHZ2THByZVVYX3BWangza1AxN3pWbXFKNmNOOWkwWUc4WHg2VmRzcUxsRXUxQ2Rhd3Q0eko1M3VHMFlKTjRnUDZwc25yUS1uM0U1aFdlMDJ3d3dBZ3F3bGlPdmd4V1RTeXJyLXY2eDI0IiwKICAgICAgICAgInByb3RlY3RlZCI6ICJleUptYjNKdFlYUk1aVzVuZEdnaU9qRTNNeXdpWm05eWJXRjBWR0ZwYkNJNkltWlJJaXdpZEdsdFpTSTZJakl3TVRjdE1EVXRNRFZVTWpFNk5UYzZNek5hSW4wIgogICAgICB9CiAgIF0KfQ=="}` ) func TestActivateNoContainerd(t *testing.T) { testCli.SetContainerizedEngineClient( - func(string) (types.ContainerizedClient, error) { + func(string) (clitypes.ContainerizedClient, error) { return nil, fmt.Errorf("some error") }, ) @@ -25,7 +37,7 @@ func TestActivateNoContainerd(t *testing.T) { func TestActivateBadLicense(t *testing.T) { testCli.SetContainerizedEngineClient( - func(string) (types.ContainerizedClient, error) { + func(string) (clitypes.ContainerizedClient, error) { return &fakeContainerizedEngineClient{}, nil }, ) @@ -37,3 +49,28 @@ func TestActivateBadLicense(t *testing.T) { err := cmd.Execute() assert.Error(t, err, "open invalidpath: no such file or directory") } + +func TestActivateExpiredLicenseDryRun(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "license") + assert.NilError(t, err) + defer os.RemoveAll(tmpdir) + filename := filepath.Join(tmpdir, "docker.lic") + err = ioutil.WriteFile(filename, []byte(expiredLicense), 0644) + assert.NilError(t, err) + isRoot = func() bool { return true } + c := test.NewFakeCli(&verClient{client.Client{}, types.Version{}, nil}) + c.SetContainerizedEngineClient( + func(string) (clitypes.ContainerizedClient, error) { + return &fakeContainerizedEngineClient{}, nil + }, + ) + cmd := newActivateCommand(c) + cmd.SilenceUsage = true + cmd.SilenceErrors = true + cmd.Flags().Set("license", filename) + cmd.Flags().Set("display-only", "true") + c.OutBuffer().Reset() + err = cmd.Execute() + assert.NilError(t, err) + golden.Assert(t, c.OutBuffer().String(), "expired-license-display-only.golden") +} diff --git a/components/cli/cli/command/engine/testdata/expired-license-display-only.golden b/components/cli/cli/command/engine/testdata/expired-license-display-only.golden new file mode 100644 index 0000000000..a1bccce21e --- /dev/null +++ b/components/cli/cli/command/engine/testdata/expired-license-display-only.golden @@ -0,0 +1 @@ +License: Quantity: 1 Nodes Expiration date: 2018-03-18 Expired! You will no longer receive updates. Please renew at https://docker.com/licensing diff --git a/components/cli/internal/licenseutils/client_test.go b/components/cli/internal/licenseutils/client_test.go index 6077f97029..40bb37f2de 100644 --- a/components/cli/internal/licenseutils/client_test.go +++ b/components/cli/internal/licenseutils/client_test.go @@ -20,6 +20,7 @@ type ( parseLicenseFunc func(license []byte) (parsedLicense *model.IssuedLicense, err error) storeLicenseFunc func(ctx context.Context, dclnt licensing.WrappedDockerClient, licenses *model.IssuedLicense, localRootDir string) error loadLocalLicenseFunc func(ctx context.Context, dclnt licensing.WrappedDockerClient) (*model.Subscription, error) + summarizeLicenseFunc func(*model.CheckResponse, string) *model.Subscription } ) @@ -102,3 +103,10 @@ func (c *fakeLicensingClient) LoadLocalLicense(ctx context.Context, dclnt licens } return nil, nil } + +func (c *fakeLicensingClient) SummarizeLicense(cr *model.CheckResponse, keyid string) *model.Subscription { + if c.summarizeLicenseFunc != nil { + return c.summarizeLicenseFunc(cr, keyid) + } + return nil +} diff --git a/components/cli/internal/licenseutils/utils.go b/components/cli/internal/licenseutils/utils.go index 4d330c115f..27d039eb8d 100644 --- a/components/cli/internal/licenseutils/utils.go +++ b/components/cli/internal/licenseutils/utils.go @@ -1,6 +1,7 @@ package licenseutils import ( + "bytes" "context" "encoding/json" "fmt" @@ -35,18 +36,22 @@ func (u HubUser) GetOrgByID(orgID string) (model.Org, error) { return model.Org{}, fmt.Errorf("org %s not found", orgID) } -// Login to the license server and return a client that can be used to look up and download license files or generate new trial licenses -func Login(ctx context.Context, authConfig *types.AuthConfig) (HubUser, error) { +func getClient() (licensing.Client, error) { baseURI, err := url.Parse(licensingDefaultBaseURI) if err != nil { - return HubUser{}, err + return nil, err } - lclient, err := licensing.New(&licensing.Config{ + return licensing.New(&licensing.Config{ BaseURI: *baseURI, HTTPClient: &http.Client{}, PublicKeys: licensingPublicKeys, }) +} + +// Login to the license server and return a client that can be used to look up and download license files or generate new trial licenses +func Login(ctx context.Context, authConfig *types.AuthConfig) (HubUser, error) { + lclient, err := getClient() if err != nil { return HubUser{}, err } @@ -143,28 +148,36 @@ func (u HubUser) GetIssuedLicense(ctx context.Context, ID string) (*model.Issued // LoadLocalIssuedLicense will load a local license file func LoadLocalIssuedLicense(ctx context.Context, filename string) (*model.IssuedLicense, error) { - baseURI, err := url.Parse(licensingDefaultBaseURI) - if err != nil { - return nil, err - } - - lclient, err := licensing.New(&licensing.Config{ - BaseURI: *baseURI, - HTTPClient: &http.Client{}, - PublicKeys: licensingPublicKeys, - }) + lclient, err := getClient() if err != nil { return nil, err } return doLoadLocalIssuedLicense(ctx, filename, lclient) } +// GetLicenseSummary summarizes the license for the user +func GetLicenseSummary(ctx context.Context, license model.IssuedLicense) (string, error) { + lclient, err := getClient() + if err != nil { + return "", err + } + + cr, err := lclient.VerifyLicense(ctx, license) + if err != nil { + return "", err + } + return lclient.SummarizeLicense(cr, license.KeyID).String(), nil +} + func doLoadLocalIssuedLicense(ctx context.Context, filename string, lclient licensing.Client) (*model.IssuedLicense, error) { var license model.IssuedLicense data, err := ioutil.ReadFile(filename) if err != nil { return nil, err } + // The file may contain a leading BOM, which will choke the + // json deserializer. + data = bytes.TrimPrefix(data, []byte("\xef\xbb\xbf")) err = json.Unmarshal(data, &license) if err != nil { From fdf9f276f8e9ff61a1111e2e50df9e2c5cf33228 Mon Sep 17 00:00:00 2001 From: Andrew Hsu Date: Fri, 28 Sep 2018 18:31:38 +0000 Subject: [PATCH 3/4] use gotest.tools/fs for TestActivateExpiredLicenseDryRun Signed-off-by: Andrew Hsu Upstream-commit: a7488d1bcd644937b51a60e67f62b287f123d0fe Component: cli --- .../cli/cli/command/engine/activate_test.go | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/components/cli/cli/command/engine/activate_test.go b/components/cli/cli/command/engine/activate_test.go index 843b64b5aa..9924e49032 100644 --- a/components/cli/cli/command/engine/activate_test.go +++ b/components/cli/cli/command/engine/activate_test.go @@ -2,9 +2,6 @@ package engine import ( "fmt" - "io/ioutil" - "os" - "path/filepath" "testing" "github.com/docker/cli/internal/test" @@ -12,6 +9,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/client" "gotest.tools/assert" + "gotest.tools/fs" "gotest.tools/golden" ) @@ -51,12 +49,9 @@ func TestActivateBadLicense(t *testing.T) { } func TestActivateExpiredLicenseDryRun(t *testing.T) { - tmpdir, err := ioutil.TempDir("", "license") - assert.NilError(t, err) - defer os.RemoveAll(tmpdir) - filename := filepath.Join(tmpdir, "docker.lic") - err = ioutil.WriteFile(filename, []byte(expiredLicense), 0644) - assert.NilError(t, err) + dir := fs.NewDir(t, "license", fs.WithFile("docker.lic", expiredLicense, fs.WithMode(0644))) + defer dir.Remove() + filename := dir.Join("docker.lic") isRoot = func() bool { return true } c := test.NewFakeCli(&verClient{client.Client{}, types.Version{}, nil}) c.SetContainerizedEngineClient( @@ -70,7 +65,7 @@ func TestActivateExpiredLicenseDryRun(t *testing.T) { cmd.Flags().Set("license", filename) cmd.Flags().Set("display-only", "true") c.OutBuffer().Reset() - err = cmd.Execute() + err := cmd.Execute() assert.NilError(t, err) golden.Assert(t, c.OutBuffer().String(), "expired-license-display-only.golden") } From ff2d0e0b894cd818f59e4d8dfb841dead6d316d1 Mon Sep 17 00:00:00 2001 From: Daniel Hiltgen Date: Fri, 28 Sep 2018 14:06:28 -0700 Subject: [PATCH 4/4] 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 Upstream-commit: c12e23a4c1f2811d06f4ba60229963b9b009bfa9 Component: cli --- components/cli/cli/command/engine/activate.go | 19 +- .../cli/cli/command/engine/activate_test.go | 2 +- components/cli/cli/command/engine/check.go | 4 +- .../cli/cli/command/engine/check_test.go | 18 +- components/cli/cli/command/engine/update.go | 3 +- .../cli/cli/command/engine/update_test.go | 9 +- .../cli/internal/containerizedengine/types.go | 11 - .../internal/containerizedengine/update.go | 90 +++----- .../containerizedengine/update_test.go | 213 ++++++++++-------- components/cli/internal/versions/versions.go | 80 +++++-- .../cli/internal/versions/versions_test.go | 47 +++- components/cli/types/types.go | 21 +- 12 files changed, 305 insertions(+), 212 deletions(-) diff --git a/components/cli/cli/command/engine/activate.go b/components/cli/cli/command/engine/activate.go index 8868931f43..a2f409c637 100644 --- a/components/cli/cli/command/engine/activate.go +++ b/components/cli/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/components/cli/cli/command/engine/activate_test.go b/components/cli/cli/command/engine/activate_test.go index 9924e49032..d930847e3a 100644 --- a/components/cli/cli/command/engine/activate_test.go +++ b/components/cli/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/components/cli/cli/command/engine/check.go b/components/cli/cli/command/engine/check.go index 587d46adb9..2a41392117 100644 --- a/components/cli/cli/command/engine/check.go +++ b/components/cli/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/components/cli/cli/command/engine/check_test.go b/components/cli/cli/command/engine/check_test.go index 89450e67f9..d1bfd933fc 100644 --- a/components/cli/cli/command/engine/check_test.go +++ b/components/cli/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/components/cli/cli/command/engine/update.go b/components/cli/cli/command/engine/update.go index 041ef0d763..e9079d97c2 100644 --- a/components/cli/cli/command/engine/update.go +++ b/components/cli/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/components/cli/cli/command/engine/update_test.go b/components/cli/cli/command/engine/update_test.go index 99dcbdc8ef..641b8458c7 100644 --- a/components/cli/cli/command/engine/update_test.go +++ b/components/cli/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/components/cli/internal/containerizedengine/types.go b/components/cli/internal/containerizedengine/types.go index 017e9e7ed4..6fd5c5bb02 100644 --- a/components/cli/internal/containerizedengine/types.go +++ b/components/cli/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/components/cli/internal/containerizedengine/update.go b/components/cli/internal/containerizedengine/update.go index 0d5bab225e..4b029165f3 100644 --- a/components/cli/internal/containerizedengine/update.go +++ b/components/cli/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/components/cli/internal/containerizedengine/update_test.go b/components/cli/internal/containerizedengine/update_test.go index 75615df271..47299a5efa 100644 --- a/components/cli/internal/containerizedengine/update_test.go +++ b/components/cli/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/components/cli/internal/versions/versions.go b/components/cli/internal/versions/versions.go index 160d00c55c..9e83bb371a 100644 --- a/components/cli/internal/versions/versions.go +++ b/components/cli/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/components/cli/internal/versions/versions_test.go b/components/cli/internal/versions/versions_test.go index 1828ad0249..3469d8bbc3 100644 --- a/components/cli/internal/versions/versions_test.go +++ b/components/cli/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/components/cli/types/types.go b/components/cli/types/types.go index 61cbcd4b55..b10a01b001 100644 --- a/components/cli/types/types.go +++ b/components/cli/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"` +}