Merge pull request #7778 from hashicorp/license-cli

License cli
This commit is contained in:
Drew Bailey 2020-05-01 08:51:40 -04:00 committed by GitHub
commit 2b8fc650c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 441 additions and 1 deletions

View File

@ -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,65 @@ 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
}

View File

@ -19,6 +19,8 @@ func (s *HTTPServer) registerEnterpriseHandlers() {
s.mux.HandleFunc("/v1/quota-usages", s.wrap(s.entOnly))
s.mux.HandleFunc("/v1/quota/", s.wrap(s.entOnly))
s.mux.HandleFunc("/v1/quota", s.wrap(s.entOnly))
s.mux.HandleFunc("/v1/operator/license", s.wrap(s.entOnly))
}
func (s *HTTPServer) entOnly(resp http.ResponseWriter, req *http.Request) (interface{}, error) {

View File

@ -81,6 +81,9 @@ type TestAgent struct {
// ports that are reserved through freeport that must be returned at
// the end of a test, done when Shutdown() is called.
ports []int
// Enterprise specifies if the agent is enterprise or not
Enterprise bool
}
// NewTestAgent returns a started agent with the given name and
@ -91,6 +94,7 @@ func NewTestAgent(t testing.T, name string, configCallback func(*Config)) *TestA
T: t,
Name: name,
ConfigCallback: configCallback,
Enterprise: EnterpriseTestAgent,
}
a.Start()

View File

@ -0,0 +1,8 @@
// +build !ent
package agent
const (
// EnterpriseTestAgent is used to configure a TestAgent's Enterprise flag
EnterpriseTestAgent = false
)

View File

@ -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,

107
command/license.go Normal file
View File

@ -0,0 +1,107 @@
package command
import (
"fmt"
"strings"
"time"
"github.com/hashicorp/nomad/api"
"github.com/mitchellh/cli"
)
var _ cli.Command = &LicenseCommand{}
type LicenseCommand struct {
Meta
}
func (l *LicenseCommand) Help() string {
helpText := `
Usage: nomad license <subcommand> [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 <path>
Install a new license from stdin:
$ nomad license put -
Retrieve the current license:
$ nomad license get
`
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 {
var validity string
if resp.Valid {
validity = "valid"
outputLicenseInfo(ui, resp.License, false, validity)
return 0
} else if resp.License != nil {
now := time.Now()
if resp.License.ExpirationTime.Before(now) {
validity = "expired!"
outputLicenseInfo(ui, resp.License, true, validity)
} else {
validity = "invalid!"
for _, warn := range resp.Warnings {
ui.Output(fmt.Sprintf(" %s", warn))
}
outputLicenseInfo(ui, resp.License, false, validity)
}
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, validity string) {
expStr := ""
if expired {
expStr = fmt.Sprintf("Expired At|%s", lic.ExpirationTime.String())
} else {
expStr = fmt.Sprintf("Expires At|%s", lic.ExpirationTime.String())
}
output := []string{
fmt.Sprintf("License Status|%s", validity),
fmt.Sprintf("License ID|%s", lic.LicenseID),
fmt.Sprintf("Customer ID|%s", lic.CustomerID),
expStr,
fmt.Sprintf("License ID|%s", lic.LicenseID),
fmt.Sprintf("Customer ID|%s", lic.CustomerID),
fmt.Sprintf("Terminates At|%s", lic.TerminationTime.String()),
fmt.Sprintf("Datacenter|%s", lic.InstallationID),
}
ui.Output(formatKV(output))
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))
}
}

52
command/license_get.go Normal file
View File

@ -0,0 +1,52 @@
package command
import (
"fmt"
)
type LicenseGetCommand struct {
Meta
}
func (c *LicenseGetCommand) Help() string {
helpText := `
Usage: nomad license get [options]
Gets a new license in Servers and Clients
General Options:
` + generalOptionsUsage()
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 {
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
}
client, err := c.Meta.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
return 1
}
resp, _, err := client.Operator().LicenseGet(nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting license: %v", err))
return 1
}
return OutputLicenseReply(c.Ui, resp)
}

View File

@ -0,0 +1,30 @@
package command
import (
"testing"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
var _ cli.Command = &LicenseGetCommand{}
func TestCommand_LicenseGet_OSSErr(t *testing.T) {
t.Parallel()
srv, _, url := testServer(t, false, nil)
defer srv.Shutdown()
ui := new(cli.MockUi)
cmd := &LicenseGetCommand{Meta: Meta{Ui: ui}}
code := cmd.Run([]string{"-address=" + url})
require.Equal(t, 1, code)
if srv.Enterprise {
require.Contains(t, ui.OutputWriter.String(), "License Status")
} else {
require.Contains(t, ui.ErrorWriter.String(), "Nomad Enterprise only endpoint")
}
}

125
command/license_put.go Normal file
View File

@ -0,0 +1,125 @@
package command
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"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 <path>
Install a new license from stdin:
$ nomad license put -
`
return strings.TrimSpace(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(file string, testStdin io.Reader) (string, error) {
// Handle empty quoted shell parameters
if len(file) == 0 {
return "", nil
}
if file == "-" {
if len(file) > 1 {
return file, nil
}
return loadFromStdin(testStdin)
}
return loadFromFile(file)
}

View File

@ -0,0 +1,32 @@
package command
import (
"strings"
"testing"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
var _ cli.Command = &LicensePutCommand{}
func TestCommand_LicensePut_Err(t *testing.T) {
t.Parallel()
srv, _, url := testServer(t, false, nil)
defer srv.Shutdown()
ui := new(cli.MockUi)
cmd := &LicensePutCommand{Meta: Meta{Ui: ui}, testStdin: strings.NewReader("testlicenseblob")}
if code := cmd.Run([]string{"-address=" + url, "-"}); code != 1 {
require.Equal(t, code, 1)
}
if srv.Enterprise {
require.Contains(t, ui.ErrorWriter.String(), "error validating license")
} else {
require.Contains(t, ui.ErrorWriter.String(), "Nomad Enterprise only endpoint")
}
}