Skip to content

Commit 90f77a3

Browse files
authored
feat: add groups support to the CLI (#4755)
1 parent ce2a7d4 commit 90f77a3

28 files changed

+766
-18
lines changed

cli/create.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func create() *cobra.Command {
3333
return err
3434
}
3535

36-
organization, err := currentOrganization(cmd, client)
36+
organization, err := CurrentOrganization(cmd, client)
3737
if err != nil {
3838
return err
3939
}

cli/login.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func login() *cobra.Command {
8686
return xerrors.Errorf("Failed to check server %q for first user, is the URL correct and is coder accessible from your browser? Error - has initial user: %w", serverURL.String(), err)
8787
}
8888
if !hasInitialUser {
89-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), caret+"Your Coder deployment hasn't been set up!\n")
89+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), Caret+"Your Coder deployment hasn't been set up!\n")
9090

9191
if username == "" {
9292
if !isTTY(cmd) {
@@ -244,7 +244,7 @@ func login() *cobra.Command {
244244
return xerrors.Errorf("write server url: %w", err)
245245
}
246246

247-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), caret+"Welcome to Coder, %s! You're authenticated.\n", cliui.Styles.Keyword.Render(resp.Username))
247+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), Caret+"Welcome to Coder, %s! You're authenticated.\n", cliui.Styles.Keyword.Render(resp.Username))
248248
return nil
249249
},
250250
}

cli/logout.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func logout() *cobra.Command {
6767
errorString := strings.TrimRight(errorStringBuilder.String(), "\n")
6868
return xerrors.New("Failed to log out.\n" + errorString)
6969
}
70-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), caret+"You are no longer logged in. You can log in using 'coder login <url>'.\n")
70+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), Caret+"You are no longer logged in. You can log in using 'coder login <url>'.\n")
7171
return nil
7272
},
7373
}

cli/parameterslist.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func parameterList() *cobra.Command {
2727
return err
2828
}
2929

30-
organization, err := currentOrganization(cmd, client)
30+
organization, err := CurrentOrganization(cmd, client)
3131
if err != nil {
3232
return xerrors.Errorf("get current organization: %w", err)
3333
}

cli/root.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import (
3030
)
3131

3232
var (
33-
caret = cliui.Styles.Prompt.String()
33+
Caret = cliui.Styles.Prompt.String()
3434

3535
// Applied as annotations to workspace commands
3636
// so they display in a separated "help" section.
@@ -352,8 +352,8 @@ func createAgentClient(cmd *cobra.Command) (*codersdk.Client, error) {
352352
return client, nil
353353
}
354354

355-
// currentOrganization returns the currently active organization for the authenticated user.
356-
func currentOrganization(cmd *cobra.Command, client *codersdk.Client) (codersdk.Organization, error) {
355+
// CurrentOrganization returns the currently active organization for the authenticated user.
356+
func CurrentOrganization(cmd *cobra.Command, client *codersdk.Client) (codersdk.Organization, error) {
357357
orgs, err := client.OrganizationsByUser(cmd.Context(), codersdk.Me)
358358
if err != nil {
359359
return codersdk.Organization{}, nil

cli/templatecreate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func templateCreate() *cobra.Command {
4040
return err
4141
}
4242

43-
organization, err := currentOrganization(cmd, client)
43+
organization, err := CurrentOrganization(cmd, client)
4444
if err != nil {
4545
return err
4646
}

cli/templatedelete.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func templateDelete() *cobra.Command {
2727
if err != nil {
2828
return err
2929
}
30-
organization, err := currentOrganization(cmd, client)
30+
organization, err := CurrentOrganization(cmd, client)
3131
if err != nil {
3232
return err
3333
}

cli/templateedit.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func templateEdit() *cobra.Command {
2929
if err != nil {
3030
return xerrors.Errorf("create client: %w", err)
3131
}
32-
organization, err := currentOrganization(cmd, client)
32+
organization, err := CurrentOrganization(cmd, client)
3333
if err != nil {
3434
return xerrors.Errorf("get current organization: %w", err)
3535
}

cli/templatelist.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func templateList() *cobra.Command {
2020
if err != nil {
2121
return err
2222
}
23-
organization, err := currentOrganization(cmd, client)
23+
organization, err := CurrentOrganization(cmd, client)
2424
if err != nil {
2525
return err
2626
}
@@ -30,7 +30,7 @@ func templateList() *cobra.Command {
3030
}
3131

3232
if len(templates) == 0 {
33-
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s No templates found in %s! Create one:\n\n", caret, color.HiWhiteString(organization.Name))
33+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s No templates found in %s! Create one:\n\n", Caret, color.HiWhiteString(organization.Name))
3434
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), color.HiMagentaString(" $ coder templates create <directory>\n"))
3535
return nil
3636
}

cli/templatepull.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func templatePull() *cobra.Command {
3535
}
3636

3737
// TODO(JonA): Do we need to add a flag for organization?
38-
organization, err := currentOrganization(cmd, client)
38+
organization, err := CurrentOrganization(cmd, client)
3939
if err != nil {
4040
return xerrors.Errorf("current organization: %w", err)
4141
}

cli/templatepush.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func templatePush() *cobra.Command {
3434
if err != nil {
3535
return err
3636
}
37-
organization, err := currentOrganization(cmd, client)
37+
organization, err := CurrentOrganization(cmd, client)
3838
if err != nil {
3939
return err
4040
}

cli/templateversions.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func templateVersionsList() *cobra.Command {
4545
if err != nil {
4646
return xerrors.Errorf("create client: %w", err)
4747
}
48-
organization, err := currentOrganization(cmd, client)
48+
organization, err := CurrentOrganization(cmd, client)
4949
if err != nil {
5050
return xerrors.Errorf("get current organization: %w", err)
5151
}

cli/usercreate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func userCreate() *cobra.Command {
2525
if err != nil {
2626
return err
2727
}
28-
organization, err := currentOrganization(cmd, client)
28+
organization, err := CurrentOrganization(cmd, client)
2929
if err != nil {
3030
return err
3131
}

coderd/httpmw/groupparam.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/http"
88

99
"github.com/go-chi/chi/v5"
10+
"golang.org/x/xerrors"
1011

1112
"github.com/coder/coder/coderd/database"
1213
"github.com/coder/coder/coderd/httpapi"
@@ -24,6 +25,42 @@ func GroupParam(r *http.Request) database.Group {
2425
return group
2526
}
2627

28+
func ExtractGroupByNameParam(db database.Store) func(http.Handler) http.Handler {
29+
return func(next http.Handler) http.Handler {
30+
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
31+
var (
32+
ctx = r.Context()
33+
org = OrganizationParam(r)
34+
)
35+
36+
name := chi.URLParam(r, "groupName")
37+
if name == "" {
38+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
39+
Message: "Missing group name in URL",
40+
})
41+
return
42+
}
43+
44+
group, err := db.GetGroupByOrgAndName(ctx, database.GetGroupByOrgAndNameParams{
45+
OrganizationID: org.ID,
46+
Name: name,
47+
})
48+
if xerrors.Is(err, sql.ErrNoRows) {
49+
httpapi.ResourceNotFound(rw)
50+
return
51+
}
52+
if err != nil {
53+
httpapi.InternalServerError(rw, err)
54+
return
55+
}
56+
57+
ctx = context.WithValue(ctx, groupParamContextKey{}, group)
58+
chi.RouteContext(ctx).URLParams.Add("organization", group.OrganizationID.String())
59+
next.ServeHTTP(rw, r.WithContext(ctx))
60+
})
61+
}
62+
}
63+
2764
// ExtraGroupParam grabs a group from the "group" URL parameter.
2865
func ExtractGroupParam(db database.Store) func(http.Handler) http.Handler {
2966
return func(next http.Handler) http.Handler {

codersdk/groups.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,23 @@ func (c *Client) GroupsByOrganization(ctx context.Context, orgID uuid.UUID) ([]G
5858
return groups, json.NewDecoder(res.Body).Decode(&groups)
5959
}
6060

61+
func (c *Client) GroupByOrgAndName(ctx context.Context, orgID uuid.UUID, name string) (Group, error) {
62+
res, err := c.Request(ctx, http.MethodGet,
63+
fmt.Sprintf("/api/v2/organizations/%s/groups/%s", orgID.String(), name),
64+
nil,
65+
)
66+
if err != nil {
67+
return Group{}, xerrors.Errorf("make request: %w", err)
68+
}
69+
defer res.Body.Close()
70+
71+
if res.StatusCode != http.StatusOK {
72+
return Group{}, readBodyAsError(res)
73+
}
74+
var resp Group
75+
return resp, json.NewDecoder(res.Body).Decode(&resp)
76+
}
77+
6178
func (c *Client) Group(ctx context.Context, group uuid.UUID) (Group, error) {
6279
res, err := c.Request(ctx, http.MethodGet,
6380
fmt.Sprintf("/api/v2/groups/%s", group.String()),

enterprise/cli/groupcreate.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
"golang.org/x/xerrors"
8+
9+
agpl "github.com/coder/coder/cli"
10+
"github.com/coder/coder/cli/cliflag"
11+
"github.com/coder/coder/cli/cliui"
12+
"github.com/coder/coder/codersdk"
13+
)
14+
15+
func groupCreate() *cobra.Command {
16+
var (
17+
avatarURL string
18+
)
19+
cmd := &cobra.Command{
20+
Use: "create <name>",
21+
Short: "Create a user group",
22+
Args: cobra.ExactArgs(1),
23+
RunE: func(cmd *cobra.Command, args []string) error {
24+
var (
25+
ctx = cmd.Context()
26+
)
27+
28+
client, err := agpl.CreateClient(cmd)
29+
if err != nil {
30+
return xerrors.Errorf("create client: %w", err)
31+
}
32+
33+
org, err := agpl.CurrentOrganization(cmd, client)
34+
if err != nil {
35+
return xerrors.Errorf("current organization: %w", err)
36+
}
37+
38+
group, err := client.CreateGroup(ctx, org.ID, codersdk.CreateGroupRequest{
39+
Name: args[0],
40+
AvatarURL: avatarURL,
41+
})
42+
if err != nil {
43+
return xerrors.Errorf("create group: %w", err)
44+
}
45+
46+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Successfully created group %s!\n", cliui.Styles.Keyword.Render(group.Name))
47+
return nil
48+
},
49+
}
50+
51+
cliflag.StringVarP(cmd.Flags(), &avatarURL, "avatar-url", "u", "CODER_AVATAR_URL", "", "set an avatar for a group")
52+
53+
return cmd
54+
}

enterprise/cli/groupcreate_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package cli_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/coder/coder/cli/clitest"
10+
"github.com/coder/coder/cli/cliui"
11+
"github.com/coder/coder/coderd/coderdtest"
12+
"github.com/coder/coder/enterprise/cli"
13+
"github.com/coder/coder/enterprise/coderd/coderdenttest"
14+
"github.com/coder/coder/pty/ptytest"
15+
)
16+
17+
func TestCreateGroup(t *testing.T) {
18+
t.Parallel()
19+
20+
t.Run("OK", func(t *testing.T) {
21+
t.Parallel()
22+
23+
client := coderdenttest.New(t, nil)
24+
coderdtest.CreateFirstUser(t, client)
25+
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
26+
TemplateRBAC: true,
27+
})
28+
29+
var (
30+
groupName = "test"
31+
avatarURL = "https://example.com"
32+
)
33+
34+
cmd, root := clitest.NewWithSubcommands(t, cli.EnterpriseSubcommands(), "groups",
35+
"create", groupName,
36+
"--avatar-url", avatarURL,
37+
)
38+
39+
pty := ptytest.New(t)
40+
cmd.SetOut(pty.Output())
41+
clitest.SetupConfig(t, client, root)
42+
43+
err := cmd.Execute()
44+
require.NoError(t, err)
45+
46+
pty.ExpectMatch(fmt.Sprintf("Successfully created group %s!", cliui.Styles.Keyword.Render(groupName)))
47+
})
48+
}

enterprise/cli/groupdelete.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
"golang.org/x/xerrors"
8+
9+
agpl "github.com/coder/coder/cli"
10+
"github.com/coder/coder/cli/cliui"
11+
)
12+
13+
func groupDelete() *cobra.Command {
14+
cmd := &cobra.Command{
15+
Use: "delete <name>",
16+
Short: "Delete a user group",
17+
Args: cobra.ExactArgs(1),
18+
RunE: func(cmd *cobra.Command, args []string) error {
19+
var (
20+
ctx = cmd.Context()
21+
groupName = args[0]
22+
)
23+
24+
client, err := agpl.CreateClient(cmd)
25+
if err != nil {
26+
return xerrors.Errorf("create client: %w", err)
27+
}
28+
29+
org, err := agpl.CurrentOrganization(cmd, client)
30+
if err != nil {
31+
return xerrors.Errorf("current organization: %w", err)
32+
}
33+
34+
group, err := client.GroupByOrgAndName(ctx, org.ID, groupName)
35+
if err != nil {
36+
return xerrors.Errorf("group by org and name: %w", err)
37+
}
38+
39+
err = client.DeleteGroup(ctx, group.ID)
40+
if err != nil {
41+
return xerrors.Errorf("delete group: %w", err)
42+
}
43+
44+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Successfully deleted group %s!\n", cliui.Styles.Keyword.Render(group.Name))
45+
return nil
46+
},
47+
}
48+
49+
return cmd
50+
}

0 commit comments

Comments
 (0)