mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
coder licenses add CLI command (#3632)
* coder licenses add CLI command Signed-off-by: Spike Curtis <spike@coder.com> * Fix up lint Signed-off-by: Spike Curtis <spike@coder.com> * Fix t.parallel call Signed-off-by: Spike Curtis <spike@coder.com> * Code review improvements Signed-off-by: Spike Curtis <spike@coder.com> * Lint Signed-off-by: Spike Curtis <spike@coder.com> Signed-off-by: Spike Curtis <spike@coder.com>
This commit is contained in:
@ -21,7 +21,13 @@ import (
|
|||||||
// New creates a CLI instance with a configuration pointed to a
|
// New creates a CLI instance with a configuration pointed to a
|
||||||
// temporary testing directory.
|
// temporary testing directory.
|
||||||
func New(t *testing.T, args ...string) (*cobra.Command, config.Root) {
|
func New(t *testing.T, args ...string) (*cobra.Command, config.Root) {
|
||||||
cmd := cli.Root(cli.AGPL())
|
return NewWithSubcommands(t, cli.AGPL(), args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWithSubcommands(
|
||||||
|
t *testing.T, subcommands []*cobra.Command, args ...string,
|
||||||
|
) (*cobra.Command, config.Root) {
|
||||||
|
cmd := cli.Root(subcommands)
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
root := config.Root(dir)
|
root := config.Root(dir)
|
||||||
cmd.SetArgs(append([]string{"--global-config", dir}, args...))
|
cmd.SetArgs(append([]string{"--global-config", dir}, args...))
|
||||||
|
@ -158,7 +158,7 @@ func configSSH() *cobra.Command {
|
|||||||
),
|
),
|
||||||
Args: cobra.ExactArgs(0),
|
Args: cobra.ExactArgs(0),
|
||||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ func create() *cobra.Command {
|
|||||||
Use: "create [name]",
|
Use: "create [name]",
|
||||||
Short: "Create a workspace from a template",
|
Short: "Create a workspace from a template",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ func deleteWorkspace() *cobra.Command {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ func featuresList() *cobra.Command {
|
|||||||
Use: "list",
|
Use: "list",
|
||||||
Aliases: []string{"ls"},
|
Aliases: []string{"ls"},
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ func list() *cobra.Command {
|
|||||||
Aliases: []string{"ls"},
|
Aliases: []string{"ls"},
|
||||||
Args: cobra.ExactArgs(0),
|
Args: cobra.ExactArgs(0),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ func logout() *cobra.Command {
|
|||||||
Use: "logout",
|
Use: "logout",
|
||||||
Short: "Remove the local authenticated session",
|
Short: "Remove the local authenticated session",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ func parameterList() *cobra.Command {
|
|||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
scope, name := args[0], args[1]
|
scope, name := args[0], args[1]
|
||||||
|
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,7 @@ func portForward() *cobra.Command {
|
|||||||
return xerrors.New("no port-forwards requested")
|
return xerrors.New("no port-forwards requested")
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ func publickey() *cobra.Command {
|
|||||||
Aliases: []string{"pubkey"},
|
Aliases: []string{"pubkey"},
|
||||||
Short: "Output your public key for Git operations",
|
Short: "Output your public key for Git operations",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("create codersdk client: %w", err)
|
return xerrors.Errorf("create codersdk client: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -114,7 +114,7 @@ func Root(subcommands []*cobra.Command) *cobra.Command {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
// If the client is unauthenticated we can ignore the check.
|
// If the client is unauthenticated we can ignore the check.
|
||||||
// The child commands should handle an unauthenticated client.
|
// The child commands should handle an unauthenticated client.
|
||||||
if xerrors.Is(err, errUnauthenticated) {
|
if xerrors.Is(err, errUnauthenticated) {
|
||||||
@ -190,9 +190,9 @@ func isTest() bool {
|
|||||||
return flag.Lookup("test.v") != nil
|
return flag.Lookup("test.v") != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// createClient returns a new client from the command context.
|
// CreateClient returns a new client from the command context.
|
||||||
// It reads from global configuration files if flags are not set.
|
// It reads from global configuration files if flags are not set.
|
||||||
func createClient(cmd *cobra.Command) (*codersdk.Client, error) {
|
func CreateClient(cmd *cobra.Command) (*codersdk.Client, error) {
|
||||||
root := createConfig(cmd)
|
root := createConfig(cmd)
|
||||||
rawURL, err := cmd.Flags().GetString(varURL)
|
rawURL, err := cmd.Flags().GetString(varURL)
|
||||||
if err != nil || rawURL == "" {
|
if err != nil || rawURL == "" {
|
||||||
@ -226,7 +226,7 @@ func createClient(cmd *cobra.Command) (*codersdk.Client, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// createAgentClient returns a new client from the command context.
|
// createAgentClient returns a new client from the command context.
|
||||||
// It works just like createClient, but uses the agent token and URL instead.
|
// It works just like CreateClient, but uses the agent token and URL instead.
|
||||||
func createAgentClient(cmd *cobra.Command) (*codersdk.Client, error) {
|
func createAgentClient(cmd *cobra.Command) (*codersdk.Client, error) {
|
||||||
rawURL, err := cmd.Flags().GetString(varAgentURL)
|
rawURL, err := cmd.Flags().GetString(varAgentURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -77,7 +77,7 @@ func scheduleShow() *cobra.Command {
|
|||||||
Long: scheduleShowDescriptionLong,
|
Long: scheduleShowDescriptionLong,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -106,7 +106,7 @@ func scheduleStart() *cobra.Command {
|
|||||||
Long: scheduleStartDescriptionLong,
|
Long: scheduleStartDescriptionLong,
|
||||||
Args: cobra.RangeArgs(2, 4),
|
Args: cobra.RangeArgs(2, 4),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -156,7 +156,7 @@ func scheduleStop() *cobra.Command {
|
|||||||
Short: "Edit workspace stop schedule",
|
Short: "Edit workspace stop schedule",
|
||||||
Long: scheduleStopDescriptionLong,
|
Long: scheduleStopDescriptionLong,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -207,7 +207,7 @@ func scheduleOverride() *cobra.Command {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("create client: %w", err)
|
return xerrors.Errorf("create client: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ func show() *cobra.Command {
|
|||||||
Short: "Show details of a workspace's resources and agents",
|
Short: "Show details of a workspace's resources and agents",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ func ssh() *cobra.Command {
|
|||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ func start() *cobra.Command {
|
|||||||
Short: "Build a workspace with the start state",
|
Short: "Build a workspace with the start state",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ func statePull() *cobra.Command {
|
|||||||
Use: "pull <workspace> [file]",
|
Use: "pull <workspace> [file]",
|
||||||
Args: cobra.MinimumNArgs(1),
|
Args: cobra.MinimumNArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -68,7 +68,7 @@ func statePush() *cobra.Command {
|
|||||||
Use: "push <workspace> <file>",
|
Use: "push <workspace> <file>",
|
||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ func stop() *cobra.Command {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ func templateCreate() *cobra.Command {
|
|||||||
Short: "Create a template from the current directory or as specified by flag",
|
Short: "Create a template from the current directory or as specified by flag",
|
||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ func templateDelete() *cobra.Command {
|
|||||||
templates = []codersdk.Template{}
|
templates = []codersdk.Template{}
|
||||||
)
|
)
|
||||||
|
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ func templateEdit() *cobra.Command {
|
|||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Short: "Edit the metadata of a template by name.",
|
Short: "Edit the metadata of a template by name.",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("create client: %w", err)
|
return xerrors.Errorf("create client: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ func templateList() *cobra.Command {
|
|||||||
Short: "List all the templates available for the organization",
|
Short: "List all the templates available for the organization",
|
||||||
Aliases: []string{"ls"},
|
Aliases: []string{"ls"},
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ func templatePull() *cobra.Command {
|
|||||||
dest = args[1]
|
dest = args[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("create client: %w", err)
|
return xerrors.Errorf("create client: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ func templatePush() *cobra.Command {
|
|||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
Short: "Push a new template version from the current directory or as specified by flag",
|
Short: "Push a new template version from the current directory or as specified by flag",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ func templateVersionsList() *cobra.Command {
|
|||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Short: "List all the versions of the specified template",
|
Short: "List all the versions of the specified template",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("create client: %w", err)
|
return xerrors.Errorf("create client: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ func update() *cobra.Command {
|
|||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Short: "Update a workspace to the latest template version",
|
Short: "Update a workspace to the latest template version",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ func userCreate() *cobra.Command {
|
|||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "create",
|
Use: "create",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ func userList() *cobra.Command {
|
|||||||
Use: "list",
|
Use: "list",
|
||||||
Aliases: []string{"ls"},
|
Aliases: []string{"ls"},
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -76,7 +76,7 @@ func userSingle() *cobra.Command {
|
|||||||
),
|
),
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ func createUserStatusCommand(sdkStatus codersdk.UserStatus) *cobra.Command {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,7 @@ func wireguardPortForward() *cobra.Command {
|
|||||||
return xerrors.New("no port-forwards requested")
|
return xerrors.New("no port-forwards requested")
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := createClient(cmd)
|
client, err := CreateClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
114
enterprise/cli/licenses.go
Normal file
114
enterprise/cli/licenses.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
|
agpl "github.com/coder/coder/cli"
|
||||||
|
"github.com/coder/coder/cli/cliui"
|
||||||
|
"github.com/coder/coder/codersdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
var jwtRegexp = regexp.MustCompile(`^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$`)
|
||||||
|
|
||||||
|
func licenses() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Short: "Add, remove, and list licenses",
|
||||||
|
Use: "licenses",
|
||||||
|
Aliases: []string{"license"},
|
||||||
|
}
|
||||||
|
cmd.AddCommand(
|
||||||
|
licenseAdd(),
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func licenseAdd() *cobra.Command {
|
||||||
|
var (
|
||||||
|
filename string
|
||||||
|
license string
|
||||||
|
debug bool
|
||||||
|
)
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "add [-f file | -l license]",
|
||||||
|
Short: "Add license to Coder deployment",
|
||||||
|
Args: cobra.ExactArgs(0),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
client, err := agpl.CreateClient(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case filename != "" && license != "":
|
||||||
|
return xerrors.New("only one of (--file, --license) may be specified")
|
||||||
|
|
||||||
|
case filename == "" && license == "":
|
||||||
|
license, err = cliui.Prompt(cmd, cliui.PromptOptions{
|
||||||
|
Text: "Paste license:",
|
||||||
|
Secret: true,
|
||||||
|
Validate: validJWT,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
case filename != "" && license == "":
|
||||||
|
var r io.Reader
|
||||||
|
if filename == "-" {
|
||||||
|
r = cmd.InOrStdin()
|
||||||
|
} else {
|
||||||
|
f, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
r = f
|
||||||
|
}
|
||||||
|
lb, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
license = string(lb)
|
||||||
|
}
|
||||||
|
license = strings.Trim(license, " \n")
|
||||||
|
err = validJWT(license)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
licResp, err := client.AddLicense(
|
||||||
|
cmd.Context(),
|
||||||
|
codersdk.AddLicenseRequest{License: license},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if debug {
|
||||||
|
enc := json.NewEncoder(cmd.OutOrStdout())
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
return enc.Encode(licResp)
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "License with ID %d added\n", licResp.ID)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVarP(&filename, "file", "f", "", "Load license from file")
|
||||||
|
cmd.Flags().StringVarP(&license, "license", "l", "", "License string")
|
||||||
|
cmd.Flags().BoolVar(&debug, "debug", false, "Output license claims for debugging")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func validJWT(s string) error {
|
||||||
|
if jwtRegexp.MatchString(s) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return xerrors.New("Invalid license")
|
||||||
|
}
|
192
enterprise/cli/licenses_test.go
Normal file
192
enterprise/cli/licenses_test.go
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
package cli_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
|
"github.com/coder/coder/cli/clitest"
|
||||||
|
"github.com/coder/coder/coderd/coderdtest"
|
||||||
|
"github.com/coder/coder/codersdk"
|
||||||
|
"github.com/coder/coder/enterprise/cli"
|
||||||
|
"github.com/coder/coder/enterprise/coderd"
|
||||||
|
"github.com/coder/coder/pty/ptytest"
|
||||||
|
"github.com/coder/coder/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
const fakeLicenseJWT = "test.jwt.sig"
|
||||||
|
|
||||||
|
func TestLicensesAddSuccess(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
// We can't check a real license into the git repo, and can't patch out the keys from here,
|
||||||
|
// so instead we have to fake the HTTP interaction.
|
||||||
|
t.Run("LFlag", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
cmd := setupFakeLicenseServerTest(t, "licenses", "add", "-l", fakeLicenseJWT)
|
||||||
|
pty := attachPty(t, cmd)
|
||||||
|
errC := make(chan error)
|
||||||
|
go func() {
|
||||||
|
errC <- cmd.ExecuteContext(ctx)
|
||||||
|
}()
|
||||||
|
require.NoError(t, <-errC)
|
||||||
|
pty.ExpectMatch("License with ID 1 added")
|
||||||
|
})
|
||||||
|
t.Run("Prompt", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
cmd := setupFakeLicenseServerTest(t, "license", "add")
|
||||||
|
pty := attachPty(t, cmd)
|
||||||
|
errC := make(chan error)
|
||||||
|
go func() {
|
||||||
|
errC <- cmd.ExecuteContext(ctx)
|
||||||
|
}()
|
||||||
|
pty.ExpectMatch("Paste license:")
|
||||||
|
pty.WriteLine(fakeLicenseJWT)
|
||||||
|
require.NoError(t, <-errC)
|
||||||
|
pty.ExpectMatch("License with ID 1 added")
|
||||||
|
})
|
||||||
|
t.Run("File", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
dir := t.TempDir()
|
||||||
|
filename := filepath.Join(dir, "license.jwt")
|
||||||
|
err := os.WriteFile(filename, []byte(fakeLicenseJWT), 0600)
|
||||||
|
require.NoError(t, err)
|
||||||
|
cmd := setupFakeLicenseServerTest(t, "license", "add", "-f", filename)
|
||||||
|
pty := attachPty(t, cmd)
|
||||||
|
errC := make(chan error)
|
||||||
|
go func() {
|
||||||
|
errC <- cmd.ExecuteContext(ctx)
|
||||||
|
}()
|
||||||
|
require.NoError(t, <-errC)
|
||||||
|
pty.ExpectMatch("License with ID 1 added")
|
||||||
|
})
|
||||||
|
t.Run("StdIn", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
cmd := setupFakeLicenseServerTest(t, "license", "add", "-f", "-")
|
||||||
|
r, w := io.Pipe()
|
||||||
|
cmd.SetIn(r)
|
||||||
|
stdout := new(bytes.Buffer)
|
||||||
|
cmd.SetOut(stdout)
|
||||||
|
errC := make(chan error)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
errC <- cmd.ExecuteContext(ctx)
|
||||||
|
}()
|
||||||
|
_, err := w.Write([]byte(fakeLicenseJWT))
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = w.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
select {
|
||||||
|
case err = <-errC:
|
||||||
|
require.NoError(t, err)
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Error("timed out")
|
||||||
|
}
|
||||||
|
assert.Equal(t, "License with ID 1 added\n", stdout.String())
|
||||||
|
})
|
||||||
|
t.Run("DebugOutput", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
cmd := setupFakeLicenseServerTest(t, "licenses", "add", "-l", fakeLicenseJWT, "--debug")
|
||||||
|
pty := attachPty(t, cmd)
|
||||||
|
errC := make(chan error)
|
||||||
|
go func() {
|
||||||
|
errC <- cmd.ExecuteContext(ctx)
|
||||||
|
}()
|
||||||
|
require.NoError(t, <-errC)
|
||||||
|
pty.ExpectMatch("\"f2\": 2")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLicensesAddFail(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
t.Run("LFlag", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{APIBuilder: coderd.NewEnterprise})
|
||||||
|
coderdtest.CreateFirstUser(t, client)
|
||||||
|
cmd, root := clitest.NewWithSubcommands(t, cli.EnterpriseSubcommands(),
|
||||||
|
"licenses", "add", "-l", fakeLicenseJWT)
|
||||||
|
clitest.SetupConfig(t, client, root)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
errC := make(chan error)
|
||||||
|
go func() {
|
||||||
|
errC <- cmd.ExecuteContext(ctx)
|
||||||
|
}()
|
||||||
|
err := <-errC
|
||||||
|
var coderError *codersdk.Error
|
||||||
|
require.True(t, xerrors.As(err, &coderError))
|
||||||
|
assert.Equal(t, 400, coderError.StatusCode())
|
||||||
|
assert.Contains(t, "Invalid license", coderError.Message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupFakeLicenseServerTest(t *testing.T, args ...string) *cobra.Command {
|
||||||
|
t.Helper()
|
||||||
|
s := httptest.NewServer(&fakeAddLicenseServer{t})
|
||||||
|
t.Cleanup(s.Close)
|
||||||
|
cmd, root := clitest.NewWithSubcommands(t, cli.EnterpriseSubcommands(), args...)
|
||||||
|
err := root.URL().Write(s.URL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = root.Session().Write("sessiontoken")
|
||||||
|
require.NoError(t, err)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func attachPty(t *testing.T, cmd *cobra.Command) *ptytest.PTY {
|
||||||
|
pty := ptytest.New(t)
|
||||||
|
cmd.SetIn(pty.Input())
|
||||||
|
cmd.SetOut(pty.Output())
|
||||||
|
return pty
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeAddLicenseServer struct {
|
||||||
|
t *testing.T
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fakeAddLicenseServer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/api/v2/buildinfo" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(s.t, http.MethodPost, r.Method)
|
||||||
|
assert.Equal(s.t, "/api/v2/licenses", r.URL.Path)
|
||||||
|
var req codersdk.AddLicenseRequest
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
require.NoError(s.t, err)
|
||||||
|
assert.Equal(s.t, "test.jwt.sig", req.License)
|
||||||
|
|
||||||
|
resp := codersdk.License{
|
||||||
|
ID: 1,
|
||||||
|
UploadedAt: time.Now(),
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"h1": "claim1",
|
||||||
|
"features": map[string]int64{
|
||||||
|
"f1": 1,
|
||||||
|
"f2": 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rw.WriteHeader(http.StatusCreated)
|
||||||
|
err = json.NewEncoder(rw).Encode(resp)
|
||||||
|
assert.NoError(s.t, err)
|
||||||
|
}
|
@ -7,7 +7,14 @@ import (
|
|||||||
"github.com/coder/coder/enterprise/coderd"
|
"github.com/coder/coder/enterprise/coderd"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func enterpriseOnly() []*cobra.Command {
|
||||||
|
return []*cobra.Command{
|
||||||
|
agpl.Server(coderd.NewEnterprise),
|
||||||
|
licenses(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func EnterpriseSubcommands() []*cobra.Command {
|
func EnterpriseSubcommands() []*cobra.Command {
|
||||||
all := append(agpl.Core(), agpl.Server(coderd.NewEnterprise))
|
all := append(agpl.Core(), enterpriseOnly()...)
|
||||||
return all
|
return all
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user