From d846be68b4d215d491e5da5bc8da8a3e349afb46 Mon Sep 17 00:00:00 2001 From: David Wahler Date: Fri, 3 Jun 2022 16:45:14 +0000 Subject: [PATCH 1/4] fix: support qualified workspace names in CLI commands --- cli/autostart.go | 6 +++--- cli/bump.go | 2 +- cli/delete.go | 2 +- cli/delete_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++ cli/root.go | 22 +++++++++++++++++++ cli/show.go | 3 +-- cli/start.go | 2 +- cli/state.go | 4 ++-- cli/stop.go | 2 +- cli/ttl.go | 6 +++--- 10 files changed, 89 insertions(+), 14 deletions(-) diff --git a/cli/autostart.go b/cli/autostart.go index ddf29191096e4..778937cf3d236 100644 --- a/cli/autostart.go +++ b/cli/autostart.go @@ -46,7 +46,7 @@ func autostartShow() *cobra.Command { return err } - workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) + workspace, err := namedWorkspace(cmd, client, organization.ID, args[0]) if err != nil { return err } @@ -104,7 +104,7 @@ func autostartEnable() *cobra.Command { return err } - workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) + workspace, err := namedWorkspace(cmd, client, organization.ID, args[0]) if err != nil { return err } @@ -147,7 +147,7 @@ func autostartDisable() *cobra.Command { return err } - workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) + workspace, err := namedWorkspace(cmd, client, organization.ID, args[0]) if err != nil { return err } diff --git a/cli/bump.go b/cli/bump.go index f539c8a1eabad..3fea50436fb95 100644 --- a/cli/bump.go +++ b/cli/bump.go @@ -48,7 +48,7 @@ func bump() *cobra.Command { return xerrors.Errorf("get current org: %w", err) } - workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) + workspace, err := namedWorkspace(cmd, client, organization.ID, args[0]) if err != nil { return xerrors.Errorf("get workspace: %w", err) } diff --git a/cli/delete.go b/cli/delete.go index 42be08965fed4..d1f13ea08c540 100644 --- a/cli/delete.go +++ b/cli/delete.go @@ -34,7 +34,7 @@ func delete() *cobra.Command { if err != nil { return err } - workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) + workspace, err := namedWorkspace(cmd, client, organization.ID, args[0]) if err != nil { return err } diff --git a/cli/delete_test.go b/cli/delete_test.go index afbeff869b4a4..8e90c6e132202 100644 --- a/cli/delete_test.go +++ b/cli/delete_test.go @@ -1,13 +1,16 @@ package cli_test import ( + "context" "io" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/coder/coder/cli/clitest" "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/codersdk" "github.com/coder/coder/pty/ptytest" ) @@ -38,4 +41,55 @@ func TestDelete(t *testing.T) { pty.ExpectMatch("Cleaning Up") <-doneChan }) + + t.Run("DifferentUser", func(t *testing.T) { + t.Parallel() + adminClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) + adminUser := coderdtest.CreateFirstUser(t, adminClient) + orgID := adminUser.OrganizationID + client := coderdtest.CreateAnotherUser(t, adminClient, orgID) + user, err := client.User(context.Background(), codersdk.Me) + require.NoError(t, err) + + version := coderdtest.CreateTemplateVersion(t, adminClient, orgID, nil) + coderdtest.AwaitTemplateVersionJob(t, adminClient, version.ID) + template := coderdtest.CreateTemplate(t, adminClient, orgID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, orgID, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + + cmd, root := clitest.New(t, "delete", user.Username+"/"+workspace.Name, "-y") + clitest.SetupConfig(t, adminClient, root) + doneChan := make(chan struct{}) + pty := ptytest.New(t) + cmd.SetIn(pty.Input()) + cmd.SetOut(pty.Output()) + go func() { + defer close(doneChan) + err := cmd.Execute() + // When running with the race detector on, we sometimes get an EOF. + if err != nil { + assert.ErrorIs(t, err, io.EOF) + } + }() + + pty.ExpectMatch("Cleaning Up") + <-doneChan + + workspace, err = client.Workspace(context.Background(), workspace.ID) + require.ErrorContains(t, err, "was deleted") + }) + + t.Run("InvalidWorkspaceIdentifier", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + cmd, root := clitest.New(t, "delete", "a/b/c", "-y") + clitest.SetupConfig(t, client, root) + doneChan := make(chan struct{}) + go func() { + defer close(doneChan) + err := cmd.Execute() + assert.ErrorContains(t, err, "invalid workspace name") + }() + <-doneChan + }) } diff --git a/cli/root.go b/cli/root.go index 96aca0c3267e3..a09bb2bb0f330 100644 --- a/cli/root.go +++ b/cli/root.go @@ -9,6 +9,7 @@ import ( "golang.org/x/xerrors" + "github.com/google/uuid" "github.com/kirsle/configdir" "github.com/mattn/go-isatty" "github.com/spf13/cobra" @@ -176,6 +177,27 @@ func currentOrganization(cmd *cobra.Command, client *codersdk.Client) (codersdk. return orgs[0], 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(cmd *cobra.Command, client *codersdk.Client, orgID uuid.UUID, 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.New("invalid workspace name") + } + + return client.WorkspaceByOwnerAndName(cmd.Context(), orgID, owner, name) +} + // createConfig consumes the global configuration flag to produce a config root. func createConfig(cmd *cobra.Command) config.Root { globalRoot, err := cmd.Flags().GetString(varGlobalConfig) diff --git a/cli/show.go b/cli/show.go index 2c51de16b619d..e2245922821f0 100644 --- a/cli/show.go +++ b/cli/show.go @@ -5,7 +5,6 @@ import ( "golang.org/x/xerrors" "github.com/coder/coder/cli/cliui" - "github.com/coder/coder/codersdk" ) func show() *cobra.Command { @@ -23,7 +22,7 @@ func show() *cobra.Command { if err != nil { return err } - workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) + workspace, err := namedWorkspace(cmd, client, organization.ID, args[0]) if err != nil { return xerrors.Errorf("get workspace: %w", err) } diff --git a/cli/start.go b/cli/start.go index 7e35b22aa915a..2fc142694986d 100644 --- a/cli/start.go +++ b/cli/start.go @@ -32,7 +32,7 @@ func start() *cobra.Command { if err != nil { return err } - workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) + workspace, err := namedWorkspace(cmd, client, organization.ID, args[0]) if err != nil { return err } diff --git a/cli/state.go b/cli/state.go index ec43a2711571b..8cd7828532476 100644 --- a/cli/state.go +++ b/cli/state.go @@ -35,7 +35,7 @@ func statePull() *cobra.Command { return err } - workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) + workspace, err := namedWorkspace(cmd, client, organization.ID, args[0]) if err != nil { return err } @@ -81,7 +81,7 @@ func statePush() *cobra.Command { return err } - workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) + workspace, err := namedWorkspace(cmd, client, organization.ID, args[0]) if err != nil { return err } diff --git a/cli/stop.go b/cli/stop.go index f2455458bd1f7..05738babebb76 100644 --- a/cli/stop.go +++ b/cli/stop.go @@ -32,7 +32,7 @@ func stop() *cobra.Command { if err != nil { return err } - workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) + workspace, err := namedWorkspace(cmd, client, organization.ID, args[0]) if err != nil { return err } diff --git a/cli/ttl.go b/cli/ttl.go index 077b561179dcb..4a7e4306ab67b 100644 --- a/cli/ttl.go +++ b/cli/ttl.go @@ -44,7 +44,7 @@ func ttlShow() *cobra.Command { return xerrors.Errorf("get current org: %w", err) } - workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) + workspace, err := namedWorkspace(cmd, client, organization.ID, args[0]) if err != nil { return xerrors.Errorf("get workspace: %w", err) } @@ -77,7 +77,7 @@ func ttlset() *cobra.Command { return xerrors.Errorf("get current org: %w", err) } - workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) + workspace, err := namedWorkspace(cmd, client, organization.ID, args[0]) if err != nil { return xerrors.Errorf("get workspace: %w", err) } @@ -125,7 +125,7 @@ func ttlunset() *cobra.Command { return xerrors.Errorf("get current org: %w", err) } - workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) + workspace, err := namedWorkspace(cmd, client, organization.ID, args[0]) if err != nil { return xerrors.Errorf("get workspace: %w", err) } From 903cbb31c6bae534cac405bc6cba1596ed75d305 Mon Sep 17 00:00:00 2001 From: David Wahler Date: Fri, 3 Jun 2022 16:55:01 +0000 Subject: [PATCH 2/4] add support for "coder ssh" and "coder portforward" --- cli/ssh.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/ssh.go b/cli/ssh.go index dcdf46a511018..48aaadd798f4e 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -207,7 +207,7 @@ func getWorkspaceAndAgent(cmd *cobra.Command, client *codersdk.Client, orgID uui return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, err } } else { - workspace, err = client.WorkspaceByOwnerAndName(cmd.Context(), orgID, userID, workspaceParts[0]) + workspace, err = namedWorkspace(cmd, client, orgID, workspaceParts[0]) if err != nil { return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, err } From bef4904b3e1c82eb2fcad0a41214a100607cc82f Mon Sep 17 00:00:00 2001 From: David Wahler Date: Fri, 3 Jun 2022 17:00:38 +0000 Subject: [PATCH 3/4] add support for "coder update" --- cli/update.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/update.go b/cli/update.go index f444ac441c544..8b4a3e5d9a371 100644 --- a/cli/update.go +++ b/cli/update.go @@ -23,7 +23,7 @@ func update() *cobra.Command { if err != nil { return err } - workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) + workspace, err := namedWorkspace(cmd, client, organization.ID, args[0]) if err != nil { return err } From 530a4b0d2785014b1ae375f2d8b5b5437b1e9220 Mon Sep 17 00:00:00 2001 From: David Wahler Date: Fri, 3 Jun 2022 17:33:34 +0000 Subject: [PATCH 4/4] improve error message for invalid workspace name --- cli/delete_test.go | 2 +- cli/root.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/delete_test.go b/cli/delete_test.go index 8e90c6e132202..c8039d454f93e 100644 --- a/cli/delete_test.go +++ b/cli/delete_test.go @@ -88,7 +88,7 @@ func TestDelete(t *testing.T) { go func() { defer close(doneChan) err := cmd.Execute() - assert.ErrorContains(t, err, "invalid workspace name") + assert.ErrorContains(t, err, "invalid workspace name: \"a/b/c\"") }() <-doneChan }) diff --git a/cli/root.go b/cli/root.go index a09bb2bb0f330..ae65115aa12a0 100644 --- a/cli/root.go +++ b/cli/root.go @@ -192,7 +192,7 @@ func namedWorkspace(cmd *cobra.Command, client *codersdk.Client, orgID uuid.UUID owner = parts[0] name = parts[1] default: - return codersdk.Workspace{}, xerrors.New("invalid workspace name") + return codersdk.Workspace{}, xerrors.Errorf("invalid workspace name: %q", identifier) } return client.WorkspaceByOwnerAndName(cmd.Context(), orgID, owner, name)