mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
chore(coderd): allow creating workspaces without specifying an organization (#14048)
This commit is contained in:
committed by
GitHub
parent
56dfc64bb0
commit
bf4b7abf14
+113
-34
@@ -340,6 +340,7 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request)
|
||||
// @Description specify either the Template ID or the Template Version ID,
|
||||
// @Description not both. If the Template ID is specified, the active version
|
||||
// @Description of the template will be used.
|
||||
// @Deprecated Use /users/{user}/workspaces instead.
|
||||
// @ID create-user-workspace-by-organization
|
||||
// @Security CoderSessionToken
|
||||
// @Accept json
|
||||
@@ -353,9 +354,9 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request)
|
||||
func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
organization = httpmw.OrganizationParam(r)
|
||||
apiKey = httpmw.APIKey(r)
|
||||
auditor = api.Auditor.Load()
|
||||
organization = httpmw.OrganizationParam(r)
|
||||
member = httpmw.OrganizationMemberParam(r)
|
||||
workspaceResourceInfo = audit.AdditionalFields{
|
||||
WorkspaceOwner: member.Username,
|
||||
@@ -380,16 +381,90 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
var createWorkspace codersdk.CreateWorkspaceRequest
|
||||
if !httpapi.Read(ctx, rw, r, &createWorkspace) {
|
||||
var req codersdk.CreateWorkspaceRequest
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
owner := workspaceOwner{
|
||||
ID: member.UserID,
|
||||
Username: member.Username,
|
||||
AvatarURL: member.AvatarURL,
|
||||
}
|
||||
|
||||
createWorkspace(ctx, aReq, apiKey.UserID, api, owner, req, rw, r)
|
||||
}
|
||||
|
||||
// Create a new workspace for the currently authenticated user.
|
||||
//
|
||||
// @Summary Create user workspace
|
||||
// @Description Create a new workspace using a template. The request must
|
||||
// @Description specify either the Template ID or the Template Version ID,
|
||||
// @Description not both. If the Template ID is specified, the active version
|
||||
// @Description of the template will be used.
|
||||
// @ID create-user-workspace
|
||||
// @Security CoderSessionToken
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Tags Workspaces
|
||||
// @Param user path string true "Username, UUID, or me"
|
||||
// @Param request body codersdk.CreateWorkspaceRequest true "Create workspace request"
|
||||
// @Success 200 {object} codersdk.Workspace
|
||||
// @Router /users/{user}/workspaces [post]
|
||||
func (api *API) postUserWorkspaces(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
apiKey = httpmw.APIKey(r)
|
||||
auditor = api.Auditor.Load()
|
||||
user = httpmw.UserParam(r)
|
||||
)
|
||||
|
||||
aReq, commitAudit := audit.InitRequest[database.Workspace](rw, &audit.RequestParams{
|
||||
Audit: *auditor,
|
||||
Log: api.Logger,
|
||||
Request: r,
|
||||
Action: database.AuditActionCreate,
|
||||
AdditionalFields: audit.AdditionalFields{
|
||||
WorkspaceOwner: user.Username,
|
||||
},
|
||||
})
|
||||
|
||||
defer commitAudit()
|
||||
|
||||
var req codersdk.CreateWorkspaceRequest
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
owner := workspaceOwner{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
AvatarURL: user.AvatarURL,
|
||||
}
|
||||
createWorkspace(ctx, aReq, apiKey.UserID, api, owner, req, rw, r)
|
||||
}
|
||||
|
||||
type workspaceOwner struct {
|
||||
ID uuid.UUID
|
||||
Username string
|
||||
AvatarURL string
|
||||
}
|
||||
|
||||
func createWorkspace(
|
||||
ctx context.Context,
|
||||
auditReq *audit.Request[database.Workspace],
|
||||
initiatorID uuid.UUID,
|
||||
api *API,
|
||||
owner workspaceOwner,
|
||||
req codersdk.CreateWorkspaceRequest,
|
||||
rw http.ResponseWriter,
|
||||
r *http.Request,
|
||||
) {
|
||||
// If we were given a `TemplateVersionID`, we need to determine the `TemplateID` from it.
|
||||
templateID := createWorkspace.TemplateID
|
||||
templateID := req.TemplateID
|
||||
if templateID == uuid.Nil {
|
||||
templateVersion, err := api.Database.GetTemplateVersionByID(ctx, createWorkspace.TemplateVersionID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
templateVersion, err := api.Database.GetTemplateVersionByID(ctx, req.TemplateVersionID)
|
||||
if httpapi.Is404Error(err) {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("Template version %q doesn't exist.", templateID.String()),
|
||||
Validations: []codersdk.ValidationError{{
|
||||
@@ -423,7 +498,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
template, err := api.Database.GetTemplateByID(ctx, templateID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
if httpapi.Is404Error(err) {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("Template %q doesn't exist.", templateID.String()),
|
||||
Validations: []codersdk.ValidationError{{
|
||||
@@ -447,6 +522,17 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
// Update audit log's organization
|
||||
auditReq.UpdateOrganizationID(template.OrganizationID)
|
||||
|
||||
// Do this upfront to save work. If this fails, the rest of the work
|
||||
// would be wasted.
|
||||
if !api.Authorize(r, policy.ActionCreate,
|
||||
rbac.ResourceWorkspace.InOrg(template.OrganizationID).WithOwner(owner.ID.String())) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
||||
templateAccessControl := (*(api.AccessControlStore.Load())).GetTemplateAccessControl(template)
|
||||
if templateAccessControl.IsDeprecated() {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
@@ -458,14 +544,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
if organization.ID != template.OrganizationID {
|
||||
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
|
||||
Message: fmt.Sprintf("Template is not in organization %q.", organization.Name),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
dbAutostartSchedule, err := validWorkspaceSchedule(createWorkspace.AutostartSchedule)
|
||||
dbAutostartSchedule, err := validWorkspaceSchedule(req.AutostartSchedule)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid Autostart Schedule.",
|
||||
@@ -483,7 +562,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
dbTTL, err := validWorkspaceTTLMillis(createWorkspace.TTLMillis, templateSchedule.DefaultTTL)
|
||||
dbTTL, err := validWorkspaceTTLMillis(req.TTLMillis, templateSchedule.DefaultTTL)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid Workspace Time to Shutdown.",
|
||||
@@ -494,8 +573,8 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
||||
|
||||
// back-compatibility: default to "never" if not included.
|
||||
dbAU := database.AutomaticUpdatesNever
|
||||
if createWorkspace.AutomaticUpdates != "" {
|
||||
dbAU, err = validWorkspaceAutomaticUpdates(createWorkspace.AutomaticUpdates)
|
||||
if req.AutomaticUpdates != "" {
|
||||
dbAU, err = validWorkspaceAutomaticUpdates(req.AutomaticUpdates)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid Workspace Automatic Updates setting.",
|
||||
@@ -509,13 +588,13 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
||||
// read other workspaces. Ideally we check the error on create and look for
|
||||
// a postgres conflict error.
|
||||
workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(ctx, database.GetWorkspaceByOwnerIDAndNameParams{
|
||||
OwnerID: member.UserID,
|
||||
Name: createWorkspace.Name,
|
||||
OwnerID: owner.ID,
|
||||
Name: req.Name,
|
||||
})
|
||||
if err == nil {
|
||||
// If the workspace already exists, don't allow creation.
|
||||
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
|
||||
Message: fmt.Sprintf("Workspace %q already exists.", createWorkspace.Name),
|
||||
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.",
|
||||
@@ -525,7 +604,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
||||
}
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: fmt.Sprintf("Internal error fetching workspace by name %q.", createWorkspace.Name),
|
||||
Message: fmt.Sprintf("Internal error fetching workspace by name %q.", req.Name),
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
@@ -542,10 +621,10 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
||||
ID: uuid.New(),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
OwnerID: member.UserID,
|
||||
OwnerID: owner.ID,
|
||||
OrganizationID: template.OrganizationID,
|
||||
TemplateID: template.ID,
|
||||
Name: createWorkspace.Name,
|
||||
Name: req.Name,
|
||||
AutostartSchedule: dbAutostartSchedule,
|
||||
Ttl: dbTTL,
|
||||
// The workspaces page will sort by last used at, and it's useful to
|
||||
@@ -559,11 +638,11 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
||||
|
||||
builder := wsbuilder.New(workspace, database.WorkspaceTransitionStart).
|
||||
Reason(database.BuildReasonInitiator).
|
||||
Initiator(apiKey.UserID).
|
||||
Initiator(initiatorID).
|
||||
ActiveVersion().
|
||||
RichParameterValues(createWorkspace.RichParameterValues)
|
||||
if createWorkspace.TemplateVersionID != uuid.Nil {
|
||||
builder = builder.VersionID(createWorkspace.TemplateVersionID)
|
||||
RichParameterValues(req.RichParameterValues)
|
||||
if req.TemplateVersionID != uuid.Nil {
|
||||
builder = builder.VersionID(req.TemplateVersionID)
|
||||
}
|
||||
|
||||
workspaceBuild, provisionerJob, err = builder.Build(
|
||||
@@ -596,7 +675,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
||||
// Client probably doesn't care about this error, so just log it.
|
||||
api.Logger.Error(ctx, "failed to post provisioner job to pubsub", slog.Error(err))
|
||||
}
|
||||
aReq.New = workspace
|
||||
auditReq.New = workspace
|
||||
|
||||
api.Telemetry.Report(&telemetry.Snapshot{
|
||||
Workspaces: []telemetry.Workspace{telemetry.ConvertWorkspace(workspace)},
|
||||
@@ -610,8 +689,8 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
||||
ProvisionerJob: *provisionerJob,
|
||||
QueuePosition: 0,
|
||||
},
|
||||
member.Username,
|
||||
member.AvatarURL,
|
||||
owner.Username,
|
||||
owner.AvatarURL,
|
||||
[]database.WorkspaceResource{},
|
||||
[]database.WorkspaceResourceMetadatum{},
|
||||
[]database.WorkspaceAgent{},
|
||||
@@ -629,12 +708,12 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
w, err := convertWorkspace(
|
||||
apiKey.UserID,
|
||||
initiatorID,
|
||||
workspace,
|
||||
apiBuild,
|
||||
template,
|
||||
member.Username,
|
||||
member.AvatarURL,
|
||||
owner.Username,
|
||||
owner.AvatarURL,
|
||||
api.Options.AllowWorkspaceRenames,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user