feat(aws/cloudformation): expose StackSet instances, drift, and operations #57
Loading…
Reference in a new issue
No description provided.
Delete branch "worktree-618"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
listInstances,listOperations,describeOperation,detectDrift) to the@swamp/aws/cloudformation/stack-setmodel via the enrichment system's newcustomMethodscapabilityAwsEnrichmentwith an optionalcustomMethodsfield — standalone methods that receive explicit(args, credentials)and are generated as new method blocks alongside existing CRUDcodegen/shared/sourceParser.tsand reorganises enrichments into a folder-per-resource structureCloud Control API structurally cannot surface StackSet instance status, drift detection, or operation history. These methods use
@aws-sdk/client-cloudformationdirectly, with explicit credential pass-through fromglobalArgs.Closes #618
Test plan
deno test aws/enrichments/parser_test.ts— 10 enrichment parser tests pass (shared parser non-regression)deno test aws/extensionModelGenerator_test.ts— 13 generator tests pass including newwith modelMethodssnapshotdeno test aws/enrichments/cfn-stackset/methods_test.ts— 6 methods source tests pass (mock HTTP, pagination, drift polling, timeout)deno task generate:aws rds— zero diff (RDS enrichment non-regression)deno task generate:aws cloudformationtwice — second run zero diff (idempotency)deno check,deno lint,deno fmt --checkpass for codegen and model/aws/cloudformation🤖 Generated with Claude Code
Code Review
Blocking Issues
Logic bug:
listInstancesfalls back tocredentials.regionas StackSet name (codegen/aws/enrichments/cfn-stackset/methods.ts, line 38)When
args.StackSetNameisundefined, the function silently uses the region string (e.g."us-east-1") as the StackSet name. This would produce a confusingStackSetNotFoundExceptionfrom AWS instead of a clear missing-argument error. All other methods in the same file (listOperations,describeOperation,detectDrift) correctly do not have this fallback. This looks like a copy-paste artifact that was never cleaned up.anytypes in hand-written code (codegen/aws/enrichments/cfn-stackset/methods.ts, lines 38, 54, 94, 153, 224) — violates the CLAUDE.md rule "noanytypes in hand-written code".The
(args as any).StackSetNamepattern is unnecessary:argsis typed asRecord<string, unknown>, soargs.StackSetName as stringis valid TypeScript without ananyintermediary. Similarly theFilters: ... as | any | undefinedcast on line 54 is avoidable. The file suppresses the lint rule with// deno-lint-ignore-file no-explicit-anyrather than fixing the casts.Suggestions
console.logstatements indetectDriftwill appear in production (codegen/aws/enrichments/cfn-stackset/methods.ts, lines 240 and 258). These are inlined verbatim into the generatedstack_set.tsmodel, so every user running drift detection will see[DRIFT] Started drift detection…and[DRIFT] Operation status: …in their logs. Consider removing them or replacing with a callback/hook if progress visibility is needed.Design doc describes a layout that doesn't exist (
codegen/designs/aws.md, section 8b). The doc says model methods live incodegen/aws/methods/— "a separate system from enrichments with its own types, registry, and generator path" and that "the enrichment system is not modified." In reality the implementation addscustomMethodsdirectly to the existingAwsEnrichmentinterface and places everything undercodegen/aws/enrichments/cfn-stackset/. The doc should be updated to reflect the actual directory structure and architecture so future contributors aren't sent to a path that doesn't exist.Unnecessary
no-import-prefixlint suppression (codegen/aws/enrichments/cfn-stackset/methods.ts, line 1). The file addsno-import-prefixto the lint ignore directive, but all imports already use thenpm:prefix correctly. This suppress is a no-op and can be removed to keep the ignore minimal.Adversarial Review
Medium
codegen/aws/enrichments/cfn-stackset/methods.ts:38-39—listInstancessilently falls back tocredentials.regionwhenStackSetNameis missingIf
StackSetNameis ever absent fromargs, this falls back to using the AWS region (e.g."us-east-1") as the StackSet name. This would produce a confusing CloudFormationStackSetNotFoundExceptionblaming a region string as a StackSet name. The other three functions (listOperations:94,describeOperation:152,detectDrift:224) do NOT have this fallback — they cast directly tostringwithout a??. This is almost certainly a copy-paste remnant.Breaking input: Call
listInstanceswith args that omitStackSetName(e.g. if globalArgs validation is bypassed or the field is undefined at runtime).Suggested fix: Remove the
?? credentials.regionfallback and either throw an explicit error if StackSetName is missing, or rely on the Zod schema enforcement alone (consistent with the other three methods):codegen/aws/extensionModelGenerator.ts:195-208— import deduplication drops entire import lines, potentially losing named importsThe import dedup logic for model methods skips an entire import line if its package string matches a package already imported by enrichment or listMethod:
If a resource has both an enrichment and customMethods that import different named symbols from the same npm package (e.g. enrichment imports
{ ListStacksCommand }and methods import{ DetectStackSetDriftCommand }from the same@aws-sdk/client-cloudformation), the model methods' import line is silently dropped. The generated code would reference symbols that are never imported, producing a compile error.Currently not triggered because cfn-stackset has no enrichment
sourceFile, so the dedup set is empty. This becomes a real bug the first time someone adds both enrichment and customMethods for a resource sharing an npm package.Suggested fix: Merge named imports from the same package rather than dropping the entire later import line. Alternatively, document that enrichment and customMethods must not share a package, with a build-time assertion.
codegen/aws/enrichments/cfn-stackset/methods.ts:226-227—detectDrifthas no bounds on poll interval or timeoutThe Zod schema validates these are numbers but imposes no minimum. A caller passing
pollIntervalMs: 0(or a negative value) getssetTimeout(resolve, 0)— a tight loop that hammers the CloudFormation DescribeStackSetOperation API as fast as the network allows. With defaulttimeoutMs: 300000, that's up to 5 minutes of uncapped API calls, likely hitting rate limits.Breaking input:
{ StackSetName: "x", pollIntervalMs: 0, timeoutMs: 300000 }Suggested fix: Clamp
pollIntervalMsto a floor (e.g.Math.max(pollIntervalMs, 1000)) andtimeoutMsto a ceiling. Or add.min()constraints in the Zod schema in config.ts.codegen/designs/aws.md(added section 8b) — design doc describes a file structure that doesn't match the implementationThe design doc says model methods live in
codegen/aws/methods/as a separate system from enrichments:In reality, model methods are implemented inside the enrichment system:
codegen/aws/enrichments/types.ts(AwsEnrichmentCustomMethod,customMethodsonAwsEnrichment)codegen/aws/enrichments/cfn-stackset/config.tsgetEnrichment()incodegen/aws/enrichments/index.tsEvery file path and "registry" name in the design doc is wrong. A developer following section 8b's "Adding a new model methods config" instructions would create files in directories that don't exist.
Low
codegen/aws/enrichments/cfn-stackset/methods.ts:240-241, 258— debugconsole.logstatements in production codedetectDriftlogs the StackSet name and operation status to stdout on every poll iteration. Not a security issue (no secrets), but noisy in production and inconsistent with the other methods which don't log.Verdict
PASS — No critical or high severity issues. The medium findings are real but either latent (import dedup) or bounded in impact (region-as-stackset-name produces a clear AWS error rather than silent data corruption; polling floor is a robustness concern). The design doc mismatch should be fixed before it misleads contributors.
Code Review
Blocking Issues
None.
Suggestions
Design doc section 8a is stale after the enrichment reorganization (
codegen/designs/aws.md, section 8a). It still shows the old flat-file structure (rds-dbcluster.ts,rds-dbcluster.enrich.ts) and the "Adding a new enrichment" steps still say to create<service>-<resource>.enrich.tsat the enrichment root. Section 8b correctly shows the new subdirectory layout (rds-dbcluster/config.ts, etc.). Section 8a should be updated to match so the two sections don't contradict each other.Duplicate
ParsedEnrichmentSource/ParsedSourceinterfaces.enrichments/types.tsdefinesParsedEnrichmentSource { imports, body }andshared/sourceParser.tsdefinesParsedSource { imports, body }— they are structurally identical.AwsExtensionModelListMethod.sourceusesParsedEnrichmentSourcewhileParsedModelMethods.sourceusesParsedSource, for no semantic reason. Could consolidate to one type as a follow-up.Non-array custom method instance naming (
extensionModelGenerator.ts:806). ForreturnsArray: falsemethods, the generated instance name iscontext.globalArgs.name?.toString() ?? "<methodName>". For naturally-named resources likeStackSet(primary identifier isStackSetName, no syntheticnamefield),globalArgs.nameis alwaysundefined, so every call todescribeOperationwrites to the slot"describeOperation"— meaning calls with differentoperationIdvalues silently overwrite each other. Using(mergedArgs.operationId ?? "<methodName>").toString()fordescribeOperation, or including a user-visible caveat in the description, would make the behaviour more predictable.Adversarial Review
Medium
Import deduplication skips entire import statements by package name, not by symbol —
codegen/aws/extensionModelGenerator.ts:195-209The custom methods import dedup logic (and the pre-existing listMethod dedup at lines 183-194) checks whether a package name has already appeared in an earlier import. If it has, the entire import statement is skipped. This means if a resource has both an
enrichStateenrichment andcustomMethodsthat import different symbols from the same@aws-sdk/client-*package, the custom methods' import would be silently dropped, producing generated code with undefined references.Breaking example: Suppose
rds-dbclustergains custom methods that import{ModifyDBClusterCommand}from@aws-sdk/client-rds. The enrichment already imports{DescribeDBClustersCommand, RDSClient}from the same package. The custom methods import would be skipped →ModifyDBClusterCommandwould be undefined in the inlined body → the generated model would compile (TypeScript sees the function body but the import is missing) and fail at runtime.Currently safe: No enrichment combines
enrichState/listMethodANDcustomMethodsimporting from the same SDK package. The StackSet enrichment only hascustomMethods, and the RDS enrichment has nocustomMethods. But this is a latent trap for the next person who adds custom methods to an already-enriched resource.Suggested fix: Merge import specifiers by package instead of skip-whole-statement. Or at minimum, add a code comment warning that each enrichment type must import all its own symbols and not rely on imports from another enrichment type using the same package.
detectDrifttimeout check is post-sleep, so wall time can exceedtimeoutMsbypollIntervalMs + API latency—codegen/aws/enrichments/cfn-stackset/methods.ts:248-249The while loop sleeps first, then makes the API call, then checks if terminal. The timeout condition (
Date.now() - startTime < timeoutMs) is only evaluated at the top of the loop. So the actual wall time before the timeout error is thrown istimeoutMs + pollIntervalMs + API_call_timein the worst case.Breaking example:
pollIntervalMs: 5000, timeoutMs: 1000. The function sleeps 5s, makes an API call (~200ms), and the timeout error says "timed out after 1000ms" even though ~5200ms elapsed. Not a data integrity issue, but the error message is misleading.Suggested fix: Check elapsed time immediately after sleeping but before the API call, or rewrite to
do { call; check; if (!terminal) sleep; } while (...).Low
timeoutMshas no lower bound — setting 0 silently abandons drift detection —codegen/aws/enrichments/cfn-stackset/methods.ts:230-233Math.min(0, 600000)→0. The while loop never enters. The function initiates drift detection on AWS (which keeps running server-side) and immediately throws"timed out after 0ms". The drift detection operation is now orphaned — the user gets an error but drift detection is actually running.Not a security issue or data loss, but users who accidentally pass
timeoutMs: 0get a confusing experience.context.globalArgs.namein non-array custom methods may reference a field that doesn't exist for natural-naming resources —codegen/aws/extensionModelGenerator.ts:807-810For non-array custom methods, the generated instance name is
context.globalArgs.name?.toString() ?? "${method.methodName}". If the resource uses natural naming (e.g.,StackSetName), there is nonamefield in globalArgs, so the fallback is always used. Every invocation ofdescribeOperationoverwrites the same"describeOperation"instance. This is arguably correct (latest-wins for a single-result method), but if someone callsdescribeOperationforop-1thenop-2, theop-1result is lost.The StackSet model uses synthetic naming (StackSetId is read-only primary identifier), so
nameis always present in practice for the current enrichment. But a future enrichment on a natural-named resource would hit this.CloudFormationClient is never destroyed in methods.ts —
codegen/aws/enrichments/cfn-stackset/methods.ts:14-33Each function call creates a new
CloudFormationClientviacreateCfnClient()but never callsclient.destroy(). FordetectDrift, which may make 60+ API calls through a single client, this is fine during the call. But after the function returns, the client's HTTP connection pool is leaked until GC. This is standard AWS SDK practice and unlikely to cause problems in normal usage, but is worth noting for long-running processes that call these methods frequently.Verdict
PASS — The code is well-structured and follows established patterns in the codebase. The custom methods enrichment architecture cleanly extends the existing enrichment system. Test coverage is solid with proper mock servers, env var restoration, and edge case tests (empty results, timeout). The import deduplication issue (Medium #1) is a latent bug that doesn't affect any current code paths, and the timeout post-sleep issue (Medium #2) is a minor behavioral wart. No blocking issues found.
Adversarial Review
Critical / High
No critical or high severity findings.
Medium
Region resolution inconsistency between CRUD and custom methods —
codegen/aws/enrichments/cfn-stackset/methods.ts:15-18createCfnClientresolves region ascredentials.region → AWS_REGION → AWS_DEFAULT_REGION → "us-east-1". The design doc (section 7) documents that_lib/aws.tsresolves region asexplicit → AWS_REGION → AWS_DEFAULT_REGION → ~/.aws/config profile region → us-east-1. Custom methods skip the profile region step.Breaking example: A user has
region = ap-southeast-1in~/.aws/config(via[profile myprofile]), setsAWS_PROFILE=myprofile, but does not setAWS_REGIONorAWS_DEFAULT_REGIONenv vars, and does not set aregionglobal arg. CRUD methods (get,create,sync) will operate againstap-southeast-1, butlistInstances,detectDrift, etc. will hitus-east-1.Suggested fix: Either read
~/.aws/configincreateCfnClientto match_lib/aws.ts, or omit the region entirely from the client config when no explicit region is provided (letting the AWS SDK resolve it from the default credential chain, which includes profile config). The simplest option:Timeout test runs ~50x slower than intended —
codegen/aws/enrichments/cfn-stackset/methods_test.ts:332-338The "detectDrift times out" test passes
pollIntervalMs: 50, timeoutMs: 200, butdetectDriftclamps these topollIntervalMs: 1000(MIN_POLL_INTERVAL_MS) andtimeoutMs: 10000(MIN_TIMEOUT_MS). The test correctness is fine (it exercises the timeout path), but it takes ~10 seconds instead of the apparent ~200ms. The successful drift test (line 277-278) is similarly affected:pollIntervalMs: 100is clamped to 1000ms.Suggested fix: Use values at or above the clamp floors in the tests to make the declared values match the actual runtime behavior:
Or, expose the constants and use them in the test to make the relationship explicit.
Low
Import dedup drops entire lines by package name —
codegen/aws/extensionModelGenerator.ts:196-213If a future enrichment combines
enrichStateandcustomMethodsthat import different symbols from the same npm package, thecustomMethodsimport line is silently dropped, causing a compile error in the generated model. The comment on line 195-198 acknowledges this. No current enrichment triggers this, but it's a latent trap for the next contributor.Unbounded instance name length for non-array custom methods —
codegen/aws/extensionModelGenerator.ts:811-819Non-array method instance names are built from
methodName + "-" + args.join("-"). Argument values likeoperationIdare user-provided strings. The sanitizer handles path traversal but not length. An extremely longoperationIdproduces an extremely long instance name. Unlikely to cause issues in practice.Verdict
PASS — The core logic (custom methods, source parser extraction, codegen emission, import dedup) is correct and well-tested. The region inconsistency (Medium #1) is the most substantive finding but is an edge case affecting only users who rely solely on
~/.aws/configprofile-based region without any env var or explicit arg. The slow test (Medium #2) is a CI hygiene issue, not a correctness bug. The code is cleanly structured, tests cover the key paths (pagination, timeout, empty results), and the enrichment system's factoring into sharedsourceParser.tsis a sound refactor.Code Review
Blocking Issues
methods_test.ts: Drift tests will be prohibitively slow due to clamped minimumsdetectDriftclamps the caller-supplied values to hard floors:MIN_POLL_INTERVAL_MS = 1000andMIN_TIMEOUT_MS = 10000. Both tests pass values far below those floors:detectDrift polls until COMPLETED— passespollIntervalMs: 100(clamped to 1000 ms). The mock returns RUNNING for the first 3DescribeStackSetOperationcalls, so the test sleeps at least 3 × 1000 ms = 3 seconds.detectDrift times out when status never completes— passespollIntervalMs: 50(→ 1000 ms) andtimeoutMs: 200(→ 10000 ms). The test will spin for ≥ 10 seconds before the timeout fires.These constants were added in the previous review fix (
fix: address review findings — clamp poll interval), but the test values were never updated to match. Fix: update the test values to meet or exceed the actual floors, e.g.pollIntervalMs: 1000, timeoutMs: 11000for the poll test andtimeoutMs: 10000for the timeout test (or reduceMIN_TIMEOUT_MSto something more test-friendly, e.g. 2000 ms).Relevant lines:
methods_test.ts:277-298(poll test) andmethods_test.ts:329-343(timeout test); constants atmethods.ts:216-218.Suggestions
rds-dbcluster/list.enrich.tsandrds-dbcluster/enrich.ts: user-supplied credentials are silently ignored for list/enrich operationsBoth functions create their own
RDSClientfromDeno.env.get("AWS_REGION")only — no credentials parameter. If a user configuresaccessKeyId/secretAccessKeyinglobalArgs, those values are used for CloudControl CRUD but silently ignored for the list and enrich calls. The newcustomMethodspattern (cfn-stackset) correctly accepts acredentials: AwsCredentialsargument; it would be worth carrying this forward to the list/enrich pattern as well.parser.ts:parseEnrichmentSourceandparseListMethodSourceare identicalBoth functions are one-liners that forward unconditionally to
parseSourceFile. They add no behavioral difference and create a misleading impression that the two paths differ. Consider either removing the indirection (callers can callparseSourceFiledirectly) or adding a meaningful distinction (e.g. the{ stripAwsLib: true }option thatpipeline.tsapplies to custom-methods sources but not to enrichment sources).Import deduplication drops symbols when packages overlap (acknowledged, but worth tracking)
The comment at
extensionModelGenerator.ts:196-198documents that if an enrichment and a customMethods source both import from the same npm package, only the enrichment's import is kept, which silently drops symbols needed by the custom methods. The current pairings don't trigger this, but it will become a landmine when a future enrichment combines both. Consider merging imports symbol-by-symbol rather than by package URL.