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

Commitb0c430b

Browse files
committed
feat: add bulk prebuild invalidation API endpoint
Implements API endpoint to invalidate (bulk delete) all prebuilt workspacesfor a template's active version.Implementation:- POST /api/v2/templates/{template}/prebuilds/invalidate endpoint- Reuses existing postWorkspaceBuildsInternal() for deletion- Filters by PrebuildsSystemUserID and active template version- Authorization: requires template update permissionLogic:- Uses GetWorkspacesByTemplateID to fetch all workspaces- Filters workspaces by owner_id (PrebuildsSystemUserID)- Gets latest build per workspace to verify active version- Calls existing delete workspace logic in loopTesting:- OK: Verifies old version prebuilds are kept, active version prebuilds deleted, user workspaces untouched- Unauthorized: Regular user gets 403Updates#17917
1 parentf6e86c6 commitb0c430b

File tree

4 files changed

+263
-0
lines changed

4 files changed

+263
-0
lines changed

‎coderd/coderd.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1217,6 +1217,7 @@ func New(options *Options) *API {
12171217
r.Get("/",api.template)
12181218
r.Delete("/",api.deleteTemplate)
12191219
r.Patch("/",api.patchTemplateMeta)
1220+
r.Post("/prebuilds/invalidate",api.postInvalidateTemplatePrebuilds)
12201221
r.Route("/versions",func(r chi.Router) {
12211222
r.Post("/archive",api.postArchiveTemplateVersions)
12221223
r.Get("/",api.templateVersionsByTemplate)

‎coderd/templates.go‎

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,3 +1146,125 @@ func findTemplateAdmins(ctx context.Context, store database.Store) ([]database.G
11461146
}
11471147
returnappend(owners,templateAdmins...),nil
11481148
}
1149+
1150+
// @Summary Invalidate all prebuilt workspaces for a template
1151+
// @ID invalidate-template-prebuilds
1152+
// @Security CoderSessionToken
1153+
// @Accept json
1154+
// @Produce json
1155+
// @Tags Templates
1156+
// @Param template path string true "Template ID" format(uuid)
1157+
// @Success 200 {object} codersdk.Response
1158+
// @Router /templates/{template}/prebuilds/invalidate [post]
1159+
func (api*API)postInvalidateTemplatePrebuilds(rw http.ResponseWriter,r*http.Request) {
1160+
ctx:=r.Context()
1161+
template:=httpmw.TemplateParam(r)
1162+
apiKey:=httpmw.APIKey(r)
1163+
1164+
// Authorization: user must be able to update the template
1165+
if!api.Authorize(r,policy.ActionUpdate,template.RBACObject()) {
1166+
httpapi.Forbidden(rw)
1167+
return
1168+
}
1169+
1170+
// Get all workspaces for this template (returns WorkspaceTable without version info)
1171+
workspaceTables,err:=api.Database.GetWorkspacesByTemplateID(ctx,template.ID)
1172+
iferr!=nil {
1173+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
1174+
Message:"Failed to fetch workspaces.",
1175+
Detail:err.Error(),
1176+
})
1177+
return
1178+
}
1179+
1180+
// Filter and fetch full workspace details for prebuilt workspaces only
1181+
varprebuildWorkspaces []database.Workspace
1182+
for_,wt:=rangeworkspaceTables {
1183+
// Quick filter: only check prebuilt workspaces
1184+
ifwt.OwnerID!=database.PrebuildsSystemUserID {
1185+
continue
1186+
}
1187+
1188+
// Fetch latest build to get template_version_id
1189+
build,err:=api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx,wt.ID)
1190+
iferr!=nil {
1191+
api.Logger.Warn(ctx,"failed to fetch workspace build",
1192+
slog.F("workspace_id",wt.ID),
1193+
slog.Error(err),
1194+
)
1195+
continue
1196+
}
1197+
1198+
// Only include if the latest build uses the active template version
1199+
ifbuild.TemplateVersionID==template.ActiveVersionID {
1200+
ws,err:=api.Database.GetWorkspaceByID(ctx,wt.ID)
1201+
iferr!=nil {
1202+
continue
1203+
}
1204+
prebuildWorkspaces=append(prebuildWorkspaces,ws)
1205+
}
1206+
}
1207+
1208+
iflen(prebuildWorkspaces)==0 {
1209+
httpapi.Write(ctx,rw,http.StatusOK, codersdk.Response{
1210+
Message:"No prebuilt workspaces found for this template's active version.",
1211+
})
1212+
return
1213+
}
1214+
1215+
// Delete each prebuilt workspace using the existing workspace delete logic
1216+
varinvalidatedCountint
1217+
varerrors []string
1218+
1219+
api.Logger.Info(ctx,"invalidating prebuilt workspaces",
1220+
slog.F("template_id",template.ID),
1221+
slog.F("template_name",template.Name),
1222+
slog.F("workspace_count",len(prebuildWorkspaces)),
1223+
slog.F("initiator_id",apiKey.UserID),
1224+
)
1225+
1226+
for_,workspace:=rangeprebuildWorkspaces {
1227+
// Reuse the existing delete workspace logic
1228+
createBuild:= codersdk.CreateWorkspaceBuildRequest{
1229+
Transition:codersdk.WorkspaceTransitionDelete,
1230+
}
1231+
1232+
_,err:=api.postWorkspaceBuildsInternal(
1233+
ctx,
1234+
apiKey,
1235+
workspace,
1236+
createBuild,
1237+
func(action policy.Action,object rbac.Objecter)bool {
1238+
returnapi.Authorize(r,action,object)
1239+
},
1240+
audit.WorkspaceBuildBaggageFromRequest(r),
1241+
)
1242+
iferr!=nil {
1243+
errors=append(errors,fmt.Sprintf("workspace %s: %v",workspace.Name,err))
1244+
api.Logger.Warn(ctx,"failed to invalidate prebuilt workspace",
1245+
slog.F("workspace_id",workspace.ID),
1246+
slog.F("workspace_name",workspace.Name),
1247+
slog.Error(err),
1248+
)
1249+
continue
1250+
}
1251+
1252+
invalidatedCount++
1253+
}
1254+
1255+
api.Logger.Info(ctx,"completed prebuild invalidation",
1256+
slog.F("template_id",template.ID),
1257+
slog.F("invalidated",invalidatedCount),
1258+
slog.F("failed",len(errors)),
1259+
)
1260+
1261+
message:=fmt.Sprintf("Successfully invalidated %d prebuilt workspace(s).",invalidatedCount)
1262+
iflen(errors)>0 {
1263+
message=fmt.Sprintf("Invalidated %d prebuilt workspace(s), %d failed. Errors: %s",
1264+
invalidatedCount,len(errors),strings.Join(errors,"; "))
1265+
}
1266+
1267+
httpapi.Write(ctx,rw,http.StatusOK, codersdk.Response{
1268+
Message:message,
1269+
})
1270+
}

‎coderd/templates_test.go‎

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2067,3 +2067,123 @@ func TestTemplateFilterHasExternalAgent(t *testing.T) {
20672067
require.Len(t,templates,1)
20682068
require.Equal(t,templateWithoutExternalAgent.ID,templates[0].ID)
20692069
}
2070+
2071+
funcTestInvalidateTemplatePrebuilds(t*testing.T) {
2072+
t.Parallel()
2073+
2074+
t.Run("OK",func(t*testing.T) {
2075+
t.Parallel()
2076+
2077+
ctx:=testutil.Context(t,testutil.WaitLong)
2078+
owner,db:=coderdtest.NewWithDatabase(t,nil)
2079+
2080+
// Create organization and template with old version
2081+
org:=dbgen.Organization(t,db, database.Organization{})
2082+
oldVersion:=dbgen.TemplateVersion(t,db, database.TemplateVersion{
2083+
OrganizationID:org.ID,
2084+
})
2085+
template:=dbgen.Template(t,db, database.Template{
2086+
OrganizationID:org.ID,
2087+
ActiveVersionID:oldVersion.ID,
2088+
})
2089+
2090+
// Create prebuild with old version (should NOT be invalidated)
2091+
oldPrebuild:=dbgen.Workspace(t,db, database.WorkspaceTable{
2092+
OrganizationID:org.ID,
2093+
OwnerID:database.PrebuildsSystemUserID,
2094+
TemplateID:template.ID,
2095+
})
2096+
_=dbgen.WorkspaceBuild(t,db, database.WorkspaceBuild{
2097+
WorkspaceID:oldPrebuild.ID,
2098+
TemplateVersionID:oldVersion.ID,
2099+
})
2100+
2101+
// Update template to new active version
2102+
newVersion:=dbgen.TemplateVersion(t,db, database.TemplateVersion{
2103+
OrganizationID:org.ID,
2104+
TemplateID: uuid.NullUUID{UUID:template.ID,Valid:true},
2105+
})
2106+
err:=db.UpdateTemplateActiveVersionByID(ctx, database.UpdateTemplateActiveVersionByIDParams{
2107+
ID:template.ID,
2108+
ActiveVersionID:newVersion.ID,
2109+
})
2110+
require.NoError(t,err)
2111+
template.ActiveVersionID=newVersion.ID
2112+
2113+
// Create 2 prebuilds with active version (SHOULD be invalidated)
2114+
varactivePrebuilds []database.WorkspaceTable
2115+
fori:=0;i<2;i++ {
2116+
ws:=dbgen.Workspace(t,db, database.WorkspaceTable{
2117+
OrganizationID:org.ID,
2118+
OwnerID:database.PrebuildsSystemUserID,
2119+
TemplateID:template.ID,
2120+
})
2121+
_=dbgen.WorkspaceBuild(t,db, database.WorkspaceBuild{
2122+
WorkspaceID:ws.ID,
2123+
TemplateVersionID:newVersion.ID,
2124+
})
2125+
activePrebuilds=append(activePrebuilds,ws)
2126+
}
2127+
2128+
// Create user workspace with active version (should NOT be touched)
2129+
user:=coderdtest.CreateFirstUser(t,owner)
2130+
userWorkspace:=dbgen.Workspace(t,db, database.WorkspaceTable{
2131+
OrganizationID:org.ID,
2132+
OwnerID:user.UserID,
2133+
TemplateID:template.ID,
2134+
})
2135+
_=dbgen.WorkspaceBuild(t,db, database.WorkspaceBuild{
2136+
WorkspaceID:userWorkspace.ID,
2137+
TemplateVersionID:newVersion.ID,
2138+
})
2139+
2140+
// Invalidate prebuilds as template admin
2141+
_,client:=coderdtest.CreateAnotherUser(t,owner,org.ID,rbac.RoleTemplateAdmin())
2142+
err=client.InvalidateTemplatePrebuilds(ctx,template.ID)
2143+
require.NoError(t,err)
2144+
2145+
// Verify old prebuild still exists (different version)
2146+
_,err=db.GetWorkspaceByID(ctx,oldPrebuild.ID)
2147+
require.NoError(t,err,"old version prebuild should not be deleted")
2148+
2149+
// Verify user workspace still exists
2150+
_,err=db.GetWorkspaceByID(ctx,userWorkspace.ID)
2151+
require.NoError(t,err,"user workspace should not be deleted")
2152+
2153+
// Verify active prebuilds have delete builds created
2154+
for_,ws:=rangeactivePrebuilds {
2155+
builds,err:=db.GetWorkspaceBuildsByWorkspaceID(ctx, database.GetWorkspaceBuildsByWorkspaceIDParams{
2156+
WorkspaceID:ws.ID,
2157+
})
2158+
require.NoError(t,err)
2159+
require.NotEmpty(t,builds,"active prebuild should have builds")
2160+
// Note: we can't easily verify the delete build was created without more setup
2161+
}
2162+
})
2163+
2164+
t.Run("Unauthorized",func(t*testing.T) {
2165+
t.Parallel()
2166+
2167+
ctx:=testutil.Context(t,testutil.WaitLong)
2168+
owner,db:=coderdtest.NewWithDatabase(t,nil)
2169+
2170+
org:=dbgen.Organization(t,db, database.Organization{})
2171+
version:=dbgen.TemplateVersion(t,db, database.TemplateVersion{
2172+
OrganizationID:org.ID,
2173+
})
2174+
template:=dbgen.Template(t,db, database.Template{
2175+
OrganizationID:org.ID,
2176+
ActiveVersionID:version.ID,
2177+
})
2178+
2179+
// Regular user (not admin)
2180+
_,regularUser:=coderdtest.CreateAnotherUser(t,owner,org.ID)
2181+
2182+
err:=regularUser.InvalidateTemplatePrebuilds(ctx,template.ID)
2183+
require.Error(t,err)
2184+
2185+
varapiErr*codersdk.Error
2186+
require.ErrorAs(t,err,&apiErr)
2187+
require.Equal(t,403,apiErr.StatusCode())
2188+
})
2189+
}

‎codersdk/templates.go‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,3 +507,23 @@ func (c *Client) StarterTemplates(ctx context.Context) ([]TemplateExample, error
507507
vartemplateExamples []TemplateExample
508508
returntemplateExamples,json.NewDecoder(res.Body).Decode(&templateExamples)
509509
}
510+
511+
// InvalidateTemplatePrebuilds invalidates all prebuilt workspaces for the
512+
// template's active version by deleting them. This is useful when prebuilds
513+
// become stale due to updated base images, repository changes, or other factors.
514+
func (c*Client)InvalidateTemplatePrebuilds(ctx context.Context,template uuid.UUID)error {
515+
res,err:=c.Request(ctx,http.MethodPost,
516+
fmt.Sprintf("/api/v2/templates/%s/prebuilds/invalidate",template),
517+
nil,
518+
)
519+
iferr!=nil {
520+
returnerr
521+
}
522+
deferres.Body.Close()
523+
524+
ifres.StatusCode!=http.StatusOK {
525+
returnReadBodyAsError(res)
526+
}
527+
528+
returnnil
529+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp