From 5a97a93ae10218557d737d06d1fd6c95bd690b5d Mon Sep 17 00:00:00 2001 From: Daniel Hiltgen Date: Mon, 10 Sep 2018 14:31:45 -0700 Subject: [PATCH] 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 --- cli/command/engine/activate.go | 11 +++-- cli/command/engine/activate_test.go | 43 +++++++++++++++++-- .../expired-license-display-only.golden | 1 + internal/licenseutils/client_test.go | 8 ++++ internal/licenseutils/utils.go | 41 ++++++++++++------ 5 files changed, 84 insertions(+), 20 deletions(-) create mode 100644 cli/command/engine/testdata/expired-license-display-only.golden diff --git a/cli/command/engine/activate.go b/cli/command/engine/activate.go index ba98eed72d..8868931f43 100644 --- a/cli/command/engine/activate.go +++ b/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/cli/command/engine/activate_test.go b/cli/command/engine/activate_test.go index 6fe552c7da..843b64b5aa 100644 --- a/cli/command/engine/activate_test.go +++ b/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/cli/command/engine/testdata/expired-license-display-only.golden b/cli/command/engine/testdata/expired-license-display-only.golden new file mode 100644 index 0000000000..a1bccce21e --- /dev/null +++ b/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/internal/licenseutils/client_test.go b/internal/licenseutils/client_test.go index 6077f97029..40bb37f2de 100644 --- a/internal/licenseutils/client_test.go +++ b/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/internal/licenseutils/utils.go b/internal/licenseutils/utils.go index 4d330c115f..27d039eb8d 100644 --- a/internal/licenseutils/utils.go +++ b/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 {