diff --git a/cli/clitest/clitest.go b/cli/clitest/clitest.go index c37e652bef39f..5a7160ffd843f 100644 --- a/cli/clitest/clitest.go +++ b/cli/clitest/clitest.go @@ -21,7 +21,13 @@ import ( // New creates a CLI instance with a configuration pointed to a // temporary testing directory. 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() root := config.Root(dir) cmd.SetArgs(append([]string{"--global-config", dir}, args...)) diff --git a/cli/configssh.go b/cli/configssh.go index a3f9a4517c8e0..9f6177a6be5a1 100644 --- a/cli/configssh.go +++ b/cli/configssh.go @@ -158,7 +158,7 @@ func configSSH() *cobra.Command { ), Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, _ []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/cli/create.go b/cli/create.go index f1fe396fbe088..1829cd71f099b 100644 --- a/cli/create.go +++ b/cli/create.go @@ -27,7 +27,7 @@ func create() *cobra.Command { Use: "create [name]", Short: "Create a workspace from a template", RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/cli/delete.go b/cli/delete.go index 5950a7b03445c..37134880ce108 100644 --- a/cli/delete.go +++ b/cli/delete.go @@ -28,7 +28,7 @@ func deleteWorkspace() *cobra.Command { return err } - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/cli/features.go b/cli/features.go index d7f0fbdee1056..5d631fc04977f 100644 --- a/cli/features.go +++ b/cli/features.go @@ -36,7 +36,7 @@ func featuresList() *cobra.Command { Use: "list", Aliases: []string{"ls"}, RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/cli/list.go b/cli/list.go index 4ecc7737d4a68..3259d3f3f09a4 100644 --- a/cli/list.go +++ b/cli/list.go @@ -65,7 +65,7 @@ func list() *cobra.Command { Aliases: []string{"ls"}, Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/cli/logout.go b/cli/logout.go index cab5f46203177..3ad84e7b61163 100644 --- a/cli/logout.go +++ b/cli/logout.go @@ -16,7 +16,7 @@ func logout() *cobra.Command { Use: "logout", Short: "Remove the local authenticated session", RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/cli/parameterslist.go b/cli/parameterslist.go index b9c675b4cfe5e..438b15acea419 100644 --- a/cli/parameterslist.go +++ b/cli/parameterslist.go @@ -22,7 +22,7 @@ func parameterList() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { scope, name := args[0], args[1] - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/cli/portforward.go b/cli/portforward.go index 9a13ef6eab6dd..b9db1ffaa4c22 100644 --- a/cli/portforward.go +++ b/cli/portforward.go @@ -70,7 +70,7 @@ func portForward() *cobra.Command { return xerrors.New("no port-forwards requested") } - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/cli/publickey.go b/cli/publickey.go index 0be912bcd9cd1..5b603516ba7fa 100644 --- a/cli/publickey.go +++ b/cli/publickey.go @@ -20,7 +20,7 @@ func publickey() *cobra.Command { Aliases: []string{"pubkey"}, Short: "Output your public key for Git operations", RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return xerrors.Errorf("create codersdk client: %w", err) } diff --git a/cli/root.go b/cli/root.go index 250052282bde9..cbece56fcf4cc 100644 --- a/cli/root.go +++ b/cli/root.go @@ -114,7 +114,7 @@ func Root(subcommands []*cobra.Command) *cobra.Command { return nil } - client, err := createClient(cmd) + client, err := CreateClient(cmd) // If the client is unauthenticated we can ignore the check. // The child commands should handle an unauthenticated client. if xerrors.Is(err, errUnauthenticated) { @@ -190,9 +190,9 @@ func isTest() bool { 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. -func createClient(cmd *cobra.Command) (*codersdk.Client, error) { +func CreateClient(cmd *cobra.Command) (*codersdk.Client, error) { root := createConfig(cmd) rawURL, err := cmd.Flags().GetString(varURL) 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. -// 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) { rawURL, err := cmd.Flags().GetString(varAgentURL) if err != nil { diff --git a/cli/schedule.go b/cli/schedule.go index b583915baf7f7..5e9de6c4869f1 100644 --- a/cli/schedule.go +++ b/cli/schedule.go @@ -77,7 +77,7 @@ func scheduleShow() *cobra.Command { Long: scheduleShowDescriptionLong, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } @@ -106,7 +106,7 @@ func scheduleStart() *cobra.Command { Long: scheduleStartDescriptionLong, Args: cobra.RangeArgs(2, 4), RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } @@ -156,7 +156,7 @@ func scheduleStop() *cobra.Command { Short: "Edit workspace stop schedule", Long: scheduleStopDescriptionLong, RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } @@ -207,7 +207,7 @@ func scheduleOverride() *cobra.Command { return err } - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return xerrors.Errorf("create client: %w", err) } diff --git a/cli/show.go b/cli/show.go index 145e8d6e3f3d0..3a0ed3973a96f 100644 --- a/cli/show.go +++ b/cli/show.go @@ -14,7 +14,7 @@ func show() *cobra.Command { Short: "Show details of a workspace's resources and agents", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/cli/ssh.go b/cli/ssh.go index 53e6ce88f9c75..084a59e2ba576 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -54,7 +54,7 @@ func ssh() *cobra.Command { ctx, cancel := context.WithCancel(cmd.Context()) defer cancel() - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/cli/start.go b/cli/start.go index a3fd586a0d9b9..9ceeed2c2b998 100644 --- a/cli/start.go +++ b/cli/start.go @@ -17,7 +17,7 @@ func start() *cobra.Command { Short: "Build a workspace with the start state", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/cli/state.go b/cli/state.go index 498397a88ce9d..d6e5a4e549608 100644 --- a/cli/state.go +++ b/cli/state.go @@ -27,7 +27,7 @@ func statePull() *cobra.Command { Use: "pull [file]", Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } @@ -68,7 +68,7 @@ func statePush() *cobra.Command { Use: "push ", Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/cli/stop.go b/cli/stop.go index 381e7634b9a90..c2bfa76e4df02 100644 --- a/cli/stop.go +++ b/cli/stop.go @@ -25,7 +25,7 @@ func stop() *cobra.Command { return err } - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/cli/templatecreate.go b/cli/templatecreate.go index 50d2e16619edd..3f8c7fd2e62e9 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -32,7 +32,7 @@ func templateCreate() *cobra.Command { Short: "Create a template from the current directory or as specified by flag", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/cli/templatedelete.go b/cli/templatedelete.go index aa4f5d6dda68e..8b1b1903a9b68 100644 --- a/cli/templatedelete.go +++ b/cli/templatedelete.go @@ -23,7 +23,7 @@ func templateDelete() *cobra.Command { templates = []codersdk.Template{} ) - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/cli/templateedit.go b/cli/templateedit.go index 166c29793b9cc..df3c582203d13 100644 --- a/cli/templateedit.go +++ b/cli/templateedit.go @@ -25,7 +25,7 @@ func templateEdit() *cobra.Command { Args: cobra.ExactArgs(1), Short: "Edit the metadata of a template by name.", RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return xerrors.Errorf("create client: %w", err) } diff --git a/cli/templatelist.go b/cli/templatelist.go index 2350f44086102..b7dc29ac497fb 100644 --- a/cli/templatelist.go +++ b/cli/templatelist.go @@ -16,7 +16,7 @@ func templateList() *cobra.Command { Short: "List all the templates available for the organization", Aliases: []string{"ls"}, RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/cli/templatepull.go b/cli/templatepull.go index 2b2509ae0b0bf..5660261c5158d 100644 --- a/cli/templatepull.go +++ b/cli/templatepull.go @@ -29,7 +29,7 @@ func templatePull() *cobra.Command { dest = args[1] } - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return xerrors.Errorf("create client: %w", err) } diff --git a/cli/templatepush.go b/cli/templatepush.go index c4be5165ed6c3..ccc6e800b01d4 100644 --- a/cli/templatepush.go +++ b/cli/templatepush.go @@ -29,7 +29,7 @@ func templatePush() *cobra.Command { Args: cobra.MaximumNArgs(1), Short: "Push a new template version from the current directory or as specified by flag", RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/cli/templateversions.go b/cli/templateversions.go index 8dcb51bd957ff..5c27970d6bedd 100644 --- a/cli/templateversions.go +++ b/cli/templateversions.go @@ -38,7 +38,7 @@ func templateVersionsList() *cobra.Command { Args: cobra.ExactArgs(1), Short: "List all the versions of the specified template", RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return xerrors.Errorf("create client: %w", err) } diff --git a/cli/update.go b/cli/update.go index 53d73800255c9..ed3a900cc1917 100644 --- a/cli/update.go +++ b/cli/update.go @@ -22,7 +22,7 @@ func update() *cobra.Command { Args: cobra.ExactArgs(1), Short: "Update a workspace to the latest template version", RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/cli/usercreate.go b/cli/usercreate.go index 899b8b4fb3d98..73bc0fb8f9947 100644 --- a/cli/usercreate.go +++ b/cli/usercreate.go @@ -21,7 +21,7 @@ func userCreate() *cobra.Command { cmd := &cobra.Command{ Use: "create", RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/cli/userlist.go b/cli/userlist.go index a3e0c6a9f36c6..b9e9116791c61 100644 --- a/cli/userlist.go +++ b/cli/userlist.go @@ -26,7 +26,7 @@ func userList() *cobra.Command { Use: "list", Aliases: []string{"ls"}, RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } @@ -76,7 +76,7 @@ func userSingle() *cobra.Command { ), Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/cli/userstatus.go b/cli/userstatus.go index 577be8e91f822..0b249416bed58 100644 --- a/cli/userstatus.go +++ b/cli/userstatus.go @@ -43,7 +43,7 @@ func createUserStatusCommand(sdkStatus codersdk.UserStatus) *cobra.Command { }, ), RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/cli/wireguardtunnel.go b/cli/wireguardtunnel.go index 592f8b4069f0d..488f7ad342074 100644 --- a/cli/wireguardtunnel.go +++ b/cli/wireguardtunnel.go @@ -67,7 +67,7 @@ func wireguardPortForward() *cobra.Command { return xerrors.New("no port-forwards requested") } - client, err := createClient(cmd) + client, err := CreateClient(cmd) if err != nil { return err } diff --git a/enterprise/cli/licenses.go b/enterprise/cli/licenses.go new file mode 100644 index 0000000000000..c548b74f31893 --- /dev/null +++ b/enterprise/cli/licenses.go @@ -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") +} diff --git a/enterprise/cli/licenses_test.go b/enterprise/cli/licenses_test.go new file mode 100644 index 0000000000000..f56be0b79ec45 --- /dev/null +++ b/enterprise/cli/licenses_test.go @@ -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) +} diff --git a/enterprise/cli/root.go b/enterprise/cli/root.go index 114c283fa6f2e..31546b5d679d0 100644 --- a/enterprise/cli/root.go +++ b/enterprise/cli/root.go @@ -7,7 +7,14 @@ import ( "github.com/coder/coder/enterprise/coderd" ) +func enterpriseOnly() []*cobra.Command { + return []*cobra.Command{ + agpl.Server(coderd.NewEnterprise), + licenses(), + } +} + func EnterpriseSubcommands() []*cobra.Command { - all := append(agpl.Core(), agpl.Server(coderd.NewEnterprise)) + all := append(agpl.Core(), enterpriseOnly()...) return all }