mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -87,6 +87,7 @@ func buildNumberOption(n *int64) serpent.Option {
|
||||
|
||||
func (r *RootCmd) statePush() *serpent.Command {
|
||||
var buildNumber int64
|
||||
var noBuild bool
|
||||
cmd := &serpent.Command{
|
||||
Use: "push <workspace> <file>",
|
||||
Short: "Push a Terraform state file to a workspace.",
|
||||
@@ -126,6 +127,16 @@ func (r *RootCmd) statePush() *serpent.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
if noBuild {
|
||||
// Update state directly without triggering a build.
|
||||
err = client.UpdateWorkspaceBuildState(inv.Context(), build.ID, state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprintln(inv.Stdout, "State updated successfully.")
|
||||
return nil
|
||||
}
|
||||
|
||||
build, err = client.CreateWorkspaceBuild(inv.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
TemplateVersionID: build.TemplateVersionID,
|
||||
Transition: build.Transition,
|
||||
@@ -139,6 +150,12 @@ func (r *RootCmd) statePush() *serpent.Command {
|
||||
}
|
||||
cmd.Options = serpent.OptionSet{
|
||||
buildNumberOption(&buildNumber),
|
||||
{
|
||||
Flag: "no-build",
|
||||
FlagShorthand: "n",
|
||||
Description: "Update the state without triggering a workspace build. Useful for state-only migrations.",
|
||||
Value: serpent.BoolOf(&noBuild),
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package cli_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/coder/coder/v2/cli/clitest"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbfake"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/provisioner/echo"
|
||||
@@ -157,4 +159,49 @@ func TestStatePush(t *testing.T) {
|
||||
err := inv.Run()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("NoBuild", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client, store := coderdtest.NewWithDatabase(t, nil)
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
templateAdmin, taUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
initialState := []byte("initial state")
|
||||
r := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{
|
||||
OrganizationID: owner.OrganizationID,
|
||||
OwnerID: taUser.ID,
|
||||
}).
|
||||
Seed(database.WorkspaceBuild{ProvisionerState: initialState}).
|
||||
Do()
|
||||
wantState := []byte("updated state")
|
||||
stateFile, err := os.CreateTemp(t.TempDir(), "")
|
||||
require.NoError(t, err)
|
||||
_, err = stateFile.Write(wantState)
|
||||
require.NoError(t, err)
|
||||
err = stateFile.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
inv, root := clitest.New(t, "state", "push", "--no-build", r.Workspace.Name, stateFile.Name())
|
||||
clitest.SetupConfig(t, templateAdmin, root)
|
||||
var stdout bytes.Buffer
|
||||
inv.Stdout = &stdout
|
||||
err = inv.Run()
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, stdout.String(), "State updated successfully")
|
||||
|
||||
// Verify the state was updated by pulling it.
|
||||
inv, root = clitest.New(t, "state", "pull", r.Workspace.Name)
|
||||
var gotState bytes.Buffer
|
||||
inv.Stdout = &gotState
|
||||
clitest.SetupConfig(t, templateAdmin, root)
|
||||
err = inv.Run()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, wantState, bytes.TrimSpace(gotState.Bytes()))
|
||||
|
||||
// Verify no new build was created.
|
||||
builds, err := store.GetWorkspaceBuildsByWorkspaceID(dbauthz.AsSystemRestricted(context.Background()), database.GetWorkspaceBuildsByWorkspaceIDParams{
|
||||
WorkspaceID: r.Workspace.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, builds, 1, "expected only the initial build, no new build should be created")
|
||||
})
|
||||
}
|
||||
|
||||
+4
@@ -9,5 +9,9 @@ OPTIONS:
|
||||
-b, --build int
|
||||
Specify a workspace build to target by name. Defaults to latest.
|
||||
|
||||
-n, --no-build bool
|
||||
Update the state without triggering a workspace build. Useful for
|
||||
state-only migrations.
|
||||
|
||||
———
|
||||
Run `coder --help` for a list of global options.
|
||||
|
||||
Reference in New Issue
Block a user