@intentius/chant-lexicon-aws 0.124.30 → 0.124.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (644) hide show
  1. package/dist/actions/dynamodb.js +41 -0
  2. package/dist/actions/ecr.js +9 -0
  3. package/dist/actions/ecs.js +5 -0
  4. package/dist/actions/iam.js +3 -0
  5. package/dist/actions/index.js +9 -0
  6. package/dist/actions/lambda.js +11 -0
  7. package/dist/actions/logs.js +4 -0
  8. package/dist/actions/s3.js +40 -0
  9. package/dist/actions/sns.js +5 -0
  10. package/dist/actions/sqs.js +15 -0
  11. package/dist/agentcore/trace-fetch.d.ts.map +1 -1
  12. package/dist/agentcore/trace-fetch.js +472 -0
  13. package/dist/agentcore/trace-render.js +359 -0
  14. package/dist/ambient.js +148 -0
  15. package/dist/api/credential-chain.d.ts +30 -3
  16. package/dist/api/credential-chain.d.ts.map +1 -1
  17. package/dist/api/credential-chain.js +509 -0
  18. package/dist/api/read-client.d.ts +16 -43
  19. package/dist/api/read-client.d.ts.map +1 -1
  20. package/dist/api/read-client.js +458 -0
  21. package/dist/api/sigv4.d.ts +4 -4
  22. package/dist/api/sigv4.js +185 -0
  23. package/dist/behaviour-kinds.js +114 -0
  24. package/dist/caller-identity.d.ts +3 -5
  25. package/dist/caller-identity.d.ts.map +1 -1
  26. package/dist/caller-identity.js +146 -0
  27. package/dist/codegen/docs-cli.js +2 -0
  28. package/dist/codegen/docs.js +120 -0
  29. package/dist/codegen/enum-overlay.js +162 -0
  30. package/dist/codegen/extensions.js +139 -0
  31. package/dist/codegen/fallback.js +32 -0
  32. package/dist/codegen/generate-cli.js +15 -0
  33. package/dist/codegen/generate-lexicon.js +70 -0
  34. package/dist/codegen/generate-typescript.js +226 -0
  35. package/dist/codegen/generate.d.ts.map +1 -1
  36. package/dist/codegen/generate.js +234 -0
  37. package/dist/codegen/naming.js +135 -0
  38. package/dist/codegen/package.js +67 -0
  39. package/dist/codegen/patches.d.ts +26 -1
  40. package/dist/codegen/patches.d.ts.map +1 -1
  41. package/dist/codegen/patches.js +90 -0
  42. package/dist/codegen/property-overlay.js +85 -0
  43. package/dist/codegen/property-overlay.json +1 -16
  44. package/dist/codegen/sam.js +402 -0
  45. package/dist/codegen/typecheck.js +4 -0
  46. package/dist/codegen/versions.js +31 -0
  47. package/dist/components/__tests__/mock-cloud-executor.js +326 -0
  48. package/dist/components/apply.d.ts +44 -1
  49. package/dist/components/apply.d.ts.map +1 -1
  50. package/dist/components/apply.js +752 -0
  51. package/dist/components/builders.js +37 -0
  52. package/dist/components/capability-plugin.js +66 -0
  53. package/dist/components/cloud-executor.d.ts +35 -0
  54. package/dist/components/cloud-executor.d.ts.map +1 -1
  55. package/dist/components/cloud-executor.js +576 -0
  56. package/dist/components/config-bom.js +207 -0
  57. package/dist/components/host-delivery.js +117 -0
  58. package/dist/components/index.js +18 -0
  59. package/dist/components/job-submission.js +77 -0
  60. package/dist/components/publish.js +234 -0
  61. package/dist/components/safety.js +62 -0
  62. package/dist/components/wait-aws.js +68 -0
  63. package/dist/composites/agentcore-agent.js +187 -0
  64. package/dist/composites/agentcore-trust-policy.js +13 -0
  65. package/dist/composites/alb-shared.js +84 -0
  66. package/dist/composites/bucket-deployment.js +93 -0
  67. package/dist/composites/catalog.js +2714 -0
  68. package/dist/composites/dynamodb-table.js +66 -0
  69. package/dist/composites/ec2-instance-bundle.js +75 -0
  70. package/dist/composites/ec2-instance-role.js +26 -0
  71. package/dist/composites/ecr-repository.js +64 -0
  72. package/dist/composites/ecs-trust-policy.js +10 -0
  73. package/dist/composites/efs-with-access-point.js +44 -0
  74. package/dist/composites/eks-cluster.js +118 -0
  75. package/dist/composites/fargate-alb.js +216 -0
  76. package/dist/composites/fargate-service.js +222 -0
  77. package/dist/composites/index.js +28 -0
  78. package/dist/composites/lambda-api.js +15 -0
  79. package/dist/composites/lambda-dynamodb.js +77 -0
  80. package/dist/composites/lambda-eventbridge.js +27 -0
  81. package/dist/composites/lambda-function.js +54 -0
  82. package/dist/composites/lambda-s3.js +93 -0
  83. package/dist/composites/lambda-sns.js +23 -0
  84. package/dist/composites/lambda-sqs.js +34 -0
  85. package/dist/composites/landing-zone.js +82 -0
  86. package/dist/composites/microvm-app.js +293 -0
  87. package/dist/composites/minimal-vpc.js +40 -0
  88. package/dist/composites/monitoring-stack.js +105 -0
  89. package/dist/composites/nlb-service.js +61 -0
  90. package/dist/composites/rds-instance.js +98 -0
  91. package/dist/composites/scheduled-lambda.js +28 -0
  92. package/dist/composites/solr-fargate-service.js +33 -0
  93. package/dist/composites/step-functions-workflow.js +78 -0
  94. package/dist/composites/vpc-default.js +132 -0
  95. package/dist/condition.js +43 -0
  96. package/dist/coverage.js +4 -0
  97. package/dist/deep-observe.d.ts.map +1 -1
  98. package/dist/deep-observe.js +709 -0
  99. package/dist/deep-topology.js +226 -0
  100. package/dist/default-tags.js +50 -0
  101. package/dist/defaults.js +85 -0
  102. package/dist/dependencies.js +269 -0
  103. package/dist/detect.js +21 -0
  104. package/dist/disruption.d.ts.map +1 -1
  105. package/dist/disruption.js +129 -0
  106. package/dist/effect-receipt-row.js +107 -0
  107. package/dist/generated/index.d.ts +1297 -82
  108. package/dist/generated/index.d.ts.map +1 -1
  109. package/dist/generated/index.js +12382 -0
  110. package/dist/generated/runtime.js +4 -0
  111. package/dist/governance.js +131 -0
  112. package/dist/identity-observe.d.ts.map +1 -1
  113. package/dist/identity-observe.js +127 -0
  114. package/dist/import/generator.js +757 -0
  115. package/dist/import/live-export.js +34 -0
  116. package/dist/import/parser.js +394 -0
  117. package/dist/index.js +69 -0
  118. package/dist/integrity.json +6 -6
  119. package/dist/intrinsics.js +393 -0
  120. package/dist/lint/audit-catalog.js +87 -0
  121. package/dist/lint/audit-lineage.js +150 -0
  122. package/dist/lint/post-synth/cf-refs.js +247 -0
  123. package/dist/lint/post-synth/cor020.js +60 -0
  124. package/dist/lint/post-synth/ext001.d.ts.map +1 -1
  125. package/dist/lint/post-synth/ext001.js +197 -0
  126. package/dist/lint/post-synth/index.js +124 -0
  127. package/dist/lint/post-synth/waw010.js +41 -0
  128. package/dist/lint/post-synth/waw011.js +41 -0
  129. package/dist/lint/post-synth/waw013.js +38 -0
  130. package/dist/lint/post-synth/waw014.js +42 -0
  131. package/dist/lint/post-synth/waw015.js +88 -0
  132. package/dist/lint/post-synth/waw016.d.ts.map +1 -1
  133. package/dist/lint/post-synth/waw016.js +116 -0
  134. package/dist/lint/post-synth/waw017.js +43 -0
  135. package/dist/lint/post-synth/waw018.js +61 -0
  136. package/dist/lint/post-synth/waw019.js +64 -0
  137. package/dist/lint/post-synth/waw020.js +56 -0
  138. package/dist/lint/post-synth/waw021.js +44 -0
  139. package/dist/lint/post-synth/waw022.js +36 -0
  140. package/dist/lint/post-synth/waw023.js +40 -0
  141. package/dist/lint/post-synth/waw024.js +46 -0
  142. package/dist/lint/post-synth/waw025.js +36 -0
  143. package/dist/lint/post-synth/waw026.js +38 -0
  144. package/dist/lint/post-synth/waw027.js +41 -0
  145. package/dist/lint/post-synth/waw028.js +39 -0
  146. package/dist/lint/post-synth/waw029.js +54 -0
  147. package/dist/lint/post-synth/waw030.js +282 -0
  148. package/dist/lint/post-synth/waw031.js +58 -0
  149. package/dist/lint/post-synth/waw032.js +45 -0
  150. package/dist/lint/post-synth/waw033.js +75 -0
  151. package/dist/lint/post-synth/waw034.js +52 -0
  152. package/dist/lint/post-synth/waw035.js +56 -0
  153. package/dist/lint/post-synth/waw036.js +79 -0
  154. package/dist/lint/post-synth/waw037.js +67 -0
  155. package/dist/lint/post-synth/waw038.js +42 -0
  156. package/dist/lint/post-synth/waw039.js +42 -0
  157. package/dist/lint/post-synth/waw040.js +45 -0
  158. package/dist/lint/post-synth/waw041.js +40 -0
  159. package/dist/lint/post-synth/waw042.js +99 -0
  160. package/dist/lint/post-synth/waw043.js +51 -0
  161. package/dist/lint/post-synth/waw044.js +64 -0
  162. package/dist/lint/post-synth/waw045.js +49 -0
  163. package/dist/lint/post-synth/waw046.js +52 -0
  164. package/dist/lint/post-synth/waw047.js +43 -0
  165. package/dist/lint/post-synth/waw048.js +40 -0
  166. package/dist/lint/post-synth/waw049.js +78 -0
  167. package/dist/lint/post-synth/waw050.js +44 -0
  168. package/dist/lint/post-synth/waw051.js +41 -0
  169. package/dist/lint/post-synth/waw052.js +43 -0
  170. package/dist/lint/post-synth/waw053.js +43 -0
  171. package/dist/lint/post-synth/waw054.js +41 -0
  172. package/dist/lint/post-synth/waw055.js +41 -0
  173. package/dist/lint/post-synth/waw056.js +65 -0
  174. package/dist/lint/post-synth/waw057.js +43 -0
  175. package/dist/lint/post-synth/waw058.js +61 -0
  176. package/dist/lint/post-synth/waw059.js +338 -0
  177. package/dist/lint/post-synth/waw060.js +83 -0
  178. package/dist/lint/post-synth/waw061.js +60 -0
  179. package/dist/lint/post-synth/waw062.js +0 -0
  180. package/dist/lint/post-synth/waw063.js +0 -0
  181. package/dist/lint/post-synth/waw064.js +45 -0
  182. package/dist/lint/post-synth/waw065.js +104 -0
  183. package/dist/lint/post-synth/waw066.js +145 -0
  184. package/dist/lint/post-synth/waw067.js +115 -0
  185. package/dist/lint/post-synth/waw068.js +69 -0
  186. package/dist/lint/post-synth/waw069.js +0 -0
  187. package/dist/lint/rules/hardcoded-region.js +38 -0
  188. package/dist/lint/rules/iam-wildcard.js +60 -0
  189. package/dist/lint/rules/index.js +6 -0
  190. package/dist/lint/rules/s3-encryption.js +60 -0
  191. package/dist/live-attrs.js +42 -0
  192. package/dist/lsp/completions.d.ts.map +1 -1
  193. package/dist/lsp/completions.js +18 -0
  194. package/dist/lsp/hover.d.ts.map +1 -1
  195. package/dist/lsp/hover.js +66 -0
  196. package/dist/manifest.json +1 -1
  197. package/dist/mapping.js +33 -0
  198. package/dist/meta.json +17359 -784
  199. package/dist/nested-stack.js +95 -0
  200. package/dist/okf/index.md +203 -0
  201. package/dist/okf/rules/EXT001.md +1 -0
  202. package/dist/okf/rules/WAW016.md +2 -0
  203. package/dist/okf/rules/WAW019.md +1 -0
  204. package/dist/okf/rules/WAW028.md +1 -0
  205. package/dist/okf/rules/WAW030.md +2 -0
  206. package/dist/okf/rules/WAW039.md +1 -0
  207. package/dist/okf/rules/WAW049.md +1 -0
  208. package/dist/okf/rules/WAW056.md +1 -0
  209. package/dist/okf/rules/WAW058.md +1 -0
  210. package/dist/okf/rules/WAW062.md +1 -0
  211. package/dist/okf/rules/WAW064.md +1 -0
  212. package/dist/okf/rules/WAW065.md +1 -0
  213. package/dist/okf/rules/WAW066.md +1 -0
  214. package/dist/okf/rules/WAW067.md +1 -0
  215. package/dist/okf/rules/WAW068.md +1 -0
  216. package/dist/okf/rules/WAW069.md +4 -0
  217. package/dist/okf/types/AIWorkloadConfig.md +23 -0
  218. package/dist/okf/types/ARCRegionSwitchPlan.md +1 -0
  219. package/dist/okf/types/AccountAccessApplication.md +28 -0
  220. package/dist/okf/types/AccountAccessEntitlement.md +22 -0
  221. package/dist/okf/types/ActionTarget.md +20 -0
  222. package/dist/okf/types/AdConfiguration.md +21 -0
  223. package/dist/okf/types/Adapter.md +26 -0
  224. package/dist/okf/types/AgentContext.md +35 -0
  225. package/dist/okf/types/AgentGoal.md +31 -0
  226. package/dist/okf/types/AgentProfile.md +34 -0
  227. package/dist/okf/types/Alarm.md +3 -1
  228. package/dist/okf/types/AlarmModel.md +14 -9
  229. package/dist/okf/types/AmplifyWebhook.md +25 -0
  230. package/dist/okf/types/ApiCache.md +8 -8
  231. package/dist/okf/types/ApiGatewayManagedOverrides.md +5 -5
  232. package/dist/okf/types/AppAuthorization.md +35 -0
  233. package/dist/okf/types/AppBlockBuilder.md +1 -0
  234. package/dist/okf/types/AppInstance.md +2 -2
  235. package/dist/okf/types/AppInstanceBot.md +2 -2
  236. package/dist/okf/types/AppStreamFleet.md +3 -2
  237. package/dist/okf/types/AppSyncApiKey.md +6 -6
  238. package/dist/okf/types/AppSyncType.md +22 -0
  239. package/dist/okf/types/ApplicationStatusCheck.md +36 -0
  240. package/dist/okf/types/ApplicationVersion.md +4 -1
  241. package/dist/okf/types/ApplicationsList.md +26 -0
  242. package/dist/okf/types/ApprovalPolicy.md +28 -0
  243. package/dist/okf/types/ArchiveRule.md +24 -0
  244. package/dist/okf/types/Asset.md +30 -0
  245. package/dist/okf/types/Backup.md +31 -0
  246. package/dist/okf/types/BedrockAgentCoreDataset.md +1 -1
  247. package/dist/okf/types/BedrockAgentCoreGateway.md +3 -0
  248. package/dist/okf/types/BedrockSession.md +28 -0
  249. package/dist/okf/types/BillEstimate.md +35 -0
  250. package/dist/okf/types/Brand.md +32 -0
  251. package/dist/okf/types/CallAnalyticsCategory.md +25 -0
  252. package/dist/okf/types/Campaign.md +28 -0
  253. package/dist/okf/types/CapacityProvider.md +30 -0
  254. package/dist/okf/types/Case.md +24 -0
  255. package/dist/okf/types/CertificateAssociation.md +13 -0
  256. package/dist/okf/types/ChangeSet.md +36 -0
  257. package/dist/okf/types/ChannelFlow.md +29 -0
  258. package/dist/okf/types/ChimeChannel.md +39 -0
  259. package/dist/okf/types/Classifier.md +6 -6
  260. package/dist/okf/types/ClientVpnAuthorizationRule.md +6 -6
  261. package/dist/okf/types/ClientVpnEndpoint.md +2 -0
  262. package/dist/okf/types/ClientVpnRoute.md +5 -5
  263. package/dist/okf/types/CloudConnector.md +29 -0
  264. package/dist/okf/types/Cluster.md +39 -0
  265. package/dist/okf/types/ClusterSchedulerConfig.md +30 -0
  266. package/dist/okf/types/CodeCommitRepository.md +12 -12
  267. package/dist/okf/types/CodeConnectionsHost.md +26 -0
  268. package/dist/okf/types/CodeRepository.md +5 -5
  269. package/dist/okf/types/CompositeAlarm.md +1 -0
  270. package/dist/okf/types/ComputeEnvironment.md +2 -0
  271. package/dist/okf/types/ComputeNodeGroup.md +1 -1
  272. package/dist/okf/types/ComputeQuota.md +34 -0
  273. package/dist/okf/types/ConfigRule.md +1 -1
  274. package/dist/okf/types/ConfigurationRecorder.md +7 -6
  275. package/dist/okf/types/ConnectAttachment.md +2 -0
  276. package/dist/okf/types/ConnectQueue.md +5 -1
  277. package/dist/okf/types/ConnectVocabulary.md +28 -0
  278. package/dist/okf/types/ConnectionType.md +24 -0
  279. package/dist/okf/types/ConsentPortal.md +33 -0
  280. package/dist/okf/types/ContactMethod.md +29 -0
  281. package/dist/okf/types/ContainerAssociation.md +26 -0
  282. package/dist/okf/types/ContainerFleet.md +1 -1
  283. package/dist/okf/types/ContainerGroupDefinition.md +1 -1
  284. package/dist/okf/types/Content.md +40 -0
  285. package/dist/okf/types/ContentAssociation.md +28 -0
  286. package/dist/okf/types/CustomDetectionRuleAssociation.md +28 -0
  287. package/dist/okf/types/CustomEntityType.md +4 -9
  288. package/dist/okf/types/Customization.md +22 -0
  289. package/dist/okf/types/CwIntegration.md +31 -0
  290. package/dist/okf/types/DAXParameterGroup.md +3 -8
  291. package/dist/okf/types/DAXSubnetGroup.md +0 -5
  292. package/dist/okf/types/DBClusterEndpoint.md +31 -0
  293. package/dist/okf/types/DBProxyTargetGroup.md +1 -0
  294. package/dist/okf/types/DBSnapshot.md +62 -0
  295. package/dist/okf/types/DLMLifecyclePolicy.md +12 -12
  296. package/dist/okf/types/DLPSetting.md +31 -0
  297. package/dist/okf/types/DMSInstanceProfile.md +1 -1
  298. package/dist/okf/types/DRSLaunchConfigurationTemplate.md +28 -0
  299. package/dist/okf/types/Daemon.md +1 -0
  300. package/dist/okf/types/DataExchangeDataSet.md +29 -0
  301. package/dist/okf/types/DataIntegrationFlow.md +27 -0
  302. package/dist/okf/types/DataMigration.md +1 -1
  303. package/dist/okf/types/DataProvider.md +2 -1
  304. package/dist/okf/types/DataQualityRuleset.md +6 -11
  305. package/dist/okf/types/DataSource.md +23 -0
  306. package/dist/okf/types/Dataset.md +20 -0
  307. package/dist/okf/types/DatasetIntegration.md +23 -0
  308. package/dist/okf/types/DbCluster.md +1 -1
  309. package/dist/okf/types/DbInstance.md +8 -2
  310. package/dist/okf/types/DeadlineBudget.md +29 -0
  311. package/dist/okf/types/DelegationSet.md +23 -0
  312. package/dist/okf/types/DeliveryChannel.md +6 -11
  313. package/dist/okf/types/DevOpsAgentAgentSpace.md +4 -0
  314. package/dist/okf/types/DevOpsAgentService.md +1 -0
  315. package/dist/okf/types/DevOpsAgentTrigger.md +28 -0
  316. package/dist/okf/types/DirectConnectGatewayAttachment.md +2 -0
  317. package/dist/okf/types/Directory.md +60 -0
  318. package/dist/okf/types/DocDBDBClusterParameterGroup.md +5 -10
  319. package/dist/okf/types/DocDBDBSubnetGroup.md +4 -9
  320. package/dist/okf/types/DocDBElasticClusterSnapshot.md +36 -0
  321. package/dist/okf/types/DocumentClassifierEndpoint.md +27 -0
  322. package/dist/okf/types/Domain.md +27 -0
  323. package/dist/okf/types/EKSCertificateAuthority.md +42 -0
  324. package/dist/okf/types/EKSCluster.md +8 -0
  325. package/dist/okf/types/EcSnapshot.md +66 -0
  326. package/dist/okf/types/EmailIdentityCertificate.md +15 -0
  327. package/dist/okf/types/EndpointConfig.md +14 -13
  328. package/dist/okf/types/EntityRecognizer.md +27 -0
  329. package/dist/okf/types/EntityRecognizerEndpoint.md +31 -0
  330. package/dist/okf/types/EnvironmentEC2.md +13 -13
  331. package/dist/okf/types/EvaluationForm.md +1 -0
  332. package/dist/okf/types/Evaluator.md +6 -2
  333. package/dist/okf/types/EventAction.md +26 -0
  334. package/dist/okf/types/EventRule.md +2 -0
  335. package/dist/okf/types/EventSource.md +30 -0
  336. package/dist/okf/types/EventTracker.md +22 -0
  337. package/dist/okf/types/EventsV2EventBus.md +28 -0
  338. package/dist/okf/types/EventsV2ResourcePolicy.md +19 -0
  339. package/dist/okf/types/EventsV2Subscriber.md +39 -0
  340. package/dist/okf/types/ExperimentDefinition.md +39 -0
  341. package/dist/okf/types/ExperimentRun.md +33 -0
  342. package/dist/okf/types/ExperimentTrialComponent.md +13 -11
  343. package/dist/okf/types/FHIRDatastore.md +3 -0
  344. package/dist/okf/types/FSxSnapshot.md +5 -5
  345. package/dist/okf/types/FSxVolume.md +9 -9
  346. package/dist/okf/types/FeaturedResultsSet.md +26 -0
  347. package/dist/okf/types/Feed.md +1 -0
  348. package/dist/okf/types/FieldLevelEncryptionConfig.md +25 -0
  349. package/dist/okf/types/FieldLevelEncryptionProfile.md +22 -0
  350. package/dist/okf/types/FileCache.md +45 -0
  351. package/dist/okf/types/Filter.md +27 -0
  352. package/dist/okf/types/Firewall.md +6 -0
  353. package/dist/okf/types/FlowMediaStream.md +28 -0
  354. package/dist/okf/types/FlowOutput.md +1 -0
  355. package/dist/okf/types/FpgaImage.md +38 -0
  356. package/dist/okf/types/Function.md +2 -2
  357. package/dist/okf/types/Gateway.md +41 -0
  358. package/dist/okf/types/GatewayClusterAssociations.md +23 -0
  359. package/dist/okf/types/GatewayRateLimit.md +26 -0
  360. package/dist/okf/types/GatewayRule.md +32 -0
  361. package/dist/okf/types/GlobalReplicationGroup.md +3 -0
  362. package/dist/okf/types/GlobalResolver.md +1 -1
  363. package/dist/okf/types/GlobalTable.md +1 -0
  364. package/dist/okf/types/GlueConnection.md +5 -4
  365. package/dist/okf/types/GlueSecurityConfiguration.md +1 -6
  366. package/dist/okf/types/GlueSession.md +38 -0
  367. package/dist/okf/types/GlueTable.md +5 -10
  368. package/dist/okf/types/GlueWorkflow.md +5 -10
  369. package/dist/okf/types/Group.md +27 -0
  370. package/dist/okf/types/Harness.md +1 -0
  371. package/dist/okf/types/HostKey.md +33 -0
  372. package/dist/okf/types/HostedZone.md +1 -0
  373. package/dist/okf/types/HttpApi.md +2 -0
  374. package/dist/okf/types/HttpNamespace.md +5 -5
  375. package/dist/okf/types/HubV2.md +1 -0
  376. package/dist/okf/types/HumanTaskUi.md +22 -0
  377. package/dist/okf/types/IPv4Pool.md +28 -0
  378. package/dist/okf/types/ImageBuilder.md +1 -0
  379. package/dist/okf/types/ImageBuilderImage.md +1 -1
  380. package/dist/okf/types/ImagePipeline.md +1 -1
  381. package/dist/okf/types/InferenceComponent.md +5 -1
  382. package/dist/okf/types/InfluxDBBackup.md +65 -0
  383. package/dist/okf/types/InfluxDBCluster.md +3 -0
  384. package/dist/okf/types/InfrastructureConfiguration.md +2 -0
  385. package/dist/okf/types/IngestionDestination.md +28 -0
  386. package/dist/okf/types/InputSecurityGroup.md +6 -4
  387. package/dist/okf/types/InspectorV2Connector.md +38 -0
  388. package/dist/okf/types/InstanceEventWindow.md +25 -0
  389. package/dist/okf/types/InstanceFleetConfig.md +10 -10
  390. package/dist/okf/types/InstanceGroupConfig.md +3 -3
  391. package/dist/okf/types/InstrumentationConfig.md +31 -0
  392. package/dist/okf/types/IntegrationAssociation.md +3 -0
  393. package/dist/okf/types/IoTSiteWiseApplication.md +34 -0
  394. package/dist/okf/types/IoTSiteWiseDataset.md +5 -2
  395. package/dist/okf/types/IoTSiteWiseTask.md +24 -0
  396. package/dist/okf/types/IpamExternalResourceVerificationToken.md +35 -0
  397. package/dist/okf/types/IpamInternetRegistryAssociation.md +32 -0
  398. package/dist/okf/types/Job.md +35 -0
  399. package/dist/okf/types/JobTemplate.md +26 -0
  400. package/dist/okf/types/KinesisChannel.md +31 -0
  401. package/dist/okf/types/KinesisStream.md +1 -0
  402. package/dist/okf/types/LambdaResourcePolicy.md +14 -0
  403. package/dist/okf/types/Lexicon.md +34 -0
  404. package/dist/okf/types/LicenseAssetGroup.md +23 -0
  405. package/dist/okf/types/LicenseConfiguration.md +33 -0
  406. package/dist/okf/types/LicenseServerEndpoint.md +32 -0
  407. package/dist/okf/types/LightsailKeyPair.md +29 -0
  408. package/dist/okf/types/LimitsProfile.md +27 -0
  409. package/dist/okf/types/LocationAzureBlob.md +2 -1
  410. package/dist/okf/types/LocationObjectStorage.md +1 -0
  411. package/dist/okf/types/LogAlarm.md +2 -0
  412. package/dist/okf/types/MGNConnector.md +23 -0
  413. package/dist/okf/types/MGNLaunchConfigurationTemplate.md +37 -0
  414. package/dist/okf/types/MLTransform.md +15 -15
  415. package/dist/okf/types/MWAAServerlessWorkflow.md +3 -0
  416. package/dist/okf/types/ManagedNotificationAccountContactAssociation.md +1 -0
  417. package/dist/okf/types/ManagedNotificationAdditionalChannelAssociation.md +1 -0
  418. package/dist/okf/types/MediaConvertJobTemplate.md +11 -13
  419. package/dist/okf/types/MediaConvertQueue.md +8 -10
  420. package/dist/okf/types/MediaInsightsPipelineConfiguration.md +28 -0
  421. package/dist/okf/types/MediaLiveInput.md +41 -18
  422. package/dist/okf/types/MediaPackageV2Channel.md +3 -0
  423. package/dist/okf/types/MediaPackageV2OriginEndpoint.md +1 -0
  424. package/dist/okf/types/MediaPipelineKinesisVideoStreamPool.md +28 -0
  425. package/dist/okf/types/MediaTailorFunction.md +27 -0
  426. package/dist/okf/types/MedicalVocabulary.md +21 -0
  427. package/dist/okf/types/Member.md +26 -0
  428. package/dist/okf/types/Memory.md +6 -0
  429. package/dist/okf/types/MemoryDBSnapshot.md +29 -0
  430. package/dist/okf/types/Metric.md +59 -0
  431. package/dist/okf/types/MetricAttribution.md +23 -0
  432. package/dist/okf/types/MicrosoftAD.md +11 -11
  433. package/dist/okf/types/MigrationProject.md +8 -1
  434. package/dist/okf/types/Monitor.md +26 -0
  435. package/dist/okf/types/NFSFileShare.md +44 -0
  436. package/dist/okf/types/NeptuneGraphGraph.md +1 -0
  437. package/dist/okf/types/NetworkConnector.md +4 -0
  438. package/dist/okf/types/NetworkFlowMonitorScope.md +23 -0
  439. package/dist/okf/types/NetworkMigrationDefinition.md +31 -0
  440. package/dist/okf/types/NetworkSecurityManagerDeployment.md +31 -0
  441. package/dist/okf/types/NetworkSecurityManagerPolicy.md +32 -0
  442. package/dist/okf/types/NetworkSecurityManagerScope.md +29 -0
  443. package/dist/okf/types/NotebookInstance.md +18 -18
  444. package/dist/okf/types/NotebookInstanceLifecycleConfig.md +6 -5
  445. package/dist/okf/types/NotifyConfiguration.md +35 -0
  446. package/dist/okf/types/OTelEnrichment.md +2 -0
  447. package/dist/okf/types/OnlineEvaluationConfig.md +0 -1
  448. package/dist/okf/types/OrganizationConfigRule.md +7 -7
  449. package/dist/okf/types/Outpost.md +32 -0
  450. package/dist/okf/types/ParallelData.md +40 -0
  451. package/dist/okf/types/Partition.md +5 -5
  452. package/dist/okf/types/PaymentConnector.md +4 -1
  453. package/dist/okf/types/Pentest.md +1 -0
  454. package/dist/okf/types/PerfReports.md +28 -0
  455. package/dist/okf/types/PermissionAssociation.md +30 -0
  456. package/dist/okf/types/PersonalizeDataset.md +1 -0
  457. package/dist/okf/types/PersonalizeDatasetGroup.md +1 -0
  458. package/dist/okf/types/PersonalizeSchema.md +1 -0
  459. package/dist/okf/types/Pipeline.md +25 -0
  460. package/dist/okf/types/PlaybackConfiguration.md +1 -0
  461. package/dist/okf/types/PolicyAssociation.md +1 -1
  462. package/dist/okf/types/PolicyGrant.md +2 -2
  463. package/dist/okf/types/PolicyPrincipalAttachment.md +2 -7
  464. package/dist/okf/types/PortalProduct.md +24 -0
  465. package/dist/okf/types/PrefetchSchedule.md +25 -0
  466. package/dist/okf/types/PricingPlanManagerSubscription.md +31 -0
  467. package/dist/okf/types/PrincipalAssociation.md +27 -0
  468. package/dist/okf/types/PrivateDnsNamespace.md +8 -8
  469. package/dist/okf/types/PrivateVirtualInterface.md +1 -0
  470. package/dist/okf/types/ProcurementPortalPreference.md +45 -0
  471. package/dist/okf/types/Program.md +33 -0
  472. package/dist/okf/types/ProjectMembership.md +1 -1
  473. package/dist/okf/types/ProtocolsList.md +26 -0
  474. package/dist/okf/types/PublicDnsNamespace.md +7 -7
  475. package/dist/okf/types/PublicVirtualInterface.md +1 -0
  476. package/dist/okf/types/QEV2IdcApplication.md +25 -0
  477. package/dist/okf/types/QueryLoggingConfig.md +21 -0
  478. package/dist/okf/types/QuerySuggestionsBlockList.md +25 -0
  479. package/dist/okf/types/QuickSightAssignment.md +24 -0
  480. package/dist/okf/types/RAMResourceAssociation.md +27 -0
  481. package/dist/okf/types/RDSClusterSnapshot.md +54 -0
  482. package/dist/okf/types/RDSDBParameterGroup.md +5 -0
  483. package/dist/okf/types/RDSDeployment.md +41 -0
  484. package/dist/okf/types/RcsAgent.md +38 -0
  485. package/dist/okf/types/ReceiptRule.md +5 -5
  486. package/dist/okf/types/ReceiptRuleSet.md +1 -6
  487. package/dist/okf/types/RecommenderFilter.md +27 -0
  488. package/dist/okf/types/RecommenderSchema.md +25 -0
  489. package/dist/okf/types/RecordSet.md +16 -22
  490. package/dist/okf/types/RecoveryPlan.md +28 -0
  491. package/dist/okf/types/RedshiftIdcApplication.md +31 -0
  492. package/dist/okf/types/RedshiftSnapshot.md +59 -0
  493. package/dist/okf/types/RegistrationAttachment.md +28 -0
  494. package/dist/okf/types/Registry.md +41 -0
  495. package/dist/okf/types/RegistryRecord.md +38 -0
  496. package/dist/okf/types/RelatedItem.md +24 -0
  497. package/dist/okf/types/ReplicationConfigurationTemplate.md +35 -0
  498. package/dist/okf/types/ReplicationSubnetGroup.md +4 -9
  499. package/dist/okf/types/ReplicationTask.md +14 -14
  500. package/dist/okf/types/ReportGenerator.md +29 -0
  501. package/dist/okf/types/ReportGroup.md +1 -3
  502. package/dist/okf/types/ResilienceHubV2Policy.md +1 -0
  503. package/dist/okf/types/ResilienceHubV2Service.md +1 -0
  504. package/dist/okf/types/ResourceConfiguration.md +1 -1
  505. package/dist/okf/types/ResourceMetricsConfiguration.md +21 -0
  506. package/dist/okf/types/RestApi.md +1 -0
  507. package/dist/okf/types/RetrievalInstruction.md +32 -0
  508. package/dist/okf/types/RouterOutput.md +1 -0
  509. package/dist/okf/types/Rule.md +31 -0
  510. package/dist/okf/types/RunCache.md +29 -0
  511. package/dist/okf/types/Runtime.md +2 -1
  512. package/dist/okf/types/SCNInstance.md +33 -0
  513. package/dist/okf/types/SDBDomain.md +2 -2
  514. package/dist/okf/types/SMBFileShare.md +49 -0
  515. package/dist/okf/types/SageMakerArtifact.md +27 -0
  516. package/dist/okf/types/SageMakerEndpoint.md +0 -1
  517. package/dist/okf/types/ScalableTarget.md +1 -0
  518. package/dist/okf/types/ScalingPlan.md +6 -6
  519. package/dist/okf/types/ScheduledReport.md +31 -0
  520. package/dist/okf/types/SecondaryNetwork.md +26 -0
  521. package/dist/okf/types/SecurityAgentArtifact.md +25 -0
  522. package/dist/okf/types/SecurityAgentIntegration.md +31 -0
  523. package/dist/okf/types/Server.md +1 -0
  524. package/dist/okf/types/ServerlessCache.md +2 -0
  525. package/dist/okf/types/ServerlessCacheSnapshot.md +31 -0
  526. package/dist/okf/types/ServerlessRepoApplication.md +32 -0
  527. package/dist/okf/types/ServiceDiscoveryInstance.md +3 -7
  528. package/dist/okf/types/SipMediaApplication.md +27 -0
  529. package/dist/okf/types/SiteToSiteVpnAttachment.md +2 -0
  530. package/dist/okf/types/SnapshotCopyGrant.md +20 -0
  531. package/dist/okf/types/Solution.md +1 -0
  532. package/dist/okf/types/SourceAssociation.md +25 -0
  533. package/dist/okf/types/SourceCredential.md +6 -6
  534. package/dist/okf/types/SourceNetwork.md +23 -0
  535. package/dist/okf/types/StackFleetAssociation.md +2 -7
  536. package/dist/okf/types/StorageTierPolicy.md +20 -0
  537. package/dist/okf/types/StorageVirtualMachine.md +9 -9
  538. package/dist/okf/types/Stream.md +28 -0
  539. package/dist/okf/types/SubnetCidrReservation.md +26 -0
  540. package/dist/okf/types/SubscriptionProvider.md +26 -0
  541. package/dist/okf/types/SupportPermit.md +29 -0
  542. package/dist/okf/types/Table.md +2 -1
  543. package/dist/okf/types/TableOptimizer.md +5 -10
  544. package/dist/okf/types/Tape.md +31 -0
  545. package/dist/okf/types/Template.md +36 -0
  546. package/dist/okf/types/Tenant.md +2 -0
  547. package/dist/okf/types/Terminology.md +39 -0
  548. package/dist/okf/types/Thesaurus.md +25 -0
  549. package/dist/okf/types/ThingPrincipalAttachment.md +3 -8
  550. package/dist/okf/types/Topic.md +1 -0
  551. package/dist/okf/types/TopicRuleDestination.md +1 -0
  552. package/dist/okf/types/TrafficPolicy.md +26 -0
  553. package/dist/okf/types/TrafficPolicyInstance.md +28 -0
  554. package/dist/okf/types/TranscribeVocabulary.md +27 -0
  555. package/dist/okf/types/TransitGatewayRouteTableAttachment.md +2 -0
  556. package/dist/okf/types/TransitVirtualInterface.md +1 -0
  557. package/dist/okf/types/Tunnel.md +25 -0
  558. package/dist/okf/types/UsageLimit.md +26 -0
  559. package/dist/okf/types/UseCase.md +23 -0
  560. package/dist/okf/types/User.md +47 -0
  561. package/dist/okf/types/VPCEndpoint.md +1 -1
  562. package/dist/okf/types/VPCEndpointService.md +6 -0
  563. package/dist/okf/types/VPCPeeringConnection.md +2 -2
  564. package/dist/okf/types/VPNConcentrator.md +1 -1
  565. package/dist/okf/types/VPNConnection.md +1 -1
  566. package/dist/okf/types/VerifiedDestinationNumber.md +25 -0
  567. package/dist/okf/types/View.md +27 -0
  568. package/dist/okf/types/VoiceConnector.md +30 -0
  569. package/dist/okf/types/Volume.md +44 -0
  570. package/dist/okf/types/Vpc.md +1 -1
  571. package/dist/okf/types/VpcAttachment.md +2 -0
  572. package/dist/okf/types/WebFunction.md +27 -0
  573. package/dist/okf/types/WebFunctionEndpoint.md +44 -0
  574. package/dist/okf/types/WebFunctionRevision.md +32 -0
  575. package/dist/okf/types/WickrNetwork.md +27 -0
  576. package/dist/okf/types/Worker.md +29 -0
  577. package/dist/okf/types/Workforce.md +26 -0
  578. package/dist/okf/types/Workload.md +35 -0
  579. package/dist/okf/types/WorkloadEstimate.md +35 -0
  580. package/dist/okf/types/Workspace.md +28 -0
  581. package/dist/okf/types/Workteam.md +8 -8
  582. package/dist/op/activities/aws-apply.js +214 -0
  583. package/dist/op/activities/floci.js +97 -0
  584. package/dist/op/activities/index.js +32 -0
  585. package/dist/op/builders.js +50 -0
  586. package/dist/ownership.js +52 -0
  587. package/dist/package-cli.js +25 -0
  588. package/dist/parameter.js +19 -0
  589. package/dist/plugin.d.ts.map +1 -1
  590. package/dist/plugin.js +1006 -0
  591. package/dist/properties.js +166 -0
  592. package/dist/pseudo.js +25 -0
  593. package/dist/receipt-store.d.ts.map +1 -1
  594. package/dist/receipt-store.js +233 -0
  595. package/dist/reference-catalog.js +106 -0
  596. package/dist/rules/ext001.ts +3 -3
  597. package/dist/rules/waw016.ts +2 -2
  598. package/dist/serializer.js +529 -0
  599. package/dist/spec/fetch.js +193 -0
  600. package/dist/spec/parse.js +259 -0
  601. package/dist/spec/pin.js +185 -0
  602. package/dist/spec/pinned-types.json +203 -0
  603. package/dist/stack-errors.js +14 -0
  604. package/dist/taggable.d.ts.map +1 -1
  605. package/dist/taggable.js +33 -0
  606. package/dist/teardown.js +199 -0
  607. package/dist/template-sections.js +53 -0
  608. package/dist/template-transform.js +50 -0
  609. package/dist/types/index.d.ts +18847 -1124
  610. package/dist/validate-cli.js +16 -0
  611. package/dist/validate.js +33 -0
  612. package/package.json +9 -9
  613. package/src/agentcore/trace-fetch.ts +2 -1
  614. package/src/api/credential-chain.ts +119 -9
  615. package/src/api/credential-isolation.test.ts +72 -0
  616. package/src/api/read-client-chain.test.ts +159 -0
  617. package/src/api/read-client.ts +42 -6
  618. package/src/api/sigv4.ts +4 -4
  619. package/src/caller-identity.ts +3 -5
  620. package/src/codegen/any-budget.test.ts +12 -2
  621. package/src/codegen/generate.ts +9 -0
  622. package/src/codegen/package.ts +2 -2
  623. package/src/codegen/patches.test.ts +95 -0
  624. package/src/codegen/patches.ts +53 -9
  625. package/src/codegen/property-overlay.json +1 -16
  626. package/src/components/apply.ts +218 -6
  627. package/src/components/cfn-plan-floci.e2e.test.ts +76 -0
  628. package/src/components/cfn-plan.test.ts +225 -16
  629. package/src/components/cloud-executor.ts +73 -25
  630. package/src/deep-observe.ts +2 -1
  631. package/src/disruption.ts +2 -1
  632. package/src/generated/index.d.ts +18847 -1124
  633. package/src/generated/index.ts +1376 -161
  634. package/src/generated/lexicon-aws.json +17359 -784
  635. package/src/identity-observe.ts +2 -1
  636. package/src/lint/post-synth/ext001.ts +3 -3
  637. package/src/lint/post-synth/waw016.ts +2 -2
  638. package/src/lsp/completions.ts +2 -1
  639. package/src/lsp/hover.ts +2 -1
  640. package/src/plugin.ts +2 -1
  641. package/src/receipt-store.ts +2 -1
  642. package/src/spec/pin.ts +3 -3
  643. package/src/spec/pinned-types.json +203 -0
  644. package/src/taggable.ts +2 -2
@@ -21,7 +21,7 @@
21
21
 
22
22
  import { createHash } from "node:crypto";
23
23
  import { readFileSync } from "node:fs";
24
- import type { Capability, CapabilityPlan, DeployContext } from "@intentius/chant/components/capability";
24
+ import type { Capability, CapabilityPlan, DeployContext, PlannedOutputs } from "@intentius/chant/components/capability";
25
25
  import type { ChangeSetEntry, ChangeSetHole, ChangeSetPart } from "@intentius/chant/change-set";
26
26
  import { computePlanDigest } from "@intentius/chant/lifecycle/plan-digest";
27
27
  import { defaultCloudExecutor, type CfnChange, type CfnChangeSet, type CloudExecutor } from "./cloud-executor";
@@ -67,13 +67,17 @@ export function cfnChangeReplaces(change: CfnChange): boolean {
67
67
  */
68
68
  export function cfnChangeEntry(member: string, change: CfnChange): ChangeSetEntry {
69
69
  const attributes = [...new Set(change.details ?? [])].sort().map((path) => ({ path }));
70
+ // A change inside a nested stack (#3997) sits under the nested stacks'
71
+ // logical ids, the way a terraform address sits under its module.
72
+ const module = change.nestedStack?.length ? change.nestedStack.join("/") : undefined;
70
73
  const base = {
71
74
  member,
72
75
  lexicon: "aws",
73
76
  planner: "chant" as const,
74
- address: change.logicalResourceId,
77
+ address: module ? `${module}/${change.logicalResourceId}` : change.logicalResourceId,
75
78
  type: change.resourceType,
76
79
  name: change.logicalResourceId,
80
+ ...(module ? { module } : {}),
77
81
  attributes,
78
82
  };
79
83
  switch (change.action) {
@@ -92,6 +96,66 @@ export function cfnChangeEntry(member: string, change: CfnChange): ChangeSetEntr
92
96
  }
93
97
  }
94
98
 
99
+ /**
100
+ * The changes `onReplace: "block"` refuses: those CloudFormation says replace
101
+ * their resource (`Replacement: True`). `run` and `plan` refuse on the same
102
+ * set, so a plan that passes is one `run` would not block (#3997).
103
+ */
104
+ function blockedReplacements(changes: CfnChange[]): CfnChange[] {
105
+ return changes.filter((c) => c.replacement);
106
+ }
107
+
108
+ /**
109
+ * What a `cfn-deploy` plan hands `run` through `DeployContext.plans[stack]`
110
+ * (#3997): the stack and a digest over the changes the plan's change set
111
+ * proposed. `run` creates its change set, and executes it only when its
112
+ * changes have the same digest.
113
+ */
114
+ export interface CfnPlanArtifact {
115
+ kind: "cfn-deploy-plan";
116
+ stack: string;
117
+ /** {@link cfnChangesDigest} of the planned change set's changes. */
118
+ changesDigest: string;
119
+ }
120
+
121
+ function isCfnPlanArtifact(value: unknown): value is CfnPlanArtifact {
122
+ const v = value as Partial<CfnPlanArtifact> | undefined;
123
+ return v?.kind === "cfn-deploy-plan" && typeof v.stack === "string" && typeof v.changesDigest === "string";
124
+ }
125
+
126
+ /**
127
+ * A digest over a change set's changes as entries, independent of the change
128
+ * set's name and of the order CloudFormation lists them in. A change set with
129
+ * nothing to apply digests as no entries.
130
+ */
131
+ export function cfnChangesDigest(stack: string, changeSet: CfnChangeSet): string {
132
+ const entries = isNoopChangeSet(changeSet) ? [] : changeSet.changes.map((c) => cfnChangeEntry(stack, c));
133
+ entries.sort((a, b) => a.address.localeCompare(b.address) || a.action.localeCompare(b.action));
134
+ return computePlanDigest("cfn-deploy-changes", { stack, entries });
135
+ }
136
+
137
+ /**
138
+ * Thrown by `run` when the change set it created proposes other changes than
139
+ * the change set its plan read (#3997): the stack, the template or the
140
+ * parameters moved between the plan and the apply. The change set is deleted
141
+ * unexecuted. `planMoved` is core's mark for a moved plan
142
+ * (`isPlanMovedError`), so the command that applied it exits 3.
143
+ */
144
+ export class CfnPlanMovedError extends Error {
145
+ readonly planMoved = true;
146
+ constructor(
147
+ public readonly stack: string,
148
+ public readonly planned: string,
149
+ public readonly now: string,
150
+ ) {
151
+ super(
152
+ `cfn-deploy "${stack}": refusing to apply. The change set proposes other changes than the plan that was approved ` +
153
+ `(planned ${planned}, now ${now}); the stack, its template or its parameters changed since. Plan it again.`,
154
+ );
155
+ this.name = "CfnPlanMovedError";
156
+ }
157
+ }
158
+
95
159
  /** The template's bytes as a digest, or `null` when it cannot be read here (the change set then reports why). */
96
160
  function templateDigest(path: string): string | null {
97
161
  try {
@@ -133,7 +197,23 @@ function cfnPlanDigest(
133
197
  * deleted too. A change set with nothing to apply (`isNoopChangeSet`) gives
134
198
  * a member with no entries. A step whose parameters read outputs that are
135
199
  * not known yet, from a stack the same run applies first, creates no change
136
- * set: its member gets a hole saying whose outputs it waits on.
200
+ * set: its member gets a hole saying whose outputs it waits on, and no
201
+ * artifact, so `run` plans it afresh.
202
+ *
203
+ * With `onReplace: "block"` (the default), a change set that replaces a
204
+ * resource fails the plan with {@link CfnReplacementBlockedError}, so the
205
+ * refusal shows in the pull request's plan rather than at apply (#3997).
206
+ * Otherwise the result carries a {@link CfnPlanArtifact} that `run` checks
207
+ * its own change set against.
208
+ *
209
+ * The plan answers the outputs the stack will have (#3999), from the
210
+ * outputs it has now (`DescribeStacks`) and the change set: an output keeps
211
+ * its value when every resource its template `Value` reads keeps its
212
+ * physical resource, which an in-place `Modify` does and an `Add`, a
213
+ * `Remove` or a replacement does not ({@link cfnPlannedOutputs}). An output
214
+ * that reads a parameter, an import or a condition is known only after
215
+ * apply. A wrong guess costs a round, never a wrong apply: a dependent whose
216
+ * input applies as another value is held back and planned again.
137
217
  */
138
218
  async function planCfnDeploy(executor: CloudExecutor, ctx: DeployContext, input: CfnDeployInput): Promise<CapabilityPlan> {
139
219
  const stack = input.stack ?? ctx.component;
@@ -176,15 +256,23 @@ async function planCfnDeploy(executor: CloudExecutor, ctx: DeployContext, input:
176
256
  });
177
257
  try {
178
258
  if (isNoopChangeSet(changeSet)) {
179
- return { member: stack, planDigest, changeSet: { member: member([]), entries: [] } };
259
+ const artifact: CfnPlanArtifact = { kind: "cfn-deploy-plan", stack, changesDigest: cfnChangesDigest(stack, changeSet) };
260
+ // Nothing to apply, so the outputs the stack has are the ones it will have (#3999).
261
+ const standing = await standingOutputs(executor, stack);
262
+ const outputs = standing ? { known: { ...standing }, unknown: [] } : undefined;
263
+ return { member: stack, planDigest, artifact, changeSet: { member: member([]), entries: [] }, ...(outputs ? { outputs } : {}) };
180
264
  }
181
265
  if (changeSet.status === "FAILED") {
182
266
  throw new Error(
183
267
  `cfn-deploy "${stack}": CloudFormation could not create a change set to plan it${changeSet.statusReason ? `: ${changeSet.statusReason}` : ""}`,
184
268
  );
185
269
  }
270
+ const blocked = blockedReplacements(changeSet.changes);
271
+ if (blocked.length > 0 && (input.onReplace ?? "block") === "block") throw new CfnReplacementBlockedError(stack, blocked);
186
272
  const entries = changeSet.changes.map((c) => cfnChangeEntry(stack, c));
187
- return { member: stack, planDigest, changeSet: { member: member([]), entries } };
273
+ const artifact: CfnPlanArtifact = { kind: "cfn-deploy-plan", stack, changesDigest: cfnChangesDigest(stack, changeSet) };
274
+ const outputs = changeSet.stackCreated ? undefined : await plannedStackOutputs(executor, stack, input.template, changeSet.changes);
275
+ return { member: stack, planDigest, artifact, changeSet: { member: member([]), entries }, ...(outputs ? { outputs } : {}) };
188
276
  } finally {
189
277
  await cleanUpPlanChangeSet(executor, changeSet);
190
278
  }
@@ -211,6 +299,118 @@ async function cleanUpPlanChangeSet(executor: CloudExecutor, changeSet: CfnChang
211
299
  }
212
300
  }
213
301
 
302
+ /** The stack's outputs as they stand, or `undefined` when they cannot be read. */
303
+ async function standingOutputs(executor: CloudExecutor, stack: string): Promise<Record<string, string> | undefined> {
304
+ try {
305
+ return { ...(await executor.cloudformation.describeStack(stack)).outputs };
306
+ } catch {
307
+ return undefined;
308
+ }
309
+ }
310
+
311
+ /**
312
+ * What the plan says of the stack's outputs once `changes` apply: none to
313
+ * say for a template with no `Outputs`, which needs no `DescribeStacks`.
314
+ */
315
+ async function plannedStackOutputs(executor: CloudExecutor, stack: string, templatePath: string, changes: readonly CfnChange[]): Promise<PlannedOutputs | undefined> {
316
+ const template = readTemplate(templatePath) as { Outputs?: Record<string, unknown> } | undefined;
317
+ if (!template || typeof template !== "object") return undefined;
318
+ if (Object.keys(template.Outputs ?? {}).length === 0) return { known: {}, unknown: [] };
319
+ return cfnPlannedOutputs(template, changes, await standingOutputs(executor, stack));
320
+ }
321
+
322
+ /** The template at `path` as JSON, or `undefined` when it cannot be read or parsed here. */
323
+ function readTemplate(path: string): unknown {
324
+ try {
325
+ return JSON.parse(readFileSync(path, "utf8"));
326
+ } catch {
327
+ return undefined;
328
+ }
329
+ }
330
+
331
+ /**
332
+ * What a template's `Value` reads (#3999): the logical ids it names through
333
+ * `Ref`, `Fn::GetAtt` and `Fn::Sub`, and whether it reads anything this plan
334
+ * cannot follow (`Fn::ImportValue`, a condition's `Fn::If`, or an intrinsic
335
+ * it does not know).
336
+ */
337
+ function valueReads(value: unknown): { ids: Set<string>; opaque: boolean } {
338
+ const ids = new Set<string>();
339
+ let opaque = false;
340
+ const sub = (text: string): void => {
341
+ for (const m of text.matchAll(/\$\{([^}!][^}]*)\}/g)) ids.add(m[1]!.split(".")[0]!);
342
+ };
343
+ const walk = (v: unknown): void => {
344
+ if (Array.isArray(v)) return v.forEach(walk);
345
+ if (v === null || typeof v !== "object") return;
346
+ const entries = Object.entries(v as Record<string, unknown>);
347
+ if (entries.length === 1) {
348
+ const [key, arg] = entries[0]!;
349
+ if (key === "Ref") {
350
+ if (typeof arg === "string") ids.add(arg);
351
+ else opaque = true;
352
+ return;
353
+ }
354
+ if (key === "Fn::GetAtt") {
355
+ if (Array.isArray(arg) && typeof arg[0] === "string") ids.add(arg[0]);
356
+ else if (typeof arg === "string") ids.add(arg.split(".")[0]!);
357
+ else opaque = true;
358
+ return;
359
+ }
360
+ if (key === "Fn::Sub") {
361
+ if (typeof arg === "string") sub(arg);
362
+ else if (Array.isArray(arg) && typeof arg[0] === "string") {
363
+ const vars = (arg[1] ?? {}) as Record<string, unknown>;
364
+ for (const m of arg[0].matchAll(/\$\{([^}!][^}]*)\}/g)) if (!(m[1]! in vars)) ids.add(m[1]!.split(".")[0]!);
365
+ walk(vars);
366
+ } else opaque = true;
367
+ return;
368
+ }
369
+ if (key === "Fn::Join" || key === "Fn::Select" || key === "Fn::Split" || key === "Fn::Base64") return walk(arg);
370
+ if (key.startsWith("Fn::") || key === "Condition") {
371
+ opaque = true;
372
+ return;
373
+ }
374
+ }
375
+ entries.forEach(([, x]) => walk(x));
376
+ };
377
+ walk(value);
378
+ return { ids, opaque };
379
+ }
380
+
381
+ /**
382
+ * The outputs a stack will have once `changes` apply (#3999), from its
383
+ * template and the outputs it has now. An output is known, at the value it
384
+ * has now, when its `Value` reads only pseudo parameters and resources whose
385
+ * physical resource the change set keeps (no `Add`, `Remove`, `Import` or
386
+ * `Dynamic`, no replacement, no change in a nested stack under it). One
387
+ * that reads a parameter, an import or a condition, one the stack does not
388
+ * have yet, and every output of a template this cannot read, is unknown.
389
+ * `undefined` when there is no template or no standing stack to read from.
390
+ */
391
+ export function cfnPlannedOutputs(template: unknown, changes: readonly CfnChange[], standing: Record<string, string> | undefined): PlannedOutputs | undefined {
392
+ if (!standing || template === null || typeof template !== "object") return undefined;
393
+ const t = template as { Outputs?: Record<string, { Value?: unknown; Condition?: unknown }>; Parameters?: Record<string, unknown> };
394
+ const parameters = new Set(Object.keys(t.Parameters ?? {}));
395
+ const moved = new Set<string>();
396
+ for (const c of changes) {
397
+ const id = c.nestedStack?.[0] ?? c.logicalResourceId;
398
+ const keeps = !c.nestedStack && c.action === "Modify" && !c.replacement && c.replacementMode !== "True" && c.replacementMode !== "Conditional";
399
+ if (!keeps) moved.add(id);
400
+ }
401
+ const known: Record<string, unknown> = {};
402
+ const unknown: string[] = [];
403
+ for (const [name, output] of Object.entries(t.Outputs ?? {}).sort(([a], [b]) => a.localeCompare(b))) {
404
+ const { ids, opaque } = valueReads(output?.Value);
405
+ const stays =
406
+ !opaque && output?.Condition === undefined && name in standing &&
407
+ [...ids].every((id) => id.startsWith("AWS::") || (!parameters.has(id) && !moved.has(id)));
408
+ if (stays) known[name] = standing[name];
409
+ else unknown.push(name);
410
+ }
411
+ return { known, unknown };
412
+ }
413
+
214
414
  /** Whether a CloudFormation error says the stack does not exist. */
215
415
  const STACK_MISSING = /does not exist/i;
216
416
 
@@ -324,6 +524,18 @@ export function createCfnDeployCapability(
324
524
  parameters: flattenCfnInputs(input),
325
525
  });
326
526
 
527
+ // Handed the plan the approver approved (#3997), apply only a change
528
+ // set that proposes the same changes. Anything else means the stack,
529
+ // the template or the parameters moved since the plan.
530
+ const planned = ctx.plans?.[stack];
531
+ if (isCfnPlanArtifact(planned)) {
532
+ const now = cfnChangesDigest(stack, changeSet);
533
+ if (now !== planned.changesDigest) {
534
+ await cleanUpPlanChangeSet(executor, changeSet);
535
+ throw new CfnPlanMovedError(stack, planned.changesDigest, now);
536
+ }
537
+ }
538
+
327
539
  // An unchanged stack yields a change set with nothing to apply.
328
540
  // `execute-change-set` rejects it ("The submitted information didn't
329
541
  // contain changes"), which would fail this component and stop the whole
@@ -340,7 +552,7 @@ export function createCfnDeployCapability(
340
552
  return { stackStatus, outputs };
341
553
  }
342
554
 
343
- const replacements = changeSet.changes.filter((c) => c.replacement);
555
+ const replacements = blockedReplacements(changeSet.changes);
344
556
  let snapshotId: string | undefined;
345
557
 
346
558
  if (replacements.length > 0 && onReplace === "block") {
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `cfn-deploy`'s plan and apply against Floci (#3997), through the real
3
+ * executor (`aws` CLI). Set CHANT_FLOCI_ENDPOINT to a running Floci, for
4
+ * example `docker run -d -p 4611:4566 floci/floci` and
5
+ * `CHANT_FLOCI_ENDPOINT=http://localhost:4611`. Skipped without it.
6
+ */
7
+
8
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
9
+ import { execFileSync } from "node:child_process";
10
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+ import { CfnPlanMovedError, createCfnDeployCapability } from "./apply";
14
+ import { defaultCloudExecutor } from "./cloud-executor";
15
+
16
+ const endpoint = process.env.CHANT_FLOCI_ENDPOINT;
17
+
18
+ describe.skipIf(!endpoint)("cfn-deploy plan against Floci (#3997)", () => {
19
+ const stack = `chant-plan-${process.pid}-${Date.now()}`;
20
+ const ctx = { env: "local", component: stack };
21
+ const saved = { ...process.env };
22
+ let dir: string;
23
+ let template: string;
24
+ const aws = (...args: string[]): string => execFileSync("aws", ["--endpoint-url", endpoint!, ...args], { encoding: "utf8" });
25
+ // The change sets still open on the stack. Floci also lists executed ones, as EXECUTE_COMPLETE.
26
+ const changeSets = (): string[] =>
27
+ ((JSON.parse(aws("cloudformation", "list-change-sets", "--stack-name", stack)) as { Summaries?: Array<{ ChangeSetName: string; ExecutionStatus?: string }> }).Summaries ?? [])
28
+ .filter((c) => c.ExecutionStatus !== "EXECUTE_COMPLETE")
29
+ .map((c) => c.ChangeSetName);
30
+ const write = (resources: Record<string, unknown>): void => writeFileSync(template, JSON.stringify({ Resources: resources }));
31
+ const topic = (name: string) => ({ Type: "AWS::SNS::Topic", Properties: { DisplayName: name } });
32
+
33
+ beforeAll(() => {
34
+ Object.assign(process.env, { AWS_ENDPOINT_URL: endpoint, AWS_ACCESS_KEY_ID: "test", AWS_SECRET_ACCESS_KEY: "test", AWS_REGION: "us-east-1" });
35
+ dir = mkdtempSync(join(tmpdir(), "cfn-plan-floci-"));
36
+ template = join(dir, "stack.template.json");
37
+ });
38
+
39
+ afterAll(() => {
40
+ try {
41
+ aws("cloudformation", "delete-stack", "--stack-name", stack);
42
+ } catch { /* never created */ }
43
+ process.env = saved;
44
+ rmSync(dir, { recursive: true, force: true });
45
+ });
46
+
47
+ it("plans a create, an update and no change, applies what it planned, and refuses a plan the template moved past", async () => {
48
+ const capability = createCfnDeployCapability(defaultCloudExecutor());
49
+
50
+ write({ Topic: topic("a") });
51
+ const created = await capability.plan!(ctx, { stack, template });
52
+ expect(created.changeSet!.entries.map((e) => [e.address, e.action])).toEqual([["Topic", "create"]]);
53
+ // The plan left no stack behind: the empty one its CREATE change set made is gone.
54
+ await expect(capability.outputs!(ctx, { stack, template })).resolves.toEqual({});
55
+ const first = await capability.run({ ...ctx, plans: { [stack]: created.artifact } }, { stack, template });
56
+ expect(first.stackStatus).toBe("CREATE_COMPLETE");
57
+
58
+ write({ Topic: topic("b"), Queue: { Type: "AWS::SQS::Queue" } });
59
+ const updated = await capability.plan!(ctx, { stack, template });
60
+ expect(updated.changeSet!.entries.map((e) => [e.address, e.action]).sort()).toEqual([["Queue", "create"], ["Topic", "update"]]);
61
+ expect(changeSets()).toEqual([]);
62
+
63
+ // The template moves after the plan: run refuses and leaves no change set.
64
+ write({ Topic: topic("b"), Queue: { Type: "AWS::SQS::Queue" }, Dlq: { Type: "AWS::SQS::Queue" } });
65
+ await expect(capability.run({ ...ctx, plans: { [stack]: updated.artifact } }, { stack, template })).rejects.toBeInstanceOf(CfnPlanMovedError);
66
+ expect(changeSets()).toEqual([]);
67
+
68
+ write({ Topic: topic("b"), Queue: { Type: "AWS::SQS::Queue" } });
69
+ const second = await capability.run({ ...ctx, plans: { [stack]: updated.artifact } }, { stack, template });
70
+ expect(second.stackStatus).toBe("UPDATE_COMPLETE");
71
+
72
+ const same = await capability.plan!(ctx, { stack, template });
73
+ expect(same.changeSet!.entries).toEqual([]);
74
+ expect(changeSets()).toEqual([]);
75
+ }, 600_000); // each `aws` call takes seconds, and a plan or a run makes several
76
+ });
@@ -11,11 +11,14 @@ import { join } from "node:path";
11
11
  import { CapabilityRegistry } from "@intentius/chant/components/capability";
12
12
  import type { DriverComponent } from "@intentius/chant/components/driver";
13
13
  import { planFanOut } from "@intentius/chant/components/fan-out";
14
- import { planPrSet, prReport } from "@intentius/chant/components/pr-run";
14
+ import { applyPrSet, planPrSet, prReport } from "@intentius/chant/components/pr-run";
15
15
  import { composeChangeSet } from "@intentius/chant/change-set";
16
16
  import { groupChangeSet, planSummaryHeadline } from "@intentius/chant/plan-summary";
17
17
  import { memberCounts, prStatusDescription, renderPrNote } from "@intentius/chant/pr-loop";
18
- import { createCfnDeployCapability } from "./apply";
18
+ import { CfnPlanMovedError, CfnReplacementBlockedError, cfnChangeEntry, createCfnDeployCapability, type CfnDeployInput } from "./apply";
19
+ import { isPlanMovedError } from "@intentius/chant/components/capability";
20
+ import { planMovedIn } from "@intentius/chant/components/driver";
21
+ import { readCfnChanges, templateHasNestedStacks, type DescribedCfnChangeSet } from "./cloud-executor";
19
22
  import { createMockCloudExecutor, type FakeStackConfig } from "./__tests__/mock-cloud-executor";
20
23
 
21
24
  let dir: string;
@@ -31,10 +34,10 @@ afterEach(() => rmSync(dir, { recursive: true, force: true }));
31
34
 
32
35
  const ctx = { env: "local", component: "network" };
33
36
 
34
- async function planOne(config: FakeStackConfig) {
37
+ async function planOne(config: FakeStackConfig, extra: Partial<CfnDeployInput> = {}) {
35
38
  const mock = createMockCloudExecutor({ stacks: { network: config } });
36
39
  const capability = createCfnDeployCapability(mock.executor);
37
- const planned = await capability.plan!(ctx, { stack: "network", template });
40
+ const planned = await capability.plan!(ctx, { stack: "network", template, ...extra });
38
41
  const doc = composeChangeSet([planned.changeSet!]);
39
42
  return { mock, planned, doc, counts: memberCounts(doc, "network") };
40
43
  }
@@ -77,17 +80,51 @@ describe("cfn-deploy plan (#4084)", () => {
77
80
  { action: "Modify", logicalResourceId: "Topic", resourceType: "AWS::SNS::Topic", replacement: false, replacementMode: "Conditional" },
78
81
  { action: "Remove", logicalResourceId: "Old", resourceType: "AWS::SQS::Queue", replacement: false },
79
82
  ],
80
- });
83
+ }, { onReplace: "allow" });
81
84
  expect(counts).toEqual({ create: 0, update: 0, replace: 2, delete: 1 });
82
85
  expect(planSummaryHeadline(groupChangeSet(doc))).toContain("3 destroys or replacements");
83
86
  });
84
87
 
85
- it("a change set with nothing to apply is a member with no entries and no holes", async () => {
86
- const { counts, doc, mock } = await planOne({ noChanges: true });
88
+ it("a change set with nothing to apply is a member with no entries and no holes, whose outputs are the stack's own (#3999)", async () => {
89
+ const { counts, doc, mock, planned } = await planOne({ noChanges: true, outputs: { VpcId: "vpc-1" } });
87
90
  expect(counts).toEqual({ create: 0, update: 0, replace: 0, delete: 0 });
88
91
  expect(doc.members[0]).toMatchObject({ status: "planned", holes: [] });
89
92
  expect(groupChangeSet(doc, { unit: "member" }).groups[0].noChanges).toBe(true);
90
- expect(methods(mock)).toEqual(["createChangeSet", "deleteChangeSet"]);
93
+ expect(planned.outputs).toEqual({ known: { VpcId: "vpc-1" }, unknown: [] });
94
+ expect(methods(mock)).toEqual(["createChangeSet", "describeStack", "deleteChangeSet"]);
95
+ });
96
+
97
+ it("a change set with changes keeps the outputs of resources it keeps, and not of the ones it adds or replaces (#3999)", async () => {
98
+ writeFileSync(
99
+ template,
100
+ JSON.stringify({
101
+ Parameters: { Env: { Type: "String" } },
102
+ Resources: { Topic: { Type: "AWS::SNS::Topic" }, Queue: { Type: "AWS::SQS::Queue" }, Table: { Type: "AWS::DynamoDB::Table" } },
103
+ Outputs: {
104
+ TopicArn: { Value: { Ref: "Topic" } },
105
+ QueueUrl: { Value: { "Fn::GetAtt": ["Queue", "QueueUrl"] } },
106
+ TableArn: { Value: { "Fn::Sub": "${Table.Arn}" } },
107
+ Region: { Value: { "Fn::Sub": "${AWS::Region}" } },
108
+ EnvName: { Value: { Ref: "Env" } },
109
+ Imported: { Value: { "Fn::ImportValue": "x" } },
110
+ },
111
+ }),
112
+ );
113
+ const standing = { TopicArn: "arn:topic", QueueUrl: "https://q", TableArn: "arn:table", Region: "us-east-1", EnvName: "prod", Imported: "x" };
114
+ const { planned } = await planOne({
115
+ outputs: standing,
116
+ changes: [
117
+ { action: "Modify", logicalResourceId: "Topic", resourceType: "AWS::SNS::Topic", replacement: false, replacementMode: "False" },
118
+ { action: "Modify", logicalResourceId: "Queue", resourceType: "AWS::SQS::Queue", replacement: true, replacementMode: "True" },
119
+ { action: "Modify", logicalResourceId: "Table", resourceType: "AWS::DynamoDB::Table", replacement: false, replacementMode: "Conditional" },
120
+ ],
121
+ }, { onReplace: "allow" });
122
+ expect(planned.outputs).toEqual({ known: { Region: "us-east-1", TopicArn: "arn:topic" }, unknown: ["EnvName", "Imported", "QueueUrl", "TableArn"] });
123
+ });
124
+
125
+ it("a stack the change set creates says nothing of its outputs (#3999)", async () => {
126
+ const { planned } = await planOne({ missing: true, changes: [{ action: "Add", logicalResourceId: "Topic", resourceType: "AWS::SNS::Topic", replacement: false }] });
127
+ expect(planned.outputs).toBeUndefined();
91
128
  });
92
129
 
93
130
  it("a stack that does not exist yet counts every resource as a create, and the empty stack the CREATE change set made is deleted", async () => {
@@ -175,21 +212,51 @@ describe("pr-plan over cfn-deploy members (#4084)", () => {
175
212
 
176
213
  it("counts each member's changes from its change set, and the note and status line show them", async () => {
177
214
  const { set, report } = await planBoth({
178
- network: {
179
- outputs: { TopicArn: "arn:topic" },
180
- changes: [{ action: "Modify", logicalResourceId: "Topic", resourceType: "AWS::SNS::Topic", replacement: false, details: ["DisplayName"] }],
181
- },
215
+ network: { outputs: { TopicArn: "arn:topic" }, noChanges: true },
182
216
  "cluster-a": { changes: [{ action: "Add", logicalResourceId: "clusterAQueue", resourceType: "AWS::SQS::Queue", replacement: false }] },
183
217
  });
184
218
  expect(set.members.map((m) => [m.member, m.counts])).toEqual([
185
219
  ["cluster-a", { create: 1, update: 0, replace: 0, delete: 0 }],
186
- ["network", { create: 0, update: 1, replace: 0, delete: 0 }],
220
+ ["network", { create: 0, update: 0, replace: 0, delete: 0 }],
187
221
  ]);
222
+ // network changes nothing, so its plan knows its outputs: the ones it has (#3999).
223
+ expect(set.plannedOutputs).toEqual({ network: ["TopicArn"] });
188
224
  const note = renderPrNote(report);
189
- expect(note).toContain("| `network` | network | planned | 0 | 1 | 0 | 0 |");
225
+ expect(note).toContain("| `network` | network | planned | 0 | 0 | 0 | 0 |");
190
226
  expect(note).toContain("| `cluster-a` | cluster-a | planned | 1 | 0 | 0 | 0 |");
191
- expect(note).not.toContain("no changes");
192
- expect(prStatusDescription(report)).toMatch(/^2 members: 2 to change, 0 to destroy or replace;/);
227
+ expect(prStatusDescription(report)).toMatch(/^2 members: 1 to change, 0 to destroy or replace;/);
228
+ });
229
+
230
+ it("a stack whose upstream changes in the same pull request reads its outputs as known only after apply (#3999)", async () => {
231
+ const { set, report, mock } = await planBoth({
232
+ network: {
233
+ outputs: { TopicArn: "arn:topic" },
234
+ changes: [{ action: "Modify", logicalResourceId: "Topic", resourceType: "AWS::SNS::Topic", replacement: false, details: ["DisplayName"] }],
235
+ },
236
+ "cluster-a": { changes: [{ action: "Add", logicalResourceId: "clusterAQueue", resourceType: "AWS::SQS::Queue", replacement: false }] },
237
+ });
238
+ // network's template has no Outputs to say TopicArn stays, so it is not the TopicArn standing now.
239
+ expect(mock.calls.filter((c) => c.method === "describeStack")).toEqual([]);
240
+ expect(set.plannedOutputs).toEqual({ network: [] });
241
+ expect(set.members.find((m) => m.member === "cluster-a")?.unknownInputs).toEqual(["network.TopicArn"]);
242
+ const note = renderPrNote(report);
243
+ expect(note).toContain("| `cluster-a` | cluster-a | planned; inputs from network; re-checked on apply | ? | ? | ? | ? |");
244
+ expect(note).toContain("Known only after apply: `cluster-a` reads `network.TopicArn`.");
245
+ });
246
+
247
+ it("a stack whose upstream changes a resource in place reads the output that resource keeps, at its value now (#3999)", async () => {
248
+ writeFileSync(template, JSON.stringify({ Resources: { Topic: { Type: "AWS::SNS::Topic" } }, Outputs: { TopicArn: { Value: { Ref: "Topic" } } } }));
249
+ const { set, mock } = await planBoth({
250
+ network: {
251
+ outputs: { TopicArn: "arn:topic" },
252
+ changes: [{ action: "Modify", logicalResourceId: "Topic", resourceType: "AWS::SNS::Topic", replacement: false, details: ["DisplayName"] }],
253
+ },
254
+ "cluster-a": { outputs: {}, changes: [{ action: "Add", logicalResourceId: "clusterAQueue", resourceType: "AWS::SQS::Queue", replacement: false }] },
255
+ });
256
+ expect(set.plannedOutputs).toEqual({ network: ["TopicArn"] });
257
+ expect(set.members.find((m) => m.member === "cluster-a")?.unknownInputs).toBeUndefined();
258
+ const created = mock.calls.filter((c) => c.method === "createChangeSet").map((c) => c.args as { stackName: string; parameters?: Record<string, string> });
259
+ expect(created.find((c) => c.stackName === "cluster-a")?.parameters).toEqual({ TopicArn: "arn:topic" });
193
260
  });
194
261
 
195
262
  it("a member whose inputs come from a stack the run creates first says so, with no counts and no change set", async () => {
@@ -222,3 +289,145 @@ describe("pr-plan over cfn-deploy members (#4084)", () => {
222
289
  expect(replaced.set.doc.digest).not.toBe(first.set.doc.digest);
223
290
  });
224
291
  });
292
+
293
+ describe("cfn-deploy applies the change set it planned (#3997)", () => {
294
+ const topicUpdate = { action: "Modify" as const, logicalResourceId: "Topic", resourceType: "AWS::SNS::Topic", replacement: false, replacementMode: "False" as const, details: ["DisplayName"] };
295
+ const queueAdd = { action: "Add" as const, logicalResourceId: "Queue", resourceType: "AWS::SQS::Queue", replacement: false };
296
+
297
+ it("a plan hands run an artifact over its changes, and run executes a change set with the same changes", async () => {
298
+ const mock = createMockCloudExecutor({ stacks: { network: { changes: [topicUpdate] } } });
299
+ const capability = createCfnDeployCapability(mock.executor);
300
+ const planned = await capability.plan!(ctx, { stack: "network", template });
301
+ expect(planned.artifact).toMatchObject({ kind: "cfn-deploy-plan", stack: "network" });
302
+ const out = await capability.run({ ...ctx, plans: { network: planned.artifact } }, { stack: "network", template });
303
+ expect(out.stackStatus).toBe("UPDATE_COMPLETE");
304
+ expect(methods(mock)).toEqual(["createChangeSet", "deleteChangeSet", "createChangeSet", "executeChangeSet", "waitForStack"]);
305
+ });
306
+
307
+ it("run refuses, deletes its change set unexecuted and marks the error as a moved plan when the changes differ from the plan's", async () => {
308
+ const mock = createMockCloudExecutor({ stacks: { network: { changes: [topicUpdate] } } });
309
+ const capability = createCfnDeployCapability(mock.executor);
310
+ const planned = await capability.plan!(ctx, { stack: "network", template });
311
+ mock.setStack("network", { changes: [topicUpdate, queueAdd] });
312
+ const err = await capability.run({ ...ctx, plans: { network: planned.artifact } }, { stack: "network", template }).catch((e: unknown) => e);
313
+ expect(err).toBeInstanceOf(CfnPlanMovedError);
314
+ expect(isPlanMovedError(err)).toBe(true);
315
+ expect((err as Error).message).toMatch(/refusing to apply.*Plan it again/);
316
+ expect(methods(mock).slice(2)).toEqual(["createChangeSet", "deleteChangeSet"]);
317
+ });
318
+
319
+ it("a plan of no changes refuses a run whose change set now has some, and accepts one that still has none", async () => {
320
+ const mock = createMockCloudExecutor({ stacks: { network: { noChanges: true } } });
321
+ const capability = createCfnDeployCapability(mock.executor);
322
+ const planned = await capability.plan!(ctx, { stack: "network", template });
323
+ await expect(capability.run({ ...ctx, plans: { network: planned.artifact } }, { stack: "network", template })).resolves.toMatchObject({ stackStatus: "UPDATE_COMPLETE" });
324
+ mock.setStack("network", { noChanges: false, changes: [queueAdd] });
325
+ await expect(capability.run({ ...ctx, plans: { network: planned.artifact } }, { stack: "network", template })).rejects.toBeInstanceOf(CfnPlanMovedError);
326
+ expect(methods(mock)).not.toContain("executeChangeSet");
327
+ });
328
+
329
+ it("a run whose CREATE change set moved deletes the empty stack it made, too", async () => {
330
+ const mock = createMockCloudExecutor({ stacks: { network: { missing: true, changes: [queueAdd] } } });
331
+ const capability = createCfnDeployCapability(mock.executor);
332
+ const planned = await capability.plan!(ctx, { stack: "network", template });
333
+ mock.setStack("network", { changes: [queueAdd, { ...queueAdd, logicalResourceId: "Dlq" }] });
334
+ await expect(capability.run({ ...ctx, plans: { network: planned.artifact } }, { stack: "network", template })).rejects.toBeInstanceOf(CfnPlanMovedError);
335
+ expect(methods(mock).slice(3)).toEqual(["createChangeSet", "deleteChangeSet", "deleteStackShell"]);
336
+ });
337
+
338
+ it("a member with holes hands run no artifact, so run plans it afresh", async () => {
339
+ const mock = createMockCloudExecutor({ stacks: { network: { changes: [queueAdd] } } });
340
+ const capability = createCfnDeployCapability(mock.executor);
341
+ const planned = await capability.plan!({ ...ctx, unresolvedInputs: ["upstream"] }, { stack: "network", template });
342
+ expect(planned.artifact).toBeUndefined();
343
+ });
344
+
345
+ it('onReplace "block" refuses at plan time, naming the resources, and the change set is still deleted', async () => {
346
+ const mock = createMockCloudExecutor({
347
+ stacks: { network: { changes: [{ action: "Modify", logicalResourceId: "Table", resourceType: "AWS::DynamoDB::Table", replacement: true, replacementMode: "True" }] } },
348
+ });
349
+ const capability = createCfnDeployCapability(mock.executor);
350
+ const err = await capability.plan!(ctx, { stack: "network", template }).catch((e: unknown) => e);
351
+ expect(err).toBeInstanceOf(CfnReplacementBlockedError);
352
+ expect((err as Error).message).toContain("(Table)");
353
+ expect(methods(mock)).toEqual(["createChangeSet", "deleteChangeSet"]);
354
+ });
355
+
356
+ it('onReplace "block" lets a Conditional replacement plan, as run does', async () => {
357
+ const { counts } = await planOne({
358
+ changes: [{ action: "Modify", logicalResourceId: "Topic", resourceType: "AWS::SNS::Topic", replacement: false, replacementMode: "Conditional" }],
359
+ });
360
+ expect(counts.replace).toBe(1);
361
+ });
362
+
363
+ it("pr-apply over a moved stack fails the member with the refusal, executes nothing, and the run reads as a moved plan", async () => {
364
+ const mock = createMockCloudExecutor({ stacks: { network: { changes: [topicUpdate] } } });
365
+ const registry = new CapabilityRegistry();
366
+ registry.register(createCfnDeployCapability(mock.executor));
367
+ const components: DriverComponent[] = [
368
+ { name: "network", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "cfn-deploy", stack: "network", template }] }] },
369
+ ];
370
+ const plan = planFanOut({ components, changed: ["network"] });
371
+ const set = await planPrSet({ components, plan, registry, env: "local" });
372
+ expect(set.plans.network?.network).toMatchObject({ kind: "cfn-deploy-plan" });
373
+
374
+ mock.setStack("network", { changes: [queueAdd] });
375
+ const moved = await applyPrSet({ components, plan, registry, env: "local", set });
376
+ expect(moved.status).toBe("failed");
377
+ expect(planMovedIn(moved.run.results)).toBe(true);
378
+ expect(moved.members[0]).toMatchObject({ member: "network", status: "failed" });
379
+ expect(methods(mock)).not.toContain("executeChangeSet");
380
+
381
+ mock.setStack("network", { changes: [topicUpdate] });
382
+ const applied = await applyPrSet({ components, plan, registry, env: "local", set });
383
+ expect(applied.status).toBe("applied");
384
+ expect(planMovedIn(applied.run.results)).toBe(false);
385
+ expect(methods(mock)).toContain("executeChangeSet");
386
+ });
387
+ });
388
+
389
+ describe("cfn-deploy plans nested stacks (#3997)", () => {
390
+ it("reads a nested change set's changes under the nested stack's logical id", async () => {
391
+ const root: DescribedCfnChangeSet = {
392
+ Status: "CREATE_COMPLETE",
393
+ Changes: [
394
+ { ResourceChange: { Action: "Modify", LogicalResourceId: "Child", ResourceType: "AWS::CloudFormation::Stack", Replacement: "False", ChangeSetId: "arn:child" } },
395
+ { ResourceChange: { Action: "Add", LogicalResourceId: "Queue", ResourceType: "AWS::SQS::Queue" } },
396
+ ],
397
+ };
398
+ const nested: Record<string, DescribedCfnChangeSet> = {
399
+ "arn:child": {
400
+ Status: "CREATE_COMPLETE",
401
+ Changes: [
402
+ { ResourceChange: { Action: "Modify", LogicalResourceId: "Grand", ResourceType: "AWS::CloudFormation::Stack", Replacement: "False", ChangeSetId: "arn:grand" } },
403
+ { ResourceChange: { Action: "Modify", LogicalResourceId: "Topic", ResourceType: "AWS::SNS::Topic", Replacement: "True", Details: [{ Target: { Name: "TopicName" } }] } },
404
+ ],
405
+ },
406
+ "arn:grand": { Status: "CREATE_COMPLETE", Changes: [{ ResourceChange: { Action: "Remove", LogicalResourceId: "Old", ResourceType: "AWS::SQS::Queue" } }] },
407
+ };
408
+ const described: string[] = [];
409
+ const changes = await readCfnChanges(root, async (id) => {
410
+ described.push(id);
411
+ return nested[id]!;
412
+ });
413
+ expect(described).toEqual(["arn:child", "arn:grand"]);
414
+ expect(changes.map((c) => [c.logicalResourceId, c.nestedStack ?? []])).toEqual([
415
+ ["Child", []],
416
+ ["Grand", ["Child"]],
417
+ ["Old", ["Child", "Grand"]],
418
+ ["Topic", ["Child"]],
419
+ ["Queue", []],
420
+ ]);
421
+ const entries = changes.map((c) => cfnChangeEntry("app", c));
422
+ expect(entries.find((e) => e.name === "Topic")).toMatchObject({ address: "Child/Topic", module: "Child", action: "replace" });
423
+ expect(entries.find((e) => e.name === "Old")).toMatchObject({ address: "Child/Grand/Old", module: "Child/Grand", action: "delete" });
424
+ expect(entries.find((e) => e.name === "Queue")).toMatchObject({ address: "Queue", action: "create" });
425
+ expect(entries.find((e) => e.name === "Queue")).not.toHaveProperty("module");
426
+ });
427
+
428
+ it("asks for nested change sets only for a template that declares a nested stack", () => {
429
+ expect(templateHasNestedStacks(JSON.stringify({ Resources: { Child: { Type: "AWS::CloudFormation::Stack" } } }))).toBe(true);
430
+ expect(templateHasNestedStacks(JSON.stringify({ Resources: { Topic: { Type: "AWS::SNS::Topic" } } }))).toBe(false);
431
+ expect(templateHasNestedStacks("not json")).toBe(false);
432
+ });
433
+ });