feat(cloudformation): add templateTextPolicy, an opt-in guard for template text - #352
Open
laazyj wants to merge 1 commit into
Open
feat(cloudformation): add templateTextPolicy, an opt-in guard for template text#352laazyj wants to merge 1 commit into
laazyj wants to merge 1 commit into
Conversation
…plate text CloudFormation stores template text as ASCII and silently transliterates anything else to `?` at deploy time. There is no error: the deployed template stops matching the synthesised one, so `cdk diff` reports a change on every run afterwards, forever, on a stack nobody touched. `templateTextPolicy(scope, config?)` installs an Aspect that finds those characters at synth instead. Three modes: - `throw` (default) fails synth, naming the construct path, the field and the offending character. - `sanitize` rewrites the value so the emitted template matches what CloudFormation will store. - `warn` lists every violation in one pass — the adoption path onto `throw` for an existing estate. The policy is opt-in because enforcing this at every builder would turn a working, if diff-noisy, deployment into a synth failure. Constraints whose violation fails the deploy rather than the diff — an EC2 GroupDescription — stay enforced at the builder. Keyed by CloudFormation resource-type string, so the registry names AWS::Lambda::Function without importing the lambda package. This is what ADR-0010 called for and it is why the policy can live centrally without inverting the dependency graph. Consumers extend it via `config.fields`; the registry const is deliberately not exported so its shape stays changeable. Sanitisation replaces per character rather than per run, since collapsing a run turns a quoted phrase into a single `-`. Unmapped characters become `?`, which is what CloudFormation would have stored anyway. `warn` degrades to the 2.0-era Annotations.addWarning where addWarningV2 is absent: this package's floor is aws-cdk-lib 2.1.0, the lowest in the repo because every other package peer-depends on it, and addWarningV2 only arrived in 2.93.0. Adds ADR-0017 and amends ADR-0002 §5, which routed pan-domain policies to packages/examples until a policies package is justified — that rule exists to contain peer-dependency surface, and a resource-type-keyed policy has none. Refs #336
Contributor
CoverageOverall line coverage: 99.36% across 23 package(s).
|
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
What & why
CloudFormation stores template text as ASCII and silently transliterates anything else to
?at deploy time. No error, noCREATE_FAILED— the deployed template just stops matching the synthesised one, socdk diffreports a change on every run afterwards, forever, on a stack nobody touched.This is the underlying-problem half of #336. The shipped-defaults half is #351.
onViolation"throw"(default)"sanitize""warn""throw".Design
Opt-in, per the plan. Enforcing this at every builder would turn a working — if diff-noisy — deployment into a synth failure for anyone living with the transliteration. Constraints whose violation fails the deploy rather than the diff (an EC2
GroupDescription, which the service rejects outright) stay enforced at the builder regardless.An Aspect, because a builder validator structurally can't do this. It only covers fields someone remembered to wire one into — which is precisely how the stack-level
Descriptionslipped through:StackBuilderpassesdescriptionstraight toStackPropsand no validator ever sees it. The Aspect walks the tree, so for registered types it also catches constructs this library never built.Keyed by resource-type string, so it imports no service package — the registry names
AWS::Lambda::Functionwithout importing@composurecdk/lambda. That's what ADR-0010's consequences called for and why the policy can live incloudformationwithout inverting the dependency graph.Three node kinds, because only one is a
CfnResource: the stack'sDescriptionlives ontemplateOptions, andCfnOutput/CfnParameterareCfnElements with their own accessor. A registry keyed by resource type alone would miss both — including the field that caused the bug.Per-character sanitisation, not
sanitizeString's run-collapse, which would turn a quoted phrase into a single-. Unmapped characters become?— CloudFormation's own substitution — so the template byte-matches the deployed value either way.Notable implementation points
warndegrades toAnnotations.addWarningwhereaddWarningV2is missing. This package's floor isaws-cdk-lib2.1.0 — the lowest in the repo, because every other package peer-depends on it — andaddWarningV2only arrived in 2.93.0 (seecdk-floors.json). Raising the floor for one optional mode would drag the whole library up with it. Covered by a test that deletes the method from the prototype.throwthrows rather thanAnnotations.addError. Only the CDK CLI acts on error metadata, soapp.synth()andTemplate.fromStack()would sail past an annotation — a guard that doesn't fire in unit tests. Cost: only the first violation is reported, which is whatwarnmode exists for.Lazyresolving to a plain string is checked, not skipped — that string is what deploys. Only values resolving to a CloudFormation intrinsic are skipped.TEMPLATE_TEXT_FIELDSis not exported. Exporting it would lock the registry's shape in as public API and foreclose a future switch to name-based detection.config.fieldsis the extension point; the default list is documented as a README table.Scope and honest limits
The registry seeds 15 resource types;
aws-cdk-libdeclares a free-text property on roughly 550. That is a starting point sized to what has been reasoned about, not a claim of completeness — the README says so, and ADR-0017 records the name-predicate alternative (/[Dd]escription$/,displayName,comment) as the likeliest successor, with why it's deferred.Also outside coverage, and documented: values resolving to intrinsics, values written via
addPropertyOverrideor a bareCfnResource'sproperties, and nested paths likeDistributionConfig.Comment(deferred — they need a resolve on read and a property override on write).Not yet verified against live AWS. The plan's Phase 0 called for confirming which fields CloudFormation actually coerces; only the template-level
Descriptionhas direct evidence. The rest are listed because they carry the same author-written prose through the same document. Thecdk diffidempotence check that would settle it is Phase 3 and depends on #351 landing first.Docs
packages/cloudformation/README.md— newtemplateTextPolicysection: failure mode, mode table, what it checks, what it doesn't.docs/adr/0017-template-text-policy.md— new.docs/adr/0002-policies.md— §5 marked amended; a resource-type-keyed policy has no peer-dependency surface, so the "pan-domain policies wait inexamples/" rule doesn't apply to it.docs/adr/0010-aws-property-constraints.md— its named follow-up now points here.docs/architecture.md§Policies anddocs/constraints.md(regenerated) updated.Follow-ups
cdk diffidempotence check indeploy-test(Phase 3) — the empirical proof of acceptance criterion Deployable examples #2, blocked on fix: strip non-ASCII from descriptions emitted into templates #351.Checklist
npm run verifypasses locallyGenerated by Claude Code