From 8b222d79d5e5d222ccb8369e00810fbdeb01a4a4 Mon Sep 17 00:00:00 2001 From: Drew Bailey <2614075+drewbailey@users.noreply.github.com> Date: Tue, 21 Apr 2020 14:55:31 -0400 Subject: [PATCH] license cli commands cli changes, formatting --- api/operator.go | 87 ++++++++++++++++++++++++- command/commands.go | 15 +++++ command/license.go | 99 +++++++++++++++++++++++++++++ command/license_get.go | 63 +++++++++++++++++++ command/license_put.go | 122 ++++++++++++++++++++++++++++++++++++ command/license_put_test.go | 13 ++++ 6 files changed, 398 insertions(+), 1 deletion(-) create mode 100644 command/license.go create mode 100644 command/license_get.go create mode 100644 command/license_put.go create mode 100644 command/license_put_test.go diff --git a/api/operator.go b/api/operator.go index 72d8fbdd2..6f20c1982 100644 --- a/api/operator.go +++ b/api/operator.go @@ -1,6 +1,9 @@ package api -import "strconv" +import ( + "strconv" + "time" +) // Operator can be used to perform low-level operator tasks for Nomad. type Operator struct { @@ -176,3 +179,85 @@ func (op *Operator) SchedulerCASConfiguration(conf *SchedulerConfiguration, q *W return &out, wm, nil } + +type License struct { + // The unique identifier of the license + LicenseID string `json:"license_id"` + + // The customer ID associated with the license + CustomerID string `json:"customer_id"` + + // If set, an identifier that should be used to lock the license to a + // particular site, cluster, etc. + InstallationID string `json:"installation_id"` + + // The time at which the license was issued + IssueTime time.Time `json:"issue_time"` + + // The time at which the license starts being valid + StartTime time.Time `json:"start_time"` + + // The time after which the license expires + ExpirationTime time.Time `json:"expiration_time"` + + // The time at which the license ceases to function and can + // no longer be used in any capacity + TerminationTime time.Time `json:"termination_time"` + + // The product the license is valid for + Product string `json:"product"` + + // License Specific Flags + Flags map[string]interface{} `json:"flags"` + + // Modules is a list of the licensed enterprise modules + Modules []string `json:"modules"` + + // List of features enabled by the license + Features []string `json:"features"` +} + +type LicenseReply struct { + Valid bool + License *License + Warnings []string + QueryMeta +} + +func (op *Operator) LicensePut(license string, q *WriteOptions) (*LicenseReply, *WriteMeta, error) { + var resp LicenseReply + wm, err := op.c.write("/v1/operator/license", license, &resp, q) + if err != nil { + return nil, nil, err + } + return &resp, wm, nil +} + +func (op *Operator) LicenseGet(q *QueryOptions) (*LicenseReply, *QueryMeta, error) { + var reply LicenseReply + qm, err := op.c.query("/v1/operator/license", &reply, q) + if err != nil { + return nil, nil, err + } + return &reply, qm, nil +} + +func (op *Operator) LicenseGetSigned(q *QueryOptions) (string, *QueryMeta, error) { + var reply string + qm, err := op.c.query("/v1/operator/license?signed=true", &reply, q) + if err != nil { + return "", nil, err + } + return reply, qm, nil +} + +// LicenseReset will reset the license to the builtin one if it is still valid. +// If the builtin license is invalid, the current license stays active +func (op *Operator) LicenseReset(q *WriteOptions) (*LicenseReply, *WriteMeta, error) { + var reply LicenseReply + wm, err := op.c.delete("/v1/operator/license", &reply, q) + if err != nil { + return nil, nil, err + } + return &reply, wm, nil +} diff --git a/command/commands.go b/command/commands.go index dbbcec006..922a07e8c 100644 --- a/command/commands.go +++ b/command/commands.go @@ -361,6 +361,21 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory { Meta: meta, }, nil }, + "license": func() (cli.Command, error) { + return &LicenseCommand{ + Meta: meta, + }, nil + }, + "license get": func() (cli.Command, error) { + return &LicenseGetCommand{ + Meta: meta, + }, nil + }, + "license put": func() (cli.Command, error) { + return &LicensePutCommand{ + Meta: meta, + }, nil + }, "logs": func() (cli.Command, error) { return &AllocLogsCommand{ Meta: meta, diff --git a/command/license.go b/command/license.go new file mode 100644 index 000000000..e3ec7a1a7 --- /dev/null +++ b/command/license.go @@ -0,0 +1,99 @@ +package command + +import ( + "fmt" + "strings" + "time" + + "github.com/hashicorp/nomad/api" + "github.com/mitchellh/cli" +) + +type LicenseCommand struct { + Meta +} + +func (l *LicenseCommand) Help() string { + helpText := ` +Usage: nomad license [options] [args] + +This command has subcommands for managing the Nomad Enterprise license. +For more detailed examples see: +https://www.nomadproject.io/docs/commands/license/ + +Install a new license from a file: + $ nomad license put @nomad.license + +Install a new license from stdin: + $ nomad license put - + +Install a new license from a string: + $ nomad license put "" + +Retrieve the current license: + + $ nomad license get + +Reset the current license: + $ nomad license reset + ` + return strings.TrimSpace(helpText) +} + +func (l *LicenseCommand) Synopsis() string { + return "Interact with Nomad Enterprise License" +} + +func (l *LicenseCommand) Name() string { return "license" } + +func (l *LicenseCommand) Run(args []string) int { + return cli.RunResultHelp +} + +func OutputLicenseReply(ui cli.Ui, resp *api.LicenseReply) int { + if resp.Valid { + ui.Output("License is valid") + outputLicenseInfo(ui, resp.License, false) + return 0 + } else if resp.License != nil { + now := time.Now() + if resp.License.ExpirationTime.Before(now) { + ui.Output("License has expired!") + outputLicenseInfo(ui, resp.License, true) + } else { + ui.Output("License is invalid!") + for _, warn := range resp.Warnings { + ui.Output(fmt.Sprintf(" %s", warn)) + } + outputLicenseInfo(ui, resp.License, false) + } + return 1 + } else { + // TODO - remove the expired message here in the future + // once the go-licensing library is updated post 1.1 + ui.Output("Nomad is unlicensed or the license has expired") + return 0 + } +} + +func outputLicenseInfo(ui cli.Ui, lic *api.License, expired bool) { + ui.Output(fmt.Sprintf("License ID: %s", lic.LicenseID)) + ui.Output(fmt.Sprintf("Customer ID: %s", lic.CustomerID)) + if expired { + ui.Output(fmt.Sprintf("Expired At: %s", lic.ExpirationTime.String())) + } else { + ui.Output(fmt.Sprintf("Expires At: %s", lic.ExpirationTime.String())) + } + ui.Output(fmt.Sprintf("Terminates At: %s", lic.TerminationTime.String())) + ui.Output(fmt.Sprintf("Datacenter: %s", lic.InstallationID)) + if len(lic.Modules) > 0 { + ui.Output("Modules:") + for _, mod := range lic.Modules { + ui.Output(fmt.Sprintf("\t%v", mod)) + } + } + ui.Output("Licensed Features:") + for _, f := range lic.Features { + ui.Output(fmt.Sprintf("\t%s", f)) + } +} diff --git a/command/license_get.go b/command/license_get.go new file mode 100644 index 000000000..64fd06789 --- /dev/null +++ b/command/license_get.go @@ -0,0 +1,63 @@ +package command + +import ( + "fmt" +) + +type LicenseGetCommand struct { + Meta +} + +func (c *LicenseGetCommand) Help() string { + helpText := ` +Usage: nomad license put [options] + +Gets a new license in Servers and Clients +General Options: + + ` + generalOptionsUsage() + ` + +Get Options: + + -signed + Determines if the returned license should be a signed blob instead of a + parsed license. + + ` + return helpText +} + +func (c *LicenseGetCommand) Synopsis() string { + return "Install a new Nomad Enterprise License" +} + +func (c *LicenseGetCommand) Name() string { return "license get" } + +func (c *LicenseGetCommand) Run(args []string) int { + var signed bool + + flags := c.Meta.FlagSet(c.Name(), FlagSetClient) + flags.Usage = func() { c.Ui.Output(c.Help()) } + flags.BoolVar(&signed, "signed", false, "Gets the signed license blob instead of a parsed license") + + client, err := c.Meta.Client() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) + } + + if signed { + resp, _, err := client.Operator().LicenseGetSigned(nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error getting signed license: %v", err)) + } + c.Ui.Output(resp) + return 0 + } + + resp, _, err := client.Operator().LicenseGet(nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error putting license: %v", err)) + } + + return OutputLicenseReply(c.Ui, resp) +} diff --git a/command/license_put.go b/command/license_put.go new file mode 100644 index 000000000..c5ac4cd74 --- /dev/null +++ b/command/license_put.go @@ -0,0 +1,122 @@ +package command + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + + "github.com/pkg/errors" +) + +type LicensePutCommand struct { + Meta + + testStdin io.Reader +} + +func (c *LicensePutCommand) Help() string { + helpText := ` +Usage: nomad license put [options] + +Puts a new license in Servers and Clients +General Options: + ` + generalOptionsUsage() + ` +Install a new license from a file: + $ nomad license put @nomad.license +Install a new license from stdin: + $ nomad license put - +Install a new license from a string: + $ nomad license put "" + ` + return helpText +} + +func (c *LicensePutCommand) Synopsis() string { + return "Install a new Nomad Enterprise License" +} + +func (c *LicensePutCommand) Name() string { return "license put" } + +func (c *LicensePutCommand) Run(args []string) int { + flags := c.Meta.FlagSet(c.Name(), FlagSetClient) + flags.Usage = func() { c.Ui.Output(c.Help()) } + + if err := flags.Parse(args); err != nil { + c.Ui.Error(fmt.Sprintf("Error parsing flags: %s", err)) + return 1 + } + + args = flags.Args() + data, err := c.dataFromArgs(args) + if err != nil { + c.Ui.Error(errors.Wrap(err, "Error parsing arguments").Error()) + return 1 + } + + client, err := c.Meta.Client() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) + return 1 + } + + resp, _, err := client.Operator().LicensePut(data, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error putting license: %v", err)) + return 1 + } + + return OutputLicenseReply(c.Ui, resp) +} + +func (c *LicensePutCommand) dataFromArgs(args []string) (string, error) { + switch len(args) { + case 0: + return "", fmt.Errorf("Missing LICENSE argument") + case 1: + return LoadDataSource(args[0], c.testStdin) + default: + return "", fmt.Errorf("Too many arguments, exptected 1, got %d", len(args)) + } +} + +func loadFromFile(path string) (string, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return "", fmt.Errorf("Failed to read file: %v", err) + } + return string(data), nil +} + +func loadFromStdin(testStdin io.Reader) (string, error) { + var stdin io.Reader = os.Stdin + if testStdin != nil { + stdin = testStdin + } + + var b bytes.Buffer + if _, err := io.Copy(&b, stdin); err != nil { + return "", fmt.Errorf("Failed to read stdin: %v", err) + } + return b.String(), nil +} + +func LoadDataSource(data string, testStdin io.Reader) (string, error) { + // Handle empty quoted shell parameters + if len(data) == 0 { + return "", nil + } + + switch data[0] { + case '@': + return loadFromFile(data[1:]) + case '-': + if len(data) > 1 { + return data, nil + } + return loadFromStdin(testStdin) + default: + return data, nil + } +} diff --git a/command/license_put_test.go b/command/license_put_test.go new file mode 100644 index 000000000..ed47ca978 --- /dev/null +++ b/command/license_put_test.go @@ -0,0 +1,13 @@ +package command + +import ( + "testing" + + "github.com/mitchellh/cli" +) + +var _ cli.Command = &LicensePutCommand{} + +func TestCommand_LicensePut(t *testing.T) { + // TODO create test once http endpoints are configured +}