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`.
This commit is contained in:
Kyle Carberry 2022-04-25 16:11:03 -05:00 committed by GitHub
parent 759fa5f626
commit 88669fd578
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 885 additions and 671 deletions

View File

@ -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())

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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
}

View File

@ -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!

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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,

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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",
})

View File

@ -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)
})

View File

@ -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(),
})

View File

@ -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)

View File

@ -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

View File

@ -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;

View File

@ -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,

View File

@ -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"`

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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()

View File

@ -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{

View File

@ -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)

View File

@ -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{

View File

@ -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)
})
}

View File

@ -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)

View File

@ -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)

View File

@ -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.

View File

@ -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) {

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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.

View File

@ -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)
}

View File

@ -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)

View File

@ -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"`

View File

@ -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