@go-to-k/cdkd 0.36.0 → 0.38.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/README.md +15 -2
- package/dist/cli.js +880 -36
- package/dist/cli.js.map +3 -3
- package/dist/go-to-k-cdkd-0.38.0.tgz +0 -0
- package/dist/index.js +79 -0
- package/dist/index.js.map +2 -2
- package/package.json +1 -1
- package/dist/go-to-k-cdkd-0.36.0.tgz +0 -0
package/dist/cli.js
CHANGED
|
@@ -9028,6 +9028,85 @@ var IAMRoleProvider = class {
|
|
|
9028
9028
|
throw err;
|
|
9029
9029
|
}
|
|
9030
9030
|
}
|
|
9031
|
+
/**
|
|
9032
|
+
* Read the AWS-current IAM role configuration in CFn-property shape.
|
|
9033
|
+
*
|
|
9034
|
+
* Issues `GetRole` for the top-level role configuration and
|
|
9035
|
+
* `ListRolePolicies` + `ListAttachedRolePolicies` for inline / managed
|
|
9036
|
+
* policy *names*. AWS URL-decodes `AssumeRolePolicyDocument` for us
|
|
9037
|
+
* when it surfaces — we re-parse it as JSON so the comparator can match
|
|
9038
|
+
* against state's already-parsed object.
|
|
9039
|
+
*
|
|
9040
|
+
* Coverage and shape decisions:
|
|
9041
|
+
* - `RoleName`, `Description`, `MaxSessionDuration`, `Path`,
|
|
9042
|
+
* `PermissionsBoundary` — straight from `Role.*`.
|
|
9043
|
+
* - `AssumeRolePolicyDocument` — `Role.AssumeRolePolicyDocument` is a
|
|
9044
|
+
* URL-encoded JSON string; we URL-decode + JSON-parse so cdkd state's
|
|
9045
|
+
* object form compares cleanly. (Both shapes — string and object — are
|
|
9046
|
+
* accepted by `create()`, but state typically stores the parsed object
|
|
9047
|
+
* after intrinsic resolution.)
|
|
9048
|
+
* - `ManagedPolicyArns` — array of ARN strings from
|
|
9049
|
+
* `ListAttachedRolePolicies`.
|
|
9050
|
+
* - `Policies` (inline policies with `PolicyDocument` bodies) is
|
|
9051
|
+
* intentionally omitted: surfacing names without bodies guarantees a
|
|
9052
|
+
* PolicyDocument-shaped drift on every role, and fetching every body
|
|
9053
|
+
* costs one extra `GetRolePolicy` per inline policy. Out of scope for
|
|
9054
|
+
* v1 — drift detection on inline IAM policy bodies can ship in a
|
|
9055
|
+
* follow-up.
|
|
9056
|
+
* - `Tags` is omitted for the same reason as Lambda's tags handling
|
|
9057
|
+
* (CDK auto-injects `aws:cdk:path` and the shape decision belongs in a
|
|
9058
|
+
* dedicated tags PR).
|
|
9059
|
+
*
|
|
9060
|
+
* Returns `undefined` when the role is gone (`NoSuchEntityException`).
|
|
9061
|
+
*/
|
|
9062
|
+
async readCurrentState(physicalId, _logicalId, _resourceType) {
|
|
9063
|
+
let role;
|
|
9064
|
+
try {
|
|
9065
|
+
const resp = await this.iamClient.send(new GetRoleCommand({ RoleName: physicalId }));
|
|
9066
|
+
role = resp.Role;
|
|
9067
|
+
} catch (err) {
|
|
9068
|
+
if (err instanceof NoSuchEntityException)
|
|
9069
|
+
return void 0;
|
|
9070
|
+
throw err;
|
|
9071
|
+
}
|
|
9072
|
+
if (!role)
|
|
9073
|
+
return void 0;
|
|
9074
|
+
const result = {};
|
|
9075
|
+
if (role.RoleName !== void 0)
|
|
9076
|
+
result["RoleName"] = role.RoleName;
|
|
9077
|
+
if (role.Description !== void 0 && role.Description !== "") {
|
|
9078
|
+
result["Description"] = role.Description;
|
|
9079
|
+
}
|
|
9080
|
+
if (role.MaxSessionDuration !== void 0) {
|
|
9081
|
+
result["MaxSessionDuration"] = role.MaxSessionDuration;
|
|
9082
|
+
}
|
|
9083
|
+
if (role.Path !== void 0)
|
|
9084
|
+
result["Path"] = role.Path;
|
|
9085
|
+
if (role.PermissionsBoundary?.PermissionsBoundaryArn !== void 0) {
|
|
9086
|
+
result["PermissionsBoundary"] = role.PermissionsBoundary.PermissionsBoundaryArn;
|
|
9087
|
+
}
|
|
9088
|
+
if (role.AssumeRolePolicyDocument) {
|
|
9089
|
+
try {
|
|
9090
|
+
result["AssumeRolePolicyDocument"] = JSON.parse(
|
|
9091
|
+
decodeURIComponent(role.AssumeRolePolicyDocument)
|
|
9092
|
+
);
|
|
9093
|
+
} catch {
|
|
9094
|
+
result["AssumeRolePolicyDocument"] = role.AssumeRolePolicyDocument;
|
|
9095
|
+
}
|
|
9096
|
+
}
|
|
9097
|
+
try {
|
|
9098
|
+
const attached = await this.iamClient.send(
|
|
9099
|
+
new ListAttachedRolePoliciesCommand({ RoleName: physicalId })
|
|
9100
|
+
);
|
|
9101
|
+
const arns = (attached.AttachedPolicies ?? []).map((p) => p.PolicyArn).filter((arn) => !!arn);
|
|
9102
|
+
if (arns.length > 0)
|
|
9103
|
+
result["ManagedPolicyArns"] = arns;
|
|
9104
|
+
} catch (err) {
|
|
9105
|
+
if (!(err instanceof NoSuchEntityException))
|
|
9106
|
+
throw err;
|
|
9107
|
+
}
|
|
9108
|
+
return result;
|
|
9109
|
+
}
|
|
9031
9110
|
/**
|
|
9032
9111
|
* Adopt an existing IAM role into cdkd state.
|
|
9033
9112
|
*
|
|
@@ -10831,7 +10910,10 @@ import {
|
|
|
10831
10910
|
PutBucketInventoryConfigurationCommand,
|
|
10832
10911
|
PutBucketReplicationCommand,
|
|
10833
10912
|
PutObjectLockConfigurationCommand,
|
|
10913
|
+
GetBucketEncryptionCommand,
|
|
10834
10914
|
GetBucketTaggingCommand,
|
|
10915
|
+
GetBucketVersioningCommand,
|
|
10916
|
+
GetPublicAccessBlockCommand,
|
|
10835
10917
|
NoSuchBucket,
|
|
10836
10918
|
ListObjectVersionsCommand,
|
|
10837
10919
|
DeleteObjectsCommand
|
|
@@ -11688,6 +11770,119 @@ var S3BucketProvider = class {
|
|
|
11688
11770
|
);
|
|
11689
11771
|
}
|
|
11690
11772
|
}
|
|
11773
|
+
/**
|
|
11774
|
+
* Read the AWS-current S3 bucket configuration in CFn-property shape.
|
|
11775
|
+
*
|
|
11776
|
+
* Issues a small handful of independent S3 GET calls and stitches them
|
|
11777
|
+
* into a single CFn-shaped object. Each call can throw a "feature not
|
|
11778
|
+
* configured" error (`NoSuchBucketConfiguration`,
|
|
11779
|
+
* `ServerSideEncryptionConfigurationNotFoundError`, `NoSuchTagSet`,
|
|
11780
|
+
* `NoSuchPublicAccessBlockConfiguration`) — those are caught individually
|
|
11781
|
+
* and the corresponding key is omitted from the result, NOT treated as
|
|
11782
|
+
* the bucket being absent.
|
|
11783
|
+
*
|
|
11784
|
+
* Only the bucket-gone case (`NoSuchBucket`, HTTP 404 from `HeadBucket`)
|
|
11785
|
+
* returns `undefined`.
|
|
11786
|
+
*
|
|
11787
|
+
* Coverage: `BucketName`, `VersioningConfiguration`, `BucketEncryption`,
|
|
11788
|
+
* `PublicAccessBlockConfiguration`, `Tags`. Other configuration
|
|
11789
|
+
* properties (Lifecycle, CORS, Website, Logging, Notification,
|
|
11790
|
+
* Replication, ObjectLock, Accelerate, Metrics/Analytics/IntelligentTier/
|
|
11791
|
+
* Inventory) are out of scope for v1 — they each need their own GET +
|
|
11792
|
+
* shape mapping; CC API drift detection picks them up via `GetResource`
|
|
11793
|
+
* once a user works through the SDK provider boundary.
|
|
11794
|
+
*/
|
|
11795
|
+
async readCurrentState(physicalId, _logicalId, _resourceType) {
|
|
11796
|
+
try {
|
|
11797
|
+
await this.s3Client.send(new HeadBucketCommand3({ Bucket: physicalId }));
|
|
11798
|
+
} catch (err) {
|
|
11799
|
+
const e = err;
|
|
11800
|
+
if (err instanceof NoSuchBucket || e.name === "NotFound" || e.name === "NoSuchBucket" || e.$metadata?.httpStatusCode === 404) {
|
|
11801
|
+
return void 0;
|
|
11802
|
+
}
|
|
11803
|
+
throw err;
|
|
11804
|
+
}
|
|
11805
|
+
const result = {
|
|
11806
|
+
BucketName: physicalId
|
|
11807
|
+
};
|
|
11808
|
+
{
|
|
11809
|
+
const resp = await this.s3Client.send(new GetBucketVersioningCommand({ Bucket: physicalId }));
|
|
11810
|
+
if (resp.Status) {
|
|
11811
|
+
result["VersioningConfiguration"] = { Status: resp.Status };
|
|
11812
|
+
}
|
|
11813
|
+
}
|
|
11814
|
+
try {
|
|
11815
|
+
const resp = await this.s3Client.send(new GetBucketEncryptionCommand({ Bucket: physicalId }));
|
|
11816
|
+
const rules = resp.ServerSideEncryptionConfiguration?.Rules;
|
|
11817
|
+
if (rules && rules.length > 0) {
|
|
11818
|
+
result["BucketEncryption"] = {
|
|
11819
|
+
ServerSideEncryptionConfiguration: rules.map((rule) => {
|
|
11820
|
+
const out = {};
|
|
11821
|
+
const sse = rule.ApplyServerSideEncryptionByDefault;
|
|
11822
|
+
if (sse) {
|
|
11823
|
+
const sseOut = {};
|
|
11824
|
+
if (sse.SSEAlgorithm !== void 0)
|
|
11825
|
+
sseOut["SSEAlgorithm"] = sse.SSEAlgorithm;
|
|
11826
|
+
if (sse.KMSMasterKeyID !== void 0)
|
|
11827
|
+
sseOut["KMSMasterKeyID"] = sse.KMSMasterKeyID;
|
|
11828
|
+
out["ServerSideEncryptionByDefault"] = sseOut;
|
|
11829
|
+
}
|
|
11830
|
+
if (rule.BucketKeyEnabled !== void 0)
|
|
11831
|
+
out["BucketKeyEnabled"] = rule.BucketKeyEnabled;
|
|
11832
|
+
return out;
|
|
11833
|
+
})
|
|
11834
|
+
};
|
|
11835
|
+
}
|
|
11836
|
+
} catch (err) {
|
|
11837
|
+
const e = err;
|
|
11838
|
+
if (e.name !== "ServerSideEncryptionConfigurationNotFoundError") {
|
|
11839
|
+
throw err;
|
|
11840
|
+
}
|
|
11841
|
+
}
|
|
11842
|
+
try {
|
|
11843
|
+
const resp = await this.s3Client.send(
|
|
11844
|
+
new GetPublicAccessBlockCommand({ Bucket: physicalId })
|
|
11845
|
+
);
|
|
11846
|
+
const cfg = resp.PublicAccessBlockConfiguration;
|
|
11847
|
+
if (cfg) {
|
|
11848
|
+
const out = {};
|
|
11849
|
+
if (cfg.BlockPublicAcls !== void 0)
|
|
11850
|
+
out["BlockPublicAcls"] = cfg.BlockPublicAcls;
|
|
11851
|
+
if (cfg.BlockPublicPolicy !== void 0)
|
|
11852
|
+
out["BlockPublicPolicy"] = cfg.BlockPublicPolicy;
|
|
11853
|
+
if (cfg.IgnorePublicAcls !== void 0)
|
|
11854
|
+
out["IgnorePublicAcls"] = cfg.IgnorePublicAcls;
|
|
11855
|
+
if (cfg.RestrictPublicBuckets !== void 0) {
|
|
11856
|
+
out["RestrictPublicBuckets"] = cfg.RestrictPublicBuckets;
|
|
11857
|
+
}
|
|
11858
|
+
if (Object.keys(out).length > 0) {
|
|
11859
|
+
result["PublicAccessBlockConfiguration"] = out;
|
|
11860
|
+
}
|
|
11861
|
+
}
|
|
11862
|
+
} catch (err) {
|
|
11863
|
+
const e = err;
|
|
11864
|
+
if (e.name !== "NoSuchPublicAccessBlockConfiguration") {
|
|
11865
|
+
throw err;
|
|
11866
|
+
}
|
|
11867
|
+
}
|
|
11868
|
+
try {
|
|
11869
|
+
const resp = await this.s3Client.send(new GetBucketTaggingCommand({ Bucket: physicalId }));
|
|
11870
|
+
if (resp.TagSet && resp.TagSet.length > 0) {
|
|
11871
|
+
const tags = resp.TagSet.filter((t) => t.Key && !t.Key.startsWith("aws:")).map((t) => ({
|
|
11872
|
+
Key: t.Key,
|
|
11873
|
+
Value: t.Value
|
|
11874
|
+
}));
|
|
11875
|
+
if (tags.length > 0)
|
|
11876
|
+
result["Tags"] = tags;
|
|
11877
|
+
}
|
|
11878
|
+
} catch (err) {
|
|
11879
|
+
const e = err;
|
|
11880
|
+
if (e.name !== "NoSuchTagSet") {
|
|
11881
|
+
throw err;
|
|
11882
|
+
}
|
|
11883
|
+
}
|
|
11884
|
+
return result;
|
|
11885
|
+
}
|
|
11691
11886
|
/**
|
|
11692
11887
|
* Adopt an existing S3 bucket into cdkd state.
|
|
11693
11888
|
*
|
|
@@ -12238,6 +12433,90 @@ var SQSQueueProvider = class {
|
|
|
12238
12433
|
return void 0;
|
|
12239
12434
|
}
|
|
12240
12435
|
}
|
|
12436
|
+
/**
|
|
12437
|
+
* Read the AWS-current SQS queue configuration in CFn-property shape.
|
|
12438
|
+
*
|
|
12439
|
+
* Issues `GetQueueAttributes` for every attribute that maps back to a
|
|
12440
|
+
* cdkd-managed CFn property. AWS returns ALL attribute values as strings;
|
|
12441
|
+
* we type-coerce numeric attributes back to numbers and parse
|
|
12442
|
+
* `RedrivePolicy` from JSON so the comparator matches cdkd state's
|
|
12443
|
+
* already-typed values.
|
|
12444
|
+
*
|
|
12445
|
+
* `QueueName` is derived from the URL tail (the `physicalId` is the
|
|
12446
|
+
* queue URL), not surfaced by `GetQueueAttributes`.
|
|
12447
|
+
*
|
|
12448
|
+
* `Tags` is omitted: `ListQueueTags` is a separate call and tag drift is
|
|
12449
|
+
* generally less interesting than configuration drift; the `aws:cdk:path`
|
|
12450
|
+
* shape question is also out of scope here.
|
|
12451
|
+
*
|
|
12452
|
+
* Returns `undefined` when the queue is gone (`QueueDoesNotExist`).
|
|
12453
|
+
*/
|
|
12454
|
+
async readCurrentState(physicalId, _logicalId, _resourceType) {
|
|
12455
|
+
let attributes;
|
|
12456
|
+
try {
|
|
12457
|
+
const resp = await this.sqsClient.send(
|
|
12458
|
+
new GetQueueAttributesCommand({
|
|
12459
|
+
QueueUrl: physicalId,
|
|
12460
|
+
AttributeNames: ["All"]
|
|
12461
|
+
})
|
|
12462
|
+
);
|
|
12463
|
+
attributes = resp.Attributes;
|
|
12464
|
+
} catch (err) {
|
|
12465
|
+
if (err instanceof QueueDoesNotExist)
|
|
12466
|
+
return void 0;
|
|
12467
|
+
throw err;
|
|
12468
|
+
}
|
|
12469
|
+
if (!attributes)
|
|
12470
|
+
return void 0;
|
|
12471
|
+
const result = {};
|
|
12472
|
+
const tail = physicalId.substring(physicalId.lastIndexOf("/") + 1);
|
|
12473
|
+
if (tail)
|
|
12474
|
+
result["QueueName"] = tail;
|
|
12475
|
+
const numeric = [
|
|
12476
|
+
"VisibilityTimeout",
|
|
12477
|
+
"MaximumMessageSize",
|
|
12478
|
+
"MessageRetentionPeriod",
|
|
12479
|
+
"DelaySeconds",
|
|
12480
|
+
"ReceiveMessageWaitTimeSeconds",
|
|
12481
|
+
"KmsDataKeyReusePeriodSeconds"
|
|
12482
|
+
];
|
|
12483
|
+
for (const key of numeric) {
|
|
12484
|
+
const v = attributes[key];
|
|
12485
|
+
if (v !== void 0) {
|
|
12486
|
+
const n = Number(v);
|
|
12487
|
+
if (!Number.isNaN(n))
|
|
12488
|
+
result[key] = n;
|
|
12489
|
+
}
|
|
12490
|
+
}
|
|
12491
|
+
const bool = [
|
|
12492
|
+
"FifoQueue",
|
|
12493
|
+
"ContentBasedDeduplication",
|
|
12494
|
+
"SqsManagedSseEnabled"
|
|
12495
|
+
];
|
|
12496
|
+
for (const key of bool) {
|
|
12497
|
+
const v = attributes[key];
|
|
12498
|
+
if (v !== void 0)
|
|
12499
|
+
result[key] = v === "true";
|
|
12500
|
+
}
|
|
12501
|
+
const str = [
|
|
12502
|
+
"KmsMasterKeyId",
|
|
12503
|
+
"DeduplicationScope",
|
|
12504
|
+
"FifoThroughputLimit"
|
|
12505
|
+
];
|
|
12506
|
+
for (const key of str) {
|
|
12507
|
+
const v = attributes[key];
|
|
12508
|
+
if (v !== void 0)
|
|
12509
|
+
result[key] = v;
|
|
12510
|
+
}
|
|
12511
|
+
if (attributes["RedrivePolicy"]) {
|
|
12512
|
+
try {
|
|
12513
|
+
result["RedrivePolicy"] = JSON.parse(attributes["RedrivePolicy"]);
|
|
12514
|
+
} catch {
|
|
12515
|
+
result["RedrivePolicy"] = attributes["RedrivePolicy"];
|
|
12516
|
+
}
|
|
12517
|
+
}
|
|
12518
|
+
return result;
|
|
12519
|
+
}
|
|
12241
12520
|
/**
|
|
12242
12521
|
* Adopt an existing SQS queue into cdkd state.
|
|
12243
12522
|
*
|
|
@@ -12795,6 +13074,76 @@ var SNSTopicProvider = class {
|
|
|
12795
13074
|
return void 0;
|
|
12796
13075
|
}
|
|
12797
13076
|
}
|
|
13077
|
+
/**
|
|
13078
|
+
* Read the AWS-current SNS topic configuration in CFn-property shape.
|
|
13079
|
+
*
|
|
13080
|
+
* Issues `GetTopicAttributes` for the topic-level configuration. AWS
|
|
13081
|
+
* returns ALL attribute values as strings; we type-coerce booleans back
|
|
13082
|
+
* to booleans and parse `ArchivePolicy` / `DataProtectionPolicy` from
|
|
13083
|
+
* JSON strings so the comparator matches cdkd state's typed values.
|
|
13084
|
+
*
|
|
13085
|
+
* `TopicName` is derived from the ARN tail (the `physicalId` is the
|
|
13086
|
+
* topic ARN).
|
|
13087
|
+
*
|
|
13088
|
+
* `Tags` and `DeliveryStatusLogging` are intentionally omitted:
|
|
13089
|
+
* `ListTagsForResource` is a separate call, and `DeliveryStatusLogging`
|
|
13090
|
+
* fans out into per-protocol attributes (`{Protocol}SuccessFeedbackRoleArn`,
|
|
13091
|
+
* etc.) whose round-trip back to the CFn array shape needs more thought
|
|
13092
|
+
* than fits in this PR.
|
|
13093
|
+
*
|
|
13094
|
+
* `Subscription` is omitted because CDK manages it via separate
|
|
13095
|
+
* `AWS::SNS::Subscription` resources, not as a Topic property.
|
|
13096
|
+
*
|
|
13097
|
+
* Returns `undefined` when the topic is gone (`NotFoundException`).
|
|
13098
|
+
*/
|
|
13099
|
+
async readCurrentState(physicalId, _logicalId, _resourceType) {
|
|
13100
|
+
let attrs;
|
|
13101
|
+
try {
|
|
13102
|
+
const resp = await this.snsClient.send(
|
|
13103
|
+
new GetTopicAttributesCommand({ TopicArn: physicalId })
|
|
13104
|
+
);
|
|
13105
|
+
attrs = resp.Attributes;
|
|
13106
|
+
} catch (err) {
|
|
13107
|
+
if (err instanceof NotFoundException)
|
|
13108
|
+
return void 0;
|
|
13109
|
+
throw err;
|
|
13110
|
+
}
|
|
13111
|
+
if (!attrs)
|
|
13112
|
+
return void 0;
|
|
13113
|
+
const result = {};
|
|
13114
|
+
const tail = physicalId.substring(physicalId.lastIndexOf(":") + 1);
|
|
13115
|
+
if (tail)
|
|
13116
|
+
result["TopicName"] = tail;
|
|
13117
|
+
const bool = ["FifoTopic", "ContentBasedDeduplication"];
|
|
13118
|
+
for (const key of bool) {
|
|
13119
|
+
const v = attrs[key];
|
|
13120
|
+
if (v !== void 0)
|
|
13121
|
+
result[key] = v === "true";
|
|
13122
|
+
}
|
|
13123
|
+
const str = [
|
|
13124
|
+
"DisplayName",
|
|
13125
|
+
"KmsMasterKeyId",
|
|
13126
|
+
"TracingConfig",
|
|
13127
|
+
"SignatureVersion",
|
|
13128
|
+
"FifoThroughputScope"
|
|
13129
|
+
];
|
|
13130
|
+
for (const key of str) {
|
|
13131
|
+
const v = attrs[key];
|
|
13132
|
+
if (v !== void 0 && v !== "")
|
|
13133
|
+
result[key] = v;
|
|
13134
|
+
}
|
|
13135
|
+
for (const key of ["ArchivePolicy", "DataProtectionPolicy"]) {
|
|
13136
|
+
const v = attrs[key];
|
|
13137
|
+
if (v) {
|
|
13138
|
+
try {
|
|
13139
|
+
result[key] = JSON.parse(v);
|
|
13140
|
+
} catch {
|
|
13141
|
+
result[key] = v;
|
|
13142
|
+
}
|
|
13143
|
+
}
|
|
13144
|
+
}
|
|
13145
|
+
return result;
|
|
13146
|
+
}
|
|
12798
13147
|
/**
|
|
12799
13148
|
* Adopt an existing SNS topic into cdkd state.
|
|
12800
13149
|
*
|
|
@@ -13880,6 +14229,93 @@ var LambdaFunctionProvider = class {
|
|
|
13880
14229
|
throw err;
|
|
13881
14230
|
}
|
|
13882
14231
|
}
|
|
14232
|
+
/**
|
|
14233
|
+
* Read the AWS-current Lambda function configuration in CFn-property shape.
|
|
14234
|
+
*
|
|
14235
|
+
* Issues a single `GetFunction` and surfaces the same property keys
|
|
14236
|
+
* `create()` accepts (`Runtime`, `Handler`, `Role`, `Timeout`, `MemorySize`,
|
|
14237
|
+
* `Description`, `Environment`, `Layers`, `Architectures`, `PackageType`,
|
|
14238
|
+
* `TracingConfig`, `EphemeralStorage`, `VpcConfig`, plus the physical
|
|
14239
|
+
* `FunctionName`). The drift comparator only descends into keys present in
|
|
14240
|
+
* cdkd state, so AWS-managed fields (timestamps, FunctionArn, RevisionId,
|
|
14241
|
+
* etc.) are filtered at compare time — we still avoid serializing them on
|
|
14242
|
+
* the wire.
|
|
14243
|
+
*
|
|
14244
|
+
* `Code` is intentionally omitted: `GetFunction` returns a pre-signed S3
|
|
14245
|
+
* URL for the deployed code, not the asset hash cdkd state holds, so they
|
|
14246
|
+
* could never match. Lambda code drift is best detected separately (the
|
|
14247
|
+
* function's `CodeSha256` does live in `GetFunction` but is not what
|
|
14248
|
+
* cdkd's `Code: { S3Bucket, S3Key }` state property carries).
|
|
14249
|
+
*
|
|
14250
|
+
* `Tags` is omitted as well: `GetFunction` returns Tags as an object map,
|
|
14251
|
+
* while CFn / cdkd state holds them as `[{Key, Value}]`. Re-shaping
|
|
14252
|
+
* accurately requires deciding how to handle the auto-injected
|
|
14253
|
+
* `aws:cdk:path` tag, which is out of scope for this PR. Tag drift is
|
|
14254
|
+
* typically less interesting than configuration drift.
|
|
14255
|
+
*
|
|
14256
|
+
* Returns `undefined` when the function is gone (`ResourceNotFoundException`).
|
|
14257
|
+
*/
|
|
14258
|
+
async readCurrentState(physicalId, _logicalId, _resourceType) {
|
|
14259
|
+
try {
|
|
14260
|
+
const resp = await this.lambdaClient.send(
|
|
14261
|
+
new GetFunctionCommand({ FunctionName: physicalId })
|
|
14262
|
+
);
|
|
14263
|
+
const cfg = resp.Configuration;
|
|
14264
|
+
if (!cfg)
|
|
14265
|
+
return void 0;
|
|
14266
|
+
const result = {};
|
|
14267
|
+
if (cfg.FunctionName !== void 0)
|
|
14268
|
+
result["FunctionName"] = cfg.FunctionName;
|
|
14269
|
+
if (cfg.Runtime !== void 0)
|
|
14270
|
+
result["Runtime"] = cfg.Runtime;
|
|
14271
|
+
if (cfg.Handler !== void 0)
|
|
14272
|
+
result["Handler"] = cfg.Handler;
|
|
14273
|
+
if (cfg.Role !== void 0)
|
|
14274
|
+
result["Role"] = cfg.Role;
|
|
14275
|
+
if (cfg.Timeout !== void 0)
|
|
14276
|
+
result["Timeout"] = cfg.Timeout;
|
|
14277
|
+
if (cfg.MemorySize !== void 0)
|
|
14278
|
+
result["MemorySize"] = cfg.MemorySize;
|
|
14279
|
+
if (cfg.Description !== void 0 && cfg.Description !== "") {
|
|
14280
|
+
result["Description"] = cfg.Description;
|
|
14281
|
+
}
|
|
14282
|
+
if (cfg.Environment?.Variables) {
|
|
14283
|
+
result["Environment"] = { Variables: cfg.Environment.Variables };
|
|
14284
|
+
}
|
|
14285
|
+
if (cfg.Layers && cfg.Layers.length > 0) {
|
|
14286
|
+
result["Layers"] = cfg.Layers.map((l) => l.Arn).filter((arn) => !!arn);
|
|
14287
|
+
}
|
|
14288
|
+
if (cfg.Architectures && cfg.Architectures.length > 0) {
|
|
14289
|
+
result["Architectures"] = [...cfg.Architectures];
|
|
14290
|
+
}
|
|
14291
|
+
if (cfg.PackageType !== void 0)
|
|
14292
|
+
result["PackageType"] = cfg.PackageType;
|
|
14293
|
+
if (cfg.TracingConfig?.Mode !== void 0) {
|
|
14294
|
+
result["TracingConfig"] = { Mode: cfg.TracingConfig.Mode };
|
|
14295
|
+
}
|
|
14296
|
+
if (cfg.EphemeralStorage?.Size !== void 0) {
|
|
14297
|
+
result["EphemeralStorage"] = { Size: cfg.EphemeralStorage.Size };
|
|
14298
|
+
}
|
|
14299
|
+
if (cfg.VpcConfig) {
|
|
14300
|
+
const vpc = {};
|
|
14301
|
+
if (cfg.VpcConfig.SubnetIds)
|
|
14302
|
+
vpc["SubnetIds"] = [...cfg.VpcConfig.SubnetIds];
|
|
14303
|
+
if (cfg.VpcConfig.SecurityGroupIds) {
|
|
14304
|
+
vpc["SecurityGroupIds"] = [...cfg.VpcConfig.SecurityGroupIds];
|
|
14305
|
+
}
|
|
14306
|
+
if (cfg.VpcConfig.Ipv6AllowedForDualStack !== void 0) {
|
|
14307
|
+
vpc["Ipv6AllowedForDualStack"] = cfg.VpcConfig.Ipv6AllowedForDualStack;
|
|
14308
|
+
}
|
|
14309
|
+
if (Object.keys(vpc).length > 0)
|
|
14310
|
+
result["VpcConfig"] = vpc;
|
|
14311
|
+
}
|
|
14312
|
+
return result;
|
|
14313
|
+
} catch (err) {
|
|
14314
|
+
if (err instanceof ResourceNotFoundException)
|
|
14315
|
+
return void 0;
|
|
14316
|
+
throw err;
|
|
14317
|
+
}
|
|
14318
|
+
}
|
|
13883
14319
|
/**
|
|
13884
14320
|
* Adopt an existing Lambda function into cdkd state.
|
|
13885
14321
|
*
|
|
@@ -15055,6 +15491,90 @@ var DynamoDBTableProvider = class {
|
|
|
15055
15491
|
throw err;
|
|
15056
15492
|
}
|
|
15057
15493
|
}
|
|
15494
|
+
/**
|
|
15495
|
+
* Read the AWS-current DynamoDB table configuration in CFn-property shape.
|
|
15496
|
+
*
|
|
15497
|
+
* `DescribeTable` returns every field cdkd manages in one call. AWS uses
|
|
15498
|
+
* the same property names CFn does (KeySchema, AttributeDefinitions,
|
|
15499
|
+
* BillingModeSummary.BillingMode, ProvisionedThroughput, etc.) — the only
|
|
15500
|
+
* shape differences are wrapping:
|
|
15501
|
+
* - BillingMode lives under `BillingModeSummary.BillingMode` in the API
|
|
15502
|
+
* response, but the CFn property is a flat `BillingMode` string.
|
|
15503
|
+
* - StreamSpecification's CFn shape includes only `StreamViewType`; the
|
|
15504
|
+
* API response carries `StreamEnabled` too. We surface both since the
|
|
15505
|
+
* drift comparator only descends into keys present in state.
|
|
15506
|
+
* - GSI / LSI in the API response include `IndexStatus`, `ItemCount` and
|
|
15507
|
+
* sizing fields that cdkd never sets; the comparator filters them.
|
|
15508
|
+
*
|
|
15509
|
+
* Returns `undefined` when the table is gone (`ResourceNotFoundException`).
|
|
15510
|
+
*
|
|
15511
|
+
* Tags are intentionally omitted: `ListTagsOfResource` is a separate call
|
|
15512
|
+
* and tag drift is generally less interesting than table-config drift;
|
|
15513
|
+
* including it would also force a tag-shape decision on the
|
|
15514
|
+
* `aws:cdk:path` auto-tag, which is out of scope here.
|
|
15515
|
+
*/
|
|
15516
|
+
async readCurrentState(physicalId, _logicalId, _resourceType) {
|
|
15517
|
+
try {
|
|
15518
|
+
const resp = await this.dynamoDBClient.send(
|
|
15519
|
+
new DescribeTableCommand2({ TableName: physicalId })
|
|
15520
|
+
);
|
|
15521
|
+
const table = resp.Table;
|
|
15522
|
+
if (!table)
|
|
15523
|
+
return void 0;
|
|
15524
|
+
const result = {};
|
|
15525
|
+
if (table.TableName !== void 0)
|
|
15526
|
+
result["TableName"] = table.TableName;
|
|
15527
|
+
if (table.KeySchema)
|
|
15528
|
+
result["KeySchema"] = table.KeySchema;
|
|
15529
|
+
if (table.AttributeDefinitions) {
|
|
15530
|
+
result["AttributeDefinitions"] = table.AttributeDefinitions;
|
|
15531
|
+
}
|
|
15532
|
+
if (table.BillingModeSummary?.BillingMode) {
|
|
15533
|
+
result["BillingMode"] = table.BillingModeSummary.BillingMode;
|
|
15534
|
+
}
|
|
15535
|
+
if (table.ProvisionedThroughput) {
|
|
15536
|
+
result["ProvisionedThroughput"] = {
|
|
15537
|
+
ReadCapacityUnits: table.ProvisionedThroughput.ReadCapacityUnits,
|
|
15538
|
+
WriteCapacityUnits: table.ProvisionedThroughput.WriteCapacityUnits
|
|
15539
|
+
};
|
|
15540
|
+
}
|
|
15541
|
+
if (table.StreamSpecification) {
|
|
15542
|
+
result["StreamSpecification"] = {
|
|
15543
|
+
StreamEnabled: table.StreamSpecification.StreamEnabled,
|
|
15544
|
+
StreamViewType: table.StreamSpecification.StreamViewType
|
|
15545
|
+
};
|
|
15546
|
+
}
|
|
15547
|
+
if (table.GlobalSecondaryIndexes && table.GlobalSecondaryIndexes.length > 0) {
|
|
15548
|
+
result["GlobalSecondaryIndexes"] = table.GlobalSecondaryIndexes;
|
|
15549
|
+
}
|
|
15550
|
+
if (table.LocalSecondaryIndexes && table.LocalSecondaryIndexes.length > 0) {
|
|
15551
|
+
result["LocalSecondaryIndexes"] = table.LocalSecondaryIndexes;
|
|
15552
|
+
}
|
|
15553
|
+
if (table.SSEDescription) {
|
|
15554
|
+
const sse = {};
|
|
15555
|
+
if (table.SSEDescription.Status === "ENABLED")
|
|
15556
|
+
sse["SSEEnabled"] = true;
|
|
15557
|
+
if (table.SSEDescription.KMSMasterKeyArn !== void 0) {
|
|
15558
|
+
sse["KMSMasterKeyId"] = table.SSEDescription.KMSMasterKeyArn;
|
|
15559
|
+
}
|
|
15560
|
+
if (table.SSEDescription.SSEType !== void 0)
|
|
15561
|
+
sse["SSEType"] = table.SSEDescription.SSEType;
|
|
15562
|
+
if (Object.keys(sse).length > 0)
|
|
15563
|
+
result["SSESpecification"] = sse;
|
|
15564
|
+
}
|
|
15565
|
+
if (table.DeletionProtectionEnabled !== void 0) {
|
|
15566
|
+
result["DeletionProtectionEnabled"] = table.DeletionProtectionEnabled;
|
|
15567
|
+
}
|
|
15568
|
+
if (table.TableClassSummary?.TableClass) {
|
|
15569
|
+
result["TableClass"] = table.TableClassSummary.TableClass;
|
|
15570
|
+
}
|
|
15571
|
+
return result;
|
|
15572
|
+
} catch (err) {
|
|
15573
|
+
if (err instanceof ResourceNotFoundException6)
|
|
15574
|
+
return void 0;
|
|
15575
|
+
throw err;
|
|
15576
|
+
}
|
|
15577
|
+
}
|
|
15058
15578
|
/**
|
|
15059
15579
|
* Adopt an existing DynamoDB table into cdkd state.
|
|
15060
15580
|
*
|
|
@@ -15359,6 +15879,48 @@ var LogsLogGroupProvider = class {
|
|
|
15359
15879
|
}
|
|
15360
15880
|
return this.buildArn(physicalId);
|
|
15361
15881
|
}
|
|
15882
|
+
/**
|
|
15883
|
+
* Read the AWS-current log group configuration in CFn-property shape.
|
|
15884
|
+
*
|
|
15885
|
+
* Issues `DescribeLogGroups` filtered by exact name and picks the first
|
|
15886
|
+
* (and only) match. AWS uses camelCase field names in the API response
|
|
15887
|
+
* (`logGroupName`, `kmsKeyId`, `retentionInDays`); we map them back to
|
|
15888
|
+
* the CFn-cased keys cdkd state holds (`LogGroupName`, `KmsKeyId`,
|
|
15889
|
+
* `RetentionInDays`).
|
|
15890
|
+
*
|
|
15891
|
+
* Coverage: `LogGroupName`, `KmsKeyId`, `RetentionInDays`,
|
|
15892
|
+
* `LogGroupClass`. Other handledProperties (`DataProtectionPolicy`,
|
|
15893
|
+
* `Tags`, `FieldIndexPolicies`, `ResourcePolicyDocument`,
|
|
15894
|
+
* `DeletionProtectionEnabled`, `BearerTokenAuthenticationEnabled`) need
|
|
15895
|
+
* their own per-property API call and are out of scope for v1.
|
|
15896
|
+
*
|
|
15897
|
+
* Returns `undefined` when the log group is gone.
|
|
15898
|
+
*/
|
|
15899
|
+
async readCurrentState(physicalId, _logicalId, _resourceType) {
|
|
15900
|
+
try {
|
|
15901
|
+
const resp = await this.logsClient.send(
|
|
15902
|
+
new DescribeLogGroupsCommand({ logGroupNamePrefix: physicalId })
|
|
15903
|
+
);
|
|
15904
|
+
const found = resp.logGroups?.find((g) => g.logGroupName === physicalId);
|
|
15905
|
+
if (!found)
|
|
15906
|
+
return void 0;
|
|
15907
|
+
const result = {};
|
|
15908
|
+
if (found.logGroupName !== void 0)
|
|
15909
|
+
result["LogGroupName"] = found.logGroupName;
|
|
15910
|
+
if (found.kmsKeyId !== void 0)
|
|
15911
|
+
result["KmsKeyId"] = found.kmsKeyId;
|
|
15912
|
+
if (found.retentionInDays !== void 0) {
|
|
15913
|
+
result["RetentionInDays"] = found.retentionInDays;
|
|
15914
|
+
}
|
|
15915
|
+
if (found.logGroupClass !== void 0)
|
|
15916
|
+
result["LogGroupClass"] = found.logGroupClass;
|
|
15917
|
+
return result;
|
|
15918
|
+
} catch (err) {
|
|
15919
|
+
if (err instanceof ResourceNotFoundException7)
|
|
15920
|
+
return void 0;
|
|
15921
|
+
throw err;
|
|
15922
|
+
}
|
|
15923
|
+
}
|
|
15362
15924
|
/**
|
|
15363
15925
|
* Adopt an existing CloudWatch Logs log group into cdkd state.
|
|
15364
15926
|
*
|
|
@@ -33504,6 +34066,7 @@ function createDiffCommand() {
|
|
|
33504
34066
|
}
|
|
33505
34067
|
|
|
33506
34068
|
// src/cli/commands/drift.ts
|
|
34069
|
+
import * as readline from "node:readline/promises";
|
|
33507
34070
|
import { Command as Command6, Option as Option3 } from "commander";
|
|
33508
34071
|
init_aws_clients();
|
|
33509
34072
|
|
|
@@ -33579,6 +34142,11 @@ async function driftCommand(stacks, options) {
|
|
|
33579
34142
|
if (!options.all && stacks.length === 0) {
|
|
33580
34143
|
throw new Error("Stack name is required. Usage: cdkd drift <stack> [<stack>...] | --all");
|
|
33581
34144
|
}
|
|
34145
|
+
if (options.accept && options.revert) {
|
|
34146
|
+
throw new Error(
|
|
34147
|
+
"--accept and --revert are mutually exclusive. Use --accept to update cdkd state from AWS, or --revert to push cdkd state values back into AWS."
|
|
34148
|
+
);
|
|
34149
|
+
}
|
|
33582
34150
|
await applyRoleArnIfSet({ roleArn: options.roleArn, region: options.region });
|
|
33583
34151
|
const awsClients = new AwsClients({
|
|
33584
34152
|
...options.region && { region: options.region },
|
|
@@ -33589,14 +34157,11 @@ async function driftCommand(stacks, options) {
|
|
|
33589
34157
|
const region = options.region || process.env["AWS_REGION"] || "us-east-1";
|
|
33590
34158
|
const bucket = await resolveStateBucketWithDefault(options.stateBucket, region);
|
|
33591
34159
|
const prefix = options.statePrefix;
|
|
33592
|
-
const
|
|
33593
|
-
|
|
33594
|
-
|
|
33595
|
-
{
|
|
33596
|
-
|
|
33597
|
-
...options.profile && { profile: options.profile }
|
|
33598
|
-
}
|
|
33599
|
-
);
|
|
34160
|
+
const stateConfig = { bucket, prefix };
|
|
34161
|
+
const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
|
|
34162
|
+
region,
|
|
34163
|
+
...options.profile && { profile: options.profile }
|
|
34164
|
+
});
|
|
33600
34165
|
await stateBackend.verifyBucketExists();
|
|
33601
34166
|
const providerRegistry = new ProviderRegistry();
|
|
33602
34167
|
registerAllProviders(providerRegistry);
|
|
@@ -33624,8 +34189,22 @@ async function driftCommand(stacks, options) {
|
|
|
33624
34189
|
writeHumanReport(reports);
|
|
33625
34190
|
}
|
|
33626
34191
|
const drifted = reports.some((r) => r.outcomes.some((o) => o.kind === "drifted"));
|
|
33627
|
-
if (
|
|
33628
|
-
|
|
34192
|
+
if (!options.accept && !options.revert) {
|
|
34193
|
+
if (drifted) {
|
|
34194
|
+
throw new DriftDetectedError();
|
|
34195
|
+
}
|
|
34196
|
+
return;
|
|
34197
|
+
}
|
|
34198
|
+
if (!drifted) {
|
|
34199
|
+
logger.info(
|
|
34200
|
+
options.accept ? "No drift detected \u2014 nothing to accept." : "No drift detected \u2014 nothing to revert."
|
|
34201
|
+
);
|
|
34202
|
+
return;
|
|
34203
|
+
}
|
|
34204
|
+
if (options.accept) {
|
|
34205
|
+
await runAccept(reports, stateBackend, stateConfig, awsClients, options);
|
|
34206
|
+
} else {
|
|
34207
|
+
await runRevert(reports, providerRegistry, stateConfig, awsClients, options);
|
|
33629
34208
|
}
|
|
33630
34209
|
} finally {
|
|
33631
34210
|
awsClients.destroy();
|
|
@@ -33726,13 +34305,261 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
|
|
|
33726
34305
|
kind: "drifted",
|
|
33727
34306
|
logicalId,
|
|
33728
34307
|
resourceType: resource.resourceType,
|
|
33729
|
-
changes
|
|
34308
|
+
changes,
|
|
34309
|
+
awsProperties: aws
|
|
33730
34310
|
});
|
|
33731
34311
|
}
|
|
33732
34312
|
}
|
|
33733
|
-
return {
|
|
34313
|
+
return {
|
|
34314
|
+
stackName,
|
|
34315
|
+
region,
|
|
34316
|
+
outcomes,
|
|
34317
|
+
state,
|
|
34318
|
+
etag: result.etag,
|
|
34319
|
+
migrationPending: result.migrationPending ?? false
|
|
34320
|
+
};
|
|
33734
34321
|
});
|
|
33735
34322
|
}
|
|
34323
|
+
function setAtPath(target, path, value) {
|
|
34324
|
+
if (path.length === 0) {
|
|
34325
|
+
return;
|
|
34326
|
+
}
|
|
34327
|
+
const segments = path.split(".");
|
|
34328
|
+
let cursor = target;
|
|
34329
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
34330
|
+
const key = segments[i];
|
|
34331
|
+
const next = cursor[key];
|
|
34332
|
+
if (next === void 0 || next === null || typeof next !== "object" || Array.isArray(next)) {
|
|
34333
|
+
const fresh = {};
|
|
34334
|
+
cursor[key] = fresh;
|
|
34335
|
+
cursor = fresh;
|
|
34336
|
+
} else {
|
|
34337
|
+
cursor = next;
|
|
34338
|
+
}
|
|
34339
|
+
}
|
|
34340
|
+
cursor[segments[segments.length - 1]] = value;
|
|
34341
|
+
}
|
|
34342
|
+
async function runAccept(reports, stateBackend, stateConfig, awsClients, options) {
|
|
34343
|
+
const logger = getLogger();
|
|
34344
|
+
printAcceptPlan(reports);
|
|
34345
|
+
if (options.dryRun) {
|
|
34346
|
+
logger.info("--dry-run: state will NOT be written. Re-run without --dry-run to apply.");
|
|
34347
|
+
return;
|
|
34348
|
+
}
|
|
34349
|
+
if (!options.yes) {
|
|
34350
|
+
const ok = await confirmPrompt(`Update cdkd state with the AWS-current values shown above?`);
|
|
34351
|
+
if (!ok) {
|
|
34352
|
+
logger.info("Aborted.");
|
|
34353
|
+
return;
|
|
34354
|
+
}
|
|
34355
|
+
}
|
|
34356
|
+
const lockManager = new LockManager(awsClients.s3, stateConfig);
|
|
34357
|
+
const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
|
|
34358
|
+
for (const report of reports) {
|
|
34359
|
+
const driftedOutcomes = report.outcomes.filter(
|
|
34360
|
+
(o) => o.kind === "drifted"
|
|
34361
|
+
);
|
|
34362
|
+
if (driftedOutcomes.length === 0) {
|
|
34363
|
+
continue;
|
|
34364
|
+
}
|
|
34365
|
+
await lockManager.acquireLock(report.stackName, report.region, owner, "drift-accept");
|
|
34366
|
+
try {
|
|
34367
|
+
const resources = { ...report.state.resources };
|
|
34368
|
+
for (const outcome of driftedOutcomes) {
|
|
34369
|
+
const existing = resources[outcome.logicalId];
|
|
34370
|
+
if (!existing)
|
|
34371
|
+
continue;
|
|
34372
|
+
const newProperties = JSON.parse(JSON.stringify(existing.properties ?? {}));
|
|
34373
|
+
for (const change of outcome.changes) {
|
|
34374
|
+
setAtPath(newProperties, change.path, change.awsValue);
|
|
34375
|
+
}
|
|
34376
|
+
resources[outcome.logicalId] = {
|
|
34377
|
+
...existing,
|
|
34378
|
+
properties: newProperties
|
|
34379
|
+
};
|
|
34380
|
+
}
|
|
34381
|
+
const newState = {
|
|
34382
|
+
...report.state,
|
|
34383
|
+
resources,
|
|
34384
|
+
lastModified: Date.now()
|
|
34385
|
+
};
|
|
34386
|
+
const saveOptions = {
|
|
34387
|
+
expectedEtag: report.etag
|
|
34388
|
+
};
|
|
34389
|
+
if (report.migrationPending) {
|
|
34390
|
+
saveOptions.migrateLegacy = true;
|
|
34391
|
+
}
|
|
34392
|
+
await stateBackend.saveState(report.stackName, report.region, newState, saveOptions);
|
|
34393
|
+
logger.info(
|
|
34394
|
+
`\u2713 State updated for ${report.stackName} (${report.region}): accepted drift on ${driftedOutcomes.length} resource(s).`
|
|
34395
|
+
);
|
|
34396
|
+
} finally {
|
|
34397
|
+
await lockManager.releaseLock(report.stackName, report.region).catch((err) => {
|
|
34398
|
+
logger.warn(
|
|
34399
|
+
`Failed to release lock for ${report.stackName} (${report.region}): ` + (err instanceof Error ? err.message : String(err))
|
|
34400
|
+
);
|
|
34401
|
+
});
|
|
34402
|
+
}
|
|
34403
|
+
}
|
|
34404
|
+
}
|
|
34405
|
+
async function runRevert(reports, providerRegistry, stateConfig, awsClients, options) {
|
|
34406
|
+
const logger = getLogger();
|
|
34407
|
+
printRevertPlan(reports);
|
|
34408
|
+
if (options.dryRun) {
|
|
34409
|
+
logger.info("--dry-run: AWS will NOT be modified. Re-run without --dry-run to apply.");
|
|
34410
|
+
return;
|
|
34411
|
+
}
|
|
34412
|
+
if (!options.yes) {
|
|
34413
|
+
const ok = await confirmPrompt(
|
|
34414
|
+
`Push cdkd state values back into AWS for the resources shown above?`
|
|
34415
|
+
);
|
|
34416
|
+
if (!ok) {
|
|
34417
|
+
logger.info("Aborted.");
|
|
34418
|
+
return;
|
|
34419
|
+
}
|
|
34420
|
+
}
|
|
34421
|
+
const lockManager = new LockManager(awsClients.s3, stateConfig);
|
|
34422
|
+
const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
|
|
34423
|
+
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
34424
|
+
let totalFailed = 0;
|
|
34425
|
+
let totalSucceeded = 0;
|
|
34426
|
+
for (const report of reports) {
|
|
34427
|
+
const driftedOutcomes = report.outcomes.filter(
|
|
34428
|
+
(o) => o.kind === "drifted"
|
|
34429
|
+
);
|
|
34430
|
+
if (driftedOutcomes.length === 0) {
|
|
34431
|
+
continue;
|
|
34432
|
+
}
|
|
34433
|
+
await lockManager.acquireLock(report.stackName, report.region, owner, "drift-revert");
|
|
34434
|
+
try {
|
|
34435
|
+
const tasks = driftedOutcomes.map((outcome) => async () => {
|
|
34436
|
+
const stateResource = report.state.resources[outcome.logicalId];
|
|
34437
|
+
if (!stateResource) {
|
|
34438
|
+
totalFailed++;
|
|
34439
|
+
logger.error(
|
|
34440
|
+
` \u2717 ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): resource missing from state; skipped.`
|
|
34441
|
+
);
|
|
34442
|
+
return;
|
|
34443
|
+
}
|
|
34444
|
+
const provider = providerRegistry.getProvider(outcome.resourceType);
|
|
34445
|
+
try {
|
|
34446
|
+
await withRetry(
|
|
34447
|
+
() => provider.update(
|
|
34448
|
+
outcome.logicalId,
|
|
34449
|
+
stateResource.physicalId,
|
|
34450
|
+
outcome.resourceType,
|
|
34451
|
+
stateResource.properties ?? {},
|
|
34452
|
+
outcome.awsProperties
|
|
34453
|
+
),
|
|
34454
|
+
outcome.logicalId,
|
|
34455
|
+
{ logger: { debug: (msg) => logger.debug(msg) } }
|
|
34456
|
+
);
|
|
34457
|
+
totalSucceeded++;
|
|
34458
|
+
logger.info(
|
|
34459
|
+
` \u2713 ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): reverted.`
|
|
34460
|
+
);
|
|
34461
|
+
} catch (err) {
|
|
34462
|
+
totalFailed++;
|
|
34463
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
34464
|
+
logger.error(
|
|
34465
|
+
` \u2717 ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): ${msg}`
|
|
34466
|
+
);
|
|
34467
|
+
}
|
|
34468
|
+
});
|
|
34469
|
+
await runWithConcurrency(tasks, concurrency);
|
|
34470
|
+
} finally {
|
|
34471
|
+
await lockManager.releaseLock(report.stackName, report.region).catch((err) => {
|
|
34472
|
+
logger.warn(
|
|
34473
|
+
`Failed to release lock for ${report.stackName} (${report.region}): ` + (err instanceof Error ? err.message : String(err))
|
|
34474
|
+
);
|
|
34475
|
+
});
|
|
34476
|
+
}
|
|
34477
|
+
}
|
|
34478
|
+
logger.info(`
|
|
34479
|
+
Revert summary: ${totalSucceeded} reverted, ${totalFailed} failed.`);
|
|
34480
|
+
if (totalFailed > 0) {
|
|
34481
|
+
throw new PartialFailureError(
|
|
34482
|
+
`Revert completed with ${totalFailed} resource error(s). Re-run 'cdkd drift <stack>' to see the remaining drift, then 'cdkd drift <stack> --revert' to retry.`
|
|
34483
|
+
);
|
|
34484
|
+
}
|
|
34485
|
+
}
|
|
34486
|
+
function printAcceptPlan(reports) {
|
|
34487
|
+
for (const report of reports) {
|
|
34488
|
+
const drifted = report.outcomes.filter(
|
|
34489
|
+
(o) => o.kind === "drifted"
|
|
34490
|
+
);
|
|
34491
|
+
if (drifted.length === 0)
|
|
34492
|
+
continue;
|
|
34493
|
+
process.stdout.write(
|
|
34494
|
+
`
|
|
34495
|
+
Plan (--accept): update cdkd state for ${report.stackName} (${report.region}):
|
|
34496
|
+
`
|
|
34497
|
+
);
|
|
34498
|
+
for (const o of drifted) {
|
|
34499
|
+
process.stdout.write(` ~ ${o.logicalId} (${o.resourceType})
|
|
34500
|
+
`);
|
|
34501
|
+
for (const change of o.changes) {
|
|
34502
|
+
process.stdout.write(
|
|
34503
|
+
` ${change.path}: ${formatScalar(change.stateValue)} -> ${formatScalar(change.awsValue)}
|
|
34504
|
+
`
|
|
34505
|
+
);
|
|
34506
|
+
}
|
|
34507
|
+
}
|
|
34508
|
+
}
|
|
34509
|
+
}
|
|
34510
|
+
function printRevertPlan(reports) {
|
|
34511
|
+
for (const report of reports) {
|
|
34512
|
+
const drifted = report.outcomes.filter(
|
|
34513
|
+
(o) => o.kind === "drifted"
|
|
34514
|
+
);
|
|
34515
|
+
if (drifted.length === 0)
|
|
34516
|
+
continue;
|
|
34517
|
+
process.stdout.write(
|
|
34518
|
+
`
|
|
34519
|
+
Plan (--revert): push cdkd state values back into AWS for ${report.stackName} (${report.region}):
|
|
34520
|
+
`
|
|
34521
|
+
);
|
|
34522
|
+
for (const o of drifted) {
|
|
34523
|
+
const word = o.changes.length === 1 ? "property path" : "property paths";
|
|
34524
|
+
process.stdout.write(
|
|
34525
|
+
` \u2192 provider.update on ${o.logicalId} (${o.resourceType}): revert ${o.changes.length} ${word}
|
|
34526
|
+
`
|
|
34527
|
+
);
|
|
34528
|
+
for (const change of o.changes) {
|
|
34529
|
+
process.stdout.write(
|
|
34530
|
+
` ${change.path}: ${formatScalar(change.awsValue)} -> ${formatScalar(change.stateValue)}
|
|
34531
|
+
`
|
|
34532
|
+
);
|
|
34533
|
+
}
|
|
34534
|
+
}
|
|
34535
|
+
}
|
|
34536
|
+
}
|
|
34537
|
+
async function runWithConcurrency(tasks, concurrency) {
|
|
34538
|
+
const queue = [...tasks];
|
|
34539
|
+
const workers = [];
|
|
34540
|
+
for (let i = 0; i < Math.min(concurrency, queue.length); i++) {
|
|
34541
|
+
workers.push(
|
|
34542
|
+
(async () => {
|
|
34543
|
+
while (queue.length > 0) {
|
|
34544
|
+
const task = queue.shift();
|
|
34545
|
+
if (!task)
|
|
34546
|
+
break;
|
|
34547
|
+
await task();
|
|
34548
|
+
}
|
|
34549
|
+
})()
|
|
34550
|
+
);
|
|
34551
|
+
}
|
|
34552
|
+
await Promise.all(workers);
|
|
34553
|
+
}
|
|
34554
|
+
async function confirmPrompt(prompt) {
|
|
34555
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
34556
|
+
try {
|
|
34557
|
+
const ans = await rl.question(`${prompt} [y/N] `);
|
|
34558
|
+
return /^y(es)?$/i.test(ans.trim());
|
|
34559
|
+
} finally {
|
|
34560
|
+
rl.close();
|
|
34561
|
+
}
|
|
34562
|
+
}
|
|
33736
34563
|
function writeJsonReport(reports) {
|
|
33737
34564
|
const payload = reports.map((r) => {
|
|
33738
34565
|
const drifted = r.outcomes.filter((o) => o.kind === "drifted").map((o) => ({ logicalId: o.logicalId, type: o.resourceType, changes: o.changes }));
|
|
@@ -33809,8 +34636,25 @@ function stackRegionOption() {
|
|
|
33809
34636
|
}
|
|
33810
34637
|
function createDriftCommand() {
|
|
33811
34638
|
const cmd = new Command6("drift").description(
|
|
33812
|
-
"Detect drift between cdkd state and AWS reality. Exits 0 when no drift, 1 when drift is detected."
|
|
33813
|
-
).argument("[stacks...]", "Stack name(s) to check (physical CloudFormation names)").option("--all", "Check every stack in the state bucket", false).option("--json", "Output as JSON", false).
|
|
34639
|
+
"Detect drift between cdkd state and AWS reality. Exits 0 when no drift, 1 when drift is detected. Pass --accept to update cdkd state from AWS, or --revert to push cdkd state values back into AWS."
|
|
34640
|
+
).argument("[stacks...]", "Stack name(s) to check (physical CloudFormation names)").option("--all", "Check every stack in the state bucket", false).option("--json", "Output as JSON", false).option(
|
|
34641
|
+
"--accept",
|
|
34642
|
+
"Update cdkd state with the AWS-current values for every drifted property (state \u2190 AWS). Mutually exclusive with --revert.",
|
|
34643
|
+
false
|
|
34644
|
+
).option(
|
|
34645
|
+
"--revert",
|
|
34646
|
+
"Push cdkd state values back into AWS via provider.update for every drifted resource (AWS \u2190 state). Mutually exclusive with --accept.",
|
|
34647
|
+
false
|
|
34648
|
+
).option(
|
|
34649
|
+
"--dry-run",
|
|
34650
|
+
"Print the planned mutations without acquiring a lock or hitting AWS / S3. Honored by --accept and --revert.",
|
|
34651
|
+
false
|
|
34652
|
+
).option(
|
|
34653
|
+
"--concurrency <number>",
|
|
34654
|
+
"Maximum concurrent provider.update calls during --revert",
|
|
34655
|
+
(value) => parseInt(value, 10),
|
|
34656
|
+
4
|
|
34657
|
+
).addOption(stackRegionOption()).action(withErrorHandling(driftCommand));
|
|
33814
34658
|
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
33815
34659
|
cmd.addOption(deprecatedRegionOption);
|
|
33816
34660
|
return cmd;
|
|
@@ -33821,7 +34665,7 @@ import { Command as Command7 } from "commander";
|
|
|
33821
34665
|
init_aws_clients();
|
|
33822
34666
|
|
|
33823
34667
|
// src/cli/commands/destroy-runner.ts
|
|
33824
|
-
import * as
|
|
34668
|
+
import * as readline2 from "node:readline/promises";
|
|
33825
34669
|
init_aws_clients();
|
|
33826
34670
|
async function runDestroyForStack(stackName, state, ctx) {
|
|
33827
34671
|
const logger = getLogger();
|
|
@@ -33847,7 +34691,7 @@ Resources to be deleted (${resourceCount}):`);
|
|
|
33847
34691
|
logger.info(` - ${logicalId} (${resource.resourceType})`);
|
|
33848
34692
|
}
|
|
33849
34693
|
if (!ctx.skipConfirmation) {
|
|
33850
|
-
const rl =
|
|
34694
|
+
const rl = readline2.createInterface({
|
|
33851
34695
|
input: process.stdin,
|
|
33852
34696
|
output: process.stdout
|
|
33853
34697
|
});
|
|
@@ -34258,7 +35102,7 @@ function createDestroyCommand() {
|
|
|
34258
35102
|
}
|
|
34259
35103
|
|
|
34260
35104
|
// src/cli/commands/orphan.ts
|
|
34261
|
-
import * as
|
|
35105
|
+
import * as readline3 from "node:readline/promises";
|
|
34262
35106
|
import { Command as Command8 } from "commander";
|
|
34263
35107
|
init_aws_clients();
|
|
34264
35108
|
|
|
@@ -34791,7 +35635,7 @@ Re-run with --force to fall back to cached attribute values from state, or fix t
|
|
|
34791
35635
|
return;
|
|
34792
35636
|
}
|
|
34793
35637
|
if (!options.yes && !options.force) {
|
|
34794
|
-
const ok = await
|
|
35638
|
+
const ok = await confirmPrompt2(
|
|
34795
35639
|
`Orphan ${orphanLogicalIds.length} resource(s) from cdkd state for ${stackInfo.stackName} (${targetRegion})? AWS resources will NOT be deleted.`
|
|
34796
35640
|
);
|
|
34797
35641
|
if (!ok) {
|
|
@@ -34930,8 +35774,8 @@ function stringifyForAudit(value) {
|
|
|
34930
35774
|
return JSON.stringify(value);
|
|
34931
35775
|
return JSON.stringify(value);
|
|
34932
35776
|
}
|
|
34933
|
-
async function
|
|
34934
|
-
const rl =
|
|
35777
|
+
async function confirmPrompt2(prompt) {
|
|
35778
|
+
const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
|
|
34935
35779
|
try {
|
|
34936
35780
|
const ans = await rl.question(`${prompt} [y/N] `);
|
|
34937
35781
|
return /^y(es)?$/i.test(ans.trim());
|
|
@@ -35206,7 +36050,7 @@ function createForceUnlockCommand() {
|
|
|
35206
36050
|
}
|
|
35207
36051
|
|
|
35208
36052
|
// src/cli/commands/state.ts
|
|
35209
|
-
import * as
|
|
36053
|
+
import * as readline5 from "node:readline/promises";
|
|
35210
36054
|
import { Command as Command12, Option as Option6 } from "commander";
|
|
35211
36055
|
import {
|
|
35212
36056
|
GetBucketLocationCommand as GetBucketLocationCommand2,
|
|
@@ -35216,7 +36060,7 @@ import {
|
|
|
35216
36060
|
init_aws_clients();
|
|
35217
36061
|
|
|
35218
36062
|
// src/cli/commands/state-migrate.ts
|
|
35219
|
-
import * as
|
|
36063
|
+
import * as readline4 from "node:readline/promises";
|
|
35220
36064
|
import { Command as Command11 } from "commander";
|
|
35221
36065
|
import {
|
|
35222
36066
|
CopyObjectCommand,
|
|
@@ -35286,7 +36130,7 @@ async function stateMigrateCommand(options) {
|
|
|
35286
36130
|
}
|
|
35287
36131
|
if (!options.yes) {
|
|
35288
36132
|
const action = options.removeLegacy ? "and DELETE the source bucket" : "(source bucket will be kept)";
|
|
35289
|
-
const ok = await
|
|
36133
|
+
const ok = await confirmPrompt3(
|
|
35290
36134
|
`Copy ${sourceObjects.length} object(s) from ${legacyBucket} -> ${newBucket} ${action}?`
|
|
35291
36135
|
);
|
|
35292
36136
|
if (!ok) {
|
|
@@ -35491,8 +36335,8 @@ async function emptyBucketAllVersions(s3, bucket) {
|
|
|
35491
36335
|
versionIdMarker = resp.NextVersionIdMarker;
|
|
35492
36336
|
} while (keyMarker || versionIdMarker);
|
|
35493
36337
|
}
|
|
35494
|
-
async function
|
|
35495
|
-
const rl =
|
|
36338
|
+
async function confirmPrompt3(prompt) {
|
|
36339
|
+
const rl = readline4.createInterface({ input: process.stdin, output: process.stdout });
|
|
35496
36340
|
try {
|
|
35497
36341
|
const ans = await rl.question(`${prompt} [y/N] `);
|
|
35498
36342
|
return /^y(es)?$/i.test(ans.trim());
|
|
@@ -35900,7 +36744,7 @@ Use 'cdkd destroy ${stackName}' if you want to delete the actual resources.
|
|
|
35900
36744
|
|
|
35901
36745
|
`
|
|
35902
36746
|
);
|
|
35903
|
-
const rl =
|
|
36747
|
+
const rl = readline5.createInterface({
|
|
35904
36748
|
input: process.stdin,
|
|
35905
36749
|
output: process.stdout
|
|
35906
36750
|
});
|
|
@@ -35991,7 +36835,7 @@ WARNING: This destroys ${stackNames.length} stack(s) and removes their state rec
|
|
|
35991
36835
|
`);
|
|
35992
36836
|
}
|
|
35993
36837
|
process.stdout.write("\n");
|
|
35994
|
-
const rl =
|
|
36838
|
+
const rl = readline5.createInterface({
|
|
35995
36839
|
input: process.stdin,
|
|
35996
36840
|
output: process.stdout
|
|
35997
36841
|
});
|
|
@@ -36233,12 +37077,12 @@ function createStateCommand() {
|
|
|
36233
37077
|
|
|
36234
37078
|
// src/cli/commands/import.ts
|
|
36235
37079
|
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
36236
|
-
import * as
|
|
37080
|
+
import * as readline7 from "node:readline/promises";
|
|
36237
37081
|
import { Command as Command13 } from "commander";
|
|
36238
37082
|
init_aws_clients();
|
|
36239
37083
|
|
|
36240
37084
|
// src/cli/commands/retire-cfn-stack.ts
|
|
36241
|
-
import * as
|
|
37085
|
+
import * as readline6 from "node:readline/promises";
|
|
36242
37086
|
import {
|
|
36243
37087
|
DescribeStacksCommand,
|
|
36244
37088
|
DescribeStackResourcesCommand,
|
|
@@ -36283,7 +37127,7 @@ async function retireCloudFormationStack(options) {
|
|
|
36283
37127
|
}
|
|
36284
37128
|
const { body: newBody, modified } = injectRetainPolicies(tpl.TemplateBody, cfnStackName);
|
|
36285
37129
|
if (!yes) {
|
|
36286
|
-
const ok = await
|
|
37130
|
+
const ok = await confirmPrompt4(
|
|
36287
37131
|
`Set DeletionPolicy=Retain and UpdateReplacePolicy=Retain on every resource in CloudFormation stack '${cfnStackName}', then delete the stack? AWS resources will NOT be deleted (cdkd state has been written).`
|
|
36288
37132
|
);
|
|
36289
37133
|
if (!ok) {
|
|
@@ -36443,8 +37287,8 @@ async function getCloudFormationResourceMapping(cfnStackName, cfnClient) {
|
|
|
36443
37287
|
}
|
|
36444
37288
|
return map;
|
|
36445
37289
|
}
|
|
36446
|
-
async function
|
|
36447
|
-
const rl =
|
|
37290
|
+
async function confirmPrompt4(prompt) {
|
|
37291
|
+
const rl = readline6.createInterface({ input: process.stdin, output: process.stdout });
|
|
36448
37292
|
try {
|
|
36449
37293
|
const ans = await rl.question(`${prompt} [y/N] `);
|
|
36450
37294
|
return /^y(es)?$/i.test(ans.trim());
|
|
@@ -36643,7 +37487,7 @@ async function importCommand(stackArg, options) {
|
|
|
36643
37487
|
const preservedCount = selectiveMode && existingState ? Object.keys(existingState.resources).filter((id) => !overrides.has(id)).length : 0;
|
|
36644
37488
|
const totalAfter = importedCount + preservedCount;
|
|
36645
37489
|
const breakdown = preservedCount > 0 ? ` (${importedCount} new/overwritten + ${preservedCount} preserved)` : "";
|
|
36646
|
-
const ok = await
|
|
37490
|
+
const ok = await confirmPrompt5(
|
|
36647
37491
|
`Write state for ${stackInfo.stackName} (${targetRegion}) with ${totalAfter} resource(s)${breakdown}?`
|
|
36648
37492
|
);
|
|
36649
37493
|
if (!ok) {
|
|
@@ -36904,8 +37748,8 @@ function formatOutcome(outcome) {
|
|
|
36904
37748
|
return "\u2717";
|
|
36905
37749
|
}
|
|
36906
37750
|
}
|
|
36907
|
-
async function
|
|
36908
|
-
const rl =
|
|
37751
|
+
async function confirmPrompt5(prompt) {
|
|
37752
|
+
const rl = readline7.createInterface({ input: process.stdin, output: process.stdout });
|
|
36909
37753
|
try {
|
|
36910
37754
|
const ans = await rl.question(`${prompt} [y/N] `);
|
|
36911
37755
|
return /^y(es)?$/i.test(ans.trim());
|
|
@@ -36982,7 +37826,7 @@ function reorderArgs(argv) {
|
|
|
36982
37826
|
}
|
|
36983
37827
|
async function main() {
|
|
36984
37828
|
const program = new Command14();
|
|
36985
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
37829
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.38.0");
|
|
36986
37830
|
program.addCommand(createBootstrapCommand());
|
|
36987
37831
|
program.addCommand(createSynthCommand());
|
|
36988
37832
|
program.addCommand(createListCommand());
|