Skip to content

feat: add groups support to the CLI #4755

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func create() *cobra.Command {
return err
}

organization, err := currentOrganization(cmd, client)
organization, err := CurrentOrganization(cmd, client)
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions cli/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func login() *cobra.Command {
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)
}
if !hasInitialUser {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), caret+"Your Coder deployment hasn't been set up!\n")
_, _ = fmt.Fprintf(cmd.OutOrStdout(), Caret+"Your Coder deployment hasn't been set up!\n")

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

_, _ = fmt.Fprintf(cmd.OutOrStdout(), caret+"Welcome to Coder, %s! You're authenticated.\n", cliui.Styles.Keyword.Render(resp.Username))
_, _ = fmt.Fprintf(cmd.OutOrStdout(), Caret+"Welcome to Coder, %s! You're authenticated.\n", cliui.Styles.Keyword.Render(resp.Username))
return nil
},
}
Expand Down
2 changes: 1 addition & 1 deletion cli/logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func logout() *cobra.Command {
errorString := strings.TrimRight(errorStringBuilder.String(), "\n")
return xerrors.New("Failed to log out.\n" + errorString)
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), caret+"You are no longer logged in. You can log in using 'coder login <url>'.\n")
_, _ = fmt.Fprintf(cmd.OutOrStdout(), Caret+"You are no longer logged in. You can log in using 'coder login <url>'.\n")
return nil
},
}
Expand Down
2 changes: 1 addition & 1 deletion cli/parameterslist.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func parameterList() *cobra.Command {
return err
}

organization, err := currentOrganization(cmd, client)
organization, err := CurrentOrganization(cmd, client)
if err != nil {
return xerrors.Errorf("get current organization: %w", err)
}
Expand Down
6 changes: 3 additions & 3 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (
)

var (
caret = cliui.Styles.Prompt.String()
Caret = cliui.Styles.Prompt.String()

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

// currentOrganization returns the currently active organization for the authenticated user.
func currentOrganization(cmd *cobra.Command, client *codersdk.Client) (codersdk.Organization, error) {
// CurrentOrganization returns the currently active organization for the authenticated user.
func CurrentOrganization(cmd *cobra.Command, client *codersdk.Client) (codersdk.Organization, error) {
orgs, err := client.OrganizationsByUser(cmd.Context(), codersdk.Me)
if err != nil {
return codersdk.Organization{}, nil
Expand Down
2 changes: 1 addition & 1 deletion cli/templatecreate.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func templateCreate() *cobra.Command {
return err
}

organization, err := currentOrganization(cmd, client)
organization, err := CurrentOrganization(cmd, client)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cli/templatedelete.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func templateDelete() *cobra.Command {
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
organization, err := CurrentOrganization(cmd, client)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cli/templateedit.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func templateEdit() *cobra.Command {
if err != nil {
return xerrors.Errorf("create client: %w", err)
}
organization, err := currentOrganization(cmd, client)
organization, err := CurrentOrganization(cmd, client)
if err != nil {
return xerrors.Errorf("get current organization: %w", err)
}
Expand Down
4 changes: 2 additions & 2 deletions cli/templatelist.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func templateList() *cobra.Command {
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
organization, err := CurrentOrganization(cmd, client)
if err != nil {
return err
}
Expand All @@ -30,7 +30,7 @@ func templateList() *cobra.Command {
}

if len(templates) == 0 {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s No templates found in %s! Create one:\n\n", caret, color.HiWhiteString(organization.Name))
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s No templates found in %s! Create one:\n\n", Caret, color.HiWhiteString(organization.Name))
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), color.HiMagentaString(" $ coder templates create <directory>\n"))
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion cli/templatepull.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func templatePull() *cobra.Command {
}

// TODO(JonA): Do we need to add a flag for organization?
organization, err := currentOrganization(cmd, client)
organization, err := CurrentOrganization(cmd, client)
if err != nil {
return xerrors.Errorf("current organization: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion cli/templatepush.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func templatePush() *cobra.Command {
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
organization, err := CurrentOrganization(cmd, client)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cli/templateversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func templateVersionsList() *cobra.Command {
if err != nil {
return xerrors.Errorf("create client: %w", err)
}
organization, err := currentOrganization(cmd, client)
organization, err := CurrentOrganization(cmd, client)
if err != nil {
return xerrors.Errorf("get current organization: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion cli/usercreate.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func userCreate() *cobra.Command {
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
organization, err := CurrentOrganization(cmd, client)
if err != nil {
return err
}
Expand Down
37 changes: 37 additions & 0 deletions coderd/httpmw/groupparam.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"

"github.com/go-chi/chi/v5"
"golang.org/x/xerrors"

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

func ExtractGroupByNameParam(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
org = OrganizationParam(r)
)

name := chi.URLParam(r, "groupName")
if name == "" {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Missing group name in URL",
})
return
}

group, err := db.GetGroupByOrgAndName(ctx, database.GetGroupByOrgAndNameParams{
OrganizationID: org.ID,
Name: name,
})
if xerrors.Is(err, sql.ErrNoRows) {
httpapi.ResourceNotFound(rw)
return
}
if err != nil {
httpapi.InternalServerError(rw, err)
return
}

ctx = context.WithValue(ctx, groupParamContextKey{}, group)
chi.RouteContext(ctx).URLParams.Add("organization", group.OrganizationID.String())
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
}

// ExtraGroupParam grabs a group from the "group" URL parameter.
func ExtractGroupParam(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
Expand Down
17 changes: 17 additions & 0 deletions codersdk/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,23 @@ func (c *Client) GroupsByOrganization(ctx context.Context, orgID uuid.UUID) ([]G
return groups, json.NewDecoder(res.Body).Decode(&groups)
}

func (c *Client) GroupByOrgAndName(ctx context.Context, orgID uuid.UUID, name string) (Group, error) {
res, err := c.Request(ctx, http.MethodGet,
fmt.Sprintf("/api/v2/organizations/%s/groups/%s", orgID.String(), name),
nil,
)
if err != nil {
return Group{}, xerrors.Errorf("make request: %w", err)
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return Group{}, readBodyAsError(res)
}
var resp Group
return resp, json.NewDecoder(res.Body).Decode(&resp)
}

func (c *Client) Group(ctx context.Context, group uuid.UUID) (Group, error) {
res, err := c.Request(ctx, http.MethodGet,
fmt.Sprintf("/api/v2/groups/%s", group.String()),
Expand Down
54 changes: 54 additions & 0 deletions enterprise/cli/groupcreate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package cli

import (
"fmt"

"github.com/spf13/cobra"
"golang.org/x/xerrors"

agpl "github.com/coder/coder/cli"
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
)

func groupCreate() *cobra.Command {
var (
avatarURL string
)
cmd := &cobra.Command{
Use: "create <name>",
Short: "Create a user group",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var (
ctx = cmd.Context()
)

client, err := agpl.CreateClient(cmd)
if err != nil {
return xerrors.Errorf("create client: %w", err)
}

org, err := agpl.CurrentOrganization(cmd, client)
if err != nil {
return xerrors.Errorf("current organization: %w", err)
}

group, err := client.CreateGroup(ctx, org.ID, codersdk.CreateGroupRequest{
Name: args[0],
AvatarURL: avatarURL,
})
if err != nil {
return xerrors.Errorf("create group: %w", err)
}

_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Successfully created group %s!\n", cliui.Styles.Keyword.Render(group.Name))
return nil
},
}

cliflag.StringVarP(cmd.Flags(), &avatarURL, "avatar-url", "u", "CODER_AVATAR_URL", "", "set an avatar for a group")

return cmd
}
48 changes: 48 additions & 0 deletions enterprise/cli/groupcreate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package cli_test

import (
"fmt"
"testing"

"github.com/stretchr/testify/require"

"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/enterprise/cli"
"github.com/coder/coder/enterprise/coderd/coderdenttest"
"github.com/coder/coder/pty/ptytest"
)

func TestCreateGroup(t *testing.T) {
t.Parallel()

t.Run("OK", func(t *testing.T) {
t.Parallel()

client := coderdenttest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
TemplateRBAC: true,
})

var (
groupName = "test"
avatarURL = "https://example.com"
)

cmd, root := clitest.NewWithSubcommands(t, cli.EnterpriseSubcommands(), "groups",
"create", groupName,
"--avatar-url", avatarURL,
)

pty := ptytest.New(t)
cmd.SetOut(pty.Output())
clitest.SetupConfig(t, client, root)

err := cmd.Execute()
require.NoError(t, err)

pty.ExpectMatch(fmt.Sprintf("Successfully created group %s!", cliui.Styles.Keyword.Render(groupName)))
})
}
50 changes: 50 additions & 0 deletions enterprise/cli/groupdelete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package cli

import (
"fmt"

"github.com/spf13/cobra"
"golang.org/x/xerrors"

agpl "github.com/coder/coder/cli"
"github.com/coder/coder/cli/cliui"
)

func groupDelete() *cobra.Command {
cmd := &cobra.Command{
Use: "delete <name>",
Short: "Delete a user group",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var (
ctx = cmd.Context()
groupName = args[0]
)

client, err := agpl.CreateClient(cmd)
if err != nil {
return xerrors.Errorf("create client: %w", err)
}

org, err := agpl.CurrentOrganization(cmd, client)
if err != nil {
return xerrors.Errorf("current organization: %w", err)
}

group, err := client.GroupByOrgAndName(ctx, org.ID, groupName)
if err != nil {
return xerrors.Errorf("group by org and name: %w", err)
}

err = client.DeleteGroup(ctx, group.ID)
if err != nil {
return xerrors.Errorf("delete group: %w", err)
}

_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Successfully deleted group %s!\n", cliui.Styles.Keyword.Render(group.Name))
return nil
},
}

return cmd
}
Loading