@kensio/yulin 1.21.15 → 1.21.17
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/sdk/router/sim-sdk-command-router.type.d.ts +9 -0
- package/dist/sdk/sim-sdk-command-dispatcher.js +1 -1
- package/dist/service/cloudformation/changeset/sim-cfn-change-set-changes.js +1 -1
- package/dist/service/cloudformation/resource/factory/sim-cfn-resource-factory.type.d.ts +25 -1
- package/dist/service/cloudformation/resource/resolve/service/sim-cfn-resource-service-factory.d.ts +18 -0
- package/dist/service/cloudformation/resource/resolve/service/sim-cfn-resource-service-factory.js +30 -0
- package/dist/service/cloudformation/resource/sim-cfn-resource.type.d.ts +18 -0
- package/dist/service/cloudformation/resource/update/sim-cfn-resource-in-place-updater.d.ts +35 -0
- package/dist/service/cloudformation/resource/update/sim-cfn-resource-in-place-updater.js +61 -0
- package/dist/service/cloudformation/resource/update/sim-cfn-resource-update-error.d.ts +9 -0
- package/dist/service/cloudformation/resource/update/sim-cfn-resource-update-error.js +14 -0
- package/dist/service/cloudformation/resource/update/sim-cfn-resource-update-validator.js +5 -18
- package/dist/service/cloudformation/stack/sim-cfn-stack-resource-operations.d.ts +7 -2
- package/dist/service/cloudformation/stack/sim-cfn-stack-resource-operations.js +16 -12
- package/dist/service/cloudformation/stack/update/sim-cfn-stack-resource-change-pairs.d.ts +12 -0
- package/dist/service/cloudformation/stack/update/sim-cfn-stack-resource-change-pairs.js +18 -0
- package/dist/service/cloudformation/stack/update/sim-cfn-stack-resource-changes.d.ts +52 -0
- package/dist/service/cloudformation/stack/update/sim-cfn-stack-resource-changes.js +86 -0
- package/dist/service/cloudformation/stack/update/sim-cfn-stack-resource-updates.d.ts +42 -0
- package/dist/service/cloudformation/stack/update/sim-cfn-stack-resource-updates.js +70 -0
- package/dist/service/cloudformation/stack/update/sim-cfn-stack-update-plan.d.ts +25 -18
- package/dist/service/cloudformation/stack/update/sim-cfn-stack-update-plan.js +43 -32
- package/dist/service/cloudformation/stack/update/sim-cfn-stack-updater.d.ts +7 -5
- package/dist/service/cloudformation/stack/update/sim-cfn-stack-updater.js +12 -7
- package/dist/service/dynamodb/document/sim-dynamodb-document-marshall-options.d.ts +42 -0
- package/dist/service/dynamodb/document/sim-dynamodb-document-marshall-options.js +36 -0
- package/dist/service/dynamodb/document/sim-dynamodb-document-marshall.d.ts +6 -5
- package/dist/service/dynamodb/document/sim-dynamodb-document-marshall.js +37 -43
- package/dist/service/dynamodb/document/sim-dynamodb-document-number.d.ts +4 -2
- package/dist/service/dynamodb/document/sim-dynamodb-document-number.js +5 -3
- package/dist/service/dynamodb/document/sim-dynamodb-document-path.d.ts +0 -4
- package/dist/service/dynamodb/document/sim-dynamodb-document-path.js +17 -10
- package/dist/service/dynamodb/document/sim-dynamodb-document-route.js +5 -1
- package/dist/service/dynamodb/document/sim-dynamodb-document-scalar.d.ts +13 -0
- package/dist/service/dynamodb/document/sim-dynamodb-document-scalar.js +46 -0
- package/dist/service/dynamodb/document/sim-dynamodb-document-set.d.ts +8 -3
- package/dist/service/dynamodb/document/sim-dynamodb-document-set.js +21 -14
- package/dist/service/secretsmanager/cfn/secret/sim-cfn-secrets-manager-secret-change.d.ts +19 -0
- package/dist/service/secretsmanager/cfn/secret/sim-cfn-secrets-manager-secret-change.js +39 -0
- package/dist/service/secretsmanager/cfn/secret/sim-cfn-secrets-manager-secret-deleter.d.ts +23 -0
- package/dist/service/secretsmanager/cfn/secret/sim-cfn-secrets-manager-secret-deleter.js +23 -0
- package/dist/service/secretsmanager/cfn/secret/sim-cfn-secrets-manager-secret-template.test-support.d.ts +21 -0
- package/dist/service/secretsmanager/cfn/secret/sim-cfn-secrets-manager-secret-template.test-support.js +64 -0
- package/dist/service/secretsmanager/cfn/secret/sim-cfn-secrets-manager-secret-update.test-support.d.ts +18 -0
- package/dist/service/secretsmanager/cfn/secret/sim-cfn-secrets-manager-secret-update.test-support.js +39 -0
- package/dist/service/secretsmanager/cfn/secret/sim-cfn-secrets-manager-secret-updater.d.ts +37 -0
- package/dist/service/secretsmanager/cfn/secret/sim-cfn-secrets-manager-secret-updater.js +66 -0
- package/dist/service/secretsmanager/cfn/sim-cfn-secrets-manager-resource-factory.d.ts +20 -10
- package/dist/service/secretsmanager/cfn/sim-cfn-secrets-manager-resource-factory.js +41 -27
- package/dist/service/secretsmanager/cfn/sim-cfn-secrets-manager-resource-type.d.ts +12 -0
- package/dist/service/secretsmanager/cfn/sim-cfn-secrets-manager-resource-type.js +22 -0
- package/docs/sdk/README.md +5 -4
- package/docs/services/cloudformation/README.md +21 -3
- package/docs/services/dynamodb/README.md +81 -2
- package/docs/services/secretsmanager/README.md +35 -1
- package/package.json +1 -1
- package/dist/service/cloudformation/stack/update/sim-cfn-stack-replaced-resources.d.ts +0 -21
- package/dist/service/cloudformation/stack/update/sim-cfn-stack-replaced-resources.js +0 -51
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { isRecord } from "../../../util/type-guard/record.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Every attribute value of an item, a key or a set of expression values.
|
|
4
|
+
*
|
|
5
|
+
* An attribute whose value is undefined is left out rather than converted, and
|
|
6
|
+
* that happens whatever the document client was built with. The real client
|
|
7
|
+
* drops it in the same place: `removeUndefinedValues` governs the values
|
|
8
|
+
* inside an attribute, and an item is not itself one of them.
|
|
4
9
|
*/
|
|
5
|
-
class
|
|
10
|
+
class SimDynamoDbDocumentValuesPath {
|
|
6
11
|
convert(value, conversion, path) {
|
|
7
|
-
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
return value.map((member, index) => conversion(member, `${path}[${index.toString()}]`));
|
|
14
|
+
}
|
|
15
|
+
if (!isRecord(value)) {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
return Object.fromEntries(Object.entries(value)
|
|
19
|
+
.filter(([, member]) => member !== undefined)
|
|
20
|
+
.map(([name, member]) => [name, conversion(member, `${path}.${name}`)]));
|
|
8
21
|
}
|
|
9
22
|
}
|
|
10
23
|
/**
|
|
@@ -55,18 +68,12 @@ class SimDynamoDbDocumentFieldsPath {
|
|
|
55
68
|
return converted;
|
|
56
69
|
}
|
|
57
70
|
}
|
|
58
|
-
/**
|
|
59
|
-
* A path to one attribute value.
|
|
60
|
-
*/
|
|
61
|
-
export function simDynamoDbDocumentValue() {
|
|
62
|
-
return new SimDynamoDbDocumentValuePath();
|
|
63
|
-
}
|
|
64
71
|
/**
|
|
65
72
|
* A path to a record or list whose every member is one attribute value, which
|
|
66
73
|
* is what an Item, a Key and a set of expression values are.
|
|
67
74
|
*/
|
|
68
75
|
export function simDynamoDbDocumentValues() {
|
|
69
|
-
return
|
|
76
|
+
return new SimDynamoDbDocumentValuesPath();
|
|
70
77
|
}
|
|
71
78
|
/**
|
|
72
79
|
* A path through every member of a record or list.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { simSdkCallerOptions, } from "../../../sdk/index.js";
|
|
2
|
+
import { simDynamoDbDocumentMarshallOptions } from "./sim-dynamodb-document-marshall-options.js";
|
|
2
3
|
import { simDynamoDbDocumentAttributeValue } from "./sim-dynamodb-document-marshall.js";
|
|
3
4
|
import { simDynamoDbDocumentNativeValue } from "./sim-dynamodb-document-unmarshall.js";
|
|
4
5
|
/**
|
|
@@ -23,7 +24,10 @@ export class SimDynamoDbDocumentRoute {
|
|
|
23
24
|
*/
|
|
24
25
|
route() {
|
|
25
26
|
return async (command, context) => {
|
|
26
|
-
|
|
27
|
+
// Read per send, as the real middleware reads it, so the same route
|
|
28
|
+
// serves document clients built with different options.
|
|
29
|
+
const options = simDynamoDbDocumentMarshallOptions(context.client);
|
|
30
|
+
const input = this.input.convert(command.input, (value, path) => simDynamoDbDocumentAttributeValue(value, path, options), "input");
|
|
27
31
|
const output = await this.send(input, simSdkCallerOptions(context));
|
|
28
32
|
return this.output.convert(output, (value, path) => simDynamoDbDocumentNativeValue(value, path), "output");
|
|
29
33
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SimDynamoDbAttributeValue } from "../command/item/item.types.js";
|
|
2
|
+
import type { SimDynamoDbDocumentMarshallOptions } from "./sim-dynamodb-document-marshall-options.js";
|
|
3
|
+
/**
|
|
4
|
+
* Read a value that stands for one attribute on its own.
|
|
5
|
+
*
|
|
6
|
+
* The kinds are tried in the order the real conversion tries them. A value
|
|
7
|
+
* that is none of them answers with nothing, and what happens to it next is
|
|
8
|
+
* for the caller to decide.
|
|
9
|
+
*
|
|
10
|
+
* An empty string and a binary value holding no bytes are written as NULL when
|
|
11
|
+
* the client was built with `convertEmptyValues`, and as themselves otherwise.
|
|
12
|
+
*/
|
|
13
|
+
export declare function simDynamoDbDocumentScalarAttribute(value: unknown, path: string, options: SimDynamoDbDocumentMarshallOptions): SimDynamoDbAttributeValue | undefined;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { isSimDynamoDbDocumentBinary } from "./sim-dynamodb-document-binary.js";
|
|
2
|
+
import { isSimDynamoDbDocumentNumberValue, simDynamoDbDocumentNumberAttribute, } from "./sim-dynamodb-document-number.js";
|
|
3
|
+
/**
|
|
4
|
+
* Read a value that stands for one attribute on its own.
|
|
5
|
+
*
|
|
6
|
+
* The kinds are tried in the order the real conversion tries them. A value
|
|
7
|
+
* that is none of them answers with nothing, and what happens to it next is
|
|
8
|
+
* for the caller to decide.
|
|
9
|
+
*
|
|
10
|
+
* An empty string and a binary value holding no bytes are written as NULL when
|
|
11
|
+
* the client was built with `convertEmptyValues`, and as themselves otherwise.
|
|
12
|
+
*/
|
|
13
|
+
export function simDynamoDbDocumentScalarAttribute(value, path, options) {
|
|
14
|
+
if (isSimDynamoDbDocumentBinary(value)) {
|
|
15
|
+
return isEmptyLength(value) && options.convertEmptyValues
|
|
16
|
+
? { NULL: true }
|
|
17
|
+
: { B: value };
|
|
18
|
+
}
|
|
19
|
+
if (typeof value === "boolean") {
|
|
20
|
+
return { BOOL: value };
|
|
21
|
+
}
|
|
22
|
+
if (typeof value === "number") {
|
|
23
|
+
return simDynamoDbDocumentNumberAttribute(value, path, options);
|
|
24
|
+
}
|
|
25
|
+
if (isSimDynamoDbDocumentNumberValue(value)) {
|
|
26
|
+
return { N: value.toAttributeValue().N };
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === "bigint") {
|
|
29
|
+
return { N: value.toString() };
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === "string") {
|
|
32
|
+
return value.length === 0 && options.convertEmptyValues
|
|
33
|
+
? { NULL: true }
|
|
34
|
+
: { S: value };
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Whether a value holds nothing, by the length the real conversion reads.
|
|
40
|
+
*
|
|
41
|
+
* An ArrayBuffer reports `byteLength` rather than `length`, so an empty one is
|
|
42
|
+
* not empty by this measure, and the real conversion leaves it alone too.
|
|
43
|
+
*/
|
|
44
|
+
function isEmptyLength(value) {
|
|
45
|
+
return value.length === 0;
|
|
46
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SimDynamoDbAttributeValue } from "../command/item/item.types.js";
|
|
2
|
+
import type { SimDynamoDbDocumentMarshallOptions } from "./sim-dynamodb-document-marshall-options.js";
|
|
2
3
|
/**
|
|
3
4
|
* Read a JavaScript Set as one of DynamoDB's three set attributes.
|
|
4
5
|
*
|
|
@@ -8,7 +9,11 @@ import type { SimDynamoDbAttributeValue } from "../command/item/item.types.js";
|
|
|
8
9
|
* further down, where the table reads the value. That is what the real one
|
|
9
10
|
* does, so a set written this way behaves the same either side.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* An undefined member is dropped when the client was built with
|
|
13
|
+
* `removeUndefinedValues`, and refused otherwise. DynamoDB has no empty set, so
|
|
14
|
+
* a set with nothing left in it is written as NULL when the client was built
|
|
15
|
+
* with `convertEmptyValues`, and refused otherwise. The two are read in that
|
|
16
|
+
* order, so a set holding nothing but undefined is an empty set by the time its
|
|
17
|
+
* size is looked at, which is where the real conversion looks at it too.
|
|
13
18
|
*/
|
|
14
|
-
export declare function simDynamoDbDocumentSetAttribute(set: ReadonlySet<unknown>, path: string): SimDynamoDbAttributeValue;
|
|
19
|
+
export declare function simDynamoDbDocumentSetAttribute(set: ReadonlySet<unknown>, path: string, options: SimDynamoDbDocumentMarshallOptions): SimDynamoDbAttributeValue;
|
|
@@ -10,31 +10,38 @@ import { isSimDynamoDbDocumentNumberValue, simDynamoDbDocumentNumberAttribute, }
|
|
|
10
10
|
* further down, where the table reads the value. That is what the real one
|
|
11
11
|
* does, so a set written this way behaves the same either side.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* An undefined member is dropped when the client was built with
|
|
14
|
+
* `removeUndefinedValues`, and refused otherwise. DynamoDB has no empty set, so
|
|
15
|
+
* a set with nothing left in it is written as NULL when the client was built
|
|
16
|
+
* with `convertEmptyValues`, and refused otherwise. The two are read in that
|
|
17
|
+
* order, so a set holding nothing but undefined is an empty set by the time its
|
|
18
|
+
* size is looked at, which is where the real conversion looks at it too.
|
|
15
19
|
*/
|
|
16
|
-
export function simDynamoDbDocumentSetAttribute(set, path) {
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
export function simDynamoDbDocumentSetAttribute(set, path, options) {
|
|
21
|
+
const members = [...set].filter((member) => !(member === undefined && options.removeUndefinedValues));
|
|
22
|
+
if (!options.removeUndefinedValues && set.has(undefined)) {
|
|
23
|
+
throw new SimDynamoDbDocumentValueError(`${path} is a Set holding undefined. Build the document client with ` +
|
|
24
|
+
`removeUndefinedValues to drop it, which is what the real one asks for`);
|
|
19
25
|
}
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
if (members.length === 0) {
|
|
27
|
+
if (options.convertEmptyValues) {
|
|
28
|
+
return { NULL: true };
|
|
29
|
+
}
|
|
30
|
+
throw new SimDynamoDbDocumentValueError(`${path} is an empty Set, and DynamoDB has no empty set`);
|
|
24
31
|
}
|
|
25
|
-
return membersAttribute(
|
|
32
|
+
return membersAttribute(members, path, options);
|
|
26
33
|
}
|
|
27
34
|
/**
|
|
28
35
|
* Read the members of a set that is known to hold something.
|
|
29
36
|
*/
|
|
30
|
-
function membersAttribute(members, path) {
|
|
37
|
+
function membersAttribute(members, path, options) {
|
|
31
38
|
const first = members[0];
|
|
32
39
|
if (typeof first === "string") {
|
|
33
40
|
return { SS: members.map(String) };
|
|
34
41
|
}
|
|
35
42
|
if (isNumberMember(first)) {
|
|
36
43
|
return {
|
|
37
|
-
NS: members.map((member, index) => numberText(member, path, index)),
|
|
44
|
+
NS: members.map((member, index) => numberText(member, path, index, options)),
|
|
38
45
|
};
|
|
39
46
|
}
|
|
40
47
|
if (isSimDynamoDbDocumentBinary(first)) {
|
|
@@ -57,14 +64,14 @@ function isNumberMember(member) {
|
|
|
57
64
|
/**
|
|
58
65
|
* The digits one member of a number set is written with.
|
|
59
66
|
*/
|
|
60
|
-
function numberText(member, path, index) {
|
|
67
|
+
function numberText(member, path, index, options) {
|
|
61
68
|
if (typeof member === "bigint") {
|
|
62
69
|
return member.toString();
|
|
63
70
|
}
|
|
64
71
|
if (isSimDynamoDbDocumentNumberValue(member)) {
|
|
65
72
|
return member.toAttributeValue().N;
|
|
66
73
|
}
|
|
67
|
-
const attribute = simDynamoDbDocumentNumberAttribute(Number(member), `${path}[${index.toString()}]
|
|
74
|
+
const attribute = simDynamoDbDocumentNumberAttribute(Number(member), `${path}[${index.toString()}]`, options);
|
|
68
75
|
// A number attribute is the only thing that function answers with.
|
|
69
76
|
return attribute.N ?? "";
|
|
70
77
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SimCfnResource } from "../../../cloudformation/resource/sim-cfn-resource.js";
|
|
2
|
+
import type { SimCfnTemplateValueRecord } from "../../../cloudformation/template/value/sim-cfn-template-value.js";
|
|
3
|
+
/**
|
|
4
|
+
* Whether the change between two definitions of a secret replaces it.
|
|
5
|
+
*
|
|
6
|
+
* `Name` is the only property real CloudFormation replaces a secret for. It is
|
|
7
|
+
* read as the template writes it, before intrinsic resolution, which is all the
|
|
8
|
+
* update plan has to go on. Two different expressions resolving to the same
|
|
9
|
+
* name replace the secret, which is the safe way round to be wrong.
|
|
10
|
+
*/
|
|
11
|
+
export declare function simCfnSecretsManagerSecretReplaced(current: SimCfnResource, updated: SimCfnResource): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Whether the new template asks the secret to hold a different value.
|
|
14
|
+
*
|
|
15
|
+
* A secret takes its value from `SecretString` or `GenerateSecretString`, so a
|
|
16
|
+
* change to either is a new version. A change to anything else leaves the
|
|
17
|
+
* versions alone, which is what keeps a generated password across an update.
|
|
18
|
+
*/
|
|
19
|
+
export declare function simCfnSecretsManagerSecretValueChanged(currentProperties: SimCfnTemplateValueRecord, updatedProperties: SimCfnTemplateValueRecord): boolean;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { simCfnTemplateSignature } from "../../../cloudformation/stack/update/sim-cfn-template-signature.js";
|
|
2
|
+
/**
|
|
3
|
+
* Whether the change between two definitions of a secret replaces it.
|
|
4
|
+
*
|
|
5
|
+
* `Name` is the only property real CloudFormation replaces a secret for. It is
|
|
6
|
+
* read as the template writes it, before intrinsic resolution, which is all the
|
|
7
|
+
* update plan has to go on. Two different expressions resolving to the same
|
|
8
|
+
* name replace the secret, which is the safe way round to be wrong.
|
|
9
|
+
*/
|
|
10
|
+
export function simCfnSecretsManagerSecretReplaced(current, updated) {
|
|
11
|
+
return nameSignature(current) !== nameSignature(updated);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Whether the new template asks the secret to hold a different value.
|
|
15
|
+
*
|
|
16
|
+
* A secret takes its value from `SecretString` or `GenerateSecretString`, so a
|
|
17
|
+
* change to either is a new version. A change to anything else leaves the
|
|
18
|
+
* versions alone, which is what keeps a generated password across an update.
|
|
19
|
+
*/
|
|
20
|
+
export function simCfnSecretsManagerSecretValueChanged(currentProperties, updatedProperties) {
|
|
21
|
+
return (valueSignature(currentProperties) !== valueSignature(updatedProperties));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A stable string for the two properties a secret's value comes from.
|
|
25
|
+
*
|
|
26
|
+
* A property the template leaves out signs as null, which no declared value can
|
|
27
|
+
* collide with. `SecretString` is a string and `GenerateSecretString` is an
|
|
28
|
+
* object.
|
|
29
|
+
*/
|
|
30
|
+
function valueSignature(properties) {
|
|
31
|
+
return simCfnTemplateSignature([
|
|
32
|
+
properties["SecretString"] ?? null,
|
|
33
|
+
properties["GenerateSecretString"] ?? null,
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
function nameSignature(resource) {
|
|
37
|
+
const name = resource.properties["Name"];
|
|
38
|
+
return name === undefined ? "" : simCfnTemplateSignature(name);
|
|
39
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SimCfnResource } from "../../../cloudformation/resource/sim-cfn-resource.js";
|
|
2
|
+
import type { SimCfnResourceCallerOptions } from "../../../cloudformation/resource/caller/sim-cfn-resource-caller-options.js";
|
|
3
|
+
import type { SimSecretsManager } from "../../sim-secrets-manager.js";
|
|
4
|
+
interface SimCfnSecretsManagerSecretDeleterProperties {
|
|
5
|
+
readonly secretsManager: SimSecretsManager;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Removes the secret an AWS::SecretsManager::Secret Resource deployed.
|
|
9
|
+
*
|
|
10
|
+
* DeleteSecret schedules the deletion rather than carrying it out, so a torn
|
|
11
|
+
* down Stack leaves a secret waiting out its recovery window. That is what
|
|
12
|
+
* CloudFormation does, and the point of the window is that the secret is
|
|
13
|
+
* recoverable afterwards.
|
|
14
|
+
*/
|
|
15
|
+
export declare class SimCfnSecretsManagerSecretDeleter {
|
|
16
|
+
private readonly secretsManager;
|
|
17
|
+
constructor(properties: SimCfnSecretsManagerSecretDeleterProperties);
|
|
18
|
+
/**
|
|
19
|
+
* Schedule the deployed secret's deletion.
|
|
20
|
+
*/
|
|
21
|
+
delete(resource: SimCfnResource, options?: SimCfnResourceCallerOptions): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { assertDefined } from "../../../../util/type-guard/defined.js";
|
|
2
|
+
/**
|
|
3
|
+
* Removes the secret an AWS::SecretsManager::Secret Resource deployed.
|
|
4
|
+
*
|
|
5
|
+
* DeleteSecret schedules the deletion rather than carrying it out, so a torn
|
|
6
|
+
* down Stack leaves a secret waiting out its recovery window. That is what
|
|
7
|
+
* CloudFormation does, and the point of the window is that the secret is
|
|
8
|
+
* recoverable afterwards.
|
|
9
|
+
*/
|
|
10
|
+
export class SimCfnSecretsManagerSecretDeleter {
|
|
11
|
+
secretsManager;
|
|
12
|
+
constructor(properties) {
|
|
13
|
+
this.secretsManager = properties.secretsManager;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Schedule the deployed secret's deletion.
|
|
17
|
+
*/
|
|
18
|
+
async delete(resource, options) {
|
|
19
|
+
const secret = resource.simResource;
|
|
20
|
+
assertDefined(secret, `sim Secrets Manager secret for CloudFormation Resource ${resource.logicalId}`);
|
|
21
|
+
await this.secretsManager.deleteSecret({ input: { SecretId: secret.arn.value } }, options);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CfnTemplateBodyRecord } from "../../../cloudformation/template/sim-cfn-template.js";
|
|
2
|
+
import type { SimCfnTemplateValueRecord } from "../../../cloudformation/template/value/sim-cfn-template-value.js";
|
|
3
|
+
interface SecretTemplateProperties {
|
|
4
|
+
readonly name?: string;
|
|
5
|
+
readonly description?: string;
|
|
6
|
+
readonly kmsKeyId?: string;
|
|
7
|
+
readonly tags?: readonly SimCfnTemplateValueRecord[];
|
|
8
|
+
readonly secretString?: string;
|
|
9
|
+
readonly passwordLength?: number;
|
|
10
|
+
/** A second Resource reading the secret through a dynamic reference. */
|
|
11
|
+
readonly reader?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A template holding one secret, and optionally a parameter reading it.
|
|
15
|
+
*
|
|
16
|
+
* The parameter stands in for anything taking a copy of the value when the
|
|
17
|
+
* Stack deploys, such as a CloudFront origin custom header. It reads the secret
|
|
18
|
+
* through the `Fn::Join` shape CDK emits for a secret in the same Stack.
|
|
19
|
+
*/
|
|
20
|
+
export declare function secretTemplate(properties?: SecretTemplateProperties): CfnTemplateBodyRecord;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A template holding one secret, and optionally a parameter reading it.
|
|
3
|
+
*
|
|
4
|
+
* The parameter stands in for anything taking a copy of the value when the
|
|
5
|
+
* Stack deploys, such as a CloudFront origin custom header. It reads the secret
|
|
6
|
+
* through the `Fn::Join` shape CDK emits for a secret in the same Stack.
|
|
7
|
+
*/
|
|
8
|
+
export function secretTemplate(properties = {}) {
|
|
9
|
+
const secret = {
|
|
10
|
+
Type: "AWS::SecretsManager::Secret",
|
|
11
|
+
Properties: secretProperties(properties),
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
Resources: properties.reader === true
|
|
15
|
+
? { EdgeCredential: secret, Reader: readerResource() }
|
|
16
|
+
: { EdgeCredential: secret },
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function secretProperties(properties) {
|
|
20
|
+
const { name, description, kmsKeyId, tags, secretString } = properties;
|
|
21
|
+
const declared = {};
|
|
22
|
+
if (name !== undefined) {
|
|
23
|
+
declared["Name"] = name;
|
|
24
|
+
}
|
|
25
|
+
if (description !== undefined) {
|
|
26
|
+
declared["Description"] = description;
|
|
27
|
+
}
|
|
28
|
+
if (kmsKeyId !== undefined) {
|
|
29
|
+
declared["KmsKeyId"] = kmsKeyId;
|
|
30
|
+
}
|
|
31
|
+
if (tags !== undefined) {
|
|
32
|
+
declared["Tags"] = [...tags];
|
|
33
|
+
}
|
|
34
|
+
if (secretString === undefined) {
|
|
35
|
+
declared["GenerateSecretString"] = {
|
|
36
|
+
SecretStringTemplate: "{}",
|
|
37
|
+
GenerateStringKey: "current",
|
|
38
|
+
PasswordLength: properties.passwordLength ?? 64,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
declared["SecretString"] = secretString;
|
|
43
|
+
}
|
|
44
|
+
return declared;
|
|
45
|
+
}
|
|
46
|
+
function readerResource() {
|
|
47
|
+
return {
|
|
48
|
+
Type: "AWS::SSM::Parameter",
|
|
49
|
+
Properties: {
|
|
50
|
+
Name: "edge-credential-copy",
|
|
51
|
+
Type: "String",
|
|
52
|
+
Value: {
|
|
53
|
+
"Fn::Join": [
|
|
54
|
+
"",
|
|
55
|
+
[
|
|
56
|
+
"{{resolve:secretsmanager:",
|
|
57
|
+
{ Ref: "EdgeCredential" },
|
|
58
|
+
":SecretString:current::}}",
|
|
59
|
+
],
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { SimAws } from "../../../aws/sim-aws.js";
|
|
2
|
+
import type { CfnTemplateBodyRecord } from "../../../cloudformation/template/sim-cfn-template.js";
|
|
3
|
+
/**
|
|
4
|
+
* A simulated AWS in one account and region, for a secret update test.
|
|
5
|
+
*/
|
|
6
|
+
export declare function secretUpdateSimAws(): SimAws;
|
|
7
|
+
/**
|
|
8
|
+
* Deploy a template and wait for its Resources.
|
|
9
|
+
*/
|
|
10
|
+
export declare function deploySecretStack(simAws: SimAws, stackName: string, template: CfnTemplateBodyRecord): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Apply a template to a deployed stack and wait for the update.
|
|
13
|
+
*/
|
|
14
|
+
export declare function updateSecretStack(simAws: SimAws, stackName: string, template: CfnTemplateBodyRecord): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* The ARN of the secret a deployed stack holds.
|
|
17
|
+
*/
|
|
18
|
+
export declare function deployedSecretArn(simAws: SimAws, stackName: string): string;
|
package/dist/service/secretsmanager/cfn/secret/sim-cfn-secrets-manager-secret-update.test-support.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { assertDefined } from "../../../../util/type-guard/defined.js";
|
|
2
|
+
import { SimAws } from "../../../aws/sim-aws.js";
|
|
3
|
+
import { jsonStringify } from "../../../../util/type-guard/json.js";
|
|
4
|
+
/**
|
|
5
|
+
* A simulated AWS in one account and region, for a secret update test.
|
|
6
|
+
*/
|
|
7
|
+
export function secretUpdateSimAws() {
|
|
8
|
+
return new SimAws({
|
|
9
|
+
defaultAccountId: "111111111111",
|
|
10
|
+
defaultRegionName: "eu-west-2",
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Deploy a template and wait for its Resources.
|
|
15
|
+
*/
|
|
16
|
+
export async function deploySecretStack(simAws, stackName, template) {
|
|
17
|
+
const stack = await simAws
|
|
18
|
+
.cloudFormation()
|
|
19
|
+
.deployTemplate({ stackName, template });
|
|
20
|
+
await stack.waitForDeployComplete();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Apply a template to a deployed stack and wait for the update.
|
|
24
|
+
*/
|
|
25
|
+
export async function updateSecretStack(simAws, stackName, template) {
|
|
26
|
+
await simAws.cloudFormation().updateStack({
|
|
27
|
+
input: { StackName: stackName, TemplateBody: jsonStringify(template) },
|
|
28
|
+
});
|
|
29
|
+
await simAws.cloudFormation().waitForStackUpdateComplete(stackName);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The ARN of the secret a deployed stack holds.
|
|
33
|
+
*/
|
|
34
|
+
export function deployedSecretArn(simAws, stackName) {
|
|
35
|
+
const stack = simAws.cloudFormation().getStackByName(stackName);
|
|
36
|
+
const secret = stack?.getResource("EdgeCredential")?.simResource;
|
|
37
|
+
assertDefined(secret, `deployed EdgeCredential secret in Stack ${stackName}`);
|
|
38
|
+
return secret.arn.value;
|
|
39
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { SimCfnResource } from "../../../cloudformation/resource/sim-cfn-resource.js";
|
|
2
|
+
import type { SimCloudFormationResourceInPlaceUpdateContext } from "../../../cloudformation/resource/sim-cfn-resource.type.js";
|
|
3
|
+
import type { SimSecretsManagerSecret } from "../../secret/sim-secrets-manager-secret.js";
|
|
4
|
+
import type { SimSecretsManager } from "../../sim-secrets-manager.js";
|
|
5
|
+
interface SimCfnSecretsManagerSecretUpdaterProperties {
|
|
6
|
+
readonly secretsManager: SimSecretsManager;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Applies a changed AWS::SecretsManager::Secret Resource to the deployed
|
|
10
|
+
* secret.
|
|
11
|
+
*
|
|
12
|
+
* Every property but `Name` is applied with no interruption, as real
|
|
13
|
+
* CloudFormation applies it. That matters most for the value. A secret keeps
|
|
14
|
+
* its versions across a description or a tag change, and the things holding a
|
|
15
|
+
* copy of the value stay in step with it.
|
|
16
|
+
*
|
|
17
|
+
* Replacing instead would also fail. DeleteSecret schedules the deletion and
|
|
18
|
+
* leaves the name held for the recovery window. The secret taking its place
|
|
19
|
+
* would then ask for a name the deleted one still has.
|
|
20
|
+
*/
|
|
21
|
+
export declare class SimCfnSecretsManagerSecretUpdater {
|
|
22
|
+
private readonly secretsManager;
|
|
23
|
+
constructor(properties: SimCfnSecretsManagerSecretUpdaterProperties);
|
|
24
|
+
/**
|
|
25
|
+
* Apply the new Resource definition to the deployed secret.
|
|
26
|
+
*/
|
|
27
|
+
update(current: SimCfnResource, updated: SimCfnResource, context: SimCloudFormationResourceInPlaceUpdateContext): Promise<SimSecretsManagerSecret>;
|
|
28
|
+
/**
|
|
29
|
+
* The value to write, where the new template asks for a different one.
|
|
30
|
+
*
|
|
31
|
+
* Nothing is written for an unchanged value. Regenerating a password for a
|
|
32
|
+
* description change would leave every copy of it taken at deployment
|
|
33
|
+
* holding the old one.
|
|
34
|
+
*/
|
|
35
|
+
private changedValue;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { assertDefined } from "../../../../util/type-guard/defined.js";
|
|
2
|
+
import { simCfnResourceCallerOptions } from "../../../cloudformation/resource/caller/sim-cfn-resource-caller-options.js";
|
|
3
|
+
import { simCfnSecretsManagerSecretValueChanged } from "./sim-cfn-secrets-manager-secret-change.js";
|
|
4
|
+
import { SimCfnSecretsManagerSecretProperties } from "./sim-cfn-secrets-manager-secret-properties.js";
|
|
5
|
+
/**
|
|
6
|
+
* Applies a changed AWS::SecretsManager::Secret Resource to the deployed
|
|
7
|
+
* secret.
|
|
8
|
+
*
|
|
9
|
+
* Every property but `Name` is applied with no interruption, as real
|
|
10
|
+
* CloudFormation applies it. That matters most for the value. A secret keeps
|
|
11
|
+
* its versions across a description or a tag change, and the things holding a
|
|
12
|
+
* copy of the value stay in step with it.
|
|
13
|
+
*
|
|
14
|
+
* Replacing instead would also fail. DeleteSecret schedules the deletion and
|
|
15
|
+
* leaves the name held for the recovery window. The secret taking its place
|
|
16
|
+
* would then ask for a name the deleted one still has.
|
|
17
|
+
*/
|
|
18
|
+
export class SimCfnSecretsManagerSecretUpdater {
|
|
19
|
+
secretsManager;
|
|
20
|
+
constructor(properties) {
|
|
21
|
+
this.secretsManager = properties.secretsManager;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Apply the new Resource definition to the deployed secret.
|
|
25
|
+
*/
|
|
26
|
+
async update(current, updated, context) {
|
|
27
|
+
const secret = current.simResource;
|
|
28
|
+
assertDefined(secret, `sim Secrets Manager secret for CloudFormation Resource ${current.logicalId}`);
|
|
29
|
+
const properties = new SimCfnSecretsManagerSecretProperties({
|
|
30
|
+
resource: updated,
|
|
31
|
+
properties: context.updatedResolvedProperties,
|
|
32
|
+
});
|
|
33
|
+
const kmsKeyId = properties.kmsKeyId();
|
|
34
|
+
await this.secretsManager.updateSecret({
|
|
35
|
+
input: {
|
|
36
|
+
SecretId: secret.arn.value,
|
|
37
|
+
// CloudFormation applies the template as the desired state, so a
|
|
38
|
+
// Description the new template leaves out is cleared rather than
|
|
39
|
+
// left as it was. An empty one is how UpdateSecret clears it.
|
|
40
|
+
Description: properties.description() ?? "",
|
|
41
|
+
KmsKeyId: kmsKeyId,
|
|
42
|
+
SecretString: this.changedValue(properties, context),
|
|
43
|
+
},
|
|
44
|
+
}, simCfnResourceCallerOptions(context.caller));
|
|
45
|
+
// The parts UpdateSecret has no way to ask for. Real Secrets Manager
|
|
46
|
+
// changes tags with TagResource and UntagResource, which are not
|
|
47
|
+
// simulated, and UpdateSecret cannot put a secret back on the
|
|
48
|
+
// aws/secretsmanager key.
|
|
49
|
+
if (kmsKeyId === undefined) {
|
|
50
|
+
secret.kmsKeyId = undefined;
|
|
51
|
+
}
|
|
52
|
+
secret.tags = properties.tags() ?? [];
|
|
53
|
+
return secret;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The value to write, where the new template asks for a different one.
|
|
57
|
+
*
|
|
58
|
+
* Nothing is written for an unchanged value. Regenerating a password for a
|
|
59
|
+
* description change would leave every copy of it taken at deployment
|
|
60
|
+
* holding the old one.
|
|
61
|
+
*/
|
|
62
|
+
changedValue(properties, context) {
|
|
63
|
+
const changed = simCfnSecretsManagerSecretValueChanged(context.currentResolvedProperties, context.updatedResolvedProperties);
|
|
64
|
+
return changed ? properties.secretString() : undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -1,33 +1,43 @@
|
|
|
1
1
|
import type { SimCfnServiceResourceFactory } from "../../cloudformation/resource/factory/sim-cfn-resource-factory.type.js";
|
|
2
2
|
import type { SimCfnResource, SimCloudFormationResourceCreateContext, SimCloudFormationResourceDeleteContext } from "../../cloudformation/resource/sim-cfn-resource.js";
|
|
3
|
+
import type { SimCloudFormationResourceInPlaceUpdateContext } from "../../cloudformation/resource/sim-cfn-resource.type.js";
|
|
3
4
|
import type { SimSecretsManager } from "../sim-secrets-manager.js";
|
|
4
5
|
interface SimSecretsManagerCfnResourceFactoryProperties {
|
|
5
6
|
readonly secretsManager: SimSecretsManager;
|
|
6
7
|
}
|
|
7
8
|
/**
|
|
8
9
|
* CloudFormation Resource factory for simulated Secrets Manager resources.
|
|
10
|
+
*
|
|
11
|
+
* Only `Secret` is deployed. Rotation, resource policies and target
|
|
12
|
+
* attachments are not simulated, so their Resource types are reported as
|
|
13
|
+
* unsupported and skipped rather than quietly treated as deployed.
|
|
9
14
|
*/
|
|
10
15
|
export declare class SimSecretsManagerCfnResourceFactory implements SimCfnServiceResourceFactory {
|
|
11
|
-
private readonly secretsManager;
|
|
12
16
|
private readonly secretCreator;
|
|
17
|
+
private readonly secretUpdater;
|
|
18
|
+
private readonly secretDeleter;
|
|
13
19
|
constructor(properties: SimSecretsManagerCfnResourceFactoryProperties);
|
|
14
20
|
/**
|
|
15
21
|
* Create a simulated Secrets Manager resource from a CloudFormation
|
|
16
22
|
* Resource.
|
|
17
|
-
*
|
|
18
|
-
* Rotation, resource policies and target attachments are not simulated, so
|
|
19
|
-
* their Resource types are reported as unsupported and skipped rather than
|
|
20
|
-
* quietly treated as deployed.
|
|
21
23
|
*/
|
|
22
24
|
create(resourceTypeName: string, resource: SimCfnResource, context: SimCloudFormationResourceCreateContext): Promise<object | undefined>;
|
|
25
|
+
/**
|
|
26
|
+
* Whether a changed secret can be applied to the deployed one.
|
|
27
|
+
*
|
|
28
|
+
* Name is the only property real CloudFormation replaces a secret for.
|
|
29
|
+
* Everything else is applied with no interruption, which is what keeps a
|
|
30
|
+
* generated value across a change to the secret's description or tags.
|
|
31
|
+
*/
|
|
32
|
+
updatesInPlace(resourceTypeName: string, current: SimCfnResource, updated: SimCfnResource): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Apply a changed AWS::SecretsManager::Secret Resource to the deployed
|
|
35
|
+
* secret.
|
|
36
|
+
*/
|
|
37
|
+
updateInPlace(resourceTypeName: string, current: SimCfnResource, updated: SimCfnResource, context: SimCloudFormationResourceInPlaceUpdateContext): Promise<object | undefined>;
|
|
23
38
|
/**
|
|
24
39
|
* Delete a simulated Secrets Manager resource created from a CloudFormation
|
|
25
40
|
* Resource.
|
|
26
|
-
*
|
|
27
|
-
* DeleteSecret schedules the deletion rather than carrying it out, so a torn
|
|
28
|
-
* down Stack leaves a secret waiting out its recovery window. That is what
|
|
29
|
-
* CloudFormation does: the secret is recoverable afterwards, which is the
|
|
30
|
-
* point of the window.
|
|
31
41
|
*/
|
|
32
42
|
delete(resourceTypeName: string, resource: SimCfnResource, context: SimCloudFormationResourceDeleteContext): Promise<void>;
|
|
33
43
|
}
|