Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

refactor: untangle workspace creation from http logic#19639

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
DanielleMaywood merged 3 commits intomainfromdanielle/refactor/create-workspace
Aug 29, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletionscoderd/aitasks.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,7 @@ import (
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpapi/httperror"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
Expand DownExpand Up@@ -154,8 +155,9 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) {
// This can be optimized. It exists as it is now for code simplicity.
// The most common case is to create a workspace for 'Me'. Which does
// not enter this code branch.
template, ok := requestTemplate(ctx, rw, createReq, api.Database)
if !ok {
template, err := requestTemplate(ctx, createReq, api.Database)
if err != nil {
httperror.WriteResponseError(ctx, rw, err)
return
}

Expand DownExpand Up@@ -188,7 +190,13 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) {
})

defer commitAudit()
createWorkspace(ctx, aReq, apiKey.UserID, api, owner, createReq, rw, r)
w, err := createWorkspace(ctx, aReq, apiKey.UserID, api, owner, createReq, r)
if err != nil {
httperror.WriteResponseError(ctx, rw, err)
return
}

httpapi.Write(ctx, rw, http.StatusCreated, w)
}

// tasksFromWorkspaces converts a slice of API workspaces into tasks, fetching
Expand Down
49 changes: 49 additions & 0 deletionscoderd/httpapi/httperror/responserror.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
package httperror

import (
"context"
"errors"
"fmt"
"net/http"

"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/codersdk"
)

Expand All@@ -17,3 +21,48 @@ func IsResponder(err error) (Responder, bool) {
}
return nil, false
}

func NewResponseError(status int, resp codersdk.Response) error {
return &responseError{
status: status,
response: resp,
}
}

func WriteResponseError(ctx context.Context, rw http.ResponseWriter, err error) {
if responseErr, ok := IsResponder(err); ok {
code, resp := responseErr.Response()

httpapi.Write(ctx, rw, code, resp)
return
}

httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal server error",
Detail: err.Error(),
})
}

type responseError struct {
status int
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

How about embedding or extendingcodersdk.Error instead? It already has these fields.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I'm unsure about that for now,codersdk.Error also contains a few extra fields such asmethodurl andHelper which I'm not sure make such sense given the context

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Fair enough 👍

response codersdk.Response
}

var (
_ error = (*responseError)(nil)
_ Responder = (*responseError)(nil)
)

func (e *responseError) Error() string {
return fmt.Sprintf("%s: %s", e.response.Message, e.response.Detail)
}

func (e *responseError) Status() int {
return e.status
}

func (e *responseError) Response() (int, codersdk.Response) {
return e.status, e.response
}

var ErrResourceNotFound = NewResponseError(http.StatusNotFound, httpapi.ResourceNotFoundResponse)
100 changes: 46 additions & 54 deletionscoderd/workspaces.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -388,7 +388,13 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
AvatarURL: member.AvatarURL,
}

createWorkspace(ctx, aReq, apiKey.UserID, api, owner, req, rw, r)
w, err := createWorkspace(ctx, aReq, apiKey.UserID, api, owner, req, r)
if err != nil {
httperror.WriteResponseError(ctx, rw, err)
return
}

httpapi.Write(ctx, rw, http.StatusCreated, w)
}

// Create a new workspace for the currently authenticated user.
Expand DownExpand Up@@ -442,8 +448,9 @@ func (api *API) postUserWorkspaces(rw http.ResponseWriter, r *http.Request) {
// This can be optimized. It exists as it is now for code simplicity.
// The most common case is to create a workspace for 'Me'. Which does
// not enter this code branch.
template, ok := requestTemplate(ctx, rw, req, api.Database)
if !ok {
template, err := requestTemplate(ctx, req, api.Database)
if err != nil {
httperror.WriteResponseError(ctx, rw, err)
return
}

Expand DownExpand Up@@ -476,7 +483,14 @@ func (api *API) postUserWorkspaces(rw http.ResponseWriter, r *http.Request) {
})

defer commitAudit()
createWorkspace(ctx, aReq, apiKey.UserID, api, owner, req, rw, r)

w, err := createWorkspace(ctx, aReq, apiKey.UserID, api, owner, req, r)
if err != nil {
httperror.WriteResponseError(ctx, rw, err)
return
}

httpapi.Write(ctx, rw, http.StatusCreated, w)
}

type workspaceOwner struct {
Expand All@@ -492,12 +506,11 @@ func createWorkspace(
api *API,
owner workspaceOwner,
req codersdk.CreateWorkspaceRequest,
rw http.ResponseWriter,
r *http.Request,
) {
template,ok := requestTemplate(ctx, rw, req, api.Database)
if!ok {
return
)(codersdk.Workspace, error){
template,err := requestTemplate(ctx, req, api.Database)
iferr != nil {
return codersdk.Workspace{}, err
}

// This is a premature auth check to avoid doing unnecessary work if the user
Expand All@@ -506,14 +519,12 @@ func createWorkspace(
rbac.ResourceWorkspace.InOrg(template.OrganizationID).WithOwner(owner.ID.String())) {
// If this check fails, return a proper unauthorized error to the user to indicate
// what is going on.
httpapi.Write(ctx, rw,http.StatusForbidden, codersdk.Response{
return codersdk.Workspace{}, httperror.NewResponseError(http.StatusForbidden, codersdk.Response{
Message: "Unauthorized to create workspace.",
Detail: "You are unable to create a workspace in this organization. " +
"It is possible to have access to the template, but not be able to create a workspace. " +
"Please contact an administrator about your permissions if you feel this is an error.",
Validations: nil,
})
return
}

// Update audit log's organization
Expand All@@ -523,49 +534,42 @@ func createWorkspace(
// would be wasted.
if !api.Authorize(r, policy.ActionCreate,
rbac.ResourceWorkspace.InOrg(template.OrganizationID).WithOwner(owner.ID.String())) {
httpapi.ResourceNotFound(rw)
return
return codersdk.Workspace{}, httperror.ErrResourceNotFound
}
// The user also needs permission to use the template. At this point they have
// read perms, but not necessarily "use". This is also checked in `db.InsertWorkspace`.
// Doing this up front can save some work below if the user doesn't have permission.
if !api.Authorize(r, policy.ActionUse, template) {
httpapi.Write(ctx, rw,http.StatusForbidden, codersdk.Response{
return codersdk.Workspace{}, httperror.NewResponseError(http.StatusForbidden, codersdk.Response{
Message: fmt.Sprintf("Unauthorized access to use the template %q.", template.Name),
Detail: "Although you are able to view the template, you are unable to create a workspace using it. " +
"Please contact an administrator about your permissions if you feel this is an error.",
Validations: nil,
})
return
}

templateAccessControl := (*(api.AccessControlStore.Load())).GetTemplateAccessControl(template)
if templateAccessControl.IsDeprecated() {
httpapi.Write(ctx, rw,http.StatusBadRequest, codersdk.Response{
return codersdk.Workspace{}, httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("Template %q has been deprecated, and cannot be used to create a new workspace.", template.Name),
// Pass the deprecated message to the user.
Detail: templateAccessControl.Deprecated,
Validations: nil,
Detail: templateAccessControl.Deprecated,
})
return
}

dbAutostartSchedule, err := validWorkspaceSchedule(req.AutostartSchedule)
if err != nil {
httpapi.Write(ctx, rw,http.StatusBadRequest, codersdk.Response{
return codersdk.Workspace{}, httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
Message: "Invalid Autostart Schedule.",
Validations: []codersdk.ValidationError{{Field: "schedule", Detail: err.Error()}},
})
return
}

templateSchedule, err := (*api.TemplateScheduleStore.Load()).Get(ctx, api.Database, template.ID)
if err != nil {
httpapi.Write(ctx, rw,http.StatusInternalServerError, codersdk.Response{
return codersdk.Workspace{}, httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching template schedule.",
Detail: err.Error(),
})
return
}

nextStartAt := sql.NullTime{}
Expand All@@ -578,23 +582,21 @@ func createWorkspace(

dbTTL, err := validWorkspaceTTLMillis(req.TTLMillis, templateSchedule.DefaultTTL)
if err != nil {
httpapi.Write(ctx, rw,http.StatusBadRequest, codersdk.Response{
return codersdk.Workspace{}, httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
Message: "Invalid Workspace Time to Shutdown.",
Validations: []codersdk.ValidationError{{Field: "ttl_ms", Detail: err.Error()}},
})
return
}

// back-compatibility: default to "never" if not included.
dbAU := database.AutomaticUpdatesNever
if req.AutomaticUpdates != "" {
dbAU, err = validWorkspaceAutomaticUpdates(req.AutomaticUpdates)
if err != nil {
httpapi.Write(ctx, rw,http.StatusBadRequest, codersdk.Response{
return codersdk.Workspace{}, httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
Message: "Invalid Workspace Automatic Updates setting.",
Validations: []codersdk.ValidationError{{Field: "automatic_updates", Detail: err.Error()}},
})
return
}
}

Expand All@@ -607,20 +609,18 @@ func createWorkspace(
})
if err == nil {
// If the workspace already exists, don't allow creation.
httpapi.Write(ctx, rw,http.StatusConflict, codersdk.Response{
return codersdk.Workspace{}, httperror.NewResponseError(http.StatusConflict, codersdk.Response{
Message: fmt.Sprintf("Workspace %q already exists.", req.Name),
Validations: []codersdk.ValidationError{{
Field: "name",
Detail: "This value is already in use and should be unique.",
}},
})
return
} else if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(ctx, rw,http.StatusInternalServerError, codersdk.Response{
return codersdk.Workspace{}, httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
Message: fmt.Sprintf("Internal error fetching workspace by name %q.", req.Name),
Detail: err.Error(),
})
return
}

var (
Expand DownExpand Up@@ -759,8 +759,7 @@ func createWorkspace(
return err
}, nil)
if err != nil {
httperror.WriteWorkspaceBuildError(ctx, rw, err)
return
return codersdk.Workspace{}, err
}

err = provisionerjobs.PostJob(api.Pubsub, *provisionerJob)
Expand DownExpand Up@@ -809,11 +808,10 @@ func createWorkspace(
provisionerDaemons,
)
if err != nil {
httpapi.Write(ctx, rw,http.StatusInternalServerError, codersdk.Response{
return codersdk.Workspace{}, httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
Message: "Internal error converting workspace build.",
Detail: err.Error(),
})
return
}

w, err := convertWorkspace(
Expand All@@ -825,40 +823,38 @@ func createWorkspace(
codersdk.WorkspaceAppStatus{},
)
if err != nil {
httpapi.Write(ctx, rw,http.StatusInternalServerError, codersdk.Response{
return codersdk.Workspace{}, httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
Message: "Internal error converting workspace.",
Detail: err.Error(),
})
return
}
httpapi.Write(ctx, rw, http.StatusCreated, w)

return w, nil
}

func requestTemplate(ctx context.Context,rw http.ResponseWriter,req codersdk.CreateWorkspaceRequest, db database.Store) (database.Template,bool) {
func requestTemplate(ctx context.Context, req codersdk.CreateWorkspaceRequest, db database.Store) (database.Template,error) {
// If we were given a `TemplateVersionID`, we need to determine the `TemplateID` from it.
templateID := req.TemplateID

if templateID == uuid.Nil {
templateVersion, err := db.GetTemplateVersionByID(ctx, req.TemplateVersionID)
if httpapi.Is404Error(err) {
httpapi.Write(ctx, rw,http.StatusBadRequest, codersdk.Response{
return database.Template{}, httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("Template version %q doesn't exist.", req.TemplateVersionID),
Validations: []codersdk.ValidationError{{
Field: "template_version_id",
Detail: "template not found",
}},
})
return database.Template{}, false
}
if err != nil {
httpapi.Write(ctx, rw,http.StatusInternalServerError, codersdk.Response{
return database.Template{}, httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching template version.",
Detail: err.Error(),
})
return database.Template{}, false
}
if templateVersion.Archived {
httpapi.Write(ctx, rw,http.StatusInternalServerError, codersdk.Response{
return database.Template{}, httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
Message: "Archived template versions cannot be used to make a workspace.",
Validations: []codersdk.ValidationError{
{
Expand All@@ -867,37 +863,33 @@ func requestTemplate(ctx context.Context, rw http.ResponseWriter, req codersdk.C
},
},
})
return database.Template{}, false
}

templateID = templateVersion.TemplateID.UUID
}

template, err := db.GetTemplateByID(ctx, templateID)
if httpapi.Is404Error(err) {
httpapi.Write(ctx, rw,http.StatusBadRequest, codersdk.Response{
return database.Template{}, httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("Template %q doesn't exist.", templateID),
Validations: []codersdk.ValidationError{{
Field: "template_id",
Detail: "template not found",
}},
})
return database.Template{}, false
}
if err != nil {
httpapi.Write(ctx, rw,http.StatusInternalServerError, codersdk.Response{
return database.Template{}, httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching template.",
Detail: err.Error(),
})
return database.Template{}, false
}
if template.Deleted {
httpapi.Write(ctx, rw,http.StatusNotFound, codersdk.Response{
return database.Template{}, httperror.NewResponseError(http.StatusNotFound, codersdk.Response{
Message: fmt.Sprintf("Template %q has been deleted!", template.Name),
})
return database.Template{}, false
}
return template,true
return template,nil
}

func claimPrebuild(
Expand Down

[8]ページ先頭

©2009-2025 Movatter.jp