feat(project): tear down the stack on a deploy of an empty project, behind --yes - #2089
feat(project): tear down the stack on a deploy of an empty project, behind --yes#2089notgitika wants to merge 2 commits into
Conversation
a1b6cbf to
c255cdd
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## refactor #2089 +/- ##
============================================
- Coverage 97.36% 97.34% -0.02%
============================================
Files 424 424
Lines 25571 25706 +135
============================================
+ Hits 24897 25024 +127
- Misses 674 682 +8 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
1a13ff1 to
dfb7780
Compare
Two review findings from #2058, both cases of deploy trusting something it had not checked. A synthesized template with no resources makes the CDK Toolkit *delete* an existing stack of that name and return as though it deployed. #2058 caught that after the fact, by which point the stack was already gone. Check the resource count before handing the assembly to the Toolkit instead. Stack selection matched on the target-name tag alone, never on the account and region the artifact was synthesized for. Those derive from the same target today and so cannot disagree, but nothing enforced it, and the Toolkit deploys where the artifact's environment points rather than where the tag says. Both fields were also being stripped on read, since the manifest schema declared neither. stackArtifactIdForTarget becomes stackArtifactForTarget, returning the template path alongside the id so the resource check needs no second read of the manifest.
378208a to
f20c060
Compare
|
Claude Security Review: no high-confidence findings. (run) |
| ): void { | ||
| // An artifact with no environment is environment-agnostic: it deploys into | ||
| // whatever the credentials resolve to, which the account preflight checked. | ||
| if (environment === undefined) return; |
There was a problem hiding this comment.
For my own understanding: wouldn't CDK write unknown-account and unknown-region if env-agnostic? The comment on L9 says thats the case, but this comment says no env = env-agnostic.
I kind of think we want the opposite. If I clear all of my resources, then deploy, I'd expect that to delete the stack. Without this functionality, how do I destroy the AWS resources I've created with the CLI? I could see an alternative where we offer an explicit |
Discussed with folks offline we are going with a confirmation + flag once the condition is user runs deploy when spec is empty. this will just inform the user that we are also going to delete their stack. ( |
…ith --yes The resource check added in the previous commit never fired. CDK writes an AWS::CDK::Metadata resource into every stack unless version reporting is disabled, so a project whose spec declares nothing still synthesizes a template with one resource in it: Object.keys(Resources).length === 0 is unreachable through the CLI. Verified against eight synthesized assemblies. Count only the resources the project asked for, and the check becomes real. Making it real needs somewhere for that deploy to go. main handles it -- an empty project plus deploy destroys the stack -- and refactor dropped that along the way, so refusing outright would trade a silent delete for a regression. Route it to an explicit toolkit.destroy() instead, gated on --yes, and report it as "Removed" rather than "Deployed" since the stack no longer exists. Destroying explicitly rather than deploying the empty template is what makes the outcome reportable: destroy fails loudly when the stack cannot be removed, where a deploy of an empty template succeeds either way. The existing guard in performCdkOperation stays as the backstop for reaching that state some other way. Two states are distinguished before anything is destroyed, both by probing CloudFormation for the stack: nothing to deploy and no stack to remove is a project that needs a resource added, not a teardown. Detection reads the synthesized template rather than counting spec collections the way main does, so a resource type added to the spec later is covered without anyone remembering to extend a list. Also corrects the comment nico flagged: an artifact with no environment is not the env-agnostic case -- CDK spells that out as aws://unknown-account/unknown-region -- it is a manifest field the cloud assembly schema leaves optional.
|
Claude Security Review: no high-confidence findings. (run) |
Hweinstock
left a comment
There was a problem hiding this comment.
one question about the flag naming, and some internal questions about toolkit lib behavior for my own understanding.
| flags: [ | ||
| flag("target", "name of the aws-targets.json entry to deploy", z.string().default("default")), | ||
| flag( | ||
| "yes", |
There was a problem hiding this comment.
to me --yes implies, answer yes to the prompt, but here there is no prompt and its instead signaling a bypassing of safety measures (kind of like git push --force).
What do you think of naming this to force?
| // with nothing in it as an instruction to delete the stack, and reports that | ||
| // as an ordinary successful deploy. | ||
| if ((await countDeployableResources(this.json, assemblyDirectory, artifact)) === 0) { | ||
| return yield* this.teardown({ project, artifact, input, options }); |
There was a problem hiding this comment.
where does the need for an explicit teardown come in? I thought the behavior of toolkit lib was that it already deleted the stack if it was empty.
| // the synthesized app, so today they cannot disagree. Checking anyway keeps a | ||
| // correct tag from carrying a stack into the wrong account or region: the Toolkit | ||
| // deploys where the artifact's environment points, not where the tag says. | ||
| function assertEnvironmentMatches( |
There was a problem hiding this comment.
the stack's environment derive from the same target in the synthesized app, so today they cannot disagree.
What could change that would make them disagree? I'm wondering if we can avoid this complexity, or if there is real concern we could deploy to the wrong environment.
Summary
Deploying a project that declares no resources tore down its stack silently. This makes that outcome explicit, gated, and reported — and fixes the stack-selection gap found in the same review.
A deploy of nothing was a silent delete. The CDK Toolkit reads a template with no resources as an instruction to delete an existing stack of that name, and reports the run as an ordinary successful deploy.
#2058caught it after the fact, by which point the stack was gone.The first attempt at a guard here was dead code. CDK writes an
AWS::CDK::Metadataresource into every stack unless version reporting is disabled, so a project whose spec declares nothing still synthesizes a template with one resource in it —Object.keys(Resources).length === 0is unreachable through the CLI. Verified against eight real synthesized assemblies. Counting only the resources the project asked for makes the check fire.Making it fire needs somewhere for that deploy to go.
mainsupports this: empty the project, deploy, and the stack is destroyed. The refactor dropped it, so refusing outright would trade a silent delete for a regression. Instead the deploy routes to an explicittoolkit.destroy(), gated on--yes:Three states are distinguished, all before anything is destroyed:
--yesproject add runtimeDestroying explicitly rather than deploying the empty template is what makes the outcome reportable:
destroyfails loudly when the stack cannot be removed, where a deploy of an empty template succeeds either way. The empty-outputs guard inperformCdkOperationstays as the backstop for reaching that state some other way.Detection reads the synthesized template rather than counting spec collections the way
maindoes, so a resource type added to the spec later is covered without anyone remembering to extend a list.Stack selection also matched on the target-name tag alone, never on the account and region the artifact was synthesized for. Those derive from the same target today and so cannot disagree, but nothing enforced it, and the Toolkit deploys where the artifact's environment points rather than where the tag says. Both fields were being stripped on read, since the manifest schema declared neither.
stackArtifactIdForTargetbecomesstackArtifactForTarget, returning the template path and the deployed stack name alongside the id. The stack name is derived asproperties.stackName ?? artifactId, which is CDK's own derivation — so the name handed to CloudFormation is the one the Toolkit would have used.Testing
bun test src/— 1985 pass, 0 fail against these files. Unit coverage for each state in the table, for the metadata-only template that made the original guard dead, and for the ordering that makes the guards meaningful: every check runs before the Toolkit is called, so a bad target or an unconfirmed teardown fails without touching AWS.probeStackis covered for a present stack, a stack mid-rollback, an absent stack, an empty response, and a permissions failure — the last matters because reporting "no stack" on anAccessDeniedExceptionwould turn a confirmed teardown into an unexplained "add a resource" error.Notes
maindeletes orphaned harnesses and config bundles before destroying the stack, because it creates some of them imperatively. This branch creates everything through CloudFormation, so the stack deletion covers them; nothing is intentionally left behind, but flagging it in case a later imperative resource needs the same treatment.