feat(cli): add --no-build flag to state push for state-only updates (#21374)

## Summary

Adds a `--no-build` flag to `coder state push` that updates the
Terraform state directly without triggering a workspace build.

## Use Case

This enables state-only migrations, such as migrating Kubernetes
resources from deprecated types (e.g., `kubernetes_config_map`) to
versioned types (e.g., `kubernetes_config_map_v1`):

```bash
coder state pull my-workspace > state.json
terraform init
terraform state rm -state=state.json kubernetes_config_map.example
terraform import -state=state.json kubernetes_config_map_v1.example default/example
coder state push --no-build my-workspace state.json
```

## Changes

- Add `PUT /api/v2/workspacebuilds/{id}/state` endpoint to update state
without triggering a build
- Add `UpdateWorkspaceBuildState` SDK method
- Add `--no-build`/`-n` flag to `coder state push`
- Add confirmation prompt (can be skipped with `--yes`/`-y`) since this
is a potentially dangerous operation
- Add test for `--no-build` functionality

Fixes #21336
This commit is contained in:
Kacper Sawicki
2026-01-12 15:16:59 +01:00
committed by GitHub
parent a581431bc8
commit 6ca70d3618
12 changed files with 315 additions and 0 deletions
+57
View File
@@ -882,6 +882,63 @@ func (api *API) workspaceBuildState(rw http.ResponseWriter, r *http.Request) {
_, _ = rw.Write(workspaceBuild.ProvisionerState)
}
// @Summary Update workspace build state
// @ID update-workspace-build-state
// @Security CoderSessionToken
// @Accept json
// @Tags Builds
// @Param workspacebuild path string true "Workspace build ID" format(uuid)
// @Param request body codersdk.UpdateWorkspaceBuildStateRequest true "Request body"
// @Success 204
// @Router /workspacebuilds/{workspacebuild}/state [put]
func (api *API) workspaceBuildUpdateState(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspaceBuild := httpmw.WorkspaceBuildParam(r)
workspace, err := api.Database.GetWorkspaceByID(ctx, workspaceBuild.WorkspaceID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "No workspace exists for this job.",
})
return
}
template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to get template",
Detail: err.Error(),
})
return
}
// You must have update permissions on the template to update the state.
if !api.Authorize(r, policy.ActionUpdate, template.RBACObject()) {
httpapi.ResourceNotFound(rw)
return
}
var req codersdk.UpdateWorkspaceBuildStateRequest
if !httpapi.Read(ctx, rw, r, &req) {
return
}
// Use system context since we've already verified authorization via template permissions.
// nolint:gocritic // System access required for provisioner state update.
err = api.Database.UpdateWorkspaceBuildProvisionerStateByID(dbauthz.AsSystemRestricted(ctx), database.UpdateWorkspaceBuildProvisionerStateByIDParams{
ID: workspaceBuild.ID,
ProvisionerState: req.State,
UpdatedAt: dbtime.Now(),
})
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to update workspace build state.",
Detail: err.Error(),
})
return
}
rw.WriteHeader(http.StatusNoContent)
}
// @Summary Get workspace build timings by ID
// @ID get-workspace-build-timings-by-id
// @Security CoderSessionToken