Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/wfctl/dsl-reference-embedded.md
Original file line number Diff line number Diff line change
Expand Up @@ -1456,6 +1456,10 @@ wfctl infra security-check --plan plan.json --strict-cidr
wfctl infra apply -c infra.yaml --plan plan.json --auto-approve
```

Use `wfctl infra apply --skip-bootstrap` only for scoped/operator applies where
the required secrets and state backend already exist and bootstrap would mutate
unrelated secret material.

Example output when a finding is detected:

```
Expand Down
4 changes: 3 additions & 1 deletion cmd/wfctl/infra.go
Original file line number Diff line number Diff line change
Expand Up @@ -1222,6 +1222,8 @@ func runInfraApply(args []string) error {
"Refresh per-field Outputs from cloud truth before applying (recommended pair with --refresh for cutover-style operations)")
var skipRefreshFlag bool
fs.BoolVar(&skipRefreshFlag, "skip-refresh", false, "Skip the WFCTL_REFRESH_OUTPUTS pre-step refresh even if the env var is set (does NOT cancel explicit --refresh-outputs)")
var skipBootstrapFlag bool
fs.BoolVar(&skipBootstrapFlag, "skip-bootstrap", false, "Skip auto-bootstrap before apply; use only when required secrets/state already exist")
var allowReplaceFlag string
fs.StringVar(&allowReplaceFlag, "allow-replace", "",
"Comma-separated list of resource names whose protected: true status is overridden for this apply (replace/delete actions only)")
Expand Down Expand Up @@ -1306,7 +1308,7 @@ func runInfraApply(args []string) error {
if err != nil {
return fmt.Errorf("parse infra config: %w", err)
}
autoBootstrap := infraCfg == nil || infraCfg.AutoBootstrap == nil || *infraCfg.AutoBootstrap
autoBootstrap := !skipBootstrapFlag && (infraCfg == nil || infraCfg.AutoBootstrap == nil || *infraCfg.AutoBootstrap)
if autoBootstrap {
fmt.Println("Running bootstrap before apply...")
bootstrapArgs := []string{"--config", cfgFile}
Expand Down
74 changes: 74 additions & 0 deletions cmd/wfctl/infra_apply_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,80 @@ modules:
}
}

func TestInfraApplyPlanSkipBootstrap(t *testing.T) {
dir := t.TempDir()
cfgPath := filepath.Join(dir, "infra.yaml")
if err := os.WriteFile(cfgPath, []byte(`
infra:
auto_bootstrap: true
secrets:
provider: env
generate:
- key: SHOULD_NOT_BOOTSTRAP
type: provider_credential
source: not-a-real-provider
name: should-not-bootstrap
modules:
- name: test-provider
type: iac.provider
config:
provider: fake-cloud
- name: my-dns
type: infra.dns
config:
provider: test-provider
domain: example.com
`), 0o600); err != nil {
t.Fatalf("write config: %v", err)
}

specs, err := parseInfraResourceSpecs(cfgPath)
if err != nil {
t.Fatalf("parseInfraResourceSpecs: %v", err)
}
plan := interfaces.IaCPlan{
ID: "dns-plan",
DesiredHash: desiredStateHash(specs),
Actions: []interfaces.PlanAction{
{Action: "create", Resource: specs[0]},
},
CreatedAt: time.Now().UTC(),
}
planData, err := json.Marshal(plan)
if err != nil {
t.Fatalf("marshal plan: %v", err)
}
planPath := filepath.Join(dir, "plan.json")
if err := os.WriteFile(planPath, planData, 0o600); err != nil {
t.Fatalf("write plan: %v", err)
}

fake := &applyCapture{}
origResolve := resolveIaCProvider
resolveIaCProvider = func(_ context.Context, _ string, _ map[string]any) (interfaces.IaCProvider, io.Closer, error) {
return fake, nil, nil
}
defer func() { resolveIaCProvider = origResolve }()

applyCalled := false
origApply := applyV2ApplyPlanWithHooksFn
applyV2ApplyPlanWithHooksFn = func(_ context.Context, _ interfaces.IaCProvider, p *interfaces.IaCPlan, _ wfctlhelpers.ApplyPlanHooks) (*interfaces.ApplyResult, error) {
applyCalled = true
if p.ID != "dns-plan" {
t.Fatalf("plan ID = %q, want dns-plan", p.ID)
}
return &interfaces.ApplyResult{PlanID: p.ID}, nil
}
defer func() { applyV2ApplyPlanWithHooksFn = origApply }()

if err := runInfraApply([]string{"--auto-approve", "--config", cfgPath, "--plan", planPath, "--skip-bootstrap"}); err != nil {
t.Fatalf("runInfraApply: %v", err)
}
if !applyCalled {
t.Fatal("v2 dispatch was not called")
}
}

// TestInfraApplyConsumesPlan_StaleDetection verifies that wfctl infra apply --plan
// fails with a descriptive error when the plan's DesiredHash no longer matches the
// current desired state (i.e. the config was edited after the plan was generated).
Expand Down
2 changes: 2 additions & 0 deletions docs/WFCTL.md
Original file line number Diff line number Diff line change
Expand Up @@ -1516,6 +1516,7 @@ Reconcile cloud infrastructure to match the desired state declared in the config
```
wfctl infra apply [-c CONFIG] [--env ENV] [--auto-approve] [--plan FILE]
[--refresh] [--allow-protected-prune] [--skip-refresh]
[--skip-bootstrap]
[--allow-replace=NAME1,NAME2,...] [--dry-run] [--format FMT]
```

Expand All @@ -1531,6 +1532,7 @@ wfctl infra apply [-c CONFIG] [--env ENV] [--auto-approve] [--plan FILE]
| `--refresh` | `false` | Detect drift and prune ghost-in-state entries before applying |
| `--allow-protected-prune` | `false` | Allow pruning state entries for resources marked `protected: true` (requires `--refresh`) |
| `--skip-refresh` | `false` | Skip the `WFCTL_REFRESH_OUTPUTS` pre-step refresh even if the env var is set |
| `--skip-bootstrap` | `false` | Skip auto-bootstrap before apply when required secrets/state already exist |
| `--allow-replace` | `` | Comma-separated list of resource names whose `protected: true` status is overridden for this apply (replace/delete actions only) |
| `--plugin-dir` | _(env `WFCTL_PLUGIN_DIR` or `data/plugins`)_ | Override the plugin directory for this invocation. Useful for isolated CI smoke tests. |

Expand Down
Loading