diff --git a/coderd/organizations.go b/coderd/organizations.go index 43bdf5432d898..feb7a7ba9dc18 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -1,17 +1,8 @@ package coderd import ( - "database/sql" - "encoding/json" - "errors" - "fmt" "net/http" - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - "github.com/moby/moby/pkg/namesgenerator" - "golang.org/x/xerrors" - "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/httpmw" @@ -23,612 +14,6 @@ func (*api) organization(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, convertOrganization(organization)) } -func (api *api) provisionerDaemonsByOrganization(rw http.ResponseWriter, r *http.Request) { - daemons, err := api.Database.GetProvisionerDaemons(r.Context()) - if errors.Is(err, sql.ErrNoRows) { - err = nil - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get provisioner daemons: %s", err), - }) - return - } - if daemons == nil { - daemons = []database.ProvisionerDaemon{} - } - httpapi.Write(rw, http.StatusOK, daemons) -} - -// Creates a new version of a template. An import job is queued to parse the storage method provided. -func (api *api) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *http.Request) { - apiKey := httpmw.APIKey(r) - organization := httpmw.OrganizationParam(r) - var req codersdk.CreateTemplateVersionRequest - if !httpapi.Read(rw, r, &req) { - return - } - if req.TemplateID != uuid.Nil { - _, err := api.Database.GetTemplateByID(r.Context(), req.TemplateID) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: "template does not exist", - }) - return - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get template: %s", err), - }) - return - } - } - - file, err := api.Database.GetFileByHash(r.Context(), req.StorageSource) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: "file not found", - }) - return - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get file: %s", err), - }) - return - } - - var templateVersion database.TemplateVersion - var provisionerJob database.ProvisionerJob - err = api.Database.InTx(func(db database.Store) error { - jobID := uuid.New() - for _, parameterValue := range req.ParameterValues { - _, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{ - ID: uuid.New(), - Name: parameterValue.Name, - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - Scope: database.ParameterScopeImportJob, - ScopeID: jobID, - SourceScheme: parameterValue.SourceScheme, - SourceValue: parameterValue.SourceValue, - DestinationScheme: parameterValue.DestinationScheme, - }) - if err != nil { - return xerrors.Errorf("insert parameter value: %w", err) - } - } - - provisionerJob, err = api.Database.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{ - ID: jobID, - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - OrganizationID: organization.ID, - InitiatorID: apiKey.UserID, - Provisioner: req.Provisioner, - StorageMethod: database.ProvisionerStorageMethodFile, - StorageSource: file.Hash, - Type: database.ProvisionerJobTypeTemplateVersionImport, - Input: []byte{'{', '}'}, - }) - if err != nil { - return xerrors.Errorf("insert provisioner job: %w", err) - } - - var templateID uuid.NullUUID - if req.TemplateID != uuid.Nil { - templateID = uuid.NullUUID{ - UUID: req.TemplateID, - Valid: true, - } - } - - templateVersion, err = api.Database.InsertTemplateVersion(r.Context(), database.InsertTemplateVersionParams{ - ID: uuid.New(), - TemplateID: templateID, - OrganizationID: organization.ID, - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - Name: namesgenerator.GetRandomName(1), - Description: "", - JobID: provisionerJob.ID, - }) - if err != nil { - return xerrors.Errorf("insert template version: %w", err) - } - return nil - }) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: err.Error(), - }) - return - } - - httpapi.Write(rw, http.StatusCreated, convertTemplateVersion(templateVersion, convertProvisionerJob(provisionerJob))) -} - -// Create a new template in an organization. -func (api *api) postTemplatesByOrganization(rw http.ResponseWriter, r *http.Request) { - var createTemplate codersdk.CreateTemplateRequest - if !httpapi.Read(rw, r, &createTemplate) { - return - } - organization := httpmw.OrganizationParam(r) - _, err := api.Database.GetTemplateByOrganizationAndName(r.Context(), database.GetTemplateByOrganizationAndNameParams{ - OrganizationID: organization.ID, - Name: createTemplate.Name, - }) - if err == nil { - httpapi.Write(rw, http.StatusConflict, httpapi.Response{ - Message: fmt.Sprintf("template %q already exists", createTemplate.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 template by name: %s", err), - }) - return - } - templateVersion, err := api.Database.GetTemplateVersionByID(r.Context(), createTemplate.VersionID) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: "template version does not exist", - }) - return - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get template version by id: %s", err), - }) - return - } - importJob, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get import job by id: %s", err), - }) - return - } - - var template codersdk.Template - err = api.Database.InTx(func(db database.Store) error { - now := database.Now() - dbTemplate, err := db.InsertTemplate(r.Context(), database.InsertTemplateParams{ - ID: uuid.New(), - CreatedAt: now, - UpdatedAt: now, - OrganizationID: organization.ID, - Name: createTemplate.Name, - Provisioner: importJob.Provisioner, - ActiveVersionID: templateVersion.ID, - }) - if err != nil { - return xerrors.Errorf("insert template: %s", err) - } - - err = db.UpdateTemplateVersionByID(r.Context(), database.UpdateTemplateVersionByIDParams{ - ID: templateVersion.ID, - TemplateID: uuid.NullUUID{ - UUID: dbTemplate.ID, - Valid: true, - }, - }) - if err != nil { - return xerrors.Errorf("insert template version: %s", err) - } - - for _, parameterValue := range createTemplate.ParameterValues { - _, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{ - ID: uuid.New(), - Name: parameterValue.Name, - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - Scope: database.ParameterScopeTemplate, - ScopeID: dbTemplate.ID, - SourceScheme: parameterValue.SourceScheme, - SourceValue: parameterValue.SourceValue, - DestinationScheme: parameterValue.DestinationScheme, - }) - if err != nil { - return xerrors.Errorf("insert parameter value: %w", err) - } - } - - template = convertTemplate(dbTemplate, 0) - return nil - }) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: err.Error(), - }) - return - } - - httpapi.Write(rw, http.StatusCreated, template) -} - -func (api *api) templatesByOrganization(rw http.ResponseWriter, r *http.Request) { - organization := httpmw.OrganizationParam(r) - templates, err := api.Database.GetTemplatesByOrganization(r.Context(), database.GetTemplatesByOrganizationParams{ - OrganizationID: organization.ID, - }) - 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.Error()), - }) - return - } - templateIDs := make([]uuid.UUID, 0, len(templates)) - for _, template := range templates { - templateIDs = append(templateIDs, template.ID) - } - workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByTemplateIDs(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 workspace counts: %s", err.Error()), - }) - return - } - - httpapi.Write(rw, http.StatusOK, convertTemplates(templates, workspaceCounts)) -} - -func (api *api) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Request) { - organization := httpmw.OrganizationParam(r) - templateName := chi.URLParam(r, "templatename") - template, err := api.Database.GetTemplateByOrganizationAndName(r.Context(), database.GetTemplateByOrganizationAndNameParams{ - OrganizationID: organization.ID, - Name: templateName, - }) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: fmt.Sprintf("no template found by name %q in the %q organization", templateName, organization.Name), - }) - return - } - - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get template by organization and name: %s", err), - }) - return - } - - workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByTemplateIDs(r.Context(), []uuid.UUID{template.ID}) - if errors.Is(err, sql.ErrNoRows) { - err = nil - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspace counts: %s", err.Error()), - }) - return - } - - count := uint32(0) - if len(workspaceCounts) > 0 { - count = uint32(workspaceCounts[0].Count) - } - - 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, owner)) -} - -// 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 - } - user, err := api.Database.GetUserByID(r.Context(), apiKey.UserID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get user: %s", err), - }) - return - } - - httpapi.Write(rw, http.StatusCreated, convertWorkspace(workspace, - convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(templateVersionJob)), template, user)) -} - // 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 74015901fcdb9..e6338f61cb7fe 100644 --- a/coderd/organizations_test.go +++ b/coderd/organizations_test.go @@ -5,328 +5,83 @@ import ( "net/http" "testing" - "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/coder/coder/coderd/coderdtest" - "github.com/coder/coder/coderd/database" "github.com/coder/coder/codersdk" - "github.com/coder/coder/provisioner/echo" ) -func TestProvisionerDaemonsByOrganization(t *testing.T) { +func TestOrganizationsByUser(t *testing.T) { t.Parallel() - t.Run("NoAuth", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.ProvisionerDaemonsByOrganization(context.Background(), uuid.New()) - require.Error(t, err) - }) - - t.Run("Get", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - _, err := client.ProvisionerDaemonsByOrganization(context.Background(), user.OrganizationID) - require.NoError(t, err) - }) -} - -func TestPostTemplateVersionsByOrganization(t *testing.T) { - t.Parallel() - t.Run("InvalidTemplate", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - templateID := uuid.New() - _, err := client.CreateTemplateVersion(context.Background(), user.OrganizationID, codersdk.CreateTemplateVersionRequest{ - TemplateID: templateID, - StorageMethod: database.ProvisionerStorageMethodFile, - StorageSource: "hash", - Provisioner: database.ProvisionerTypeEcho, - }) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) - }) - - t.Run("FileNotFound", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - _, err := client.CreateTemplateVersion(context.Background(), user.OrganizationID, codersdk.CreateTemplateVersionRequest{ - StorageMethod: database.ProvisionerStorageMethodFile, - StorageSource: "hash", - Provisioner: database.ProvisionerTypeEcho, - }) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) - }) - - t.Run("WithParameters", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - data, err := echo.Tar(&echo.Responses{ - Parse: echo.ParseComplete, - Provision: echo.ProvisionComplete, - ProvisionDryRun: echo.ProvisionComplete, - }) - require.NoError(t, err) - file, err := client.Upload(context.Background(), codersdk.ContentTypeTar, data) - require.NoError(t, err) - _, err = client.CreateTemplateVersion(context.Background(), user.OrganizationID, codersdk.CreateTemplateVersionRequest{ - StorageMethod: database.ProvisionerStorageMethodFile, - StorageSource: file.Hash, - Provisioner: database.ProvisionerTypeEcho, - ParameterValues: []codersdk.CreateParameterRequest{{ - Name: "example", - SourceValue: "value", - SourceScheme: database.ParameterSourceSchemeData, - DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable, - }}, - }) - require.NoError(t, err) - }) -} - -func TestPostTemplatesByOrganization(t *testing.T) { - t.Parallel() - t.Run("Create", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - _ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - }) - - t.Run("AlreadyExists", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - _, err := client.CreateTemplate(context.Background(), user.OrganizationID, codersdk.CreateTemplateRequest{ - Name: template.Name, - VersionID: version.ID, - }) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusConflict, apiErr.StatusCode()) - }) - - t.Run("NoVersion", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - _, err := client.CreateTemplate(context.Background(), user.OrganizationID, codersdk.CreateTemplateRequest{ - Name: "test", - VersionID: uuid.New(), - }) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) - }) -} - -func TestTemplatesByOrganization(t *testing.T) { - t.Parallel() - t.Run("ListEmpty", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - templates, err := client.TemplatesByOrganization(context.Background(), user.OrganizationID) - require.NoError(t, err) - require.NotNil(t, templates) - require.Len(t, templates, 0) - }) - - t.Run("List", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - templates, err := client.TemplatesByOrganization(context.Background(), user.OrganizationID) - require.NoError(t, err) - require.Len(t, templates, 1) - }) - t.Run("ListMultiple", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - templates, err := client.TemplatesByOrganization(context.Background(), user.OrganizationID) - require.NoError(t, err) - require.Len(t, templates, 2) - }) + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + orgs, err := client.OrganizationsByUser(context.Background(), codersdk.Me) + require.NoError(t, err) + require.NotNil(t, orgs) + require.Len(t, orgs, 1) } -func TestTemplateByOrganizationAndName(t *testing.T) { +func TestOrganizationByUserAndName(t *testing.T) { t.Parallel() - t.Run("NotFound", func(t *testing.T) { + t.Run("NoExist", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - _, err := client.TemplateByName(context.Background(), user.OrganizationID, "something") + coderdtest.CreateFirstUser(t, client) + _, err := client.OrganizationByName(context.Background(), codersdk.Me, "nothing") var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) }) - t.Run("Found", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - _, err := client.TemplateByName(context.Background(), user.OrganizationID, template.Name) - 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.Run("NoMember", 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) + _, err = client.OrganizationByName(context.Background(), codersdk.Me, org.Name) 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.Run("Valid", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) - _, err := client.WorkspacesByOrganization(context.Background(), user.OrganizationID) + org, err := client.Organization(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) + _, err = client.OrganizationByName(context.Background(), codersdk.Me, org.Name) require.NoError(t, err) - require.Len(t, workspaces, 1) }) } -func TestWorkspacesByOwner(t *testing.T) { +func TestPostOrganizationsByUser(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.Run("Conflict", 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) + org, err := client.Organization(context.Background(), user.OrganizationID) 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") + _, err = client.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{ + Name: org.Name, + }) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + require.Equal(t, http.StatusConflict, apiErr.StatusCode()) }) - t.Run("Get", func(t *testing.T) { + + 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) - 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) + _ = coderdtest.CreateFirstUser(t, client) + _, err := client.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{ + Name: "new", + }) require.NoError(t, err) }) } diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index a7639e6b76c3c..9a27fbe6e4857 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -32,6 +32,23 @@ import ( sdkproto "github.com/coder/coder/provisionersdk/proto" ) +func (api *api) provisionerDaemonsByOrganization(rw http.ResponseWriter, r *http.Request) { + daemons, err := api.Database.GetProvisionerDaemons(r.Context()) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner daemons: %s", err), + }) + return + } + if daemons == nil { + daemons = []database.ProvisionerDaemon{} + } + httpapi.Write(rw, http.StatusOK, daemons) +} + // Serves the provisioner daemon protobuf API over a WebSocket. func (api *api) provisionerDaemonsListen(rw http.ResponseWriter, r *http.Request) { api.websocketWaitMutex.Lock() diff --git a/coderd/provisionerdaemons_test.go b/coderd/provisionerdaemons_test.go index 01e9b2dd1abc8..647045f7296a7 100644 --- a/coderd/provisionerdaemons_test.go +++ b/coderd/provisionerdaemons_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/coder/coder/coderd/coderdtest" @@ -46,3 +47,21 @@ func TestProvisionerDaemons(t *testing.T) { }, 5*time.Second, 25*time.Millisecond) }) } + +func TestProvisionerDaemonsByOrganization(t *testing.T) { + t.Parallel() + t.Run("NoAuth", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _, err := client.ProvisionerDaemonsByOrganization(context.Background(), uuid.New()) + require.Error(t, err) + }) + + t.Run("Get", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + _, err := client.ProvisionerDaemonsByOrganization(context.Background(), user.OrganizationID) + require.NoError(t, err) + }) +} diff --git a/coderd/templates.go b/coderd/templates.go index 12958c0860070..89350c38c8f0c 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -8,6 +8,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/google/uuid" + "golang.org/x/xerrors" "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" @@ -73,132 +74,181 @@ func (api *api) deleteTemplate(rw http.ResponseWriter, r *http.Request) { }) } -func (api *api) templateVersionsByTemplate(rw http.ResponseWriter, r *http.Request) { - template := httpmw.TemplateParam(r) - - paginationParams, ok := parsePagination(rw, r) - if !ok { +// Create a new template in an organization. +func (api *api) postTemplatesByOrganization(rw http.ResponseWriter, r *http.Request) { + var createTemplate codersdk.CreateTemplateRequest + if !httpapi.Read(rw, r, &createTemplate) { return } - - apiVersion := []codersdk.TemplateVersion{} - versions, err := api.Database.GetTemplateVersionsByTemplateID(r.Context(), database.GetTemplateVersionsByTemplateIDParams{ - TemplateID: template.ID, - AfterID: paginationParams.AfterID, - LimitOpt: int32(paginationParams.Limit), - OffsetOpt: int32(paginationParams.Offset), + organization := httpmw.OrganizationParam(r) + _, err := api.Database.GetTemplateByOrganizationAndName(r.Context(), database.GetTemplateByOrganizationAndNameParams{ + OrganizationID: organization.ID, + Name: createTemplate.Name, }) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusOK, apiVersion) + if err == nil { + httpapi.Write(rw, http.StatusConflict, httpapi.Response{ + Message: fmt.Sprintf("template %q already exists", createTemplate.Name), + Errors: []httpapi.Error{{ + Field: "name", + Detail: "this value is already in use and should be unique", + }}, + }) return } - if err != nil { + if !errors.Is(err, sql.ErrNoRows) { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get template version: %s", err), + Message: fmt.Sprintf("get template by name: %s", err), }) return } - jobIDs := make([]uuid.UUID, 0, len(versions)) - for _, version := range versions { - jobIDs = append(jobIDs, version.JobID) + templateVersion, err := api.Database.GetTemplateVersionByID(r.Context(), createTemplate.VersionID) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: "template version does not exist", + }) + return } - jobs, err := api.Database.GetProvisionerJobsByIDs(r.Context(), jobIDs) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get jobs: %s", err), + Message: fmt.Sprintf("get template version by id: %s", err), }) return } - jobByID := map[string]database.ProvisionerJob{} - for _, job := range jobs { - jobByID[job.ID.String()] = job + importJob, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get import job by id: %s", err), + }) + return } - for _, version := range versions { - job, exists := jobByID[version.JobID.String()] - if !exists { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("job %q doesn't exist for version %q", version.JobID, version.ID), + var template codersdk.Template + err = api.Database.InTx(func(db database.Store) error { + now := database.Now() + dbTemplate, err := db.InsertTemplate(r.Context(), database.InsertTemplateParams{ + ID: uuid.New(), + CreatedAt: now, + UpdatedAt: now, + OrganizationID: organization.ID, + Name: createTemplate.Name, + Provisioner: importJob.Provisioner, + ActiveVersionID: templateVersion.ID, + }) + if err != nil { + return xerrors.Errorf("insert template: %s", err) + } + + err = db.UpdateTemplateVersionByID(r.Context(), database.UpdateTemplateVersionByIDParams{ + ID: templateVersion.ID, + TemplateID: uuid.NullUUID{ + UUID: dbTemplate.ID, + Valid: true, + }, + }) + if err != nil { + return xerrors.Errorf("insert template version: %s", err) + } + + for _, parameterValue := range createTemplate.ParameterValues { + _, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{ + ID: uuid.New(), + Name: parameterValue.Name, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + Scope: database.ParameterScopeTemplate, + ScopeID: dbTemplate.ID, + SourceScheme: parameterValue.SourceScheme, + SourceValue: parameterValue.SourceValue, + DestinationScheme: parameterValue.DestinationScheme, }) - return + if err != nil { + return xerrors.Errorf("insert parameter value: %w", err) + } } - apiVersion = append(apiVersion, convertTemplateVersion(version, convertProvisionerJob(job))) + + template = convertTemplate(dbTemplate, 0) + return nil + }) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: err.Error(), + }) + return } - httpapi.Write(rw, http.StatusOK, apiVersion) + httpapi.Write(rw, http.StatusCreated, template) } -func (api *api) templateVersionByName(rw http.ResponseWriter, r *http.Request) { - template := httpmw.TemplateParam(r) - templateVersionName := chi.URLParam(r, "templateversionname") - templateVersion, err := api.Database.GetTemplateVersionByTemplateIDAndName(r.Context(), database.GetTemplateVersionByTemplateIDAndNameParams{ - TemplateID: uuid.NullUUID{ - UUID: template.ID, - Valid: true, - }, - Name: templateVersionName, +func (api *api) templatesByOrganization(rw http.ResponseWriter, r *http.Request) { + organization := httpmw.OrganizationParam(r) + templates, err := api.Database.GetTemplatesByOrganization(r.Context(), database.GetTemplatesByOrganizationParams{ + OrganizationID: organization.ID, }) if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: fmt.Sprintf("no template version found by name %q", templateVersionName), - }) - return + err = nil } if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get template version by name: %s", err), + Message: fmt.Sprintf("get templates: %s", err.Error()), }) return } - job, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID) + templateIDs := make([]uuid.UUID, 0, len(templates)) + for _, template := range templates { + templateIDs = append(templateIDs, template.ID) + } + workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByTemplateIDs(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 provisioner job: %s", err), + Message: fmt.Sprintf("get workspace counts: %s", err.Error()), }) return } - httpapi.Write(rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(job))) + httpapi.Write(rw, http.StatusOK, convertTemplates(templates, workspaceCounts)) } -func (api *api) patchActiveTemplateVersion(rw http.ResponseWriter, r *http.Request) { - var req codersdk.UpdateActiveTemplateVersion - if !httpapi.Read(rw, r, &req) { - return - } - template := httpmw.TemplateParam(r) - version, err := api.Database.GetTemplateVersionByID(r.Context(), req.ID) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: "template version not found", - }) - return - } +func (api *api) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Request) { + organization := httpmw.OrganizationParam(r) + templateName := chi.URLParam(r, "templatename") + template, err := api.Database.GetTemplateByOrganizationAndName(r.Context(), database.GetTemplateByOrganizationAndNameParams{ + OrganizationID: organization.ID, + Name: templateName, + }) if err != nil { + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: fmt.Sprintf("no template found by name %q in the %q organization", templateName, organization.Name), + }) + return + } + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get template version: %s", err), + Message: fmt.Sprintf("get template by organization and name: %s", err), }) return } - if version.TemplateID.UUID.String() != template.ID.String() { - httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: "The provided template version doesn't belong to the specified template.", - }) - return + + workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByTemplateIDs(r.Context(), []uuid.UUID{template.ID}) + if errors.Is(err, sql.ErrNoRows) { + err = nil } - err = api.Database.UpdateTemplateActiveVersionByID(r.Context(), database.UpdateTemplateActiveVersionByIDParams{ - ID: template.ID, - ActiveVersionID: req.ID, - }) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("update active template version: %s", err), + Message: fmt.Sprintf("get workspace counts: %s", err.Error()), }) return } - httpapi.Write(rw, http.StatusOK, httpapi.Response{ - Message: "Updated the active template version!", - }) + + count := uint32(0) + if len(workspaceCounts) > 0 { + count = uint32(workspaceCounts[0].Count) + } + + httpapi.Write(rw, http.StatusOK, convertTemplate(template, count)) } func convertTemplates(templates []database.Template, workspaceCounts []database.GetWorkspaceOwnerCountsByTemplateIDsRow) []codersdk.Template { diff --git a/coderd/templates_test.go b/coderd/templates_test.go index f0082408f3a23..425635f9b216c 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -6,13 +6,10 @@ import ( "testing" "github.com/google/uuid" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coder/coder/coderd/coderdtest" - "github.com/coder/coder/coderd/database" "github.com/coder/coder/codersdk" - "github.com/coder/coder/provisioner/echo" ) func TestTemplate(t *testing.T) { @@ -29,105 +26,90 @@ func TestTemplate(t *testing.T) { }) } -func TestDeleteTemplate(t *testing.T) { +func TestPostTemplatesByOrganization(t *testing.T) { t.Parallel() - - t.Run("NoWorkspaces", func(t *testing.T) { + t.Run("Create", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - err := client.DeleteTemplate(context.Background(), template.ID) - require.NoError(t, err) + _ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) }) - t.Run("Workspaces", func(t *testing.T) { + t.Run("AlreadyExists", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) - coderdtest.NewProvisionerDaemon(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) - err := client.DeleteTemplate(context.Background(), template.ID) + _, err := client.CreateTemplate(context.Background(), user.OrganizationID, codersdk.CreateTemplateRequest{ + Name: template.Name, + VersionID: version.ID, + }) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode()) + require.Equal(t, http.StatusConflict, apiErr.StatusCode()) }) -} -func TestTemplateVersionsByTemplate(t *testing.T) { - t.Parallel() - t.Run("Get", func(t *testing.T) { + t.Run("NoVersion", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - versions, err := client.TemplateVersionsByTemplate(context.Background(), codersdk.TemplateVersionsByTemplateRequest{ - TemplateID: template.ID, + _, err := client.CreateTemplate(context.Background(), user.OrganizationID, codersdk.CreateTemplateRequest{ + Name: "test", + VersionID: uuid.New(), }) - require.NoError(t, err) - require.Len(t, versions, 1) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) }) } -func TestTemplateVersionByName(t *testing.T) { +func TestTemplatesByOrganization(t *testing.T) { t.Parallel() - t.Run("NotFound", func(t *testing.T) { + t.Run("ListEmpty", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - _, err := client.TemplateVersionByName(context.Background(), template.ID, "nothing") - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + templates, err := client.TemplatesByOrganization(context.Background(), user.OrganizationID) + require.NoError(t, err) + require.NotNil(t, templates) + require.Len(t, templates, 0) }) - t.Run("Found", func(t *testing.T) { + t.Run("List", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - _, err := client.TemplateVersionByName(context.Background(), template.ID, version.Name) + coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + templates, err := client.TemplatesByOrganization(context.Background(), user.OrganizationID) require.NoError(t, err) + require.Len(t, templates, 1) }) -} - -func TestPatchActiveTemplateVersion(t *testing.T) { - t.Parallel() - t.Run("NotFound", func(t *testing.T) { + t.Run("ListMultiple", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - err := client.UpdateActiveTemplateVersion(context.Background(), template.ID, codersdk.UpdateActiveTemplateVersion{ - ID: uuid.New(), - }) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + templates, err := client.TemplatesByOrganization(context.Background(), user.OrganizationID) + require.NoError(t, err) + require.Len(t, templates, 2) }) +} - t.Run("DoesNotBelong", func(t *testing.T) { +func TestTemplateByOrganizationAndName(t *testing.T) { + t.Parallel() + t.Run("NotFound", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - err := client.UpdateActiveTemplateVersion(context.Background(), template.ID, codersdk.UpdateActiveTemplateVersion{ - ID: version.ID, - }) + _, err := client.TemplateByName(context.Background(), user.OrganizationID, "something") var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) }) t.Run("Found", func(t *testing.T) { @@ -136,102 +118,36 @@ func TestPatchActiveTemplateVersion(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - err := client.UpdateActiveTemplateVersion(context.Background(), template.ID, codersdk.UpdateActiveTemplateVersion{ - ID: version.ID, - }) + _, err := client.TemplateByName(context.Background(), user.OrganizationID, template.Name) require.NoError(t, err) }) } -// TestPaginatedTemplateVersions creates a list of template versions and paginate. -func TestPaginatedTemplateVersions(t *testing.T) { +func TestDeleteTemplate(t *testing.T) { t.Parallel() - ctx := context.Background() - client := coderdtest.New(t, &coderdtest.Options{APIRateLimit: -1}) - // Prepare database. - user := coderdtest.CreateFirstUser(t, client) - coderdtest.NewProvisionerDaemon(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) - - // Populate database with template versions. - total := 9 - for i := 0; i < total; i++ { - data, err := echo.Tar(nil) - require.NoError(t, err) - file, err := client.Upload(context.Background(), codersdk.ContentTypeTar, data) - require.NoError(t, err) - templateVersion, err := client.CreateTemplateVersion(ctx, user.OrganizationID, codersdk.CreateTemplateVersionRequest{ - TemplateID: template.ID, - StorageSource: file.Hash, - StorageMethod: database.ProvisionerStorageMethodFile, - Provisioner: database.ProvisionerTypeEcho, - }) + t.Run("NoWorkspaces", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + err := client.DeleteTemplate(context.Background(), template.ID) require.NoError(t, err) + }) - _ = coderdtest.AwaitTemplateVersionJob(t, client, templateVersion.ID) - } - - templateVersions, err := client.TemplateVersionsByTemplate(ctx, - codersdk.TemplateVersionsByTemplateRequest{ - TemplateID: template.ID, - }, - ) - require.NoError(t, err) - require.Len(t, templateVersions, 10, "wrong number of template versions created") - - type args struct { - ctx context.Context - pagination codersdk.Pagination - } - tests := []struct { - name string - args args - want []codersdk.TemplateVersion - }{ - { - name: "Single result", - args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 1}}, - want: templateVersions[:1], - }, - { - name: "Single result, second page", - args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 1, Offset: 1}}, - want: templateVersions[1:2], - }, - { - name: "Last two results", - args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 2, Offset: 8}}, - want: templateVersions[8:10], - }, - { - name: "AfterID returns next two results", - args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 2, AfterID: templateVersions[1].ID}}, - want: templateVersions[2:4], - }, - { - name: "No result after last AfterID", - args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 2, AfterID: templateVersions[9].ID}}, - want: []codersdk.TemplateVersion{}, - }, - { - name: "No result after last Offset", - args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 2, Offset: 10}}, - want: []codersdk.TemplateVersion{}, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - got, err := client.TemplateVersionsByTemplate(tt.args.ctx, codersdk.TemplateVersionsByTemplateRequest{ - TemplateID: template.ID, - Pagination: tt.args.pagination, - }) - assert.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } + t.Run("Workspaces", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(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) + err := client.DeleteTemplate(context.Background(), template.ID) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode()) + }) } diff --git a/coderd/templateversions.go b/coderd/templateversions.go index e0f9323825c46..99c6c62383811 100644 --- a/coderd/templateversions.go +++ b/coderd/templateversions.go @@ -6,6 +6,11 @@ import ( "fmt" "net/http" + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/moby/moby/pkg/namesgenerator" + "golang.org/x/xerrors" + "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/httpmw" @@ -134,6 +139,242 @@ func (api *api) templateVersionParameters(rw http.ResponseWriter, r *http.Reques httpapi.Write(rw, http.StatusOK, values) } +func (api *api) templateVersionsByTemplate(rw http.ResponseWriter, r *http.Request) { + template := httpmw.TemplateParam(r) + + paginationParams, ok := parsePagination(rw, r) + if !ok { + return + } + + apiVersion := []codersdk.TemplateVersion{} + versions, err := api.Database.GetTemplateVersionsByTemplateID(r.Context(), database.GetTemplateVersionsByTemplateIDParams{ + TemplateID: template.ID, + AfterID: paginationParams.AfterID, + LimitOpt: int32(paginationParams.Limit), + OffsetOpt: int32(paginationParams.Offset), + }) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusOK, apiVersion) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get template version: %s", err), + }) + return + } + jobIDs := make([]uuid.UUID, 0, len(versions)) + for _, version := range versions { + jobIDs = append(jobIDs, version.JobID) + } + jobs, err := api.Database.GetProvisionerJobsByIDs(r.Context(), jobIDs) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get jobs: %s", err), + }) + return + } + jobByID := map[string]database.ProvisionerJob{} + for _, job := range jobs { + jobByID[job.ID.String()] = job + } + + for _, version := range versions { + job, exists := jobByID[version.JobID.String()] + if !exists { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("job %q doesn't exist for version %q", version.JobID, version.ID), + }) + return + } + apiVersion = append(apiVersion, convertTemplateVersion(version, convertProvisionerJob(job))) + } + + httpapi.Write(rw, http.StatusOK, apiVersion) +} + +func (api *api) templateVersionByName(rw http.ResponseWriter, r *http.Request) { + template := httpmw.TemplateParam(r) + templateVersionName := chi.URLParam(r, "templateversionname") + templateVersion, err := api.Database.GetTemplateVersionByTemplateIDAndName(r.Context(), database.GetTemplateVersionByTemplateIDAndNameParams{ + TemplateID: uuid.NullUUID{ + UUID: template.ID, + Valid: true, + }, + Name: templateVersionName, + }) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: fmt.Sprintf("no template version found by name %q", templateVersionName), + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get template version by name: %s", err), + }) + return + } + job, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job: %s", err), + }) + return + } + + httpapi.Write(rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(job))) +} + +func (api *api) patchActiveTemplateVersion(rw http.ResponseWriter, r *http.Request) { + var req codersdk.UpdateActiveTemplateVersion + if !httpapi.Read(rw, r, &req) { + return + } + template := httpmw.TemplateParam(r) + version, err := api.Database.GetTemplateVersionByID(r.Context(), req.ID) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: "template version not found", + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get template version: %s", err), + }) + return + } + if version.TemplateID.UUID.String() != template.ID.String() { + httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ + Message: "The provided template version doesn't belong to the specified template.", + }) + return + } + err = api.Database.UpdateTemplateActiveVersionByID(r.Context(), database.UpdateTemplateActiveVersionByIDParams{ + ID: template.ID, + ActiveVersionID: req.ID, + }) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("update active template version: %s", err), + }) + return + } + httpapi.Write(rw, http.StatusOK, httpapi.Response{ + Message: "Updated the active template version!", + }) +} + +// Creates a new version of a template. An import job is queued to parse the storage method provided. +func (api *api) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *http.Request) { + apiKey := httpmw.APIKey(r) + organization := httpmw.OrganizationParam(r) + var req codersdk.CreateTemplateVersionRequest + if !httpapi.Read(rw, r, &req) { + return + } + if req.TemplateID != uuid.Nil { + _, err := api.Database.GetTemplateByID(r.Context(), req.TemplateID) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: "template does not exist", + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get template: %s", err), + }) + return + } + } + + file, err := api.Database.GetFileByHash(r.Context(), req.StorageSource) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: "file not found", + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get file: %s", err), + }) + return + } + + var templateVersion database.TemplateVersion + var provisionerJob database.ProvisionerJob + err = api.Database.InTx(func(db database.Store) error { + jobID := uuid.New() + for _, parameterValue := range req.ParameterValues { + _, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{ + ID: uuid.New(), + Name: parameterValue.Name, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + Scope: database.ParameterScopeImportJob, + ScopeID: jobID, + SourceScheme: parameterValue.SourceScheme, + SourceValue: parameterValue.SourceValue, + DestinationScheme: parameterValue.DestinationScheme, + }) + if err != nil { + return xerrors.Errorf("insert parameter value: %w", err) + } + } + + provisionerJob, err = api.Database.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{ + ID: jobID, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + OrganizationID: organization.ID, + InitiatorID: apiKey.UserID, + Provisioner: req.Provisioner, + StorageMethod: database.ProvisionerStorageMethodFile, + StorageSource: file.Hash, + Type: database.ProvisionerJobTypeTemplateVersionImport, + Input: []byte{'{', '}'}, + }) + if err != nil { + return xerrors.Errorf("insert provisioner job: %w", err) + } + + var templateID uuid.NullUUID + if req.TemplateID != uuid.Nil { + templateID = uuid.NullUUID{ + UUID: req.TemplateID, + Valid: true, + } + } + + templateVersion, err = api.Database.InsertTemplateVersion(r.Context(), database.InsertTemplateVersionParams{ + ID: uuid.New(), + TemplateID: templateID, + OrganizationID: organization.ID, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + Name: namesgenerator.GetRandomName(1), + Description: "", + JobID: provisionerJob.ID, + }) + if err != nil { + return xerrors.Errorf("insert template version: %w", err) + } + return nil + }) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: err.Error(), + }) + return + } + + httpapi.Write(rw, http.StatusCreated, convertTemplateVersion(templateVersion, convertProvisionerJob(provisionerJob))) +} + func (api *api) templateVersionResources(rw http.ResponseWriter, r *http.Request) { templateVersion := httpmw.TemplateVersionParam(r) job, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID) diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index d7e00cdd43115..6512e482a1527 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -7,9 +7,11 @@ import ( "time" "github.com/google/uuid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "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" @@ -27,6 +29,65 @@ func TestTemplateVersion(t *testing.T) { }) } +func TestPostTemplateVersionsByOrganization(t *testing.T) { + t.Parallel() + t.Run("InvalidTemplate", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + templateID := uuid.New() + _, err := client.CreateTemplateVersion(context.Background(), user.OrganizationID, codersdk.CreateTemplateVersionRequest{ + TemplateID: templateID, + StorageMethod: database.ProvisionerStorageMethodFile, + StorageSource: "hash", + Provisioner: database.ProvisionerTypeEcho, + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + + t.Run("FileNotFound", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + _, err := client.CreateTemplateVersion(context.Background(), user.OrganizationID, codersdk.CreateTemplateVersionRequest{ + StorageMethod: database.ProvisionerStorageMethodFile, + StorageSource: "hash", + Provisioner: database.ProvisionerTypeEcho, + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + + t.Run("WithParameters", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + data, err := echo.Tar(&echo.Responses{ + Parse: echo.ParseComplete, + Provision: echo.ProvisionComplete, + ProvisionDryRun: echo.ProvisionComplete, + }) + require.NoError(t, err) + file, err := client.Upload(context.Background(), codersdk.ContentTypeTar, data) + require.NoError(t, err) + _, err = client.CreateTemplateVersion(context.Background(), user.OrganizationID, codersdk.CreateTemplateVersionRequest{ + StorageMethod: database.ProvisionerStorageMethodFile, + StorageSource: file.Hash, + Provisioner: database.ProvisionerTypeEcho, + ParameterValues: []codersdk.CreateParameterRequest{{ + Name: "example", + SourceValue: "value", + SourceScheme: database.ParameterSourceSchemeData, + DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable, + }}, + }) + require.NoError(t, err) + }) +} + func TestPatchCancelTemplateVersion(t *testing.T) { t.Parallel() t.Run("AlreadyCompleted", func(t *testing.T) { @@ -280,3 +341,181 @@ func TestTemplateVersionLogs(t *testing.T) { } } } + +func TestTemplateVersionsByTemplate(t *testing.T) { + t.Parallel() + t.Run("Get", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + versions, err := client.TemplateVersionsByTemplate(context.Background(), codersdk.TemplateVersionsByTemplateRequest{ + TemplateID: template.ID, + }) + require.NoError(t, err) + require.Len(t, versions, 1) + }) +} + +func TestTemplateVersionByName(t *testing.T) { + t.Parallel() + t.Run("NotFound", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + _, err := client.TemplateVersionByName(context.Background(), template.ID, "nothing") + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + + t.Run("Found", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + _, err := client.TemplateVersionByName(context.Background(), template.ID, version.Name) + require.NoError(t, err) + }) +} + +func TestPatchActiveTemplateVersion(t *testing.T) { + t.Parallel() + t.Run("NotFound", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + err := client.UpdateActiveTemplateVersion(context.Background(), template.ID, codersdk.UpdateActiveTemplateVersion{ + ID: uuid.New(), + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + + t.Run("DoesNotBelong", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + err := client.UpdateActiveTemplateVersion(context.Background(), template.ID, codersdk.UpdateActiveTemplateVersion{ + ID: version.ID, + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) + }) + + t.Run("Found", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + err := client.UpdateActiveTemplateVersion(context.Background(), template.ID, codersdk.UpdateActiveTemplateVersion{ + ID: version.ID, + }) + require.NoError(t, err) + }) +} + +// TestPaginatedTemplateVersions creates a list of template versions and paginate. +func TestPaginatedTemplateVersions(t *testing.T) { + t.Parallel() + ctx := context.Background() + + client := coderdtest.New(t, &coderdtest.Options{APIRateLimit: -1}) + // Prepare database. + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(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) + + // Populate database with template versions. + total := 9 + for i := 0; i < total; i++ { + data, err := echo.Tar(nil) + require.NoError(t, err) + file, err := client.Upload(context.Background(), codersdk.ContentTypeTar, data) + require.NoError(t, err) + templateVersion, err := client.CreateTemplateVersion(ctx, user.OrganizationID, codersdk.CreateTemplateVersionRequest{ + TemplateID: template.ID, + StorageSource: file.Hash, + StorageMethod: database.ProvisionerStorageMethodFile, + Provisioner: database.ProvisionerTypeEcho, + }) + require.NoError(t, err) + + _ = coderdtest.AwaitTemplateVersionJob(t, client, templateVersion.ID) + } + + templateVersions, err := client.TemplateVersionsByTemplate(ctx, + codersdk.TemplateVersionsByTemplateRequest{ + TemplateID: template.ID, + }, + ) + require.NoError(t, err) + require.Len(t, templateVersions, 10, "wrong number of template versions created") + + type args struct { + ctx context.Context + pagination codersdk.Pagination + } + tests := []struct { + name string + args args + want []codersdk.TemplateVersion + }{ + { + name: "Single result", + args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 1}}, + want: templateVersions[:1], + }, + { + name: "Single result, second page", + args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 1, Offset: 1}}, + want: templateVersions[1:2], + }, + { + name: "Last two results", + args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 2, Offset: 8}}, + want: templateVersions[8:10], + }, + { + name: "AfterID returns next two results", + args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 2, AfterID: templateVersions[1].ID}}, + want: templateVersions[2:4], + }, + { + name: "No result after last AfterID", + args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 2, AfterID: templateVersions[9].ID}}, + want: []codersdk.TemplateVersion{}, + }, + { + name: "No result after last Offset", + args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 2, Offset: 10}}, + want: []codersdk.TemplateVersion{}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := client.TemplateVersionsByTemplate(tt.args.ctx, codersdk.TemplateVersionsByTemplateRequest{ + TemplateID: template.ID, + Pagination: tt.args.pagination, + }) + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/coderd/users.go b/coderd/users.go index f6dc26ccb51fe..3900a4d4e7340 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -488,7 +488,7 @@ func (api *api) organizationByUserAndName(rw http.ResponseWriter, r *http.Reques }) if errors.Is(err, sql.ErrNoRows) { httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: "you are not a member of that organization", + Message: fmt.Sprintf("no organization found by name %q", organizationName), }) return } @@ -776,73 +776,6 @@ func (api *api) createUser(ctx context.Context, req codersdk.CreateUserRequest) }) } -func (api *api) workspacesByUser(rw http.ResponseWriter, r *http.Request) { - user := httpmw.UserParam(r) - roles := httpmw.UserRoles(r) - - organizations, err := api.Database.GetOrganizationsByUserID(r.Context(), user.ID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get organizations: %s", err), - }) - return - } - organizationIDs := make([]uuid.UUID, 0) - for _, organization := range organizations { - err = api.Authorizer.AuthorizeByRoleName(r.Context(), user.ID.String(), roles.Roles, rbac.ActionRead, rbac.ResourceWorkspace.All().InOrg(organization.ID)) - var apiErr *rbac.UnauthorizedError - if xerrors.As(err, &apiErr) { - continue - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("authorize: %s", err), - }) - return - } - organizationIDs = append(organizationIDs, organization.ID) - } - - workspaceIDs := map[uuid.UUID]struct{}{} - allWorkspaces, err := api.Database.GetWorkspacesByOrganizationIDs(r.Context(), database.GetWorkspacesByOrganizationIDsParams{ - Ids: organizationIDs, - }) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspaces for organizations: %s", err), - }) - return - } - for _, ws := range allWorkspaces { - workspaceIDs[ws.ID] = struct{}{} - } - userWorkspaces, err := api.Database.GetWorkspacesByOwnerID(r.Context(), database.GetWorkspacesByOwnerIDParams{ - OwnerID: user.ID, - }) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspaces for user: %s", err), - }) - return - } - for _, ws := range userWorkspaces { - _, exists := workspaceIDs[ws.ID] - if exists { - continue - } - allWorkspaces = append(allWorkspaces, ws) - } - - apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, allWorkspaces) - 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 convertUser(user database.User, organizationIDs []uuid.UUID) codersdk.User { convertedUser := codersdk.User{ ID: user.ID, diff --git a/coderd/users_test.go b/coderd/users_test.go index 99d1849ca6cf8..488afe697a7eb 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -562,81 +562,6 @@ func TestGetUsers(t *testing.T) { }) } -func TestOrganizationsByUser(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateFirstUser(t, client) - orgs, err := client.OrganizationsByUser(context.Background(), codersdk.Me) - require.NoError(t, err) - require.NotNil(t, orgs) - require.Len(t, orgs, 1) -} - -func TestOrganizationByUserAndName(t *testing.T) { - t.Parallel() - t.Run("NoExist", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - coderdtest.CreateFirstUser(t, client) - _, err := client.OrganizationByName(context.Background(), codersdk.Me, "nothing") - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) - }) - - t.Run("NoMember", 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) - _, err = client.OrganizationByName(context.Background(), codersdk.Me, org.Name) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) - }) - - t.Run("Valid", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - org, err := client.Organization(context.Background(), user.OrganizationID) - require.NoError(t, err) - _, err = client.OrganizationByName(context.Background(), codersdk.Me, org.Name) - require.NoError(t, err) - }) -} - -func TestPostOrganizationsByUser(t *testing.T) { - t.Parallel() - t.Run("Conflict", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - org, err := client.Organization(context.Background(), user.OrganizationID) - require.NoError(t, err) - _, err = client.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{ - Name: org.Name, - }) - 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.CreateFirstUser(t, client) - _, err := client.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{ - Name: "new", - }) - require.NoError(t, err) - }) -} - func TestPostAPIKey(t *testing.T) { t.Parallel() t.Run("InvalidUser", func(t *testing.T) { diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 72690144b2d13..e9e908bc660eb 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -2,9 +2,16 @@ package coderd import ( "database/sql" + "encoding/json" + "errors" "fmt" "net/http" + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/moby/moby/pkg/namesgenerator" + "golang.org/x/xerrors" + "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/httpmw" @@ -24,6 +31,251 @@ func (api *api) workspaceBuild(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(job))) } +func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { + workspace := httpmw.WorkspaceParam(r) + + builds, err := api.Database.GetWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) + if xerrors.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 + } + jobIDs := make([]uuid.UUID, 0, len(builds)) + for _, version := range builds { + jobIDs = append(jobIDs, version.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 jobs: %s", err), + }) + return + } + jobByID := map[string]database.ProvisionerJob{} + for _, job := range jobs { + jobByID[job.ID.String()] = job + } + + apiBuilds := make([]codersdk.WorkspaceBuild, 0) + for _, build := range builds { + job, exists := jobByID[build.JobID.String()] + if !exists { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("job %q doesn't exist for build %q", build.JobID, build.ID), + }) + return + } + apiBuilds = append(apiBuilds, convertWorkspaceBuild(build, convertProvisionerJob(job))) + } + + httpapi.Write(rw, http.StatusOK, apiBuilds) +} + +func (api *api) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) { + workspace := httpmw.WorkspaceParam(r) + workspaceBuildName := chi.URLParam(r, "workspacebuildname") + workspaceBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDAndName(r.Context(), database.GetWorkspaceBuildByWorkspaceIDAndNameParams{ + WorkspaceID: workspace.ID, + Name: workspaceBuildName, + }) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: fmt.Sprintf("no workspace build found by name %q", workspaceBuildName), + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace build by name: %s", err), + }) + return + } + job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job: %s", err), + }) + return + } + + httpapi.Write(rw, http.StatusOK, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(job))) +} + +func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { + apiKey := httpmw.APIKey(r) + workspace := httpmw.WorkspaceParam(r) + var createBuild codersdk.CreateWorkspaceBuildRequest + if !httpapi.Read(rw, r, &createBuild) { + return + } + if createBuild.TemplateVersionID == uuid.Nil { + latestBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get latest workspace build: %s", err), + }) + return + } + createBuild.TemplateVersionID = latestBuild.TemplateVersionID + } + templateVersion, err := api.Database.GetTemplateVersionByID(r.Context(), createBuild.TemplateVersionID) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: "template version not found", + Errors: []httpapi.Error{{ + Field: "template_version_id", + Detail: "template version not found", + }}, + }) + return + } + 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 provisioner 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: %q. You cannot build workspaces with it!", templateVersion.Name, templateVersionJob.Error.String), + }) + return + case codersdk.ProvisionerJobCanceled: + httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ + Message: "The provided template version was canceled during import. You cannot builds workspaces with it!", + }) + return + } + + template, err := api.Database.GetTemplateByID(r.Context(), templateVersion.TemplateID.UUID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get template: %s", err), + }) + return + } + + // Store prior history ID if it exists to update it after we create new! + priorHistoryID := uuid.NullUUID{} + priorHistory, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID) + if err == nil { + priorJob, err := api.Database.GetProvisionerJobByID(r.Context(), priorHistory.JobID) + if err == nil && convertProvisionerJob(priorJob).Status.Active() { + httpapi.Write(rw, http.StatusConflict, httpapi.Response{ + Message: "a workspace build is already active", + }) + return + } + + priorHistoryID = uuid.NullUUID{ + UUID: priorHistory.ID, + Valid: true, + } + } else if !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get prior workspace build: %s", err), + }) + return + } + + var workspaceBuild database.WorkspaceBuild + var provisionerJob database.ProvisionerJob + // This must happen in a transaction to ensure history can be inserted, and + // the prior history can update it's "after" column to point at the new. + err = api.Database.InTx(func(db database.Store) error { + workspaceBuildID := uuid.New() + 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) + } + state := createBuild.ProvisionerState + if len(state) == 0 { + state = priorHistory.ProvisionerState + } + + workspaceBuild, err = db.InsertWorkspaceBuild(r.Context(), database.InsertWorkspaceBuildParams{ + ID: workspaceBuildID, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + WorkspaceID: workspace.ID, + TemplateVersionID: templateVersion.ID, + BeforeID: priorHistoryID, + Name: namesgenerator.GetRandomName(1), + ProvisionerState: state, + InitiatorID: apiKey.UserID, + Transition: createBuild.Transition, + JobID: provisionerJob.ID, + }) + if err != nil { + return xerrors.Errorf("insert workspace build: %w", err) + } + + if priorHistoryID.Valid { + // Update the prior history entries "after" column. + err = db.UpdateWorkspaceBuildByID(r.Context(), database.UpdateWorkspaceBuildByIDParams{ + ID: priorHistory.ID, + ProvisionerState: priorHistory.ProvisionerState, + UpdatedAt: database.Now(), + AfterID: uuid.NullUUID{ + UUID: workspaceBuild.ID, + Valid: true, + }, + }) + if err != nil { + return xerrors.Errorf("update prior workspace build: %w", err) + } + } + + return nil + }) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: err.Error(), + }) + return + } + + httpapi.Write(rw, http.StatusCreated, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(provisionerJob))) +} + func (api *api) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Request) { workspaceBuild := httpmw.WorkspaceBuildParam(r) job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID) diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index ab94884efc9d6..a9cec2cf3355c 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -27,6 +27,22 @@ func TestWorkspaceBuild(t *testing.T) { require.NoError(t, err) } +func TestWorkspaceBuilds(t *testing.T) { + t.Parallel() + t.Run("Single", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(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.WorkspaceBuilds(context.Background(), workspace.ID) + require.NoError(t, err) + }) +} + func TestPatchCancelWorkspaceBuild(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 704ee4217728f..7176f3d20198a 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -18,6 +18,7 @@ import ( "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/httpmw" + "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/codersdk" ) @@ -61,81 +62,256 @@ func (api *api) workspace(rw http.ResponseWriter, r *http.Request) { convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), template, owner)) } -func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { - workspace := httpmw.WorkspaceParam(r) - - builds, err := api.Database.GetWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) +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 workspace builds: %s", err), + 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) workspacesByUser(rw http.ResponseWriter, r *http.Request) { + user := httpmw.UserParam(r) + roles := httpmw.UserRoles(r) + + organizations, err := api.Database.GetOrganizationsByUserID(r.Context(), user.ID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get organizations: %s", err), }) return } - jobIDs := make([]uuid.UUID, 0, len(builds)) - for _, version := range builds { - jobIDs = append(jobIDs, version.JobID) + organizationIDs := make([]uuid.UUID, 0) + for _, organization := range organizations { + err = api.Authorizer.AuthorizeByRoleName(r.Context(), user.ID.String(), roles.Roles, rbac.ActionRead, rbac.ResourceWorkspace.All().InOrg(organization.ID)) + var apiErr *rbac.UnauthorizedError + if xerrors.As(err, &apiErr) { + continue + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("authorize: %s", err), + }) + return + } + organizationIDs = append(organizationIDs, organization.ID) } - jobs, err := api.Database.GetProvisionerJobsByIDs(r.Context(), jobIDs) + + workspaceIDs := map[uuid.UUID]struct{}{} + allWorkspaces, err := api.Database.GetWorkspacesByOrganizationIDs(r.Context(), database.GetWorkspacesByOrganizationIDsParams{ + Ids: organizationIDs, + }) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspaces for organizations: %s", err), + }) + return + } + for _, ws := range allWorkspaces { + workspaceIDs[ws.ID] = struct{}{} + } + userWorkspaces, err := api.Database.GetWorkspacesByOwnerID(r.Context(), database.GetWorkspacesByOwnerIDParams{ + OwnerID: user.ID, + }) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspaces for user: %s", err), + }) + return + } + for _, ws := range userWorkspaces { + _, exists := workspaceIDs[ws.ID] + if exists { + continue + } + allWorkspaces = append(allWorkspaces, ws) + } + + apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, allWorkspaces) + 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 jobs: %s", err), + Message: fmt.Sprintf("get workspaces: %s", err), }) return } - jobByID := map[string]database.ProvisionerJob{} - for _, job := range jobs { - jobByID[job.ID.String()] = job + 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) +} - apiBuilds := make([]codersdk.WorkspaceBuild, 0) - for _, build := range builds { - job, exists := jobByID[build.JobID.String()] - if !exists { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("job %q doesn't exist for build %q", build.JobID, build.ID), - }) - return - } - apiBuilds = append(apiBuilds, convertWorkspaceBuild(build, convertProvisionerJob(job))) +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, apiBuilds) + httpapi.Write(rw, http.StatusOK, convertWorkspace(workspace, + convertWorkspaceBuild(build, convertProvisionerJob(job)), template, owner)) } -func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { +// 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) - workspace := httpmw.WorkspaceParam(r) - var createBuild codersdk.CreateWorkspaceBuildRequest - if !httpapi.Read(rw, r, &createBuild) { + 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 createBuild.TemplateVersionID == uuid.Nil { - latestBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID) + 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("get latest workspace build: %s", err), + Message: fmt.Sprintf("find template for conflicting workspace name %q: %s", createWorkspace.Name, err), }) return } - createBuild.TemplateVersionID = latestBuild.TemplateVersionID - } - templateVersion, err := api.Database.GetTemplateVersionByID(r.Context(), createBuild.TemplateVersionID) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: "template version not found", + // 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: "template_version_id", - Detail: "template version not found", + 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), @@ -145,7 +321,7 @@ func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { templateVersionJob, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get provisioner job: %s", err), + Message: fmt.Sprintf("get template version job: %s", err), }) return } @@ -158,53 +334,50 @@ func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { return case codersdk.ProvisionerJobFailed: httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ - Message: fmt.Sprintf("The provided template version %q has failed to import: %q. You cannot build workspaces with it!", templateVersion.Name, templateVersionJob.Error.String), + 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 builds workspaces with it!", + Message: "The provided template version was canceled during import. You cannot create workspaces using it!", }) return } - template, err := api.Database.GetTemplateByID(r.Context(), templateVersion.TemplateID.UUID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get template: %s", err), + 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, }) - return - } - - // Store prior history ID if it exists to update it after we create new! - priorHistoryID := uuid.NullUUID{} - priorHistory, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID) - if err == nil { - priorJob, err := api.Database.GetProvisionerJobByID(r.Context(), priorHistory.JobID) - if err == nil && convertProvisionerJob(priorJob).Status.Active() { - httpapi.Write(rw, http.StatusConflict, httpapi.Response{ - Message: "a workspace build is already active", - }) - return + if err != nil { + return xerrors.Errorf("insert workspace: %w", err) } - - priorHistoryID = uuid.NullUUID{ - UUID: priorHistory.ID, - Valid: true, + 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) + } } - } else if !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get prior workspace build: %s", err), - }) - return - } - var workspaceBuild database.WorkspaceBuild - var provisionerJob database.ProvisionerJob - // This must happen in a transaction to ensure history can be inserted, and - // the prior history can update it's "after" column to point at the new. - err = api.Database.InTx(func(db database.Store) error { - workspaceBuildID := uuid.New() input, err := json.Marshal(workspaceProvisionJob{ WorkspaceBuildID: workspaceBuildID, }) @@ -226,84 +399,38 @@ func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { if err != nil { return xerrors.Errorf("insert provisioner job: %w", err) } - state := createBuild.ProvisionerState - if len(state) == 0 { - state = priorHistory.ProvisionerState - } - workspaceBuild, err = db.InsertWorkspaceBuild(r.Context(), database.InsertWorkspaceBuildParams{ ID: workspaceBuildID, CreatedAt: database.Now(), UpdatedAt: database.Now(), WorkspaceID: workspace.ID, TemplateVersionID: templateVersion.ID, - BeforeID: priorHistoryID, Name: namesgenerator.GetRandomName(1), - ProvisionerState: state, InitiatorID: apiKey.UserID, - Transition: createBuild.Transition, + Transition: database.WorkspaceTransitionStart, JobID: provisionerJob.ID, }) if err != nil { return xerrors.Errorf("insert workspace build: %w", err) } - - if priorHistoryID.Valid { - // Update the prior history entries "after" column. - err = db.UpdateWorkspaceBuildByID(r.Context(), database.UpdateWorkspaceBuildByIDParams{ - ID: priorHistory.ID, - ProvisionerState: priorHistory.ProvisionerState, - UpdatedAt: database.Now(), - AfterID: uuid.NullUUID{ - UUID: workspaceBuild.ID, - Valid: true, - }, - }) - if err != nil { - return xerrors.Errorf("update prior workspace build: %w", err) - } - } - return nil }) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: err.Error(), - }) - return - } - - httpapi.Write(rw, http.StatusCreated, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(provisionerJob))) -} - -func (api *api) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) { - workspace := httpmw.WorkspaceParam(r) - workspaceBuildName := chi.URLParam(r, "workspacebuildname") - workspaceBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDAndName(r.Context(), database.GetWorkspaceBuildByWorkspaceIDAndNameParams{ - WorkspaceID: workspace.ID, - Name: workspaceBuildName, - }) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: fmt.Sprintf("no workspace build found by name %q", workspaceBuildName), + Message: fmt.Sprintf("create workspace: %s", err), }) return } + user, err := api.Database.GetUserByID(r.Context(), apiKey.UserID) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspace build by name: %s", err), - }) - return - } - job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get provisioner job: %s", err), + Message: fmt.Sprintf("get user: %s", err), }) return } - httpapi.Write(rw, http.StatusOK, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(job))) + httpapi.Write(rw, http.StatusCreated, convertWorkspace(workspace, + convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(templateVersionJob)), template, user)) } func (api *api) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) { diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index f7f31e0017637..8ba7a7a232424 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -31,18 +31,145 @@ func TestWorkspace(t *testing.T) { require.NoError(t, err) } -func TestWorkspaceBuilds(t *testing.T) { +func TestPostWorkspacesByOrganization(t *testing.T) { t.Parallel() - t.Run("Single", func(t *testing.T) { + 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.WorkspaceBuilds(context.Background(), workspace.ID) + _, err := client.WorkspaceByOwnerAndName(context.Background(), user.OrganizationID, codersdk.Me, workspace.Name) require.NoError(t, err) }) }