@go-to-k/cdkd 0.243.0 → 0.245.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.
- package/dist/cli.js +1141 -20
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-DNqZHDA5.js → deploy-engine-Ci_7Y2ol.js} +100 -17
- package/dist/{deploy-engine-DNqZHDA5.js.map → deploy-engine-Ci_7Y2ol.js.map} +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, f as withSkipPrefix, i as runStackBuffered, n as getLogger, o as PATTERN_B_NAME_OPTIONS, p as withStackName, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-BzO-joNR.js";
|
|
3
3
|
import { A as withErrorHandling, C as StackHasActiveImportsError, S as ResourceUpdateNotSupportedError, _ as MissingCdkCliError, a as normalizeAwsTagsToCfn, b as ProvisioningError, f as LocalInvokeBuildError$1, i as matchesCdkPath, k as normalizeAwsError, l as CdkdError, m as LocalStartServiceError, n as isTerminationProtectionPropagationError, o as resolveExplicitPhysicalId, p as LocalMigrateError, r as CDK_PATH_TAG, s as assertRegionMatch, t as disableInstanceApiTermination, v as NestedStackChildDirectDestroyError, w as StackTerminationProtectionError, x as ResourceTimeoutError, y as PartialFailureError } from "./ec2-termination-protection-q-WGWgxa.js";
|
|
4
4
|
import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-B15NAPbL.js";
|
|
5
|
-
import { $ as getDockerCmd, A as DiffCalculator, B as buildAssetRedirectMap, C as findActionableSilentDrops, Ct as resolveBucketRegion, D as refStateLookupFromResource, E as cfnRefValueFromPhysicalId, F as rebuildClientForBucketRegion, G as BOOTSTRAP_MARKER_PREFIX, H as loadPublishableAssetManifest, I as shouldRetainResource, J as parseBootstrapMarker, K as ensureAssetStorage, L as AssetPublisher, M as TemplateParser, N as LockManager, O as WAFv2WebACLProvider, P as S3StateBackend, Q as formatDockerLoginError, R as stringifyValue, S as ProviderRegistry, T as IntrinsicFunctionResolver, U as rewriteTemplateAssetReferences, V as createAssetRedirectResolver, W as AssetModeResolver, X as validateContainerRepoName, Y as validateAssetBucketName, Z as buildDockerImage, _ as green, _t as CFN_TEMPLATE_URL_LIMIT, a as withRetry, at as synthesisStatusMessage, b as IAMRoleProvider, bt as uploadCfnTemplate, c as computeImplicitDeleteEdges, ct as resolveApp, d as isStatefulRecreateTargetSync, dt as resolveSkipPrefix, et as runDockerForeground, f as renderStatefulReason, ft as resolveStateBucketWithDefault, g as gray, gt as CFN_TEMPLATE_BODY_LIMIT, h as cyan, ht as warnDeprecatedNoPrefixCliFlag, i as withResourceDeadline, it as Synthesizer, j as DagBuilder, k as applyRoleArnIfSet, l as extractDeploymentEventError, lt as resolveAutoAssetStorage, m as bold, mt as resolveUseCdkBootstrapAssets, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as AssetManifestLoader, o as isRetryableTransientError, ot as getDefaultStateBucketName, p as formatResourceLine, pt as resolveStateBucketWithDefaultAndSource, q as getBootstrapMarkerKey, r as DeployEngine, rt as getDockerImageBySourceHash, s as IMPLICIT_DELETE_DEPENDENCIES, st as getLegacyStateBucketName, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as runDockerStreaming, u as MULTI_REGION_RECREATE_BLOCKED_TYPES, ut as resolveCaptureObservedState, v as red, vt as MIGRATE_TMP_PREFIX, w as CloudControlProvider, x as collectInlinePolicyNamesManagedBySiblings, xt as AssemblyReader, y as yellow, yt as findLargeInlineResources, z as WorkGraph } from "./deploy-engine-
|
|
5
|
+
import { $ as getDockerCmd, A as DiffCalculator, B as buildAssetRedirectMap, C as findActionableSilentDrops, Ct as resolveBucketRegion, D as refStateLookupFromResource, E as cfnRefValueFromPhysicalId, F as rebuildClientForBucketRegion, G as BOOTSTRAP_MARKER_PREFIX, H as loadPublishableAssetManifest, I as shouldRetainResource, J as parseBootstrapMarker, K as ensureAssetStorage, L as AssetPublisher, M as TemplateParser, N as LockManager, O as WAFv2WebACLProvider, P as S3StateBackend, Q as formatDockerLoginError, R as stringifyValue, S as ProviderRegistry, T as IntrinsicFunctionResolver, U as rewriteTemplateAssetReferences, V as createAssetRedirectResolver, W as AssetModeResolver, X as validateContainerRepoName, Y as validateAssetBucketName, Z as buildDockerImage, _ as green, _t as CFN_TEMPLATE_URL_LIMIT, a as withRetry, at as synthesisStatusMessage, b as IAMRoleProvider, bt as uploadCfnTemplate, c as computeImplicitDeleteEdges, ct as resolveApp, d as isStatefulRecreateTargetSync, dt as resolveSkipPrefix, et as runDockerForeground, f as renderStatefulReason, ft as resolveStateBucketWithDefault, g as gray, gt as CFN_TEMPLATE_BODY_LIMIT, h as cyan, ht as warnDeprecatedNoPrefixCliFlag, i as withResourceDeadline, it as Synthesizer, j as DagBuilder, k as applyRoleArnIfSet, l as extractDeploymentEventError, lt as resolveAutoAssetStorage, m as bold, mt as resolveUseCdkBootstrapAssets, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as AssetManifestLoader, o as isRetryableTransientError, ot as getDefaultStateBucketName, p as formatResourceLine, pt as resolveStateBucketWithDefaultAndSource, q as getBootstrapMarkerKey, r as DeployEngine, rt as getDockerImageBySourceHash, s as IMPLICIT_DELETE_DEPENDENCIES, st as getLegacyStateBucketName, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as runDockerStreaming, u as MULTI_REGION_RECREATE_BLOCKED_TYPES, ut as resolveCaptureObservedState, v as red, vt as MIGRATE_TMP_PREFIX, w as CloudControlProvider, x as collectInlinePolicyNamesManagedBySiblings, xt as AssemblyReader, y as yellow, yt as findLargeInlineResources, z as WorkGraph } from "./deploy-engine-Ci_7Y2ol.js";
|
|
6
6
|
import { t as expectedOwnerParam } from "./expected-bucket-owner-BjEfUnkA.js";
|
|
7
7
|
import { t as ASGProvider } from "./asg-provider-Dbl1_mwT.js";
|
|
8
8
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
@@ -59,13 +59,15 @@ import { BudgetsClient, CreateBudgetCommand, CreateNotificationCommand, CreateSu
|
|
|
59
59
|
import { AddTagsToStreamCommand, CreateStreamCommand, DecreaseStreamRetentionPeriodCommand, DeleteStreamCommand, DeregisterStreamConsumerCommand, DescribeStreamCommand, DescribeStreamConsumerCommand, DescribeStreamSummaryCommand, IncreaseStreamRetentionPeriodCommand, KinesisClient, ListStreamsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$18, ListTagsForStreamCommand, RegisterStreamConsumerCommand, RemoveTagsFromStreamCommand, ResourceNotFoundException as ResourceNotFoundException$8, StartStreamEncryptionCommand, StopStreamEncryptionCommand, TagResourceCommand as TagResourceCommand$17, UntagResourceCommand as UntagResourceCommand$17, UpdateShardCountCommand, UpdateStreamModeCommand } from "@aws-sdk/client-kinesis";
|
|
60
60
|
import { CreateScheduleCommand, DeleteScheduleCommand, GetScheduleCommand, ResourceNotFoundException as ResourceNotFoundException$9, SchedulerClient, UpdateScheduleCommand } from "@aws-sdk/client-scheduler";
|
|
61
61
|
import { AccessPointNotFound, CreateAccessPointCommand, CreateFileSystemCommand, CreateMountTargetCommand, DeleteAccessPointCommand, DeleteFileSystemCommand, DeleteMountTargetCommand, DescribeAccessPointsCommand, DescribeBackupPolicyCommand, DescribeFileSystemPolicyCommand, DescribeFileSystemsCommand, DescribeLifecycleConfigurationCommand, DescribeMountTargetSecurityGroupsCommand, DescribeMountTargetsCommand, EFSClient, FileSystemNotFound, ModifyMountTargetSecurityGroupsCommand, MountTargetNotFound, PutBackupPolicyCommand, PutFileSystemPolicyCommand, PutLifecycleConfigurationCommand, UpdateFileSystemCommand, UpdateFileSystemProtectionCommand } from "@aws-sdk/client-efs";
|
|
62
|
+
import { CreateFileSystemCommand as CreateFileSystemCommand$1, CreateFileSystemFromBackupCommand, DeleteFileSystemCommand as DeleteFileSystemCommand$1, DescribeFileSystemsCommand as DescribeFileSystemsCommand$1, FSxClient, FileSystemNotFound as FileSystemNotFound$1, TagResourceCommand as TagResourceCommand$18, UntagResourceCommand as UntagResourceCommand$18, UpdateFileSystemCommand as UpdateFileSystemCommand$1 } from "@aws-sdk/client-fsx";
|
|
63
|
+
import { AddTagsCommand as AddTagsCommand$1, DescribeClusterCommand, EMRClient, InvalidRequestException, ModifyClusterCommand, PutAutoTerminationPolicyCommand, PutManagedScalingPolicyCommand, RemoveAutoTerminationPolicyCommand, RemoveManagedScalingPolicyCommand, RemoveTagsCommand as RemoveTagsCommand$1, RunJobFlowCommand, SetTerminationProtectionCommand, SetVisibleToAllUsersCommand, TerminateJobFlowsCommand } from "@aws-sdk/client-emr";
|
|
62
64
|
import { CreateDeliveryStreamCommand, DeleteDeliveryStreamCommand, DescribeDeliveryStreamCommand, FirehoseClient, ListDeliveryStreamsCommand, ListTagsForDeliveryStreamCommand, ResourceNotFoundException as ResourceNotFoundException$10, TagDeliveryStreamCommand, UntagDeliveryStreamCommand, UpdateDestinationCommand } from "@aws-sdk/client-firehose";
|
|
63
|
-
import { AddTagsCommand as AddTagsCommand$
|
|
65
|
+
import { AddTagsCommand as AddTagsCommand$2, CloudTrailClient, CreateTrailCommand, DeleteTrailCommand, GetEventSelectorsCommand, GetInsightSelectorsCommand, GetTrailCommand, GetTrailStatusCommand, ListTagsCommand as ListTagsCommand$1, ListTrailsCommand, PutEventSelectorsCommand, PutInsightSelectorsCommand, RemoveTagsCommand as RemoveTagsCommand$2, StartLoggingCommand, StopLoggingCommand, TrailNotFoundException, UpdateTrailCommand } from "@aws-sdk/client-cloudtrail";
|
|
64
66
|
import { BatchGetProjectsCommand, CodeBuildClient, CreateProjectCommand, DeleteProjectCommand, ListProjectsCommand, ResourceNotFoundException as ResourceNotFoundException$11, UpdateProjectCommand } from "@aws-sdk/client-codebuild";
|
|
65
|
-
import { CodeCommitClient, CreateRepositoryCommand as CreateRepositoryCommand$1, DeleteRepositoryCommand as DeleteRepositoryCommand$1, GetRepositoryCommand, ListRepositoriesCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$19, RepositoryDoesNotExistException, TagResourceCommand as TagResourceCommand$
|
|
66
|
-
import { CreateLifecyclePolicyCommand, DLMClient, DeleteLifecyclePolicyCommand as DeleteLifecyclePolicyCommand$1, GetLifecyclePoliciesCommand, GetLifecyclePolicyCommand as GetLifecyclePolicyCommand$1, ResourceNotFoundException as ResourceNotFoundException$12, TagResourceCommand as TagResourceCommand$
|
|
67
|
-
import { CreateVectorBucketCommand, DeleteIndexCommand, DeleteVectorBucketCommand, GetVectorBucketCommand, ListIndexesCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$20, ListVectorBucketsCommand, S3VectorsClient, TagResourceCommand as TagResourceCommand$
|
|
68
|
-
import { CreateNamespaceCommand, CreateTableBucketCommand, CreateTableCommand as CreateTableCommand$2, DeleteNamespaceCommand as DeleteNamespaceCommand$1, DeleteTableBucketCommand, DeleteTableCommand as DeleteTableCommand$2, GetTableBucketCommand, GetTableCommand as GetTableCommand$1, ListNamespacesCommand as ListNamespacesCommand$1, ListTableBucketsCommand, ListTablesCommand as ListTablesCommand$1, ListTagsForResourceCommand as ListTagsForResourceCommand$21, NotFoundException as NotFoundException$6, S3TablesClient, TagResourceCommand as TagResourceCommand$
|
|
67
|
+
import { CodeCommitClient, CreateRepositoryCommand as CreateRepositoryCommand$1, DeleteRepositoryCommand as DeleteRepositoryCommand$1, GetRepositoryCommand, ListRepositoriesCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$19, RepositoryDoesNotExistException, TagResourceCommand as TagResourceCommand$19, UntagResourceCommand as UntagResourceCommand$19, UpdateRepositoryDescriptionCommand, UpdateRepositoryEncryptionKeyCommand, UpdateRepositoryNameCommand } from "@aws-sdk/client-codecommit";
|
|
68
|
+
import { CreateLifecyclePolicyCommand, DLMClient, DeleteLifecyclePolicyCommand as DeleteLifecyclePolicyCommand$1, GetLifecyclePoliciesCommand, GetLifecyclePolicyCommand as GetLifecyclePolicyCommand$1, ResourceNotFoundException as ResourceNotFoundException$12, TagResourceCommand as TagResourceCommand$20, UntagResourceCommand as UntagResourceCommand$20, UpdateLifecyclePolicyCommand } from "@aws-sdk/client-dlm";
|
|
69
|
+
import { CreateVectorBucketCommand, DeleteIndexCommand, DeleteVectorBucketCommand, GetVectorBucketCommand, ListIndexesCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$20, ListVectorBucketsCommand, S3VectorsClient, TagResourceCommand as TagResourceCommand$21, UntagResourceCommand as UntagResourceCommand$21 } from "@aws-sdk/client-s3vectors";
|
|
70
|
+
import { CreateNamespaceCommand, CreateTableBucketCommand, CreateTableCommand as CreateTableCommand$2, DeleteNamespaceCommand as DeleteNamespaceCommand$1, DeleteTableBucketCommand, DeleteTableCommand as DeleteTableCommand$2, GetTableBucketCommand, GetTableCommand as GetTableCommand$1, ListNamespacesCommand as ListNamespacesCommand$1, ListTableBucketsCommand, ListTablesCommand as ListTablesCommand$1, ListTagsForResourceCommand as ListTagsForResourceCommand$21, NotFoundException as NotFoundException$6, S3TablesClient, TagResourceCommand as TagResourceCommand$22, UntagResourceCommand as UntagResourceCommand$22 } from "@aws-sdk/client-s3tables";
|
|
69
71
|
import { Document, Pair, Scalar, YAMLMap, YAMLSeq, parse as parse$1, stringify } from "yaml";
|
|
70
72
|
import { createLocalStartAgentCoreCommand, createLocalStartCloudFrontCommand, createLocalStateProvider, getEmbedConfig, isCfnFlagPresent, listTargets, rejectExplicitCfnStackWithMultipleStacks, resolveCfnFallbackRegion, setEmbedConfig, substituteAgainstState, substituteAgainstStateAsync, substituteEnvVarsFromState, substituteEnvVarsFromStateAsync } from "cdk-local";
|
|
71
73
|
import { A2A_CONTAINER_PORT, A2A_PATH, AGENTCORE_A2A_PROTOCOL, AGENTCORE_AGUI_PROTOCOL, AGENTCORE_MCP_PROTOCOL, ConnectionRegistry, EcsTaskResolutionError, HOST_GATEWAY_MIN_VERSION, LocalInvokeBuildError, MCP_CONTAINER_PORT, MCP_PATH, a2aInvokeOnce, addAlbSpecificOptions, addCommonEcsServiceOptions, addStartServiceSpecificOptions, albStrategy, architectureToPlatform, attachAuthorizers, attachStageContext, availableApiIdentifiers, bufferToBody, buildAgentCoreCodeImage, buildCognitoJwksUrl, buildConnectEvent, buildContainerImage, buildCorsConfigByApiId, buildCorsConfigFromCloudFrontChain, buildDisconnectEvent, buildJwksUrlFromIssuer, buildMessageEvent, buildMgmtEndpointEnvUrl, buildStageMap, classifySourceChange, createAuthorizerCache, createFileWatcher, createFileWatcher as createFileWatcher$1, createJwksCache, createWatchPredicates, defaultCredentialsLoader, derivePseudoParametersFromRegion, discoverRoutes, discoverWebSocketApis, downloadAndExtractS3Bundle, filterRoutesByApiIdentifier, groupRoutesByServer, handleConnectionsRequest, invokeAgentCore, invokeAgentCoreWs, materializeLayerFromArn, mcpInvokeOnce, parseConnectionsPath, parseSelectionExpressionPath, pickAgentCoreCandidateStack, pickAgentCoreCandidateStack as pickAgentCoreCandidateStack$1, probeHostGatewaySupport, readMtlsMaterialsFromDisk, resolveAgentCoreTarget, resolveEnvVars, resolveHostGatewayExtraHosts, resolveRuntimeCodeMountPath, resolveRuntimeFileExtension, resolveRuntimeImage, resolveSingleTarget, resolveWatchConfig, runEcsServiceEmulator, signAgentCoreInvocation, startApiServer, substituteImagePlaceholders, tryResolveImageFnJoin, verifyJwtViaDiscovery, waitForAgentCorePing } from "cdk-local/internal";
|
|
@@ -1888,7 +1890,7 @@ const FLUSH_INTERVAL_MS = 2e3;
|
|
|
1888
1890
|
const FLUSH_EVENT_THRESHOLD = 50;
|
|
1889
1891
|
/** Build-time cdkd version, with a dev fallback for non-built contexts. */
|
|
1890
1892
|
function getCdkdVersion() {
|
|
1891
|
-
return "0.
|
|
1893
|
+
return "0.245.0";
|
|
1892
1894
|
}
|
|
1893
1895
|
/**
|
|
1894
1896
|
* Generate a time-sortable unique run id, e.g.
|
|
@@ -34402,6 +34404,1123 @@ var EFSProvider = class {
|
|
|
34402
34404
|
}
|
|
34403
34405
|
};
|
|
34404
34406
|
|
|
34407
|
+
//#endregion
|
|
34408
|
+
//#region src/provisioning/providers/fsx-filesystem-provider.ts
|
|
34409
|
+
/**
|
|
34410
|
+
* Default polling budget for FSx lifecycle transitions. Lustre SCRATCH
|
|
34411
|
+
* creates typically finish in 5-10 minutes, but PERSISTENT creates,
|
|
34412
|
+
* storage-capacity growth and deletes can take substantially longer —
|
|
34413
|
+
* mirror the Custom Resource provider's 1-hour ceiling.
|
|
34414
|
+
*/
|
|
34415
|
+
const DEFAULT_MAX_WAIT_MS$1 = 3600 * 1e3;
|
|
34416
|
+
const DEFAULT_POLL_INTERVAL_MS$1 = 15e3;
|
|
34417
|
+
/**
|
|
34418
|
+
* Lustre sub-properties that `UpdateFileSystem` accepts (the mutable
|
|
34419
|
+
* subset of the CFn `LustreConfiguration` block). Everything else in the
|
|
34420
|
+
* block is fixed at creation — FSx has no API to change it in place.
|
|
34421
|
+
*/
|
|
34422
|
+
const LUSTRE_MUTABLE_SUBPROPS = new Set([
|
|
34423
|
+
"WeeklyMaintenanceStartTime",
|
|
34424
|
+
"DailyAutomaticBackupStartTime",
|
|
34425
|
+
"AutomaticBackupRetentionDays",
|
|
34426
|
+
"AutoImportPolicy",
|
|
34427
|
+
"DataCompressionType",
|
|
34428
|
+
"PerUnitStorageThroughput",
|
|
34429
|
+
"MetadataConfiguration",
|
|
34430
|
+
"ThroughputCapacity",
|
|
34431
|
+
"DataReadCacheConfiguration"
|
|
34432
|
+
]);
|
|
34433
|
+
/**
|
|
34434
|
+
* Top-level properties that are create-only per the CFn registry schema
|
|
34435
|
+
* (`createOnlyProperties`). A change on any of these is routed through
|
|
34436
|
+
* DELETE+CREATE by the replacement-detection layer; if a diff somehow
|
|
34437
|
+
* reaches update() with one of them changed, refuse loudly instead of
|
|
34438
|
+
* silently applying a partial update.
|
|
34439
|
+
*/
|
|
34440
|
+
const TOP_LEVEL_IMMUTABLE_PROPS = [
|
|
34441
|
+
"FileSystemType",
|
|
34442
|
+
"SubnetIds",
|
|
34443
|
+
"SecurityGroupIds",
|
|
34444
|
+
"KmsKeyId",
|
|
34445
|
+
"BackupId"
|
|
34446
|
+
];
|
|
34447
|
+
const toNumber$1 = (v) => v === void 0 ? void 0 : Number(v);
|
|
34448
|
+
const toBoolean$1 = (v) => {
|
|
34449
|
+
if (v === void 0) return void 0;
|
|
34450
|
+
if (typeof v === "string") return v === "true";
|
|
34451
|
+
return Boolean(v);
|
|
34452
|
+
};
|
|
34453
|
+
/**
|
|
34454
|
+
* SDK Provider for `AWS::FSx::FileSystem`.
|
|
34455
|
+
*
|
|
34456
|
+
* The type is `ProvisioningType: NON_PROVISIONABLE` in the CFn registry,
|
|
34457
|
+
* so cdkd's Cloud Control fallback cannot handle it (issue #1042).
|
|
34458
|
+
*
|
|
34459
|
+
* v1 scope: the **Lustre** variant (the CDK L2, `aws-fsx.LustreFileSystem`).
|
|
34460
|
+
* The `WindowsConfiguration` / `OntapConfiguration` / `OpenZFSConfiguration`
|
|
34461
|
+
* sub-trees are declared `unhandledByDesign`, so templates using them are
|
|
34462
|
+
* rejected by the property-coverage pre-flight instead of silently dropping
|
|
34463
|
+
* the whole variant config (each non-Lustre `FileSystemType` REQUIRES its
|
|
34464
|
+
* variant block, so no non-Lustre template can slip past the pre-flight).
|
|
34465
|
+
*
|
|
34466
|
+
* Lifecycle handling — every mutation is async on AWS:
|
|
34467
|
+
* - `create` → `CreateFileSystem` (or `CreateFileSystemFromBackup` when
|
|
34468
|
+
* `BackupId` is set) + poll `DescribeFileSystems` until `AVAILABLE`.
|
|
34469
|
+
* - `update` → `UpdateFileSystem` for the mutable surface (StorageCapacity,
|
|
34470
|
+
* Lustre mutable sub-props, StorageType, FileSystemTypeVersion,
|
|
34471
|
+
* NetworkType) + `TagResource`/`UntagResource` for `Tags`; polls back to
|
|
34472
|
+
* `AVAILABLE`.
|
|
34473
|
+
* - `delete` → `DeleteFileSystem` + poll until the file system is GONE
|
|
34474
|
+
* (`FileSystemNotFound` / dropped from the Describe response). A timeout
|
|
34475
|
+
* here is a hard error — a lingering FSx file system bills per hour.
|
|
34476
|
+
*
|
|
34477
|
+
* `getMinResourceTimeoutMs()` lifts the deploy engine's per-resource
|
|
34478
|
+
* deadline to the polling ceiling (mirrors `CustomResourceProvider`), so
|
|
34479
|
+
* slow FSx creates don't require a manual `--resource-timeout`.
|
|
34480
|
+
*/
|
|
34481
|
+
var FSxFileSystemProvider = class {
|
|
34482
|
+
/**
|
|
34483
|
+
* Cloud Control has NO handlers for this type (`ProvisioningType:
|
|
34484
|
+
* NON_PROVISIONABLE` in the CFn registry), so the deploy engine's #614
|
|
34485
|
+
* silent-drop auto-route MUST NOT send a Windows/ONTAP/OpenZFS template
|
|
34486
|
+
* to CC — it would fail at provisioning time with an opaque
|
|
34487
|
+
* UnsupportedActionException. With this opt-out the ProviderRegistry
|
|
34488
|
+
* rejects such templates pre-flight with a clear error instead.
|
|
34489
|
+
* (The runtime Tier 3 set cannot express this: it excludes SDK-covered
|
|
34490
|
+
* types by design, so `isNonProvisionable()` is false for this type.)
|
|
34491
|
+
*/
|
|
34492
|
+
disableCcApiFallback = true;
|
|
34493
|
+
client;
|
|
34494
|
+
providerRegion = process.env["AWS_REGION"];
|
|
34495
|
+
logger = getLogger().child("FSxFileSystemProvider");
|
|
34496
|
+
pollIntervalMs;
|
|
34497
|
+
maxWaitMs;
|
|
34498
|
+
constructor(options) {
|
|
34499
|
+
this.pollIntervalMs = options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS$1;
|
|
34500
|
+
this.maxWaitMs = options?.maxWaitMs ?? DEFAULT_MAX_WAIT_MS$1;
|
|
34501
|
+
}
|
|
34502
|
+
handledProperties = new Map([["AWS::FSx::FileSystem", new Set([
|
|
34503
|
+
"BackupId",
|
|
34504
|
+
"FileSystemType",
|
|
34505
|
+
"FileSystemTypeVersion",
|
|
34506
|
+
"KmsKeyId",
|
|
34507
|
+
"LustreConfiguration",
|
|
34508
|
+
"NetworkType",
|
|
34509
|
+
"SecurityGroupIds",
|
|
34510
|
+
"StorageCapacity",
|
|
34511
|
+
"StorageType",
|
|
34512
|
+
"SubnetIds",
|
|
34513
|
+
"Tags"
|
|
34514
|
+
])]]);
|
|
34515
|
+
unhandledByDesign = new Map([["AWS::FSx::FileSystem", new Map([
|
|
34516
|
+
["WindowsConfiguration", "FSx for Windows File Server variant — requires Active Directory wiring and Windows-specific update/delete semantics (final backups, throughput tiers) that v1 does not implement; only the Lustre variant (the CDK L2) is supported. Follow-up to issue #1042."],
|
|
34517
|
+
["OntapConfiguration", "FSx for NetApp ONTAP variant — the file system is only a container for SVMs/volumes (separate AWS::FSx::StorageVirtualMachine / AWS::FSx::Volume types, both still unsupported), so shipping it alone would be misleading; only the Lustre variant is supported in v1. Follow-up to issue #1042."],
|
|
34518
|
+
["OpenZFSConfiguration", "FSx for OpenZFS variant — root-volume semantics (RootVolumeId, child AWS::FSx::Volume trees) are not implemented in v1; only the Lustre variant is supported. Follow-up to issue #1042."]
|
|
34519
|
+
])]]);
|
|
34520
|
+
getClient() {
|
|
34521
|
+
if (!this.client) this.client = new FSxClient(this.providerRegion ? { region: this.providerRegion } : {});
|
|
34522
|
+
return this.client;
|
|
34523
|
+
}
|
|
34524
|
+
/**
|
|
34525
|
+
* Self-reported minimum per-resource timeout: the deploy engine resolves
|
|
34526
|
+
* `max(getMinResourceTimeoutMs(), globalCliDefault)` so FSx's slow
|
|
34527
|
+
* create/delete polling fits inside the resource deadline without the
|
|
34528
|
+
* user passing `--resource-timeout`.
|
|
34529
|
+
*/
|
|
34530
|
+
getMinResourceTimeoutMs() {
|
|
34531
|
+
return this.maxWaitMs;
|
|
34532
|
+
}
|
|
34533
|
+
async create(logicalId, resourceType, properties) {
|
|
34534
|
+
if (resourceType !== "AWS::FSx::FileSystem") throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId);
|
|
34535
|
+
const backupId = properties["BackupId"];
|
|
34536
|
+
const fileSystemType = properties["FileSystemType"];
|
|
34537
|
+
if (backupId === void 0 && fileSystemType !== "LUSTRE") throw new ProvisioningError(`AWS::FSx::FileSystem: FileSystemType '${fileSystemType ?? "(unset)"}' is not supported by cdkd yet — only the LUSTRE variant is implemented (issue #1042). Windows / ONTAP / OpenZFS file systems can follow in a later PR.`, resourceType, logicalId);
|
|
34538
|
+
this.logger.debug(`Creating FSx FileSystem ${logicalId}`);
|
|
34539
|
+
const tokenHash = createHash("sha256").update([
|
|
34540
|
+
properties["FileSystemType"],
|
|
34541
|
+
properties["SubnetIds"],
|
|
34542
|
+
properties["SecurityGroupIds"],
|
|
34543
|
+
properties["KmsKeyId"],
|
|
34544
|
+
properties["BackupId"]
|
|
34545
|
+
].map((v) => JSON.stringify(v ?? null)).join(" ")).digest("hex").slice(0, 12);
|
|
34546
|
+
const clientRequestToken = `cdkd-${logicalId.slice(0, 45)}-${tokenHash}`;
|
|
34547
|
+
const tags = properties["Tags"];
|
|
34548
|
+
const common = {
|
|
34549
|
+
ClientRequestToken: clientRequestToken,
|
|
34550
|
+
SubnetIds: properties["SubnetIds"],
|
|
34551
|
+
SecurityGroupIds: properties["SecurityGroupIds"],
|
|
34552
|
+
KmsKeyId: properties["KmsKeyId"],
|
|
34553
|
+
StorageType: properties["StorageType"],
|
|
34554
|
+
StorageCapacity: toNumber$1(properties["StorageCapacity"]),
|
|
34555
|
+
FileSystemTypeVersion: properties["FileSystemTypeVersion"],
|
|
34556
|
+
NetworkType: properties["NetworkType"],
|
|
34557
|
+
LustreConfiguration: this.toCreateLustreConfiguration(properties["LustreConfiguration"]),
|
|
34558
|
+
Tags: tags?.map((t) => ({
|
|
34559
|
+
Key: t.Key,
|
|
34560
|
+
Value: t.Value
|
|
34561
|
+
}))
|
|
34562
|
+
};
|
|
34563
|
+
let fileSystemId;
|
|
34564
|
+
try {
|
|
34565
|
+
let created;
|
|
34566
|
+
if (backupId !== void 0) created = (await this.getClient().send(new CreateFileSystemFromBackupCommand({
|
|
34567
|
+
...common,
|
|
34568
|
+
BackupId: backupId
|
|
34569
|
+
}))).FileSystem;
|
|
34570
|
+
else created = (await this.getClient().send(new CreateFileSystemCommand$1({
|
|
34571
|
+
...common,
|
|
34572
|
+
FileSystemType: fileSystemType
|
|
34573
|
+
}))).FileSystem;
|
|
34574
|
+
fileSystemId = created?.FileSystemId;
|
|
34575
|
+
if (!fileSystemId) throw new ProvisioningError(`FSx CreateFileSystem for ${logicalId} returned no FileSystemId`, resourceType, logicalId);
|
|
34576
|
+
const fs = await this.waitForFileSystemAvailable(fileSystemId, logicalId, resourceType);
|
|
34577
|
+
this.logger.debug(`Successfully created FSx FileSystem ${logicalId}: ${fileSystemId}`);
|
|
34578
|
+
return {
|
|
34579
|
+
physicalId: fileSystemId,
|
|
34580
|
+
attributes: this.buildAttributes(fs)
|
|
34581
|
+
};
|
|
34582
|
+
} catch (error) {
|
|
34583
|
+
if (fileSystemId !== void 0) try {
|
|
34584
|
+
await this.getClient().send(new DeleteFileSystemCommand$1({ FileSystemId: fileSystemId }));
|
|
34585
|
+
this.logger.warn(`Rolled back partially-created FSx FileSystem ${fileSystemId}`);
|
|
34586
|
+
} catch (cleanupError) {
|
|
34587
|
+
this.logger.warn(`Failed to roll back partially-created FSx FileSystem ${fileSystemId}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)} — delete it manually to stop billing`);
|
|
34588
|
+
}
|
|
34589
|
+
if (error instanceof ProvisioningError) throw error;
|
|
34590
|
+
const cause = error instanceof Error ? error : void 0;
|
|
34591
|
+
throw new ProvisioningError(`Failed to create FSx FileSystem ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
|
|
34592
|
+
}
|
|
34593
|
+
}
|
|
34594
|
+
/**
|
|
34595
|
+
* Map the CFn `LustreConfiguration` block to the SDK create shape. The
|
|
34596
|
+
* field names are identical (both PascalCase); numeric / boolean fields
|
|
34597
|
+
* are coerced defensively because template values can arrive as strings.
|
|
34598
|
+
*/
|
|
34599
|
+
toCreateLustreConfiguration(config) {
|
|
34600
|
+
if (!config) return void 0;
|
|
34601
|
+
const metadata = config["MetadataConfiguration"];
|
|
34602
|
+
const readCache = config["DataReadCacheConfiguration"];
|
|
34603
|
+
return {
|
|
34604
|
+
WeeklyMaintenanceStartTime: config["WeeklyMaintenanceStartTime"],
|
|
34605
|
+
ImportPath: config["ImportPath"],
|
|
34606
|
+
ExportPath: config["ExportPath"],
|
|
34607
|
+
ImportedFileChunkSize: toNumber$1(config["ImportedFileChunkSize"]),
|
|
34608
|
+
DeploymentType: config["DeploymentType"],
|
|
34609
|
+
AutoImportPolicy: config["AutoImportPolicy"],
|
|
34610
|
+
PerUnitStorageThroughput: toNumber$1(config["PerUnitStorageThroughput"]),
|
|
34611
|
+
DailyAutomaticBackupStartTime: config["DailyAutomaticBackupStartTime"],
|
|
34612
|
+
AutomaticBackupRetentionDays: toNumber$1(config["AutomaticBackupRetentionDays"]),
|
|
34613
|
+
CopyTagsToBackups: toBoolean$1(config["CopyTagsToBackups"]),
|
|
34614
|
+
DriveCacheType: config["DriveCacheType"],
|
|
34615
|
+
DataCompressionType: config["DataCompressionType"],
|
|
34616
|
+
EfaEnabled: toBoolean$1(config["EfaEnabled"]),
|
|
34617
|
+
ThroughputCapacity: toNumber$1(config["ThroughputCapacity"]),
|
|
34618
|
+
MetadataConfiguration: metadata ? {
|
|
34619
|
+
Mode: metadata["Mode"],
|
|
34620
|
+
Iops: toNumber$1(metadata["Iops"])
|
|
34621
|
+
} : void 0,
|
|
34622
|
+
DataReadCacheConfiguration: readCache ? {
|
|
34623
|
+
SizingMode: readCache["SizingMode"],
|
|
34624
|
+
SizeGiB: toNumber$1(readCache["SizeGiB"])
|
|
34625
|
+
} : void 0
|
|
34626
|
+
};
|
|
34627
|
+
}
|
|
34628
|
+
async update(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
34629
|
+
const changed = (key) => JSON.stringify(properties[key]) !== JSON.stringify(previousProperties[key]);
|
|
34630
|
+
for (const key of TOP_LEVEL_IMMUTABLE_PROPS) {
|
|
34631
|
+
const next = properties[key];
|
|
34632
|
+
const prev = previousProperties[key];
|
|
34633
|
+
if (JSON.stringify(next ?? null) !== JSON.stringify(prev ?? null)) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS FSx FileSystem ${key} is immutable on AWS — UpdateFileSystem does not accept ${key}; the property is fixed at creation. Re-deploy with cdkd deploy --replace, or destroy + redeploy the stack.`);
|
|
34634
|
+
}
|
|
34635
|
+
const nextLustre = properties["LustreConfiguration"] ?? {};
|
|
34636
|
+
const prevLustre = previousProperties["LustreConfiguration"] ?? {};
|
|
34637
|
+
const lustreKeys = new Set([...Object.keys(nextLustre), ...Object.keys(prevLustre)]);
|
|
34638
|
+
const lustreMutableDiff = {};
|
|
34639
|
+
let lustreHasMutableDiff = false;
|
|
34640
|
+
for (const key of lustreKeys) {
|
|
34641
|
+
const next = nextLustre[key];
|
|
34642
|
+
const prev = prevLustre[key];
|
|
34643
|
+
if (JSON.stringify(next) === JSON.stringify(prev)) continue;
|
|
34644
|
+
if (!LUSTRE_MUTABLE_SUBPROPS.has(key)) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS FSx FileSystem LustreConfiguration.${key} is immutable on AWS — UpdateFileSystem cannot change it after creation. Re-deploy with cdkd deploy --replace, or destroy + redeploy the stack.`);
|
|
34645
|
+
lustreHasMutableDiff = true;
|
|
34646
|
+
switch (key) {
|
|
34647
|
+
case "WeeklyMaintenanceStartTime":
|
|
34648
|
+
case "DailyAutomaticBackupStartTime":
|
|
34649
|
+
lustreMutableDiff[key] = next;
|
|
34650
|
+
break;
|
|
34651
|
+
case "AutomaticBackupRetentionDays":
|
|
34652
|
+
case "PerUnitStorageThroughput":
|
|
34653
|
+
case "ThroughputCapacity":
|
|
34654
|
+
lustreMutableDiff[key] = toNumber$1(next);
|
|
34655
|
+
break;
|
|
34656
|
+
case "AutoImportPolicy":
|
|
34657
|
+
case "DataCompressionType":
|
|
34658
|
+
lustreMutableDiff[key] = next;
|
|
34659
|
+
break;
|
|
34660
|
+
case "MetadataConfiguration": {
|
|
34661
|
+
const metadata = next;
|
|
34662
|
+
lustreMutableDiff.MetadataConfiguration = metadata ? {
|
|
34663
|
+
Mode: metadata["Mode"],
|
|
34664
|
+
Iops: toNumber$1(metadata["Iops"])
|
|
34665
|
+
} : void 0;
|
|
34666
|
+
break;
|
|
34667
|
+
}
|
|
34668
|
+
case "DataReadCacheConfiguration": {
|
|
34669
|
+
const readCache = next;
|
|
34670
|
+
lustreMutableDiff.DataReadCacheConfiguration = readCache ? {
|
|
34671
|
+
SizingMode: readCache["SizingMode"],
|
|
34672
|
+
SizeGiB: toNumber$1(readCache["SizeGiB"])
|
|
34673
|
+
} : void 0;
|
|
34674
|
+
break;
|
|
34675
|
+
}
|
|
34676
|
+
}
|
|
34677
|
+
}
|
|
34678
|
+
const storageCapacityChanged = changed("StorageCapacity");
|
|
34679
|
+
const storageTypeChanged = changed("StorageType");
|
|
34680
|
+
const typeVersionChanged = changed("FileSystemTypeVersion");
|
|
34681
|
+
const networkTypeChanged = changed("NetworkType");
|
|
34682
|
+
const tagsChanged = changed("Tags");
|
|
34683
|
+
if (!lustreHasMutableDiff && !storageCapacityChanged && !storageTypeChanged && !typeVersionChanged && !networkTypeChanged && !tagsChanged) {
|
|
34684
|
+
this.logger.debug(`No mutable diff for FSx FileSystem ${logicalId}, skipping update`);
|
|
34685
|
+
return {
|
|
34686
|
+
physicalId,
|
|
34687
|
+
wasReplaced: false
|
|
34688
|
+
};
|
|
34689
|
+
}
|
|
34690
|
+
this.logger.debug(`Updating FSx FileSystem ${logicalId}: ${physicalId}`);
|
|
34691
|
+
try {
|
|
34692
|
+
const needsUpdateCall = lustreHasMutableDiff || storageCapacityChanged || storageTypeChanged || typeVersionChanged || networkTypeChanged;
|
|
34693
|
+
let updatedFs;
|
|
34694
|
+
if (needsUpdateCall) {
|
|
34695
|
+
const callTimeMs = Date.now();
|
|
34696
|
+
const respPendingActions = ((await this.getClient().send(new UpdateFileSystemCommand$1({
|
|
34697
|
+
FileSystemId: physicalId,
|
|
34698
|
+
...storageCapacityChanged && { StorageCapacity: toNumber$1(properties["StorageCapacity"]) },
|
|
34699
|
+
...storageTypeChanged && { StorageType: properties["StorageType"] },
|
|
34700
|
+
...typeVersionChanged && { FileSystemTypeVersion: properties["FileSystemTypeVersion"] },
|
|
34701
|
+
...networkTypeChanged && { NetworkType: properties["NetworkType"] },
|
|
34702
|
+
...lustreHasMutableDiff && { LustreConfiguration: lustreMutableDiff }
|
|
34703
|
+
}))).FileSystem?.AdministrativeActions ?? []).filter((a) => a.AdministrativeActionType === "FILE_SYSTEM_UPDATE" && (a.Status === "PENDING" || a.Status === "IN_PROGRESS"));
|
|
34704
|
+
const requestTimes = respPendingActions.map((a) => a.RequestTime?.getTime()).filter((t) => t !== void 0);
|
|
34705
|
+
const actionThresholdMs = requestTimes.length > 0 ? Math.min(...requestTimes) : callTimeMs - 6e4;
|
|
34706
|
+
updatedFs = await this.waitForUpdateActionComplete(physicalId, logicalId, resourceType, actionThresholdMs, respPendingActions.length > 0);
|
|
34707
|
+
}
|
|
34708
|
+
if (tagsChanged) await this.applyTagDiff(physicalId, properties["Tags"], previousProperties["Tags"]);
|
|
34709
|
+
if (!updatedFs) try {
|
|
34710
|
+
updatedFs = (await this.getClient().send(new DescribeFileSystemsCommand$1({ FileSystemIds: [physicalId] }))).FileSystems?.[0];
|
|
34711
|
+
} catch (describeError) {
|
|
34712
|
+
this.logger.debug(`Post-update attribute refresh for ${physicalId} failed (returning without attributes): ${describeError instanceof Error ? describeError.message : String(describeError)}`);
|
|
34713
|
+
}
|
|
34714
|
+
this.logger.debug(`Successfully updated FSx FileSystem ${logicalId}`);
|
|
34715
|
+
return {
|
|
34716
|
+
physicalId,
|
|
34717
|
+
wasReplaced: false,
|
|
34718
|
+
...updatedFs && { attributes: this.buildAttributes(updatedFs) }
|
|
34719
|
+
};
|
|
34720
|
+
} catch (error) {
|
|
34721
|
+
if (error instanceof ProvisioningError) throw error;
|
|
34722
|
+
const cause = error instanceof Error ? error : void 0;
|
|
34723
|
+
throw new ProvisioningError(`Failed to update FSx FileSystem ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
|
|
34724
|
+
}
|
|
34725
|
+
}
|
|
34726
|
+
/**
|
|
34727
|
+
* Apply a `Tags` diff via `TagResource` / `UntagResource` (the FSx
|
|
34728
|
+
* `UpdateFileSystem` API does not accept tags). Needs the ARN — resolve
|
|
34729
|
+
* it from `DescribeFileSystems`.
|
|
34730
|
+
*/
|
|
34731
|
+
async applyTagDiff(physicalId, nextTags, prevTags) {
|
|
34732
|
+
const arn = (await this.getClient().send(new DescribeFileSystemsCommand$1({ FileSystemIds: [physicalId] }))).FileSystems?.[0]?.ResourceARN;
|
|
34733
|
+
if (!arn) throw new Error(`could not resolve ResourceARN for FSx FileSystem ${physicalId}`);
|
|
34734
|
+
const next = new Map((nextTags ?? []).map((t) => [t.Key, t.Value]));
|
|
34735
|
+
const prev = new Map((prevTags ?? []).map((t) => [t.Key, t.Value]));
|
|
34736
|
+
const toSet = [];
|
|
34737
|
+
for (const [key, value] of next) {
|
|
34738
|
+
if (key === void 0) continue;
|
|
34739
|
+
if (prev.get(key) !== value) toSet.push({
|
|
34740
|
+
Key: key,
|
|
34741
|
+
Value: value
|
|
34742
|
+
});
|
|
34743
|
+
}
|
|
34744
|
+
const toRemove = [];
|
|
34745
|
+
for (const key of prev.keys()) if (key !== void 0 && !next.has(key)) toRemove.push(key);
|
|
34746
|
+
if (toSet.length > 0) await this.getClient().send(new TagResourceCommand$18({
|
|
34747
|
+
ResourceARN: arn,
|
|
34748
|
+
Tags: toSet
|
|
34749
|
+
}));
|
|
34750
|
+
if (toRemove.length > 0) await this.getClient().send(new UntagResourceCommand$18({
|
|
34751
|
+
ResourceARN: arn,
|
|
34752
|
+
TagKeys: toRemove
|
|
34753
|
+
}));
|
|
34754
|
+
}
|
|
34755
|
+
async delete(logicalId, physicalId, resourceType, _properties, context) {
|
|
34756
|
+
this.logger.debug(`Deleting FSx FileSystem ${logicalId}: ${physicalId}`);
|
|
34757
|
+
try {
|
|
34758
|
+
await this.getClient().send(new DeleteFileSystemCommand$1({ FileSystemId: physicalId }));
|
|
34759
|
+
} catch (error) {
|
|
34760
|
+
if (error instanceof FileSystemNotFound$1) {
|
|
34761
|
+
assertRegionMatch(await this.getClient().config.region(), context?.expectedRegion, resourceType, logicalId, physicalId);
|
|
34762
|
+
this.logger.debug(`FSx FileSystem ${physicalId} does not exist, skipping deletion`);
|
|
34763
|
+
return;
|
|
34764
|
+
}
|
|
34765
|
+
const cause = error instanceof Error ? error : void 0;
|
|
34766
|
+
throw new ProvisioningError(`Failed to delete FSx FileSystem ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
|
|
34767
|
+
}
|
|
34768
|
+
await this.waitForFileSystemDeleted(physicalId, logicalId, resourceType);
|
|
34769
|
+
this.logger.debug(`Successfully deleted FSx FileSystem ${logicalId}`);
|
|
34770
|
+
}
|
|
34771
|
+
/**
|
|
34772
|
+
* Issue the polling `DescribeFileSystems` with bounded tolerance for
|
|
34773
|
+
* TRANSIENT errors (throttling / 5xx / connection resets): up to
|
|
34774
|
+
* `maxConsecutiveTransient` consecutive failures are absorbed (with the
|
|
34775
|
+
* normal poll delay) before the error propagates. A 5-10 minute create
|
|
34776
|
+
* poll at 15s intervals would otherwise turn a single throttle into a
|
|
34777
|
+
* spurious failure + best-effort rollback cycle. Non-transient errors
|
|
34778
|
+
* (incl. FileSystemNotFound) propagate immediately.
|
|
34779
|
+
*/
|
|
34780
|
+
async describeForPoll(fileSystemId, transientState, maxConsecutiveTransient = 5) {
|
|
34781
|
+
try {
|
|
34782
|
+
const response = await this.getClient().send(new DescribeFileSystemsCommand$1({ FileSystemIds: [fileSystemId] }));
|
|
34783
|
+
transientState.count = 0;
|
|
34784
|
+
return response.FileSystems?.[0];
|
|
34785
|
+
} catch (error) {
|
|
34786
|
+
const name = error instanceof Error ? error.name : "";
|
|
34787
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
34788
|
+
if ((name === "ThrottlingException" || name === "ServiceLimitExceeded" || name === "InternalServerError" || name === "TimeoutError" || /rate exceeded|too many requests|timed? ?out|ECONNRESET|EPIPE|socket hang up/i.test(msg)) && transientState.count < maxConsecutiveTransient) {
|
|
34789
|
+
transientState.count += 1;
|
|
34790
|
+
this.logger.debug(`Transient DescribeFileSystems error while polling ${fileSystemId} (${transientState.count}/${maxConsecutiveTransient}): ${msg} — retrying`);
|
|
34791
|
+
return;
|
|
34792
|
+
}
|
|
34793
|
+
throw error;
|
|
34794
|
+
}
|
|
34795
|
+
}
|
|
34796
|
+
async waitForFileSystemAvailable(fileSystemId, logicalId, resourceType) {
|
|
34797
|
+
const startTime = Date.now();
|
|
34798
|
+
const transientState = { count: 0 };
|
|
34799
|
+
while (Date.now() - startTime < this.maxWaitMs) {
|
|
34800
|
+
const fs = await this.describeForPoll(fileSystemId, transientState);
|
|
34801
|
+
const lifecycle = fs?.Lifecycle;
|
|
34802
|
+
if (fs && lifecycle === "AVAILABLE") return fs;
|
|
34803
|
+
if (lifecycle === "FAILED" || lifecycle === "MISCONFIGURED") throw new ProvisioningError(`FSx FileSystem ${fileSystemId} entered lifecycle state ${lifecycle}: ${fs?.FailureDetails?.Message ?? "no failure details reported"}`, resourceType, logicalId, fileSystemId);
|
|
34804
|
+
this.logger.debug(`FSx FileSystem ${fileSystemId} state: ${lifecycle ?? "unknown"}, waiting...`);
|
|
34805
|
+
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
34806
|
+
}
|
|
34807
|
+
throw new ProvisioningError(`Timed out waiting for FSx FileSystem ${fileSystemId} to become AVAILABLE (${Math.round(this.maxWaitMs / 6e4)} min)`, resourceType, logicalId, fileSystemId);
|
|
34808
|
+
}
|
|
34809
|
+
/**
|
|
34810
|
+
* Wait until the async `FILE_SYSTEM_UPDATE` administrative action kicked
|
|
34811
|
+
* off by `UpdateFileSystem` completes (and the Lifecycle is AVAILABLE).
|
|
34812
|
+
* `STORAGE_OPTIMIZATION` actions are intentionally ignored — they can run
|
|
34813
|
+
* for hours after a capacity grow, and CloudFormation does not wait for
|
|
34814
|
+
* them either (`UPDATED_OPTIMIZING` counts as done). A FAILED update
|
|
34815
|
+
* action is a hard error carrying the action's failure message.
|
|
34816
|
+
*
|
|
34817
|
+
* `actionThresholdMs` scopes the wait to THIS update's actions: FSx keeps
|
|
34818
|
+
* terminal (COMPLETED / FAILED) actions in the history, so only actions
|
|
34819
|
+
* whose `RequestTime` is at or after the threshold are considered — a
|
|
34820
|
+
* PAST failed update must not fail this one's retry. An action without a
|
|
34821
|
+
* `RequestTime` is conservatively tracked.
|
|
34822
|
+
*
|
|
34823
|
+
* `requireActionObservation` guards the eventual-consistency race: when
|
|
34824
|
+
* the UpdateFileSystem response confirmed a newly-created action, the
|
|
34825
|
+
* wait refuses to return until an in-threshold action has been observed
|
|
34826
|
+
* in at least one Describe response — a lagging read replica could
|
|
34827
|
+
* otherwise briefly show none and cause a premature return with the
|
|
34828
|
+
* pre-update config. Terminal actions stay in the history, so the
|
|
34829
|
+
* observation is guaranteed to eventually succeed (bounded by
|
|
34830
|
+
* `maxWaitMs`).
|
|
34831
|
+
*/
|
|
34832
|
+
async waitForUpdateActionComplete(fileSystemId, logicalId, resourceType, actionThresholdMs, requireActionObservation) {
|
|
34833
|
+
const startTime = Date.now();
|
|
34834
|
+
const transientState = { count: 0 };
|
|
34835
|
+
let seenTrackedAction = false;
|
|
34836
|
+
while (Date.now() - startTime < this.maxWaitMs) {
|
|
34837
|
+
const fs = await this.describeForPoll(fileSystemId, transientState);
|
|
34838
|
+
if (fs) {
|
|
34839
|
+
const updateActions = (fs.AdministrativeActions ?? []).filter((a) => a.AdministrativeActionType === "FILE_SYSTEM_UPDATE" && (a.RequestTime === void 0 || a.RequestTime.getTime() >= actionThresholdMs));
|
|
34840
|
+
if (updateActions.length > 0) seenTrackedAction = true;
|
|
34841
|
+
const failed = updateActions.find((a) => a.Status === "FAILED");
|
|
34842
|
+
if (failed) throw new ProvisioningError(`FSx FileSystem ${fileSystemId} update failed (FILE_SYSTEM_UPDATE administrative action): ${failed.FailureDetails?.Message ?? "no failure details reported"}`, resourceType, logicalId, fileSystemId);
|
|
34843
|
+
if (!updateActions.some((a) => a.Status === "PENDING" || a.Status === "IN_PROGRESS") && (!requireActionObservation || seenTrackedAction) && fs.Lifecycle === "AVAILABLE") return fs;
|
|
34844
|
+
this.logger.debug(`FSx FileSystem ${fileSystemId} update in progress (lifecycle: ${fs.Lifecycle ?? "unknown"}), waiting...`);
|
|
34845
|
+
}
|
|
34846
|
+
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
34847
|
+
}
|
|
34848
|
+
throw new ProvisioningError(`Timed out waiting for FSx FileSystem ${fileSystemId} update to complete (${Math.round(this.maxWaitMs / 6e4)} min)`, resourceType, logicalId, fileSystemId);
|
|
34849
|
+
}
|
|
34850
|
+
async waitForFileSystemDeleted(fileSystemId, logicalId, resourceType) {
|
|
34851
|
+
const startTime = Date.now();
|
|
34852
|
+
const transientState = { count: 0 };
|
|
34853
|
+
while (Date.now() - startTime < this.maxWaitMs) {
|
|
34854
|
+
let fs;
|
|
34855
|
+
let sawResponse = true;
|
|
34856
|
+
try {
|
|
34857
|
+
fs = await this.describeForPoll(fileSystemId, transientState);
|
|
34858
|
+
if (fs === void 0 && transientState.count > 0) sawResponse = false;
|
|
34859
|
+
} catch (error) {
|
|
34860
|
+
if (error instanceof FileSystemNotFound$1) return;
|
|
34861
|
+
const cause = error instanceof Error ? error : void 0;
|
|
34862
|
+
throw new ProvisioningError(`Failed to poll FSx FileSystem ${fileSystemId} deletion: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, fileSystemId, cause);
|
|
34863
|
+
}
|
|
34864
|
+
if (!fs && sawResponse) return;
|
|
34865
|
+
if (!fs) {
|
|
34866
|
+
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
34867
|
+
continue;
|
|
34868
|
+
}
|
|
34869
|
+
if (fs.Lifecycle === "FAILED") throw new ProvisioningError(`FSx FileSystem ${fileSystemId} entered lifecycle state FAILED during deletion: ${fs.FailureDetails?.Message ?? "no failure details reported"}`, resourceType, logicalId, fileSystemId);
|
|
34870
|
+
this.logger.debug(`FSx FileSystem ${fileSystemId} state: ${fs.Lifecycle ?? "unknown"}, waiting for deletion...`);
|
|
34871
|
+
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
34872
|
+
}
|
|
34873
|
+
throw new ProvisioningError(`Timed out waiting for FSx FileSystem ${fileSystemId} deletion (${Math.round(this.maxWaitMs / 6e4)} min) — verify and delete it manually to stop billing`, resourceType, logicalId, fileSystemId);
|
|
34874
|
+
}
|
|
34875
|
+
buildAttributes(fs) {
|
|
34876
|
+
const attributes = {};
|
|
34877
|
+
if (fs.ResourceARN !== void 0) attributes["ResourceARN"] = fs.ResourceARN;
|
|
34878
|
+
if (fs.DNSName !== void 0) attributes["DNSName"] = fs.DNSName;
|
|
34879
|
+
if (fs.LustreConfiguration?.MountName !== void 0) attributes["LustreMountName"] = fs.LustreConfiguration.MountName;
|
|
34880
|
+
if (fs.FileSystemId !== void 0) attributes["FileSystemId"] = fs.FileSystemId;
|
|
34881
|
+
return attributes;
|
|
34882
|
+
}
|
|
34883
|
+
async getAttribute(physicalId, _resourceType, attributeName) {
|
|
34884
|
+
if (attributeName === "Id" || attributeName === "FileSystemId") return physicalId;
|
|
34885
|
+
const fs = (await this.getClient().send(new DescribeFileSystemsCommand$1({ FileSystemIds: [physicalId] }))).FileSystems?.[0];
|
|
34886
|
+
if (!fs) return void 0;
|
|
34887
|
+
switch (attributeName) {
|
|
34888
|
+
case "ResourceARN": return fs.ResourceARN;
|
|
34889
|
+
case "DNSName": return fs.DNSName;
|
|
34890
|
+
case "LustreMountName": return fs.LustreConfiguration?.MountName;
|
|
34891
|
+
case "RootVolumeId": return fs.OpenZFSConfiguration?.RootVolumeId;
|
|
34892
|
+
default: return;
|
|
34893
|
+
}
|
|
34894
|
+
}
|
|
34895
|
+
/**
|
|
34896
|
+
* State property paths this provider cannot read back from AWS:
|
|
34897
|
+
* - `SecurityGroupIds` — `DescribeFileSystems` returns network
|
|
34898
|
+
* interface ids, never the original security group list.
|
|
34899
|
+
* - `BackupId` — creation-source input, not surfaced on the deployed
|
|
34900
|
+
* file system.
|
|
34901
|
+
*/
|
|
34902
|
+
getDriftUnknownPaths(resourceType) {
|
|
34903
|
+
if (resourceType !== "AWS::FSx::FileSystem") return [];
|
|
34904
|
+
return ["SecurityGroupIds", "BackupId"];
|
|
34905
|
+
}
|
|
34906
|
+
/**
|
|
34907
|
+
* Read the AWS-current file system configuration in CFn-property shape
|
|
34908
|
+
* via `DescribeFileSystems`. Lustre data-repository fields (`ImportPath`
|
|
34909
|
+
* / `ExportPath` / `AutoImportPolicy` / `ImportedFileChunkSize`) are
|
|
34910
|
+
* mapped back from the nested `DataRepositoryConfiguration` the API
|
|
34911
|
+
* returns. Returns `undefined` when the file system is gone.
|
|
34912
|
+
*/
|
|
34913
|
+
async readCurrentState(physicalId, _logicalId, resourceType) {
|
|
34914
|
+
if (resourceType !== "AWS::FSx::FileSystem") return void 0;
|
|
34915
|
+
let fs;
|
|
34916
|
+
try {
|
|
34917
|
+
fs = (await this.getClient().send(new DescribeFileSystemsCommand$1({ FileSystemIds: [physicalId] }))).FileSystems?.[0];
|
|
34918
|
+
} catch (err) {
|
|
34919
|
+
if (err instanceof FileSystemNotFound$1) return void 0;
|
|
34920
|
+
throw err;
|
|
34921
|
+
}
|
|
34922
|
+
if (!fs) return void 0;
|
|
34923
|
+
const result = {};
|
|
34924
|
+
if (fs.FileSystemType !== void 0) result["FileSystemType"] = fs.FileSystemType;
|
|
34925
|
+
if (fs.StorageCapacity !== void 0) result["StorageCapacity"] = fs.StorageCapacity;
|
|
34926
|
+
if (fs.StorageType !== void 0) result["StorageType"] = fs.StorageType;
|
|
34927
|
+
if (fs.SubnetIds !== void 0) result["SubnetIds"] = [...fs.SubnetIds];
|
|
34928
|
+
if (fs.KmsKeyId !== void 0) result["KmsKeyId"] = fs.KmsKeyId;
|
|
34929
|
+
if (fs.FileSystemTypeVersion !== void 0) result["FileSystemTypeVersion"] = fs.FileSystemTypeVersion;
|
|
34930
|
+
if (fs.NetworkType !== void 0) result["NetworkType"] = fs.NetworkType;
|
|
34931
|
+
const lustre = fs.LustreConfiguration;
|
|
34932
|
+
if (lustre) {
|
|
34933
|
+
const config = {};
|
|
34934
|
+
if (lustre.WeeklyMaintenanceStartTime !== void 0) config["WeeklyMaintenanceStartTime"] = lustre.WeeklyMaintenanceStartTime;
|
|
34935
|
+
if (lustre.DeploymentType !== void 0) config["DeploymentType"] = lustre.DeploymentType;
|
|
34936
|
+
if (lustre.PerUnitStorageThroughput !== void 0) config["PerUnitStorageThroughput"] = lustre.PerUnitStorageThroughput;
|
|
34937
|
+
if (lustre.DailyAutomaticBackupStartTime !== void 0) config["DailyAutomaticBackupStartTime"] = lustre.DailyAutomaticBackupStartTime;
|
|
34938
|
+
if (lustre.AutomaticBackupRetentionDays !== void 0) config["AutomaticBackupRetentionDays"] = lustre.AutomaticBackupRetentionDays;
|
|
34939
|
+
if (lustre.CopyTagsToBackups !== void 0) config["CopyTagsToBackups"] = lustre.CopyTagsToBackups;
|
|
34940
|
+
if (lustre.DriveCacheType !== void 0) config["DriveCacheType"] = lustre.DriveCacheType;
|
|
34941
|
+
if (lustre.DataCompressionType !== void 0) config["DataCompressionType"] = lustre.DataCompressionType;
|
|
34942
|
+
if (lustre.EfaEnabled !== void 0) config["EfaEnabled"] = lustre.EfaEnabled;
|
|
34943
|
+
if (lustre.ThroughputCapacity !== void 0) config["ThroughputCapacity"] = lustre.ThroughputCapacity;
|
|
34944
|
+
if (lustre.MetadataConfiguration) {
|
|
34945
|
+
const metadata = {};
|
|
34946
|
+
if (lustre.MetadataConfiguration.Mode !== void 0) metadata["Mode"] = lustre.MetadataConfiguration.Mode;
|
|
34947
|
+
if (lustre.MetadataConfiguration.Iops !== void 0) metadata["Iops"] = lustre.MetadataConfiguration.Iops;
|
|
34948
|
+
if (Object.keys(metadata).length > 0) config["MetadataConfiguration"] = metadata;
|
|
34949
|
+
}
|
|
34950
|
+
if (lustre.DataReadCacheConfiguration) {
|
|
34951
|
+
const readCache = {};
|
|
34952
|
+
if (lustre.DataReadCacheConfiguration.SizingMode !== void 0) readCache["SizingMode"] = lustre.DataReadCacheConfiguration.SizingMode;
|
|
34953
|
+
if (lustre.DataReadCacheConfiguration.SizeGiB !== void 0) readCache["SizeGiB"] = lustre.DataReadCacheConfiguration.SizeGiB;
|
|
34954
|
+
if (Object.keys(readCache).length > 0) config["DataReadCacheConfiguration"] = readCache;
|
|
34955
|
+
}
|
|
34956
|
+
const dataRepo = lustre.DataRepositoryConfiguration;
|
|
34957
|
+
if (dataRepo) {
|
|
34958
|
+
if (dataRepo.ImportPath !== void 0) config["ImportPath"] = dataRepo.ImportPath;
|
|
34959
|
+
if (dataRepo.ExportPath !== void 0) config["ExportPath"] = dataRepo.ExportPath;
|
|
34960
|
+
if (dataRepo.AutoImportPolicy !== void 0) config["AutoImportPolicy"] = dataRepo.AutoImportPolicy;
|
|
34961
|
+
if (dataRepo.ImportedFileChunkSize !== void 0) config["ImportedFileChunkSize"] = dataRepo.ImportedFileChunkSize;
|
|
34962
|
+
}
|
|
34963
|
+
if (Object.keys(config).length > 0) result["LustreConfiguration"] = config;
|
|
34964
|
+
}
|
|
34965
|
+
result["Tags"] = normalizeAwsTagsToCfn(fs.Tags);
|
|
34966
|
+
return result;
|
|
34967
|
+
}
|
|
34968
|
+
/**
|
|
34969
|
+
* Adopt an existing FSx file system into cdkd state. Explicit physical
|
|
34970
|
+
* id is verified via `DescribeFileSystems`; otherwise a paginated walk
|
|
34971
|
+
* matches the `aws:cdk:path` tag (`Tags` ride inline on each item).
|
|
34972
|
+
*/
|
|
34973
|
+
async import(input) {
|
|
34974
|
+
if (input.resourceType !== "AWS::FSx::FileSystem") return null;
|
|
34975
|
+
if (input.knownPhysicalId) try {
|
|
34976
|
+
const fs = (await this.getClient().send(new DescribeFileSystemsCommand$1({ FileSystemIds: [input.knownPhysicalId] }))).FileSystems?.[0];
|
|
34977
|
+
return fs?.FileSystemId ? {
|
|
34978
|
+
physicalId: fs.FileSystemId,
|
|
34979
|
+
attributes: this.buildAttributes(fs)
|
|
34980
|
+
} : null;
|
|
34981
|
+
} catch (err) {
|
|
34982
|
+
if (err instanceof FileSystemNotFound$1) return null;
|
|
34983
|
+
throw err;
|
|
34984
|
+
}
|
|
34985
|
+
if (!input.cdkPath) return null;
|
|
34986
|
+
let nextToken;
|
|
34987
|
+
do {
|
|
34988
|
+
const list = await this.getClient().send(new DescribeFileSystemsCommand$1({ ...nextToken && { NextToken: nextToken } }));
|
|
34989
|
+
for (const fs of list.FileSystems ?? []) {
|
|
34990
|
+
if (!fs.FileSystemId) continue;
|
|
34991
|
+
if (matchesCdkPath(fs.Tags, input.cdkPath)) return {
|
|
34992
|
+
physicalId: fs.FileSystemId,
|
|
34993
|
+
attributes: this.buildAttributes(fs)
|
|
34994
|
+
};
|
|
34995
|
+
}
|
|
34996
|
+
nextToken = list.NextToken;
|
|
34997
|
+
} while (nextToken);
|
|
34998
|
+
return null;
|
|
34999
|
+
}
|
|
35000
|
+
};
|
|
35001
|
+
|
|
35002
|
+
//#endregion
|
|
35003
|
+
//#region src/provisioning/providers/emr-cluster-provider.ts
|
|
35004
|
+
/**
|
|
35005
|
+
* Default polling budget for EMR cluster lifecycle transitions. A cluster
|
|
35006
|
+
* create (RunJobFlow → WAITING/RUNNING) typically takes 5-15 minutes
|
|
35007
|
+
* (instance provisioning + bootstrap + application install), and a delete
|
|
35008
|
+
* (TerminateJobFlows → TERMINATED) 5-10 minutes. Mirror the Custom Resource /
|
|
35009
|
+
* FSx providers' 1-hour ceiling so the slowest realistic create/terminate
|
|
35010
|
+
* still fits inside the per-resource deadline.
|
|
35011
|
+
*/
|
|
35012
|
+
const DEFAULT_MAX_WAIT_MS = 3600 * 1e3;
|
|
35013
|
+
const DEFAULT_POLL_INTERVAL_MS = 15e3;
|
|
35014
|
+
/** Cluster states that mean "create succeeded, the cluster is up". */
|
|
35015
|
+
const CREATE_READY_STATES = new Set(["WAITING", "RUNNING"]);
|
|
35016
|
+
/**
|
|
35017
|
+
* Cluster states that mean "the cluster is gone" (for delete polling and
|
|
35018
|
+
* delete idempotency). `TERMINATED_WITH_ERRORS` still counts as gone — the
|
|
35019
|
+
* cluster no longer bills — but is logged so a failed terminate is visible.
|
|
35020
|
+
*/
|
|
35021
|
+
const TERMINAL_STATES = new Set(["TERMINATED", "TERMINATED_WITH_ERRORS"]);
|
|
35022
|
+
/**
|
|
35023
|
+
* Top-level CFn properties that map to a MUTABLE EMR API surface. Every other
|
|
35024
|
+
* property is either registry-createOnly (routed through DELETE+CREATE by the
|
|
35025
|
+
* replacement-detection layer) or lives inside the `Instances` block (handled
|
|
35026
|
+
* specially in `update()` — only `TerminationProtected` is mutable there).
|
|
35027
|
+
*/
|
|
35028
|
+
const MUTABLE_TOP_LEVEL_PROPS = new Set([
|
|
35029
|
+
"Tags",
|
|
35030
|
+
"VisibleToAllUsers",
|
|
35031
|
+
"StepConcurrencyLevel",
|
|
35032
|
+
"ManagedScalingPolicy",
|
|
35033
|
+
"AutoTerminationPolicy"
|
|
35034
|
+
]);
|
|
35035
|
+
const toNumber = (v) => {
|
|
35036
|
+
if (v === void 0) return void 0;
|
|
35037
|
+
const n = Number(v);
|
|
35038
|
+
return Number.isNaN(n) ? void 0 : n;
|
|
35039
|
+
};
|
|
35040
|
+
const toBoolean = (v) => {
|
|
35041
|
+
if (v === void 0) return void 0;
|
|
35042
|
+
if (typeof v === "string") return v === "true";
|
|
35043
|
+
return Boolean(v);
|
|
35044
|
+
};
|
|
35045
|
+
const jsonEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
35046
|
+
/**
|
|
35047
|
+
* SDK Provider for `AWS::EMR::Cluster` (EMR on EC2).
|
|
35048
|
+
*
|
|
35049
|
+
* The type is `ProvisioningType: NON_PROVISIONABLE` in the CFn registry, so
|
|
35050
|
+
* cdkd's Cloud Control fallback cannot handle it (issue #1043) — pre-flight
|
|
35051
|
+
* would otherwise reject it via `unsupported-types.generated.ts`.
|
|
35052
|
+
*
|
|
35053
|
+
* Lifecycle — a cluster is a stateful, per-instance-hour-billed resource, so
|
|
35054
|
+
* every path is polled to completion:
|
|
35055
|
+
* - `create` → `RunJobFlow` + poll `DescribeCluster` until `WAITING`/`RUNNING`
|
|
35056
|
+
* (the cluster is up and idle / running steps). A `TERMINATED*` terminal
|
|
35057
|
+
* during create is a hard error; the partially-created cluster is
|
|
35058
|
+
* best-effort terminated so it does not bill.
|
|
35059
|
+
* - `update` → the limited mutable surface only: `SetTerminationProtection`
|
|
35060
|
+
* (`Instances.TerminationProtected`), `SetVisibleToAllUsers`,
|
|
35061
|
+
* `ModifyCluster` (`StepConcurrencyLevel`), managed-scaling / auto-
|
|
35062
|
+
* termination policy APIs, and `AddTags`/`RemoveTags`. Everything else
|
|
35063
|
+
* (instance topology, applications, release label, ...) is createOnly →
|
|
35064
|
+
* replacement via the schema fallback; a change that reaches `update()`
|
|
35065
|
+
* anyway is refused with a `--replace` pointer.
|
|
35066
|
+
* - `delete` → `TerminateJobFlows` + poll until `TERMINATED`. Idempotent on
|
|
35067
|
+
* an already-gone cluster (`assertRegionMatch` guards the region). Honors
|
|
35068
|
+
* termination protection: under `--remove-protection` it flips
|
|
35069
|
+
* `SetTerminationProtection(false)` first (mirroring the EC2/ASG pattern).
|
|
35070
|
+
*
|
|
35071
|
+
* `getMinResourceTimeoutMs()` lifts the deploy engine's per-resource deadline
|
|
35072
|
+
* to the polling ceiling (mirrors `CustomResourceProvider` / `FSxFileSystem
|
|
35073
|
+
* Provider`), so slow EMR creates/terminates don't require `--resource-timeout`.
|
|
35074
|
+
*/
|
|
35075
|
+
var EMRClusterProvider = class {
|
|
35076
|
+
/**
|
|
35077
|
+
* Cloud Control has NO handlers for this type (`ProvisioningType:
|
|
35078
|
+
* NON_PROVISIONABLE`), so the deploy engine's #614 silent-drop auto-route
|
|
35079
|
+
* MUST NOT send an unhandled-property EMR template to CC — it would fail at
|
|
35080
|
+
* provisioning time with an opaque UnsupportedActionException. With this
|
|
35081
|
+
* opt-out the ProviderRegistry rejects such templates pre-flight with a
|
|
35082
|
+
* clear error instead.
|
|
35083
|
+
*/
|
|
35084
|
+
disableCcApiFallback = true;
|
|
35085
|
+
client;
|
|
35086
|
+
providerRegion = process.env["AWS_REGION"];
|
|
35087
|
+
logger = getLogger().child("EMRClusterProvider");
|
|
35088
|
+
pollIntervalMs;
|
|
35089
|
+
maxWaitMs;
|
|
35090
|
+
constructor(options) {
|
|
35091
|
+
this.pollIntervalMs = options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
35092
|
+
this.maxWaitMs = options?.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
|
|
35093
|
+
}
|
|
35094
|
+
handledProperties = new Map([["AWS::EMR::Cluster", new Set([
|
|
35095
|
+
"AdditionalInfo",
|
|
35096
|
+
"Applications",
|
|
35097
|
+
"AutoScalingRole",
|
|
35098
|
+
"AutoTerminationPolicy",
|
|
35099
|
+
"BootstrapActions",
|
|
35100
|
+
"Configurations",
|
|
35101
|
+
"CustomAmiId",
|
|
35102
|
+
"EbsRootVolumeIops",
|
|
35103
|
+
"EbsRootVolumeSize",
|
|
35104
|
+
"EbsRootVolumeThroughput",
|
|
35105
|
+
"Instances",
|
|
35106
|
+
"JobFlowRole",
|
|
35107
|
+
"KerberosAttributes",
|
|
35108
|
+
"LogEncryptionKmsKeyId",
|
|
35109
|
+
"LogUri",
|
|
35110
|
+
"ManagedScalingPolicy",
|
|
35111
|
+
"Name",
|
|
35112
|
+
"OSReleaseLabel",
|
|
35113
|
+
"PlacementGroupConfigs",
|
|
35114
|
+
"ReleaseLabel",
|
|
35115
|
+
"ScaleDownBehavior",
|
|
35116
|
+
"SecurityConfiguration",
|
|
35117
|
+
"ServiceRole",
|
|
35118
|
+
"StepConcurrencyLevel",
|
|
35119
|
+
"Steps",
|
|
35120
|
+
"Tags",
|
|
35121
|
+
"VisibleToAllUsers"
|
|
35122
|
+
])]]);
|
|
35123
|
+
getClient() {
|
|
35124
|
+
if (!this.client) this.client = new EMRClient(this.providerRegion ? { region: this.providerRegion } : {});
|
|
35125
|
+
return this.client;
|
|
35126
|
+
}
|
|
35127
|
+
/**
|
|
35128
|
+
* Self-reported minimum per-resource timeout: the deploy engine resolves
|
|
35129
|
+
* `max(getMinResourceTimeoutMs(), globalCliDefault)` so EMR's slow
|
|
35130
|
+
* create/terminate polling fits inside the resource deadline without the
|
|
35131
|
+
* user passing `--resource-timeout`.
|
|
35132
|
+
*
|
|
35133
|
+
* Return the poll ceiling PLUS one poll interval (not exactly `maxWaitMs`):
|
|
35134
|
+
* the deploy engine's `withResourceDeadline` is a non-cancelling
|
|
35135
|
+
* `Promise.race`, so if the external deadline were exactly equal to the
|
|
35136
|
+
* internal poll ceiling the two could fire together and the external one
|
|
35137
|
+
* could win — leaving the internal timeout + best-effort rollback terminate
|
|
35138
|
+
* un-run and (if the CLI then exits) a live cluster billing. The extra
|
|
35139
|
+
* interval guarantees the internal `waitForCluster*` timeout fires first and
|
|
35140
|
+
* the rollback path always runs.
|
|
35141
|
+
*/
|
|
35142
|
+
getMinResourceTimeoutMs() {
|
|
35143
|
+
return this.maxWaitMs + this.pollIntervalMs;
|
|
35144
|
+
}
|
|
35145
|
+
async create(logicalId, resourceType, properties) {
|
|
35146
|
+
if (resourceType !== "AWS::EMR::Cluster") throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId);
|
|
35147
|
+
this.logger.debug(`Creating EMR Cluster ${logicalId}`);
|
|
35148
|
+
let clusterId;
|
|
35149
|
+
try {
|
|
35150
|
+
const tags = properties["Tags"];
|
|
35151
|
+
const input = {
|
|
35152
|
+
Name: properties["Name"],
|
|
35153
|
+
ReleaseLabel: properties["ReleaseLabel"],
|
|
35154
|
+
ServiceRole: properties["ServiceRole"],
|
|
35155
|
+
JobFlowRole: properties["JobFlowRole"],
|
|
35156
|
+
LogUri: properties["LogUri"],
|
|
35157
|
+
LogEncryptionKmsKeyId: properties["LogEncryptionKmsKeyId"],
|
|
35158
|
+
AdditionalInfo: properties["AdditionalInfo"],
|
|
35159
|
+
AutoScalingRole: properties["AutoScalingRole"],
|
|
35160
|
+
ScaleDownBehavior: properties["ScaleDownBehavior"],
|
|
35161
|
+
CustomAmiId: properties["CustomAmiId"],
|
|
35162
|
+
OSReleaseLabel: properties["OSReleaseLabel"],
|
|
35163
|
+
SecurityConfiguration: properties["SecurityConfiguration"],
|
|
35164
|
+
EbsRootVolumeSize: toNumber(properties["EbsRootVolumeSize"]),
|
|
35165
|
+
EbsRootVolumeIops: toNumber(properties["EbsRootVolumeIops"]),
|
|
35166
|
+
EbsRootVolumeThroughput: toNumber(properties["EbsRootVolumeThroughput"]),
|
|
35167
|
+
StepConcurrencyLevel: toNumber(properties["StepConcurrencyLevel"]),
|
|
35168
|
+
VisibleToAllUsers: toBoolean(properties["VisibleToAllUsers"]),
|
|
35169
|
+
Applications: properties["Applications"],
|
|
35170
|
+
Configurations: properties["Configurations"],
|
|
35171
|
+
BootstrapActions: properties["BootstrapActions"],
|
|
35172
|
+
Steps: properties["Steps"],
|
|
35173
|
+
KerberosAttributes: properties["KerberosAttributes"],
|
|
35174
|
+
PlacementGroupConfigs: properties["PlacementGroupConfigs"],
|
|
35175
|
+
ManagedScalingPolicy: this.toManagedScalingPolicy(properties["ManagedScalingPolicy"]),
|
|
35176
|
+
AutoTerminationPolicy: this.toAutoTerminationPolicy(properties["AutoTerminationPolicy"]),
|
|
35177
|
+
Tags: tags?.map((t) => ({
|
|
35178
|
+
Key: t.Key,
|
|
35179
|
+
Value: t.Value
|
|
35180
|
+
})),
|
|
35181
|
+
Instances: this.toJobFlowInstancesConfig(properties["Instances"])
|
|
35182
|
+
};
|
|
35183
|
+
clusterId = (await this.getClient().send(new RunJobFlowCommand(input))).JobFlowId;
|
|
35184
|
+
if (!clusterId) throw new ProvisioningError(`EMR RunJobFlow for ${logicalId} returned no JobFlowId`, resourceType, logicalId);
|
|
35185
|
+
const cluster = await this.waitForClusterReady(clusterId, logicalId, resourceType);
|
|
35186
|
+
this.logger.debug(`Successfully created EMR Cluster ${logicalId}: ${clusterId}`);
|
|
35187
|
+
return {
|
|
35188
|
+
physicalId: clusterId,
|
|
35189
|
+
attributes: this.buildAttributes(cluster)
|
|
35190
|
+
};
|
|
35191
|
+
} catch (error) {
|
|
35192
|
+
if (clusterId !== void 0) try {
|
|
35193
|
+
await this.getClient().send(new SetTerminationProtectionCommand({
|
|
35194
|
+
JobFlowIds: [clusterId],
|
|
35195
|
+
TerminationProtected: false
|
|
35196
|
+
}));
|
|
35197
|
+
await this.getClient().send(new TerminateJobFlowsCommand({ JobFlowIds: [clusterId] }));
|
|
35198
|
+
this.logger.warn(`Rolled back partially-created EMR Cluster ${clusterId}`);
|
|
35199
|
+
} catch (cleanupError) {
|
|
35200
|
+
this.logger.warn(`Failed to roll back partially-created EMR Cluster ${clusterId}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)} — terminate it manually to stop billing`);
|
|
35201
|
+
}
|
|
35202
|
+
if (error instanceof ProvisioningError) throw error;
|
|
35203
|
+
const cause = error instanceof Error ? error : void 0;
|
|
35204
|
+
throw new ProvisioningError(`Failed to create EMR Cluster ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
|
|
35205
|
+
}
|
|
35206
|
+
}
|
|
35207
|
+
/**
|
|
35208
|
+
* Map the CFn `Instances` (`JobFlowInstancesConfig`) block to the SDK
|
|
35209
|
+
* `RunJobFlow.Instances` shape. Most field names are identical; the one
|
|
35210
|
+
* structural difference is the role-keyed CFn groups/fleets
|
|
35211
|
+
* (`MasterInstanceGroup` / `CoreInstanceGroup` / `TaskInstanceGroups` and
|
|
35212
|
+
* the `*InstanceFleet(s)` siblings) which the SDK expresses as flat
|
|
35213
|
+
* `InstanceGroups` / `InstanceFleets` arrays with an explicit
|
|
35214
|
+
* `InstanceRole` / `InstanceFleetType` discriminator per entry.
|
|
35215
|
+
*/
|
|
35216
|
+
toJobFlowInstancesConfig(config) {
|
|
35217
|
+
if (!config) return void 0;
|
|
35218
|
+
const instanceGroups = [];
|
|
35219
|
+
const pushGroup = (raw, role) => {
|
|
35220
|
+
if (raw === void 0 || raw === null) return;
|
|
35221
|
+
instanceGroups.push(this.toInstanceGroupConfig(raw, role));
|
|
35222
|
+
};
|
|
35223
|
+
pushGroup(config["MasterInstanceGroup"], "MASTER");
|
|
35224
|
+
pushGroup(config["CoreInstanceGroup"], "CORE");
|
|
35225
|
+
for (const task of config["TaskInstanceGroups"] ?? []) pushGroup(task, "TASK");
|
|
35226
|
+
const instanceFleets = [];
|
|
35227
|
+
const pushFleet = (raw, type) => {
|
|
35228
|
+
if (raw === void 0 || raw === null) return;
|
|
35229
|
+
instanceFleets.push(this.toInstanceFleetConfig(raw, type));
|
|
35230
|
+
};
|
|
35231
|
+
pushFleet(config["MasterInstanceFleet"], "MASTER");
|
|
35232
|
+
pushFleet(config["CoreInstanceFleet"], "CORE");
|
|
35233
|
+
for (const task of config["TaskInstanceFleets"] ?? []) pushFleet(task, "TASK");
|
|
35234
|
+
return {
|
|
35235
|
+
InstanceGroups: instanceGroups.length > 0 ? instanceGroups : void 0,
|
|
35236
|
+
InstanceFleets: instanceFleets.length > 0 ? instanceFleets : void 0,
|
|
35237
|
+
Ec2KeyName: config["Ec2KeyName"],
|
|
35238
|
+
Ec2SubnetId: config["Ec2SubnetId"],
|
|
35239
|
+
Ec2SubnetIds: config["Ec2SubnetIds"],
|
|
35240
|
+
HadoopVersion: config["HadoopVersion"],
|
|
35241
|
+
Placement: config["Placement"],
|
|
35242
|
+
KeepJobFlowAliveWhenNoSteps: toBoolean(config["KeepJobFlowAliveWhenNoSteps"]),
|
|
35243
|
+
TerminationProtected: toBoolean(config["TerminationProtected"]),
|
|
35244
|
+
UnhealthyNodeReplacement: toBoolean(config["UnhealthyNodeReplacement"]),
|
|
35245
|
+
EmrManagedMasterSecurityGroup: config["EmrManagedMasterSecurityGroup"],
|
|
35246
|
+
EmrManagedSlaveSecurityGroup: config["EmrManagedSlaveSecurityGroup"],
|
|
35247
|
+
ServiceAccessSecurityGroup: config["ServiceAccessSecurityGroup"],
|
|
35248
|
+
AdditionalMasterSecurityGroups: config["AdditionalMasterSecurityGroups"],
|
|
35249
|
+
AdditionalSlaveSecurityGroups: config["AdditionalSlaveSecurityGroups"]
|
|
35250
|
+
};
|
|
35251
|
+
}
|
|
35252
|
+
toInstanceGroupConfig(raw, role) {
|
|
35253
|
+
return {
|
|
35254
|
+
InstanceRole: role,
|
|
35255
|
+
InstanceType: raw["InstanceType"],
|
|
35256
|
+
InstanceCount: toNumber(raw["InstanceCount"]),
|
|
35257
|
+
Name: raw["Name"],
|
|
35258
|
+
Market: raw["Market"],
|
|
35259
|
+
BidPrice: raw["BidPrice"],
|
|
35260
|
+
Configurations: raw["Configurations"],
|
|
35261
|
+
EbsConfiguration: raw["EbsConfiguration"],
|
|
35262
|
+
AutoScalingPolicy: raw["AutoScalingPolicy"],
|
|
35263
|
+
CustomAmiId: raw["CustomAmiId"]
|
|
35264
|
+
};
|
|
35265
|
+
}
|
|
35266
|
+
toInstanceFleetConfig(raw, type) {
|
|
35267
|
+
return {
|
|
35268
|
+
InstanceFleetType: type,
|
|
35269
|
+
Name: raw["Name"],
|
|
35270
|
+
TargetOnDemandCapacity: toNumber(raw["TargetOnDemandCapacity"]),
|
|
35271
|
+
TargetSpotCapacity: toNumber(raw["TargetSpotCapacity"]),
|
|
35272
|
+
InstanceTypeConfigs: raw["InstanceTypeConfigs"],
|
|
35273
|
+
LaunchSpecifications: raw["LaunchSpecifications"],
|
|
35274
|
+
ResizeSpecifications: raw["ResizeSpecifications"]
|
|
35275
|
+
};
|
|
35276
|
+
}
|
|
35277
|
+
toManagedScalingPolicy(config) {
|
|
35278
|
+
if (!config) return void 0;
|
|
35279
|
+
const limits = config["ComputeLimits"];
|
|
35280
|
+
if (!limits) return void 0;
|
|
35281
|
+
return {
|
|
35282
|
+
ComputeLimits: {
|
|
35283
|
+
UnitType: limits["UnitType"],
|
|
35284
|
+
MinimumCapacityUnits: toNumber(limits["MinimumCapacityUnits"]),
|
|
35285
|
+
MaximumCapacityUnits: toNumber(limits["MaximumCapacityUnits"]),
|
|
35286
|
+
MaximumOnDemandCapacityUnits: toNumber(limits["MaximumOnDemandCapacityUnits"]),
|
|
35287
|
+
MaximumCoreCapacityUnits: toNumber(limits["MaximumCoreCapacityUnits"])
|
|
35288
|
+
},
|
|
35289
|
+
UtilizationPerformanceIndex: toNumber(config["UtilizationPerformanceIndex"]),
|
|
35290
|
+
ScalingStrategy: config["ScalingStrategy"]
|
|
35291
|
+
};
|
|
35292
|
+
}
|
|
35293
|
+
toAutoTerminationPolicy(config) {
|
|
35294
|
+
if (!config) return void 0;
|
|
35295
|
+
return { IdleTimeout: toNumber(config["IdleTimeout"]) };
|
|
35296
|
+
}
|
|
35297
|
+
async update(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
35298
|
+
const changed = (key) => !jsonEqual(properties[key], previousProperties[key]);
|
|
35299
|
+
const nextInstances = properties["Instances"] ?? {};
|
|
35300
|
+
const prevInstances = previousProperties["Instances"] ?? {};
|
|
35301
|
+
let terminationProtectedChanged = false;
|
|
35302
|
+
if (changed("Instances")) {
|
|
35303
|
+
const instanceKeys = new Set([...Object.keys(nextInstances), ...Object.keys(prevInstances)]);
|
|
35304
|
+
for (const key of instanceKeys) {
|
|
35305
|
+
if (jsonEqual(nextInstances[key], prevInstances[key])) continue;
|
|
35306
|
+
if (key === "TerminationProtected") {
|
|
35307
|
+
terminationProtectedChanged = true;
|
|
35308
|
+
continue;
|
|
35309
|
+
}
|
|
35310
|
+
throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS EMR Cluster Instances.${key} is immutable on AWS — a running cluster's instance topology / networking cannot be changed in place. Re-deploy with cdkd deploy --replace, or destroy + redeploy the stack.`);
|
|
35311
|
+
}
|
|
35312
|
+
}
|
|
35313
|
+
for (const key of Object.keys({
|
|
35314
|
+
...properties,
|
|
35315
|
+
...previousProperties
|
|
35316
|
+
})) {
|
|
35317
|
+
if (key === "Instances") continue;
|
|
35318
|
+
if (!changed(key)) continue;
|
|
35319
|
+
if (!MUTABLE_TOP_LEVEL_PROPS.has(key)) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS EMR Cluster ${key} is immutable on AWS — it is fixed at cluster creation. Re-deploy with cdkd deploy --replace, or destroy + redeploy the stack.`);
|
|
35320
|
+
}
|
|
35321
|
+
const visibleChanged = changed("VisibleToAllUsers");
|
|
35322
|
+
const stepConcurrencyChanged = changed("StepConcurrencyLevel");
|
|
35323
|
+
const managedScalingChanged = changed("ManagedScalingPolicy");
|
|
35324
|
+
const autoTerminationChanged = changed("AutoTerminationPolicy");
|
|
35325
|
+
const tagsChanged = changed("Tags");
|
|
35326
|
+
if (!terminationProtectedChanged && !visibleChanged && !stepConcurrencyChanged && !managedScalingChanged && !autoTerminationChanged && !tagsChanged) {
|
|
35327
|
+
this.logger.debug(`No mutable diff for EMR Cluster ${logicalId}, skipping update`);
|
|
35328
|
+
return {
|
|
35329
|
+
physicalId,
|
|
35330
|
+
wasReplaced: false
|
|
35331
|
+
};
|
|
35332
|
+
}
|
|
35333
|
+
this.logger.debug(`Updating EMR Cluster ${logicalId}: ${physicalId}`);
|
|
35334
|
+
try {
|
|
35335
|
+
if (terminationProtectedChanged) await this.getClient().send(new SetTerminationProtectionCommand({
|
|
35336
|
+
JobFlowIds: [physicalId],
|
|
35337
|
+
TerminationProtected: toBoolean(nextInstances["TerminationProtected"]) ?? false
|
|
35338
|
+
}));
|
|
35339
|
+
if (visibleChanged) await this.getClient().send(new SetVisibleToAllUsersCommand({
|
|
35340
|
+
JobFlowIds: [physicalId],
|
|
35341
|
+
VisibleToAllUsers: toBoolean(properties["VisibleToAllUsers"]) ?? false
|
|
35342
|
+
}));
|
|
35343
|
+
if (stepConcurrencyChanged) await this.getClient().send(new ModifyClusterCommand({
|
|
35344
|
+
ClusterId: physicalId,
|
|
35345
|
+
StepConcurrencyLevel: toNumber(properties["StepConcurrencyLevel"])
|
|
35346
|
+
}));
|
|
35347
|
+
if (managedScalingChanged) {
|
|
35348
|
+
const next = this.toManagedScalingPolicy(properties["ManagedScalingPolicy"]);
|
|
35349
|
+
if (next) await this.getClient().send(new PutManagedScalingPolicyCommand({
|
|
35350
|
+
ClusterId: physicalId,
|
|
35351
|
+
ManagedScalingPolicy: next
|
|
35352
|
+
}));
|
|
35353
|
+
else await this.getClient().send(new RemoveManagedScalingPolicyCommand({ ClusterId: physicalId }));
|
|
35354
|
+
}
|
|
35355
|
+
if (autoTerminationChanged) {
|
|
35356
|
+
const next = this.toAutoTerminationPolicy(properties["AutoTerminationPolicy"]);
|
|
35357
|
+
if (next && next.IdleTimeout !== void 0) await this.getClient().send(new PutAutoTerminationPolicyCommand({
|
|
35358
|
+
ClusterId: physicalId,
|
|
35359
|
+
AutoTerminationPolicy: next
|
|
35360
|
+
}));
|
|
35361
|
+
else await this.getClient().send(new RemoveAutoTerminationPolicyCommand({ ClusterId: physicalId }));
|
|
35362
|
+
}
|
|
35363
|
+
if (tagsChanged) await this.applyTagDiff(physicalId, properties["Tags"], previousProperties["Tags"]);
|
|
35364
|
+
let cluster;
|
|
35365
|
+
try {
|
|
35366
|
+
cluster = (await this.getClient().send(new DescribeClusterCommand({ ClusterId: physicalId }))).Cluster;
|
|
35367
|
+
} catch (describeError) {
|
|
35368
|
+
this.logger.debug(`Post-update attribute refresh for ${physicalId} failed (returning without attributes): ${describeError instanceof Error ? describeError.message : String(describeError)}`);
|
|
35369
|
+
}
|
|
35370
|
+
this.logger.debug(`Successfully updated EMR Cluster ${logicalId}`);
|
|
35371
|
+
return {
|
|
35372
|
+
physicalId,
|
|
35373
|
+
wasReplaced: false,
|
|
35374
|
+
...cluster && { attributes: this.buildAttributes(cluster) }
|
|
35375
|
+
};
|
|
35376
|
+
} catch (error) {
|
|
35377
|
+
if (error instanceof ProvisioningError) throw error;
|
|
35378
|
+
const cause = error instanceof Error ? error : void 0;
|
|
35379
|
+
throw new ProvisioningError(`Failed to update EMR Cluster ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
|
|
35380
|
+
}
|
|
35381
|
+
}
|
|
35382
|
+
/**
|
|
35383
|
+
* Apply a `Tags` diff via `AddTags` / `RemoveTags`. Full-tag-removal is
|
|
35384
|
+
* handled explicitly (a tag present before and absent now must be removed
|
|
35385
|
+
* via `RemoveTags` — mirrors the #981 ECR regression class where an empty
|
|
35386
|
+
* desired tag set silently left the old tags in place).
|
|
35387
|
+
*/
|
|
35388
|
+
async applyTagDiff(physicalId, nextTags, prevTags) {
|
|
35389
|
+
const next = new Map((nextTags ?? []).map((t) => [t.Key, t.Value]));
|
|
35390
|
+
const prev = new Map((prevTags ?? []).map((t) => [t.Key, t.Value]));
|
|
35391
|
+
const toSet = [];
|
|
35392
|
+
for (const [key, value] of next) {
|
|
35393
|
+
if (key === void 0) continue;
|
|
35394
|
+
if (prev.get(key) !== value) toSet.push({
|
|
35395
|
+
Key: key,
|
|
35396
|
+
Value: value
|
|
35397
|
+
});
|
|
35398
|
+
}
|
|
35399
|
+
const toRemove = [];
|
|
35400
|
+
for (const key of prev.keys()) if (key !== void 0 && !next.has(key)) toRemove.push(key);
|
|
35401
|
+
if (toRemove.length > 0) await this.getClient().send(new RemoveTagsCommand$1({
|
|
35402
|
+
ResourceId: physicalId,
|
|
35403
|
+
TagKeys: toRemove
|
|
35404
|
+
}));
|
|
35405
|
+
if (toSet.length > 0) await this.getClient().send(new AddTagsCommand$1({
|
|
35406
|
+
ResourceId: physicalId,
|
|
35407
|
+
Tags: toSet
|
|
35408
|
+
}));
|
|
35409
|
+
}
|
|
35410
|
+
async delete(logicalId, physicalId, resourceType, _properties, context) {
|
|
35411
|
+
this.logger.debug(`Deleting EMR Cluster ${logicalId}: ${physicalId}`);
|
|
35412
|
+
let current;
|
|
35413
|
+
try {
|
|
35414
|
+
current = (await this.getClient().send(new DescribeClusterCommand({ ClusterId: physicalId }))).Cluster;
|
|
35415
|
+
} catch (error) {
|
|
35416
|
+
if (error instanceof InvalidRequestException) {
|
|
35417
|
+
assertRegionMatch(await this.getClient().config.region(), context?.expectedRegion, resourceType, logicalId, physicalId);
|
|
35418
|
+
this.logger.debug(`EMR Cluster ${physicalId} does not exist, skipping deletion`);
|
|
35419
|
+
return;
|
|
35420
|
+
}
|
|
35421
|
+
const cause = error instanceof Error ? error : void 0;
|
|
35422
|
+
throw new ProvisioningError(`Failed to describe EMR Cluster ${logicalId} before deletion: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
|
|
35423
|
+
}
|
|
35424
|
+
const currentState = current?.Status?.State;
|
|
35425
|
+
if (currentState && TERMINAL_STATES.has(currentState)) {
|
|
35426
|
+
this.logger.debug(`EMR Cluster ${physicalId} already ${currentState}, skipping deletion`);
|
|
35427
|
+
return;
|
|
35428
|
+
}
|
|
35429
|
+
try {
|
|
35430
|
+
if (context?.removeProtection) {
|
|
35431
|
+
await this.getClient().send(new SetTerminationProtectionCommand({
|
|
35432
|
+
JobFlowIds: [physicalId],
|
|
35433
|
+
TerminationProtected: false
|
|
35434
|
+
}));
|
|
35435
|
+
this.logger.debug(`Disabled termination protection on EMR Cluster ${physicalId} before deletion`);
|
|
35436
|
+
}
|
|
35437
|
+
await this.getClient().send(new TerminateJobFlowsCommand({ JobFlowIds: [physicalId] }));
|
|
35438
|
+
} catch (error) {
|
|
35439
|
+
const cause = error instanceof Error ? error : void 0;
|
|
35440
|
+
throw new ProvisioningError(`Failed to terminate EMR Cluster ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
|
|
35441
|
+
}
|
|
35442
|
+
await this.waitForClusterTerminated(physicalId, logicalId, resourceType);
|
|
35443
|
+
this.logger.debug(`Successfully deleted EMR Cluster ${logicalId}`);
|
|
35444
|
+
}
|
|
35445
|
+
/**
|
|
35446
|
+
* Issue the polling `DescribeCluster` with bounded tolerance for TRANSIENT
|
|
35447
|
+
* errors (throttling / 5xx / connection resets): up to
|
|
35448
|
+
* `maxConsecutiveTransient` consecutive failures are absorbed before the
|
|
35449
|
+
* error propagates. A 10-minute poll at 15s intervals would otherwise turn
|
|
35450
|
+
* a single throttle into a spurious failure + rollback cycle. Non-transient
|
|
35451
|
+
* errors propagate immediately.
|
|
35452
|
+
*/
|
|
35453
|
+
async describeForPoll(clusterId, transientState, maxConsecutiveTransient = 5) {
|
|
35454
|
+
try {
|
|
35455
|
+
const response = await this.getClient().send(new DescribeClusterCommand({ ClusterId: clusterId }));
|
|
35456
|
+
transientState.count = 0;
|
|
35457
|
+
return response.Cluster;
|
|
35458
|
+
} catch (error) {
|
|
35459
|
+
const name = error instanceof Error ? error.name : "";
|
|
35460
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
35461
|
+
if ((name === "ThrottlingException" || name === "InternalServerException" || name === "InternalServerError" || name === "TimeoutError" || /rate exceeded|too many requests|throttl|timed? ?out|ECONNRESET|EPIPE|socket hang up/i.test(msg)) && transientState.count < maxConsecutiveTransient) {
|
|
35462
|
+
transientState.count += 1;
|
|
35463
|
+
this.logger.debug(`Transient DescribeCluster error while polling ${clusterId} (${transientState.count}/${maxConsecutiveTransient}): ${msg} — retrying`);
|
|
35464
|
+
return;
|
|
35465
|
+
}
|
|
35466
|
+
throw error;
|
|
35467
|
+
}
|
|
35468
|
+
}
|
|
35469
|
+
async waitForClusterReady(clusterId, logicalId, resourceType) {
|
|
35470
|
+
const startTime = Date.now();
|
|
35471
|
+
const transientState = { count: 0 };
|
|
35472
|
+
while (Date.now() - startTime < this.maxWaitMs) {
|
|
35473
|
+
const cluster = await this.describeForPoll(clusterId, transientState);
|
|
35474
|
+
const state = cluster?.Status?.State;
|
|
35475
|
+
if (cluster && state && CREATE_READY_STATES.has(state)) return cluster;
|
|
35476
|
+
if (state && TERMINAL_STATES.has(state)) throw new ProvisioningError(`EMR Cluster ${clusterId} entered terminal state ${state} during creation: ${cluster?.Status?.StateChangeReason?.Message ?? "no state-change reason reported"}`, resourceType, logicalId, clusterId);
|
|
35477
|
+
this.logger.debug(`EMR Cluster ${clusterId} state: ${state ?? "unknown"}, waiting...`);
|
|
35478
|
+
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
35479
|
+
}
|
|
35480
|
+
throw new ProvisioningError(`Timed out waiting for EMR Cluster ${clusterId} to reach WAITING/RUNNING (${Math.round(this.maxWaitMs / 6e4)} min)`, resourceType, logicalId, clusterId);
|
|
35481
|
+
}
|
|
35482
|
+
async waitForClusterTerminated(clusterId, logicalId, resourceType) {
|
|
35483
|
+
const startTime = Date.now();
|
|
35484
|
+
const transientState = { count: 0 };
|
|
35485
|
+
while (Date.now() - startTime < this.maxWaitMs) {
|
|
35486
|
+
let cluster;
|
|
35487
|
+
try {
|
|
35488
|
+
cluster = await this.describeForPoll(clusterId, transientState);
|
|
35489
|
+
} catch (error) {
|
|
35490
|
+
if (error instanceof InvalidRequestException) return;
|
|
35491
|
+
const cause = error instanceof Error ? error : void 0;
|
|
35492
|
+
throw new ProvisioningError(`Failed to poll EMR Cluster ${clusterId} termination: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, clusterId, cause);
|
|
35493
|
+
}
|
|
35494
|
+
const state = cluster?.Status?.State;
|
|
35495
|
+
if (state && TERMINAL_STATES.has(state)) {
|
|
35496
|
+
if (state === "TERMINATED_WITH_ERRORS") {
|
|
35497
|
+
const reason = cluster?.Status?.StateChangeReason?.Message ?? "no state-change reason reported";
|
|
35498
|
+
this.logger.warn(`EMR Cluster ${clusterId} terminated with errors: ${reason} (the cluster is gone and no longer bills)`);
|
|
35499
|
+
}
|
|
35500
|
+
return;
|
|
35501
|
+
}
|
|
35502
|
+
this.logger.debug(`EMR Cluster ${clusterId} state: ${state ?? "unknown"}, waiting for termination...`);
|
|
35503
|
+
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
35504
|
+
}
|
|
35505
|
+
throw new ProvisioningError(`Timed out waiting for EMR Cluster ${clusterId} termination (${Math.round(this.maxWaitMs / 6e4)} min) — verify and terminate it manually to stop billing`, resourceType, logicalId, clusterId);
|
|
35506
|
+
}
|
|
35507
|
+
buildAttributes(cluster) {
|
|
35508
|
+
const attributes = {};
|
|
35509
|
+
if (cluster?.Id !== void 0) attributes["Id"] = cluster.Id;
|
|
35510
|
+
if (cluster?.MasterPublicDnsName !== void 0) attributes["MasterPublicDNS"] = cluster.MasterPublicDnsName;
|
|
35511
|
+
return attributes;
|
|
35512
|
+
}
|
|
35513
|
+
async getAttribute(physicalId, _resourceType, attributeName) {
|
|
35514
|
+
if (attributeName === "Id") return physicalId;
|
|
35515
|
+
const cluster = (await this.getClient().send(new DescribeClusterCommand({ ClusterId: physicalId }))).Cluster;
|
|
35516
|
+
if (!cluster) return void 0;
|
|
35517
|
+
switch (attributeName) {
|
|
35518
|
+
case "MasterPublicDNS": return cluster.MasterPublicDnsName;
|
|
35519
|
+
default: return;
|
|
35520
|
+
}
|
|
35521
|
+
}
|
|
35522
|
+
};
|
|
35523
|
+
|
|
34405
35524
|
//#endregion
|
|
34406
35525
|
//#region src/provisioning/providers/firehose-provider.ts
|
|
34407
35526
|
/**
|
|
@@ -35883,14 +37002,14 @@ var CloudTrailProvider = class {
|
|
|
35883
37002
|
Value: v
|
|
35884
37003
|
});
|
|
35885
37004
|
if (tagsToRemove.length > 0) {
|
|
35886
|
-
await this.getClient().send(new RemoveTagsCommand$
|
|
37005
|
+
await this.getClient().send(new RemoveTagsCommand$2({
|
|
35887
37006
|
ResourceId: trailArn,
|
|
35888
37007
|
TagsList: tagsToRemove
|
|
35889
37008
|
}));
|
|
35890
37009
|
this.logger.debug(`Removed ${tagsToRemove.length} tag(s) from CloudTrail Trail ${trailArn}`);
|
|
35891
37010
|
}
|
|
35892
37011
|
if (tagsToAdd.length > 0) {
|
|
35893
|
-
await this.getClient().send(new AddTagsCommand$
|
|
37012
|
+
await this.getClient().send(new AddTagsCommand$2({
|
|
35894
37013
|
ResourceId: trailArn,
|
|
35895
37014
|
TagsList: tagsToAdd
|
|
35896
37015
|
}));
|
|
@@ -36623,11 +37742,11 @@ var CodeCommitRepositoryProvider = class {
|
|
|
36623
37742
|
const repoArn = metadata?.Arn;
|
|
36624
37743
|
if (repoArn) {
|
|
36625
37744
|
const removedKeys = Object.keys(oldTagMap).filter((k) => !(k in newTagMap));
|
|
36626
|
-
if (removedKeys.length > 0) await this.getClient().send(new UntagResourceCommand$
|
|
37745
|
+
if (removedKeys.length > 0) await this.getClient().send(new UntagResourceCommand$19({
|
|
36627
37746
|
resourceArn: repoArn,
|
|
36628
37747
|
tagKeys: removedKeys
|
|
36629
37748
|
}));
|
|
36630
|
-
if (Object.keys(newTagMap).length > 0) await this.getClient().send(new TagResourceCommand$
|
|
37749
|
+
if (Object.keys(newTagMap).length > 0) await this.getClient().send(new TagResourceCommand$19({
|
|
36631
37750
|
resourceArn: repoArn,
|
|
36632
37751
|
tags: newTagMap
|
|
36633
37752
|
}));
|
|
@@ -36884,7 +38003,7 @@ var DLMLifecyclePolicyProvider = class {
|
|
|
36884
38003
|
if (tagsChanged && arn) {
|
|
36885
38004
|
const removedKeys = Object.keys(oldTags).filter((k) => !(k in newTags));
|
|
36886
38005
|
if (removedKeys.length > 0) {
|
|
36887
|
-
await this.getClient().send(new UntagResourceCommand$
|
|
38006
|
+
await this.getClient().send(new UntagResourceCommand$20({
|
|
36888
38007
|
ResourceArn: arn,
|
|
36889
38008
|
TagKeys: removedKeys
|
|
36890
38009
|
}));
|
|
@@ -36893,7 +38012,7 @@ var DLMLifecyclePolicyProvider = class {
|
|
|
36893
38012
|
const tagsToAdd = {};
|
|
36894
38013
|
for (const [k, v] of Object.entries(newTags)) if (oldTags[k] !== v) tagsToAdd[k] = v;
|
|
36895
38014
|
if (Object.keys(tagsToAdd).length > 0) {
|
|
36896
|
-
await this.getClient().send(new TagResourceCommand$
|
|
38015
|
+
await this.getClient().send(new TagResourceCommand$20({
|
|
36897
38016
|
ResourceArn: arn,
|
|
36898
38017
|
Tags: tagsToAdd
|
|
36899
38018
|
}));
|
|
@@ -37096,11 +38215,11 @@ var S3VectorsProvider = class {
|
|
|
37096
38215
|
}
|
|
37097
38216
|
if (!resourceArn) throw new ProvisioningError(`Could not resolve ARN for S3 VectorBucket ${logicalId} (${physicalId}); cannot apply tag update.`, resourceType, logicalId, physicalId);
|
|
37098
38217
|
try {
|
|
37099
|
-
if (Object.keys(toSet).length > 0) await this.getClient().send(new TagResourceCommand$
|
|
38218
|
+
if (Object.keys(toSet).length > 0) await this.getClient().send(new TagResourceCommand$21({
|
|
37100
38219
|
resourceArn,
|
|
37101
38220
|
tags: toSet
|
|
37102
38221
|
}));
|
|
37103
|
-
if (toRemove.length > 0) await this.getClient().send(new UntagResourceCommand$
|
|
38222
|
+
if (toRemove.length > 0) await this.getClient().send(new UntagResourceCommand$21({
|
|
37104
38223
|
resourceArn,
|
|
37105
38224
|
tagKeys: toRemove
|
|
37106
38225
|
}));
|
|
@@ -38091,7 +39210,7 @@ var S3TablesProvider = class {
|
|
|
38091
39210
|
const resourceArn = await this.lookupTableArn(tableBucketARN, namespace, name);
|
|
38092
39211
|
if (!resourceArn) throw new ProvisioningError(`applyTableTagsDiff: GetTable returned no tableARN for ${physicalId} — table is gone or state is out-of-sync. Refusing to silently drop the tag update (run 'cdkd state orphan ${logicalId}' to clean up if the table was deleted out-of-band).`, resourceType, logicalId, physicalId);
|
|
38093
39212
|
if (removedKeys.length > 0) try {
|
|
38094
|
-
await this.getClient().send(new UntagResourceCommand$
|
|
39213
|
+
await this.getClient().send(new UntagResourceCommand$22({
|
|
38095
39214
|
resourceArn,
|
|
38096
39215
|
tagKeys: removedKeys
|
|
38097
39216
|
}));
|
|
@@ -38100,7 +39219,7 @@ var S3TablesProvider = class {
|
|
|
38100
39219
|
throw new ProvisioningError(`applyTableTagsDiff: UntagResource failed for ${resourceArn} (keys: ${removedKeys.join(", ")}): ${err instanceof Error ? err.message : String(err)}. State has NOT been updated so the next deploy will retry the tag-diff.`, resourceType, logicalId, physicalId, cause);
|
|
38101
39220
|
}
|
|
38102
39221
|
if (Object.keys(upserts).length > 0) try {
|
|
38103
|
-
await this.getClient().send(new TagResourceCommand$
|
|
39222
|
+
await this.getClient().send(new TagResourceCommand$22({
|
|
38104
39223
|
resourceArn,
|
|
38105
39224
|
tags: upserts
|
|
38106
39225
|
}));
|
|
@@ -38132,7 +39251,7 @@ var S3TablesProvider = class {
|
|
|
38132
39251
|
if (removedKeys.length === 0 && Object.keys(upserts).length === 0) return;
|
|
38133
39252
|
const resourceArn = physicalId;
|
|
38134
39253
|
if (removedKeys.length > 0) try {
|
|
38135
|
-
await this.getClient().send(new UntagResourceCommand$
|
|
39254
|
+
await this.getClient().send(new UntagResourceCommand$22({
|
|
38136
39255
|
resourceArn,
|
|
38137
39256
|
tagKeys: removedKeys
|
|
38138
39257
|
}));
|
|
@@ -38141,7 +39260,7 @@ var S3TablesProvider = class {
|
|
|
38141
39260
|
throw new ProvisioningError(`applyTableBucketTagsDiff: UntagResource failed for ${resourceArn} (keys: ${removedKeys.join(", ")}): ${err instanceof Error ? err.message : String(err)}. State has NOT been updated so the next deploy will retry the tag-diff.`, resourceType, logicalId, physicalId, cause);
|
|
38142
39261
|
}
|
|
38143
39262
|
if (Object.keys(upserts).length > 0) try {
|
|
38144
|
-
await this.getClient().send(new TagResourceCommand$
|
|
39263
|
+
await this.getClient().send(new TagResourceCommand$22({
|
|
38145
39264
|
resourceArn,
|
|
38146
39265
|
tags: upserts
|
|
38147
39266
|
}));
|
|
@@ -39790,6 +40909,8 @@ function registerAllProviders(registry) {
|
|
|
39790
40909
|
registry.register("AWS::EFS::FileSystem", efsProvider);
|
|
39791
40910
|
registry.register("AWS::EFS::MountTarget", efsProvider);
|
|
39792
40911
|
registry.register("AWS::EFS::AccessPoint", efsProvider);
|
|
40912
|
+
registry.register("AWS::FSx::FileSystem", new FSxFileSystemProvider());
|
|
40913
|
+
registry.register("AWS::EMR::Cluster", new EMRClusterProvider());
|
|
39793
40914
|
registry.register("AWS::KinesisFirehose::DeliveryStream", new FirehoseProvider());
|
|
39794
40915
|
registry.register("AWS::CloudTrail::Trail", new CloudTrailProvider());
|
|
39795
40916
|
registry.register("AWS::CodeBuild::Project", new CodeBuildProvider());
|
|
@@ -59068,7 +60189,7 @@ function reorderArgs(argv) {
|
|
|
59068
60189
|
async function main() {
|
|
59069
60190
|
installPipeCloseHandler();
|
|
59070
60191
|
const program = new Command();
|
|
59071
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
60192
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.245.0");
|
|
59072
60193
|
program.addCommand(createBootstrapCommand());
|
|
59073
60194
|
program.addCommand(createSynthCommand());
|
|
59074
60195
|
program.addCommand(createListCommand());
|