Repository navigation
docs(cloudformation): phase 1 docs, examples, and fix-log (split from #2999) #3156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Erik Osterman (Cloud Posse) (osterman)
wants to merge
8
commits into
osterman/cfn-wiring-gap-fixes
from
osterman/cfn-phase1a-docs
+2,924
−7
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
4155941
docs(cloudformation): phase 1 docs, examples, and fix-log (split from…
osterman 5842c53
docs(cloudformation): mark template field as conditionally required
osterman f79e487
docs(cloudformation): defer command publication until implementation
osterman df7dfbe
docs: align CloudFormation preview with site metadata
osterman f4edc2a
test(docs): cover draft release-index filtering
osterman bb07709
test(docs): run draft filtering regression during website builds
osterman 303e5fb
docs: clarify publish-only effects on existing stacks
osterman 2f393e7
docs: clarify CloudFormation no-op results and template paths
osterman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
66 changes: 66 additions & 0 deletions
66
docs/fixes/2026-08-31-source-provisioner-single-file-misdetection.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| # Fix: source provisioner no longer misdetects one-file module directories as template sources | ||
|
|
||
| **Date:** 2026-08-31 | ||
|
|
||
| **Implementation status:** Pending in the documentation-only [PR #3156](https://github.com/cloudposse/atmos/pull/3156). | ||
| The implementation and regression tests are in the follow-up [PR #3157](https://github.com/cloudposse/atmos/pull/3157). | ||
| This report preserves the development findings and validation of that implementation; | ||
| it does not mean the fix is present in the documentation-only revision. | ||
|
|
||
| ## Summary | ||
|
|
||
| `VendorSource`'s single-file detection — added to support bare template URIs like | ||
| `source: {uri: https://.../dns.yaml}` — inspected only the shape of the downloaded staging | ||
| directory (exactly one entry, not a directory) to decide whether to write the result as a file | ||
| or a directory. That heuristic misfired for any Git/S3/archive source whose resolved | ||
| subdirectory happened to contain exactly one file, writing a *file* at the target path instead | ||
| of a directory. Every downstream consumer that assumes a JIT-provisioned workdir is a directory | ||
| then failed. | ||
|
|
||
| ## Context | ||
|
|
||
| Discovered while investigating a persistently-failing CI shard on an unrelated branch | ||
| (`osterman/cfn-phase4-migration-graduation`). The failing tests | ||
| (`TestJITSource_WorkdirWithLocalComponent`, its `_AllSubcommands` variants, | ||
| `TestJITSource_GenerateVarfile`, `TestJITSource_GenerateBackend`, | ||
| `TestTerraformOutputJITWorkdirFromSource`) were initially assumed to be a flaky, pre-existing | ||
| race condition unrelated to the branch under review — the affected test file had zero diff | ||
| against `main`. Re-running the exact same test locally 5/5 times reproduced the failure | ||
| deterministically, which ruled out a race and prompted a real root-cause investigation instead | ||
| of continuing to report it as an environment flake. | ||
|
|
||
| ## Proposed implementation (PR #3157) | ||
|
|
||
| - `pkg/provisioner/source/vendor.go`: excluded Git, S3, and archive sources from the | ||
| `singleFileInDir` single-file heuristic in `VendorSource`. These getters always unpack to a | ||
| directory — even a directory containing just one file (e.g., | ||
| `github.com/cloudposse/terraform-null-label//exports`, which only has `context.tf`, or a | ||
| module tarball whose sole member is `main.tf`) — so they must never be routed through | ||
| `copySingleFileToTarget`. | ||
| - `pkg/vendor/uri.go`: added `IsArchiveURI`, matching go-getter's own directory-producing | ||
| decompressor extensions (`.tar`, `.tar.gz`/`.tgz`, `.tar.bz2`/`.tbz2`, `.tar.xz`/`.txz`, | ||
| `.tar.zst`/`.tzst`, `.zip`). Deliberately excludes the single-compressed-file formats (bare | ||
| `.gz`, `.bz2`, `.xz`, `.zst`), which legitimately decompress to exactly one file and must keep | ||
| going through the single-file path. | ||
| - `pkg/provisioner/source/vendor_test.go`: added | ||
| `TestVendorSourceArchiveWithOneFileIsNotMisdetectedAsSingleFileURI`, a regression test that | ||
| packages a one-file tarball and asserts the target is a directory, not a file. | ||
| - `pkg/vendor/uri_test.go`: added `TestIsArchiveURI` covering the directory-producing and | ||
| single-compressed-file extension cases. | ||
|
|
||
| ## Development validation (implementation revision) | ||
|
|
||
| - Reverted the fix locally and confirmed the new regression test fails with the exact | ||
| `"...: not a directory"` signature seen in CI, then restored the fix and confirmed it passes. | ||
| - `go test ./tests -run 'TestJITSource_WorkdirWithLocalComponent$|TestJITSource_WorkdirWithLocalComponent_AllSubcommands|TestJITSource_GenerateVarfile|TestJITSource_GenerateBackend|TestTerraformOutputJITWorkdirFromSource' -v`: all 5 previously-failing tests now pass. | ||
| - `go test ./pkg/provisioner/source/... ./pkg/vendor/...`: pass. | ||
| - `atmos lint --changed`: 0 issues. | ||
| - Broader local sweep (`TestJITSource*`, `TestVendor*`, `TestCLICommands`) run to check for | ||
| regressions: only pre-existing, unrelated failures remained | ||
| (`atmos_vendor_pull#01`/`atmos_vendor_pull_oci`, both blocked by this sandbox having no network | ||
| route to `ghcr.io`'s anonymous OCI auth — confirmed via the captured `UNAUTHORIZED` error, not | ||
| caused by this change). | ||
|
|
||
| ## Follow-ups | ||
|
|
||
| None. | ||
164 changes: 164 additions & 0 deletions
164
docs/fixes/2026-09-09-cfn-apply-publish-only-gating-and-errors.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| # Fix: `aws/cloudformation apply` no longer crashes on a publish-only target, and gains a friendlier "stack not found" error | ||
|
|
||
| **Date:** 2026-09-09 | ||
|
|
||
| **Implementation status:** Pending in the documentation-only [PR #3156](https://github.com/cloudposse/atmos/pull/3156). | ||
| The implementation and regression tests are in the follow-up [PR #3157](https://github.com/cloudposse/atmos/pull/3157). | ||
| This report preserves the development findings and validation of that implementation; | ||
| it does not mean the fix is present in the documentation-only revision. | ||
|
|
||
| ## Summary | ||
|
|
||
| `atmos aws cfn apply <component> -s <stack> --target <publish-only-target>` crashed with a raw, | ||
| unwrapped AWS SDK error (`ValidationError: Stack [...] does not exist`) when the named stack was | ||
| absent and the selected provision target did not deploy a stack directly — a `kind: aws/s3` target selected via | ||
| `--target`, or any other non-`aws/cloudformation` target kind (e.g. `kind: git`). `runApply` | ||
| unconditionally ran the stack-policy, termination-protection, and outputs follow-up steps after | ||
| `deliverApply` returned, regardless of whether the direct CloudFormation path had returned a | ||
| result without error. Fixed by gating those three follow-up steps behind `deliverApply`'s | ||
| `*changeSetResult` return value. | ||
| Separately, a user explicitly asked for friendlier error messages after hitting the raw AWS error | ||
| verbatim; a narrowly-scoped helper now recognizes AWS's "stack does not exist" validation-error | ||
| shape and adds an explanation + actionable hint via the error builder, reusing this package's | ||
| existing `isStackNotFoundError` pattern-recognition helper. | ||
|
|
||
| ## Context | ||
|
|
||
| `pkg/component/aws/cloudformation/provision.go`'s `deliverApply` resolves the selected provision | ||
| target for an `apply` and has three possible outcomes: | ||
|
|
||
| 1. `kind: aws/cloudformation` (the default/implicit target) — uses `deployDirect` and its | ||
| changeset flow, returning a non-nil `*changeSetResult` on success, including a no-op. | ||
| 2. `kind: aws/s3` selected explicitly (e.g. `--target artifacts`) — publish-only: uploads the | ||
| template to S3 and returns immediately with `result == nil`. No stack is created or touched. | ||
| 3. Any other kind (e.g. `kind: git`) — packages through S3, then delivers via the generic target | ||
| registry (`target.Deliver`), also never touching a direct stack. Returns `result == nil`. | ||
|
|
||
| `pkg/component/aws/cloudformation/executor.go`'s `runApply` called `setStackPolicy`, | ||
| `applyTerminationProtection`, and `describeStackOutputs` unconditionally after `deliverApply` | ||
| returned without error — regardless of which of the three outcomes had occurred. For outcomes 2 | ||
| and 3, the target does not deploy the stack directly. If the named stack is absent, the | ||
| stack-scoped calls fail; if an unrelated stack with that name already exists, policy or | ||
| termination-protection calls may change its settings, and output lookup may return its outputs. | ||
| The confirmed live repro: a component with | ||
| `termination_protection: true` and an `aws/s3` provision target named `artifacts`, never yet | ||
| deployed as a direct stack, running `atmos aws cfn apply <component> -s <stack> --target | ||
| artifacts`, hit: | ||
|
|
||
| ```text | ||
| Error: aws/cloudformation API call failed: operation error CloudFormation: UpdateTerminationProtection, https response error | ||
| StatusCode: 400, ... api error ValidationError: Stack [<name>] does not exist | ||
| ``` | ||
|
|
||
| This error is a raw AWS SDK error string with no explanation or actionable next step, following | ||
| this package's dominant (but not universal) `fmt.Errorf("%w: %w", errUtils.ErrAwsCloudFormationAPICallFailed, err)` | ||
| wrapping pattern — contrasted with the curated errors elsewhere in the same package (e.g. | ||
| `delete.go`'s termination-protection/retain-resources gates) that use the full error-builder | ||
| pattern with an explanation and hint. | ||
|
|
||
| ## Changes | ||
|
|
||
| **Proposed implementation (PR #3157).** | ||
|
|
||
| **Bug 1 (primary) — `pkg/component/aws/cloudformation/executor.go`, `runApply` (~line 397-448):** | ||
|
|
||
| After the error check, `result != nil` means the direct CloudFormation path returned a result | ||
| without an error, including when the operation was a no-op. It does not prove that a stack | ||
| deploy occurred. Verified by reading `deliverApply`, `deployDirect`, `createChangeSet`, and | ||
| `waitForChangeSet` in full: | ||
|
|
||
| - `deliverApply` only ever returns a non-nil `result` from the `selected.Kind == | ||
| cfg.CloudFormationComponentType` branch (`deployDirect`'s outcome); both other branches | ||
| (`aws/s3` publish-only, and the generic external-target delivery) explicitly return `nil` for | ||
| `result`. | ||
| - `deployDirect` returns the non-nil result from `createChangeSet` immediately when | ||
| `result.NoOp` is true, without executing the changeset. Otherwise it executes the changeset | ||
| and waits for the stack operation to complete. Both successful paths return a non-nil result; | ||
| failures return a non-nil error. | ||
| - `runApply` already checks `if err != nil { return summary, err }` *before* looking at `result`, | ||
| so by the time the code reaches the `result` check, any path that could produce `(nil, non-nil | ||
| err)` has already returned. At this point, `result != nil` identifies a direct CloudFormation | ||
| result without an error, including a no-op — no separate boolean needed. | ||
|
|
||
| `runApply` now returns immediately after merging `deliverApply`'s summary when `result == nil`, | ||
| skipping `setStackPolicy`, `applyTerminationProtection`, and `describeStackOutputs` entirely for | ||
| the publish-only and external-target outcomes. The direct-deploy path's behavior (all three | ||
| follow-up steps still run, in the same order) is unchanged. | ||
|
|
||
| **Bug 2 (secondary, narrowly scoped) — new helper `wrapAPICallError` in | ||
| `pkg/component/aws/cloudformation/changeset.go`, applied at 3 call sites:** | ||
|
|
||
| Rather than rewrapping every `fmt.Errorf("%w: %w", errUtils.ErrAwsCloudFormationAPICallFailed, | ||
| err)` call site in the package (a much larger effort spanning `executor.go`, `validate.go`, | ||
| `delete.go`, `stackset.go`, `backend.go`, `drift.go`, `get.go`, `list.go`, `observability.go` — | ||
| out of scope here and a real regression risk), this fix adds one small, targeted helper next to | ||
| the package's existing `isStackNotFoundError` pattern-recognition function (already used by | ||
| `changeset.go`, `changeset_verbs.go`, and `events.go` to special-case this exact AWS error shape): | ||
|
|
||
| ```go | ||
| func wrapAPICallError(stackName string, err error) error { | ||
| if !isStackNotFoundError(err) { | ||
| return fmt.Errorf(wrapFmt, errUtils.ErrAwsCloudFormationAPICallFailed, err) | ||
| } | ||
| return errUtils.Build(errUtils.ErrAwsCloudFormationAPICallFailed). | ||
| WithCause(err). | ||
| WithExplanationf("Stack %q doesn't exist yet.", stackName). | ||
| WithHint("Check `--target`/`-s`/component name, or run `apply` without `--target` first if you need to create the stack directly."). | ||
| Err() | ||
| } | ||
| ``` | ||
|
|
||
| `WithCause` preserves `errors.Is` matching against both the sentinel and the original AWS error, | ||
| per the error builder's documented contract. Unrecognized AWS error shapes fall through to the | ||
| existing plain wrap unchanged — this deliberately avoids inventing a hint for error shapes this | ||
| fix hasn't specifically verified. | ||
|
|
||
| Applied at the 3 call sites most directly implicated by the confirmed bug (all three are | ||
| stack-scoped follow-up calls that can legitimately target a non-existent stack): | ||
|
|
||
| - `pkg/component/aws/cloudformation/validate.go`, `setStackPolicy` | ||
| - `pkg/component/aws/cloudformation/validate.go`, `applyTerminationProtection` | ||
| - `pkg/component/aws/cloudformation/output.go`, `describeStackOutputs` (also used by the | ||
| standalone `output` verb, not just `runApply`'s end-of-deploy summary) | ||
|
|
||
| `output.go` no longer needs the `fmt` or `errUtils` imports after this change and had both | ||
| removed. | ||
|
|
||
| ## Validation | ||
|
|
||
| The documentation-only correction was checked against PR #3157 commit `a2feba08b` and the | ||
| CloudFormation CLI configuration reference. The fix-log validator, MDX parser, and | ||
| `git diff --check` passed. No implementation tests were rerun for this prose-only correction. | ||
|
|
||
| **Development validation (implementation revision).** | ||
|
|
||
| - `go build ./...` — passes. | ||
| - `go vet ./pkg/component/aws/cloudformation/...` — passes. | ||
| - `go test ./pkg/component/aws/cloudformation/...` — passes (full package, including new tests). | ||
| - New/updated tests in `pkg/component/aws/cloudformation/executor_test.go` and | ||
| `pkg/component/aws/cloudformation/changeset_test.go`: | ||
| - `TestRunApply_PublishOnlyTarget_SkipsPostDeploySteps` — regression test for Bug 1. Uses a | ||
| `MockCloudFormationClient` with **zero** `EXPECT()` calls set, so any | ||
| `UpdateTerminationProtection`/`SetStackPolicy`/`DescribeStacks` call fails the test outright. | ||
| The `stackSpec` sets both `TerminationProtection: true` and a non-empty `StackPolicyBody` to | ||
| prove the gate actively skips those steps rather than merely having nothing to do. | ||
| - `TestRunApply_Success`, `TestRunApply_SetsStackPolicy`, `TestRunApply_TerminationProtectionError`, | ||
| `TestRunApply_DescribeOutputsError` (pre-existing, unmodified) — continue to pass unchanged, | ||
| proving the direct-deploy path still runs all three follow-up steps exactly as before. | ||
| - `TestWrapAPICallError_StackNotFound` — asserts the wrapped error matches both | ||
| `errUtils.ErrAwsCloudFormationAPICallFailed` and the original AWS error via `errors.Is`, and | ||
| carries a hint (via `cockroachdb/errors.GetAllHints`) mentioning `--target` and `apply`. | ||
| - `TestWrapAPICallError_OtherError_PlainWrap` — asserts an unrecognized AWS error still matches | ||
| the sentinel and original error, but carries **no** hint. | ||
| - `atmos lint --changed` (via `GOTOOLCHAIN=go1.26.6 ./custom-gcl run --config=.golangci.yml | ||
| --allow-serial-runners --new-from-rev=origin/main`, working around a known Homebrew-Go/go.mod | ||
| toolchain mismatch in this environment): zero findings on any file touched by this fix. Three | ||
| pre-existing `dupl` findings surfaced in `executor_test.go` (among three untouched, already | ||
| near-identical `TestRunApply_*Error` tests) and one unrelated `godot` finding in | ||
| `internal/exec/yaml_func_terraform_output.go` — both pre-date this change (confirmed via `git | ||
| diff origin/main`, which shows the whole `executor_test.go` file as new relative to `origin/main` | ||
| since this feature branch hasn't merged yet) and are out of this fix's scope. | ||
|
|
||
| ## Follow-ups | ||
|
|
||
| None. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| # Fix: `aws/cloudformation` base path no longer collapses to the project root when unset | ||
|
|
||
| **Date:** 2026-09-09 | ||
|
|
||
| **Implementation status:** Pending in the documentation-only [PR #3156](https://github.com/cloudposse/atmos/pull/3156). | ||
| The implementation and regression tests are in the follow-up [PR #3157](https://github.com/cloudposse/atmos/pull/3157). | ||
| This report preserves the development findings and validation of that implementation; | ||
| it does not mean the fix is present in the documentation-only revision. | ||
|
|
||
| ## Summary | ||
|
|
||
| `pkg/config/config.go`'s absolute-path resolution for `aws/cloudformation` components joined | ||
| `atmosConfig.Components.CloudFormation.BasePath` onto the base path with no fallback for the Go | ||
| zero-value (empty string). Because every pre-existing `atmos.yaml` never declares a | ||
| `components."aws/cloudformation"` section, `BasePath` was always empty in practice, and the join | ||
| collapsed to the bare repository root instead of the documented default | ||
| `components/cloudformation/`. Atmos then looked for `<repo-root>/<component>/template.yaml` | ||
| instead of `<repo-root>/components/cloudformation/<component>/template.yaml`, producing a | ||
| confusing "no such file or directory" error instead of working out of the box. | ||
|
|
||
| ## Context | ||
|
|
||
| Found via a real-AWS field test against an external, pre-existing repo (infra-live) that had | ||
| never configured `components."aws/cloudformation"` — the scenario every existing atmos.yaml is | ||
| actually in, since that section did not exist before CloudFormation support was added. The exact | ||
| same bug shape was already found and fixed for `components.container.base_path` a few lines above | ||
| this block in the same function (`AtmosConfigAbsolutePaths`), with an explicit defensive default | ||
| and an explanatory comment. `aws/cloudformation` was added later and missed the same treatment. | ||
|
|
||
| A correct, independent fallback already existed in | ||
| `pkg/component/aws/cloudformation/config.go` (`DefaultConfig()`, returning | ||
| `Config{BasePath: "components/cloudformation"}`) and is already consumed by | ||
| `ComponentProvider.GetBasePath()` in `pkg/component/aws/cloudformation/cloudformation.go` — but | ||
| neither is on the path that `pkg/config/config.go` uses to compute | ||
| `CloudFormationDirAbsolutePath`, so that resolution had no fallback at all. | ||
|
|
||
| ## Proposed implementation (PR #3157) | ||
|
|
||
| - `pkg/config/config.go` (`AtmosConfigAbsolutePaths`): before joining | ||
| `atmosConfig.Components.CloudFormation.BasePath` into an absolute path, default it to | ||
| `"components/cloudformation"` when empty, mirroring the Container fix's style and comment | ||
| tone. Switched the `filepath.Abs` call to the same `absPathOrError` helper the Container and | ||
| Vendor/Workflows blocks already use, for consistent error wrapping. | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| - `pkg/config/config_test.go` (`TestAtmosConfigAbsolutePaths`): added two sub-tests mirroring the | ||
| existing Vendor/Workflows empty-base-path sub-test — | ||
| `computes cloudformation absolute path from empty base_path default` (asserts | ||
| `CloudFormationDirAbsolutePath` resolves to `<root>/components/cloudformation` and that the | ||
| in-place default is applied) and `computes cloudformation absolute path from explicit base_path | ||
| override` (asserts an explicit `BasePath` is respected and not clobbered by the default). | ||
|
|
||
| Verified no other call site double-applies or conflicts with this default: | ||
| `pkg/component/aws/cloudformation/cloudformation.go`'s `GetBasePath()` is an independent consumer | ||
| with its own empty-check fallback to `DefaultConfig()`, not reached from | ||
| `AtmosConfigAbsolutePaths`. Other readers of `Components.CloudFormation.BasePath` (e.g. | ||
| `pkg/utils/component_path_utils.go`, `internal/exec/describe_affected_changed_files_index.go`, | ||
| `internal/exec/describe_stacks.go`) run after `AtmosConfigAbsolutePaths` has already defaulted | ||
| the field in place, exactly as they already do for `Components.Container.BasePath`. | ||
|
|
||
| ## Development validation (implementation revision) | ||
|
|
||
| - `go build ./...` — passes. | ||
| - `go test ./pkg/config/...` — passes, including the two new sub-tests and the full existing | ||
| suite (no regressions). | ||
| - `GOTOOLCHAIN=go1.26.6 ./custom-gcl run --config=.golangci.yml --new-from-rev=origin/main | ||
| pkg/config/...` — 0 issues on the changed lines. (The repo-wide `atmos lint --changed` | ||
| currently fails on an unrelated, pre-existing typecheck error in | ||
| `pkg/component/aws/cloudformation/delete.go`, which is mid-edit in a separate, concurrent fix | ||
| and out of scope here.) | ||
|
|
||
| ## Follow-ups | ||
|
|
||
| None. | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.