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

Commit48ae5a2

Browse files
committed
feat(cli): add --no-build flag to state push for state-only updates
Adds a --no-build flag to 'coder state push' that updates the Terraformstate directly without triggering a workspace build. This enablesstate-only migrations, such as migrating Kubernetes resources fromdeprecated types to versioned types.Changes:- Add PUT /api/v2/workspacebuilds/{id}/state endpoint- Add UpdateWorkspaceBuildState SDK method- Add --no-build/-n flag to 'coder state push'- Add confirmation prompt with --yes/-y skip option- Add test for --no-build functionalityFixes#21336
1 parent5b3c24c commit48ae5a2

File tree

5 files changed

+146
-0
lines changed

5 files changed

+146
-0
lines changed

‎cli/state.go‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ func buildNumberOption(n *int64) serpent.Option {
8787

8888
func (r*RootCmd)statePush()*serpent.Command {
8989
varbuildNumberint64
90+
varnoBuildbool
9091
cmd:=&serpent.Command{
9192
Use:"push <workspace> <file>",
9293
Short:"Push a Terraform state file to a workspace.",
@@ -126,6 +127,29 @@ func (r *RootCmd) statePush() *serpent.Command {
126127
returnerr
127128
}
128129

130+
ifnoBuild {
131+
// Warn user about the dangerous operation.
132+
cliui.Warn(inv.Stderr,
133+
"This will update the Terraform state directly without triggering a build.\n"+
134+
"The workspace will not be reconciled with the new state.")
135+
_,err=cliui.Prompt(inv, cliui.PromptOptions{
136+
Text:"Confirm state update?",
137+
IsConfirm:true,
138+
Default:cliui.ConfirmNo,
139+
})
140+
iferr!=nil {
141+
returnerr
142+
}
143+
144+
// Update state directly without triggering a build.
145+
err=client.UpdateWorkspaceBuildState(inv.Context(),build.ID,state)
146+
iferr!=nil {
147+
returnerr
148+
}
149+
_,_=fmt.Fprintln(inv.Stdout,"State updated successfully.")
150+
returnnil
151+
}
152+
129153
build,err=client.CreateWorkspaceBuild(inv.Context(),workspace.ID, codersdk.CreateWorkspaceBuildRequest{
130154
TemplateVersionID:build.TemplateVersionID,
131155
Transition:build.Transition,
@@ -139,6 +163,13 @@ func (r *RootCmd) statePush() *serpent.Command {
139163
}
140164
cmd.Options= serpent.OptionSet{
141165
buildNumberOption(&buildNumber),
166+
{
167+
Flag:"no-build",
168+
FlagShorthand:"n",
169+
Description:"Update the state without triggering a workspace build. Useful for state-only migrations.",
170+
Value:serpent.BoolOf(&noBuild),
171+
},
172+
cliui.SkipPromptOption(),
142173
}
143174
returncmd
144175
}

‎cli/state_test.go‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,42 @@ func TestStatePush(t *testing.T) {
158158
err:=inv.Run()
159159
require.NoError(t,err)
160160
})
161+
162+
t.Run("NoBuild",func(t*testing.T) {
163+
t.Parallel()
164+
client,store:=coderdtest.NewWithDatabase(t,nil)
165+
owner:=coderdtest.CreateFirstUser(t,client)
166+
templateAdmin,taUser:=coderdtest.CreateAnotherUser(t,client,owner.OrganizationID,rbac.RoleTemplateAdmin())
167+
initialState:= []byte("initial state")
168+
r:=dbfake.WorkspaceBuild(t,store, database.WorkspaceTable{
169+
OrganizationID:owner.OrganizationID,
170+
OwnerID:taUser.ID,
171+
}).
172+
Seed(database.WorkspaceBuild{ProvisionerState:initialState}).
173+
Do()
174+
wantState:= []byte("updated state")
175+
stateFile,err:=os.CreateTemp(t.TempDir(),"")
176+
require.NoError(t,err)
177+
_,err=stateFile.Write(wantState)
178+
require.NoError(t,err)
179+
err=stateFile.Close()
180+
require.NoError(t,err)
181+
182+
inv,root:=clitest.New(t,"state","push","--no-build","--yes",r.Workspace.Name,stateFile.Name())
183+
clitest.SetupConfig(t,templateAdmin,root)
184+
varstdout bytes.Buffer
185+
inv.Stdout=&stdout
186+
err=inv.Run()
187+
require.NoError(t,err)
188+
require.Contains(t,stdout.String(),"State updated successfully")
189+
190+
// Verify the state was updated by pulling it.
191+
inv,root=clitest.New(t,"state","pull",r.Workspace.Name)
192+
vargotState bytes.Buffer
193+
inv.Stdout=&gotState
194+
clitest.SetupConfig(t,templateAdmin,root)
195+
err=inv.Run()
196+
require.NoError(t,err)
197+
require.Equal(t,wantState,bytes.TrimSpace(gotState.Bytes()))
198+
})
161199
}

‎coderd/coderd.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1507,6 +1507,7 @@ func New(options *Options) *API {
15071507
r.Get("/parameters",api.workspaceBuildParameters)
15081508
r.Get("/resources",api.workspaceBuildResourcesDeprecated)
15091509
r.Get("/state",api.workspaceBuildState)
1510+
r.Put("/state",api.workspaceBuildUpdateState)
15101511
r.Get("/timings",api.workspaceBuildTimings)
15111512
})
15121513
r.Route("/authcheck",func(r chi.Router) {

‎coderd/workspacebuilds.go‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"errors"
88
"fmt"
9+
"io"
910
"math"
1011
"net/http"
1112
"slices"
@@ -884,6 +885,67 @@ func (api *API) workspaceBuildState(rw http.ResponseWriter, r *http.Request) {
884885
}
885886

886887
// @Summary Get workspace build timings by ID
888+
// @Summary Update workspace build state
889+
// @ID update-workspace-build-state
890+
// @Security CoderSessionToken
891+
// @Accept application/octet-stream
892+
// @Tags Builds
893+
// @Param workspacebuild path string true "Workspace build ID" format(uuid)
894+
// @Param request body []byte true "New Terraform state"
895+
// @Success 204
896+
// @Router /workspacebuilds/{workspacebuild}/state [put]
897+
func (api*API)workspaceBuildUpdateState(rw http.ResponseWriter,r*http.Request) {
898+
ctx:=r.Context()
899+
workspaceBuild:=httpmw.WorkspaceBuildParam(r)
900+
workspace,err:=api.Database.GetWorkspaceByID(ctx,workspaceBuild.WorkspaceID)
901+
iferr!=nil {
902+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
903+
Message:"No workspace exists for this job.",
904+
})
905+
return
906+
}
907+
template,err:=api.Database.GetTemplateByID(ctx,workspace.TemplateID)
908+
iferr!=nil {
909+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
910+
Message:"Failed to get template",
911+
Detail:err.Error(),
912+
})
913+
return
914+
}
915+
916+
// You must have update permissions on the template to update the state.
917+
if!api.Authorize(r,policy.ActionUpdate,template.RBACObject()) {
918+
httpapi.ResourceNotFound(rw)
919+
return
920+
}
921+
922+
state,err:=io.ReadAll(r.Body)
923+
iferr!=nil {
924+
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
925+
Message:"Failed to read request body.",
926+
Detail:err.Error(),
927+
})
928+
return
929+
}
930+
931+
// Use system context since we've already verified authorization via template permissions.
932+
// nolint:gocritic // System access required for provisioner state update.
933+
err=api.Database.UpdateWorkspaceBuildProvisionerStateByID(dbauthz.AsSystemRestricted(ctx), database.UpdateWorkspaceBuildProvisionerStateByIDParams{
934+
ID:workspaceBuild.ID,
935+
ProvisionerState:state,
936+
UpdatedAt:dbtime.Now(),
937+
})
938+
iferr!=nil {
939+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
940+
Message:"Failed to update workspace build state.",
941+
Detail:err.Error(),
942+
})
943+
return
944+
}
945+
946+
rw.WriteHeader(http.StatusNoContent)
947+
}
948+
887949
// @ID get-workspace-build-timings-by-id
888950
// @Security CoderSessionToken
889951
// @Produce json

‎codersdk/workspacebuilds.go‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,20 @@ func (c *Client) WorkspaceBuildState(ctx context.Context, build uuid.UUID) ([]by
188188
returnio.ReadAll(res.Body)
189189
}
190190

191+
// UpdateWorkspaceBuildState updates the provisioner state of the build without
192+
// triggering a new build. This is useful for state-only migrations.
193+
func (c*Client)UpdateWorkspaceBuildState(ctx context.Context,build uuid.UUID,state []byte)error {
194+
res,err:=c.Request(ctx,http.MethodPut,fmt.Sprintf("/api/v2/workspacebuilds/%s/state",build),state)
195+
iferr!=nil {
196+
returnerr
197+
}
198+
deferres.Body.Close()
199+
ifres.StatusCode!=http.StatusNoContent {
200+
returnReadBodyAsError(res)
201+
}
202+
returnnil
203+
}
204+
191205
func (c*Client)WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(ctx context.Context,usernamestring,workspaceNamestring,buildNumberstring) (WorkspaceBuild,error) {
192206
res,err:=c.Request(ctx,http.MethodGet,fmt.Sprintf("/api/v2/users/%s/workspace/%s/builds/%s",username,workspaceName,buildNumber),nil)
193207
iferr!=nil {

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp