@fjall/deploy-core 2.34.0 → 3.1.0

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 (209) hide show
  1. package/dist/.minified +1 -1
  2. package/dist/src/aws/SimpleAwsProvider.js +1 -1
  3. package/dist/src/aws/cloudtrail/orgTrailDelivery.js +1 -1
  4. package/dist/src/aws/organisations/accountGlobals.js +1 -1
  5. package/dist/src/aws/organisations/accounts.js +1 -1
  6. package/dist/src/aws/organisations/backup.js +2 -2
  7. package/dist/src/aws/organisations/costAllocation.js +1 -1
  8. package/dist/src/aws/organisations/delegatedAdmin.js +2 -2
  9. package/dist/src/aws/organisations/identityCentre.js +1 -1
  10. package/dist/src/aws/organisations/identityCentreSnapshot.js +1 -1
  11. package/dist/src/aws/organisations/importedAccounts.js +1 -1
  12. package/dist/src/aws/organisations/ipam.js +1 -1
  13. package/dist/src/aws/organisations/oidcProvider.js +1 -1
  14. package/dist/src/aws/organisations/organisation.js +1 -1
  15. package/dist/src/aws/organisations/organisationalUnits.js +1 -1
  16. package/dist/src/aws/organisations/pagination.js +1 -1
  17. package/dist/src/aws/organisations/policies.js +1 -1
  18. package/dist/src/aws/organisations/ram.js +1 -1
  19. package/dist/src/aws/organisations/rootAccess.js +3 -3
  20. package/dist/src/aws/organisations/serviceAccess.js +1 -1
  21. package/dist/src/aws/organisations/trustedAccess.js +1 -1
  22. package/dist/src/aws/organisations/types.js +1 -1
  23. package/dist/src/aws/regions/regionOptStatus.js +1 -1
  24. package/dist/src/aws/sts/assumeRoot.js +1 -1
  25. package/dist/src/aws/targetReadiness.d.ts +7 -4
  26. package/dist/src/aws/targetReadiness.js +1 -1
  27. package/dist/src/aws/targetSetAdvisory.js +1 -1
  28. package/dist/src/aws/utils/arnParser.js +1 -1
  29. package/dist/src/aws/utils/awsErrorHandler.js +1 -1
  30. package/dist/src/aws/utils/cloudformationEventHelpers.js +1 -1
  31. package/dist/src/aws/utils/cloudformationEvents.js +1 -1
  32. package/dist/src/index.d.ts +4 -2
  33. package/dist/src/index.js +1 -1
  34. package/dist/src/orchestration/activeDeploymentGuard.js +5 -5
  35. package/dist/src/orchestration/application/applicationDeploy.js +1 -1
  36. package/dist/src/orchestration/application/applicationDeployHelpers.js +3 -3
  37. package/dist/src/orchestration/application/applicationDestroy.js +1 -1
  38. package/dist/src/orchestration/application/approvalGate.js +1 -1
  39. package/dist/src/orchestration/application/artefactCollection.js +1 -1
  40. package/dist/src/orchestration/application/buildArgResolver.js +1 -1
  41. package/dist/src/orchestration/application/buildGroupValidator.js +1 -1
  42. package/dist/src/orchestration/application/buildSecretResolver.js +1 -1
  43. package/dist/src/orchestration/application/buildSecretSession.js +1 -1
  44. package/dist/src/orchestration/application/codeOnlyDeploy.js +1 -1
  45. package/dist/src/orchestration/application/databaseEndpointReconcile.js +1 -1
  46. package/dist/src/orchestration/application/deploySessionPolicy.js +1 -1
  47. package/dist/src/orchestration/application/destroyPreFlight.js +1 -1
  48. package/dist/src/orchestration/application/destructionGate.js +3 -3
  49. package/dist/src/orchestration/application/detectionPipeline.js +1 -1
  50. package/dist/src/orchestration/application/dockerBuildHelper.js +1 -1
  51. package/dist/src/orchestration/application/ecrCacheRepository.js +1 -1
  52. package/dist/src/orchestration/application/lambdaCodeOnlyRollout.js +1 -1
  53. package/dist/src/orchestration/application/leaseGate.js +1 -1
  54. package/dist/src/orchestration/application/openNextBuild.js +3 -3
  55. package/dist/src/orchestration/application/plan/approvalToken.js +1 -1
  56. package/dist/src/orchestration/application/plan/assemblyDigest.js +2 -2
  57. package/dist/src/orchestration/application/plan/buildDeployPlan.js +1 -1
  58. package/dist/src/orchestration/application/plan/changeSetEscalation.js +1 -1
  59. package/dist/src/orchestration/application/plan/classify.js +1 -1
  60. package/dist/src/orchestration/application/plan/computeDeployPlan.js +1 -1
  61. package/dist/src/orchestration/application/plan/loadDeployPlan.js +1 -1
  62. package/dist/src/orchestration/application/plan/pinnedNames.js +1 -1
  63. package/dist/src/orchestration/application/plan/registryOverlay.js +1 -1
  64. package/dist/src/orchestration/application/plan/renderDeployPlan.js +1 -1
  65. package/dist/src/orchestration/application/serviceImageTagsRollback.js +1 -1
  66. package/dist/src/orchestration/application/taskDefinitionRoll.js +1 -1
  67. package/dist/src/orchestration/application/welcomeImageHelper.js +1 -1
  68. package/dist/src/orchestration/builders/dockerBuilder.js +1 -1
  69. package/dist/src/orchestration/builders/frameworkRegistry.js +1 -1
  70. package/dist/src/orchestration/builders/openNextBuilder.js +1 -1
  71. package/dist/src/orchestration/builders/staticSiteBuilder.js +2 -2
  72. package/dist/src/orchestration/contextHelpers.js +1 -1
  73. package/dist/src/orchestration/deploy.js +1 -1
  74. package/dist/src/orchestration/destroy.js +1 -1
  75. package/dist/src/orchestration/domain/caaPreflight.d.ts +56 -0
  76. package/dist/src/orchestration/domain/caaPreflight.js +1 -0
  77. package/dist/src/orchestration/domain/delegatedDomainDeploy.d.ts +79 -0
  78. package/dist/src/orchestration/domain/delegatedDomainDeploy.js +1 -0
  79. package/dist/src/orchestration/domain/delegationDestroyMirror.d.ts +141 -0
  80. package/dist/src/orchestration/domain/delegationDestroyMirror.js +1 -0
  81. package/dist/src/orchestration/domain/index.d.ts +10 -0
  82. package/dist/src/orchestration/domain/index.js +1 -0
  83. package/dist/src/orchestration/domain/nsPropagationGate.d.ts +59 -0
  84. package/dist/src/orchestration/domain/nsPropagationGate.js +1 -0
  85. package/dist/src/orchestration/domain/types.d.ts +75 -0
  86. package/dist/src/orchestration/domain/types.js +1 -0
  87. package/dist/src/orchestration/domainInterface.d.ts +10 -0
  88. package/dist/src/orchestration/drift/classifyDriftFailure.js +1 -1
  89. package/dist/src/orchestration/drift/detectStackDrift.js +1 -1
  90. package/dist/src/orchestration/drift/driftPreFlight.js +5 -5
  91. package/dist/src/orchestration/drift/driftProbe.d.ts +12 -6
  92. package/dist/src/orchestration/drift/driftProbe.js +1 -1
  93. package/dist/src/orchestration/drift/driftSuspectJournal.js +1 -1
  94. package/dist/src/orchestration/drift/preFlightGate.d.ts +10 -2
  95. package/dist/src/orchestration/drift/preFlightGate.js +2 -2
  96. package/dist/src/orchestration/drift/route53Names.d.ts +35 -0
  97. package/dist/src/orchestration/drift/route53Names.js +1 -0
  98. package/dist/src/orchestration/drift/route53RecordPreflight.d.ts +105 -0
  99. package/dist/src/orchestration/drift/route53RecordPreflight.js +2 -0
  100. package/dist/src/orchestration/index.d.ts +3 -1
  101. package/dist/src/orchestration/index.js +1 -1
  102. package/dist/src/orchestration/manifestSecretParser.js +1 -1
  103. package/dist/src/orchestration/organisation/accountsConfig.js +1 -1
  104. package/dist/src/orchestration/organisation/cascadeDestroyHelpers.js +1 -1
  105. package/dist/src/orchestration/organisation/cascadeHelpers.d.ts +18 -1
  106. package/dist/src/orchestration/organisation/cascadeHelpers.js +1 -1
  107. package/dist/src/orchestration/organisation/cascadeSummary.js +1 -1
  108. package/dist/src/orchestration/organisation/organisationDeploy.js +1 -1
  109. package/dist/src/orchestration/organisation/organisationDestroy.js +2 -2
  110. package/dist/src/orchestration/organisation/organisationSetup.js +1 -1
  111. package/dist/src/orchestration/organisation/reconcileProviderAccounts.js +1 -1
  112. package/dist/src/orchestration/organisationDeploy/cascadeExecution.js +1 -1
  113. package/dist/src/orchestration/organisationDeploy/infraSteps.js +1 -1
  114. package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.js +3 -3
  115. package/dist/src/orchestration/organisationDeploy/orgContext.js +1 -1
  116. package/dist/src/orchestration/organisationDeploy/resolveCascadeAccounts.js +1 -1
  117. package/dist/src/orchestration/organisationDeploy/singleComponentDeploy.js +1 -1
  118. package/dist/src/orchestration/organisationDeploy/trailReconciliation.js +1 -1
  119. package/dist/src/orchestration/probes/deleteBlockProbes.js +1 -1
  120. package/dist/src/orchestration/remediation/driftRepairGate.js +1 -1
  121. package/dist/src/orchestration/remediation/forensicsCapture.js +1 -1
  122. package/dist/src/orchestration/remediation/forgetResource.js +1 -1
  123. package/dist/src/orchestration/remediation/pinRemediation.js +1 -1
  124. package/dist/src/orchestration/remediation/postFailureRepairOffer.js +8 -8
  125. package/dist/src/orchestration/remediation/recreatePreFlight.js +1 -1
  126. package/dist/src/orchestration/remediation/recreateResource.js +1 -1
  127. package/dist/src/orchestration/remediation/remediationJournal.js +2 -2
  128. package/dist/src/orchestration/remediation/removalUpdate.js +1 -1
  129. package/dist/src/orchestration/remediation/retainFlip.js +1 -1
  130. package/dist/src/orchestration/resolveOperation.js +1 -1
  131. package/dist/src/orchestration/restart/restartApplication.js +1 -1
  132. package/dist/src/orchestration/restart/secretsDrift.js +1 -1
  133. package/dist/src/orchestration/secretArnResolver.js +1 -1
  134. package/dist/src/orchestration/serviceFactory.js +1 -1
  135. package/dist/src/orchestration/spawnHelpers.js +1 -1
  136. package/dist/src/orchestration/stackCleanup/bucketOps.js +1 -1
  137. package/dist/src/orchestration/stackCleanup/failedStack.js +1 -1
  138. package/dist/src/orchestration/stackCleanup/logging.js +1 -1
  139. package/dist/src/orchestration/stackCleanup/messages.js +1 -1
  140. package/dist/src/orchestration/stackCleanup/orphanSweep.js +1 -1
  141. package/dist/src/orchestration/stackCleanup/preEmpty.js +1 -1
  142. package/dist/src/orchestration/stackCleanup/stackResources.js +1 -1
  143. package/dist/src/orchestration/stepLifecycle.js +1 -1
  144. package/dist/src/orchestration/trailMigration/memberTrailCleanup.js +1 -1
  145. package/dist/src/orchestration/trailMigration/trailMigration.js +1 -1
  146. package/dist/src/orchestration/unlock/bucketPolicyTriage.js +1 -1
  147. package/dist/src/orchestration/unlock/restoreAndReconcileQuarantinedBucket.js +1 -1
  148. package/dist/src/orchestration/unlock/restoreBucketPolicy.js +1 -1
  149. package/dist/src/orchestration/unlock/scpRemediation.js +1 -1
  150. package/dist/src/orchestration/unlock/toResourcePolicyStatements.js +1 -1
  151. package/dist/src/orchestration/unlock/unlockBucket.js +1 -1
  152. package/dist/src/orchestration/unlock/unlockQueue.js +1 -1
  153. package/dist/src/services/application/ApplicationStackService.d.ts +7 -0
  154. package/dist/src/services/application/ApplicationStackService.js +1 -1
  155. package/dist/src/services/application/applicationStackHelpers.js +3 -3
  156. package/dist/src/services/infrastructure/CdkArgumentBuilder.js +1 -1
  157. package/dist/src/services/infrastructure/CdkCommandRunner.js +2 -2
  158. package/dist/src/services/infrastructure/CdkErrorFormatter.js +10 -10
  159. package/dist/src/services/infrastructure/CdkEventMonitoring.js +1 -1
  160. package/dist/src/services/infrastructure/CdkOutputAnalyser.js +2 -2
  161. package/dist/src/services/infrastructure/CdkOutputParser.js +2 -2
  162. package/dist/src/services/infrastructure/CdkProcessManager.js +3 -3
  163. package/dist/src/services/infrastructure/CdkService.d.ts +1 -1
  164. package/dist/src/services/infrastructure/CdkService.js +2 -2
  165. package/dist/src/services/infrastructure/CdkServiceTypes.d.ts +39 -0
  166. package/dist/src/services/infrastructure/CfnRegistryService.js +1 -1
  167. package/dist/src/services/infrastructure/CloudFormationService.js +1 -1
  168. package/dist/src/services/infrastructure/EcrImageInspectorService.js +1 -1
  169. package/dist/src/services/infrastructure/EcsService.js +1 -1
  170. package/dist/src/services/infrastructure/EcsServiceResolver.js +1 -1
  171. package/dist/src/services/infrastructure/LambdaService.js +1 -1
  172. package/dist/src/services/infrastructure/SsmParameterMetadataService.js +1 -1
  173. package/dist/src/services/infrastructure/cancelInFlightStack.js +3 -3
  174. package/dist/src/services/infrastructure/cdkServiceHelpers.js +1 -1
  175. package/dist/src/services/infrastructure/changeSetProbe.js +1 -1
  176. package/dist/src/services/infrastructure/constructMapEnrichment.js +1 -1
  177. package/dist/src/services/supporting/CdkContextBuilder.d.ts +1 -0
  178. package/dist/src/services/supporting/CdkContextBuilder.js +1 -1
  179. package/dist/src/services/supporting/TemplateHashService.js +1 -1
  180. package/dist/src/services/supporting/helpers.js +1 -1
  181. package/dist/src/steps/stepRegistry.js +1 -1
  182. package/dist/src/types/ProgressEvent.js +1 -1
  183. package/dist/src/types/application/ApplicationServiceTypes.js +1 -1
  184. package/dist/src/types/callbacks.js +1 -1
  185. package/dist/src/types/config/FjallState.js +1 -1
  186. package/dist/src/types/constants.js +1 -1
  187. package/dist/src/types/deployEmitter.js +1 -1
  188. package/dist/src/types/deployEvent.d.ts +1 -0
  189. package/dist/src/types/deployEvent.js +1 -1
  190. package/dist/src/types/deployEventParse.js +1 -1
  191. package/dist/src/types/deployment/DeploymentServiceTypes.js +1 -1
  192. package/dist/src/types/deployment/DeploymentTypes.d.ts +1 -0
  193. package/dist/src/types/deploymentEventSchema.d.ts +1 -0
  194. package/dist/src/types/deploymentEventSchema.js +1 -1
  195. package/dist/src/types/detection/patternDetection.js +1 -1
  196. package/dist/src/types/detection/patternTypes.d.ts +9 -0
  197. package/dist/src/types/drift.js +1 -1
  198. package/dist/src/types/errors/CdkError.js +1 -1
  199. package/dist/src/types/errors/ServiceError.js +1 -1
  200. package/dist/src/types/operations.d.ts +26 -2
  201. package/dist/src/types/operations.js +1 -1
  202. package/dist/src/types/params.d.ts +20 -0
  203. package/dist/src/types/remediation.js +1 -1
  204. package/dist/src/types/stepDefinitions.d.ts +3 -0
  205. package/dist/src/types/stepDefinitions.js +1 -1
  206. package/dist/src/util/dockerfileDetection.js +1 -1
  207. package/dist/src/util/sequencedCallbacks.js +1 -1
  208. package/dist/src/util/sleepAbortable.js +1 -1
  209. package/package.json +5 -4
@@ -1 +1 @@
1
- import{success as E,failure as R}from"@fjall/generator";import{completeForgetAfterConverge as z,fetchLiveStackState as B}from"./forgetResource.js";import{archiveCompletedRemediationJournal as Q,computeRemediationOpId as X,formatArchivedOperationNotice as Z,formatCompletedOperationNotice as ee,readRemediationJournal as te,writeRemediationJournal as ae}from"./remediationJournal.js";import{captureRemediationForensics as re}from"./forensicsCapture.js";import{applyRetainFlip as x,computeTemplateDigest as H,deriveRetainFlipTemplate as oe,findMarkedResourcesByOpId as ne,proveFlipMetadataOnly as ie,readRetainFlipState as _,verifyRetainFlipDiff as se,RECREATE_MARKER_KEY as C}from"./retainFlip.js";import{deriveRemovalTemplate as le,findRemovalDeletionFailures as ce,findResourcesPresent as de,proveRemovalTargetsOnly as pe}from"./removalUpdate.js";const ue=new Set(["AWS::DocDB::DBCluster","AWS::EC2::Volume","AWS::ElastiCache::CacheCluster","AWS::ElastiCache::ReplicationGroup","AWS::Neptune::DBCluster","AWS::RDS::DBCluster","AWS::RDS::DBInstance","AWS::Redshift::Cluster"]);function fe(i){return ue.has(i)?"Snapshot":"Delete"}const me=30*6e4;function he(i){const n={};for(const c of i)n[c.logicalId]=fe(c.resourceType);return n}function U(i,n,c,e){const T=e.filter(o=>o!==c);if(T.length===0)return E(void 0);const p=T.map(o=>{const u=ne(i,o,n,C);return`${o} covers ${u.success&&u.data.length>0?u.data.join(", "):"(could not recover its target set)"}`});return R(new Error(`Recreate operation ${c} on ${n} found target resources already carrying a ${C} marker from a different operation \u2014 its journal may live on another machine, and completing this operation over it would strand that surgery. Marked resources recovered from the live template: ${p.join("; ")}. Complete the foreign operation first (re-run its converge deploy on the machine that holds its journal), then re-run this deploy. Nothing was mutated.`))}async function we(i,n,c){const{stackName:e,targets:T,onProgress:p,abortSignal:o}=n;if(T.length===0)return R(new Error("Recreate surgery requires at least one target"));const u=c?.now??(()=>new Date),O=i.getAccountId(),N=i.getRegion(),S=T.map(a=>a.logicalId),d=X(e,S),A=he(T),D={markerKey:C,policyForTarget:a=>A[a]??"Delete"},j=n.appName!==void 0?`Re-run 'fjall deploy ${n.appName}' to resume the recreate.`:"Re-run the deploy to resume the recreate.",F=a=>a.snapshotPolicies??A;let t=await te(O,N,e,d,c?.journal);if(t!==null&&t.verb!=="recreate"&&t.phase!=="done")return R(new Error(`Recreate operation ${d} on ${e} collides with an in-flight '${t.verb}' operation covering the same resources \u2014 complete it first with 'fjall drift resume ${d}', then re-run this deploy. Nothing was mutated.`));if(t!==null&&t.phase==="removal-applied"){const a=await z(i,{journal:t,...o!==void 0?{abortSignal:o}:{}},c);if(!a.success)return a;if(t=a.data.journal,a.data.status==="converge-pending"){const s=a.data.unverified,w=(n.plannedReplacementLogicalIds??[]).filter(l=>s.includes(l));if(w.length>0){const l=n.appName!==void 0?`fjall deploy ${n.appName}`:"fjall deploy";return R(new Error(`Recreate operation ${d} on ${e} already applied its removal, but the closure check could not verify ${w.join(", ")} (re-created, removed, or still orphaned) \u2014 and this deploy plans a pinned-name replacement for the same target(s), which CloudFormation would refuse while the name may still be held. Nothing further was mutated. Retry the deploy once AWS settles \u2014 a re-assert or converge still in flight clears on its own. If it persists, verify the resource state in the owning service; the terminating cure is to remove the resource(s) from the fjall config and re-run '${l}' \u2014 the converge clears the unverifiable declaration and the operation closes as removed \u2014 then re-add the configuration to re-create them under their pinned names.`))}return p?.(`Removal already applied on ${e} \u2014 the deploy re-creates any still-missing ${S.join(", ")} under the pinned names; already-present targets close once the post-deploy sweep verifies them`),E({opId:d,phase:t.phase,convergePending:!0,...t.forensicsPath!==void 0?{forensicsPath:t.forensicsPath}:{},snapshotPolicies:F(t),journal:t})}}if(t!==null&&t.phase==="done"){if(n.doneJournalPolicy!=="archive-and-restart")return p?.(ee(t)),E({opId:d,phase:"done",convergePending:!1,...t.forensicsPath!==void 0?{forensicsPath:t.forensicsPath}:{},snapshotPolicies:F(t),journal:t});const a=await Q(t,c?.journal);if(!a.success)return a;p?.(Z(t)),t=null}const I=async a=>{const s=await ae(a,c?.journal);return s.success?E(a):R(s.error)};let f;if(t===null){const a=await B(i,e,o);if(!a.success)return a;const s=_(a.data.templateBody,S,e,D);if(!s.success)return s;const w=U(a.data.templateBody,e,d,s.data.markerOpIds);if(!w.success)return w;if(s.data.markerOpIds.includes(d)){p?.(`Recognised in-flight recreate markers on ${e} \u2014 resuming from the ${C} marker`);const l=u().toISOString(),y=await I({version:1,opId:d,verb:"recreate",accountId:O,region:N,stackName:e,...n.appName!==void 0?{appName:n.appName}:{},targets:[...T],phase:s.data.allFlipped?"flip-applied":"captured",createdAt:l,updatedAt:l,snapshotPolicies:A});if(!y.success)return y;t=y.data,f=a.data}else{p?.(`Capturing forensics for ${e} (${d})`);const l=await re(i,{stackName:e,opId:d,findings:n.findings??[],...o!==void 0?{abortSignal:o}:{}},c?.forensics);if(!l.success)return l;const y=u().toISOString(),P=await I({version:1,opId:d,verb:"recreate",accountId:O,region:N,stackName:e,...n.appName!==void 0?{appName:n.appName}:{},targets:[...T],phase:"captured",createdAt:y,updatedAt:y,forensicsPath:l.data.forensicsPath,snapshotPolicies:A,...n.consentedPhysicalNames!==void 0?{consentedPhysicalNames:[...n.consentedPhysicalNames]}:{}});if(!P.success)return P;t=P.data,f={templateBody:l.data.liveTemplateBody,capabilities:l.data.capabilities,parameterKeys:l.data.parameterKeys}}}if(f===void 0){const a=await B(i,e,o);if(!a.success)return a;f=a.data}let m=f.templateBody,v=_(m,S,e,D);if(!v.success)return v;const L=U(m,e,d,v.data.markerOpIds);if(!L.success)return L;const q=new Set(v.data.absentTargets),h=S.filter(a=>!q.has(a));let b=[];if(h.length>0){if(v.data.unflippedTargets.filter(r=>h.includes(r)).length>0){p?.(`Deriving the snapshot flip for ${e}`);const r=oe({liveTemplateBody:m,stackName:e,targets:h,opId:d,at:u().toISOString(),spec:D});if(!r.success)return r;const $=se(m,r.data.flipTemplateBody,h,e,C);if(!$.success)return $;p?.("Proving the flip is metadata-only (review change set)");const g=await ie(i,{stackName:e,flipTemplateBody:r.data.flipTemplateBody,targets:h,parameterKeys:f.parameterKeys,...o!==void 0?{abortSignal:o}:{}},c?.flip);if(!g.success)return g;const K=await I({...t,phase:"flip-verified",updatedAt:u().toISOString(),liveTemplateDigest:r.data.liveTemplateDigest,proof:{diffGuardPassed:!0,changeSetMetadataOnly:!0,noChanges:g.data.noChanges,provenAt:u().toISOString()}});if(!K.success)return K;t=K.data,p?.(`Applying the snapshot flip to ${e}`);const M=await x(i,{stackName:e,flipTemplateBody:r.data.flipTemplateBody,capabilities:f.capabilities,parameterKeys:f.parameterKeys,derivedFromTemplateDigest:r.data.liveTemplateDigest,opId:d,operationLabel:"Snapshot-flip",resumeHint:j,...o!==void 0?{abortSignal:o}:{}},c?.flip);if(!M.success)return M;const W=await B(i,e,o);if(!W.success)return W;if(m=W.data.templateBody,v=_(m,S,e,D),!v.success)return v;const V=new Set(v.data.absentTargets),J=v.data.unflippedTargets.filter(Y=>!V.has(Y));if(J.length>0)return R(new Error(`Snapshot flip on ${e} reported ${M.data.outcome} but GetTemplate does not show the snapshot policy + the ${C} marker on ${J.join(", ")} \u2014 aborting to manual repair.`))}if(t.phase==="captured"||t.phase==="flip-verified"){const r=await I({...t,phase:"flip-applied",updatedAt:u().toISOString(),liveTemplateDigest:H(m)});if(!r.success)return r;t=r.data}p?.(`Deriving the surgical removal of ${h.join(", ")} from ${e}`);const s=le(m,S,e);if(!s.success)return R(new Error(`${s.error.message} The snapshot flip has already been applied on ${e}. ${j}`));b=s.data.coRemoved.map(r=>r.logicalId);const w=[...S,...b];if(s.data.coRemoved.length>0){const r=s.data.coRemoved.map($=>`${$.logicalId} (${$.resourceType})`).join(", ");p?.(`Co-removing stateless referrer(s) of the recreate target(s): ${r} \u2014 CloudFormation cannot keep a resource that references a deleted one; the deploy re-creates them alongside the targets. No data is lost.`)}p?.("Proving the removal touches only the recreate targets (review change set)");const l=await pe(i,{stackName:e,removalTemplateBody:s.data.removalTemplateBody,targets:w,parameterKeys:f.parameterKeys,...o!==void 0?{abortSignal:o}:{}},c?.flip);if(!l.success)return l;if(!l.data.noChanges){const r=h.map(g=>`${g} \u2192 ${A[g]??"Delete"}`).join(", ");p?.(`Removing ${h.join(", ")}${b.length>0?` (with co-removed ${b.join(", ")})`:""} from ${e} (deletion policies: ${r})`);const $=await x(i,{stackName:e,flipTemplateBody:s.data.removalTemplateBody,capabilities:f.capabilities,parameterKeys:f.parameterKeys,derivedFromTemplateDigest:s.data.liveTemplateDigest,opId:d,operationLabel:"Recreate-removal",resumeHint:j,...o!==void 0?{abortSignal:o}:{}},{...c?.flip,updateTimeoutMs:c?.flip?.updateTimeoutMs??me});if(!$.success)return $}const y=await B(i,e,o);if(!y.success)return y;m=y.data.templateBody;const P=de(m,w,e);if(!P.success)return P;if(P.data.length>0)return R(new Error(`Recreate removal on ${e} completed but GetTemplate still shows ${P.data.join(", ")} \u2014 aborting to manual repair. ${j}`));if(!l.data.noChanges){const r=await ce(i,e,w,o);if(!r.success)return R(new Error(`Recreate removal on ${e} removed ${h.join(", ")} from the template, but the physical deletion outcome could not be verified: ${r.error.message}. ${j}`));if(r.data.length>0){const $=r.data.map(g=>`${g.status} for ${g.logicalId}${g.statusReason!==void 0?` (${g.statusReason})`:""}`).join("; ");return R(new Error(`Recreate removal on ${e} removed ${h.join(", ")} from the template, but CloudFormation reported ${$} \u2014 the physical resource(s) still exist outside the stack; a recreate target orphaned this way still holds its pinned name, so the deploy's re-create would collide. Resolve the deletion failure and delete the orphaned resource(s) manually, then re-run the deploy to resume.`))}}const G=h.filter(r=>A[r]==="Snapshot");G.length>0&&p?.(`Final snapshots were requested for ${G.join(", ")} \u2014 locate them in the owning service's console to restore data into the re-created resources`)}const k=await I({...t,phase:"removal-applied",updatedAt:u().toISOString(),liveTemplateDigest:H(m),...b.length>0?{coRemovedLogicalIds:b}:{}});return k.success?(t=k.data,p?.(`Surgical removal complete on ${e} \u2014 the deploy now re-creates ${S.join(", ")} under the pinned names`),E({opId:d,phase:t.phase,convergePending:!0,...t.forensicsPath!==void 0?{forensicsPath:t.forensicsPath}:{},snapshotPolicies:F(t),journal:t})):k}export{ue as SNAPSHOT_CAPABLE_RESOURCE_TYPES,we as runRecreateSurgery,fe as snapshotPolicyForType};
1
+ var Q=Object.defineProperty;var A=(i,o)=>Q(i,"name",{value:o,configurable:!0});import{success as D,failure as R}from"@fjall/generator";import{completeForgetAfterConverge as X,fetchLiveStackState as O}from"./forgetResource.js";import{archiveCompletedRemediationJournal as Z,computeRemediationOpId as ee,formatArchivedOperationNotice as te,formatCompletedOperationNotice as ae,readRemediationJournal as re,writeRemediationJournal as oe}from"./remediationJournal.js";import{captureRemediationForensics as ne}from"./forensicsCapture.js";import{applyRetainFlip as H,computeTemplateDigest as U,deriveRetainFlipTemplate as ie,findMarkedResourcesByOpId as se,proveFlipMetadataOnly as le,readRetainFlipState as L,verifyRetainFlipDiff as ce,RECREATE_MARKER_KEY as j}from"./retainFlip.js";import{deriveRemovalTemplate as de,findRemovalDeletionFailures as pe,findResourcesPresent as ue,proveRemovalTargetsOnly as fe}from"./removalUpdate.js";const me=new Set(["AWS::DocDB::DBCluster","AWS::EC2::Volume","AWS::ElastiCache::CacheCluster","AWS::ElastiCache::ReplicationGroup","AWS::Neptune::DBCluster","AWS::RDS::DBCluster","AWS::RDS::DBInstance","AWS::Redshift::Cluster"]);function he(i){return me.has(i)?"Snapshot":"Delete"}A(he,"snapshotPolicyForType");const ge=30*6e4;function ve(i){const o={};for(const c of i)o[c.logicalId]=he(c.resourceType);return o}A(ve,"buildSnapshotPolicies");function q(i,o,c,e){const T=e.filter(n=>n!==c);if(T.length===0)return D(void 0);const p=T.map(n=>{const u=se(i,n,o,j);return`${n} covers ${u.success&&u.data.length>0?u.data.join(", "):"(could not recover its target set)"}`});return R(new Error(`Recreate operation ${c} on ${o} found target resources already carrying a ${j} marker from a different operation \u2014 its journal may live on another machine, and completing this operation over it would strand that surgery. Marked resources recovered from the live template: ${p.join("; ")}. Complete the foreign operation first (re-run its converge deploy on the machine that holds its journal), then re-run this deploy. Nothing was mutated.`))}A(q,"guardForeignRecreateMarkers");async function Ae(i,o,c){const{stackName:e,targets:T,onProgress:p,abortSignal:n}=o;if(T.length===0)return R(new Error("Recreate surgery requires at least one target"));const u=c?.now??(()=>new Date),N=i.getAccountId(),F=i.getRegion(),S=T.map(a=>a.logicalId),d=ee(e,S),b=ve(T),B={markerKey:j,policyForTarget:A(a=>b[a]??"Delete","policyForTarget")},I=o.appName!==void 0?`Re-run 'fjall deploy ${o.appName}' to resume the recreate.`:"Re-run the deploy to resume the recreate.",k=A(a=>a.snapshotPolicies??b,"reportPolicies");let t=await re(N,F,e,d,c?.journal);if(t!==null&&t.verb!=="recreate"&&t.phase!=="done")return R(new Error(`Recreate operation ${d} on ${e} collides with an in-flight '${t.verb}' operation covering the same resources \u2014 complete it first with 'fjall drift resume ${d}', then re-run this deploy. Nothing was mutated.`));if(t!==null&&t.phase==="removal-applied"){const a=await X(i,{journal:t,...n!==void 0?{abortSignal:n}:{}},c);if(!a.success)return a;if(t=a.data.journal,a.data.status==="converge-pending"){const s=a.data.unverified,w=(o.plannedReplacementLogicalIds??[]).filter(l=>s.includes(l));if(w.length>0){const l=o.appName!==void 0?`fjall deploy ${o.appName}`:"fjall deploy";return R(new Error(`Recreate operation ${d} on ${e} already applied its removal, but the closure check could not verify ${w.join(", ")} (re-created, removed, or still orphaned) \u2014 and this deploy plans a pinned-name replacement for the same target(s), which CloudFormation would refuse while the name may still be held. Nothing further was mutated. Retry the deploy once AWS settles \u2014 a re-assert or converge still in flight clears on its own. If it persists, verify the resource state in the owning service; the terminating cure is to remove the resource(s) from the fjall config and re-run '${l}' \u2014 the converge clears the unverifiable declaration and the operation closes as removed \u2014 then re-add the configuration to re-create them under their pinned names.`))}return p?.(`Removal already applied on ${e} \u2014 the deploy re-creates any still-missing ${S.join(", ")} under the pinned names; already-present targets close once the post-deploy sweep verifies them`),D({opId:d,phase:t.phase,convergePending:!0,...t.forensicsPath!==void 0?{forensicsPath:t.forensicsPath}:{},snapshotPolicies:k(t),journal:t})}}if(t!==null&&t.phase==="done"){if(o.doneJournalPolicy!=="archive-and-restart")return p?.(ae(t)),D({opId:d,phase:"done",convergePending:!1,...t.forensicsPath!==void 0?{forensicsPath:t.forensicsPath}:{},snapshotPolicies:k(t),journal:t});const a=await Z(t,c?.journal);if(!a.success)return a;p?.(te(t)),t=null}const E=A(async a=>{const s=await oe(a,c?.journal);return s.success?D(a):R(s.error)},"persistJournal");let f;if(t===null){const a=await O(i,e,n);if(!a.success)return a;const s=L(a.data.templateBody,S,e,B);if(!s.success)return s;const w=q(a.data.templateBody,e,d,s.data.markerOpIds);if(!w.success)return w;if(s.data.markerOpIds.includes(d)){p?.(`Recognised in-flight recreate markers on ${e} \u2014 resuming from the ${j} marker`);const l=u().toISOString(),y=await E({version:1,opId:d,verb:"recreate",accountId:N,region:F,stackName:e,...o.appName!==void 0?{appName:o.appName}:{},targets:[...T],phase:s.data.allFlipped?"flip-applied":"captured",createdAt:l,updatedAt:l,snapshotPolicies:b});if(!y.success)return y;t=y.data,f=a.data}else{p?.(`Capturing forensics for ${e} (${d})`);const l=await ne(i,{stackName:e,opId:d,findings:o.findings??[],...n!==void 0?{abortSignal:n}:{}},c?.forensics);if(!l.success)return l;const y=u().toISOString(),P=await E({version:1,opId:d,verb:"recreate",accountId:N,region:F,stackName:e,...o.appName!==void 0?{appName:o.appName}:{},targets:[...T],phase:"captured",createdAt:y,updatedAt:y,forensicsPath:l.data.forensicsPath,snapshotPolicies:b,...o.consentedPhysicalNames!==void 0?{consentedPhysicalNames:[...o.consentedPhysicalNames]}:{}});if(!P.success)return P;t=P.data,f={templateBody:l.data.liveTemplateBody,capabilities:l.data.capabilities,parameterKeys:l.data.parameterKeys}}}if(f===void 0){const a=await O(i,e,n);if(!a.success)return a;f=a.data}let m=f.templateBody,v=L(m,S,e,B);if(!v.success)return v;const G=q(m,e,d,v.data.markerOpIds);if(!G.success)return G;const V=new Set(v.data.absentTargets),h=S.filter(a=>!V.has(a));let C=[];if(h.length>0){if(v.data.unflippedTargets.filter(r=>h.includes(r)).length>0){p?.(`Deriving the snapshot flip for ${e}`);const r=ie({liveTemplateBody:m,stackName:e,targets:h,opId:d,at:u().toISOString(),spec:B});if(!r.success)return r;const $=ce(m,r.data.flipTemplateBody,h,e,j);if(!$.success)return $;p?.("Proving the flip is metadata-only (review change set)");const g=await le(i,{stackName:e,flipTemplateBody:r.data.flipTemplateBody,targets:h,parameterKeys:f.parameterKeys,...n!==void 0?{abortSignal:n}:{}},c?.flip);if(!g.success)return g;const M=await E({...t,phase:"flip-verified",updatedAt:u().toISOString(),liveTemplateDigest:r.data.liveTemplateDigest,proof:{diffGuardPassed:!0,changeSetMetadataOnly:!0,noChanges:g.data.noChanges,provenAt:u().toISOString()}});if(!M.success)return M;t=M.data,p?.(`Applying the snapshot flip to ${e}`);const W=await H(i,{stackName:e,flipTemplateBody:r.data.flipTemplateBody,capabilities:f.capabilities,parameterKeys:f.parameterKeys,derivedFromTemplateDigest:r.data.liveTemplateDigest,opId:d,operationLabel:"Snapshot-flip",resumeHint:I,...n!==void 0?{abortSignal:n}:{}},c?.flip);if(!W.success)return W;const _=await O(i,e,n);if(!_.success)return _;if(m=_.data.templateBody,v=L(m,S,e,B),!v.success)return v;const Y=new Set(v.data.absentTargets),x=v.data.unflippedTargets.filter(z=>!Y.has(z));if(x.length>0)return R(new Error(`Snapshot flip on ${e} reported ${W.data.outcome} but GetTemplate does not show the snapshot policy + the ${j} marker on ${x.join(", ")} \u2014 aborting to manual repair.`))}if(t.phase==="captured"||t.phase==="flip-verified"){const r=await E({...t,phase:"flip-applied",updatedAt:u().toISOString(),liveTemplateDigest:U(m)});if(!r.success)return r;t=r.data}p?.(`Deriving the surgical removal of ${h.join(", ")} from ${e}`);const s=de(m,S,e);if(!s.success)return R(new Error(`${s.error.message} The snapshot flip has already been applied on ${e}. ${I}`));C=s.data.coRemoved.map(r=>r.logicalId);const w=[...S,...C];if(s.data.coRemoved.length>0){const r=s.data.coRemoved.map($=>`${$.logicalId} (${$.resourceType})`).join(", ");p?.(`Co-removing stateless referrer(s) of the recreate target(s): ${r} \u2014 CloudFormation cannot keep a resource that references a deleted one; the deploy re-creates them alongside the targets. No data is lost.`)}p?.("Proving the removal touches only the recreate targets (review change set)");const l=await fe(i,{stackName:e,removalTemplateBody:s.data.removalTemplateBody,targets:w,parameterKeys:f.parameterKeys,...n!==void 0?{abortSignal:n}:{}},c?.flip);if(!l.success)return l;if(!l.data.noChanges){const r=h.map(g=>`${g} \u2192 ${b[g]??"Delete"}`).join(", ");p?.(`Removing ${h.join(", ")}${C.length>0?` (with co-removed ${C.join(", ")})`:""} from ${e} (deletion policies: ${r})`);const $=await H(i,{stackName:e,flipTemplateBody:s.data.removalTemplateBody,capabilities:f.capabilities,parameterKeys:f.parameterKeys,derivedFromTemplateDigest:s.data.liveTemplateDigest,opId:d,operationLabel:"Recreate-removal",resumeHint:I,...n!==void 0?{abortSignal:n}:{}},{...c?.flip,updateTimeoutMs:c?.flip?.updateTimeoutMs??ge});if(!$.success)return $}const y=await O(i,e,n);if(!y.success)return y;m=y.data.templateBody;const P=ue(m,w,e);if(!P.success)return P;if(P.data.length>0)return R(new Error(`Recreate removal on ${e} completed but GetTemplate still shows ${P.data.join(", ")} \u2014 aborting to manual repair. ${I}`));if(!l.data.noChanges){const r=await pe(i,e,w,n);if(!r.success)return R(new Error(`Recreate removal on ${e} removed ${h.join(", ")} from the template, but the physical deletion outcome could not be verified: ${r.error.message}. ${I}`));if(r.data.length>0){const $=r.data.map(g=>`${g.status} for ${g.logicalId}${g.statusReason!==void 0?` (${g.statusReason})`:""}`).join("; ");return R(new Error(`Recreate removal on ${e} removed ${h.join(", ")} from the template, but CloudFormation reported ${$} \u2014 the physical resource(s) still exist outside the stack; a recreate target orphaned this way still holds its pinned name, so the deploy's re-create would collide. Resolve the deletion failure and delete the orphaned resource(s) manually, then re-run the deploy to resume.`))}}const J=h.filter(r=>b[r]==="Snapshot");J.length>0&&p?.(`Final snapshots were requested for ${J.join(", ")} \u2014 locate them in the owning service's console to restore data into the re-created resources`)}const K=await E({...t,phase:"removal-applied",updatedAt:u().toISOString(),liveTemplateDigest:U(m),...C.length>0?{coRemovedLogicalIds:C}:{}});return K.success?(t=K.data,p?.(`Surgical removal complete on ${e} \u2014 the deploy now re-creates ${S.join(", ")} under the pinned names`),D({opId:d,phase:t.phase,convergePending:!0,...t.forensicsPath!==void 0?{forensicsPath:t.forensicsPath}:{},snapshotPolicies:k(t),journal:t})):K}A(Ae,"runRecreateSurgery");export{me as SNAPSHOT_CAPABLE_RESOURCE_TYPES,Ae as runRecreateSurgery,he as snapshotPolicyForType};
@@ -1,2 +1,2 @@
1
- import{createHash as J}from"node:crypto";import{mkdir as b,readdir as j,readFile as $,rename as N,rm as x,rmdir as E,writeFile as S}from"node:fs/promises";import{homedir as T}from"node:os";import{basename as O,dirname as I,join as m}from"node:path";import{success as h,failure as g}from"@fjall/generator";import{getErrorMessage as f,maskSensitiveOutput as s}from"@fjall/util";import{logger as l}from"@fjall/util/logger";import{RemediationJournalRecordSchema as _}from"../../types/remediation.js";const d="RemediationJournal",k=90;function w(){return m(T(),".fjall","state","remediation")}function V(e,n){const r=J("sha256");r.update(e);for(const t of[...n].sort())r.update(`
2
- `),r.update(t);return r.digest("hex").slice(0,16)}function q(e){return`${C(e.verb)} operation ${e.opId} already completed`}function Q(e){return`Archived completed ${e.verb} operation ${e.opId} from a previous incarnation \u2014 starting a fresh operation`}function C(e){return e.charAt(0).toUpperCase()+e.slice(1)}const D=/^[A-Za-z0-9][A-Za-z0-9-]*$/,F=/^[a-f0-9]{8,64}$/;function v(e,n,r,t,a){for(const[o,u,i]of[["accountId",n,D],["region",r,D],["stackName",t,D],["opId",a,F]])if(!i.test(u))return g(new Error(`Refusing remediation journal path: unsafe ${o} segment "${s(u)}"`));return h(m(e,n,r,t,`${a}.json`))}async function X(e,n){const r=n?.journalDir??w(),t=v(r,e.accountId,e.region,e.stackName,e.opId);if(!t.success)return t;try{await b(I(t.data),{recursive:!0});const a=`${t.data}.tmp-${process.pid}`;return await S(a,JSON.stringify(e,null,2),"utf8"),await N(a,t.data),h(t.data)}catch(a){return g(new Error(`Failed to write remediation journal for ${e.stackName}/${e.opId}: ${s(f(a))}`))}}async function P(e,n,r,t,a){const o=a?.journalDir??w(),u=v(o,e,n,r,t);if(!u.success)return null;let i;try{i=await $(u.data,"utf8")}catch{return null}return R(i,`${r}/${t}`)}function R(e,n){let r;try{r=JSON.parse(e)}catch(a){return l.warn(d,"Corrupt remediation journal ignored",{label:n,error:s(f(a))}),null}const t=_.safeParse(r);return t.success?t.data:(l.warn(d,"Schema-drifted remediation journal ignored",{label:n,error:s(t.error.message)}),null)}async function ee(e,n){const r=n?.journalDir??w(),t=v(r,e.accountId,e.region,e.stackName,e.opId);if(!t.success)return t;const a=(e.completedAt??e.updatedAt).replace(/[^0-9A-Za-z.-]/g,""),o=t.data.replace(/\.json$/,`.done-${a}.json`);try{return await N(t.data,o),h(o)}catch(u){return g(new Error(`Failed to archive the completed remediation journal for ${e.stackName}/${e.opId}: ${s(f(u))}`))}}async function te(e,n,r){if(!F.test(e))return h(null);const t=r?.journalDir??w();let a;try{a=await A(t,3)}catch{return h(null)}const o=[];for(const i of a.filter(c=>c.endsWith(`/${e}.json`))){let c;try{c=await $(i,"utf8")}catch{continue}const p=R(c,e);p!==null&&o.push(p)}if(o.length===0)return h(null);if(o.length===1)return h(o[0]??null);const u=o.find(i=>i.accountId===n?.accountId&&i.region===n?.region);return u!==void 0?h(u):g(new Error(`Remediation operation ${e} exists in more than one context: ${o.map(i=>`${i.accountId}/${i.region}`).join(", ")}${n!==void 0?` \u2014 none matches the current credentials (${n.accountId}/${n.region}). Switch to the matching account/region and re-run 'fjall drift resume ${e}'.`:"."}`))}async function M(e,n,r,t){const a=t?.journalDir??w(),o=m(a,e,n,r);if(!D.test(r))return[];let u;try{u=await j(o)}catch{return[]}const i=[];for(const c of u){if(!c.endsWith(".json"))continue;let p;try{p=await $(m(o,c),"utf8")}catch{continue}const y=R(p,`${r}/${c}`);y!==null&&y.phase!=="done"&&i.push(y)}return i}async function re(e,n,r){const t=r?.journalDir??w();let a;try{a=await j(m(t,e,n))}catch{return[]}const o=[];for(const u of a)o.push(...await M(e,n,u,r));return o}async function ne(e,n=()=>new Date){const r=e?.journalDir??w(),t=n().getTime()-k*24*60*60*1e3;let a;try{a=await A(r,3)}catch{return}for(const o of a){let u;try{u=await $(o,"utf8")}catch{continue}const i=R(u,o);if(i===null||i.phase!=="done")continue;const c=Date.parse(i.completedAt??i.updatedAt);if(Number.isNaN(c)||c>=t)continue;let p=!0;await x(o,{force:!0}).catch(y=>{p=!1,l.debug(d,"Failed to sweep expired remediation journal",{file:o,error:s(f(y))})}),!(!p||i.forensicsPath===void 0)&&await K(i.forensicsPath,i.opId)}}async function K(e,n){const r=I(e),t=O(e)===n;if(!F.test(n)||!t&&O(r)!==n){l.debug(d,"Kept forensics bundle \u2014 unexpected path shape",{forensicsPath:e});return}if(t){await z(e);return}let a=!0;await x(e,{recursive:!0,force:!0}).catch(o=>{a=!1,l.warn(d,"Failed to sweep the expired journal's forensics",{forensicsPath:e,error:s(f(o))})}),a&&await E(r).catch(o=>{l.debug(d,"Kept forensics opId directory",{opDir:r,error:s(f(o))})})}async function z(e){let n;try{n=await j(e,{withFileTypes:!0})}catch(t){l.warn(d,"Failed to sweep the expired journal's forensics",{forensicsPath:e,error:s(f(t))});return}let r=!1;for(const t of n){if(!t.isFile()){r=!0;continue}await x(m(e,t.name),{force:!0}).catch(a=>{r=!0,l.warn(d,"Failed to sweep the expired journal's forensics",{forensicsPath:m(e,t.name),error:s(f(a))})})}if(r){l.debug(d,"Kept flat forensics directory \u2014 nested bundles live",{forensicsPath:e});return}await E(e).catch(t=>{l.debug(d,"Kept forensics opId directory",{forensicsPath:e,error:s(f(t))})})}async function A(e,n){const r=await j(e,{withFileTypes:!0}),t=[];for(const a of r){const o=m(e,a.name);a.isFile()&&a.name.endsWith(".json")?t.push(o):a.isDirectory()&&n>0&&t.push(...await A(o,n-1).catch(()=>[]))}return t}export{k as REMEDIATION_JOURNAL_RETENTION_DAYS,ee as archiveCompletedRemediationJournal,V as computeRemediationOpId,w as defaultRemediationJournalDir,te as findRemediationJournalByOpId,Q as formatArchivedOperationNotice,q as formatCompletedOperationNotice,M as listActiveRemediationOps,re as listActiveRemediationOpsForContext,P as readRemediationJournal,ne as sweepExpiredRemediationJournals,X as writeRemediationJournal};
1
+ var b=Object.defineProperty;var c=(e,r)=>b(e,"name",{value:r,configurable:!0});import{createHash as S}from"node:crypto";import{mkdir as T,readdir as $,readFile as g,rename as E,rm as F,rmdir as O,writeFile as _}from"node:fs/promises";import{homedir as k}from"node:os";import{basename as I,dirname as J,join as h}from"node:path";import{success as w,failure as D}from"@fjall/generator";import{getErrorMessage as p,maskSensitiveOutput as l}from"@fjall/util";import{logger as d}from"@fjall/util/logger";import{RemediationJournalRecordSchema as C}from"../../types/remediation.js";const f="RemediationJournal",M=90;function y(){return h(k(),".fjall","state","remediation")}c(y,"defaultRemediationJournalDir");function X(e,r){const n=S("sha256");n.update(e);for(const t of[...r].sort())n.update(`
2
+ `),n.update(t);return n.digest("hex").slice(0,16)}c(X,"computeRemediationOpId");function P(e){return`${K(e.verb)} operation ${e.opId} already completed`}c(P,"formatCompletedOperationNotice");function ee(e){return`Archived completed ${e.verb} operation ${e.opId} from a previous incarnation \u2014 starting a fresh operation`}c(ee,"formatArchivedOperationNotice");function K(e){return e.charAt(0).toUpperCase()+e.slice(1)}c(K,"capitaliseVerb");const R=/^[A-Za-z0-9][A-Za-z0-9-]*$/,v=/^[a-f0-9]{8,64}$/;function A(e,r,n,t,a){for(const[o,u,i]of[["accountId",r,R],["region",n,R],["stackName",t,R],["opId",a,v]])if(!i.test(u))return D(new Error(`Refusing remediation journal path: unsafe ${o} segment "${l(u)}"`));return w(h(e,r,n,t,`${a}.json`))}c(A,"journalPath");async function te(e,r){const n=r?.journalDir??y(),t=A(n,e.accountId,e.region,e.stackName,e.opId);if(!t.success)return t;try{await T(J(t.data),{recursive:!0});const a=`${t.data}.tmp-${process.pid}`;return await _(a,JSON.stringify(e,null,2),"utf8"),await E(a,t.data),w(t.data)}catch(a){return D(new Error(`Failed to write remediation journal for ${e.stackName}/${e.opId}: ${l(p(a))}`))}}c(te,"writeRemediationJournal");async function re(e,r,n,t,a){const o=a?.journalDir??y(),u=A(o,e,r,n,t);if(!u.success)return null;let i;try{i=await g(u.data,"utf8")}catch{return null}return x(i,`${n}/${t}`)}c(re,"readRemediationJournal");function x(e,r){let n;try{n=JSON.parse(e)}catch(a){return d.warn(f,"Corrupt remediation journal ignored",{label:r,error:l(p(a))}),null}const t=C.safeParse(n);return t.success?t.data:(d.warn(f,"Schema-drifted remediation journal ignored",{label:r,error:l(t.error.message)}),null)}c(x,"parseJournal");async function ne(e,r){const n=r?.journalDir??y(),t=A(n,e.accountId,e.region,e.stackName,e.opId);if(!t.success)return t;const a=(e.completedAt??e.updatedAt).replace(/[^0-9A-Za-z.-]/g,""),o=t.data.replace(/\.json$/,`.done-${a}.json`);try{return await E(t.data,o),w(o)}catch(u){return D(new Error(`Failed to archive the completed remediation journal for ${e.stackName}/${e.opId}: ${l(p(u))}`))}}c(ne,"archiveCompletedRemediationJournal");async function ae(e,r,n){if(!v.test(e))return w(null);const t=n?.journalDir??y();let a;try{a=await N(t,3)}catch{return w(null)}const o=[];for(const i of a.filter(s=>s.endsWith(`/${e}.json`))){let s;try{s=await g(i,"utf8")}catch{continue}const m=x(s,e);m!==null&&o.push(m)}if(o.length===0)return w(null);if(o.length===1)return w(o[0]??null);const u=o.find(i=>i.accountId===r?.accountId&&i.region===r?.region);return u!==void 0?w(u):D(new Error(`Remediation operation ${e} exists in more than one context: ${o.map(i=>`${i.accountId}/${i.region}`).join(", ")}${r!==void 0?` \u2014 none matches the current credentials (${r.accountId}/${r.region}). Switch to the matching account/region and re-run 'fjall drift resume ${e}'.`:"."}`))}c(ae,"findRemediationJournalByOpId");async function z(e,r,n,t){const a=t?.journalDir??y(),o=h(a,e,r,n);if(!R.test(n))return[];let u;try{u=await $(o)}catch{return[]}const i=[];for(const s of u){if(!s.endsWith(".json"))continue;let m;try{m=await g(h(o,s),"utf8")}catch{continue}const j=x(m,`${n}/${s}`);j!==null&&j.phase!=="done"&&i.push(j)}return i}c(z,"listActiveRemediationOps");async function oe(e,r,n){const t=n?.journalDir??y();let a;try{a=await $(h(t,e,r))}catch{return[]}const o=[];for(const u of a)o.push(...await z(e,r,u,n));return o}c(oe,"listActiveRemediationOpsForContext");async function ie(e,r=()=>new Date){const n=e?.journalDir??y(),t=r().getTime()-M*24*60*60*1e3;let a;try{a=await N(n,3)}catch{return}for(const o of a){let u;try{u=await g(o,"utf8")}catch{continue}const i=x(u,o);if(i===null||i.phase!=="done")continue;const s=Date.parse(i.completedAt??i.updatedAt);if(Number.isNaN(s)||s>=t)continue;let m=!0;await F(o,{force:!0}).catch(j=>{m=!1,d.debug(f,"Failed to sweep expired remediation journal",{file:o,error:l(p(j))})}),!(!m||i.forensicsPath===void 0)&&await B(i.forensicsPath,i.opId)}}c(ie,"sweepExpiredRemediationJournals");async function B(e,r){const n=J(e),t=I(e)===r;if(!v.test(r)||!t&&I(n)!==r){d.debug(f,"Kept forensics bundle \u2014 unexpected path shape",{forensicsPath:e});return}if(t){await G(e);return}let a=!0;await F(e,{recursive:!0,force:!0}).catch(o=>{a=!1,d.warn(f,"Failed to sweep the expired journal's forensics",{forensicsPath:e,error:l(p(o))})}),a&&await O(n).catch(o=>{d.debug(f,"Kept forensics opId directory",{opDir:n,error:l(p(o))})})}c(B,"sweepForensicsBundle");async function G(e){let r;try{r=await $(e,{withFileTypes:!0})}catch(t){d.warn(f,"Failed to sweep the expired journal's forensics",{forensicsPath:e,error:l(p(t))});return}let n=!1;for(const t of r){if(!t.isFile()){n=!0;continue}await F(h(e,t.name),{force:!0}).catch(a=>{n=!0,d.warn(f,"Failed to sweep the expired journal's forensics",{forensicsPath:h(e,t.name),error:l(p(a))})})}if(n){d.debug(f,"Kept flat forensics directory \u2014 nested bundles live",{forensicsPath:e});return}await O(e).catch(t=>{d.debug(f,"Kept forensics opId directory",{forensicsPath:e,error:l(p(t))})})}c(G,"sweepFlatBundleFiles");async function N(e,r){const n=await $(e,{withFileTypes:!0}),t=[];for(const a of n){const o=h(e,a.name);a.isFile()&&a.name.endsWith(".json")?t.push(o):a.isDirectory()&&r>0&&t.push(...await N(o,r-1).catch(()=>[]))}return t}c(N,"listFilesRecursive");export{M as REMEDIATION_JOURNAL_RETENTION_DAYS,ne as archiveCompletedRemediationJournal,X as computeRemediationOpId,y as defaultRemediationJournalDir,ae as findRemediationJournalByOpId,ee as formatArchivedOperationNotice,P as formatCompletedOperationNotice,z as listActiveRemediationOps,oe as listActiveRemediationOpsForContext,re as readRemediationJournal,ie as sweepExpiredRemediationJournals,te as writeRemediationJournal};
@@ -1 +1 @@
1
- import{randomUUID as N}from"node:crypto";import{CloudFormationClient as P,CreateChangeSetCommand as j,DeleteChangeSetCommand as F,DescribeChangeSetCommand as I}from"@aws-sdk/client-cloudformation";import{success as E,failure as h}from"@fjall/generator";import{getErrorMessage as _,maskSensitiveOutput as v}from"@fjall/util";import{logger as M}from"@fjall/util/logger";import{composeSdkAbortSignal as b,isAborted as O}from"../../aws/organisations/types.js";import{isStatefulResourceType as x}from"../application/plan/classify.js";import{PREFLIGHT_CHANGE_SET_PREFIX as k}from"../../services/infrastructure/changeSetProbe.js";import{sleepAbortable as U}from"../../util/sleepAbortable.js";import{fetchLastOperationEvents as G}from"../drift/classifyDriftFailure.js";import{composeFlipCapabilities as B,computeTemplateDigest as K,echoPreviousParameters as V,resolveTemplateLocation as H,deleteStagedTemplate as J}from"./retainFlip.js";const W="RemovalUpdate",Y=/didn't contain changes|No updates are to be performed/i,z=new Set(["CREATE_COMPLETE","FAILED"]),X=3e3,q=9e4;function C(n,o){let e;try{e=JSON.parse(n)}catch{return h(new Error(`The live template for ${o} is not JSON (YAML-authored stacks are not supported by the removal update) \u2014 aborting to manual repair.`))}return typeof e!="object"||e===null||Array.isArray(e)?h(new Error(`The live template for ${o} did not parse to an object \u2014 aborting to manual repair.`)):E(e)}function R(n,o,e){if(typeof n=="string")return[];if(Array.isArray(n))return n.flatMap((c,u)=>R(c,o,`${e}[${u}]`));if(typeof n!="object"||n===null)return[];const r=n,t=[],l=r.Ref;typeof l=="string"&&o.has(l)&&t.push(`${e}/Ref(${l})`);const a=r["Fn::GetAtt"];typeof a=="string"&&o.has(a.split(".")[0]??"")&&t.push(`${e}/Fn::GetAtt(${a.split(".")[0]??""})`),Array.isArray(a)&&typeof a[0]=="string"&&o.has(a[0])&&t.push(`${e}/Fn::GetAtt(${a[0]})`);const s=r["Fn::Sub"],f=typeof s=="string"?s:Array.isArray(s)&&typeof s[0]=="string"?s[0]:void 0;if(f!==void 0)for(const c of f.matchAll(/\$\{([^!}][^}]*)\}/g)){const u=(c[1]??"").split(".")[0]??"";o.has(u)&&t.push(`${e}/Fn::Sub(${u})`)}for(const[c,u]of Object.entries(r))t.push(...R(u,o,`${e}/${c}`));return t}function pe(n,o,e){const r=C(n,e);if(!r.success)return r;const t=new Set(o),l=[];for(const[a,s]of Object.entries(r.data.Resources??{})){const f=t.has(a)?new Set([...t].filter(c=>c!==a)):t;f.size!==0&&R(s,f,a).length>0&&l.push(a)}return E(l)}function he(n,o,e,r){const t=C(n,r);if(!t.success)return t;const l=t.data.Resources??{},a=[];for(const s of o){const f=l[s];if(f===void 0)continue;const c=e.filter(u=>u!==s&&R(f,new Set([u]),s).length>0);c.length>0&&a.push({pendingTarget:s,deadReferences:c})}return E(a)}function Q(n,o){const e=n.DependsOn;if(typeof e=="string"){o.has(e)&&delete n.DependsOn;return}if(Array.isArray(e)){const r=e.filter(t=>typeof t!="string"||!o.has(t));r.length===0?delete n.DependsOn:r.length!==e.length&&(n.DependsOn=r)}}const L=["AWS::SecretsManager::SecretTargetAttachment","AWS::SecretsManager::RotationSchedule"],Z=new Set(L),ee=new Set(["Retain","RetainExceptOnCreate"]);function $(n){return`${n.slice(0,10).join(", ")}${n.length>10?` (+${n.length-10} more)`:""}`}function me(n,o,e){const r=C(n,e);if(!r.success)return r;const t=r.data,l=t.Resources??{},a=o.filter(i=>l[i]===void 0),s=new Set(o),f=[];let c=!0;for(;c;){c=!1;for(const[i,d]of Object.entries(l)){if(s.has(i))continue;const p=R(d,s,i);if(p.length===0)continue;const m=d.Type;if(typeof m!="string")return h(new Error(`Cannot orchestrate the recreate for ${e}: ${i} references the removal set at ${$(p)} but declares no resource Type, so fjall cannot prove it is safe to co-remove. Restructure the dependency or perform the replacement manually.`));if(x(m))return h(new Error(`Cannot orchestrate the recreate for ${e}: ${i} (${m}) references the removal set at ${$(p)} \u2014 it is a stateful resource holding data, and removing it was not consented. Restructure the dependency or perform the replacement manually.`));if(!Z.has(m))return h(new Error(`Cannot orchestrate the recreate for ${e}: ${i} (${m}) references the removal set at ${$(p)} \u2014 fjall only co-removes provably-safe glue resources (${L.join(", ")}), and removing anything else was not consented. Restructure the dependency or perform the replacement manually.`));const S=d.DeletionPolicy;if(typeof S=="string"&&ee.has(S))return h(new Error(`Cannot orchestrate the recreate for ${e}: ${i} (${m}) references the removal set at ${$(p)} but carries DeletionPolicy: ${S} \u2014 CloudFormation would skip its physical deletion (DELETE_SKIPPED) and the converge's re-create would collide with the retained resource. Restructure the dependency or perform the replacement manually.`));s.add(i),f.push({logicalId:i,resourceType:m,referencePaths:p}),c=!0}}for(const i of s)delete l[i];const u=[];for(const[i,d]of Object.entries(l))Q(d,s),u.push(...R(d,s,i));if(u.length>0)return h(new Error(`Cannot orchestrate the recreate for ${e}: other resources still reference ${[...s].join(", ")} at ${$(u)} \u2014 removing the target would break them. Restructure the dependency or perform the replacement manually.`));const g=[],y=t.Outputs;if(typeof y=="object"&&y!==null){for(const[i,d]of Object.entries(y))if(R(d,s,`Outputs/${i}`).length>0){const p=typeof d=="object"&&d!==null?d.Export:void 0;if(typeof p=="object"&&p!==null){const m=p.Name;g.push({outputName:i,...typeof m=="string"?{exportName:m}:{}})}delete y[i]}Object.keys(y).length===0&&delete t.Outputs}return E({removalTemplateBody:JSON.stringify(t),liveTemplateDigest:K(n),alreadyAbsent:a,strippedExports:g,coRemoved:f})}function ge(n,o,e){const r=C(n,e);if(!r.success)return r;const t=r.data.Resources??{};return E(o.filter(l=>t[l]!==void 0))}const te=new Set(["DELETE_FAILED","DELETE_SKIPPED"]);async function Se(n,o,e,r){const t=await G(n,o,r);if(!t.success)return t;const l=new Set(e);return E(t.data.filter(a=>l.has(a.logicalId)&&te.has(a.status)).map(a=>({logicalId:a.logicalId,status:a.status,...a.statusReason!==void 0?{statusReason:v(a.statusReason)}:{}})))}async function Ee(n,o,e){const{stackName:r,abortSignal:t}=o,l=e?.pollIntervalMs??X,a=e?.changeSetTimeoutMs??q,s=e?.now??(()=>Date.now()),f=n.getClient(P),c=`${k}removal-${N()}`;let u;try{const g=await H(n,c,o.removalTemplateBody,t);if(!g.success)return g;u=g.data.key,await f.send(new j({StackName:r,ChangeSetName:c,ChangeSetType:"UPDATE",IncludeNestedStacks:!0,Capabilities:B([]),Description:"fjall recreate-removal targets-only proof \u2014 review-only, never executed",...V(o.parameterKeys),...g.data.location}),{abortSignal:b(t)});const y=s();let i,d;for(;!O(t);){const T=await f.send(new I({StackName:r,ChangeSetName:c}),{abortSignal:b(t)});if(i=T.Status,d=T.StatusReason,i!==void 0&&z.has(i))break;if(s()-y>=a)return h(new Error(`Recreate-removal proof timed out after ${a}ms for ${r} (last status: ${i??"unknown"})`));await U(l,t)}if(O(t))return h(new Error(`Recreate-removal proof aborted while polling ${r}`));if(i==="FAILED")return d!==void 0&&Y.test(d)?E({noChanges:!0}):h(new Error(`Recreate-removal proof change set failed for ${r}: ${v(d??"no status reason reported")}`));const p=[],m=new Set(o.targets);let S;do{if(O(t))return h(new Error(`Recreate-removal proof aborted while reading ${r}`));const T=await f.send(new I({StackName:r,ChangeSetName:c,...S!==void 0?{NextToken:S}:{}}),{abortSignal:b(t)});for(const D of T.Changes??[]){if(D.Type!=="Resource")continue;const w=D.ResourceChange,A=w?.LogicalResourceId;if(!(w===void 0||A===void 0)){if(!m.has(A)){p.push(`${A} (not a removal target)`);continue}w.Action!=="Remove"&&p.push(`${A} (action ${w.Action??"unknown"})`)}}S=T.NextToken}while(S!==void 0);return p.length>0?h(new Error(`Recreate removal is NOT targets-only for ${r} \u2014 the review change set reported changes at ${[...new Set(p)].join(", ")}. Aborting before the removal ran; the snapshot flip has already been applied, so re-run the deploy to resume once the change is understood.`)):E({noChanges:!1})}catch(g){return h(new Error(`Recreate-removal proof failed for ${r}: ${v(_(g))}`))}finally{await f.send(new F({StackName:r,ChangeSetName:c}),{abortSignal:b()}).catch(g=>{M.warn(W,"Failed to delete recreate-removal proof change set",{stackName:r,changeSetName:c,error:v(_(g))})}),u!==void 0&&await J(n,u)}}export{L as CO_REMOVABLE_RESOURCE_TYPES,me as deriveRemovalTemplate,he as findDeadReferencesFromTargets,Se as findRemovalDeletionFailures,ge as findResourcesPresent,pe as findTargetReferrers,Ee as proveRemovalTargetsOnly};
1
+ var P=Object.defineProperty;var S=(n,o)=>P(n,"name",{value:o,configurable:!0});import{randomUUID as j}from"node:crypto";import{CloudFormationClient as F,CreateChangeSetCommand as M,DeleteChangeSetCommand as x,DescribeChangeSetCommand as _}from"@aws-sdk/client-cloudformation";import{success as y,failure as h}from"@fjall/generator";import{getErrorMessage as L,maskSensitiveOutput as b}from"@fjall/util";import{logger as k}from"@fjall/util/logger";import{composeSdkAbortSignal as C,isAborted as D}from"../../aws/organisations/types.js";import{isStatefulResourceType as U}from"../application/plan/classify.js";import{PREFLIGHT_CHANGE_SET_PREFIX as G}from"../../services/infrastructure/changeSetProbe.js";import{sleepAbortable as B}from"../../util/sleepAbortable.js";import{fetchLastOperationEvents as K}from"../drift/classifyDriftFailure.js";import{composeFlipCapabilities as V,computeTemplateDigest as H,echoPreviousParameters as J,resolveTemplateLocation as W,deleteStagedTemplate as Y}from"./retainFlip.js";const z="RemovalUpdate",X=/didn't contain changes|No updates are to be performed/i,q=new Set(["CREATE_COMPLETE","FAILED"]),Q=3e3,Z=9e4;function O(n,o){let e;try{e=JSON.parse(n)}catch{return h(new Error(`The live template for ${o} is not JSON (YAML-authored stacks are not supported by the removal update) \u2014 aborting to manual repair.`))}return typeof e!="object"||e===null||Array.isArray(e)?h(new Error(`The live template for ${o} did not parse to an object \u2014 aborting to manual repair.`)):y(e)}S(O,"parseTemplate");function T(n,o,e){if(typeof n=="string")return[];if(Array.isArray(n))return n.flatMap((c,u)=>T(c,o,`${e}[${u}]`));if(typeof n!="object"||n===null)return[];const r=n,t=[],l=r.Ref;typeof l=="string"&&o.has(l)&&t.push(`${e}/Ref(${l})`);const a=r["Fn::GetAtt"];typeof a=="string"&&o.has(a.split(".")[0]??"")&&t.push(`${e}/Fn::GetAtt(${a.split(".")[0]??""})`),Array.isArray(a)&&typeof a[0]=="string"&&o.has(a[0])&&t.push(`${e}/Fn::GetAtt(${a[0]})`);const s=r["Fn::Sub"],f=typeof s=="string"?s:Array.isArray(s)&&typeof s[0]=="string"?s[0]:void 0;if(f!==void 0)for(const c of f.matchAll(/\$\{([^!}][^}]*)\}/g)){const u=(c[1]??"").split(".")[0]??"";o.has(u)&&t.push(`${e}/Fn::Sub(${u})`)}for(const[c,u]of Object.entries(r))t.push(...T(u,o,`${e}/${c}`));return t}S(T,"collectTargetReferences");function ge(n,o,e){const r=O(n,e);if(!r.success)return r;const t=new Set(o),l=[];for(const[a,s]of Object.entries(r.data.Resources??{})){const f=t.has(a)?new Set([...t].filter(c=>c!==a)):t;f.size!==0&&T(s,f,a).length>0&&l.push(a)}return y(l)}S(ge,"findTargetReferrers");function Se(n,o,e,r){const t=O(n,r);if(!t.success)return t;const l=t.data.Resources??{},a=[];for(const s of o){const f=l[s];if(f===void 0)continue;const c=e.filter(u=>u!==s&&T(f,new Set([u]),s).length>0);c.length>0&&a.push({pendingTarget:s,deadReferences:c})}return y(a)}S(Se,"findDeadReferencesFromTargets");function ee(n,o){const e=n.DependsOn;if(typeof e=="string"){o.has(e)&&delete n.DependsOn;return}if(Array.isArray(e)){const r=e.filter(t=>typeof t!="string"||!o.has(t));r.length===0?delete n.DependsOn:r.length!==e.length&&(n.DependsOn=r)}}S(ee,"pruneDependsOn");const N=["AWS::SecretsManager::SecretTargetAttachment","AWS::SecretsManager::RotationSchedule"],te=new Set(N),re=new Set(["Retain","RetainExceptOnCreate"]);function w(n){return`${n.slice(0,10).join(", ")}${n.length>10?` (+${n.length-10} more)`:""}`}S(w,"formatReferencePaths");function Ee(n,o,e){const r=O(n,e);if(!r.success)return r;const t=r.data,l=t.Resources??{},a=o.filter(i=>l[i]===void 0),s=new Set(o),f=[];let c=!0;for(;c;){c=!1;for(const[i,d]of Object.entries(l)){if(s.has(i))continue;const p=T(d,s,i);if(p.length===0)continue;const m=d.Type;if(typeof m!="string")return h(new Error(`Cannot orchestrate the recreate for ${e}: ${i} references the removal set at ${w(p)} but declares no resource Type, so fjall cannot prove it is safe to co-remove. Restructure the dependency or perform the replacement manually.`));if(U(m))return h(new Error(`Cannot orchestrate the recreate for ${e}: ${i} (${m}) references the removal set at ${w(p)} \u2014 it is a stateful resource holding data, and removing it was not consented. Restructure the dependency or perform the replacement manually.`));if(!te.has(m))return h(new Error(`Cannot orchestrate the recreate for ${e}: ${i} (${m}) references the removal set at ${w(p)} \u2014 fjall only co-removes provably-safe glue resources (${N.join(", ")}), and removing anything else was not consented. Restructure the dependency or perform the replacement manually.`));const E=d.DeletionPolicy;if(typeof E=="string"&&re.has(E))return h(new Error(`Cannot orchestrate the recreate for ${e}: ${i} (${m}) references the removal set at ${w(p)} but carries DeletionPolicy: ${E} \u2014 CloudFormation would skip its physical deletion (DELETE_SKIPPED) and the converge's re-create would collide with the retained resource. Restructure the dependency or perform the replacement manually.`));s.add(i),f.push({logicalId:i,resourceType:m,referencePaths:p}),c=!0}}for(const i of s)delete l[i];const u=[];for(const[i,d]of Object.entries(l))ee(d,s),u.push(...T(d,s,i));if(u.length>0)return h(new Error(`Cannot orchestrate the recreate for ${e}: other resources still reference ${[...s].join(", ")} at ${w(u)} \u2014 removing the target would break them. Restructure the dependency or perform the replacement manually.`));const g=[],R=t.Outputs;if(typeof R=="object"&&R!==null){for(const[i,d]of Object.entries(R))if(T(d,s,`Outputs/${i}`).length>0){const p=typeof d=="object"&&d!==null?d.Export:void 0;if(typeof p=="object"&&p!==null){const m=p.Name;g.push({outputName:i,...typeof m=="string"?{exportName:m}:{}})}delete R[i]}Object.keys(R).length===0&&delete t.Outputs}return y({removalTemplateBody:JSON.stringify(t),liveTemplateDigest:H(n),alreadyAbsent:a,strippedExports:g,coRemoved:f})}S(Ee,"deriveRemovalTemplate");function ye(n,o,e){const r=O(n,e);if(!r.success)return r;const t=r.data.Resources??{};return y(o.filter(l=>t[l]!==void 0))}S(ye,"findResourcesPresent");const ne=new Set(["DELETE_FAILED","DELETE_SKIPPED"]);async function Re(n,o,e,r){const t=await K(n,o,r);if(!t.success)return t;const l=new Set(e);return y(t.data.filter(a=>l.has(a.logicalId)&&ne.has(a.status)).map(a=>({logicalId:a.logicalId,status:a.status,...a.statusReason!==void 0?{statusReason:b(a.statusReason)}:{}})))}S(Re,"findRemovalDeletionFailures");async function Te(n,o,e){const{stackName:r,abortSignal:t}=o,l=e?.pollIntervalMs??Q,a=e?.changeSetTimeoutMs??Z,s=e?.now??(()=>Date.now()),f=n.getClient(F),c=`${G}removal-${j()}`;let u;try{const g=await W(n,c,o.removalTemplateBody,t);if(!g.success)return g;u=g.data.key,await f.send(new M({StackName:r,ChangeSetName:c,ChangeSetType:"UPDATE",IncludeNestedStacks:!0,Capabilities:V([]),Description:"fjall recreate-removal targets-only proof \u2014 review-only, never executed",...J(o.parameterKeys),...g.data.location}),{abortSignal:C(t)});const R=s();let i,d;for(;!D(t);){const $=await f.send(new _({StackName:r,ChangeSetName:c}),{abortSignal:C(t)});if(i=$.Status,d=$.StatusReason,i!==void 0&&q.has(i))break;if(s()-R>=a)return h(new Error(`Recreate-removal proof timed out after ${a}ms for ${r} (last status: ${i??"unknown"})`));await B(l,t)}if(D(t))return h(new Error(`Recreate-removal proof aborted while polling ${r}`));if(i==="FAILED")return d!==void 0&&X.test(d)?y({noChanges:!0}):h(new Error(`Recreate-removal proof change set failed for ${r}: ${b(d??"no status reason reported")}`));const p=[],m=new Set(o.targets);let E;do{if(D(t))return h(new Error(`Recreate-removal proof aborted while reading ${r}`));const $=await f.send(new _({StackName:r,ChangeSetName:c,...E!==void 0?{NextToken:E}:{}}),{abortSignal:C(t)});for(const I of $.Changes??[]){if(I.Type!=="Resource")continue;const A=I.ResourceChange,v=A?.LogicalResourceId;if(!(A===void 0||v===void 0)){if(!m.has(v)){p.push(`${v} (not a removal target)`);continue}A.Action!=="Remove"&&p.push(`${v} (action ${A.Action??"unknown"})`)}}E=$.NextToken}while(E!==void 0);return p.length>0?h(new Error(`Recreate removal is NOT targets-only for ${r} \u2014 the review change set reported changes at ${[...new Set(p)].join(", ")}. Aborting before the removal ran; the snapshot flip has already been applied, so re-run the deploy to resume once the change is understood.`)):y({noChanges:!1})}catch(g){return h(new Error(`Recreate-removal proof failed for ${r}: ${b(L(g))}`))}finally{await f.send(new x({StackName:r,ChangeSetName:c}),{abortSignal:C()}).catch(g=>{k.warn(z,"Failed to delete recreate-removal proof change set",{stackName:r,changeSetName:c,error:b(L(g))})}),u!==void 0&&await Y(n,u)}}S(Te,"proveRemovalTargetsOnly");export{N as CO_REMOVABLE_RESOURCE_TYPES,Ee as deriveRemovalTemplate,Se as findDeadReferencesFromTargets,Re as findRemovalDeletionFailures,ye as findResourcesPresent,ge as findTargetReferrers,Te as proveRemovalTargetsOnly};
@@ -1 +1 @@
1
- import{createHash as J,randomUUID as C}from"node:crypto";import{Capability as I,CloudFormationClient as _,CreateChangeSetCommand as V,DeleteChangeSetCommand as W,DescribeChangeSetCommand as M,DescribeStacksCommand as X,GetTemplateCommand as z,UpdateStackCommand as q}from"@aws-sdk/client-cloudformation";import{DeleteObjectCommand as Q,PutObjectCommand as Z,S3Client as P}from"@aws-sdk/client-s3";import{success as y,failure as g}from"@fjall/generator";import{getErrorMessage as b,maskSensitiveOutput as w}from"@fjall/util";import{logger as O}from"@fjall/util/logger";import{composeSdkAbortSignal as S,isAborted as $}from"../../aws/organisations/types.js";import{bootstrapAssetsBucketName as v,MAX_INLINE_TEMPLATE_BYTES as ee,PREFLIGHT_CHANGE_SET_PREFIX as te}from"../../services/infrastructure/changeSetProbe.js";import{sleepAbortable as N}from"../../util/sleepAbortable.js";const j="RetainFlip",k="fjall:forget",we="fjall:recreate",D={markerKey:k,policyForTarget:()=>"Retain"},ne=new Set(["DeletionPolicy","UpdateReplacePolicy","Metadata"]),L=/didn't contain changes|No updates are to be performed/i,re=new Set(["CREATE_COMPLETE","FAILED"]),oe=new Set(Object.values(I));function ae(e){return oe.has(e)}const ie=[I.CAPABILITY_IAM,I.CAPABILITY_NAMED_IAM,I.CAPABILITY_AUTO_EXPAND];function F(e){return[...new Set([...e.filter(ae),...ie])]}function U(e){return e.length===0?{}:{Parameters:e.map(n=>({ParameterKey:n,UsePreviousValue:!0}))}}const x=3e3,se=9e4,ce=10*6e4;function A(e,n){let r;try{r=JSON.parse(e)}catch{return g(new Error(`The live template for ${n} is not JSON (YAML-authored stacks are not supported by the retain flip) \u2014 aborting to manual repair.`))}return typeof r!="object"||r===null||Array.isArray(r)?g(new Error(`The live template for ${n} did not parse to an object \u2014 aborting to manual repair.`)):y(r)}function B(e){return J("sha256").update(e).digest("hex")}function le(e,n,r,t=D){const o=A(e,r);if(!o.success)return o;const a=o.data.Resources??{},i=[],s=[],c=[],f=new Set;for(const p of n){const l=a[p];if(l===void 0){i.push(p);continue}const u=K(l,t.markerKey);u!==void 0&&f.add(u.opId);const d=t.policyForTarget(p),m=l.DeletionPolicy===d&&l.UpdateReplacePolicy===d;m||c.push(p),(!m||u===void 0)&&s.push(p)}return y({allFlipped:i.length===0&&s.length===0,absentTargets:i,unflippedTargets:s,policyPendingTargets:c,markerOpIds:[...f]})}function K(e,n){const r=e.Metadata;if(typeof r!="object"||r===null)return;const t=r[n];if(typeof t!="object"||t===null)return;const o=t.opId;return typeof o=="string"?{opId:o}:void 0}function be(e,n,r,t=k){const o=A(e,r);if(!o.success)return o;const a=[];for(const[i,s]of Object.entries(o.data.Resources??{}))K(s,t)?.opId===n&&a.push(i);return y(a)}function Ae(e,n,r){const t=A(e,r);if(!t.success)return{};const o=t.data.Resources??{},a={};for(const i of n){const s=o[i]?.Type;typeof s=="string"&&(a[i]=s)}return a}function Re(e){const{liveTemplateBody:n,stackName:r,targets:t,opId:o,at:a}=e,i=e.spec??D,s=A(n,r);if(!s.success)return s;const c=le(n,t,r,i);if(!c.success)return c;if(c.data.absentTargets.length>0)return g(new Error(`Cannot derive the retain flip for ${r}: the live template no longer contains ${c.data.absentTargets.join(", ")}.`));const f=c.data.markerOpIds.filter(u=>u!==o);if(f.length>0)return g(new Error(`Cannot derive the retain flip for ${r}: target resources carry a ${i.markerKey} marker from a different operation (${f.join(", ")}) \u2014 resolve that operation first (fjall drift resume <opId>).`));const p=s.data,l=p.Resources??{};for(const u of t){const d=l[u];if(d===void 0)continue;const m=i.policyForTarget(u);d.DeletionPolicy=m,d.UpdateReplacePolicy=m;const h=typeof d.Metadata=="object"&&d.Metadata!==null?d.Metadata:{};h[i.markerKey]={opId:o,at:a},d.Metadata=h}return y({flipTemplateBody:JSON.stringify(p),liveTemplateDigest:B(n),alreadyApplied:c.data.allFlipped})}function Ie(e,n,r,t,o=k){const a=A(e,t);if(!a.success)return a;const i=A(n,t);if(!i.success)return i;const s=new Set(r);for(const f of[a.data,i.data]){const p=f.Resources??{};for(const l of Object.keys(p)){if(!s.has(l))continue;const u=p[l];if(u===void 0)continue;delete u.DeletionPolicy,delete u.UpdateReplacePolicy;const d=u.Metadata;typeof d=="object"&&d!==null&&(delete d[o],Object.keys(d).length===0&&delete u.Metadata)}}const c=G(a.data,i.data,"");return c.length>0?g(new Error(`Retain-flip diff-guard failed for ${t}: the flip template differs from the live template beyond the allowed keys at ${c.slice(0,10).join(", ")}${c.length>10?` (+${c.length-10} more)`:""} \u2014 aborting to manual repair, nothing was mutated.`)):y(void 0)}function G(e,n,r){if(e===n)return[];if(!(typeof e=="object"&&e!==null)||!(typeof n=="object"&&n!==null)||Array.isArray(e)!==Array.isArray(n))return JSON.stringify(e)===JSON.stringify(n)?[]:[r||"(root)"];const a=new Set([...Object.keys(e),...Object.keys(n)]),i=[];for(const s of a)i.push(...G(e[s],n[s],r===""?s:`${r}/${s}`));return i}function fe(e,n){if(e.Action!=="Modify"||e.Replacement==="True"||e.Replacement==="Conditional")return!1;const r=e.Details??[];if(r.length===0)return!1;const t=new Set(r.flatMap(a=>a.Target?.Attribute!==void 0?[a.Target.Attribute]:[]));return(e.Scope??[]).every(a=>t.has(a))?r.every(a=>{if(a.Evaluation!=="Dynamic"||a.ChangeSource!=="ResourceReference"&&a.ChangeSource!=="ResourceAttribute")return!1;const i=a.CausingEntity?.split(".")[0];return i!==void 0&&i!==""&&n.has(i)}):!1}function ue(e,n){const r=new Set(n),t=[],o=new Map;for(const s of e){if(s.Type!=="Resource")continue;const c=s.ResourceChange,f=c?.LogicalResourceId;if(c===void 0||f===void 0)continue;if(!r.has(f)){const l=o.get(f)??[];l.push(c),o.set(f,l);continue}if(c.Action!=="Modify"){t.push(`${f} (action ${c.Action??"unknown"})`);continue}if(c.Replacement==="True"||c.Replacement==="Conditional"){t.push(`${f} (replacement ${c.Replacement})`);continue}const p=[...c.Scope??[],...(c.Details??[]).flatMap(l=>l.Target?.Attribute!==void 0?[l.Target.Attribute]:[])];for(const l of p)ne.has(l)||t.push(`${f}/${l}`)}const a=new Set(n);let i=!0;for(;i;){i=!1;for(const[s,c]of o)a.has(s)||c.every(f=>fe(f,a))&&(a.add(s),i=!0)}for(const s of o.keys())a.has(s)||t.push(`${s} (not a flip target)`);return t}async function $e(e,n,r){const{stackName:t,abortSignal:o}=n,a=r?.pollIntervalMs??x,i=r?.changeSetTimeoutMs??se,s=r?.now??(()=>Date.now()),c=e.getClient(_),f=`${te}flip-${C()}`;let p;try{const l=await Y(e,f,n.flipTemplateBody,o);if(!l.success)return l;p=l.data.key,await c.send(new V({StackName:t,ChangeSetName:f,ChangeSetType:"UPDATE",IncludeNestedStacks:!0,Capabilities:F([]),Description:"fjall retain-flip metadata-only proof \u2014 review-only, never executed",...U(n.parameterKeys),...l.data.location}),{abortSignal:S(o)});const u=s();let d,m;for(;!$(o);){const R=await c.send(new M({StackName:t,ChangeSetName:f}),{abortSignal:S(o)});if(d=R.Status,m=R.StatusReason,d!==void 0&&re.has(d))break;if(s()-u>=i)return g(new Error(`Retain-flip proof timed out after ${i}ms for ${t} (last status: ${d??"unknown"})`));await N(a,o)}if($(o))return g(new Error(`Retain-flip proof aborted while polling ${t}`));if(d==="FAILED")return m!==void 0&&L.test(m)?y({noChanges:!0}):g(new Error(`Retain-flip proof change set failed for ${t}: ${w(m??"no status reason reported")}`));const h=[];let T;do{if($(o))return g(new Error(`Retain-flip proof aborted while reading ${t}`));const R=await c.send(new M({StackName:t,ChangeSetName:f,...T!==void 0?{NextToken:T}:{}}),{abortSignal:S(o)});h.push(...R.Changes??[]),T=R.NextToken}while(T!==void 0);const E=ue(h,n.targets);return E.length>0?g(new Error(`Retain flip is NOT metadata-only for ${t} \u2014 the review change set reported property-level changes at ${[...new Set(E)].join(", ")}. Aborting to manual repair; nothing was mutated.`)):y({noChanges:!1})}catch(l){return g(new Error(`Retain-flip proof failed for ${t}: ${w(b(l))}`))}finally{await c.send(new W({StackName:t,ChangeSetName:f}),{abortSignal:S()}).catch(l=>{O.warn(j,"Failed to delete retain-flip proof change set",{stackName:t,changeSetName:f,error:w(b(l))})}),p!==void 0&&await H(e,p)}}async function ke(e,n,r){const{stackName:t,abortSignal:o}=n,a=n.operationLabel??"Retain-flip",i=r?.pollIntervalMs??x,s=r?.updateTimeoutMs??ce,c=r?.now??(()=>Date.now()),f=e.getClient(_),p=`flip-apply-${C()}`;let l;try{const u=await Y(e,p,n.flipTemplateBody,o);if(!u.success)return u;l=u.data.key;const d=await f.send(new z({StackName:t,TemplateStage:"Original"}),{abortSignal:S(o)});if(B(d.TemplateBody??"")!==n.derivedFromTemplateDigest)return g(new Error(`${a} apply aborted for ${t}: the live template changed mid-surgery (after the metadata-only proof) \u2014 nothing was mutated. ${n.resumeHint??`Re-run 'fjall drift resume ${n.opId}' to re-verify from the current template.`}`));try{await f.send(new q({StackName:t,Capabilities:F(n.capabilities),...U(n.parameterKeys),...u.data.location}),{abortSignal:S(o)})}catch(h){if(L.test(b(h)))return y({outcome:"no-updates"});throw h}const m=c();for(;!$(o);){const T=(await f.send(new X({StackName:t}),{abortSignal:S(o)})).Stacks?.[0],E=T?.StackStatus??"UNKNOWN";if(E==="UPDATE_COMPLETE")return y({outcome:"updated"});if(!E.endsWith("_IN_PROGRESS"))return g(new Error(`${a} UpdateStack on ${t} ended ${E}: ${w(T?.StackStatusReason??"no status reason reported")}`));if(c()-m>=s)return g(new Error(`${a} UpdateStack on ${t} did not settle within ${s}ms (last status: ${E}). ${n.resumeHint??"Re-run 'fjall drift resume' once the stack settles."}`));await N(i,o)}return g(new Error(`${a} UpdateStack aborted while polling ${t}`))}catch(u){return g(new Error(`${a} UpdateStack failed for ${t}: ${w(b(u))}`))}finally{l!==void 0&&await H(e,l)}}async function Y(e,n,r,t){if(Buffer.byteLength(r,"utf8")<=ee)return y({location:{TemplateBody:r}});const o=e.getRegion(),a=v(e.getAccountId(),o),i=`fjall-preflight/${n}.template.json`;try{return await e.getClient(P).send(new Z({Bucket:a,Key:i,Body:r,ContentType:"application/json"}),{abortSignal:S(t)}),y({location:{TemplateURL:`https://${a}.s3.${o}.amazonaws.com/${i}`},key:i})}catch(s){return g(new Error(`Retain flip could not stage the template in ${a}: ${w(b(s))}`))}}async function H(e,n){const r=v(e.getAccountId(),e.getRegion());try{await e.getClient(P).send(new Q({Bucket:r,Key:n}),{abortSignal:S()})}catch(t){O.warn(j,"Failed to delete staged retain-flip template",{bucket:r,key:n,error:w(b(t))})}}export{D as FORGET_FLIP_SPEC,k as FORGET_MARKER_KEY,we as RECREATE_MARKER_KEY,ke as applyRetainFlip,ue as classifyProofViolations,F as composeFlipCapabilities,B as computeTemplateDigest,H as deleteStagedTemplate,Re as deriveRetainFlipTemplate,U as echoPreviousParameters,be as findMarkedResourcesByOpId,$e as proveFlipMetadataOnly,Ae as readResourceTypes,le as readRetainFlipState,Y as resolveTemplateLocation,Ie as verifyRetainFlipDiff};
1
+ var V=Object.defineProperty;var p=(e,t)=>V(e,"name",{value:t,configurable:!0});import{createHash as W,randomUUID as _}from"node:crypto";import{Capability as $,CloudFormationClient as M,CreateChangeSetCommand as X,DeleteChangeSetCommand as z,DescribeChangeSetCommand as P,DescribeStacksCommand as q,GetTemplateCommand as Q,UpdateStackCommand as Z}from"@aws-sdk/client-cloudformation";import{DeleteObjectCommand as ee,PutObjectCommand as te,S3Client as O}from"@aws-sdk/client-s3";import{success as h,failure as m}from"@fjall/generator";import{getErrorMessage as A,maskSensitiveOutput as b}from"@fjall/util";import{logger as v}from"@fjall/util/logger";import{composeSdkAbortSignal as T,isAborted as k}from"../../aws/organisations/types.js";import{bootstrapAssetsBucketName as N,MAX_INLINE_TEMPLATE_BYTES as ne,PREFLIGHT_CHANGE_SET_PREFIX as re}from"../../services/infrastructure/changeSetProbe.js";import{sleepAbortable as j}from"../../util/sleepAbortable.js";const D="RetainFlip",C="fjall:forget",Re="fjall:recreate",L={markerKey:C,policyForTarget:p(()=>"Retain","policyForTarget")},oe=new Set(["DeletionPolicy","UpdateReplacePolicy","Metadata"]),F=/didn't contain changes|No updates are to be performed/i,ae=new Set(["CREATE_COMPLETE","FAILED"]),ie=new Set(Object.values($));function se(e){return ie.has(e)}p(se,"isCapability");const ce=[$.CAPABILITY_IAM,$.CAPABILITY_NAMED_IAM,$.CAPABILITY_AUTO_EXPAND];function U(e){return[...new Set([...e.filter(se),...ce])]}p(U,"composeFlipCapabilities");function x(e){return e.length===0?{}:{Parameters:e.map(t=>({ParameterKey:t,UsePreviousValue:!0}))}}p(x,"echoPreviousParameters");const B=3e3,le=9e4,fe=10*6e4;function R(e,t){let r;try{r=JSON.parse(e)}catch{return m(new Error(`The live template for ${t} is not JSON (YAML-authored stacks are not supported by the retain flip) \u2014 aborting to manual repair.`))}return typeof r!="object"||r===null||Array.isArray(r)?m(new Error(`The live template for ${t} did not parse to an object \u2014 aborting to manual repair.`)):h(r)}p(R,"parseTemplate");function K(e){return W("sha256").update(e).digest("hex")}p(K,"computeTemplateDigest");function ue(e,t,r,n=L){const o=R(e,r);if(!o.success)return o;const a=o.data.Resources??{},i=[],s=[],c=[],f=new Set;for(const g of t){const l=a[g];if(l===void 0){i.push(g);continue}const u=G(l,n.markerKey);u!==void 0&&f.add(u.opId);const d=n.policyForTarget(g),y=l.DeletionPolicy===d&&l.UpdateReplacePolicy===d;y||c.push(g),(!y||u===void 0)&&s.push(g)}return h({allFlipped:i.length===0&&s.length===0,absentTargets:i,unflippedTargets:s,policyPendingTargets:c,markerOpIds:[...f]})}p(ue,"readRetainFlipState");function G(e,t){const r=e.Metadata;if(typeof r!="object"||r===null)return;const n=r[t];if(typeof n!="object"||n===null)return;const o=n.opId;return typeof o=="string"?{opId:o}:void 0}p(G,"readMarker");function Ie(e,t,r,n=C){const o=R(e,r);if(!o.success)return o;const a=[];for(const[i,s]of Object.entries(o.data.Resources??{}))G(s,n)?.opId===t&&a.push(i);return h(a)}p(Ie,"findMarkedResourcesByOpId");function $e(e,t,r){const n=R(e,r);if(!n.success)return{};const o=n.data.Resources??{},a={};for(const i of t){const s=o[i]?.Type;typeof s=="string"&&(a[i]=s)}return a}p($e,"readResourceTypes");function ke(e){const{liveTemplateBody:t,stackName:r,targets:n,opId:o,at:a}=e,i=e.spec??L,s=R(t,r);if(!s.success)return s;const c=ue(t,n,r,i);if(!c.success)return c;if(c.data.absentTargets.length>0)return m(new Error(`Cannot derive the retain flip for ${r}: the live template no longer contains ${c.data.absentTargets.join(", ")}.`));const f=c.data.markerOpIds.filter(u=>u!==o);if(f.length>0)return m(new Error(`Cannot derive the retain flip for ${r}: target resources carry a ${i.markerKey} marker from a different operation (${f.join(", ")}) \u2014 resolve that operation first (fjall drift resume <opId>).`));const g=s.data,l=g.Resources??{};for(const u of n){const d=l[u];if(d===void 0)continue;const y=i.policyForTarget(u);d.DeletionPolicy=y,d.UpdateReplacePolicy=y;const S=typeof d.Metadata=="object"&&d.Metadata!==null?d.Metadata:{};S[i.markerKey]={opId:o,at:a},d.Metadata=S}return h({flipTemplateBody:JSON.stringify(g),liveTemplateDigest:K(t),alreadyApplied:c.data.allFlipped})}p(ke,"deriveRetainFlipTemplate");function Ce(e,t,r,n,o=C){const a=R(e,n);if(!a.success)return a;const i=R(t,n);if(!i.success)return i;const s=new Set(r);for(const f of[a.data,i.data]){const g=f.Resources??{};for(const l of Object.keys(g)){if(!s.has(l))continue;const u=g[l];if(u===void 0)continue;delete u.DeletionPolicy,delete u.UpdateReplacePolicy;const d=u.Metadata;typeof d=="object"&&d!==null&&(delete d[o],Object.keys(d).length===0&&delete u.Metadata)}}const c=Y(a.data,i.data,"");return c.length>0?m(new Error(`Retain-flip diff-guard failed for ${n}: the flip template differs from the live template beyond the allowed keys at ${c.slice(0,10).join(", ")}${c.length>10?` (+${c.length-10} more)`:""} \u2014 aborting to manual repair, nothing was mutated.`)):h(void 0)}p(Ce,"verifyRetainFlipDiff");function Y(e,t,r){if(e===t)return[];if(!(typeof e=="object"&&e!==null)||!(typeof t=="object"&&t!==null)||Array.isArray(e)!==Array.isArray(t))return JSON.stringify(e)===JSON.stringify(t)?[]:[r||"(root)"];const a=new Set([...Object.keys(e),...Object.keys(t)]),i=[];for(const s of a)i.push(...Y(e[s],t[s],r===""?s:`${r}/${s}`));return i}p(Y,"collectDifferingPaths");function de(e,t){if(e.Action!=="Modify"||e.Replacement==="True"||e.Replacement==="Conditional")return!1;const r=e.Details??[];if(r.length===0)return!1;const n=new Set(r.flatMap(a=>a.Target?.Attribute!==void 0?[a.Target.Attribute]:[]));return(e.Scope??[]).every(a=>n.has(a))?r.every(a=>{if(a.Evaluation!=="Dynamic"||a.ChangeSource!=="ResourceReference"&&a.ChangeSource!=="ResourceAttribute")return!1;const i=a.CausingEntity?.split(".")[0];return i!==void 0&&i!==""&&t.has(i)}):!1}p(de,"isReferenceReEvaluationOf");function pe(e,t){const r=new Set(t),n=[],o=new Map;for(const s of e){if(s.Type!=="Resource")continue;const c=s.ResourceChange,f=c?.LogicalResourceId;if(c===void 0||f===void 0)continue;if(!r.has(f)){const l=o.get(f)??[];l.push(c),o.set(f,l);continue}if(c.Action!=="Modify"){n.push(`${f} (action ${c.Action??"unknown"})`);continue}if(c.Replacement==="True"||c.Replacement==="Conditional"){n.push(`${f} (replacement ${c.Replacement})`);continue}const g=[...c.Scope??[],...(c.Details??[]).flatMap(l=>l.Target?.Attribute!==void 0?[l.Target.Attribute]:[])];for(const l of g)oe.has(l)||n.push(`${f}/${l}`)}const a=new Set(t);let i=!0;for(;i;){i=!1;for(const[s,c]of o)a.has(s)||c.every(f=>de(f,a))&&(a.add(s),i=!0)}for(const s of o.keys())a.has(s)||n.push(`${s} (not a flip target)`);return n}p(pe,"classifyProofViolations");async function _e(e,t,r){const{stackName:n,abortSignal:o}=t,a=r?.pollIntervalMs??B,i=r?.changeSetTimeoutMs??le,s=r?.now??(()=>Date.now()),c=e.getClient(M),f=`${re}flip-${_()}`;let g;try{const l=await H(e,f,t.flipTemplateBody,o);if(!l.success)return l;g=l.data.key,await c.send(new X({StackName:n,ChangeSetName:f,ChangeSetType:"UPDATE",IncludeNestedStacks:!0,Capabilities:U([]),Description:"fjall retain-flip metadata-only proof \u2014 review-only, never executed",...x(t.parameterKeys),...l.data.location}),{abortSignal:T(o)});const u=s();let d,y;for(;!k(o);){const I=await c.send(new P({StackName:n,ChangeSetName:f}),{abortSignal:T(o)});if(d=I.Status,y=I.StatusReason,d!==void 0&&ae.has(d))break;if(s()-u>=i)return m(new Error(`Retain-flip proof timed out after ${i}ms for ${n} (last status: ${d??"unknown"})`));await j(a,o)}if(k(o))return m(new Error(`Retain-flip proof aborted while polling ${n}`));if(d==="FAILED")return y!==void 0&&F.test(y)?h({noChanges:!0}):m(new Error(`Retain-flip proof change set failed for ${n}: ${b(y??"no status reason reported")}`));const S=[];let E;do{if(k(o))return m(new Error(`Retain-flip proof aborted while reading ${n}`));const I=await c.send(new P({StackName:n,ChangeSetName:f,...E!==void 0?{NextToken:E}:{}}),{abortSignal:T(o)});S.push(...I.Changes??[]),E=I.NextToken}while(E!==void 0);const w=pe(S,t.targets);return w.length>0?m(new Error(`Retain flip is NOT metadata-only for ${n} \u2014 the review change set reported property-level changes at ${[...new Set(w)].join(", ")}. Aborting to manual repair; nothing was mutated.`)):h({noChanges:!1})}catch(l){return m(new Error(`Retain-flip proof failed for ${n}: ${b(A(l))}`))}finally{await c.send(new z({StackName:n,ChangeSetName:f}),{abortSignal:T()}).catch(l=>{v.warn(D,"Failed to delete retain-flip proof change set",{stackName:n,changeSetName:f,error:b(A(l))})}),g!==void 0&&await J(e,g)}}p(_e,"proveFlipMetadataOnly");async function Me(e,t,r){const{stackName:n,abortSignal:o}=t,a=t.operationLabel??"Retain-flip",i=r?.pollIntervalMs??B,s=r?.updateTimeoutMs??fe,c=r?.now??(()=>Date.now()),f=e.getClient(M),g=`flip-apply-${_()}`;let l;try{const u=await H(e,g,t.flipTemplateBody,o);if(!u.success)return u;l=u.data.key;const d=await f.send(new Q({StackName:n,TemplateStage:"Original"}),{abortSignal:T(o)});if(K(d.TemplateBody??"")!==t.derivedFromTemplateDigest)return m(new Error(`${a} apply aborted for ${n}: the live template changed mid-surgery (after the metadata-only proof) \u2014 nothing was mutated. ${t.resumeHint??`Re-run 'fjall drift resume ${t.opId}' to re-verify from the current template.`}`));try{await f.send(new Z({StackName:n,Capabilities:U(t.capabilities),...x(t.parameterKeys),...u.data.location}),{abortSignal:T(o)})}catch(S){if(F.test(A(S)))return h({outcome:"no-updates"});throw S}const y=c();for(;!k(o);){const E=(await f.send(new q({StackName:n}),{abortSignal:T(o)})).Stacks?.[0],w=E?.StackStatus??"UNKNOWN";if(w==="UPDATE_COMPLETE")return h({outcome:"updated"});if(!w.endsWith("_IN_PROGRESS"))return m(new Error(`${a} UpdateStack on ${n} ended ${w}: ${b(E?.StackStatusReason??"no status reason reported")}`));if(c()-y>=s)return m(new Error(`${a} UpdateStack on ${n} did not settle within ${s}ms (last status: ${w}). ${t.resumeHint??"Re-run 'fjall drift resume' once the stack settles."}`));await j(i,o)}return m(new Error(`${a} UpdateStack aborted while polling ${n}`))}catch(u){return m(new Error(`${a} UpdateStack failed for ${n}: ${b(A(u))}`))}finally{l!==void 0&&await J(e,l)}}p(Me,"applyRetainFlip");async function H(e,t,r,n){if(Buffer.byteLength(r,"utf8")<=ne)return h({location:{TemplateBody:r}});const o=e.getRegion(),a=N(e.getAccountId(),o),i=`fjall-preflight/${t}.template.json`;try{return await e.getClient(O).send(new te({Bucket:a,Key:i,Body:r,ContentType:"application/json"}),{abortSignal:T(n)}),h({location:{TemplateURL:`https://${a}.s3.${o}.amazonaws.com/${i}`},key:i})}catch(s){return m(new Error(`Retain flip could not stage the template in ${a}: ${b(A(s))}`))}}p(H,"resolveTemplateLocation");async function J(e,t){const r=N(e.getAccountId(),e.getRegion());try{await e.getClient(O).send(new ee({Bucket:r,Key:t}),{abortSignal:T()})}catch(n){v.warn(D,"Failed to delete staged retain-flip template",{bucket:r,key:t,error:b(A(n))})}}p(J,"deleteStagedTemplate");export{L as FORGET_FLIP_SPEC,C as FORGET_MARKER_KEY,Re as RECREATE_MARKER_KEY,Me as applyRetainFlip,pe as classifyProofViolations,U as composeFlipCapabilities,K as computeTemplateDigest,J as deleteStagedTemplate,ke as deriveRetainFlipTemplate,x as echoPreviousParameters,Ie as findMarkedResourcesByOpId,_e as proveFlipMetadataOnly,$e as readResourceTypes,ue as readRetainFlipState,H as resolveTemplateLocation,Ce as verifyRetainFlipDiff};
@@ -1 +1 @@
1
- import{join as s,relative as g}from"path";import{readdir as b}from"fs/promises";import{success as h,failure as l,VALIDATION_PATTERNS as w}from"@fjall/generator";import{logger as a}from"@fjall/util/logger";import{ORGANISATION_TYPES as m}from"../types/operations.js";import{fileExists as p}from"@fjall/util/fsHelpers";import{INFRASTRUCTURE_FILENAME as N}from"../types/constants.js";const x=new Set(Object.values(m));function A(e){return x.has(e)}const E=w.NEW_APP_NAME;async function T(e,i){const u=s(e,"fjall"),c=await b(u,{withFileTypes:!0}).catch(n=>{a.debug("resolveOperation","existing-app readdir failed",{fjallDir:u,error:String(n)})});if(c===void 0)return;const f=i.toLowerCase(),r=[];for(const n of c){if(!n.isDirectory()||n.name.toLowerCase()!==f)continue;const o=s(u,n.name);g(e,o).startsWith("..")||!(await p(s(o,N))||await p(s(o,"cdk.json")))||r.push({appName:n.name,path:o})}if(r.length!==0)return r.length>1?"ambiguous":r[0]}async function R(e,i){a.debug("resolveOperation","called",{target:e,workingDirectory:i});const u=await p(i);a.debug("resolveOperation","workingDirectory exists",{exists:u});const c=s(i,"fjall"),f=await p(c);if(a.debug("resolveOperation","fjall/ dir check",{fjallDir:c,exists:f}),f&&a.isDebugEnabled())try{const t=await b(c);a.debug("resolveOperation","fjall/ contents",{entries:t})}catch(t){a.debug("resolveOperation","fjall/ readdir failed",{error:String(t)})}const r=e.toLowerCase();if(A(r)){const t=s(i,"fjall",r),d=await p(t);return a.debug("resolveOperation","org type match",{normalisedTarget:r,orgPath:t,exists:d}),d?h({kind:"organisation",type:r,target:e,path:t}):l(new Error(`Organisation target "${e}" resolved to fjall/${r}/ but directory not found`))}if(r.startsWith("account-")){const t=r.slice(8);if(!E.test(t))return l(new Error(`Invalid account target "${e}": suffix must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens`));const d=s(i,"fjall",m.ACCOUNT),O=await p(d);return a.debug("resolveOperation","account-prefixed target",{suffix:t,orgPath:d,exists:O}),O?h({kind:"organisation",type:m.ACCOUNT,target:e,path:d}):l(new Error(`Organisation target "${e}" resolved to fjall/${m.ACCOUNT}/ but directory not found`))}if(!E.test(e)){const t=await T(i,e);return t==="ambiguous"?l(new Error(`Ambiguous target "${e}": multiple fjall/ folders match case-insensitively \u2014 rename so exactly one matches`)):t!==void 0?(a.debug("resolveOperation","resolved existing non-standard app",{target:e,appName:t.appName,path:t.path}),h({kind:"application",appName:t.appName,path:t.path})):(a.debug("resolveOperation","target failed pattern validation",{target:e,pattern:E.source}),l(new Error(`Invalid target "${e}": must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens`)))}const n=s(i,"fjall",e),o=g(i,n);if(o.startsWith(".."))return l(new Error(`Invalid target "${e}": resolved path escapes working directory`));const v=await p(n);return a.debug("resolveOperation","app path check",{appPath:n,exists:v,relative:o}),v?(a.debug("resolveOperation","resolved as application",{appName:e,path:n}),h({kind:"application",appName:e,path:n})):(a.debug("resolveOperation","FAILED \u2014 no match",{target:e,workingDirectory:i,checkedPath:n}),l(new Error(`Target "${e}" is not a recognised organisation type and no directory found at fjall/${e}`)))}export{R as resolveOperation};
1
+ var N=Object.defineProperty;var h=(e,n)=>N(e,"name",{value:n,configurable:!0});import{join as s,relative as b}from"path";import{readdir as w}from"fs/promises";import{success as m,failure as l,VALIDATION_PATTERNS as x}from"@fjall/generator";import{logger as a}from"@fjall/util/logger";import{ORGANISATION_TYPES as v}from"../types/operations.js";import{fileExists as p}from"@fjall/util/fsHelpers";import{INFRASTRUCTURE_FILENAME as A}from"../types/constants.js";const T=new Set(Object.values(v));function j(e){return T.has(e)}h(j,"isOrganisationType");const O=x.NEW_APP_NAME;async function P(e,n){const u=s(e,"fjall"),c=await w(u,{withFileTypes:!0}).catch(r=>{a.debug("resolveOperation","existing-app readdir failed",{fjallDir:u,error:String(r)})});if(c===void 0)return;const f=n.toLowerCase(),i=[];for(const r of c){if(!r.isDirectory()||r.name.toLowerCase()!==f)continue;const o=s(u,r.name);b(e,o).startsWith("..")||!(await p(s(o,A))||await p(s(o,"cdk.json")))||i.push({appName:r.name,path:o})}if(i.length!==0)return i.length>1?"ambiguous":i[0]}h(P,"resolveExistingNonStandardApp");async function U(e,n){a.debug("resolveOperation","called",{target:e,workingDirectory:n});const u=await p(n);a.debug("resolveOperation","workingDirectory exists",{exists:u});const c=s(n,"fjall"),f=await p(c);if(a.debug("resolveOperation","fjall/ dir check",{fjallDir:c,exists:f}),f&&a.isDebugEnabled())try{const t=await w(c);a.debug("resolveOperation","fjall/ contents",{entries:t})}catch(t){a.debug("resolveOperation","fjall/ readdir failed",{error:String(t)})}const i=e.toLowerCase();if(j(i)){const t=s(n,"fjall",i),d=await p(t);return a.debug("resolveOperation","org type match",{normalisedTarget:i,orgPath:t,exists:d}),d?m({kind:"organisation",type:i,target:e,path:t}):l(new Error(`Organisation target "${e}" resolved to fjall/${i}/ but directory not found`))}if(i.startsWith("account-")){const t=i.slice(8);if(!O.test(t))return l(new Error(`Invalid account target "${e}": suffix must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens`));const d=s(n,"fjall",v.ACCOUNT),g=await p(d);return a.debug("resolveOperation","account-prefixed target",{suffix:t,orgPath:d,exists:g}),g?m({kind:"organisation",type:v.ACCOUNT,target:e,path:d}):l(new Error(`Organisation target "${e}" resolved to fjall/${v.ACCOUNT}/ but directory not found`))}if(!O.test(e)){const t=await P(n,e);return t==="ambiguous"?l(new Error(`Ambiguous target "${e}": multiple fjall/ folders match case-insensitively \u2014 rename so exactly one matches`)):t!==void 0?(a.debug("resolveOperation","resolved existing non-standard app",{target:e,appName:t.appName,path:t.path}),m({kind:"application",appName:t.appName,path:t.path})):(a.debug("resolveOperation","target failed pattern validation",{target:e,pattern:O.source}),l(new Error(`Invalid target "${e}": must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens`)))}const r=s(n,"fjall",e),o=b(n,r);if(o.startsWith(".."))return l(new Error(`Invalid target "${e}": resolved path escapes working directory`));const E=await p(r);return a.debug("resolveOperation","app path check",{appPath:r,exists:E,relative:o}),E?(a.debug("resolveOperation","resolved as application",{appName:e,path:r}),m({kind:"application",appName:e,path:r})):(a.debug("resolveOperation","FAILED \u2014 no match",{target:e,workingDirectory:n,checkedPath:r}),l(new Error(`Target "${e}" is not a recognised organisation type and no directory found at fjall/${e}`)))}h(U,"resolveOperation");export{U as resolveOperation};
@@ -1 +1 @@
1
- import{success as $,failure as g}from"@fjall/generator";import{CONTENT_HASH_TAG_PATTERN as I,maskSensitiveOutput as R}from"@fjall/util";import{logger as x}from"@fjall/util/logger";import{SimpleAwsProvider as O}from"../../aws/SimpleAwsProvider.js";import{CloudFormationService as L}from"../../services/infrastructure/CloudFormationService.js";import{EcsService as P}from"../../services/infrastructure/EcsService.js";import{EcsServiceResolver as M}from"../../services/infrastructure/EcsServiceResolver.js";import{EcrImageInspectorService as _,stripTagOrDigest as G}from"../../services/infrastructure/EcrImageInspectorService.js";import{SsmParameterMetadataService as F}from"../../services/infrastructure/SsmParameterMetadataService.js";import{extractEcsServiceName as k}from"../../aws/utils/arnParser.js";import{isAborted as H}from"../../aws/organisations/types.js";import{matchesServiceName as U,resolveServiceTaskDefinition as V}from"../application/serviceImageTagsRollback.js";import{deriveEcrRepositoryArn as j}from"../application/codeOnlyDeploy.js";import{registerAndRollTaskDefinition as B}from"../application/taskDefinitionRoll.js";import{computeSecretsDrift as y}from"./secretsDrift.js";const Y="restartApplication";function h(n){const e=new O(n),i=new L(e);return{ecsService:new P(e),ecsResolver:new M(i),ecrImageInspector:new _(e),ssmService:new F(e)}}async function b(n,e){const{target:i}=e;let r,t;if(e.knownServices!==void 0)r=e.knownServices.clusterArn,t=e.knownServices.serviceArns;else{const s=await n.getDeployableClusterAndServices(i,e.abortSignal);if(!s.success)return g(new Error(`Failed to discover ECS services for ${i}: ${R(s.error.message)}`));r=s.data.clusterArn,t=s.data.serviceArns}if(r===void 0||t.length===0)return g(new Error(`No running ECS services found for ${i} \u2014 nothing to restart. Deploy the application first.`));if(e.serviceName!==void 0){const s=t.filter(o=>{const a=k(o);return a!==void 0&&e.serviceName!==void 0?U(a,e.serviceName):!1});if(s.length===0){const o=t.map(a=>k(a)).filter(a=>a!==void 0).join(", ");return g(new Error(`No ECS service matching "${e.serviceName}" found for ${i}. Live services: ${o||"none"}`))}t=s}return $({clusterArn:r,serviceArns:t})}async function ce(n,e){const{ecsService:i,ecsResolver:r,ssmService:t}=e??h(n.awsCredentials),s=await b(r,n);return s.success?y({ecsService:i,ssmService:t},s.data,n.abortSignal):g(s.error)}async function fe(n,e){const{target:i,callbacks:r,abortSignal:t}=n,s=Date.now(),{ecsService:o,ecsResolver:a,ecrImageInspector:S,ssmService:u}=e??h(n.awsCredentials),f=await b(a,n);if(!f.success)return r.onError?.(f.error),g(f.error);const{clusterArn:w,serviceArns:c}=f.data,d=await y({ecsService:o,ssmService:u},{clusterArn:w,serviceArns:c},t);let l,E={};d.success?(l=d.data,E=d.data.parameterVersions):r.onLog?.(`Could not compute secrets drift before restart: ${R(d.error.message)}. Restarting anyway.`,"warn");const A=[];for(const D of c){if(H(t))return $({target:i,artefacts:A,appliedParameterVersions:E,...l!==void 0?{preRestartDrift:l}:{},durationMs:Date.now()-s,cancelled:!0});const C=Date.now(),m=k(D)??D;r.onLog?.(`Restarting ${m}\u2026`,"info");const v=await V({ecsService:o},w,D,m,t);if(!v.success){const p=new Error(`Restart failed for ${m}: ${R(v.error.message)}`);return r.onError?.(p),g(p)}const T=await B({ecsService:o},{clusterArn:w,serviceArn:D,serviceName:m,taskDefinition:v.data.taskDefinition,containerDefinitions:v.data.containerDefinitions,previousTaskDefArn:v.data.previousTaskDefArn,startedAtMs:C},r,t);if(!T.success){const p=new Error(`Restart failed for ${m}: ${R(T.error.message)}`);return r.onError?.(p),g(p)}const N=await q(S,m,v.data.containerDefinitions,T.data.newTaskDefArn,v.data.previousTaskDefArn,t);N!==void 0?A.push(N):r.onLog?.(`Restarted ${m}, but its image tag could not be recovered \u2014 the restart release will omit this service's artefact.`,"warn")}return x.debug(Y,`Restarted ${i}`,{services:c.length,artefacts:A.length}),$({target:i,artefacts:A,appliedParameterVersions:E,...l!==void 0?{preRestartDrift:l}:{},durationMs:Date.now()-s})}async function q(n,e,i,r,t,s){const o=i.find(c=>c.image)?.image;if(o===void 0)return;const a=G(o),S=j(a);let u,f;const w=o.lastIndexOf("@");if(w>0){f=o.slice(w+1);const c=await n.getImageTagsByDigest(a,f,s);if(c.success&&c.data!==void 0){const d=c.data;u=d.find(l=>I.test(l))??d[0]}}else{const c=o.lastIndexOf(":");if(c>0){u=o.slice(c+1);const d=await n.getImageDigest(a,u,s);d.success&&(f=d.data)}}if(!(u===void 0||u===""))return{serviceName:e,imageTag:u,imageUri:`${a}:${u}`,taskDefinitionArn:r,previousTaskDefinitionArn:t,...f!==void 0?{imageDigest:f}:{},...S!==""?{ecrRepositoryArn:S}:{}}}export{h as createRestartServices,ce as probeSecretsDrift,fe as restart};
1
+ var x=Object.defineProperty;var p=(n,e)=>x(n,"name",{value:e,configurable:!0});import{success as k,failure as g}from"@fjall/generator";import{CONTENT_HASH_TAG_PATTERN as O,maskSensitiveOutput as E}from"@fjall/util";import{logger as L}from"@fjall/util/logger";import{SimpleAwsProvider as P}from"../../aws/SimpleAwsProvider.js";import{CloudFormationService as M}from"../../services/infrastructure/CloudFormationService.js";import{EcsService as _}from"../../services/infrastructure/EcsService.js";import{EcsServiceResolver as G}from"../../services/infrastructure/EcsServiceResolver.js";import{EcrImageInspectorService as F,stripTagOrDigest as H}from"../../services/infrastructure/EcrImageInspectorService.js";import{SsmParameterMetadataService as U}from"../../services/infrastructure/SsmParameterMetadataService.js";import{extractEcsServiceName as N}from"../../aws/utils/arnParser.js";import{isAborted as V}from"../../aws/organisations/types.js";import{matchesServiceName as j,resolveServiceTaskDefinition as B}from"../application/serviceImageTagsRollback.js";import{deriveEcrRepositoryArn as Y}from"../application/codeOnlyDeploy.js";import{registerAndRollTaskDefinition as q}from"../application/taskDefinitionRoll.js";import{computeSecretsDrift as h}from"./secretsDrift.js";const z="restartApplication";function b(n){const e=new P(n),i=new M(e);return{ecsService:new _(e),ecsResolver:new G(i),ecrImageInspector:new F(e),ssmService:new U(e)}}p(b,"createRestartServices");async function C(n,e){const{target:i}=e;let r,t;if(e.knownServices!==void 0)r=e.knownServices.clusterArn,t=e.knownServices.serviceArns;else{const s=await n.getDeployableClusterAndServices(i,e.abortSignal);if(!s.success)return g(new Error(`Failed to discover ECS services for ${i}: ${E(s.error.message)}`));r=s.data.clusterArn,t=s.data.serviceArns}if(r===void 0||t.length===0)return g(new Error(`No running ECS services found for ${i} \u2014 nothing to restart. Deploy the application first.`));if(e.serviceName!==void 0){const s=t.filter(o=>{const a=N(o);return a!==void 0&&e.serviceName!==void 0?j(a,e.serviceName):!1});if(s.length===0){const o=t.map(a=>N(a)).filter(a=>a!==void 0).join(", ");return g(new Error(`No ECS service matching "${e.serviceName}" found for ${i}. Live services: ${o||"none"}`))}t=s}return k({clusterArn:r,serviceArns:t})}p(C,"resolveRestartTargets");async function ue(n,e){const{ecsService:i,ecsResolver:r,ssmService:t}=e??b(n.awsCredentials),s=await C(r,n);return s.success?h({ecsService:i,ssmService:t},s.data,n.abortSignal):g(s.error)}p(ue,"probeSecretsDrift");async function le(n,e){const{target:i,callbacks:r,abortSignal:t}=n,s=Date.now(),{ecsService:o,ecsResolver:a,ecrImageInspector:A,ssmService:u}=e??b(n.awsCredentials),f=await C(a,n);if(!f.success)return r.onError?.(f.error),g(f.error);const{clusterArn:w,serviceArns:c}=f.data,d=await h({ecsService:o,ssmService:u},{clusterArn:w,serviceArns:c},t);let l,T={};d.success?(l=d.data,T=d.data.parameterVersions):r.onLog?.(`Could not compute secrets drift before restart: ${E(d.error.message)}. Restarting anyway.`,"warn");const D=[];for(const R of c){if(V(t))return k({target:i,artefacts:D,appliedParameterVersions:T,...l!==void 0?{preRestartDrift:l}:{},durationMs:Date.now()-s,cancelled:!0});const I=Date.now(),m=N(R)??R;r.onLog?.(`Restarting ${m}\u2026`,"info");const v=await B({ecsService:o},w,R,m,t);if(!v.success){const S=new Error(`Restart failed for ${m}: ${E(v.error.message)}`);return r.onError?.(S),g(S)}const $=await q({ecsService:o},{clusterArn:w,serviceArn:R,serviceName:m,taskDefinition:v.data.taskDefinition,containerDefinitions:v.data.containerDefinitions,previousTaskDefArn:v.data.previousTaskDefArn,startedAtMs:I},r,t);if(!$.success){const S=new Error(`Restart failed for ${m}: ${E($.error.message)}`);return r.onError?.(S),g(S)}const y=await J(A,m,v.data.containerDefinitions,$.data.newTaskDefArn,v.data.previousTaskDefArn,t);y!==void 0?D.push(y):r.onLog?.(`Restarted ${m}, but its image tag could not be recovered \u2014 the restart release will omit this service's artefact.`,"warn")}return L.debug(z,`Restarted ${i}`,{services:c.length,artefacts:D.length}),k({target:i,artefacts:D,appliedParameterVersions:T,...l!==void 0?{preRestartDrift:l}:{},durationMs:Date.now()-s})}p(le,"restart");async function J(n,e,i,r,t,s){const o=i.find(c=>c.image)?.image;if(o===void 0)return;const a=H(o),A=Y(a);let u,f;const w=o.lastIndexOf("@");if(w>0){f=o.slice(w+1);const c=await n.getImageTagsByDigest(a,f,s);if(c.success&&c.data!==void 0){const d=c.data;u=d.find(l=>O.test(l))??d[0]}}else{const c=o.lastIndexOf(":");if(c>0){u=o.slice(c+1);const d=await n.getImageDigest(a,u,s);d.success&&(f=d.data)}}if(!(u===void 0||u===""))return{serviceName:e,imageTag:u,imageUri:`${a}:${u}`,taskDefinitionArn:r,previousTaskDefinitionArn:t,...f!==void 0?{imageDigest:f}:{},...A!==""?{ecrRepositoryArn:A}:{}}}p(J,"deriveRestartArtefact");export{b as createRestartServices,ue as probeSecretsDrift,le as restart};
@@ -1 +1 @@
1
- import{success as D,failure as S}from"@fjall/generator";import{extractEcsServiceName as g}from"../../aws/utils/arnParser.js";import{isAborted as h}from"../../aws/organisations/types.js";const N=/^arn:aws[a-z-]*:ssm:[a-z0-9-]*:\d{12}:parameter/;function k(t){if(t.startsWith("arn:")){const r=t.match(N);if(!r)return;const n=t.slice(r[0].length);return n.startsWith("/")?n:`/${n}`}return t}function w(t){const r=new Set;for(const n of t.containerDefinitions??[])for(const a of n.secrets??[]){if(a.valueFrom===void 0)continue;const s=k(a.valueFrom);s!==void 0&&r.add(s)}return[...r].sort()}function M(t,r,n){if(t===void 0)return[];const a=new Map(n.map(o=>[o.name,o])),s=[];for(const o of r){const i=a.get(o);i?.lastModifiedDate!==void 0&&i.lastModifiedDate.getTime()>t.getTime()&&s.push({name:o,lastModifiedDate:i.lastModifiedDate.toISOString(),...i.version!==void 0?{version:i.version}:{}})}return s}async function b(t,r,n){const a=await t.ecsService.describeServices(r.clusterArn,r.serviceArns,n);if(!a.success)return S(new Error(`Failed to describe services: ${a.error.message}`));const s=[],o=new Set;for(const e of a.data){if(h(n))break;const c=e.serviceArn??"",d=e.serviceName??(c!==""?g(c):void 0);if(d===void 0||d==="")continue;const m=e.taskDefinition;if(m===void 0){s.push({serviceName:d,serviceArn:c,consumed:[]});continue}const f=await t.ecsService.getLatestTaskDefinition(m,n);if(!f.success||f.data===void 0){s.push({serviceName:d,serviceArn:c,taskDefinitionArn:m,consumed:[]});continue}const l=w(f.data);l.forEach(v=>o.add(v)),s.push({serviceName:d,serviceArn:c,taskDefinitionArn:m,...f.data.registeredAt!==void 0?{registeredAt:f.data.registeredAt}:{},consumed:l})}const i=await t.ssmService.describeParametersByName([...o].sort(),n);if(!i.success)return S(i.error);const u=i.data,p={};for(const e of u)e.version!==void 0&&(p[e.name]=e.version);const A=s.map(e=>{const c=M(e.registeredAt,e.consumed,u);return{serviceName:e.serviceName,serviceArn:e.serviceArn,...e.taskDefinitionArn!==void 0?{taskDefinitionArn:e.taskDefinitionArn}:{},...e.registeredAt!==void 0?{taskDefRegisteredAt:e.registeredAt.toISOString()}:{},consumedParameters:e.consumed,stale:c.length>0,staleParameters:c}});return D({services:A,anyStale:A.some(e=>e.stale),parameterVersions:p})}export{b as computeSecretsDrift,M as deriveStaleParameters,w as extractConsumedSsmParameters,k as ssmParameterNameFromValueFrom};
1
+ var g=Object.defineProperty;var m=(t,n)=>g(t,"name",{value:n,configurable:!0});import{success as h,failure as v}from"@fjall/generator";import{extractEcsServiceName as N}from"../../aws/utils/arnParser.js";import{isAborted as k}from"../../aws/organisations/types.js";const w=/^arn:aws[a-z-]*:ssm:[a-z0-9-]*:\d{12}:parameter/;function M(t){if(t.startsWith("arn:")){const n=t.match(w);if(!n)return;const r=t.slice(n[0].length);return r.startsWith("/")?r:`/${r}`}return t}m(M,"ssmParameterNameFromValueFrom");function P(t){const n=new Set;for(const r of t.containerDefinitions??[])for(const a of r.secrets??[]){if(a.valueFrom===void 0)continue;const s=M(a.valueFrom);s!==void 0&&n.add(s)}return[...n].sort()}m(P,"extractConsumedSsmParameters");function x(t,n,r){if(t===void 0)return[];const a=new Map(r.map(o=>[o.name,o])),s=[];for(const o of n){const i=a.get(o);i?.lastModifiedDate!==void 0&&i.lastModifiedDate.getTime()>t.getTime()&&s.push({name:o,lastModifiedDate:i.lastModifiedDate.toISOString(),...i.version!==void 0?{version:i.version}:{}})}return s}m(x,"deriveStaleParameters");async function T(t,n,r){const a=await t.ecsService.describeServices(n.clusterArn,n.serviceArns,r);if(!a.success)return v(new Error(`Failed to describe services: ${a.error.message}`));const s=[],o=new Set;for(const e of a.data){if(k(r))break;const c=e.serviceArn??"",d=e.serviceName??(c!==""?N(c):void 0);if(d===void 0||d==="")continue;const u=e.taskDefinition;if(u===void 0){s.push({serviceName:d,serviceArn:c,consumed:[]});continue}const f=await t.ecsService.getLatestTaskDefinition(u,r);if(!f.success||f.data===void 0){s.push({serviceName:d,serviceArn:c,taskDefinitionArn:u,consumed:[]});continue}const S=P(f.data);S.forEach(D=>o.add(D)),s.push({serviceName:d,serviceArn:c,taskDefinitionArn:u,...f.data.registeredAt!==void 0?{registeredAt:f.data.registeredAt}:{},consumed:S})}const i=await t.ssmService.describeParametersByName([...o].sort(),r);if(!i.success)return v(i.error);const p=i.data,A={};for(const e of p)e.version!==void 0&&(A[e.name]=e.version);const l=s.map(e=>{const c=x(e.registeredAt,e.consumed,p);return{serviceName:e.serviceName,serviceArn:e.serviceArn,...e.taskDefinitionArn!==void 0?{taskDefinitionArn:e.taskDefinitionArn}:{},...e.registeredAt!==void 0?{taskDefRegisteredAt:e.registeredAt.toISOString()}:{},consumedParameters:e.consumed,stale:c.length>0,staleParameters:c}});return h({services:l,anyStale:l.some(e=>e.stale),parameterVersions:A})}m(T,"computeSecretsDrift");export{T as computeSecretsDrift,x as deriveStaleParameters,P as extractConsumedSsmParameters,M as ssmParameterNameFromValueFrom};
@@ -1 +1 @@
1
- import{SecretsManagerClient as a,DescribeSecretCommand as m}from"@aws-sdk/client-secrets-manager";import{success as d,failure as n}from"@fjall/generator";import{getErrorMessage as p,maskSensitiveOutput as l}from"@fjall/util";import{composeSdkAbortSignal as f}from"../aws/organisations/types.js";const u=3e4;async function b(s,c){const i=s.getClient(a),o={};for(const r of c)try{const e=(await i.send(new m({SecretId:r}),{abortSignal:f(void 0,u)})).ARN;if(e===void 0||e==="")return n(new Error(`DescribeSecret for "${r}" returned no ARN \u2014 cannot resolve a complete ARN.`));o[r]=e}catch(t){const e=t instanceof Error?t.name:"";return e==="ResourceNotFoundException"?n(new Error(`Imported secret "${r}" does not exist in this account/region. Create it, or reference it by complete ARN via the secretsImport { arn } escape hatch.`)):e==="AccessDeniedException"||e==="AccessDenied"?n(new Error(`The deploy identity lacks secretsmanager:DescribeSecret on "${r}" \u2014 grant DescribeSecret on this secret so its complete ARN can be resolved.`)):n(new Error(`Failed to resolve imported secret "${r}": ${l(p(t))}`))}return d(o)}export{b as resolveSecretArns};
1
+ var m=Object.defineProperty;var i=(o,s)=>m(o,"name",{value:s,configurable:!0});import{SecretsManagerClient as d,DescribeSecretCommand as p}from"@aws-sdk/client-secrets-manager";import{success as l,failure as n}from"@fjall/generator";import{getErrorMessage as f,maskSensitiveOutput as u}from"@fjall/util";import{composeSdkAbortSignal as S}from"../aws/organisations/types.js";const E=3e4;async function v(o,s){const a=o.getClient(d),c={};for(const r of s)try{const e=(await a.send(new p({SecretId:r}),{abortSignal:S(void 0,E)})).ARN;if(e===void 0||e==="")return n(new Error(`DescribeSecret for "${r}" returned no ARN \u2014 cannot resolve a complete ARN.`));c[r]=e}catch(t){const e=t instanceof Error?t.name:"";return e==="ResourceNotFoundException"?n(new Error(`Imported secret "${r}" does not exist in this account/region. Create it, or reference it by complete ARN via the secretsImport { arn } escape hatch.`)):e==="AccessDeniedException"||e==="AccessDenied"?n(new Error(`The deploy identity lacks secretsmanager:DescribeSecret on "${r}" \u2014 grant DescribeSecret on this secret so its complete ARN can be resolved.`)):n(new Error(`Failed to resolve imported secret "${r}": ${u(f(t))}`))}return l(c)}i(v,"resolveSecretArns");export{v as resolveSecretArns};
@@ -1 +1 @@
1
- import{SimpleAwsProvider as v}from"../aws/SimpleAwsProvider.js";import{CdkService as f}from"../services/infrastructure/CdkService.js";import{CdkArgumentBuilder as p}from"../services/infrastructure/CdkArgumentBuilder.js";import{CdkProcessManager as w}from"../services/infrastructure/CdkProcessManager.js";import{CloudFormationService as g}from"../services/infrastructure/CloudFormationService.js";import{EcsService as u}from"../services/infrastructure/EcsService.js";import{EcsServiceResolver as b}from"../services/infrastructure/EcsServiceResolver.js";import{LambdaService as k}from"../services/infrastructure/LambdaService.js";import{EcrImageInspectorService as y}from"../services/infrastructure/EcrImageInspectorService.js";import{ApplicationStackService as A}from"../services/application/ApplicationStackService.js";import{TemplateHashService as C}from"../services/supporting/TemplateHashService.js";import{FrameworkRegistry as F}from"./builders/frameworkRegistry.js";function j(e){const r=new v(e.awsCredentials,e.mintScopedBuildSecretSession);r.exportToEnv();const c=new p,i=new w(c,e.abortSignal),n=new f({processManager:i,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{},...e.callbacks.onFailureAnalysis!==void 0?{onFailureAnalysis:e.callbacks.onFailureAnalysis}:{}}),o=new g(r),t=new u(r),s=new b(o),a=new k(r),l=new y(r),m=new A(n,o,r),S=new C,d=F.createDefault();return{awsProvider:r,cdkService:n,cfnService:o,ecsService:t,ecsResolver:s,lambdaService:a,ecrImageInspector:l,stackService:m,hashService:S,frameworkRegistry:d,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{},dispose(){i.dispose()}}}export{j as createDeployServices};
1
+ var f=Object.defineProperty;var c=(e,r)=>f(e,"name",{value:r,configurable:!0});import{SimpleAwsProvider as p}from"../aws/SimpleAwsProvider.js";import{CdkService as w}from"../services/infrastructure/CdkService.js";import{CdkArgumentBuilder as g}from"../services/infrastructure/CdkArgumentBuilder.js";import{CdkProcessManager as u}from"../services/infrastructure/CdkProcessManager.js";import{CloudFormationService as b}from"../services/infrastructure/CloudFormationService.js";import{EcsService as k}from"../services/infrastructure/EcsService.js";import{EcsServiceResolver as y}from"../services/infrastructure/EcsServiceResolver.js";import{LambdaService as A}from"../services/infrastructure/LambdaService.js";import{EcrImageInspectorService as C}from"../services/infrastructure/EcrImageInspectorService.js";import{ApplicationStackService as F}from"../services/application/ApplicationStackService.js";import{TemplateHashService as E}from"../services/supporting/TemplateHashService.js";import{FrameworkRegistry as I}from"./builders/frameworkRegistry.js";function G(e){const r=new p(e.awsCredentials,e.mintScopedBuildSecretSession);r.exportToEnv();const t=new g,i=new u(t,e.abortSignal),n=new w({processManager:i,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{},...e.callbacks.onFailureAnalysis!==void 0?{onFailureAnalysis:e.callbacks.onFailureAnalysis}:{}}),o=new b(r),s=new k(r),a=new y(o),l=new A(r),m=new C(r),S=new F(n,o,r),d=new E,v=I.createDefault();return{awsProvider:r,cdkService:n,cfnService:o,ecsService:s,ecsResolver:a,lambdaService:l,ecrImageInspector:m,stackService:S,hashService:d,frameworkRegistry:v,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{},dispose(){i.dispose()}}}c(G,"createDeployServices");export{G as createDeployServices};
@@ -1 +1 @@
1
- import{spawn as j}from"child_process";import{logger as l}from"@fjall/util/logger";function m(e){try{e.stdout?.destroy()}catch(s){l.debug("spawnHelpers","stdout destroy failed",{error:String(s)})}try{e.stderr?.destroy()}catch(s){l.debug("spawnHelpers","stderr destroy failed",{error:String(s)})}}function v(e){return e instanceof Error&&"code"in e&&e.code==="ENOENT"}function x(e){const{command:s,args:f,cwd:y,env:g,timeout:S,abortSignal:a,onStdoutData:b,onStderrData:E}=e;return new Promise(h=>{let c=!1;const r=t=>{c||(c=!0,h(t))};if(a?.aborted===!0){r({type:"aborted",stdout:"",stderr:""});return}const o=j(s,f,{cwd:y,shell:!1,stdio:["ignore","pipe","pipe"],env:g}),d=[],i=[];o.stdout?.on("data",t=>{const n=t.toString();d.push(n),b?.(n)}),o.stderr?.on("data",t=>{const n=t.toString();i.push(n),E?.(n)});const w=setTimeout(()=>{m(o),o.kill("SIGTERM"),u(),r({type:"timeout",stdout:d.join(""),stderr:i.join("")})},S),p=()=>{m(o),o.kill("SIGTERM"),u(),r({type:"aborted",stdout:d.join(""),stderr:i.join("")})},u=()=>{clearTimeout(w),a?.removeEventListener("abort",p)};a?.addEventListener("abort",p,{once:!0}),o.on("close",t=>{u(),r({type:"success",code:t??1,stdout:d.join(""),stderr:i.join("")})}),o.on("error",t=>{u(),v(t)?r({type:"enoent",error:t}):r({type:"spawn_error",error:t})})})}export{m as destroyProcessStreams,v as isEnoentError,x as spawnWithTimeout};
1
+ var v=Object.defineProperty;var s=(e,r)=>v(e,"name",{value:r,configurable:!0});import{spawn as T}from"child_process";import{logger as m}from"@fjall/util/logger";function f(e){try{e.stdout?.destroy()}catch(r){m.debug("spawnHelpers","stdout destroy failed",{error:String(r)})}try{e.stderr?.destroy()}catch(r){m.debug("spawnHelpers","stderr destroy failed",{error:String(r)})}}s(f,"destroyProcessStreams");function k(e){return e instanceof Error&&"code"in e&&e.code==="ENOENT"}s(k,"isEnoentError");function C(e){const{command:r,args:y,cwd:g,env:S,timeout:b,abortSignal:c,onStdoutData:E,onStderrData:h}=e;return new Promise(w=>{let p=!1;const n=s(t=>{p||(p=!0,w(t))},"safeResolve");if(c?.aborted===!0){n({type:"aborted",stdout:"",stderr:""});return}const o=T(r,y,{cwd:g,shell:!1,stdio:["ignore","pipe","pipe"],env:S}),i=[],u=[];o.stdout?.on("data",t=>{const d=t.toString();i.push(d),E?.(d)}),o.stderr?.on("data",t=>{const d=t.toString();u.push(d),h?.(d)});const j=setTimeout(()=>{f(o),o.kill("SIGTERM"),a(),n({type:"timeout",stdout:i.join(""),stderr:u.join("")})},b),l=s(()=>{f(o),o.kill("SIGTERM"),a(),n({type:"aborted",stdout:i.join(""),stderr:u.join("")})},"onAbort"),a=s(()=>{clearTimeout(j),c?.removeEventListener("abort",l)},"cleanup");c?.addEventListener("abort",l,{once:!0}),o.on("close",t=>{a(),n({type:"success",code:t??1,stdout:i.join(""),stderr:u.join("")})}),o.on("error",t=>{a(),k(t)?n({type:"enoent",error:t}):n({type:"spawn_error",error:t})})})}s(C,"spawnWithTimeout");export{f as destroyProcessStreams,k as isEnoentError,C as spawnWithTimeout};
@@ -1 +1 @@
1
- import{ListObjectVersionsCommand as K,DeleteObjectsCommand as b,GetBucketTaggingCommand as x}from"@aws-sdk/client-s3";import{logger as g}from"@fjall/util/logger";import{getErrorMessage as y,maskSensitiveOutput as E}from"@fjall/util";import{SDK_PRE_EMPTY_TAG_KEY as C}from"@fjall/util/aws";import{composeSdkAbortSignal as $,extractErrorName as A,isAborted as B,isAccessDenied as D}from"../../aws/organisations/types.js";import{STACK_CLEANUP_LOG as l,warnViaCallbacks as m}from"./logging.js";const T=1e3,P=["aws-cdk:auto-delete-objects",C],V=new Set(["NoSuchBucket","NotFound","404"]);function _(t){return V.has(A(t))?!0:(t instanceof Error?t.message??"":"").includes("NoSuchBucket")}function M(t){const e=t instanceof Error?t.message??"":"";return D(A(t))||e.includes("AccessDenied")}async function L(t,e,i,a){let s,d,u=!1;i?.onStackCleanupProgress?.(e,"emptying-bucket");const r=1e3;let f=0;for(;f++<r;){if(B(a))return g.debug(l,`Aborted while emptying bucket ${e}`),"failed";let n;try{n=await t.send(new K({Bucket:e,KeyMarker:s,VersionIdMarker:d}),{abortSignal:$(a)})}catch(o){if(_(o))return g.debug(l,`Bucket ${e} no longer exists, skipping`),"missing";const S=`Unexpected error emptying bucket ${e}: ${E(y(o))}`;return m(S,i),M(o)?"access-denied":"failed"}const h=[...n.Versions??[],...n.DeleteMarkers??[]];if(h.length===0)break;for(let o=0;o<h.length;o+=T){if(B(a))return g.debug(l,`Aborted while emptying bucket ${e}`),"failed";const S=h.slice(o,o+T);try{const c=(await t.send(new b({Bucket:e,Delete:{Objects:S.map(p=>({Key:p.Key,VersionId:p.VersionId})),Quiet:!0}}),{abortSignal:$(a)})).Errors??[];if(c.length>0){const p=`Failed to delete ${c.length} object(s) from ${e}: ${E(c[0]?.Message??c[0]?.Code??"unknown error")}`;if(m(p,i),c.some(w=>w.Code?.includes("AccessDenied")))return"access-denied";u=!0}}catch(k){const c=`Failed to delete batch from ${e}: ${E(y(k))}`;if(m(c,i),M(k))return"access-denied";u=!0}}if(!n.IsTruncated)break;s=n.NextKeyMarker,d=n.NextVersionIdMarker}if(f>r){const n=`Bucket ${e} reached ${r} page limit \u2014 some objects may remain`;return m(n,i),"failed"}return g.debug(l,`Emptied bucket ${e}`),u?"failed":"emptied"}async function Y(t,e,i,a){try{const s=await t.send(new x({Bucket:e}),{abortSignal:$(a)}),d=P;return(s.TagSet??[]).some(r=>r.Key!==void 0&&d.includes(r.Key)&&r.Value==="true")?"matched":"retained"}catch(s){const d=A(s),u=s instanceof Error?s.message??"":"",r=_(s),f=d==="NoSuchTagSet"||u.includes("NoSuchTagSet");if(r||f)return g.debug(l,`Bucket ${e} ${r?"no longer exists":"has no tags"} \u2014 skipping`),r?"gone":"retained";const n=`Could not read tags for bucket ${e}, leaving it untouched: ${E(y(s))}`;return m(n,i),"unreadable"}}export{V as BUCKET_GONE_ERROR_NAMES,P as PRE_EMPTY_TAG_KEYS,Y as classifyBucketForPreEmpty,L as emptyS3Bucket,_ as isBucketGoneError};
1
+ var b=Object.defineProperty;var g=(t,e)=>b(t,"name",{value:e,configurable:!0});import{ListObjectVersionsCommand as x,DeleteObjectsCommand as C,GetBucketTaggingCommand as D}from"@aws-sdk/client-s3";import{logger as l}from"@fjall/util/logger";import{getErrorMessage as $,maskSensitiveOutput as h}from"@fjall/util";import{SDK_PRE_EMPTY_TAG_KEY as P}from"@fjall/util/aws";import{composeSdkAbortSignal as A,extractErrorName as B,isAborted as T,isAccessDenied as V}from"../../aws/organisations/types.js";import{STACK_CLEANUP_LOG as m,warnViaCallbacks as f}from"./logging.js";const _=1e3,G=["aws-cdk:auto-delete-objects",P],I=new Set(["NoSuchBucket","NotFound","404"]);function M(t){return I.has(B(t))?!0:(t instanceof Error?t.message??"":"").includes("NoSuchBucket")}g(M,"isBucketGoneError");function w(t){const e=t instanceof Error?t.message??"":"";return V(B(t))||e.includes("AccessDenied")}g(w,"isAccessDeniedError");async function U(t,e,i,a){let s,d,u=!1;i?.onStackCleanupProgress?.(e,"emptying-bucket");const r=1e3;let p=0;for(;p++<r;){if(T(a))return l.debug(m,`Aborted while emptying bucket ${e}`),"failed";let n;try{n=await t.send(new x({Bucket:e,KeyMarker:s,VersionIdMarker:d}),{abortSignal:A(a)})}catch(o){if(M(o))return l.debug(m,`Bucket ${e} no longer exists, skipping`),"missing";const k=`Unexpected error emptying bucket ${e}: ${h($(o))}`;return f(k,i),w(o)?"access-denied":"failed"}const S=[...n.Versions??[],...n.DeleteMarkers??[]];if(S.length===0)break;for(let o=0;o<S.length;o+=_){if(T(a))return l.debug(m,`Aborted while emptying bucket ${e}`),"failed";const k=S.slice(o,o+_);try{const c=(await t.send(new C({Bucket:e,Delete:{Objects:k.map(E=>({Key:E.Key,VersionId:E.VersionId})),Quiet:!0}}),{abortSignal:A(a)})).Errors??[];if(c.length>0){const E=`Failed to delete ${c.length} object(s) from ${e}: ${h(c[0]?.Message??c[0]?.Code??"unknown error")}`;if(f(E,i),c.some(K=>K.Code?.includes("AccessDenied")))return"access-denied";u=!0}}catch(y){const c=`Failed to delete batch from ${e}: ${h($(y))}`;if(f(c,i),w(y))return"access-denied";u=!0}}if(!n.IsTruncated)break;s=n.NextKeyMarker,d=n.NextVersionIdMarker}if(p>r){const n=`Bucket ${e} reached ${r} page limit \u2014 some objects may remain`;return f(n,i),"failed"}return l.debug(m,`Emptied bucket ${e}`),u?"failed":"emptied"}g(U,"emptyS3Bucket");async function N(t,e,i,a){try{const s=await t.send(new D({Bucket:e}),{abortSignal:A(a)}),d=G;return(s.TagSet??[]).some(r=>r.Key!==void 0&&d.includes(r.Key)&&r.Value==="true")?"matched":"retained"}catch(s){const d=B(s),u=s instanceof Error?s.message??"":"",r=M(s),p=d==="NoSuchTagSet"||u.includes("NoSuchTagSet");if(r||p)return l.debug(m,`Bucket ${e} ${r?"no longer exists":"has no tags"} \u2014 skipping`),r?"gone":"retained";const n=`Could not read tags for bucket ${e}, leaving it untouched: ${h($(s))}`;return f(n,i),"unreadable"}}g(N,"classifyBucketForPreEmpty");export{I as BUCKET_GONE_ERROR_NAMES,G as PRE_EMPTY_TAG_KEYS,N as classifyBucketForPreEmpty,U as emptyS3Bucket,M as isBucketGoneError};
@@ -1 +1 @@
1
- import{CloudFormationClient as A,DescribeStacksCommand as m,DeleteStackCommand as C}from"@aws-sdk/client-cloudformation";import{S3Client as P}from"@aws-sdk/client-s3";import{NodeHttpHandler as D}from"@smithy/node-http-handler";import{logger as u}from"@fjall/util/logger";import{getErrorMessage as f,maskSensitiveOutput as p,sleep as R}from"@fjall/util";import{composeSdkAbortSignal as w,isAborted as _}from"../../aws/organisations/types.js";import{STACK_NOT_FOUND_PATTERN as g,isCleanableState as F}from"../../types/constants.js";import{emptyS3Bucket as I}from"./bucketOps.js";import{STACK_CLEANUP_LOG as s,warnViaCallbacks as M}from"./logging.js";import{formatQuarantineSuspectedMessage as k}from"./messages.js";import{collectStackResources as y}from"./stackResources.js";async function O(e,t,a){return y(e,t,r=>r.ResourceType==="AWS::S3::Bucket"&&r.ResourceStatus==="DELETE_FAILED",r=>r.PhysicalResourceId,a)}function h(e,t){return t===void 0?R(e):t.aborted?Promise.resolve():new Promise(a=>{const r=()=>{clearTimeout(n),a()},n=setTimeout(()=>{t.removeEventListener("abort",r),a()},e);t.addEventListener("abort",r,{once:!0})})}async function J(e,t,a,r,n){const S=r?.timeoutMs??3e5,E=r?.pollMs??5e3,o=r?.abortSignal;try{const c=new A({region:t,credentials:a,requestHandler:new D({requestTimeout:15e3})});let d;try{d=(await c.send(new m({StackName:e}),{abortSignal:w(o)})).Stacks?.[0]?.StackStatus}catch(i){if(i instanceof Error&&i.message?.includes(g)){u.debug(s,`Stack ${e} does not exist, no cleanup needed`);return}u.warn(s,`Failed to check stack status: ${p(f(i))}`,{stackName:e,region:t});return}if(!d||!F(d)){u.debug(s,`Stack ${e} status ${d??"unknown"} is not cleanable, skipping`);return}u.warn(s,`Cleaning up ${e} stack in ${d} state`,{region:t}),n?.onStackCleanupProgress?.(e,"deleting-stack");const b=new P({region:t,credentials:a,requestHandler:new D({requestTimeout:15e3})});try{const i=await O(c,e,o);for(const l of i){if(_(o))break;if(u.warn(s,`Emptying bucket ${l}`,{region:t}),await I(b,l,n,o)==="access-denied"){const L={bucketName:l,...r?.accountId!==void 0?{accountId:r.accountId}:{}};u.warn(s,k(e,L),{region:t}),n?.onStackCleanupProgress?.(e,"quarantine-suspected",L)}}}catch(i){const l=`Failed to empty S3 buckets: ${p(f(i))}`;M(l,n,{stackName:e,region:t})}await c.send(new C({StackName:e}),{abortSignal:w(o)}),n?.onStackCleanupProgress?.(e,"waiting");const T=await $(c,e,S,E,o);if(T==="DELETE_COMPLETE"){u.warn(s,`${e} stack deleted successfully`,{region:t}),n?.onStackCleanupProgress?.(e,"complete");return}if(T==="DELETE_FAILED"){u.warn(s,`${e} still in DELETE_FAILED, retrying with RetainResources`,{region:t});const i=await B(c,e,o);if(i.length===0)u.warn(s,`${e} in DELETE_FAILED but no non-bucket resources to retain \u2014 cannot retry`,{region:t}),n?.onStackCleanupProgress?.(e,"error");else{await c.send(new C({StackName:e,RetainResources:i}),{abortSignal:w(o)});const l=await $(c,e,S,E,o);l==="DELETE_COMPLETE"?(u.warn(s,`${e} stack deleted on retry (retained: ${i.join(", ")})`,{region:t}),n?.onStackCleanupProgress?.(e,"complete")):(u.warn(s,`${e} stack still not deleted after retry: ${l}`,{region:t}),n?.onStackCleanupProgress?.(e,"error"))}}}catch(c){u.warn(s,`Stack cleanup failed: ${p(f(c))}`,{stackName:e,region:t}),n?.onStackCleanupProgress?.(e,"error")}}async function $(e,t,a,r,n){const S=Date.now();for(;Date.now()-S<a;){if(await h(r,n),_(n))return"ABORTED";try{const o=(await e.send(new m({StackName:t}),{abortSignal:w(n)})).Stacks?.[0]?.StackStatus;if(!o||o==="DELETE_COMPLETE")return"DELETE_COMPLETE";if(o==="DELETE_FAILED")return"DELETE_FAILED";u.debug(s,`Waiting for ${t}: ${o}`)}catch(E){if(E instanceof Error&&E.message?.includes(g))return"DELETE_COMPLETE";throw u.debug(s,`Unexpected error polling ${t}: ${p(f(E))}`),E}}return"TIMEOUT"}async function B(e,t,a){return y(e,t,r=>r.ResourceStatus==="DELETE_FAILED"&&r.ResourceType!=="AWS::S3::Bucket",r=>r.LogicalResourceId,a)}export{J as cleanupFailedStack};
1
+ var P=Object.defineProperty;var d=(e,t)=>P(e,"name",{value:t,configurable:!0});import{CloudFormationClient as R,DescribeStacksCommand as C,DeleteStackCommand as D}from"@aws-sdk/client-cloudformation";import{S3Client as F}from"@aws-sdk/client-s3";import{NodeHttpHandler as _}from"@smithy/node-http-handler";import{logger as u}from"@fjall/util/logger";import{getErrorMessage as p,maskSensitiveOutput as w,sleep as I}from"@fjall/util";import{composeSdkAbortSignal as T,isAborted as g}from"../../aws/organisations/types.js";import{STACK_NOT_FOUND_PATTERN as y,isCleanableState as M}from"../../types/constants.js";import{emptyS3Bucket as k}from"./bucketOps.js";import{STACK_CLEANUP_LOG as s,warnViaCallbacks as O}from"./logging.js";import{formatQuarantineSuspectedMessage as h}from"./messages.js";import{collectStackResources as $}from"./stackResources.js";async function B(e,t,a){return $(e,t,r=>r.ResourceType==="AWS::S3::Bucket"&&r.ResourceStatus==="DELETE_FAILED",r=>r.PhysicalResourceId,a)}d(B,"findFailedStackBuckets");function q(e,t){return t===void 0?I(e):t.aborted?Promise.resolve():new Promise(a=>{const r=d(()=>{clearTimeout(n),a()},"onAbort"),n=setTimeout(()=>{t.removeEventListener("abort",r),a()},e);t.addEventListener("abort",r,{once:!0})})}d(q,"sleepAbortable");async function Z(e,t,a,r,n){const f=r?.timeoutMs??3e5,E=r?.pollMs??5e3,o=r?.abortSignal;try{const c=new R({region:t,credentials:a,requestHandler:new _({requestTimeout:15e3})});let S;try{S=(await c.send(new C({StackName:e}),{abortSignal:T(o)})).Stacks?.[0]?.StackStatus}catch(i){if(i instanceof Error&&i.message?.includes(y)){u.debug(s,`Stack ${e} does not exist, no cleanup needed`);return}u.warn(s,`Failed to check stack status: ${w(p(i))}`,{stackName:e,region:t});return}if(!S||!M(S)){u.debug(s,`Stack ${e} status ${S??"unknown"} is not cleanable, skipping`);return}u.warn(s,`Cleaning up ${e} stack in ${S} state`,{region:t}),n?.onStackCleanupProgress?.(e,"deleting-stack");const A=new F({region:t,credentials:a,requestHandler:new _({requestTimeout:15e3})});try{const i=await B(c,e,o);for(const l of i){if(g(o))break;if(u.warn(s,`Emptying bucket ${l}`,{region:t}),await k(A,l,n,o)==="access-denied"){const m={bucketName:l,...r?.accountId!==void 0?{accountId:r.accountId}:{}};u.warn(s,h(e,m),{region:t}),n?.onStackCleanupProgress?.(e,"quarantine-suspected",m)}}}catch(i){const l=`Failed to empty S3 buckets: ${w(p(i))}`;O(l,n,{stackName:e,region:t})}await c.send(new D({StackName:e}),{abortSignal:T(o)}),n?.onStackCleanupProgress?.(e,"waiting");const L=await b(c,e,f,E,o);if(L==="DELETE_COMPLETE"){u.warn(s,`${e} stack deleted successfully`,{region:t}),n?.onStackCleanupProgress?.(e,"complete");return}if(L==="DELETE_FAILED"){u.warn(s,`${e} still in DELETE_FAILED, retrying with RetainResources`,{region:t});const i=await v(c,e,o);if(i.length===0)u.warn(s,`${e} in DELETE_FAILED but no non-bucket resources to retain \u2014 cannot retry`,{region:t}),n?.onStackCleanupProgress?.(e,"error");else{await c.send(new D({StackName:e,RetainResources:i}),{abortSignal:T(o)});const l=await b(c,e,f,E,o);l==="DELETE_COMPLETE"?(u.warn(s,`${e} stack deleted on retry (retained: ${i.join(", ")})`,{region:t}),n?.onStackCleanupProgress?.(e,"complete")):(u.warn(s,`${e} stack still not deleted after retry: ${l}`,{region:t}),n?.onStackCleanupProgress?.(e,"error"))}}}catch(c){u.warn(s,`Stack cleanup failed: ${w(p(c))}`,{stackName:e,region:t}),n?.onStackCleanupProgress?.(e,"error")}}d(Z,"cleanupFailedStack");async function b(e,t,a,r,n){const f=Date.now();for(;Date.now()-f<a;){if(await q(r,n),g(n))return"ABORTED";try{const o=(await e.send(new C({StackName:t}),{abortSignal:T(n)})).Stacks?.[0]?.StackStatus;if(!o||o==="DELETE_COMPLETE")return"DELETE_COMPLETE";if(o==="DELETE_FAILED")return"DELETE_FAILED";u.debug(s,`Waiting for ${t}: ${o}`)}catch(E){if(E instanceof Error&&E.message?.includes(y))return"DELETE_COMPLETE";throw u.debug(s,`Unexpected error polling ${t}: ${w(p(E))}`),E}}return"TIMEOUT"}d(b,"pollForDeletion");async function v(e,t,a){return $(e,t,r=>r.ResourceStatus==="DELETE_FAILED"&&r.ResourceType!=="AWS::S3::Bucket",r=>r.LogicalResourceId,a)}d(v,"findNonBucketFailedResources");export{Z as cleanupFailedStack};
@@ -1 +1 @@
1
- import{logger as a}from"@fjall/util/logger";const t="stackCleanup";function l(o,n,r){a.warn(t,o,r),n?.onLog?.(o,"warn")}export{t as STACK_CLEANUP_LOG,l as warnViaCallbacks};
1
+ var t=Object.defineProperty;var r=(o,n)=>t(o,"name",{value:n,configurable:!0});import{logger as c}from"@fjall/util/logger";const l="stackCleanup";function i(o,n,a){c.warn(l,o,a),n?.onLog?.(o,"warn")}r(i,"warnViaCallbacks");export{l as STACK_CLEANUP_LOG,i as warnViaCallbacks};
@@ -1 +1 @@
1
- function u(n,e){const t=e?.bucketName??"unknown bucket",o=e?.accountId!==void 0?` (account ${e.accountId})`:"",c=e?.bucketName!==void 0&&e?.accountId!==void 0?` \u2014 run \`fjall unlock bucket ${e.bucketName} --account ${e.accountId} --acknowledge-root-session\` to remove it`:" \u2014 a root session is required to remove it (fjall unlock bucket)";return`Bucket ${t} in stack ${n}${o} appears quarantined by a stranded deny policy${c}`}function a(n,e){return`Stack ${n}: bucket(s) retained, not deleted: ${e.join(", ")} \u2014 production buckets survive destroy by design (RemovalPolicy.RETAIN). Delete deliberately when the data is no longer needed, or redeploy with an explicit DESTROY removal policy before destroying.`}export{u as formatQuarantineSuspectedMessage,a as formatRetainedBucketsMessage};
1
+ var a=Object.defineProperty;var t=(n,e)=>a(n,"name",{value:e,configurable:!0});function s(n,e){const o=e?.bucketName??"unknown bucket",c=e?.accountId!==void 0?` (account ${e.accountId})`:"",u=e?.bucketName!==void 0&&e?.accountId!==void 0?` \u2014 run \`fjall unlock bucket ${e.bucketName} --account ${e.accountId} --acknowledge-root-session\` to remove it`:" \u2014 a root session is required to remove it (fjall unlock bucket)";return`Bucket ${o} in stack ${n}${c} appears quarantined by a stranded deny policy${u}`}t(s,"formatQuarantineSuspectedMessage");function d(n,e){return`Stack ${n}: bucket(s) retained, not deleted: ${e.join(", ")} \u2014 production buckets survive destroy by design (RemovalPolicy.RETAIN). Delete deliberately when the data is no longer needed, or redeploy with an explicit DESTROY removal policy before destroying.`}t(d,"formatRetainedBucketsMessage");export{s as formatQuarantineSuspectedMessage,d as formatRetainedBucketsMessage};
@@ -1 +1 @@
1
- import{DeleteBucketCommand as p}from"@aws-sdk/client-s3";import{logger as a}from"@fjall/util/logger";import{getErrorMessage as d,maskSensitiveOutput as k}from"@fjall/util";import{composeSdkAbortSignal as h,isAborted as w}from"../../aws/organisations/types.js";import{classifyBucketForPreEmpty as y,emptyS3Bucket as g,isBucketGoneError as l}from"./bucketOps.js";import{STACK_CLEANUP_LOG as B,warnViaCallbacks as b}from"./logging.js";async function $(i,m,o,r){const t={swept:[],failed:[]};for(const e of m){if(w(r))break;const n=await y(i,e,o,r);if(n==="unreadable"){t.failed.push(e);continue}if(n!=="matched")continue;const s=await g(i,e,o,r);if(s==="missing")continue;if(s!=="emptied"){t.failed.push(e);continue}try{await i.send(new p({Bucket:e}),{abortSignal:h(r)})}catch(u){if(l(u))continue;const f=`Could not delete orphaned bucket ${e} after stack delete: ${k(d(u))}`;b(f,o),t.failed.push(e);continue}t.swept.push(e);const c=`Swept orphaned bucket ${e} left behind by the stack delete`;a.info(B,c),o?.onLog?.(c,"info")}return t}export{$ as sweepOrphanedDestroyBuckets};
1
+ var a=Object.defineProperty;var f=(t,n)=>a(t,"name",{value:n,configurable:!0});import{DeleteBucketCommand as d}from"@aws-sdk/client-s3";import{logger as k}from"@fjall/util/logger";import{getErrorMessage as h,maskSensitiveOutput as w}from"@fjall/util";import{composeSdkAbortSignal as y,isAborted as g}from"../../aws/organisations/types.js";import{classifyBucketForPreEmpty as l,emptyS3Bucket as B,isBucketGoneError as b}from"./bucketOps.js";import{STACK_CLEANUP_LOG as A,warnViaCallbacks as E}from"./logging.js";async function G(t,n,r,i){const o={swept:[],failed:[]};for(const e of n){if(g(i))break;const s=await l(t,e,r,i);if(s==="unreadable"){o.failed.push(e);continue}if(s!=="matched")continue;const c=await B(t,e,r,i);if(c==="missing")continue;if(c!=="emptied"){o.failed.push(e);continue}try{await t.send(new d({Bucket:e}),{abortSignal:y(i)})}catch(m){if(b(m))continue;const p=`Could not delete orphaned bucket ${e} after stack delete: ${w(h(m))}`;E(p,r),o.failed.push(e);continue}o.swept.push(e);const u=`Swept orphaned bucket ${e} left behind by the stack delete`;k.info(A,u),r?.onLog?.(u,"info")}return o}f(G,"sweepOrphanedDestroyBuckets");export{G as sweepOrphanedDestroyBuckets};
@@ -1 +1 @@
1
- import{success as m,failure as k}from"@fjall/generator";import{logger as h}from"@fjall/util/logger";import{getErrorMessage as n,maskSensitiveOutput as d}from"@fjall/util";import{isAborted as y}from"../../aws/organisations/types.js";import{STACK_NOT_FOUND_PATTERN as g}from"../../types/constants.js";import{classifyBucketForPreEmpty as S,emptyS3Bucket as B}from"./bucketOps.js";import{STACK_CLEANUP_LOG as w,warnViaCallbacks as a}from"./logging.js";import{formatRetainedBucketsMessage as A}from"./messages.js";import{collectStackResources as E}from"./stackResources.js";async function b(i,c,s,o,p){let u;try{u=await E(i,s,e=>e.ResourceType==="AWS::S3::Bucket",e=>e.PhysicalResourceId,p)}catch(e){if(e instanceof Error&&e.message?.includes(g))return h.debug(w,`Stack ${s} does not exist, nothing to pre-empty`),m({matched:[],skipped:[]});const r=`Could not list ${s} resources for the pre-empty pass: ${d(n(e))}`;return a(r,o),k(new Error(r))}const t={matched:[],skipped:[]};for(const e of u){if(y(p))break;try{const r=await S(c,e,o,p);r==="matched"?(t.matched.push(e),await B(c,e,o,p)):(r==="retained"||r==="unreadable")&&t.skipped.push(e)}catch(r){const f=`Pre-empty pass failed for bucket ${e}: ${d(n(r))}`;a(f,o)}}return t.skipped.length>0&&o?.onStackCleanupProgress?.(s,"retained-buckets",{retainedBuckets:[...t.skipped],message:A(s,t.skipped)}),m(t)}function v(i){return i.success?i.data.matched:[]}export{v as capturedSweepBuckets,b as preEmptyStackBuckets};
1
+ var h=Object.defineProperty;var u=(t,p)=>h(t,"name",{value:p,configurable:!0});import{success as n,failure as y}from"@fjall/generator";import{logger as g}from"@fjall/util/logger";import{getErrorMessage as d,maskSensitiveOutput as a}from"@fjall/util";import{isAborted as S}from"../../aws/organisations/types.js";import{STACK_NOT_FOUND_PATTERN as B}from"../../types/constants.js";import{classifyBucketForPreEmpty as w,emptyS3Bucket as A}from"./bucketOps.js";import{STACK_CLEANUP_LOG as E,warnViaCallbacks as f}from"./logging.js";import{formatRetainedBucketsMessage as P}from"./messages.js";import{collectStackResources as T}from"./stackResources.js";async function K(t,p,o,i,c){let m;try{m=await T(t,o,e=>e.ResourceType==="AWS::S3::Bucket",e=>e.PhysicalResourceId,c)}catch(e){if(e instanceof Error&&e.message?.includes(B))return g.debug(E,`Stack ${o} does not exist, nothing to pre-empty`),n({matched:[],skipped:[]});const r=`Could not list ${o} resources for the pre-empty pass: ${a(d(e))}`;return f(r,i),y(new Error(r))}const s={matched:[],skipped:[]};for(const e of m){if(S(c))break;try{const r=await w(p,e,i,c);r==="matched"?(s.matched.push(e),await A(p,e,i,c)):(r==="retained"||r==="unreadable")&&s.skipped.push(e)}catch(r){const k=`Pre-empty pass failed for bucket ${e}: ${a(d(r))}`;f(k,i)}}return s.skipped.length>0&&i?.onStackCleanupProgress?.(o,"retained-buckets",{retainedBuckets:[...s.skipped],message:P(o,s.skipped)}),n(s)}u(K,"preEmptyStackBuckets");function L(t){return t.success?t.data.matched:[]}u(L,"capturedSweepBuckets");export{L as capturedSweepBuckets,K as preEmptyStackBuckets};
@@ -1 +1 @@
1
- import{ListStackResourcesCommand as S}from"@aws-sdk/client-cloudformation";import{logger as k}from"@fjall/util/logger";import{composeSdkAbortSignal as l,isAborted as p}from"../../aws/organisations/types.js";import{STACK_CLEANUP_LOG as u}from"./logging.js";async function w(n,o,a,m,t){const r=[];let e,f=0;do{if(p(t))break;if(f++>=100){k.warn(u,"Reached 100 page limit listing stack resources",{stackName:o});break}const s=await n.send(new S({StackName:o,NextToken:e}),{abortSignal:l(t)});for(const c of s.StackResourceSummaries??[])if(a(c)){const i=m(c);i&&r.push(i)}e=s.NextToken}while(e);return r}export{w as collectStackResources};
1
+ var k=Object.defineProperty;var a=(o,e)=>k(o,"name",{value:e,configurable:!0});import{ListStackResourcesCommand as l}from"@aws-sdk/client-cloudformation";import{logger as p}from"@fjall/util/logger";import{composeSdkAbortSignal as u,isAborted as A}from"../../aws/organisations/types.js";import{STACK_CLEANUP_LOG as d}from"./logging.js";async function G(o,e,m,f,r){const s=[];let t,S=0;do{if(A(r))break;if(S++>=100){p.warn(d,"Reached 100 page limit listing stack resources",{stackName:e});break}const c=await o.send(new l({StackName:e,NextToken:t}),{abortSignal:u(r)});for(const i of c.StackResourceSummaries??[])if(m(i)){const n=f(i);n&&s.push(n)}t=c.NextToken}while(t);return s}a(G,"collectStackResources");export{G as collectStackResources};
@@ -1 +1 @@
1
- import{success as u,failure as c}from"@fjall/generator";import{isAborted as f}from"../aws/organisations/types.js";async function S(t,e,o,n){t.onStepStart?.(e.stepId,e.stepName,e.stepIndex,e.totalSteps);let r;try{r=await o()}catch(i){const m=i instanceof Error?i:new Error(String(i));return d(t,e,m,n)}return r.kind==="error"?d(t,e,r.error,n):(t.onStepComplete?.(e.stepId,e.stepName,r.kind,e.stepIndex,e.totalSteps),u(r.data))}function d(t,e,o,n){const r=f(n);return t.onStepComplete?.(e.stepId,e.stepName,r?"cancelled":"error",e.stepIndex,e.totalSteps),r||t.onError?.(o),c(o)}export{S as withStepLifecycle};
1
+ var c=Object.defineProperty;var d=(r,e)=>c(r,"name",{value:e,configurable:!0});import{success as f,failure as p}from"@fjall/generator";import{isAborted as a}from"../aws/organisations/types.js";async function s(r,e,o,n){r.onStepStart?.(e.stepId,e.stepName,e.stepIndex,e.totalSteps);let t;try{t=await o()}catch(i){const u=i instanceof Error?i:new Error(String(i));return m(r,e,u,n)}return t.kind==="error"?m(r,e,t.error,n):(r.onStepComplete?.(e.stepId,e.stepName,t.kind,e.stepIndex,e.totalSteps),f(t.data))}d(s,"withStepLifecycle");function m(r,e,o,n){const t=a(n);return r.onStepComplete?.(e.stepId,e.stepName,t?"cancelled":"error",e.stepIndex,e.totalSteps),t||r.onError?.(o),p(o)}d(m,"emitFailure");export{s as withStepLifecycle};
@@ -1 +1 @@
1
- import{DeleteBucketCommand as f,HeadBucketCommand as y,ListObjectVersionsCommand as k}from"@aws-sdk/client-s3";import{DescribeTrailsCommand as g}from"@aws-sdk/client-cloudtrail";import{DescribeKeyCommand as w,ScheduleKeyDeletionCommand as S}from"@aws-sdk/client-kms";import{success as o,failure as a}from"@fjall/generator";import{getErrorMessage as s,maskSensitiveOutput as c}from"@fjall/util";import{logger as E}from"@fjall/util/logger";import{composeSdkAbortSignal as l,extractErrorName as m}from"../../aws/organisations/types.js";import{emptyS3Bucket as C,isBucketGoneError as b}from"../stackCleanup.js";const p=14,N=new Set(["NotFoundException","KMSInvalidStateException"]);async function D(t,e,n){try{const r=await t.send(new g({trailNameList:[e]}),{abortSignal:l(n)});return o((r.trailList??[]).length>0)}catch(r){return m(r)==="TrailNotFoundException"?o(!1):a(new Error(`Failed to probe trail ${e}: ${c(s(r))}`))}}async function h(t,e,n){try{return await t.send(new y({Bucket:e}),{abortSignal:l(n)}),o(!0)}catch(r){return b(r)?o(!1):a(new Error(`Failed to probe bucket ${e}: ${c(s(r))}`))}}async function K(t,e,n){try{const r=await t.send(new k({Bucket:e,MaxKeys:1}),{abortSignal:l(n)});return(r.Versions??[]).length===0&&(r.DeleteMarkers??[]).length===0}catch(r){return E.debug("memberTrailCleanup",`Emptiness probe failed for ${e}; treating as not proven empty`,{error:c(s(r))}),!1}}async function M(t,e,n){try{const i=(await t.send(new w({KeyId:e}),{abortSignal:l(n)})).KeyMetadata?.KeyState;if(i==="PendingDeletion"||i==="PendingReplicaDeletion")return o(void 0)}catch(r){return m(r)==="NotFoundException"?o(void 0):a(new Error(`Failed to probe CMK: ${c(s(r))}`))}try{return await t.send(new S({KeyId:e,PendingWindowInDays:p}),{abortSignal:l(n)}),o(void 0)}catch(r){return N.has(m(r))?o(void 0):a(new Error(`Failed to schedule CMK deletion: ${c(s(r))}`))}}async function O(t,e,n){const r=await D(t.cloudTrailClient,e.trailName,e.abortSignal);if(!r.success)return a(r.error);if(r.data)return o({outcome:"blocked-trail-still-exists"});const i=await h(t.s3Client,e.bucketName,e.abortSignal);if(!i.success)return a(i.error);if(i.data){if(!await K(t.s3Client,e.bucketName,e.abortSignal)&&e.acknowledgeTrailHistoryLoss!==!0)return o({outcome:"blocked-awaiting-acknowledgement",bucketName:e.bucketName});await C(t.s3Client,e.bucketName,n,e.abortSignal);try{await t.s3Client.send(new f({Bucket:e.bucketName}),{abortSignal:l(e.abortSignal)})}catch(u){if(!b(u))return a(new Error(`Failed to delete bucket ${e.bucketName}: ${c(s(u))}`))}}if(e.keyArn!==void 0&&e.keyArn!==""){const d=await M(t.kmsClient,e.keyArn,e.abortSignal);if(!d.success)return a(d.error)}return o({outcome:"decommissioned"})}export{O as decommissionMemberTrailStorage};
1
+ var y=Object.defineProperty;var s=(t,e)=>y(t,"name",{value:e,configurable:!0});import{DeleteBucketCommand as k,HeadBucketCommand as g,ListObjectVersionsCommand as w}from"@aws-sdk/client-s3";import{DescribeTrailsCommand as S}from"@aws-sdk/client-cloudtrail";import{DescribeKeyCommand as E,ScheduleKeyDeletionCommand as C}from"@aws-sdk/client-kms";import{success as o,failure as a}from"@fjall/generator";import{getErrorMessage as c,maskSensitiveOutput as l}from"@fjall/util";import{logger as p}from"@fjall/util/logger";import{composeSdkAbortSignal as d,extractErrorName as u}from"../../aws/organisations/types.js";import{emptyS3Bucket as N,isBucketGoneError as f}from"../stackCleanup.js";const D=14,h=new Set(["NotFoundException","KMSInvalidStateException"]);async function K(t,e,n){try{const r=await t.send(new S({trailNameList:[e]}),{abortSignal:d(n)});return o((r.trailList??[]).length>0)}catch(r){return u(r)==="TrailNotFoundException"?o(!1):a(new Error(`Failed to probe trail ${e}: ${l(c(r))}`))}}s(K,"trailStillExists");async function M(t,e,n){try{return await t.send(new g({Bucket:e}),{abortSignal:d(n)}),o(!0)}catch(r){return f(r)?o(!1):a(new Error(`Failed to probe bucket ${e}: ${l(c(r))}`))}}s(M,"bucketExists");async function x(t,e,n){try{const r=await t.send(new w({Bucket:e,MaxKeys:1}),{abortSignal:d(n)});return(r.Versions??[]).length===0&&(r.DeleteMarkers??[]).length===0}catch(r){return p.debug("memberTrailCleanup",`Emptiness probe failed for ${e}; treating as not proven empty`,{error:l(c(r))}),!1}}s(x,"bucketProvenEmpty");async function $(t,e,n){try{const i=(await t.send(new E({KeyId:e}),{abortSignal:d(n)})).KeyMetadata?.KeyState;if(i==="PendingDeletion"||i==="PendingReplicaDeletion")return o(void 0)}catch(r){return u(r)==="NotFoundException"?o(void 0):a(new Error(`Failed to probe CMK: ${l(c(r))}`))}try{return await t.send(new C({KeyId:e,PendingWindowInDays:D}),{abortSignal:d(n)}),o(void 0)}catch(r){return h.has(u(r))?o(void 0):a(new Error(`Failed to schedule CMK deletion: ${l(c(r))}`))}}s($,"scheduleKeyDeletion");async function L(t,e,n){const r=await K(t.cloudTrailClient,e.trailName,e.abortSignal);if(!r.success)return a(r.error);if(r.data)return o({outcome:"blocked-trail-still-exists"});const i=await M(t.s3Client,e.bucketName,e.abortSignal);if(!i.success)return a(i.error);if(i.data){if(!await x(t.s3Client,e.bucketName,e.abortSignal)&&e.acknowledgeTrailHistoryLoss!==!0)return o({outcome:"blocked-awaiting-acknowledgement",bucketName:e.bucketName});await N(t.s3Client,e.bucketName,n,e.abortSignal);try{await t.s3Client.send(new k({Bucket:e.bucketName}),{abortSignal:d(e.abortSignal)})}catch(b){if(!f(b))return a(new Error(`Failed to delete bucket ${e.bucketName}: ${l(c(b))}`))}}if(e.keyArn!==void 0&&e.keyArn!==""){const m=await $(t.kmsClient,e.keyArn,e.abortSignal);if(!m.success)return a(m.error)}return o({outcome:"decommissioned"})}s(L,"decommissionMemberTrailStorage");export{L as decommissionMemberTrailStorage};
@@ -1 +1 @@
1
- import{maskSensitiveOutput as h}from"@fjall/util";import{ACCOUNT_TRAIL_NAME as N,ORG_TRAIL_BUCKET_OUTPUT_KEY as $,TRAIL_BUCKET_OUTPUT_KEY as p,TRAIL_KEY_ARN_OUTPUT_KEY as T}from"@fjall/util/config";import{verifyOrgTrailDelivery as L}from"../../aws/cloudtrail/orgTrailDelivery.js";import{decommissionMemberTrailStorage as R}from"./memberTrailCleanup.js";function w(r,i){if(r==="account")return{action:"hold",reason:"pinned to a per-account trail"};if(r==="org")return{action:"hold",reason:"already migrated to the org trail"};if(r===void 0)return i.orgDeliveryVerified===!0?{action:"advance",to:"draining"}:{action:"hold",reason:i.orgDeliveryVerified===!1?"org-trail delivery not yet verified (first delivery can take up to 24h)":"org-trail delivery could not be determined"};switch(i.decommissionOutcome){case"decommissioned":return{action:"advance",to:"org"};case"blocked-trail-still-exists":return{action:"hold",reason:"per-account trail still exists \u2014 draining deploy not yet applied"};case"blocked-awaiting-acknowledgement":return{action:"hold",reason:"awaiting acknowledgeTrailHistoryLoss before discarding history"};case"failed":return{action:"hold",reason:"decommission attempt failed"};default:return{action:"hold",reason:"decommission was not attempted"}}}function a(r,i){r.onTrailMigrationPhase?.({...i,...i.detail!==void 0?{detail:h(i.detail)}:{}})}async function C(r,i,o){const d=[],m=i.accounts.filter(e=>e.trailLifecycle===void 0),I=i.accounts.filter(e=>e.trailLifecycle==="draining"),f=new Map;if(m.length>0){a(o,{accountId:"*",accountName:"organisation",phase:"verify-delivery",status:"started",detail:`Checking org-trail delivery for ${m.length} account(s) in s3://${i.orgTrailBucketName}`});const e=await L(r.managementS3Client,{bucketName:i.orgTrailBucketName,orgId:i.orgId,accountIds:m.map(n=>n.id),...i.recencyWindowMs!==void 0?{recencyWindowMs:i.recencyWindowMs}:{},...i.abortSignal!==void 0?{abortSignal:i.abortSignal}:{}});if(e.success){for(const n of e.data.perAccount)f.set(n.accountId,n.recentDelivery===void 0?void 0:n.delivered&&n.recentDelivery);a(o,{accountId:"*",accountName:"organisation",phase:"verify-delivery",status:"completed"})}else a(o,{accountId:"*",accountName:"organisation",phase:"verify-delivery",status:"failed",detail:e.error.message})}for(const e of m){const n={...f.get(e.id)!==void 0?{orgDeliveryVerified:f.get(e.id)}:{}},s=w(void 0,n);s.action==="advance"?(o.onTrailLifecycleChanged?.(e.id,void 0,s.to),o.onLog?.(`${e.name}: org-trail delivery verified \u2014 trail lifecycle advanced to "draining" (the next deploy removes its per-account trail)`,"info"),d.push({accountId:e.id,accountName:e.name,from:void 0,to:s.to})):d.push({accountId:e.id,accountName:e.name,from:void 0,heldReason:s.reason})}for(const e of I){const n=e.region??i.defaultRegion,s=i.cascadeOutputs.get(e.id),c=s?.[p],y=s?.[T];if(c===void 0||c===""){const t=`stack output ${p} unavailable for this run`;o.onProgress?.({type:"warning",message:`${e.name}: trail decommission skipped \u2014 ${t}. The account stays "draining"; the next deploy retries.`}),d.push({accountId:e.id,accountName:e.name,from:"draining",heldReason:t});continue}a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"started"});let l;const u=await r.getMemberClients(e,n);if(!u.success)l={decommissionOutcome:"failed"},o.onProgress?.({type:"warning",message:h(`${e.name}: trail decommission failed \u2014 ${u.error.message}. The account stays "draining"; the next deploy retries.`)}),a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"failed",detail:u.error.message});else{const t=await R(u.data,{accountId:e.id,trailName:N,bucketName:c,...y!==void 0&&y!==""?{keyArn:y}:{},acknowledgeTrailHistoryLoss:e.acknowledgeTrailHistoryLoss===!0,...i.abortSignal!==void 0?{abortSignal:i.abortSignal}:{}},o);if(!t.success)l={decommissionOutcome:"failed"},o.onProgress?.({type:"warning",message:h(`${e.name}: trail decommission failed \u2014 ${t.error.message}. The account stays "draining"; the next deploy retries.`)}),a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"failed",detail:t.error.message});else if(l={decommissionOutcome:t.data.outcome},t.data.outcome==="blocked-awaiting-acknowledgement"){const v=`${e.name}: per-account trail removed, but its log history in s3://${c} will be DISCARDED on decommission. Back the bucket up if the history matters, then set acknowledgeTrailHistoryLoss: true for account ${e.id} and redeploy.`;o.onProgress?.({type:"warning",message:v}),a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"blocked",detail:v})}else t.data.outcome==="blocked-trail-still-exists"?a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"blocked",detail:"per-account trail still exists \u2014 the draining deploy has not been applied yet"}):a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"completed",detail:`bucket s3://${c} removed; CMK scheduled for deletion`})}const g=w("draining",l);g.action==="advance"?(o.onTrailLifecycleChanged?.(e.id,"draining",g.to),o.onLog?.(`${e.name}: trail storage decommissioned \u2014 trail lifecycle advanced to "org"`,"info"),d.push({accountId:e.id,accountName:e.name,from:"draining",to:g.to})):d.push({accountId:e.id,accountName:e.name,from:"draining",heldReason:g.reason})}return d}export{$ as ORG_TRAIL_BUCKET_OUTPUT_KEY,p as TRAIL_BUCKET_OUTPUT_KEY,T as TRAIL_KEY_ARN_OUTPUT_KEY,w as decideNextTransition,C as reconcileTrailMigration};
1
+ var $=Object.defineProperty;var f=(n,i)=>$(n,"name",{value:i,configurable:!0});import{maskSensitiveOutput as p}from"@fjall/util";import{ACCOUNT_TRAIL_NAME as L,ORG_TRAIL_BUCKET_OUTPUT_KEY as R,TRAIL_BUCKET_OUTPUT_KEY as v,TRAIL_KEY_ARN_OUTPUT_KEY as w}from"@fjall/util/config";import{verifyOrgTrailDelivery as A}from"../../aws/cloudtrail/orgTrailDelivery.js";import{decommissionMemberTrailStorage as _}from"./memberTrailCleanup.js";function I(n,i){if(n==="account")return{action:"hold",reason:"pinned to a per-account trail"};if(n==="org")return{action:"hold",reason:"already migrated to the org trail"};if(n===void 0)return i.orgDeliveryVerified===!0?{action:"advance",to:"draining"}:{action:"hold",reason:i.orgDeliveryVerified===!1?"org-trail delivery not yet verified (first delivery can take up to 24h)":"org-trail delivery could not be determined"};switch(i.decommissionOutcome){case"decommissioned":return{action:"advance",to:"org"};case"blocked-trail-still-exists":return{action:"hold",reason:"per-account trail still exists \u2014 draining deploy not yet applied"};case"blocked-awaiting-acknowledgement":return{action:"hold",reason:"awaiting acknowledgeTrailHistoryLoss before discarding history"};case"failed":return{action:"hold",reason:"decommission attempt failed"};default:return{action:"hold",reason:"decommission was not attempted"}}}f(I,"decideNextTransition");function r(n,i){n.onTrailMigrationPhase?.({...i,...i.detail!==void 0?{detail:p(i.detail)}:{}})}f(r,"emitPhase");async function D(n,i,o){const d=[],m=i.accounts.filter(e=>e.trailLifecycle===void 0),N=i.accounts.filter(e=>e.trailLifecycle==="draining"),y=new Map;if(m.length>0){r(o,{accountId:"*",accountName:"organisation",phase:"verify-delivery",status:"started",detail:`Checking org-trail delivery for ${m.length} account(s) in s3://${i.orgTrailBucketName}`});const e=await A(n.managementS3Client,{bucketName:i.orgTrailBucketName,orgId:i.orgId,accountIds:m.map(t=>t.id),...i.recencyWindowMs!==void 0?{recencyWindowMs:i.recencyWindowMs}:{},...i.abortSignal!==void 0?{abortSignal:i.abortSignal}:{}});if(e.success){for(const t of e.data.perAccount)y.set(t.accountId,t.recentDelivery===void 0?void 0:t.delivered&&t.recentDelivery);r(o,{accountId:"*",accountName:"organisation",phase:"verify-delivery",status:"completed"})}else r(o,{accountId:"*",accountName:"organisation",phase:"verify-delivery",status:"failed",detail:e.error.message})}for(const e of m){const t={...y.get(e.id)!==void 0?{orgDeliveryVerified:y.get(e.id)}:{}},s=I(void 0,t);s.action==="advance"?(o.onTrailLifecycleChanged?.(e.id,void 0,s.to),o.onLog?.(`${e.name}: org-trail delivery verified \u2014 trail lifecycle advanced to "draining" (the next deploy removes its per-account trail)`,"info"),d.push({accountId:e.id,accountName:e.name,from:void 0,to:s.to})):d.push({accountId:e.id,accountName:e.name,from:void 0,heldReason:s.reason})}for(const e of N){const t=e.region??i.defaultRegion,s=i.cascadeOutputs.get(e.id),c=s?.[v],h=s?.[w];if(c===void 0||c===""){const a=`stack output ${v} unavailable for this run`;o.onProgress?.({type:"warning",message:`${e.name}: trail decommission skipped \u2014 ${a}. The account stays "draining"; the next deploy retries.`}),d.push({accountId:e.id,accountName:e.name,from:"draining",heldReason:a});continue}r(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"started"});let l;const u=await n.getMemberClients(e,t);if(!u.success)l={decommissionOutcome:"failed"},o.onProgress?.({type:"warning",message:p(`${e.name}: trail decommission failed \u2014 ${u.error.message}. The account stays "draining"; the next deploy retries.`)}),r(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"failed",detail:u.error.message});else{const a=await _(u.data,{accountId:e.id,trailName:L,bucketName:c,...h!==void 0&&h!==""?{keyArn:h}:{},acknowledgeTrailHistoryLoss:e.acknowledgeTrailHistoryLoss===!0,...i.abortSignal!==void 0?{abortSignal:i.abortSignal}:{}},o);if(!a.success)l={decommissionOutcome:"failed"},o.onProgress?.({type:"warning",message:p(`${e.name}: trail decommission failed \u2014 ${a.error.message}. The account stays "draining"; the next deploy retries.`)}),r(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"failed",detail:a.error.message});else if(l={decommissionOutcome:a.data.outcome},a.data.outcome==="blocked-awaiting-acknowledgement"){const T=`${e.name}: per-account trail removed, but its log history in s3://${c} will be DISCARDED on decommission. Back the bucket up if the history matters, then set acknowledgeTrailHistoryLoss: true for account ${e.id} and redeploy.`;o.onProgress?.({type:"warning",message:T}),r(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"blocked",detail:T})}else a.data.outcome==="blocked-trail-still-exists"?r(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"blocked",detail:"per-account trail still exists \u2014 the draining deploy has not been applied yet"}):r(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"completed",detail:`bucket s3://${c} removed; CMK scheduled for deletion`})}const g=I("draining",l);g.action==="advance"?(o.onTrailLifecycleChanged?.(e.id,"draining",g.to),o.onLog?.(`${e.name}: trail storage decommissioned \u2014 trail lifecycle advanced to "org"`,"info"),d.push({accountId:e.id,accountName:e.name,from:"draining",to:g.to})):d.push({accountId:e.id,accountName:e.name,from:"draining",heldReason:g.reason})}return d}f(D,"reconcileTrailMigration");export{R as ORG_TRAIL_BUCKET_OUTPUT_KEY,v as TRAIL_BUCKET_OUTPUT_KEY,w as TRAIL_KEY_ARN_OUTPUT_KEY,I as decideNextTransition,D as reconcileTrailMigration};
@@ -1 +1 @@
1
- import{success as A,failure as l}from"@fjall/generator";import{maskSensitiveOutput as y}from"@fjall/util";function f(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function u(e){return typeof e=="string"?[e]:Array.isArray(e)?e.filter(n=>typeof n=="string"):[]}function S(e){const{Condition:n}=e;if(!f(n))return!1;for(const[t,s]of Object.entries(n)){const c=t.toLowerCase();if(!(c!=="bool"&&c!=="boolifexists")&&f(s))for(const[o,i]of Object.entries(s)){if(o.toLowerCase()!=="aws:securetransport")continue;if((Array.isArray(i)?i:[i]).some(a=>a===!1||typeof a=="string"&&a.toLowerCase()==="false"))return!0}}return!1}function b(e){return e.Effect==="Deny"&&S(e)}function w(e){const{Condition:n}=e;return f(n)&&Object.keys(n).length>0}function h(e){return e.Action!==void 0?u(e.Action).some(n=>n==="*"||n.toLowerCase()==="s3:*"):e.NotAction!==void 0?!u(e.NotAction).map(t=>t.toLowerCase()).some(t=>t==="*"||t==="s3:*"):!1}function E(e){return h(e)?!0:u(e.Action).some(n=>n.includes("*"))}function C(e){if(e.NotPrincipal!==void 0)return!1;const{Principal:n}=e;return n===void 0||n==="*"?!0:f(n)?u(n.AWS).includes("*"):!1}function L(e){const n=u(e.Resource);return n.length===0?!0:n.some(t=>t==="*"||/^arn:[^:]*:s3:::[^/]+$/.test(t)||/^arn:[^:]*:s3:::[^/]+\/\*$/.test(t))}function v(e){return e.Effect==="Allow"?{statement:e,classification:"keep",reason:"Allow grant preserved."}:S(e)?{statement:e,classification:"keep",reason:"enforceSSL TLS-only deny preserved."}:E(e)?h(e)&&C(e)&&L(e)&&!w(e)?{statement:e,classification:"strip",reason:"Stranded quarantine deny (denies all S3, every principal, whole bucket, no condition) removed."}:{statement:e,classification:"keep-flagged",reason:"Broad deny kept; review that its principal/resource/condition scoping is intended, not a disguised lockout."}:{statement:e,classification:"keep",reason:"Scoped deny preserved."}}function g(e){return e.map(n=>typeof n.Sid=="string"&&n.Sid!==""?n.Sid:"(unnamed)").join(", ")}function N(e){const{strippedStatements:n,flaggedStatements:t,hadEnforceSsl:s,keptCount:c}=e,o=[];return o.push(n.length>0?`Stripped ${n.length} stranded quarantine deny statement(s): ${g(n)}.`:"No stranded quarantine deny statement was found in the captured policy."),s||o.push("The captured policy carried no enforceSSL TLS-only deny \u2014 the re-put must add one to keep the bucket TLS-enforced."),t.length>0&&o.push(`Flagged ${t.length} broad conditioned deny statement(s) for review: ${g(t)}.`),c===0&&o.push("No statements survived the triage \u2014 the re-put must synthesise an enforceSSL-only policy or the bucket will be left without a policy."),o}function $(e){let n;try{n=JSON.parse(e)}catch(r){const m=y(r instanceof Error?r.message:String(r));return l(new Error(`Captured bucket policy is not valid JSON: ${m}`))}if(!f(n))return l(new Error("Captured bucket policy is not a JSON object."));const t=n.Statement,s=Array.isArray(t)?t:t===void 0?[]:[t];for(const r of s){if(!f(r))return l(new Error("Captured bucket policy contains a non-object statement."));if(r.Effect!=="Allow"&&r.Effect!=="Deny")return l(new Error(`Captured bucket policy statement has an unrecognised Effect: ${y(String(r.Effect))}.`))}const c=s,o=c.map(v),i=[],d=[],a=[];for(const r of o){if(r.classification==="strip"){d.push(r.statement);continue}i.push(r.statement),r.classification==="keep-flagged"&&a.push(r.statement)}const p=c.some(b),k=i.length===0?null:{...n,Statement:i};return A({sanePolicyDocument:k,keptStatements:i,strippedStatements:d,flaggedStatements:a,hadEnforceSsl:p,classifications:o,notes:N({strippedStatements:d,flaggedStatements:a,hadEnforceSsl:p,keptCount:i.length})})}export{b as isEnforceSslStatement,$ as triageBucketPolicy};
1
+ var b=Object.defineProperty;var o=(e,n)=>b(e,"name",{value:n,configurable:!0});import{success as w,failure as p}from"@fjall/generator";import{maskSensitiveOutput as S}from"@fjall/util";function u(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}o(u,"isPlainObject");function d(e){return typeof e=="string"?[e]:Array.isArray(e)?e.filter(n=>typeof n=="string"):[]}o(d,"asStringArray");function h(e){const{Condition:n}=e;if(!u(n))return!1;for(const[t,c]of Object.entries(n)){const a=t.toLowerCase();if(!(a!=="bool"&&a!=="boolifexists")&&u(c))for(const[i,s]of Object.entries(c)){if(i.toLowerCase()!=="aws:securetransport")continue;if((Array.isArray(s)?s:[s]).some(f=>f===!1||typeof f=="string"&&f.toLowerCase()==="false"))return!0}}return!1}o(h,"hasEnforceSslCondition");function E(e){return e.Effect==="Deny"&&h(e)}o(E,"isEnforceSslStatement");function C(e){const{Condition:n}=e;return u(n)&&Object.keys(n).length>0}o(C,"hasAnyCondition");function g(e){return e.Action!==void 0?d(e.Action).some(n=>n==="*"||n.toLowerCase()==="s3:*"):e.NotAction!==void 0?!d(e.NotAction).map(t=>t.toLowerCase()).some(t=>t==="*"||t==="s3:*"):!1}o(g,"deniesAllS3Actions");function L(e){return g(e)?!0:d(e.Action).some(n=>n.includes("*"))}o(L,"looksBroadDeny");function v(e){if(e.NotPrincipal!==void 0)return!1;const{Principal:n}=e;return n===void 0||n==="*"?!0:u(n)?d(n.AWS).includes("*"):!1}o(v,"appliesToEveryone");function N(e){const n=d(e.Resource);return n.length===0?!0:n.some(t=>t==="*"||/^arn:[^:]*:s3:::[^/]+$/.test(t)||/^arn:[^:]*:s3:::[^/]+\/\*$/.test(t))}o(N,"coversWholeBucket");function j(e){return e.Effect==="Allow"?{statement:e,classification:"keep",reason:"Allow grant preserved."}:h(e)?{statement:e,classification:"keep",reason:"enforceSSL TLS-only deny preserved."}:L(e)?g(e)&&v(e)&&N(e)&&!C(e)?{statement:e,classification:"strip",reason:"Stranded quarantine deny (denies all S3, every principal, whole bucket, no condition) removed."}:{statement:e,classification:"keep-flagged",reason:"Broad deny kept; review that its principal/resource/condition scoping is intended, not a disguised lockout."}:{statement:e,classification:"keep",reason:"Scoped deny preserved."}}o(j,"classifyStatement");function k(e){return e.map(n=>typeof n.Sid=="string"&&n.Sid!==""?n.Sid:"(unnamed)").join(", ")}o(k,"describeSids");function O(e){const{strippedStatements:n,flaggedStatements:t,hadEnforceSsl:c,keptCount:a}=e,i=[];return i.push(n.length>0?`Stripped ${n.length} stranded quarantine deny statement(s): ${k(n)}.`:"No stranded quarantine deny statement was found in the captured policy."),c||i.push("The captured policy carried no enforceSSL TLS-only deny \u2014 the re-put must add one to keep the bucket TLS-enforced."),t.length>0&&i.push(`Flagged ${t.length} broad conditioned deny statement(s) for review: ${k(t)}.`),a===0&&i.push("No statements survived the triage \u2014 the re-put must synthesise an enforceSSL-only policy or the bucket will be left without a policy."),i}o(O,"buildNotes");function B(e){let n;try{n=JSON.parse(e)}catch(r){const A=S(r instanceof Error?r.message:String(r));return p(new Error(`Captured bucket policy is not valid JSON: ${A}`))}if(!u(n))return p(new Error("Captured bucket policy is not a JSON object."));const t=n.Statement,c=Array.isArray(t)?t:t===void 0?[]:[t];for(const r of c){if(!u(r))return p(new Error("Captured bucket policy contains a non-object statement."));if(r.Effect!=="Allow"&&r.Effect!=="Deny")return p(new Error(`Captured bucket policy statement has an unrecognised Effect: ${S(String(r.Effect))}.`))}const a=c,i=a.map(j),s=[],l=[],f=[];for(const r of i){if(r.classification==="strip"){l.push(r.statement);continue}s.push(r.statement),r.classification==="keep-flagged"&&f.push(r.statement)}const y=a.some(E),m=s.length===0?null:{...n,Statement:s};return w({sanePolicyDocument:m,keptStatements:s,strippedStatements:l,flaggedStatements:f,hadEnforceSsl:y,classifications:i,notes:O({strippedStatements:l,flaggedStatements:f,hadEnforceSsl:y,keptCount:s.length})})}o(B,"triageBucketPolicy");export{E as isEnforceSslStatement,B as triageBucketPolicy};
@@ -1 +1 @@
1
- import{S3Client as y}from"@aws-sdk/client-s3";import{AssumeRoleCommand as k}from"@aws-sdk/client-sts";import{success as f,failure as l}from"@fjall/generator";import{modifyResource as h,resolveConstructByLiteralProperty as A}from"@fjall/generator/codemod";import{getErrorMessage as C,maskSensitiveOutput as P}from"@fjall/util";import{composeSdkAbortSignal as b}from"../../aws/organisations/types.js";import{assumeRootForTask as w,MAX_ROOT_SESSION_SECONDS as S}from"../../aws/sts/assumeRoot.js";import{buildCascadeRoleArn as E}from"../contextHelpers.js";import{triageBucketPolicy as R}from"./bucketPolicyTriage.js";import{ensureEnforceSsl as v,restoreBucketPolicy as g,synthesiseEnforceSslDocument as D}from"./restoreBucketPolicy.js";import{formatScpSuspectedFailure as I}from"./scpRemediation.js";import{toResourcePolicyStatements as N}from"./toResourcePolicyStatements.js";import{S3_UNLOCK_POLICY_ARN as O,unlockBucket as _}from"./unlockBucket.js";const K="fjall-bucket-policy-restore";async function G(i,a){const{accountId:o,bucketName:t,region:e,abortSignal:n,onPolicyCaptured:c}=a,r=await _(i,{accountId:o,bucketName:t,region:e,abortSignal:n,...c!==void 0&&{onPolicyCaptured:c}});if(!r.success)return l(r.error);if(r.data.outcome==="no-policy")return f({unlock:"no-policy",capturedPolicy:null,triage:null,enforceSslSynthesised:!1,restore:{outcome:"skipped-empty"},reconcile:{outcome:"nothing-to-change",reason:"no-statements-to-reconcile"}});const u=r.data.capturedPolicy,s=R(u);if(!s.success)return l(s.error);const d=s.data,m=d.sanePolicyDocument===null?D(t):JSON.stringify(v(d.sanePolicyDocument,t)),p=await B(i,{accountId:o,bucketName:t,region:e,policyDocument:m,abortSignal:n});return p.success?f({unlock:"unlocked",capturedPolicy:u,triage:d,enforceSslSynthesised:!d.hadEnforceSsl,restore:{outcome:"restored",via:p.data},reconcile:F(a,d.keptStatements)}):l(p.error)}async function B(i,a){const{accountId:o,bucketName:t,region:e,policyDocument:n,abortSignal:c}=a,r=await w(i,{targetAccountId:o,taskPolicyArn:O,durationSeconds:S,abortSignal:c});if(r.success){const d=new y({region:e,credentials:r.data.credentials}),m=await g(d,{bucketName:t,policyDocument:n,abortSignal:c});if(m.success)return f("root");if(!m.error.isAccessDenied)return l(new Error(m.error.message))}const u=await T(i,o,e,c);if(!u.success)return l(u.error);const s=await g(u.data,{bucketName:t,policyDocument:n,abortSignal:c});return s.success?f("cascade"):s.error.isAccessDenied?l(new Error(I("PutBucketPolicy",{kind:"bucket",name:t},o))):l(new Error(s.error.message))}async function T(i,a,o,t){let e;try{e=(await i.send(new k({RoleArn:E(a),RoleSessionName:K,DurationSeconds:S}),{abortSignal:b(t)})).Credentials}catch(n){return l(new Error(`Failed to assume the admin role in account ${a} for the policy re-put: ${P(C(n))}`))}return e?.AccessKeyId===void 0||e.SecretAccessKey===void 0||e.SessionToken===void 0?l(new Error(`Assuming the admin role in account ${a} returned no credentials.`)):f(new y({region:o,credentials:{accessKeyId:e.AccessKeyId,secretAccessKey:e.SecretAccessKey,sessionToken:e.SessionToken}}))}function F(i,a){const{infrastructureContent:o,infrastructureFilePath:t,bucketName:e}=i;if(o===void 0)return{outcome:"nothing-to-change",reason:"no-infrastructure-content"};const{statements:n,unconvertible:c}=N(a);if(n.length===0&&c.length===0)return{outcome:"nothing-to-change",reason:"no-statements-to-reconcile"};const r=A(o,{type:"storage",property:"bucketName",value:e},t);if(!r.success)return{outcome:"report-only",reason:"unresolved-construct",managed:!1};if(!r.data.resolved)return{outcome:"report-only",reason:"unresolved-construct",managed:r.data.candidates.length>0,candidates:r.data.candidates.map(d=>d.constructId)};if(c.length>0)return{outcome:"report-only",reason:"has-unconvertible",managed:!0,unconvertibleCount:c.length};const u=r.data.construct.constructId,s=h(o,{type:"storage",name:u,properties:{resourcePolicyStatements:n},...t!==void 0&&{filePath:t}});return s.success?{outcome:"edit-ready",constructId:u,modifiedContent:s.data.content,addedStatements:n.length}:{outcome:"report-only",reason:"edit-failed",managed:!0,detail:s.error.kind}}export{G as restoreAndReconcileQuarantinedBucket};
1
+ var h=Object.defineProperty;var f=(s,o)=>h(s,"name",{value:o,configurable:!0});import{S3Client as S}from"@aws-sdk/client-s3";import{AssumeRoleCommand as A}from"@aws-sdk/client-sts";import{success as p,failure as l}from"@fjall/generator";import{modifyResource as C,resolveConstructByLiteralProperty as P}from"@fjall/generator/codemod";import{getErrorMessage as b,maskSensitiveOutput as w}from"@fjall/util";import{composeSdkAbortSignal as E}from"../../aws/organisations/types.js";import{assumeRootForTask as R,MAX_ROOT_SESSION_SECONDS as g}from"../../aws/sts/assumeRoot.js";import{buildCascadeRoleArn as v}from"../contextHelpers.js";import{triageBucketPolicy as D}from"./bucketPolicyTriage.js";import{ensureEnforceSsl as I,restoreBucketPolicy as k,synthesiseEnforceSslDocument as N}from"./restoreBucketPolicy.js";import{formatScpSuspectedFailure as O}from"./scpRemediation.js";import{toResourcePolicyStatements as _}from"./toResourcePolicyStatements.js";import{S3_UNLOCK_POLICY_ARN as K,unlockBucket as B}from"./unlockBucket.js";const T="fjall-bucket-policy-restore";async function Z(s,o){const{accountId:n,bucketName:t,region:e,abortSignal:c,onPolicyCaptured:a}=o,r=await B(s,{accountId:n,bucketName:t,region:e,abortSignal:c,...a!==void 0&&{onPolicyCaptured:a}});if(!r.success)return l(r.error);if(r.data.outcome==="no-policy")return p({unlock:"no-policy",capturedPolicy:null,triage:null,enforceSslSynthesised:!1,restore:{outcome:"skipped-empty"},reconcile:{outcome:"nothing-to-change",reason:"no-statements-to-reconcile"}});const u=r.data.capturedPolicy,i=D(u);if(!i.success)return l(i.error);const d=i.data,m=d.sanePolicyDocument===null?N(t):JSON.stringify(I(d.sanePolicyDocument,t)),y=await F(s,{accountId:n,bucketName:t,region:e,policyDocument:m,abortSignal:c});return y.success?p({unlock:"unlocked",capturedPolicy:u,triage:d,enforceSslSynthesised:!d.hadEnforceSsl,restore:{outcome:"restored",via:y.data},reconcile:$(o,d.keptStatements)}):l(y.error)}f(Z,"restoreAndReconcileQuarantinedBucket");async function F(s,o){const{accountId:n,bucketName:t,region:e,policyDocument:c,abortSignal:a}=o,r=await R(s,{targetAccountId:n,taskPolicyArn:K,durationSeconds:g,abortSignal:a});if(r.success){const d=new S({region:e,credentials:r.data.credentials}),m=await k(d,{bucketName:t,policyDocument:c,abortSignal:a});if(m.success)return p("root");if(!m.error.isAccessDenied)return l(new Error(m.error.message))}const u=await L(s,n,e,a);if(!u.success)return l(u.error);const i=await k(u.data,{bucketName:t,policyDocument:c,abortSignal:a});return i.success?p("cascade"):i.error.isAccessDenied?l(new Error(O("PutBucketPolicy",{kind:"bucket",name:t},n))):l(new Error(i.error.message))}f(F,"reputWithFallback");async function L(s,o,n,t){let e;try{e=(await s.send(new A({RoleArn:v(o),RoleSessionName:T,DurationSeconds:g}),{abortSignal:E(t)})).Credentials}catch(c){return l(new Error(`Failed to assume the admin role in account ${o} for the policy re-put: ${w(b(c))}`))}return e?.AccessKeyId===void 0||e.SecretAccessKey===void 0||e.SessionToken===void 0?l(new Error(`Assuming the admin role in account ${o} returned no credentials.`)):p(new S({region:n,credentials:{accessKeyId:e.AccessKeyId,secretAccessKey:e.SecretAccessKey,sessionToken:e.SessionToken}}))}f(L,"assumeCascadeS3Client");function $(s,o){const{infrastructureContent:n,infrastructureFilePath:t,bucketName:e}=s;if(n===void 0)return{outcome:"nothing-to-change",reason:"no-infrastructure-content"};const{statements:c,unconvertible:a}=_(o);if(c.length===0&&a.length===0)return{outcome:"nothing-to-change",reason:"no-statements-to-reconcile"};const r=P(n,{type:"storage",property:"bucketName",value:e},t);if(!r.success)return{outcome:"report-only",reason:"unresolved-construct",managed:!1};if(!r.data.resolved)return{outcome:"report-only",reason:"unresolved-construct",managed:r.data.candidates.length>0,candidates:r.data.candidates.map(d=>d.constructId)};if(a.length>0)return{outcome:"report-only",reason:"has-unconvertible",managed:!0,unconvertibleCount:a.length};const u=r.data.construct.constructId,i=C(n,{type:"storage",name:u,properties:{resourcePolicyStatements:c},...t!==void 0&&{filePath:t}});return i.success?{outcome:"edit-ready",constructId:u,modifiedContent:i.data.content,addedStatements:c.length}:{outcome:"report-only",reason:"edit-failed",managed:!0,detail:i.error.kind}}f($,"reconcileIac");export{Z as restoreAndReconcileQuarantinedBucket};