From 88669fd5786fe108515c3c2f76bdecd525b901eb Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 25 Apr 2022 16:11:03 -0500 Subject: [PATCH] feat: Move workspaces under organizations (#1109) This removes split ownership for workspaces. They are now a resource of organizations and have a designated owner, which is a user. This enables simple administration for commands like: - `coder stop ben/dev` - `coder build logs colin/arch` or if we decide to allow administrators to access workspaces, they could even SSH using this syntax: `coder ssh colin/dev`. --- cli/agent_test.go | 7 +- cli/configssh.go | 6 +- cli/configssh_test.go | 2 +- cli/gitssh_test.go | 2 +- cli/parameters.go | 2 +- cli/server.go | 6 +- cli/server_test.go | 2 +- cli/ssh.go | 6 +- cli/ssh_test.go | 4 +- cli/workspaceautostart.go | 12 +- cli/workspaceautostart_test.go | 5 +- cli/workspaceautostop.go | 12 +- cli/workspaceautostop_test.go | 5 +- cli/workspacecreate.go | 6 +- cli/workspacedelete.go | 6 +- cli/workspacelist.go | 6 +- cli/workspaces.go | 6 +- cli/workspaceshow.go | 6 +- cli/workspacestart.go | 6 +- cli/workspacestop.go | 6 +- cli/workspaceupdate.go | 6 +- cmd/templater/main.go | 2 +- coderd/coderd.go | 14 +- coderd/coderdtest/coderdtest.go | 4 +- coderd/coderdtest/coderdtest_test.go | 3 +- coderd/database/databasefake/databasefake.go | 37 +- coderd/database/dump.sql | 4 + .../migrations/000003_workspaces.up.sql | 1 + coderd/database/models.go | 1 + coderd/database/querier.go | 5 +- coderd/database/queries.sql.go | 175 ++++++---- coderd/database/queries/workspaces.sql | 10 +- coderd/gitsshkey_test.go | 2 +- coderd/httpmw/httpmw.go | 6 - coderd/httpmw/userparam.go | 17 +- coderd/organizations.go | 298 ++++++++++++++++ coderd/organizations_test.go | 143 ++++++++ coderd/provisionerjobs_test.go | 7 +- coderd/templates_test.go | 2 +- coderd/users.go | 329 ------------------ coderd/users_test.go | 119 ------- coderd/workspaceagents_test.go | 6 +- coderd/workspacebuilds_test.go | 10 +- coderd/workspaceresourceauth_test.go | 6 +- coderd/workspaceresources_test.go | 3 +- coderd/workspaces.go | 66 ++++ coderd/workspaces_test.go | 24 +- codersdk/organizations.go | 73 ++++ codersdk/users.go | 56 --- codersdk/workspaces.go | 4 +- site/src/api/typesGenerated.ts | 10 +- 51 files changed, 885 insertions(+), 671 deletions(-) diff --git a/cli/agent_test.go b/cli/agent_test.go index 32cfd6dfbc..dc2ceac1a9 100644 --- a/cli/agent_test.go +++ b/cli/agent_test.go @@ -8,7 +8,6 @@ import ( "github.com/coder/coder/cli/clitest" "github.com/coder/coder/coderd/coderdtest" - "github.com/coder/coder/codersdk" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" ) @@ -44,7 +43,7 @@ func TestWorkspaceAgent(t *testing.T) { }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) cmd, _ := clitest.New(t, "agent", "--auth", "azure-instance-identity", "--url", client.URL.String()) @@ -98,7 +97,7 @@ func TestWorkspaceAgent(t *testing.T) { }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) cmd, _ := clitest.New(t, "agent", "--auth", "aws-instance-identity", "--url", client.URL.String()) @@ -152,7 +151,7 @@ func TestWorkspaceAgent(t *testing.T) { }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) cmd, _ := clitest.New(t, "agent", "--auth", "google-instance-identity", "--url", client.URL.String()) diff --git a/cli/configssh.go b/cli/configssh.go index 36a750af2b..b58e2c481c 100644 --- a/cli/configssh.go +++ b/cli/configssh.go @@ -42,6 +42,10 @@ func configSSH() *cobra.Command { if err != nil { return err } + organization, err := currentOrganization(cmd, client) + if err != nil { + return err + } if strings.HasPrefix(sshConfigFile, "~/") { dirname, _ := os.UserHomeDir() sshConfigFile = filepath.Join(dirname, sshConfigFile[2:]) @@ -55,7 +59,7 @@ func configSSH() *cobra.Command { sshConfigContent = sshConfigContent[:startIndex-1] + sshConfigContent[endIndex+len(sshEndToken):] } - workspaces, err := client.WorkspacesByUser(cmd.Context(), codersdk.Me) + workspaces, err := client.WorkspacesByOwner(cmd.Context(), organization.ID, codersdk.Me) if err != nil { return err } diff --git a/cli/configssh_test.go b/cli/configssh_test.go index 43217d345c..70a6e4feb7 100644 --- a/cli/configssh_test.go +++ b/cli/configssh_test.go @@ -67,7 +67,7 @@ func TestConfigSSH(t *testing.T) { }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) agentClient := codersdk.New(client.URL) agentClient.SessionToken = authToken diff --git a/cli/gitssh_test.go b/cli/gitssh_test.go index fcaf8da9e0..ef4a07686c 100644 --- a/cli/gitssh_test.go +++ b/cli/gitssh_test.go @@ -57,7 +57,7 @@ func TestGitSSH(t *testing.T) { }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) // start workspace agent diff --git a/cli/parameters.go b/cli/parameters.go index 4843552d50..a7f15719ee 100644 --- a/cli/parameters.go +++ b/cli/parameters.go @@ -53,7 +53,7 @@ func parseScopeAndID(ctx context.Context, client *codersdk.Client, organization } scopeID = user.ID case codersdk.ParameterWorkspace: - workspace, err := client.WorkspaceByName(ctx, codersdk.Me, name) + workspace, err := client.WorkspaceByOwnerAndName(ctx, organization.ID, codersdk.Me, name) if err != nil { return scope, uuid.Nil, err } diff --git a/cli/server.go b/cli/server.go index 4fa1339179..9511c00098 100644 --- a/cli/server.go +++ b/cli/server.go @@ -327,7 +327,11 @@ func server() *cobra.Command { _, _ = fmt.Fprintln(cmd.OutOrStdout(), "\n\n"+cliui.Styles.Bold.Render("Interrupt caught. Gracefully exiting...")) if dev { - workspaces, err := client.WorkspacesByUser(cmd.Context(), codersdk.Me) + organizations, err := client.OrganizationsByUser(cmd.Context(), codersdk.Me) + if err != nil { + return xerrors.Errorf("get organizations: %w", err) + } + workspaces, err := client.WorkspacesByOwner(cmd.Context(), organizations[0].ID, codersdk.Me) if err != nil { return xerrors.Errorf("get workspaces: %w", err) } diff --git a/cli/server_test.go b/cli/server_test.go index 6d848f92cb..91cec9e347 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -190,7 +190,7 @@ func TestServer(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, orgs[0].ID, nil) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, orgs[0].ID, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, orgs[0].ID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) require.NoError(t, err) diff --git a/cli/ssh.go b/cli/ssh.go index 6bc2da7231..5595143968 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -33,9 +33,13 @@ func ssh() *cobra.Command { if err != nil { return err } + organization, err := currentOrganization(cmd, client) + if err != nil { + return err + } workspaceParts := strings.Split(args[0], ".") - workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, workspaceParts[0]) + workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, workspaceParts[0]) if err != nil { return err } diff --git a/cli/ssh_test.go b/cli/ssh_test.go index 9fed1fee8a..095720b35c 100644 --- a/cli/ssh_test.go +++ b/cli/ssh_test.go @@ -52,7 +52,7 @@ func TestSSH(t *testing.T) { }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) cmd, root := clitest.New(t, "ssh", workspace.Name) clitest.SetupConfig(t, client, root) doneChan := make(chan struct{}) @@ -105,7 +105,7 @@ func TestSSH(t *testing.T) { }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) go func() { // Run this async so the SSH command has to wait for // the build and agent to connect! diff --git a/cli/workspaceautostart.go b/cli/workspaceautostart.go index df23084fd5..857893eb6e 100644 --- a/cli/workspaceautostart.go +++ b/cli/workspaceautostart.go @@ -46,6 +46,10 @@ func workspaceAutostartEnable() *cobra.Command { if err != nil { return err } + organization, err := currentOrganization(cmd, client) + if err != nil { + return err + } spec := fmt.Sprintf("CRON_TZ=%s %s %s * * %s", autostartTimezone, autostartMinute, autostartHour, autostartDayOfWeek) validSchedule, err := schedule.Weekly(spec) @@ -53,7 +57,7 @@ func workspaceAutostartEnable() *cobra.Command { return err } - workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0]) + workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) if err != nil { return err } @@ -92,8 +96,12 @@ func workspaceAutostartDisable() *cobra.Command { if err != nil { return err } + organization, err := currentOrganization(cmd, client) + if err != nil { + return err + } - workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0]) + workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) if err != nil { return err } diff --git a/cli/workspaceautostart_test.go b/cli/workspaceautostart_test.go index 5772e1450b..196864e394 100644 --- a/cli/workspaceautostart_test.go +++ b/cli/workspaceautostart_test.go @@ -11,7 +11,6 @@ import ( "github.com/coder/coder/cli/clitest" "github.com/coder/coder/coderd/coderdtest" - "github.com/coder/coder/codersdk" ) func TestWorkspaceAutostart(t *testing.T) { @@ -28,7 +27,7 @@ func TestWorkspaceAutostart(t *testing.T) { version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID) + workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID) tz = "Europe/Dublin" cmdArgs = []string{"workspaces", "autostart", "enable", workspace.Name, "--minute", "30", "--hour", "9", "--days", "1-5", "--tz", tz} sched = "CRON_TZ=Europe/Dublin 30 9 * * 1-5" @@ -110,7 +109,7 @@ func TestWorkspaceAutostart(t *testing.T) { version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID) + workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID) ) // check current TZ env var diff --git a/cli/workspaceautostop.go b/cli/workspaceautostop.go index 086c283d0b..2164e5be5e 100644 --- a/cli/workspaceautostop.go +++ b/cli/workspaceautostop.go @@ -46,6 +46,10 @@ func workspaceAutostopEnable() *cobra.Command { if err != nil { return err } + organization, err := currentOrganization(cmd, client) + if err != nil { + return err + } spec := fmt.Sprintf("CRON_TZ=%s %s %s * * %s", autostopTimezone, autostopMinute, autostopHour, autostopDayOfWeek) validSchedule, err := schedule.Weekly(spec) @@ -53,7 +57,7 @@ func workspaceAutostopEnable() *cobra.Command { return err } - workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0]) + workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) if err != nil { return err } @@ -92,8 +96,12 @@ func workspaceAutostopDisable() *cobra.Command { if err != nil { return err } + organization, err := currentOrganization(cmd, client) + if err != nil { + return err + } - workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0]) + workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) if err != nil { return err } diff --git a/cli/workspaceautostop_test.go b/cli/workspaceautostop_test.go index f42deeab3b..0e4f0b644b 100644 --- a/cli/workspaceautostop_test.go +++ b/cli/workspaceautostop_test.go @@ -11,7 +11,6 @@ import ( "github.com/coder/coder/cli/clitest" "github.com/coder/coder/coderd/coderdtest" - "github.com/coder/coder/codersdk" ) func TestWorkspaceAutostop(t *testing.T) { @@ -28,7 +27,7 @@ func TestWorkspaceAutostop(t *testing.T) { version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID) + workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID) cmdArgs = []string{"workspaces", "autostop", "enable", workspace.Name, "--minute", "30", "--hour", "17", "--days", "1-5", "--tz", "Europe/Dublin"} sched = "CRON_TZ=Europe/Dublin 30 17 * * 1-5" stdoutBuf = &bytes.Buffer{} @@ -109,7 +108,7 @@ func TestWorkspaceAutostop(t *testing.T) { version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID) + workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID) ) // check current TZ env var diff --git a/cli/workspacecreate.go b/cli/workspacecreate.go index 2f09cbadb3..678b55ee6d 100644 --- a/cli/workspacecreate.go +++ b/cli/workspacecreate.go @@ -41,7 +41,7 @@ func workspaceCreate() *cobra.Command { workspaceName, err = cliui.Prompt(cmd, cliui.PromptOptions{ Text: "Specify a name for your workspace:", Validate: func(workspaceName string) error { - _, err = client.WorkspaceByName(cmd.Context(), codersdk.Me, workspaceName) + _, err = client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, workspaceName) if err == nil { return xerrors.Errorf("A workspace already exists named %q!", workspaceName) } @@ -53,7 +53,7 @@ func workspaceCreate() *cobra.Command { } } - _, err = client.WorkspaceByName(cmd.Context(), codersdk.Me, workspaceName) + _, err = client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, workspaceName) if err == nil { return xerrors.Errorf("A workspace already exists named %q!", workspaceName) } @@ -162,7 +162,7 @@ func workspaceCreate() *cobra.Command { } before := time.Now() - workspace, err := client.CreateWorkspace(cmd.Context(), codersdk.Me, codersdk.CreateWorkspaceRequest{ + workspace, err := client.CreateWorkspace(cmd.Context(), organization.ID, codersdk.CreateWorkspaceRequest{ TemplateID: template.ID, Name: workspaceName, ParameterValues: parameters, diff --git a/cli/workspacedelete.go b/cli/workspacedelete.go index 17edffa9b7..f3d6802766 100644 --- a/cli/workspacedelete.go +++ b/cli/workspacedelete.go @@ -21,7 +21,11 @@ func workspaceDelete() *cobra.Command { if err != nil { return err } - workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0]) + organization, err := currentOrganization(cmd, client) + if err != nil { + return err + } + workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) if err != nil { return err } diff --git a/cli/workspacelist.go b/cli/workspacelist.go index 06ebd87203..8e4118880b 100644 --- a/cli/workspacelist.go +++ b/cli/workspacelist.go @@ -20,7 +20,11 @@ func workspaceList() *cobra.Command { if err != nil { return err } - workspaces, err := client.WorkspacesByUser(cmd.Context(), codersdk.Me) + organization, err := currentOrganization(cmd, client) + if err != nil { + return err + } + workspaces, err := client.WorkspacesByOwner(cmd.Context(), organization.ID, codersdk.Me) if err != nil { return err } diff --git a/cli/workspaces.go b/cli/workspaces.go index f6d740cd83..3ca5d03c4c 100644 --- a/cli/workspaces.go +++ b/cli/workspaces.go @@ -32,7 +32,11 @@ func validArgsWorkspaceName(cmd *cobra.Command, _ []string, toComplete string) ( if err != nil { return nil, cobra.ShellCompDirectiveError } - workspaces, err := client.WorkspacesByUser(cmd.Context(), codersdk.Me) + organization, err := currentOrganization(cmd, client) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + workspaces, err := client.WorkspacesByOwner(cmd.Context(), organization.ID, codersdk.Me) if err != nil { return nil, cobra.ShellCompDirectiveError } diff --git a/cli/workspaceshow.go b/cli/workspaceshow.go index a968916a3e..98b77ace0f 100644 --- a/cli/workspaceshow.go +++ b/cli/workspaceshow.go @@ -17,7 +17,11 @@ func workspaceShow() *cobra.Command { if err != nil { return err } - workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0]) + organization, err := currentOrganization(cmd, client) + if err != nil { + return err + } + workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) if err != nil { return xerrors.Errorf("get workspace: %w", err) } diff --git a/cli/workspacestart.go b/cli/workspacestart.go index d9cd1cf9b4..5b985f7d88 100644 --- a/cli/workspacestart.go +++ b/cli/workspacestart.go @@ -20,7 +20,11 @@ func workspaceStart() *cobra.Command { if err != nil { return err } - workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0]) + organization, err := currentOrganization(cmd, client) + if err != nil { + return err + } + workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) if err != nil { return err } diff --git a/cli/workspacestop.go b/cli/workspacestop.go index 5e647815ee..cbf90c987c 100644 --- a/cli/workspacestop.go +++ b/cli/workspacestop.go @@ -20,7 +20,11 @@ func workspaceStop() *cobra.Command { if err != nil { return err } - workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0]) + organization, err := currentOrganization(cmd, client) + if err != nil { + return err + } + workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) if err != nil { return err } diff --git a/cli/workspaceupdate.go b/cli/workspaceupdate.go index 155bdd9625..002b449606 100644 --- a/cli/workspaceupdate.go +++ b/cli/workspaceupdate.go @@ -17,7 +17,11 @@ func workspaceUpdate() *cobra.Command { if err != nil { return err } - workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0]) + organization, err := currentOrganization(cmd, client) + if err != nil { + return err + } + workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) if err != nil { return err } diff --git a/cmd/templater/main.go b/cmd/templater/main.go index 6f029c87f1..3338862866 100644 --- a/cmd/templater/main.go +++ b/cmd/templater/main.go @@ -171,7 +171,7 @@ func parse(cmd *cobra.Command, parameters []codersdk.CreateParameterRequest) err return err } - workspace, err := client.CreateWorkspace(cmd.Context(), created.UserID, codersdk.CreateWorkspaceRequest{ + workspace, err := client.CreateWorkspace(cmd.Context(), created.OrganizationID, codersdk.CreateWorkspaceRequest{ TemplateID: template.ID, Name: "example", }) diff --git a/coderd/coderd.go b/coderd/coderd.go index f0efad8013..b51a6ab9e3 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -111,6 +111,15 @@ func New(options *Options) (http.Handler, func()) { r.Get("/", api.templatesByOrganization) r.Get("/{templatename}", api.templateByOrganizationAndName) }) + r.Route("/workspaces", func(r chi.Router) { + r.Post("/", api.postWorkspacesByOrganization) + r.Get("/", api.workspacesByOrganization) + r.Route("/{user}", func(r chi.Router) { + r.Use(httpmw.ExtractUserParam(options.Database)) + r.Get("/{workspace}", api.workspaceByOwnerAndName) + r.Get("/", api.workspacesByOwner) + }) + }) }) r.Route("/parameters/{scope}/{id}", func(r chi.Router) { r.Use(apiKeyMiddleware) @@ -181,11 +190,6 @@ func New(options *Options) (http.Handler, func()) { r.Get("/", api.organizationsByUser) r.Get("/{organizationname}", api.organizationByUserAndName) }) - r.Route("/workspaces", func(r chi.Router) { - r.Post("/", api.postWorkspacesByUser) - r.Get("/", api.workspacesByUser) - r.Get("/{workspacename}", api.workspaceByUserAndName) - }) r.Get("/gitsshkey", api.gitSSHKey) r.Put("/gitsshkey", api.regenerateGitSSHKey) }) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index ab9db83d81..074767f359 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -290,8 +290,8 @@ func AwaitWorkspaceAgents(t *testing.T, client *codersdk.Client, build uuid.UUID // CreateWorkspace creates a workspace for the user and template provided. // A random name is generated for it. -func CreateWorkspace(t *testing.T, client *codersdk.Client, user uuid.UUID, templateID uuid.UUID) codersdk.Workspace { - workspace, err := client.CreateWorkspace(context.Background(), user, codersdk.CreateWorkspaceRequest{ +func CreateWorkspace(t *testing.T, client *codersdk.Client, organization uuid.UUID, templateID uuid.UUID) codersdk.Workspace { + workspace, err := client.CreateWorkspace(context.Background(), organization, codersdk.CreateWorkspaceRequest{ TemplateID: templateID, Name: randomUsername(), }) diff --git a/coderd/coderdtest/coderdtest_test.go b/coderd/coderdtest/coderdtest_test.go index 2e8a8034fc..86a358405f 100644 --- a/coderd/coderdtest/coderdtest_test.go +++ b/coderd/coderdtest/coderdtest_test.go @@ -6,7 +6,6 @@ import ( "go.uber.org/goleak" "github.com/coder/coder/coderd/coderdtest" - "github.com/coder/coder/codersdk" ) func TestMain(m *testing.M) { @@ -21,7 +20,7 @@ func TestNew(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID) _, _ = coderdtest.NewGoogleInstanceIdentity(t, "example", false) diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index acf9f0913f..d7c70be2fa 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -263,7 +263,7 @@ func (q *fakeQuerier) GetWorkspaceByID(_ context.Context, id uuid.UUID) (databas return database.Workspace{}, sql.ErrNoRows } -func (q *fakeQuerier) GetWorkspaceByUserIDAndName(_ context.Context, arg database.GetWorkspaceByUserIDAndNameParams) (database.Workspace, error) { +func (q *fakeQuerier) GetWorkspaceByOwnerIDAndName(_ context.Context, arg database.GetWorkspaceByOwnerIDAndNameParams) (database.Workspace, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -412,7 +412,27 @@ func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndName(_ context.Context, a return database.WorkspaceBuild{}, sql.ErrNoRows } -func (q *fakeQuerier) GetWorkspacesByUserID(_ context.Context, req database.GetWorkspacesByUserIDParams) ([]database.Workspace, error) { +func (q *fakeQuerier) GetWorkspacesByOrganizationID(_ context.Context, req database.GetWorkspacesByOrganizationIDParams) ([]database.Workspace, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + workspaces := make([]database.Workspace, 0) + for _, workspace := range q.workspaces { + if workspace.OrganizationID != req.OrganizationID { + continue + } + if workspace.Deleted != req.Deleted { + continue + } + workspaces = append(workspaces, workspace) + } + if len(workspaces) == 0 { + return nil, sql.ErrNoRows + } + return workspaces, nil +} + +func (q *fakeQuerier) GetWorkspacesByOwnerID(_ context.Context, req database.GetWorkspacesByOwnerIDParams) ([]database.Workspace, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -1145,12 +1165,13 @@ func (q *fakeQuerier) InsertWorkspace(_ context.Context, arg database.InsertWork //nolint:gosimple workspace := database.Workspace{ - ID: arg.ID, - CreatedAt: arg.CreatedAt, - UpdatedAt: arg.UpdatedAt, - OwnerID: arg.OwnerID, - TemplateID: arg.TemplateID, - Name: arg.Name, + ID: arg.ID, + CreatedAt: arg.CreatedAt, + UpdatedAt: arg.UpdatedAt, + OwnerID: arg.OwnerID, + OrganizationID: arg.OrganizationID, + TemplateID: arg.TemplateID, + Name: arg.Name, } q.workspaces = append(q.workspaces, workspace) return workspace, nil diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index fbc137f122..7ef4190420 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -272,6 +272,7 @@ CREATE TABLE workspaces ( created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, owner_id uuid NOT NULL, + organization_id uuid NOT NULL, template_id uuid NOT NULL, deleted boolean DEFAULT false NOT NULL, name character varying(64) NOT NULL, @@ -423,6 +424,9 @@ ALTER TABLE ONLY workspace_builds ALTER TABLE ONLY workspace_resources ADD CONSTRAINT workspace_resources_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; +ALTER TABLE ONLY workspaces + ADD CONSTRAINT workspaces_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT; + ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT; diff --git a/coderd/database/migrations/000003_workspaces.up.sql b/coderd/database/migrations/000003_workspaces.up.sql index 1762d7c514..d1b174ebe1 100644 --- a/coderd/database/migrations/000003_workspaces.up.sql +++ b/coderd/database/migrations/000003_workspaces.up.sql @@ -5,6 +5,7 @@ CREATE TABLE workspaces ( -- Use ON DELETE RESTRICT so that we can cleanup external workspace -- resources first. owner_id uuid NOT NULL REFERENCES users (id) ON DELETE RESTRICT, + organization_id uuid NOT NULL REFERENCES organizations (id) ON DELETE RESTRICT, template_id uuid NOT NULL REFERENCES templates (id) ON DELETE RESTRICT, deleted boolean NOT NULL DEFAULT FALSE, name varchar(64) NOT NULL, diff --git a/coderd/database/models.go b/coderd/database/models.go index 56dc8fd77a..350e1dc927 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -385,6 +385,7 @@ type Workspace struct { CreatedAt time.Time `db:"created_at" json:"created_at"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` TemplateID uuid.UUID `db:"template_id" json:"template_id"` Deleted bool `db:"deleted" json:"deleted"` Name string `db:"name" json:"name"` diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 3b8f317b62..6e30c18e09 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -51,12 +51,13 @@ type querier interface { GetWorkspaceBuildByWorkspaceIDWithoutAfter(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error) GetWorkspaceBuildsByWorkspaceIDsWithoutAfter(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Workspace, error) - GetWorkspaceByUserIDAndName(ctx context.Context, arg GetWorkspaceByUserIDAndNameParams) (Workspace, error) + GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWorkspaceByOwnerIDAndNameParams) (Workspace, error) GetWorkspaceOwnerCountsByTemplateIDs(ctx context.Context, ids []uuid.UUID) ([]GetWorkspaceOwnerCountsByTemplateIDsRow, error) GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceResource, error) + GetWorkspacesByOrganizationID(ctx context.Context, arg GetWorkspacesByOrganizationIDParams) ([]Workspace, error) + GetWorkspacesByOwnerID(ctx context.Context, arg GetWorkspacesByOwnerIDParams) ([]Workspace, error) GetWorkspacesByTemplateID(ctx context.Context, arg GetWorkspacesByTemplateIDParams) ([]Workspace, error) - GetWorkspacesByUserID(ctx context.Context, arg GetWorkspacesByUserIDParams) ([]Workspace, error) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error) InsertFile(ctx context.Context, arg InsertFileParams) (File, error) InsertGitSSHKey(ctx context.Context, arg InsertGitSSHKeyParams) (GitSSHKey, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 3f76b362f4..a79ec16623 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2686,7 +2686,7 @@ func (q *sqlQuerier) InsertWorkspaceResource(ctx context.Context, arg InsertWork const getWorkspaceByID = `-- name: GetWorkspaceByID :one SELECT - id, created_at, updated_at, owner_id, template_id, deleted, name, autostart_schedule, autostop_schedule + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule FROM workspaces WHERE @@ -2703,6 +2703,7 @@ func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Worksp &i.CreatedAt, &i.UpdatedAt, &i.OwnerID, + &i.OrganizationID, &i.TemplateID, &i.Deleted, &i.Name, @@ -2712,9 +2713,9 @@ func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Worksp return i, err } -const getWorkspaceByUserIDAndName = `-- name: GetWorkspaceByUserIDAndName :one +const getWorkspaceByOwnerIDAndName = `-- name: GetWorkspaceByOwnerIDAndName :one SELECT - id, created_at, updated_at, owner_id, template_id, deleted, name, autostart_schedule, autostop_schedule + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule FROM workspaces WHERE @@ -2723,20 +2724,21 @@ WHERE AND LOWER("name") = LOWER($3) ` -type GetWorkspaceByUserIDAndNameParams struct { +type GetWorkspaceByOwnerIDAndNameParams struct { OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` Deleted bool `db:"deleted" json:"deleted"` Name string `db:"name" json:"name"` } -func (q *sqlQuerier) GetWorkspaceByUserIDAndName(ctx context.Context, arg GetWorkspaceByUserIDAndNameParams) (Workspace, error) { - row := q.db.QueryRowContext(ctx, getWorkspaceByUserIDAndName, arg.OwnerID, arg.Deleted, arg.Name) +func (q *sqlQuerier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWorkspaceByOwnerIDAndNameParams) (Workspace, error) { + row := q.db.QueryRowContext(ctx, getWorkspaceByOwnerIDAndName, arg.OwnerID, arg.Deleted, arg.Name) var i Workspace err := row.Scan( &i.ID, &i.CreatedAt, &i.UpdatedAt, &i.OwnerID, + &i.OrganizationID, &i.TemplateID, &i.Deleted, &i.Name, @@ -2787,9 +2789,101 @@ func (q *sqlQuerier) GetWorkspaceOwnerCountsByTemplateIDs(ctx context.Context, i return items, nil } +const getWorkspacesByOrganizationID = `-- name: GetWorkspacesByOrganizationID :many +SELECT id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule FROM workspaces WHERE organization_id = $1 AND deleted = $2 +` + +type GetWorkspacesByOrganizationIDParams struct { + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + Deleted bool `db:"deleted" json:"deleted"` +} + +func (q *sqlQuerier) GetWorkspacesByOrganizationID(ctx context.Context, arg GetWorkspacesByOrganizationIDParams) ([]Workspace, error) { + rows, err := q.db.QueryContext(ctx, getWorkspacesByOrganizationID, arg.OrganizationID, arg.Deleted) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Workspace + for rows.Next() { + var i Workspace + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.OwnerID, + &i.OrganizationID, + &i.TemplateID, + &i.Deleted, + &i.Name, + &i.AutostartSchedule, + &i.AutostopSchedule, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getWorkspacesByOwnerID = `-- name: GetWorkspacesByOwnerID :many +SELECT + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule +FROM + workspaces +WHERE + owner_id = $1 + AND deleted = $2 +` + +type GetWorkspacesByOwnerIDParams struct { + OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` + Deleted bool `db:"deleted" json:"deleted"` +} + +func (q *sqlQuerier) GetWorkspacesByOwnerID(ctx context.Context, arg GetWorkspacesByOwnerIDParams) ([]Workspace, error) { + rows, err := q.db.QueryContext(ctx, getWorkspacesByOwnerID, arg.OwnerID, arg.Deleted) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Workspace + for rows.Next() { + var i Workspace + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.OwnerID, + &i.OrganizationID, + &i.TemplateID, + &i.Deleted, + &i.Name, + &i.AutostartSchedule, + &i.AutostopSchedule, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getWorkspacesByTemplateID = `-- name: GetWorkspacesByTemplateID :many SELECT - id, created_at, updated_at, owner_id, template_id, deleted, name, autostart_schedule, autostop_schedule + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule FROM workspaces WHERE @@ -2816,54 +2910,7 @@ func (q *sqlQuerier) GetWorkspacesByTemplateID(ctx context.Context, arg GetWorks &i.CreatedAt, &i.UpdatedAt, &i.OwnerID, - &i.TemplateID, - &i.Deleted, - &i.Name, - &i.AutostartSchedule, - &i.AutostopSchedule, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const getWorkspacesByUserID = `-- name: GetWorkspacesByUserID :many -SELECT - id, created_at, updated_at, owner_id, template_id, deleted, name, autostart_schedule, autostop_schedule -FROM - workspaces -WHERE - owner_id = $1 - AND deleted = $2 -` - -type GetWorkspacesByUserIDParams struct { - OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` - Deleted bool `db:"deleted" json:"deleted"` -} - -func (q *sqlQuerier) GetWorkspacesByUserID(ctx context.Context, arg GetWorkspacesByUserIDParams) ([]Workspace, error) { - rows, err := q.db.QueryContext(ctx, getWorkspacesByUserID, arg.OwnerID, arg.Deleted) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Workspace - for rows.Next() { - var i Workspace - if err := rows.Scan( - &i.ID, - &i.CreatedAt, - &i.UpdatedAt, - &i.OwnerID, + &i.OrganizationID, &i.TemplateID, &i.Deleted, &i.Name, @@ -2890,20 +2937,22 @@ INSERT INTO created_at, updated_at, owner_id, + organization_id, template_id, name ) VALUES - ($1, $2, $3, $4, $5, $6) RETURNING id, created_at, updated_at, owner_id, template_id, deleted, name, autostart_schedule, autostop_schedule + ($1, $2, $3, $4, $5, $6, $7) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule ` type InsertWorkspaceParams struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` - TemplateID uuid.UUID `db:"template_id" json:"template_id"` - Name string `db:"name" json:"name"` + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + TemplateID uuid.UUID `db:"template_id" json:"template_id"` + Name string `db:"name" json:"name"` } func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (Workspace, error) { @@ -2912,6 +2961,7 @@ func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspacePar arg.CreatedAt, arg.UpdatedAt, arg.OwnerID, + arg.OrganizationID, arg.TemplateID, arg.Name, ) @@ -2921,6 +2971,7 @@ func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspacePar &i.CreatedAt, &i.UpdatedAt, &i.OwnerID, + &i.OrganizationID, &i.TemplateID, &i.Deleted, &i.Name, diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 37f26cf79a..dec8177837 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -8,6 +8,9 @@ WHERE LIMIT 1; +-- name: GetWorkspacesByOrganizationID :many +SELECT * FROM workspaces WHERE organization_id = $1 AND deleted = $2; + -- name: GetWorkspacesByTemplateID :many SELECT * @@ -17,7 +20,7 @@ WHERE template_id = $1 AND deleted = $2; --- name: GetWorkspacesByUserID :many +-- name: GetWorkspacesByOwnerID :many SELECT * FROM @@ -26,7 +29,7 @@ WHERE owner_id = $1 AND deleted = $2; --- name: GetWorkspaceByUserIDAndName :one +-- name: GetWorkspaceByOwnerIDAndName :one SELECT * FROM @@ -55,11 +58,12 @@ INSERT INTO created_at, updated_at, owner_id, + organization_id, template_id, name ) VALUES - ($1, $2, $3, $4, $5, $6) RETURNING *; + ($1, $2, $3, $4, $5, $6, $7) RETURNING *; -- name: UpdateWorkspaceDeletedByID :exec UPDATE diff --git a/coderd/gitsshkey_test.go b/coderd/gitsshkey_test.go index dc9ffe23ca..4a53727817 100644 --- a/coderd/gitsshkey_test.go +++ b/coderd/gitsshkey_test.go @@ -105,7 +105,7 @@ func TestAgentGitSSHKey(t *testing.T) { }) project := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) daemonCloser.Close() diff --git a/coderd/httpmw/httpmw.go b/coderd/httpmw/httpmw.go index ce0a7955bb..88964154e8 100644 --- a/coderd/httpmw/httpmw.go +++ b/coderd/httpmw/httpmw.go @@ -20,12 +20,6 @@ func parseUUID(rw http.ResponseWriter, r *http.Request, param string) (uuid.UUID return uuid.UUID{}, false } - // Automatically set uuid.Nil to the acting users id. - if param == UserKey && rawID == "me" { - key := APIKey(r) - return key.UserID, true - } - parsed, err := uuid.Parse(rawID) if err != nil { httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ diff --git a/coderd/httpmw/userparam.go b/coderd/httpmw/userparam.go index 965e545cb0..4342cbb695 100644 --- a/coderd/httpmw/userparam.go +++ b/coderd/httpmw/userparam.go @@ -5,12 +5,13 @@ import ( "fmt" "net/http" + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" ) -const UserKey = "user" - type userParamContextKey struct{} // UserParam returns the user from the ExtractUserParam handler. @@ -26,9 +27,15 @@ func UserParam(r *http.Request) database.User { func ExtractUserParam(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) { - userID, ok := parseUUID(rw, r, UserKey) - if !ok { - return + var userID uuid.UUID + if chi.URLParam(r, "user") == "me" { + userID = APIKey(r).UserID + } else { + var ok bool + userID, ok = parseUUID(rw, r, "user") + if !ok { + return + } } apiKey := APIKey(r) diff --git a/coderd/organizations.go b/coderd/organizations.go index c6a944ed89..686b526fa2 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -2,6 +2,7 @@ package coderd import ( "database/sql" + "encoding/json" "errors" "fmt" "net/http" @@ -323,6 +324,303 @@ func (api *api) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Re httpapi.Write(rw, http.StatusOK, convertTemplate(template, count)) } +func (api *api) workspacesByOrganization(rw http.ResponseWriter, r *http.Request) { + organization := httpmw.OrganizationParam(r) + workspaces, err := api.Database.GetWorkspacesByOrganizationID(r.Context(), database.GetWorkspacesByOrganizationIDParams{ + OrganizationID: organization.ID, + Deleted: false, + }) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspaces: %s", err), + }) + return + } + apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, workspaces) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("convert workspaces: %s", err), + }) + return + } + httpapi.Write(rw, http.StatusOK, apiWorkspaces) +} + +func (api *api) workspacesByOwner(rw http.ResponseWriter, r *http.Request) { + owner := httpmw.UserParam(r) + workspaces, err := api.Database.GetWorkspacesByOwnerID(r.Context(), database.GetWorkspacesByOwnerIDParams{ + OwnerID: owner.ID, + }) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspaces: %s", err), + }) + return + } + apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, workspaces) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("convert workspaces: %s", err), + }) + return + } + httpapi.Write(rw, http.StatusOK, apiWorkspaces) +} + +func (api *api) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) { + owner := httpmw.UserParam(r) + organization := httpmw.OrganizationParam(r) + workspaceName := chi.URLParam(r, "workspace") + + workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{ + OwnerID: owner.ID, + Name: workspaceName, + }) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: fmt.Sprintf("no workspace found by name %q", workspaceName), + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace by name: %s", err), + }) + return + } + + if workspace.OrganizationID != organization.ID { + httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ + Message: fmt.Sprintf("workspace is not owned by organization %q", organization.Name), + }) + return + } + + build, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace build: %s", err), + }) + return + } + job, err := api.Database.GetProvisionerJobByID(r.Context(), build.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job: %s", err), + }) + return + } + template, err := api.Database.GetTemplateByID(r.Context(), workspace.TemplateID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get template: %s", err), + }) + return + } + + httpapi.Write(rw, http.StatusOK, convertWorkspace(workspace, + convertWorkspaceBuild(build, convertProvisionerJob(job)), template)) +} + +// Create a new workspace for the currently authenticated user. +func (api *api) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Request) { + var createWorkspace codersdk.CreateWorkspaceRequest + if !httpapi.Read(rw, r, &createWorkspace) { + return + } + apiKey := httpmw.APIKey(r) + template, err := api.Database.GetTemplateByID(r.Context(), createWorkspace.TemplateID) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: fmt.Sprintf("template %q doesn't exist", createWorkspace.TemplateID.String()), + Errors: []httpapi.Error{{ + Field: "template_id", + Detail: "template not found", + }}, + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get template: %s", err), + }) + return + } + organization := httpmw.OrganizationParam(r) + if organization.ID != template.OrganizationID { + httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ + Message: fmt.Sprintf("template is not in organization %q", organization.Name), + }) + return + } + _, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{ + OrganizationID: template.OrganizationID, + UserID: apiKey.UserID, + }) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ + Message: "you aren't allowed to access templates in that organization", + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get organization member: %s", err), + }) + return + } + + workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{ + OwnerID: apiKey.UserID, + Name: createWorkspace.Name, + }) + if err == nil { + // If the workspace already exists, don't allow creation. + template, err := api.Database.GetTemplateByID(r.Context(), workspace.TemplateID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("find template for conflicting workspace name %q: %s", createWorkspace.Name, err), + }) + return + } + // The template is fetched for clarity to the user on where the conflicting name may be. + httpapi.Write(rw, http.StatusConflict, httpapi.Response{ + Message: fmt.Sprintf("workspace %q already exists in the %q template", createWorkspace.Name, template.Name), + Errors: []httpapi.Error{{ + Field: "name", + Detail: "this value is already in use and should be unique", + }}, + }) + return + } + if !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace by name: %s", err.Error()), + }) + return + } + + templateVersion, err := api.Database.GetTemplateVersionByID(r.Context(), template.ActiveVersionID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get template version: %s", err), + }) + return + } + templateVersionJob, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get template version job: %s", err), + }) + return + } + templateVersionJobStatus := convertProvisionerJob(templateVersionJob).Status + switch templateVersionJobStatus { + case codersdk.ProvisionerJobPending, codersdk.ProvisionerJobRunning: + httpapi.Write(rw, http.StatusNotAcceptable, httpapi.Response{ + Message: fmt.Sprintf("The provided template version is %s. Wait for it to complete importing!", templateVersionJobStatus), + }) + return + case codersdk.ProvisionerJobFailed: + httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ + Message: fmt.Sprintf("The provided template version %q has failed to import. You cannot create workspaces using it!", templateVersion.Name), + }) + return + case codersdk.ProvisionerJobCanceled: + httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ + Message: "The provided template version was canceled during import. You cannot create workspaces using it!", + }) + return + } + + var provisionerJob database.ProvisionerJob + var workspaceBuild database.WorkspaceBuild + err = api.Database.InTx(func(db database.Store) error { + workspaceBuildID := uuid.New() + // Workspaces are created without any versions. + workspace, err = db.InsertWorkspace(r.Context(), database.InsertWorkspaceParams{ + ID: uuid.New(), + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + OwnerID: apiKey.UserID, + OrganizationID: template.OrganizationID, + TemplateID: template.ID, + Name: createWorkspace.Name, + }) + if err != nil { + return xerrors.Errorf("insert workspace: %w", err) + } + for _, parameterValue := range createWorkspace.ParameterValues { + _, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{ + ID: uuid.New(), + Name: parameterValue.Name, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + Scope: database.ParameterScopeWorkspace, + ScopeID: workspace.ID, + SourceScheme: parameterValue.SourceScheme, + SourceValue: parameterValue.SourceValue, + DestinationScheme: parameterValue.DestinationScheme, + }) + if err != nil { + return xerrors.Errorf("insert parameter value: %w", err) + } + } + + input, err := json.Marshal(workspaceProvisionJob{ + WorkspaceBuildID: workspaceBuildID, + }) + if err != nil { + return xerrors.Errorf("marshal provision job: %w", err) + } + provisionerJob, err = db.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{ + ID: uuid.New(), + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + InitiatorID: apiKey.UserID, + OrganizationID: template.OrganizationID, + Provisioner: template.Provisioner, + Type: database.ProvisionerJobTypeWorkspaceBuild, + StorageMethod: templateVersionJob.StorageMethod, + StorageSource: templateVersionJob.StorageSource, + Input: input, + }) + if err != nil { + return xerrors.Errorf("insert provisioner job: %w", err) + } + workspaceBuild, err = db.InsertWorkspaceBuild(r.Context(), database.InsertWorkspaceBuildParams{ + ID: workspaceBuildID, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + WorkspaceID: workspace.ID, + TemplateVersionID: templateVersion.ID, + Name: namesgenerator.GetRandomName(1), + InitiatorID: apiKey.UserID, + Transition: database.WorkspaceTransitionStart, + JobID: provisionerJob.ID, + }) + if err != nil { + return xerrors.Errorf("insert workspace build: %w", err) + } + return nil + }) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("create workspace: %s", err), + }) + return + } + + httpapi.Write(rw, http.StatusCreated, convertWorkspace(workspace, + convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(templateVersionJob)), template)) +} + // convertOrganization consumes the database representation and outputs an API friendly representation. func convertOrganization(organization database.Organization) codersdk.Organization { return codersdk.Organization{ diff --git a/coderd/organizations_test.go b/coderd/organizations_test.go index 4fe4da14b3..74015901fc 100644 --- a/coderd/organizations_test.go +++ b/coderd/organizations_test.go @@ -187,3 +187,146 @@ func TestTemplateByOrganizationAndName(t *testing.T) { require.NoError(t, err) }) } + +func TestPostWorkspacesByOrganization(t *testing.T) { + t.Parallel() + t.Run("InvalidTemplate", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + _, err := client.CreateWorkspace(context.Background(), user.OrganizationID, codersdk.CreateWorkspaceRequest{ + TemplateID: uuid.New(), + Name: "workspace", + }) + require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) + }) + + t.Run("NoTemplateAccess", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + first := coderdtest.CreateFirstUser(t, client) + + other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) + org, err := other.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{ + Name: "another", + }) + require.NoError(t, err) + version := coderdtest.CreateTemplateVersion(t, other, org.ID, nil) + template := coderdtest.CreateTemplate(t, other, org.ID, version.ID) + + _, err = client.CreateWorkspace(context.Background(), first.OrganizationID, codersdk.CreateWorkspaceRequest{ + TemplateID: template.ID, + Name: "workspace", + }) + require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) + }) + + t.Run("AlreadyExists", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + coderdtest.NewProvisionerDaemon(t, client) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + _, err := client.CreateWorkspace(context.Background(), user.OrganizationID, codersdk.CreateWorkspaceRequest{ + TemplateID: template.ID, + Name: workspace.Name, + }) + require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusConflict, apiErr.StatusCode()) + }) + + t.Run("Create", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + coderdtest.NewProvisionerDaemon(t, client) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + _ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + }) +} + +func TestWorkspacesByOrganization(t *testing.T) { + t.Parallel() + t.Run("ListEmpty", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + _, err := client.WorkspacesByOrganization(context.Background(), user.OrganizationID) + require.NoError(t, err) + }) + t.Run("List", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + coderdtest.NewProvisionerDaemon(t, client) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + _ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + workspaces, err := client.WorkspacesByOrganization(context.Background(), user.OrganizationID) + require.NoError(t, err) + require.Len(t, workspaces, 1) + }) +} + +func TestWorkspacesByOwner(t *testing.T) { + t.Parallel() + t.Run("ListEmpty", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + _, err := client.WorkspacesByOwner(context.Background(), user.OrganizationID, codersdk.Me) + require.NoError(t, err) + }) + t.Run("List", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + coderdtest.NewProvisionerDaemon(t, client) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + _ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + workspaces, err := client.WorkspacesByOwner(context.Background(), user.OrganizationID, codersdk.Me) + require.NoError(t, err) + require.Len(t, workspaces, 1) + }) +} + +func TestWorkspaceByOwnerAndName(t *testing.T) { + t.Parallel() + t.Run("NotFound", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + _, err := client.WorkspaceByOwnerAndName(context.Background(), user.OrganizationID, codersdk.Me, "something") + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + t.Run("Get", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + coderdtest.NewProvisionerDaemon(t, client) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + _, err := client.WorkspaceByOwnerAndName(context.Background(), user.OrganizationID, codersdk.Me, workspace.Name) + require.NoError(t, err) + }) +} diff --git a/coderd/provisionerjobs_test.go b/coderd/provisionerjobs_test.go index db64e54433..1d04c78c54 100644 --- a/coderd/provisionerjobs_test.go +++ b/coderd/provisionerjobs_test.go @@ -9,7 +9,6 @@ import ( "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/coderd/database" - "github.com/coder/coder/codersdk" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" ) @@ -38,7 +37,7 @@ func TestProvisionerJobLogs(t *testing.T) { }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) before := time.Now().UTC() coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) @@ -76,7 +75,7 @@ func TestProvisionerJobLogs(t *testing.T) { }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) before := database.Now() ctx, cancelFunc := context.WithCancel(context.Background()) t.Cleanup(cancelFunc) @@ -112,7 +111,7 @@ func TestProvisionerJobLogs(t *testing.T) { }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) logs, err := client.WorkspaceBuildLogsBefore(context.Background(), workspace.LatestBuild.ID, time.Now()) require.NoError(t, err) diff --git a/coderd/templates_test.go b/coderd/templates_test.go index c5a19096d4..d7b0e67a65 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -47,7 +47,7 @@ func TestDeleteTemplate(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) err := client.DeleteTemplate(context.Background(), template.ID) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) diff --git a/coderd/users.go b/coderd/users.go index e5f78325d3..148386e93c 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -4,7 +4,6 @@ import ( "context" "crypto/sha256" "database/sql" - "encoding/json" "errors" "fmt" "net/http" @@ -14,7 +13,6 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/render" "github.com/google/uuid" - "github.com/moby/moby/pkg/namesgenerator" "golang.org/x/xerrors" "github.com/coder/coder/coderd/database" @@ -484,333 +482,6 @@ func (*api) postLogout(rw http.ResponseWriter, _ *http.Request) { }) } -// Create a new workspace for the currently authenticated user. -func (api *api) postWorkspacesByUser(rw http.ResponseWriter, r *http.Request) { - var createWorkspace codersdk.CreateWorkspaceRequest - if !httpapi.Read(rw, r, &createWorkspace) { - return - } - apiKey := httpmw.APIKey(r) - template, err := api.Database.GetTemplateByID(r.Context(), createWorkspace.TemplateID) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: fmt.Sprintf("template %q doesn't exist", createWorkspace.TemplateID.String()), - Errors: []httpapi.Error{{ - Field: "template_id", - Detail: "template not found", - }}, - }) - return - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get template: %s", err), - }) - return - } - _, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{ - OrganizationID: template.OrganizationID, - UserID: apiKey.UserID, - }) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: "you aren't allowed to access templates in that organization", - }) - return - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get organization member: %s", err), - }) - return - } - - workspace, err := api.Database.GetWorkspaceByUserIDAndName(r.Context(), database.GetWorkspaceByUserIDAndNameParams{ - OwnerID: apiKey.UserID, - Name: createWorkspace.Name, - }) - if err == nil { - // If the workspace already exists, don't allow creation. - template, err := api.Database.GetTemplateByID(r.Context(), workspace.TemplateID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("find template for conflicting workspace name %q: %s", createWorkspace.Name, err), - }) - return - } - // The template is fetched for clarity to the user on where the conflicting name may be. - httpapi.Write(rw, http.StatusConflict, httpapi.Response{ - Message: fmt.Sprintf("workspace %q already exists in the %q template", createWorkspace.Name, template.Name), - Errors: []httpapi.Error{{ - Field: "name", - Detail: "this value is already in use and should be unique", - }}, - }) - return - } - if !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspace by name: %s", err.Error()), - }) - return - } - - templateVersion, err := api.Database.GetTemplateVersionByID(r.Context(), template.ActiveVersionID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get template version: %s", err), - }) - return - } - templateVersionJob, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get template version job: %s", err), - }) - return - } - templateVersionJobStatus := convertProvisionerJob(templateVersionJob).Status - switch templateVersionJobStatus { - case codersdk.ProvisionerJobPending, codersdk.ProvisionerJobRunning: - httpapi.Write(rw, http.StatusNotAcceptable, httpapi.Response{ - Message: fmt.Sprintf("The provided template version is %s. Wait for it to complete importing!", templateVersionJobStatus), - }) - return - case codersdk.ProvisionerJobFailed: - httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ - Message: fmt.Sprintf("The provided template version %q has failed to import. You cannot create workspaces using it!", templateVersion.Name), - }) - return - case codersdk.ProvisionerJobCanceled: - httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ - Message: "The provided template version was canceled during import. You cannot create workspaces using it!", - }) - return - } - - var provisionerJob database.ProvisionerJob - var workspaceBuild database.WorkspaceBuild - err = api.Database.InTx(func(db database.Store) error { - workspaceBuildID := uuid.New() - // Workspaces are created without any versions. - workspace, err = db.InsertWorkspace(r.Context(), database.InsertWorkspaceParams{ - ID: uuid.New(), - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - OwnerID: apiKey.UserID, - TemplateID: template.ID, - Name: createWorkspace.Name, - }) - if err != nil { - return xerrors.Errorf("insert workspace: %w", err) - } - for _, parameterValue := range createWorkspace.ParameterValues { - _, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{ - ID: uuid.New(), - Name: parameterValue.Name, - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - Scope: database.ParameterScopeWorkspace, - ScopeID: workspace.ID, - SourceScheme: parameterValue.SourceScheme, - SourceValue: parameterValue.SourceValue, - DestinationScheme: parameterValue.DestinationScheme, - }) - if err != nil { - return xerrors.Errorf("insert parameter value: %w", err) - } - } - - input, err := json.Marshal(workspaceProvisionJob{ - WorkspaceBuildID: workspaceBuildID, - }) - if err != nil { - return xerrors.Errorf("marshal provision job: %w", err) - } - provisionerJob, err = db.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{ - ID: uuid.New(), - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - InitiatorID: apiKey.UserID, - OrganizationID: template.OrganizationID, - Provisioner: template.Provisioner, - Type: database.ProvisionerJobTypeWorkspaceBuild, - StorageMethod: templateVersionJob.StorageMethod, - StorageSource: templateVersionJob.StorageSource, - Input: input, - }) - if err != nil { - return xerrors.Errorf("insert provisioner job: %w", err) - } - workspaceBuild, err = db.InsertWorkspaceBuild(r.Context(), database.InsertWorkspaceBuildParams{ - ID: workspaceBuildID, - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - WorkspaceID: workspace.ID, - TemplateVersionID: templateVersion.ID, - Name: namesgenerator.GetRandomName(1), - InitiatorID: apiKey.UserID, - Transition: database.WorkspaceTransitionStart, - JobID: provisionerJob.ID, - }) - if err != nil { - return xerrors.Errorf("insert workspace build: %w", err) - } - return nil - }) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("create workspace: %s", err), - }) - return - } - - httpapi.Write(rw, http.StatusCreated, convertWorkspace(workspace, - convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(templateVersionJob)), template)) -} - -func (api *api) workspacesByUser(rw http.ResponseWriter, r *http.Request) { - user := httpmw.UserParam(r) - workspaces, err := api.Database.GetWorkspacesByUserID(r.Context(), database.GetWorkspacesByUserIDParams{ - OwnerID: user.ID, - }) - if errors.Is(err, sql.ErrNoRows) { - err = nil - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspaces: %s", err), - }) - return - } - workspaceIDs := make([]uuid.UUID, 0, len(workspaces)) - templateIDs := make([]uuid.UUID, 0, len(workspaces)) - for _, workspace := range workspaces { - workspaceIDs = append(workspaceIDs, workspace.ID) - templateIDs = append(templateIDs, workspace.TemplateID) - } - workspaceBuilds, err := api.Database.GetWorkspaceBuildsByWorkspaceIDsWithoutAfter(r.Context(), workspaceIDs) - if errors.Is(err, sql.ErrNoRows) { - err = nil - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspace builds: %s", err), - }) - return - } - templates, err := api.Database.GetTemplatesByIDs(r.Context(), templateIDs) - if errors.Is(err, sql.ErrNoRows) { - err = nil - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get templates: %s", err), - }) - return - } - jobIDs := make([]uuid.UUID, 0, len(workspaceBuilds)) - for _, build := range workspaceBuilds { - jobIDs = append(jobIDs, build.JobID) - } - jobs, err := api.Database.GetProvisionerJobsByIDs(r.Context(), jobIDs) - if errors.Is(err, sql.ErrNoRows) { - err = nil - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get provisioner jobs: %s", err), - }) - return - } - - buildByWorkspaceID := map[uuid.UUID]database.WorkspaceBuild{} - for _, workspaceBuild := range workspaceBuilds { - buildByWorkspaceID[workspaceBuild.WorkspaceID] = workspaceBuild - } - templateByID := map[uuid.UUID]database.Template{} - for _, template := range templates { - templateByID[template.ID] = template - } - jobByID := map[uuid.UUID]database.ProvisionerJob{} - for _, job := range jobs { - jobByID[job.ID] = job - } - apiWorkspaces := make([]codersdk.Workspace, 0, len(workspaces)) - for _, workspace := range workspaces { - build, exists := buildByWorkspaceID[workspace.ID] - if !exists { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("build not found for workspace %q", workspace.Name), - }) - return - } - template, exists := templateByID[workspace.TemplateID] - if !exists { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("template not found for workspace %q", workspace.Name), - }) - return - } - job, exists := jobByID[build.JobID] - if !exists { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("build job not found for workspace %q", workspace.Name), - }) - return - } - apiWorkspaces = append(apiWorkspaces, - convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), template)) - } - - httpapi.Write(rw, http.StatusOK, apiWorkspaces) -} - -func (api *api) workspaceByUserAndName(rw http.ResponseWriter, r *http.Request) { - user := httpmw.UserParam(r) - workspaceName := chi.URLParam(r, "workspacename") - workspace, err := api.Database.GetWorkspaceByUserIDAndName(r.Context(), database.GetWorkspaceByUserIDAndNameParams{ - OwnerID: user.ID, - Name: workspaceName, - }) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: fmt.Sprintf("no workspace found by name %q", workspaceName), - }) - return - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspace by name: %s", err), - }) - return - } - build, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspace build: %s", err), - }) - return - } - job, err := api.Database.GetProvisionerJobByID(r.Context(), build.JobID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get provisioner job: %s", err), - }) - return - } - template, err := api.Database.GetTemplateByID(r.Context(), workspace.TemplateID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get template: %s", err), - }) - return - } - - httpapi.Write(rw, http.StatusOK, convertWorkspace(workspace, - convertWorkspaceBuild(build, convertProvisionerJob(job)), template)) -} - // Generates a new ID and secret for an API key. func generateAPIKeyIDSecret() (id string, secret string, err error) { // Length of an API Key ID. diff --git a/coderd/users_test.go b/coderd/users_test.go index 845179bb75..ed2e4e95f4 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -410,125 +410,6 @@ func TestPostAPIKey(t *testing.T) { }) } -func TestPostWorkspacesByUser(t *testing.T) { - t.Parallel() - t.Run("InvalidTemplate", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateFirstUser(t, client) - _, err := client.CreateWorkspace(context.Background(), codersdk.Me, codersdk.CreateWorkspaceRequest{ - TemplateID: uuid.New(), - Name: "workspace", - }) - require.Error(t, err) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) - }) - - t.Run("NoTemplateAccess", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - first := coderdtest.CreateFirstUser(t, client) - - other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) - org, err := other.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{ - Name: "another", - }) - require.NoError(t, err) - version := coderdtest.CreateTemplateVersion(t, other, org.ID, nil) - template := coderdtest.CreateTemplate(t, other, org.ID, version.ID) - - _, err = client.CreateWorkspace(context.Background(), codersdk.Me, codersdk.CreateWorkspaceRequest{ - TemplateID: template.ID, - Name: "workspace", - }) - require.Error(t, err) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) - }) - - t.Run("AlreadyExists", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - coderdtest.NewProvisionerDaemon(t, client) - user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) - _, err := client.CreateWorkspace(context.Background(), codersdk.Me, codersdk.CreateWorkspaceRequest{ - TemplateID: template.ID, - Name: workspace.Name, - }) - require.Error(t, err) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusConflict, apiErr.StatusCode()) - }) - - t.Run("Create", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - coderdtest.NewProvisionerDaemon(t, client) - user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - _ = coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) - }) -} - -func TestWorkspacesByUser(t *testing.T) { - t.Parallel() - t.Run("ListEmpty", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - coderdtest.CreateFirstUser(t, client) - _, err := client.WorkspacesByUser(context.Background(), codersdk.Me) - require.NoError(t, err) - }) - t.Run("List", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - coderdtest.NewProvisionerDaemon(t, client) - user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - _ = coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) - workspaces, err := client.WorkspacesByUser(context.Background(), codersdk.Me) - require.NoError(t, err) - require.Len(t, workspaces, 1) - }) -} - -func TestWorkspaceByUserAndName(t *testing.T) { - t.Parallel() - t.Run("NotFound", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - coderdtest.CreateFirstUser(t, client) - _, err := client.WorkspaceByName(context.Background(), codersdk.Me, "something") - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) - }) - t.Run("Get", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - coderdtest.NewProvisionerDaemon(t, client) - user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) - _, err := client.WorkspaceByName(context.Background(), codersdk.Me, workspace.Name) - require.NoError(t, err) - }) -} - // TestPaginatedUsers creates a list of users, then tries to paginate through // them using different page sizes. func TestPaginatedUsers(t *testing.T) { diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 42af675d4b..018905e0fb 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -46,7 +46,7 @@ func TestWorkspaceAgent(t *testing.T) { }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) daemonCloser.Close() @@ -84,7 +84,7 @@ func TestWorkspaceAgentListen(t *testing.T) { }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) daemonCloser.Close() @@ -134,7 +134,7 @@ func TestWorkspaceAgentTURN(t *testing.T) { }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) daemonCloser.Close() diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 2e3b2eb7e8..493b469537 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -22,7 +22,7 @@ func TestWorkspaceBuild(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) _, err := client.WorkspaceBuild(context.Background(), workspace.LatestBuild.ID) require.NoError(t, err) } @@ -43,7 +43,7 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) { }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) var build codersdk.WorkspaceBuild require.Eventually(t, func() bool { var err error @@ -72,7 +72,7 @@ func TestWorkspaceBuildResources(t *testing.T) { coderdtest.AwaitTemplateVersionJob(t, client, version.ID) closeDaemon.Close() template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) _, err := client.WorkspaceResourcesByBuild(context.Background(), workspace.LatestBuild.ID) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) @@ -105,7 +105,7 @@ func TestWorkspaceBuildResources(t *testing.T) { }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) resources, err := client.WorkspaceResourcesByBuild(context.Background(), workspace.LatestBuild.ID) require.NoError(t, err) @@ -152,7 +152,7 @@ func TestWorkspaceBuildLogs(t *testing.T) { }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) ctx, cancelFunc := context.WithCancel(context.Background()) t.Cleanup(cancelFunc) logs, err := client.WorkspaceBuildLogsAfter(ctx, workspace.LatestBuild.ID, before) diff --git a/coderd/workspaceresourceauth_test.go b/coderd/workspaceresourceauth_test.go index 034b73bf44..496ead71fc 100644 --- a/coderd/workspaceresourceauth_test.go +++ b/coderd/workspaceresourceauth_test.go @@ -42,7 +42,7 @@ func TestPostWorkspaceAuthAzureInstanceIdentity(t *testing.T) { }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) client.HTTPClient = metadataClient @@ -81,7 +81,7 @@ func TestPostWorkspaceAuthAWSInstanceIdentity(t *testing.T) { }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) client.HTTPClient = metadataClient @@ -147,7 +147,7 @@ func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) { }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) _, err := client.AuthWorkspaceGoogleInstanceIdentity(context.Background(), "", metadata) diff --git a/coderd/workspaceresources_test.go b/coderd/workspaceresources_test.go index e5673c191c..8b4a846cfa 100644 --- a/coderd/workspaceresources_test.go +++ b/coderd/workspaceresources_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/require" "github.com/coder/coder/coderd/coderdtest" - "github.com/coder/coder/codersdk" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" ) @@ -38,7 +37,7 @@ func TestWorkspaceResource(t *testing.T) { }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) resources, err := client.WorkspaceResourcesByBuild(context.Background(), workspace.LatestBuild.ID) require.NoError(t, err) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 67402cc925..07c0d8500b 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1,6 +1,7 @@ package coderd import ( + "context" "database/sql" "encoding/json" "errors" @@ -350,6 +351,71 @@ func (api *api) putWorkspaceAutostop(rw http.ResponseWriter, r *http.Request) { } } +func convertWorkspaces(ctx context.Context, db database.Store, workspaces []database.Workspace) ([]codersdk.Workspace, error) { + workspaceIDs := make([]uuid.UUID, 0, len(workspaces)) + templateIDs := make([]uuid.UUID, 0, len(workspaces)) + for _, workspace := range workspaces { + workspaceIDs = append(workspaceIDs, workspace.ID) + templateIDs = append(templateIDs, workspace.TemplateID) + } + workspaceBuilds, err := db.GetWorkspaceBuildsByWorkspaceIDsWithoutAfter(ctx, workspaceIDs) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + if err != nil { + return nil, xerrors.Errorf("get workspace builds: %w", err) + } + templates, err := db.GetTemplatesByIDs(ctx, templateIDs) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + if err != nil { + return nil, xerrors.Errorf("get templates: %w", err) + } + jobIDs := make([]uuid.UUID, 0, len(workspaceBuilds)) + for _, build := range workspaceBuilds { + jobIDs = append(jobIDs, build.JobID) + } + jobs, err := db.GetProvisionerJobsByIDs(ctx, jobIDs) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + if err != nil { + return nil, xerrors.Errorf("get provisioner jobs: %w", err) + } + + buildByWorkspaceID := map[uuid.UUID]database.WorkspaceBuild{} + for _, workspaceBuild := range workspaceBuilds { + buildByWorkspaceID[workspaceBuild.WorkspaceID] = workspaceBuild + } + templateByID := map[uuid.UUID]database.Template{} + for _, template := range templates { + templateByID[template.ID] = template + } + jobByID := map[uuid.UUID]database.ProvisionerJob{} + for _, job := range jobs { + jobByID[job.ID] = job + } + apiWorkspaces := make([]codersdk.Workspace, 0, len(workspaces)) + for _, workspace := range workspaces { + build, exists := buildByWorkspaceID[workspace.ID] + if !exists { + return nil, xerrors.Errorf("build not found for workspace %q", workspace.Name) + } + template, exists := templateByID[workspace.TemplateID] + if !exists { + return nil, xerrors.Errorf("template not found for workspace %q", workspace.Name) + } + job, exists := jobByID[build.JobID] + if !exists { + return nil, xerrors.Errorf("build job not found for workspace: %q", err) + } + apiWorkspaces = append(apiWorkspaces, + convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), template)) + } + return apiWorkspaces, nil +} + func convertWorkspace(workspace database.Workspace, workspaceBuild codersdk.WorkspaceBuild, template database.Template) codersdk.Workspace { return codersdk.Workspace{ ID: workspace.ID, diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index a46d2eadaa..f27cf7bd94 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -26,7 +26,7 @@ func TestWorkspace(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) _, err := client.Workspace(context.Background(), workspace.ID) require.NoError(t, err) } @@ -41,7 +41,7 @@ func TestWorkspaceBuilds(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) _, err := client.WorkspaceBuilds(context.Background(), workspace.ID) require.NoError(t, err) }) @@ -57,7 +57,7 @@ func TestPostWorkspaceBuild(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) _, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{ TemplateVersionID: uuid.New(), Transition: database.WorkspaceTransitionStart, @@ -78,7 +78,7 @@ func TestPostWorkspaceBuild(t *testing.T) { }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - _, err := client.CreateWorkspace(context.Background(), codersdk.Me, codersdk.CreateWorkspaceRequest{ + _, err := client.CreateWorkspace(context.Background(), user.OrganizationID, codersdk.CreateWorkspaceRequest{ TemplateID: template.ID, Name: "workspace", }) @@ -97,7 +97,7 @@ func TestPostWorkspaceBuild(t *testing.T) { coderdtest.AwaitTemplateVersionJob(t, client, version.ID) // Close here so workspace build doesn't process! closeDaemon.Close() - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) _, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{ TemplateVersionID: template.ActiveVersionID, Transition: database.WorkspaceTransitionStart, @@ -116,7 +116,7 @@ func TestPostWorkspaceBuild(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{ TemplateVersionID: template.ActiveVersionID, @@ -138,7 +138,7 @@ func TestPostWorkspaceBuild(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{ Transition: database.WorkspaceTransitionDelete, @@ -147,7 +147,7 @@ func TestPostWorkspaceBuild(t *testing.T) { require.Equal(t, workspace.LatestBuild.ID.String(), build.BeforeID.String()) coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID) - workspaces, err := client.WorkspacesByUser(context.Background(), user.UserID) + workspaces, err := client.WorkspacesByOwner(context.Background(), user.OrganizationID, user.UserID) require.NoError(t, err) require.Len(t, workspaces, 0) }) @@ -163,7 +163,7 @@ func TestWorkspaceBuildByName(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) _, err := client.WorkspaceBuildByName(context.Background(), workspace.ID, "something") var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) @@ -178,7 +178,7 @@ func TestWorkspaceBuildByName(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) build, err := client.WorkspaceBuild(context.Background(), workspace.LatestBuild.ID) require.NoError(t, err) _, err = client.WorkspaceBuildByName(context.Background(), workspace.ID, build.Name) @@ -266,7 +266,7 @@ func TestWorkspaceUpdateAutostart(t *testing.T) { version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID) + workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID) ) // ensure test invariant: new workspaces have no autostart schedule. @@ -400,7 +400,7 @@ func TestWorkspaceUpdateAutostop(t *testing.T) { version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID) + workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID) ) // ensure test invariant: new workspaces have no autostop schedule. diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 68c563c685..ce0690c466 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -48,6 +48,15 @@ type CreateTemplateRequest struct { ParameterValues []CreateParameterRequest `json:"parameter_values"` } +// CreateWorkspaceRequest provides options for creating a new workspace. +type CreateWorkspaceRequest struct { + TemplateID uuid.UUID `json:"template_id" validate:"required"` + Name string `json:"name" validate:"username,required"` + // ParameterValues allows for additional parameters to be provided + // during the initial provision. + ParameterValues []CreateParameterRequest `json:"parameter_values"` +} + func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization, error) { res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s", id.String()), nil) if err != nil { @@ -158,3 +167,67 @@ func (c *Client) TemplateByName(ctx context.Context, organizationID uuid.UUID, n var template Template return template, json.NewDecoder(res.Body).Decode(&template) } + +// CreateWorkspace creates a new workspace for the template specified. +func (c *Client) CreateWorkspace(ctx context.Context, organizationID uuid.UUID, request CreateWorkspaceRequest) (Workspace, error) { + res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/workspaces", organizationID), request) + if err != nil { + return Workspace{}, err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusCreated { + return Workspace{}, readBodyAsError(res) + } + + var workspace Workspace + return workspace, json.NewDecoder(res.Body).Decode(&workspace) +} + +// WorkspacesByOrganization returns all workspaces in the specified organization. +func (c *Client) WorkspacesByOrganization(ctx context.Context, organizationID uuid.UUID) ([]Workspace, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/workspaces", organizationID), nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, readBodyAsError(res) + } + + var workspaces []Workspace + return workspaces, json.NewDecoder(res.Body).Decode(&workspaces) +} + +// WorkspacesByOwner returns all workspaces contained in the organization owned by the user. +func (c *Client) WorkspacesByOwner(ctx context.Context, organizationID, userID uuid.UUID) ([]Workspace, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/workspaces/%s", organizationID, uuidOrMe(userID)), nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, readBodyAsError(res) + } + + var workspaces []Workspace + return workspaces, json.NewDecoder(res.Body).Decode(&workspaces) +} + +// WorkspaceByOwnerAndName returns a workspace by the owner's UUID and the workspace's name. +func (c *Client) WorkspaceByOwnerAndName(ctx context.Context, organization, owner uuid.UUID, name string) (Workspace, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/workspaces/%s/%s", organization, uuidOrMe(owner), name), nil) + if err != nil { + return Workspace{}, err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return Workspace{}, readBodyAsError(res) + } + + var workspace Workspace + return workspace, json.NewDecoder(res.Body).Decode(&workspace) +} diff --git a/codersdk/users.go b/codersdk/users.go index da5f8f23fa..84d73285a7 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -81,15 +81,6 @@ type CreateOrganizationRequest struct { Name string `json:"name" validate:"required,username"` } -// CreateWorkspaceRequest provides options for creating a new workspace. -type CreateWorkspaceRequest struct { - TemplateID uuid.UUID `json:"template_id" validate:"required"` - Name string `json:"name" validate:"username,required"` - // ParameterValues allows for additional parameters to be provided - // during the initial provision. - ParameterValues []CreateParameterRequest `json:"parameter_values"` -} - // AuthMethods contains whether authentication types are enabled or not. type AuthMethods struct { Password bool `json:"password"` @@ -287,53 +278,6 @@ func (c *Client) CreateOrganization(ctx context.Context, userID uuid.UUID, req C return org, json.NewDecoder(res.Body).Decode(&org) } -// CreateWorkspace creates a new workspace for the template specified. -func (c *Client) CreateWorkspace(ctx context.Context, userID uuid.UUID, request CreateWorkspaceRequest) (Workspace, error) { - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/workspaces", uuidOrMe(userID)), request) - if err != nil { - return Workspace{}, err - } - defer res.Body.Close() - - if res.StatusCode != http.StatusCreated { - return Workspace{}, readBodyAsError(res) - } - - var workspace Workspace - return workspace, json.NewDecoder(res.Body).Decode(&workspace) -} - -// WorkspacesByUser returns all workspaces the specified user has access to. -func (c *Client) WorkspacesByUser(ctx context.Context, userID uuid.UUID) ([]Workspace, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/workspaces", uuidOrMe(userID)), nil) - if err != nil { - return nil, err - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) - } - - var workspaces []Workspace - return workspaces, json.NewDecoder(res.Body).Decode(&workspaces) -} - -func (c *Client) WorkspaceByName(ctx context.Context, userID uuid.UUID, name string) (Workspace, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/workspaces/%s", uuidOrMe(userID), name), nil) - if err != nil { - return Workspace{}, err - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - return Workspace{}, readBodyAsError(res) - } - - var workspace Workspace - return workspace, json.NewDecoder(res.Body).Decode(&workspace) -} - // AuthMethods returns types of authentication available to the user. func (c *Client) AuthMethods(ctx context.Context) (AuthMethods, error) { res, err := c.request(ctx, http.MethodGet, "/api/v2/users/authmethods", nil) diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 05f42cd06e..7c739737dd 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -13,8 +13,8 @@ import ( "github.com/coder/coder/coderd/database" ) -// Workspace is a per-user deployment of a template. It tracks -// template versions, and can be updated. +// Workspace is a deployment of a template. It references a specific +// version and can be updated. type Workspace struct { ID uuid.UUID `json:"id"` CreatedAt time.Time `json:"created_at"` diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 230e600eee..58507b2ffb 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -35,6 +35,11 @@ export interface CreateTemplateRequest { readonly name: string } +// From codersdk/organizations.go:52:6. +export interface CreateWorkspaceRequest { + readonly name: string +} + // From codersdk/parameters.go:26:6. export interface Parameter { readonly scope: ParameterScope @@ -127,11 +132,6 @@ export interface CreateOrganizationRequest { } // From codersdk/users.go:85:6. -export interface CreateWorkspaceRequest { - readonly name: string -} - -// From codersdk/users.go:94:6. export interface AuthMethods { readonly password: boolean readonly github: boolean