Skip to content

Commit d2998c6

Browse files
authored
feat: implement organization context in the cli (#12259)
* feat: implement organization context in the cli `coder org show current`
1 parent f44c89d commit d2998c6

24 files changed

+290
-52
lines changed

cli/config/file.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ func (r Root) PostgresPort() File {
7070
// File provides convenience methods for interacting with *os.File.
7171
type File string
7272

73+
func (f File) Exists() bool {
74+
if f == "" {
75+
return false
76+
}
77+
_, err := os.Stat(string(f))
78+
return err == nil
79+
}
80+
7381
// Delete deletes the file.
7482
func (f File) Delete() error {
7583
if f == "" {

cli/create.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func (r *RootCmd) create() *clibase.Cmd {
4343
),
4444
Middleware: clibase.Chain(r.InitClient(client)),
4545
Handler: func(inv *clibase.Invocation) error {
46-
organization, err := CurrentOrganization(inv, client)
46+
organization, err := CurrentOrganization(r, inv, client)
4747
if err != nil {
4848
return err
4949
}

cli/organization.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/coder/coder/v2/cli/clibase"
8+
"github.com/coder/coder/v2/cli/cliui"
9+
"github.com/coder/coder/v2/codersdk"
10+
)
11+
12+
func (r *RootCmd) organizations() *clibase.Cmd {
13+
cmd := &clibase.Cmd{
14+
Annotations: workspaceCommand,
15+
Use: "organizations [subcommand]",
16+
Short: "Organization related commands",
17+
Aliases: []string{"organization", "org", "orgs"},
18+
Hidden: true, // Hidden until these commands are complete.
19+
Handler: func(inv *clibase.Invocation) error {
20+
return inv.Command.HelpHandler(inv)
21+
},
22+
Children: []*clibase.Cmd{
23+
r.currentOrganization(),
24+
},
25+
}
26+
27+
cmd.Options = clibase.OptionSet{}
28+
return cmd
29+
}
30+
31+
func (r *RootCmd) currentOrganization() *clibase.Cmd {
32+
var (
33+
stringFormat func(orgs []codersdk.Organization) (string, error)
34+
client = new(codersdk.Client)
35+
formatter = cliui.NewOutputFormatter(
36+
cliui.ChangeFormatterData(cliui.TextFormat(), func(data any) (any, error) {
37+
typed, ok := data.([]codersdk.Organization)
38+
if !ok {
39+
// This should never happen
40+
return "", fmt.Errorf("expected []Organization, got %T", data)
41+
}
42+
return stringFormat(typed)
43+
}),
44+
cliui.TableFormat([]codersdk.Organization{}, []string{"id", "name", "default"}),
45+
cliui.JSONFormat(),
46+
)
47+
onlyID = false
48+
)
49+
cmd := &clibase.Cmd{
50+
Use: "show [current|me|uuid]",
51+
Short: "Show the organization, if no argument is given, the organization currently in use will be shown.",
52+
Middleware: clibase.Chain(
53+
r.InitClient(client),
54+
clibase.RequireRangeArgs(0, 1),
55+
),
56+
Options: clibase.OptionSet{
57+
{
58+
Name: "only-id",
59+
Description: "Only print the organization ID.",
60+
Required: false,
61+
Flag: "only-id",
62+
Value: clibase.BoolOf(&onlyID),
63+
},
64+
},
65+
Handler: func(inv *clibase.Invocation) error {
66+
orgArg := "current"
67+
if len(inv.Args) >= 1 {
68+
orgArg = inv.Args[0]
69+
}
70+
71+
var orgs []codersdk.Organization
72+
var err error
73+
switch strings.ToLower(orgArg) {
74+
case "current":
75+
stringFormat = func(orgs []codersdk.Organization) (string, error) {
76+
if len(orgs) != 1 {
77+
return "", fmt.Errorf("expected 1 organization, got %d", len(orgs))
78+
}
79+
return fmt.Sprintf("Current CLI Organization: %s (%s)\n", orgs[0].Name, orgs[0].ID.String()), nil
80+
}
81+
org, err := CurrentOrganization(r, inv, client)
82+
if err != nil {
83+
return err
84+
}
85+
orgs = []codersdk.Organization{org}
86+
case "me":
87+
stringFormat = func(orgs []codersdk.Organization) (string, error) {
88+
var str strings.Builder
89+
_, _ = fmt.Fprint(&str, "Organizations you are a member of:\n")
90+
for _, org := range orgs {
91+
_, _ = fmt.Fprintf(&str, "\t%s (%s)\n", org.Name, org.ID.String())
92+
}
93+
return str.String(), nil
94+
}
95+
orgs, err = client.OrganizationsByUser(inv.Context(), codersdk.Me)
96+
if err != nil {
97+
return err
98+
}
99+
default:
100+
stringFormat = func(orgs []codersdk.Organization) (string, error) {
101+
if len(orgs) != 1 {
102+
return "", fmt.Errorf("expected 1 organization, got %d", len(orgs))
103+
}
104+
return fmt.Sprintf("Organization: %s (%s)\n", orgs[0].Name, orgs[0].ID.String()), nil
105+
}
106+
// This works for a uuid or a name
107+
org, err := client.OrganizationByName(inv.Context(), orgArg)
108+
if err != nil {
109+
return err
110+
}
111+
orgs = []codersdk.Organization{org}
112+
}
113+
114+
if onlyID {
115+
for _, org := range orgs {
116+
_, _ = fmt.Fprintf(inv.Stdout, "%s\n", org.ID)
117+
}
118+
} else {
119+
out, err := formatter.Format(inv.Context(), orgs)
120+
if err != nil {
121+
return err
122+
}
123+
_, _ = fmt.Fprint(inv.Stdout, out)
124+
}
125+
return nil
126+
},
127+
}
128+
formatter.AttachOptions(&cmd.Options)
129+
130+
return cmd
131+
}

cli/organization_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package cli_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
"github.com/coder/coder/v2/cli/clitest"
9+
"github.com/coder/coder/v2/coderd/coderdtest"
10+
"github.com/coder/coder/v2/coderd/rbac"
11+
"github.com/coder/coder/v2/codersdk"
12+
"github.com/coder/coder/v2/pty/ptytest"
13+
"github.com/coder/coder/v2/testutil"
14+
)
15+
16+
func TestCurrentOrganization(t *testing.T) {
17+
t.Parallel()
18+
19+
t.Run("OnlyID", func(t *testing.T) {
20+
t.Parallel()
21+
ownerClient := coderdtest.New(t, nil)
22+
first := coderdtest.CreateFirstUser(t, ownerClient)
23+
// Owner is required to make orgs
24+
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, first.OrganizationID, rbac.RoleOwner())
25+
26+
ctx := testutil.Context(t, testutil.WaitMedium)
27+
orgs := []string{"foo", "bar"}
28+
for _, orgName := range orgs {
29+
_, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
30+
Name: orgName,
31+
})
32+
require.NoError(t, err)
33+
}
34+
35+
inv, root := clitest.New(t, "organizations", "show", "--only-id")
36+
clitest.SetupConfig(t, client, root)
37+
pty := ptytest.New(t).Attach(inv)
38+
errC := make(chan error)
39+
go func() {
40+
errC <- inv.Run()
41+
}()
42+
require.NoError(t, <-errC)
43+
pty.ExpectMatch(first.OrganizationID.String())
44+
})
45+
}

cli/root.go

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ func (r *RootCmd) Core() []*clibase.Cmd {
9494
r.tokens(),
9595
r.users(),
9696
r.version(defaultVersionInfo),
97+
r.organizations(),
9798

9899
// Workspace Commands
99100
r.autoupdate(),
@@ -698,14 +699,44 @@ func (r *RootCmd) createAgentClient() (*agentsdk.Client, error) {
698699
}
699700

700701
// CurrentOrganization returns the currently active organization for the authenticated user.
701-
func CurrentOrganization(inv *clibase.Invocation, client *codersdk.Client) (codersdk.Organization, error) {
702+
func CurrentOrganization(r *RootCmd, inv *clibase.Invocation, client *codersdk.Client) (codersdk.Organization, error) {
703+
conf := r.createConfig()
704+
selected := ""
705+
if conf.Organization().Exists() {
706+
org, err := conf.Organization().Read()
707+
if err != nil {
708+
return codersdk.Organization{}, fmt.Errorf("read selected organization from config file %q: %w", conf.Organization(), err)
709+
}
710+
selected = org
711+
}
712+
713+
// Verify the org exists and the user is a member
702714
orgs, err := client.OrganizationsByUser(inv.Context(), codersdk.Me)
703715
if err != nil {
704-
return codersdk.Organization{}, nil
716+
return codersdk.Organization{}, err
705717
}
706-
// For now, we won't use the config to set this.
707-
// Eventually, we will support changing using "coder switch <org>"
708-
return orgs[0], nil
718+
719+
// User manually selected an organization
720+
if selected != "" {
721+
index := slices.IndexFunc(orgs, func(org codersdk.Organization) bool {
722+
return org.Name == selected || org.ID.String() == selected
723+
})
724+
725+
if index < 0 {
726+
return codersdk.Organization{}, xerrors.Errorf("organization %q not found, are you sure you are a member of this organization?", selected)
727+
}
728+
return orgs[index], nil
729+
}
730+
731+
// User did not select an organization, so use the default.
732+
index := slices.IndexFunc(orgs, func(org codersdk.Organization) bool {
733+
return org.IsDefault
734+
})
735+
if index < 0 {
736+
return codersdk.Organization{}, xerrors.Errorf("unable to determine current organization. Use 'coder organizations switch <org>' to select an organization to use")
737+
}
738+
739+
return orgs[index], nil
709740
}
710741

711742
func splitNamedWorkspace(identifier string) (owner string, workspaceName string, err error) {

cli/templatecreate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
6969
}
7070
}
7171

72-
organization, err := CurrentOrganization(inv, client)
72+
organization, err := CurrentOrganization(r, inv, client)
7373
if err != nil {
7474
return err
7575
}

cli/templatecreate_test.go

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -243,19 +243,7 @@ func TestTemplateCreate(t *testing.T) {
243243
assert.Error(t, err)
244244
}()
245245

246-
matches := []struct {
247-
match string
248-
write string
249-
}{
250-
{match: "Upload", write: "yes"},
251-
}
252-
for _, m := range matches {
253-
pty.ExpectMatch(m.match)
254-
if len(m.write) > 0 {
255-
pty.WriteLine(m.write)
256-
}
257-
}
258-
246+
pty.ExpectMatch("context canceled")
259247
<-ctx.Done()
260248
})
261249

cli/templatedelete.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func (r *RootCmd) templateDelete() *clibase.Cmd {
3232
templates = []codersdk.Template{}
3333
)
3434

35-
organization, err := CurrentOrganization(inv, client)
35+
organization, err := CurrentOrganization(r, inv, client)
3636
if err != nil {
3737
return err
3838
}

cli/templateedit.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
7979
}
8080
}
8181

82-
organization, err := CurrentOrganization(inv, client)
82+
organization, err := CurrentOrganization(r, inv, client)
8383
if err != nil {
8484
return xerrors.Errorf("get current organization: %w", err)
8585
}

cli/templatelist.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func (r *RootCmd) templateList() *clibase.Cmd {
2525
r.InitClient(client),
2626
),
2727
Handler: func(inv *clibase.Invocation) error {
28-
organization, err := CurrentOrganization(inv, client)
28+
organization, err := CurrentOrganization(r, inv, client)
2929
if err != nil {
3030
return err
3131
}

cli/templatepull.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func (r *RootCmd) templatePull() *clibase.Cmd {
4444
return xerrors.Errorf("either tar or zip can be selected")
4545
}
4646

47-
organization, err := CurrentOrganization(inv, client)
47+
organization, err := CurrentOrganization(r, inv, client)
4848
if err != nil {
4949
return xerrors.Errorf("get current organization: %w", err)
5050
}

cli/templatepush.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func (r *RootCmd) templatePush() *clibase.Cmd {
4646
Handler: func(inv *clibase.Invocation) error {
4747
uploadFlags.setWorkdir(workdir)
4848

49-
organization, err := CurrentOrganization(inv, client)
49+
organization, err := CurrentOrganization(r, inv, client)
5050
if err != nil {
5151
return err
5252
}

cli/templateversionarchive.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func (r *RootCmd) setArchiveTemplateVersion(archive bool) *clibase.Cmd {
4747
versions []codersdk.TemplateVersion
4848
)
4949

50-
organization, err := CurrentOrganization(inv, client)
50+
organization, err := CurrentOrganization(r, inv, client)
5151
if err != nil {
5252
return err
5353
}
@@ -121,7 +121,7 @@ func (r *RootCmd) archiveTemplateVersions() *clibase.Cmd {
121121
templates = []codersdk.Template{}
122122
)
123123

124-
organization, err := CurrentOrganization(inv, client)
124+
organization, err := CurrentOrganization(r, inv, client)
125125
if err != nil {
126126
return err
127127
}

cli/templateversions.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func (r *RootCmd) templateVersionsList() *clibase.Cmd {
9393
},
9494
},
9595
Handler: func(inv *clibase.Invocation) error {
96-
organization, err := CurrentOrganization(inv, client)
96+
organization, err := CurrentOrganization(r, inv, client)
9797
if err != nil {
9898
return xerrors.Errorf("get current organization: %w", err)
9999
}

cli/usercreate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func (r *RootCmd) userCreate() *clibase.Cmd {
3131
r.InitClient(client),
3232
),
3333
Handler: func(inv *clibase.Invocation) error {
34-
organization, err := CurrentOrganization(inv, client)
34+
organization, err := CurrentOrganization(r, inv, client)
3535
if err != nil {
3636
return err
3737
}

0 commit comments

Comments
 (0)