Repository navigation
fix: respect workdir path for generate: writes and hook-triggered terraform - #2309
Conversation
…raform When `provision.workdir.enabled: true`, atmos was using the base component directory for two operations that must target the JIT workdir instead. **Bug 1 – generate: double-write to base component directory** `resolveAndProvisionComponentPath` called `autoGenerateComponentFiles` with the base component path *before* calling `provisionComponentSource`. By the time the workdir was provisioned, generated files (e.g. `locals_override.tf`) had already been written to `components/terraform/<component>/`. They were then written again to the workdir — leaving an orphaned override file in the base directory with no primary source to override against. Fix: swap the call order so `provisionComponentSource` runs first and returns the workdir path; `autoGenerateComponentFiles` then writes to that path only. **Bug 2 – hooks run `terraform output` against base component directory** `before-terraform-apply` hooks call `tfoutput.GetOutput`, which calls `DescribeComponent` to get fresh sections. `extractComponentPath` in `pkg/terraform/output/config.go` reconstructed the path via `utils.GetComponentPath`, which always returns the base directory. The `_workdir_path` runtime key is absent from freshly-described sections, so hooks ran `terraform init` against the base directory — where only the orphaned override file existed, causing init to fail with "Missing base local value definition to override". Fix: in `extractComponentPath`, check `provision.workdir.enabled` in sections and rebuild the deterministic workdir path via `workdir.BuildPath` when it is set, ensuring all callers (including hooks) target the correct directory. Fixes cloudposse#2308
|
Tip Atmos Pro
No affected stacks workflow was detected for this pull request. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughReorders workdir provisioning to occur before file generation and hooks; components with workdir enabled return a deterministic workdir path and signal reprovisioning; terraform init reconfigure and workspace cleanup now respect workdir reprovision state; hooks are filtered by event and store-output retrieval is event-aware. Changes
Sequence Diagram(s)sequenceDiagram
participant CLI as rgba(66,133,244,0.5) CLI
participant Exec as rgba(52,168,83,0.5) Executor
participant Prov as rgba(255,193,7,0.5) Provisioner/Workdir
participant FS as rgba(244,67,54,0.5) Filesystem
participant Hooks as rgba(156,39,176,0.5) Hooks/Store
participant Terraform as rgba(33,150,243,0.5) Terraform
CLI->>Exec: run terraform command (init/apply/etc.)
Exec->>Prov: provisionComponentSource (resolve JIT workdir)
Prov->>FS: create/write workdir files & metadata
Prov-->>Exec: return componentPath + WorkdirReprovisionedKey (if changed)
Exec->>FS: autoGenerateComponentFiles -> write to returned componentPath
Exec->>Hooks: run pre-execution hooks (use returned componentPath)
Hooks->>Exec: request terraform output (event-aware getter)
Exec->>Terraform: init/apply (include -reconfigure only if reprovisioned)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…T workdir
Three bugs fixed for the provision.workdir + after-terraform-apply hook scenario:
Bug 3 – after-terraform-apply hooks fired on every event
RunAll had no event matching: all hooks ran regardless of their events: list.
Added MatchesEvent() with hyphen→dot normalization so YAML's
"after-terraform-apply" matches the Go HookEvent "after.terraform.apply".
Bug 4 – store hook re-ran terraform init after apply, triggering state-migration prompt
GetOutput (the previous outputGetter in StoreCommand) ran a full
terraform init cycle including CleanWorkspace. With stdin closed post-apply,
tofu prompted for state migration and failed. Replaced with GetOutputSkipInit
(new OutputOptions{SkipInit: true} path in GetOutputWithOptions) so the hook
reads outputs without re-initialising an already-initialised workdir.
Bug 5 – JIT workdir triggers "Do you want to migrate?" on every apply
When provision.workdir.enabled + source.ttl:"0s" are set, VendorSource
calls os.RemoveAll on the workdir before each invocation, wiping
.terraform/terraform.tfstate. The freshly regenerated backend.tf.json then
mismatches the cached backend state, and terraform prompts for migration.
Fixed by passing -reconfigure to terraform init automatically whenever
WorkdirPathKey is set in info.ComponentSection (covers both source+workdir
and workdir-only modes). Same fix applied to buildInitSubcommandArgs for
the explicit atmos terraform init path.
Hooks with an empty or absent 'events' field now match all events, matching the pre-event-filtering behavior where every hook always ran. This prevents a silent breaking change for existing configs that predate the events field.
- Store hook now selects GetOutputSkipInit for after-events (workdir already initialized) and GetOutput for before-events (init may not have run yet), preventing both silent failures and unnecessary re-init prompts. - Error messages now include hook name, event, output key, component, and stack so failures are immediately actionable. - Use correct error sentinels: ErrTerraformOutputFailed for retrieval errors, ErrTerraformOutputNotFound for missing keys (was ErrNilTerraformOutput for both). - IsPostExecution() helper on HookEvent encodes the before/after contract.
The previous fix added -reconfigure whenever WorkdirPathKey was set, which covers both 'workdir exists and was reused (TTL not expired)' and 'workdir was wiped and re-provisioned (TTL=0s or expired)'. This was too broad: for preserved workdirs, .terraform/ contains a valid cached backend state, and combining -reconfigure with cleanTerraformWorkspace's deletion of .terraform/environment causes tofu to prompt for workspace migration on every run. Fix: introduce WorkdirReprovisionedKey (_workdir_reprovisioned), set only when vendorToTarget (source provisioner) or SyncDir (workdir provisioner with file changes) actually ran. buildInitArgs and buildInitSubcommandArgs now check this key instead of WorkdirPathKey. Result: - TTL=0s / expired: workdir wiped -> reprovisioned -> -reconfigure added - TTL not expired: workdir preserved -> key absent -> no -reconfigure - Local workdir, files changed: synced -> -reconfigure added - Local workdir, no changes: key absent -> no -reconfigure
cleanTerraformWorkspace was designed to prevent workspace-selection prompts when different backends are used for the same component. For workdir-enabled components the backend config is always consistent (generated from the same stack config), so deleting .terraform/environment before every init is wrong. When init_run_reconfigure is set (or -reconfigure added), OpenTofu sees workspace state dirs (terraform.tfstate.d/) but no active workspace recorded in .terraform/environment and interprets this as a backend migration, producing 'Do you want to migrate all workspaces?' on every apply after the first. Skipping the cleanup for workdir components eliminates the prompt. With ttl: 0s the issue is hidden because os.RemoveAll wipes the entire workdir including .terraform/, so there is nothing to clean. Without TTL (workdir preserved) the environment file survives, cleanup deletes it, and the prompt appears on every subsequent apply.
-reconfigure combined with existing terraform.tfstate.d/ workspace state directories causes OpenTofu to prompt 'Do you want to migrate all workspaces?' even when the backend is unchanged. This happened because InitRunReconfigure:true always added -reconfigure, overriding the WorkdirReprovisionedKey guard. For workdir components with a preserved workdir (TTL not expired), the backend config is always generated deterministically from the same stack config and never changes between runs. InitRunReconfigure is now ignored for this case; -reconfigure is only added when: - the workdir was actually wiped and re-provisioned (WorkdirReprovisionedKey), or - the subcommand is 'workspace' (backend may need reinitialization) InitRunReconfigure continues to work as expected for non-workdir components.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
pkg/terraform/output/executor_test.go (1)
1517-1519: Optional test-isolation tweak: clean cache after execution too.You clear the key before running; adding a deferred delete after setup keeps global cache state fully isolated for future tests.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/terraform/output/executor_test.go` around lines 1517 - 1519, The test currently clears terraformOutputsCache for stackSlug before running but doesn't guarantee cleanup afterwards; add a deferred cache delete to fully isolate global state by calling terraformOutputsCache.Delete(stackSlug) in a defer immediately after the initial Delete (using the same stackSlug variable) so the key is removed again when the test exits.pkg/hooks/hooks_test.go (1)
346-357: Assert exact stored key/value in matching cases.
assert.NotEmptycan still pass if the wrong key is written. Prefer asserting the expected"stack/comp/label_id"entry and value directly.Based on learnings: "Test behavior, not implementation. Never test stub functions. Avoid tautological tests. Make code testable via dependency injection. No coverage theater. Remove always-skipped tests. Use
errors.Is()for error checking."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/hooks/hooks_test.go` around lines 346 - 357, Replace the loose assert.NotEmpty checks in the two tests that call makeHooks and h.RunAll (the "after-apply hook runs on after-apply event" and "hook with dot-format event matches correctly" cases) with exact assertions that the store (use getStore(h).GetData()) contains the specific key "stack/comp/label_id" and that its value equals the expected value produced by the hook; keep require.NoError on RunAll, then assert the map has the key (e.g., via assert.Contains/assertTrue) and assert.Equal for the exact value so the test verifies the behavior rather than just non-emptiness.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@pkg/hooks/hook.go`:
- Around line 27-28: The comment above the MatchesEvent method on type Hook is
truncated and missing terminal punctuation; update the doc comment for func (h
Hook) MatchesEvent(event HookEvent) bool to complete the sentence and end with a
period (for example: "If the hook has no events configured, it matches all
events to preserve backward compatibility."), ensuring the comment is a complete
sentence and ends with a period to satisfy the linter.
In `@pkg/terraform/output/executor.go`:
- Around line 340-348: In GetOutputSkipInit, the DescribeComponent call
hard-codes ProcessYamlFunctions: true which can evaluate YAML functions when
opts.SkipInit is set and authManager is nil; change the DescribeComponentParams
to set ProcessYamlFunctions to false when opts.SkipInit && authManager == nil
(matching the guard used in fetchAndCacheOutputs) or add the same conditional
guard before calling DescribeComponent so that DescribeComponent(...) uses
ProcessYamlFunctions: false in that case to avoid evaluating auth-backed YAML
functions without an authManager.
---
Nitpick comments:
In `@pkg/hooks/hooks_test.go`:
- Around line 346-357: Replace the loose assert.NotEmpty checks in the two tests
that call makeHooks and h.RunAll (the "after-apply hook runs on after-apply
event" and "hook with dot-format event matches correctly" cases) with exact
assertions that the store (use getStore(h).GetData()) contains the specific key
"stack/comp/label_id" and that its value equals the expected value produced by
the hook; keep require.NoError on RunAll, then assert the map has the key (e.g.,
via assert.Contains/assertTrue) and assert.Equal for the exact value so the test
verifies the behavior rather than just non-emptiness.
In `@pkg/terraform/output/executor_test.go`:
- Around line 1517-1519: The test currently clears terraformOutputsCache for
stackSlug before running but doesn't guarantee cleanup afterwards; add a
deferred cache delete to fully isolate global state by calling
terraformOutputsCache.Delete(stackSlug) in a defer immediately after the initial
Delete (using the same stackSlug variable) so the key is removed again when the
test exits.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 09a78763-c6e2-492d-b96d-93e20afd6d8b
📒 Files selected for processing (17)
internal/exec/terraform_execute_helpers.gointernal/exec/terraform_execute_helpers_args.gointernal/exec/terraform_execute_helpers_test.gopkg/hooks/event.gopkg/hooks/hook.gopkg/hooks/hook_test.gopkg/hooks/hooks.gopkg/hooks/hooks_test.gopkg/hooks/store_cmd.gopkg/hooks/store_cmd_nil_handling_test.gopkg/hooks/store_cmd_test.gopkg/provisioner/source/provision_hook.gopkg/provisioner/workdir/types.gopkg/provisioner/workdir/workdir.gopkg/terraform/output/executor.gopkg/terraform/output/executor_test.gopkg/terraform/output/get.go
✅ Files skipped from review due to trivial changes (1)
- pkg/provisioner/workdir/types.go
🚧 Files skipped from review as they are similar to previous changes (1)
- internal/exec/terraform_execute_helpers.go
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@pkg/terraform/output/executor.go`:
- Around line 381-384: The error return uses dynamic fmt.Errorf; replace it with
the repository's static sentinel from errors/errors.go (use the appropriate
sentinel for Terraform output failures) and attach the original err as the
cause/context (using the project's error-wrapping helper such as
errors.Wrap/WithMessage or the repo's convention) so the returned error is built
from the static sentinel plus context about component and stack; update the
return in the executor.go block that currently references component, stack, err
and fmt.Errorf, and keep the u.PrintfMessageToTUI logging as-is.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 202da824-0a30-4f8a-8558-bc6482ec27c2
📒 Files selected for processing (3)
pkg/hooks/hook.gopkg/terraform/output/executor.gopkg/terraform/output/executor_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
- pkg/hooks/hook.go
- pkg/terraform/output/executor_test.go
- Apply ProcessYamlFunctions guard to GetOutputWithOptions (mirrors fetchAndCacheOutputs) - Use errUtils.Build pattern instead of fmt.Errorf for execute errors in GetOutput and GetOutputWithOptions - Add ProcessYamlFunctions assertion to TestExecutor_GetOutputWithOptions_SkipInit
There was a problem hiding this comment.
🧹 Nitpick comments (1)
pkg/terraform/output/executor.go (1)
305-389: Consider extracting shared logic to reduce duplication.
GetOutputWithOptionsandGetOutputshare ~80% of their code (authManager validation, cache check, spinner setup, static remote state handling, execute call, caching, output extraction). A private helper could consolidate this, withGetOutputcalling it withopts = nil.Not blocking — the current structure is readable and backward-compatible. Something to consider if this area grows.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/terraform/output/executor.go` around lines 305 - 389, Extract the shared logic between GetOutputWithOptions and GetOutput into a new unexported helper (e.g., fetchTerraformOutput) that accepts parameters: atmosConfig, stack, component, output, skipCache, authContext, authManager, opts *OutputOptions; move duplicated steps (authManager type check, cache lookup using terraformOutputsCache and stackSlug, startSpinnerOrLog/defer stopSpinner, DescribeComponent via componentDescriber.DescribeComponent with ProcessYamlFunctions logic, static remote-state handling via staticRemoteStateGetter.GetStaticRemoteStateOutputs and GetStaticRemoteStateOutput, context creation, and the call to e.execute) into that helper, returning (any, bool, error), then have GetOutputWithOptions and GetOutput simply call this helper (GetOutput passes nil for opts) so all caching and error handling remains centralized while preserving existing symbols like execute, componentDescriber, terraformOutputsCache, getOutputVariable and GetStaticRemoteStateOutput.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@pkg/terraform/output/executor.go`:
- Around line 305-389: Extract the shared logic between GetOutputWithOptions and
GetOutput into a new unexported helper (e.g., fetchTerraformOutput) that accepts
parameters: atmosConfig, stack, component, output, skipCache, authContext,
authManager, opts *OutputOptions; move duplicated steps (authManager type check,
cache lookup using terraformOutputsCache and stackSlug, startSpinnerOrLog/defer
stopSpinner, DescribeComponent via componentDescriber.DescribeComponent with
ProcessYamlFunctions logic, static remote-state handling via
staticRemoteStateGetter.GetStaticRemoteStateOutputs and
GetStaticRemoteStateOutput, context creation, and the call to e.execute) into
that helper, returning (any, bool, error), then have GetOutputWithOptions and
GetOutput simply call this helper (GetOutput passes nil for opts) so all caching
and error handling remains centralized while preserving existing symbols like
execute, componentDescriber, terraformOutputsCache, getOutputVariable and
GetStaticRemoteStateOutput.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: f26bf817-f439-4b97-b9b1-4f5b55f5735e
📒 Files selected for processing (1)
pkg/terraform/output/executor.go
CodeRabbit (@coderabbitai) I'm concerned about this conclusion. I just want to be clear that hooks are used, not just from the YAML in configs, but as the core plumbing within Atmos that provisioning ties into. So the events list in YAML isn't the exhaustive list. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chainLet me look at how hooks are constructed and used internally in the provisioner plumbing. 🏁 Script executed: Length of output: 10259 Let me dig deeper into how provisioners tie into the hook system vs user YAML hooks. 🏁 Script executed: Length of output: 32394
After looking at the code, there are actually two entirely separate hook systems in Atmos, and the PR's System 1 — User YAML hooks (
System 2 — Internal provisioner hooks (
The new The original "Bug 3" concern was narrowly about user-configured YAML hooks ignoring their own That said, your clarification is a useful call-out for documentation/comments — worth making explicit in the ✏️ Learnings added
🧠 Learnings used |
|
🧠 Learnings used✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
pkg/terraform/output/executor.go (1)
239-329: Consider collapsingGetOutputintoGetOutputWithOptionsto avoid drift.These two paths now duplicate most orchestration logic (cache, describe, static state, execute, UI/error handling). A single implementation would reduce future divergence risk.
Also applies to: 331-435
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/terraform/output/executor.go` around lines 239 - 329, GetOutput duplicates most orchestration logic found in GetOutputWithOptions; refactor by consolidating the shared flow (cache check using stackComponentKey, componentDescriber.DescribeComponent, staticRemoteStateGetter handling, execute call, caching, getOutputVariable, and UI/error handling) into a single canonical implementation (e.g., move that flow into GetOutputWithOptions or a private helper like fetchTerraformOutput) and have GetOutput delegate to it with default options (preserving behavior around skipCache, authContext, authManager). Ensure references to componentDescriber, staticRemoteStateGetter, execute, terraformOutputsCache, getOutputVariable, and UI/error wrapping remain identical and only option-specific differences are handled by parameters to the shared function so both original call sites (including the other block at lines 331-435) call the same implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@pkg/terraform/output/config.go`:
- Line 172: The call to provWorkdir.BuildPath currently passes baseComponent
which is used as the fallback when atmos_component is absent, causing wrong
workdir for instance-specific components; change the argument so BuildPath
receives the instance component (e.g., instanceComponent or
instance.Name/instance.Component as used in this file) instead of baseComponent
(keep basePath, componentType, stack, sections unchanged) so the fallback uses
the instance component value when atmos_component is not present.
---
Nitpick comments:
In `@pkg/terraform/output/executor.go`:
- Around line 239-329: GetOutput duplicates most orchestration logic found in
GetOutputWithOptions; refactor by consolidating the shared flow (cache check
using stackComponentKey, componentDescriber.DescribeComponent,
staticRemoteStateGetter handling, execute call, caching, getOutputVariable, and
UI/error handling) into a single canonical implementation (e.g., move that flow
into GetOutputWithOptions or a private helper like fetchTerraformOutput) and
have GetOutput delegate to it with default options (preserving behavior around
skipCache, authContext, authManager). Ensure references to componentDescriber,
staticRemoteStateGetter, execute, terraformOutputsCache, getOutputVariable, and
UI/error wrapping remain identical and only option-specific differences are
handled by parameters to the shared function so both original call sites
(including the other block at lines 331-435) call the same implementation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 88d1ffc3-a6f8-4d4d-805d-0f9263852376
📒 Files selected for processing (15)
cmd/terraform/deploy.gopkg/hooks/event.gopkg/hooks/hook.gopkg/hooks/hook_test.gopkg/hooks/hooks_test.gopkg/hooks/store_cmd_test.gopkg/provisioner/source/provision_hook.gopkg/provisioner/source/provision_hook_test.gopkg/provisioner/source/source.gopkg/provisioner/workdir/workdir.gopkg/provisioner/workdir/workdir_test.gopkg/terraform/output/config.gopkg/terraform/output/config_test.gopkg/terraform/output/executor.gopkg/terraform/output/executor_test.go
✅ Files skipped from review due to trivial changes (2)
- pkg/hooks/hooks_test.go
- pkg/hooks/hook_test.go
🚧 Files skipped from review as they are similar to previous changes (5)
- pkg/hooks/event.go
- pkg/provisioner/source/provision_hook.go
- pkg/hooks/store_cmd_test.go
- pkg/terraform/output/config_test.go
- pkg/provisioner/workdir/workdir.go
3c0e748
into
cloudposse:main
|
These changes were released in v1.216.0-rc.1. |
Summary
Fixes a cluster of bugs in
provision.workdir.enabled: truemode covering file generation, hook dispatch, store hook correctness, and repeated-apply terraform init prompts.Bug 1 –
generate:writes to base component directory instead of workdirresolveAndProvisionComponentPathcalledautoGenerateComponentFilesbeforeprovisionComponentSource. Generated files (e.g.locals_override.tf) were written tocomponents/terraform/<component>/instead of the JIT workdir.Fix: swap call order — provision source first, then generate into the returned (workdir) path.
Bug 2 – hooks and output executor used base component directory
extractComponentPathalways returned the base component directory because_workdir_pathis a runtime key absent from freshly-described sections. Hooks callingterraform outputwould fail with "no such file or directory" when trying to writebackend.tf.jsonto a path that doesn't exist.Fix: check
provision.workdir.enabledin sections and rebuild the deterministic workdir path viaworkdir.BuildPath.Bug 3 – hooks fired on every event regardless of
events:listRunAllhad no event matching — all hooks ran regardless of theirevents:list. YAML uses hyphens (after-terraform-apply) but GoHookEventconstants use dots (after.terraform.apply).Fix: added
MatchesEvent()with hyphen→dot normalisation. Hooks with noevents:field match all events to preserve backward compatibility with configs written before event filtering existed.Bug 4 – store hook used wrong output getter and wrong error sentinels
The store hook always used
GetOutput(which runsterraform init) regardless of when it fires. Running init after apply with a closed stdin triggers state-migration prompts. Additionally, errors usedErrNilTerraformOutputfor both retrieval failures and missing keys, and included no context about which hook or event caused the failure.Fix:
RunEnow selects the getter based on the event —after-events useGetOutputSkipInit(workdir already initialised);before-events useGetOutput(init may not have run yet).IsPostExecution()helper onHookEventencodes the contract. Error messages now include hook name, event, output key, component, and stack. Correct sentinels:ErrTerraformOutputFailedfor retrieval errors,ErrTerraformOutputNotFoundfor missing keys.Bug 5 – "Do you want to migrate all workspaces?" prompt on every apply
This was caused by three interacting problems:
-reconfigureadded wheneverWorkdirPathKeywas set —WorkdirPathKeyis set for both a preserved workdir (TTL not expired) and a wiped/re-provisioned workdir (TTL=0s or expired). Checking it unconditionally added-reconfigureeven when.terraform/was intact.init_run_reconfigure: trueoverriding the preserved-workdir guard — even after scoping-reconfiguretoWorkdirReprovisionedKey, the globalInitRunReconfigureflag bypassed the check and always added-reconfigure.cleanTerraformWorkspacedeleting.terraform/environmentfor workdir components — this function was designed for backend-switching on non-workdir components. For workdir components it deleted the active workspace record before every init, causing OpenTofu to see orphanedterraform.tfstate.d/<workspace>/directories with no active workspace and prompt for migration.When combined:
-reconfiguretells OpenTofu to ignore the saved backend and treat init as fresh. A fresh-init with existing workspace state dirs triggers the migration prompt even when the backend is unchanged.Fix (three parts):
WorkdirReprovisionedKey(_workdir_reprovisioned), set only byvendorToTarget(source wiped) orSyncDirwith file changes (workdir synced). This is the correct signal that.terraform/was actually cleared.InitRunReconfigure— the backend is always generated deterministically from the same stack config and never changes between runs.-reconfigureis only added whenWorkdirReprovisionedKeyis set or the subcommand isworkspace.cleanTerraformWorkspacefor workdir-enabled components — the backend is consistent, so there is no reason to clear the workspace record.Tested end-to-end
Full producer → store → consumer pipeline:
null-labelapplies with JIT workdir +generate:overrideafter-terraform-applyhook reads.idoutput and writes it to Redis (no init re-run, no migration prompt)consumerreads the value via!store local/redis null-label label_id, injects it into its owngenerate:template, applies successfullyinit_run_reconfigure: trueand with or withoutttl: "0s"Reproduction
This worked successfully for the deployment that I was initially having this issue with. Local reproduction below.
Test plan
TestHook_MatchesEvent— hyphen/dot formats, no match, nil/empty events (backward compat), multiple eventsTestRunAll_EventFiltering— store called/skipped based on event matchingTestExecutor_GetOutputWithOptions_SkipInit—terraform initNOT called whenSkipInit: trueTestBuildInitArgs_ReconfigureWhenWorkdirReprovisioned—-reconfigureadded when workdir wipedTestBuildInitArgs_NoReconfigureWhenWorkdirPreserved—-reconfigureNOT added for preserved workdirTestBuildInitArgs_NoReconfigureWhenWorkdirPreserved_InitRunReconfigureIgnored— globalInitRunReconfigure: truedoes not override the preserved-workdir guardTestBuildInitArgs_ReconfigureForNonWorkdir_InitRunReconfigure—InitRunReconfigurestill works for non-workdir componentsTestPrepareInitExecution_SkipsCleanWorkspaceForWorkdir—.terraform/environmentpreserved for workdir componentsTestPrepareInitExecution_CleansWorkspaceForNonWorkdir—.terraform/environmentstill cleaned for non-workdir componentsTestIsWorkdirEnabled/TestExtractComponentPath/workdir_enabled_*— workdir path resolutionpkg/hooks,pkg/terraform/output,internal/exectest suites passCloses #2308
Closes #2307
Summary by CodeRabbit
New Features
Improvements
Tests