From 4c4d966c7b19bac23a993ed32ae28a9ab6a029b4 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 14 Jul 2023 09:48:02 -0400 Subject: [PATCH] feat: add ability to make workspace for other user from cli (#8481) * feat: add ability to make workspace for other user from cli * Add example to show functionality --- cli/create.go | 18 ++++++-- cli/create_test.go | 57 +++++++++++++++++++++++++ cli/root.go | 32 ++++++++------ cli/testdata/coder_create_--help.golden | 4 ++ docs/cli/create.md | 8 ++++ 5 files changed, 102 insertions(+), 17 deletions(-) diff --git a/cli/create.go b/cli/create.go index bb2b9647bf..a7673a87d8 100644 --- a/cli/create.go +++ b/cli/create.go @@ -31,15 +31,25 @@ func (r *RootCmd) create() *clibase.Cmd { Annotations: workspaceCommand, Use: "create [name]", Short: "Create a workspace", - Middleware: clibase.Chain(r.InitClient(client)), + Long: formatExamples( + example{ + Description: "Create a workspace for another user (if you have permission)", + Command: "coder create /", + }, + ), + Middleware: clibase.Chain(r.InitClient(client)), Handler: func(inv *clibase.Invocation) error { organization, err := CurrentOrganization(inv, client) if err != nil { return err } + workspaceOwner := codersdk.Me if len(inv.Args) >= 1 { - workspaceName = inv.Args[0] + workspaceOwner, workspaceName, err = splitNamedWorkspace(inv.Args[0]) + if err != nil { + return err + } } if workspaceName == "" { @@ -58,7 +68,7 @@ func (r *RootCmd) create() *clibase.Cmd { } } - _, err = client.WorkspaceByOwnerAndName(inv.Context(), codersdk.Me, workspaceName, codersdk.WorkspaceOptions{}) + _, err = client.WorkspaceByOwnerAndName(inv.Context(), workspaceOwner, workspaceName, codersdk.WorkspaceOptions{}) if err == nil { return xerrors.Errorf("A workspace already exists named %q!", workspaceName) } @@ -146,7 +156,7 @@ func (r *RootCmd) create() *clibase.Cmd { ttlMillis = &template.MaxTTLMillis } - workspace, err := client.CreateWorkspace(inv.Context(), organization.ID, codersdk.Me, codersdk.CreateWorkspaceRequest{ + workspace, err := client.CreateWorkspace(inv.Context(), organization.ID, workspaceOwner, codersdk.CreateWorkspaceRequest{ TemplateID: template.ID, Name: workspaceName, AutostartSchedule: schedSpec, diff --git a/cli/create_test.go b/cli/create_test.go index 753c8b9e2d..2e6df97443 100644 --- a/cli/create_test.go +++ b/cli/create_test.go @@ -79,6 +79,63 @@ func TestCreate(t *testing.T) { } }) + t.Run("CreateForOtherUser", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + owner := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionApply: provisionCompleteWithAgent, + ProvisionPlan: provisionCompleteWithAgent, + }) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) + _, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) + args := []string{ + "create", + user.Username + "/their-workspace", + "--template", template.Name, + "--start-at", "9:30AM Mon-Fri US/Central", + "--stop-after", "8h", + } + + inv, root := clitest.New(t, args...) + clitest.SetupConfig(t, client, root) + doneChan := make(chan struct{}) + pty := ptytest.New(t).Attach(inv) + go func() { + defer close(doneChan) + err := inv.Run() + assert.NoError(t, err) + }() + matches := []struct { + match string + write string + }{ + {match: "compute.main"}, + {match: "smith (linux, i386)"}, + {match: "Confirm create", write: "yes"}, + } + for _, m := range matches { + pty.ExpectMatch(m.match) + if len(m.write) > 0 { + pty.WriteLine(m.write) + } + } + <-doneChan + + ws, err := client.WorkspaceByOwnerAndName(context.Background(), user.Username, "their-workspace", codersdk.WorkspaceOptions{}) + if assert.NoError(t, err, "expected workspace to be created") { + assert.Equal(t, ws.TemplateName, template.Name) + if assert.NotNil(t, ws.AutostartSchedule) { + assert.Equal(t, *ws.AutostartSchedule, "CRON_TZ=US/Central 30 9 * * Mon-Fri") + } + if assert.NotNil(t, ws.TTLMillis) { + assert.Equal(t, *ws.TTLMillis, 8*time.Hour.Milliseconds()) + } + } + }) + t.Run("InheritStopAfterFromTemplate", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) diff --git a/cli/root.go b/cli/root.go index c5843f97f5..4c268235a0 100644 --- a/cli/root.go +++ b/cli/root.go @@ -626,24 +626,30 @@ func CurrentOrganization(inv *clibase.Invocation, client *codersdk.Client) (code return orgs[0], nil } +func splitNamedWorkspace(identifier string) (owner string, workspaceName string, err error) { + parts := strings.Split(identifier, "/") + + switch len(parts) { + case 1: + owner = codersdk.Me + workspaceName = parts[0] + case 2: + owner = parts[0] + workspaceName = parts[1] + default: + return "", "", xerrors.Errorf("invalid workspace name: %q", identifier) + } + return owner, workspaceName, nil +} + // namedWorkspace fetches and returns a workspace by an identifier, which may be either // a bare name (for a workspace owned by the current user) or a "user/workspace" combination, // where user is either a username or UUID. func namedWorkspace(ctx context.Context, client *codersdk.Client, identifier string) (codersdk.Workspace, error) { - parts := strings.Split(identifier, "/") - - var owner, name string - switch len(parts) { - case 1: - owner = codersdk.Me - name = parts[0] - case 2: - owner = parts[0] - name = parts[1] - default: - return codersdk.Workspace{}, xerrors.Errorf("invalid workspace name: %q", identifier) + owner, name, err := splitNamedWorkspace(identifier) + if err != nil { + return codersdk.Workspace{}, err } - return client.WorkspaceByOwnerAndName(ctx, owner, name, codersdk.WorkspaceOptions{}) } diff --git a/cli/testdata/coder_create_--help.golden b/cli/testdata/coder_create_--help.golden index bf0b40660b..be40c4907b 100644 --- a/cli/testdata/coder_create_--help.golden +++ b/cli/testdata/coder_create_--help.golden @@ -2,6 +2,10 @@ Usage: coder create [flags] [name] Create a workspace +- Create a workspace for another user (if you have permission): + +  $ coder create /  + Options --build-options bool Prompt for one-time build options defined with ephemeral parameters. diff --git a/docs/cli/create.md b/docs/cli/create.md index d959908075..166f0037fd 100644 --- a/docs/cli/create.md +++ b/docs/cli/create.md @@ -10,6 +10,14 @@ Create a workspace coder create [flags] [name] ``` +## Description + +```console + - Create a workspace for another user (if you have permission): + + $ coder create / +``` + ## Options ### --build-options