@kensio/yulin 1.20.14 → 1.20.16
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 +13 -0
- package/{dist/config/oxlint/cffjs2.oxlintrc.json → cffjs2.oxlintrc.json} +1 -1
- package/dist/config/oxlint/cffjs2.oxlint.config.js +4 -3
- package/dist/sdk/module/sim-sdk-module-client-interceptor.d.ts +5 -0
- package/dist/sdk/module/sim-sdk-module-client-interceptor.js +14 -0
- package/dist/sdk/module/sim-sdk-static-client-factory.d.ts +26 -0
- package/dist/sdk/module/sim-sdk-static-client-factory.js +59 -0
- package/dist/sdk/send-patch.d.ts +8 -0
- package/dist/sdk/send-patch.js +11 -0
- package/dist/service/aws/caller/sim-aws-caller-resolver.d.ts +28 -6
- package/dist/service/aws/caller/sim-aws-caller-resolver.js +18 -11
- package/dist/service/aws/caller/sim-aws-caller.d.ts +9 -0
- package/dist/service/aws/caller/sim-aws-resolved-caller.d.ts +19 -0
- package/dist/service/aws/caller/sim-aws-resolved-caller.js +26 -0
- package/dist/service/aws/factory/sim-aws-account-region-service-builder.d.ts +6 -0
- package/dist/service/aws/factory/sim-aws-account-region-service-builder.js +3 -0
- package/dist/service/aws/factory/sim-aws-account-service-cache.d.ts +6 -0
- package/dist/service/aws/factory/sim-aws-account-service-cache.js +5 -0
- package/dist/service/aws/factory/sim-aws-service-factory.d.ts +5 -0
- package/dist/service/aws/factory/sim-aws-service-factory.js +6 -0
- package/dist/service/aws/sim-aws-properties.d.ts +15 -0
- package/dist/service/aws/sim-aws.js +2 -1
- package/dist/service/cloudformation/resource/resolve/property/sim-cfn-resource-property-resolver.d.ts +11 -4
- package/dist/service/cloudformation/resource/resolve/property/sim-cfn-resource-property-resolver.js +21 -8
- package/dist/service/cloudformation/resource/sim-cfn-resource.type.d.ts +9 -0
- package/dist/service/cloudformation/template/dynamic/make-sim-cfn-dynamic-references.d.ts +2 -0
- package/dist/service/cloudformation/template/dynamic/make-sim-cfn-dynamic-references.js +2 -2
- package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-resolvers.d.ts +10 -0
- package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-resolvers.js +4 -3
- package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-scan.d.ts +11 -0
- package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-scan.js +21 -0
- package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-references.d.ts +24 -2
- package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-references.js +50 -11
- package/dist/service/cloudformation/template/dynamic/sim-cfn-prefetched-dynamic-references.d.ts +42 -0
- package/dist/service/cloudformation/template/dynamic/sim-cfn-prefetched-dynamic-references.js +80 -0
- package/dist/service/eventbridge/delivery/sim-event-bridge-delivery-failures.d.ts +16 -0
- package/dist/service/eventbridge/delivery/sim-event-bridge-delivery-failures.js +19 -0
- package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.d.ts +30 -7
- package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.js +20 -11
- package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.d.ts +2 -1
- package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.js +5 -1
- package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.d.ts +10 -0
- package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.js +15 -3
- package/dist/service/iam/authorize/sim-iam-account-auth-z.d.ts +11 -4
- package/dist/service/iam/authorize/sim-iam-account-auth-z.js +5 -3
- package/dist/service/iam/authorize/sim-iam-authorizer.d.ts +2 -1
- package/dist/service/iam/authorize/sim-iam-authorizer.js +1 -0
- package/dist/service/iam/command/policy/create-policy/create-policy-input-resolver.js +4 -1
- package/dist/service/iam/command/policy/put-role-policy/put-role-policy.handler.js +5 -1
- package/dist/service/iam/command/policy/put-user-policy/put-user-policy.handler.js +5 -1
- package/dist/service/iam/policy/parse/sim-iam-document-parser.d.ts +4 -1
- package/dist/service/iam/policy/parse/sim-iam-document-parser.js +13 -15
- package/dist/service/iam/policy/sim-iam-statement-label.d.ts +28 -0
- package/dist/service/iam/policy/sim-iam-statement-label.js +17 -0
- package/dist/service/iam/policy/sim-iam-statement-strings.d.ts +11 -0
- package/dist/service/iam/policy/sim-iam-statement-strings.js +27 -0
- package/dist/service/iam/role/sim-iam-role-with-policy.factory.d.ts +9 -0
- package/dist/service/iam/role/sim-iam-role-with-policy.factory.js +4 -2
- package/dist/service/iam/sim-iam-account-parts.d.ts +8 -0
- package/dist/service/iam/sim-iam-account-parts.js +1 -0
- package/dist/service/iam/validate/sim-iam-policy-document-validator.d.ts +9 -2
- package/dist/service/iam/validate/sim-iam-policy-document-validator.js +37 -19
- package/dist/service/lambda/function/code/sim-lambda-vm-zip-function.factory.d.ts +7 -0
- package/dist/service/lambda/function/code/sim-lambda-vm-zip-function.factory.js +1 -0
- package/dist/service/lambda/function/code/vm/sdk/sim-lambda-provided-sdk-module.d.ts +19 -0
- package/dist/service/lambda/function/code/vm/sdk/sim-lambda-provided-sdk-module.js +78 -0
- package/dist/service/lambda/function/code/vm/sdk/sim-lambda-sdk-packages-not-installed.error.d.ts +15 -0
- package/dist/service/lambda/function/code/vm/sdk/sim-lambda-sdk-packages-not-installed.error.js +27 -0
- package/dist/service/lambda/function/code/vm/sdk/sim-lambda-vm-sdk-module-provider.d.ts +9 -0
- package/dist/service/lambda/function/code/vm/sdk/sim-sdk-lambda-vm-module-provider.d.ts +10 -0
- package/dist/service/lambda/function/code/vm/sdk/sim-sdk-lambda-vm-module-provider.js +28 -34
- package/dist/service/lambda/function/code/vm/sim-lambda-host-modules.d.ts +15 -0
- package/dist/service/lambda/function/code/vm/sim-lambda-host-modules.js +31 -0
- package/dist/service/lambda/function/code/vm/sim-lambda-vm-module-resolver.d.ts +7 -0
- package/dist/service/lambda/function/code/vm/sim-lambda-vm-module-resolver.js +15 -0
- package/dist/service/lambda/function/code/vm/sim-lambda-vm-modules.js +5 -4
- package/dist/service/s3/command/put-bucket-policy/put-bucket-policy.handler.js +4 -1
- package/dist/service/scheduler/delivery/sim-scheduler-delivery-failures.d.ts +16 -0
- package/dist/service/scheduler/delivery/sim-scheduler-delivery-failures.js +19 -0
- package/dist/service/scheduler/index.d.ts +1 -1
- package/dist/service/scheduler/index.js +1 -1
- package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-dynamic-reference-resolver.d.ts +13 -15
- package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-dynamic-reference-resolver.js +22 -43
- package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-stand-in.d.ts +9 -0
- package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-stand-in.js +14 -0
- package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-value.d.ts +12 -0
- package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-value.js +29 -0
- package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-dynamic-reference-resolver.d.ts +22 -6
- package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-dynamic-reference-resolver.js +39 -24
- package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-secure-dynamic-reference-resolver.d.ts +10 -5
- package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-secure-dynamic-reference-resolver.js +10 -8
- package/dist/service/ssm/sim-ssm.d.ts +10 -2
- package/dist/service/ssm/sim-ssm.js +12 -4
- package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.d.ts +9 -2
- package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.js +2 -2
- package/dist/service/sts/auth-z/assume-role-source-account-auth-z.d.ts +10 -4
- package/dist/service/sts/auth-z/assume-role-source-account-auth-z.js +8 -7
- package/dist/service/sts/auth-z/assume-role-target-auth-z.d.ts +2 -2
- package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.d.ts +6 -2
- package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.js +14 -11
- package/dist/service/sts/command/assume-role/assume-role.handler.d.ts +5 -1
- package/dist/service/sts/command/assume-role/assume-role.handler.js +5 -2
- package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.d.ts +5 -1
- package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.js +4 -1
- package/dist/service/sts/service-role/sim-service-role.js +5 -1
- package/dist/service/sts/sim-sts.d.ts +9 -1
- package/dist/service/sts/sim-sts.js +4 -0
- package/docs/README.md +54 -0
- package/docs/ai-skill/README.md +69 -0
- package/docs/cli/README.md +284 -0
- package/docs/factories/README.md +140 -0
- package/docs/lint/README.md +176 -0
- package/docs/non-aws-dependencies/README.md +261 -0
- package/docs/sdk/README.md +272 -0
- package/docs/serve/README.md +1126 -0
- package/docs/services/acm/README.md +823 -0
- package/docs/services/apigateway/README.md +1919 -0
- package/docs/services/apigatewayv2/README.md +2678 -0
- package/docs/services/athena/README.md +1044 -0
- package/docs/services/bedrock/README.md +350 -0
- package/docs/services/cloudformation/README.md +3440 -0
- package/docs/services/cloudfront/README.md +2874 -0
- package/docs/services/cloudwatch/README.md +420 -0
- package/docs/services/cognito/README.md +4923 -0
- package/docs/services/dynamodb/README.md +3784 -0
- package/docs/services/ecr/README.md +302 -0
- package/docs/services/ecs/README.md +2010 -0
- package/docs/services/elbv2/README.md +1897 -0
- package/docs/services/eventbridge/README.md +1110 -0
- package/docs/services/firehose/README.md +805 -0
- package/docs/services/glue/README.md +501 -0
- package/docs/services/iam/README.md +1283 -0
- package/docs/services/kinesis/README.md +454 -0
- package/docs/services/kms/README.md +659 -0
- package/docs/services/lambda/README.md +3645 -0
- package/docs/services/logs/README.md +868 -0
- package/docs/services/organizations/README.md +574 -0
- package/docs/services/personalize/README.md +835 -0
- package/docs/services/rekognition/README.md +1070 -0
- package/docs/services/route53/README.md +1783 -0
- package/docs/services/s3/README.md +3024 -0
- package/docs/services/scheduler/README.md +806 -0
- package/docs/services/secretsmanager/README.md +667 -0
- package/docs/services/ses/README.md +1011 -0
- package/docs/services/sns/README.md +1904 -0
- package/docs/services/sqs/README.md +1185 -0
- package/docs/services/ssm/README.md +1072 -0
- package/docs/services/stepfunctions/README.md +1332 -0
- package/docs/services/sts/README.md +245 -0
- package/docs/services/wafv2/README.md +1378 -0
- package/docs/terraform/README.md +248 -0
- package/docs/time/README.md +284 -0
- package/llms.txt +59 -0
- package/package.json +9 -5
|
@@ -0,0 +1,1126 @@
|
|
|
1
|
+
# Localhost server
|
|
2
|
+
|
|
3
|
+
Serving puts a simulated AWS environment behind a real port. A browser, `curl` or an SDK client then
|
|
4
|
+
reaches it over HTTP. The same request path is also available in the process with no port at all,
|
|
5
|
+
which is the form a test usually wants.
|
|
6
|
+
|
|
7
|
+
## Serving a port
|
|
8
|
+
|
|
9
|
+
`serveSimAws` starts the server and returns once it is listening:
|
|
10
|
+
|
|
11
|
+
```typescript sim-serve-localhost
|
|
12
|
+
/**
|
|
13
|
+
* Serving a simulated environment on a port of your choosing.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { SimAws } from "@kensio/yulin";
|
|
17
|
+
import { serveSimAws } from "@kensio/yulin/serve";
|
|
18
|
+
|
|
19
|
+
const simAws = new SimAws();
|
|
20
|
+
const srv = await serveSimAws({ simAws, port: 8787 });
|
|
21
|
+
|
|
22
|
+
console.log(srv.port); // "8787"
|
|
23
|
+
|
|
24
|
+
await srv.close();
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Without a `port` the server takes whatever port is free, which changes on every run. Pin one when
|
|
28
|
+
the URL needs to stay the same, such as when a browser is pointed at it.
|
|
29
|
+
|
|
30
|
+
A URL from a simulated service points at a simulated hostname on its usual port, and needs adapting
|
|
31
|
+
before it can be fetched. `srv.localUrl(...)` swaps in the port the server took:
|
|
32
|
+
|
|
33
|
+
```typescript sim-serve-local-url
|
|
34
|
+
/**
|
|
35
|
+
* Turning a simulated AWS URL into one that reaches the local server.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { CreateBucketCommand } from "@aws-sdk/client-s3";
|
|
39
|
+
import { SimAws } from "@kensio/yulin";
|
|
40
|
+
import { serveSimAws } from "@kensio/yulin/serve";
|
|
41
|
+
|
|
42
|
+
const simAws = new SimAws();
|
|
43
|
+
const srv = await serveSimAws({ simAws });
|
|
44
|
+
|
|
45
|
+
await simAws.s3().createBucket(new CreateBucketCommand({ Bucket: "foo-site" }));
|
|
46
|
+
|
|
47
|
+
const websiteUrl = simAws.s3().getBucketWebsiteUrl("foo-site");
|
|
48
|
+
console.log(srv.localUrl(websiteUrl).toString());
|
|
49
|
+
// http://foo-site.s3-website.us-east-1.sim-aws.localhost:<srv.port>/
|
|
50
|
+
// with whatever port this run took, since none was pinned.
|
|
51
|
+
|
|
52
|
+
await srv.close();
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Redirects
|
|
56
|
+
|
|
57
|
+
A simulated service redirects to the hostname it would use against real AWS. Served on localhost that
|
|
58
|
+
address reaches the public internet, and a browser following it leaves the simulation behind.
|
|
59
|
+
|
|
60
|
+
A `Location` header naming a hostname the simulation serves is put into its localhost form on the way
|
|
61
|
+
out of the local server. A sign-in page behind `www.example.com` redirecting to its user pool domain
|
|
62
|
+
answers with an address the browser can open:
|
|
63
|
+
|
|
64
|
+
```http
|
|
65
|
+
303 See Other
|
|
66
|
+
Location: http://auth.example.com.sim-aws.localhost:8787/oauth2/authorize?client_id=1a2b3c
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The rewrite is `srv.localUrl(...)` applied to the header, and the hostname is matched the way an
|
|
70
|
+
arriving request's hostname is matched. A `Location` naming an address the simulation serves nothing
|
|
71
|
+
at is passed on as the service wrote it. So is a relative one, since that is already relative to the
|
|
72
|
+
address the client arrived at.
|
|
73
|
+
|
|
74
|
+
Only the local server does this. `SimAwsHttp` reaches every simulated hostname by its own name. A
|
|
75
|
+
test using it sees the `Location` the service issued and asserts on the production hostname.
|
|
76
|
+
|
|
77
|
+
## Requests without a port
|
|
78
|
+
|
|
79
|
+
`SimAwsHttp` is the same request path with no socket under it. It takes a Fetch API `Request` and
|
|
80
|
+
answers with a `Response`, in the process that built the environment:
|
|
81
|
+
|
|
82
|
+
```typescript sim-serve-in-process-request
|
|
83
|
+
/**
|
|
84
|
+
* Requesting a simulated S3 website with no server listening.
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
import {
|
|
88
|
+
CreateBucketCommand,
|
|
89
|
+
PutBucketPolicyCommand,
|
|
90
|
+
PutBucketWebsiteCommand,
|
|
91
|
+
PutObjectCommand,
|
|
92
|
+
PutPublicAccessBlockCommand,
|
|
93
|
+
} from "@aws-sdk/client-s3";
|
|
94
|
+
import { SimAws } from "@kensio/yulin";
|
|
95
|
+
import { SimAwsHttp } from "@kensio/yulin/serve";
|
|
96
|
+
|
|
97
|
+
const simAws = new SimAws();
|
|
98
|
+
const simAwsHttp = new SimAwsHttp({ simAws });
|
|
99
|
+
const simS3 = simAws.region("eu-west-2").s3();
|
|
100
|
+
|
|
101
|
+
await simS3.createBucket(new CreateBucketCommand({ Bucket: "foo-site" }));
|
|
102
|
+
await simS3.putObject(
|
|
103
|
+
new PutObjectCommand({
|
|
104
|
+
Bucket: "foo-site",
|
|
105
|
+
Key: "index.html",
|
|
106
|
+
Body: "<h1>Hello, world!</h1>",
|
|
107
|
+
ContentType: "text/html; charset=utf-8",
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
await simS3.putBucketWebsite(
|
|
111
|
+
new PutBucketWebsiteCommand({
|
|
112
|
+
Bucket: "foo-site",
|
|
113
|
+
WebsiteConfiguration: {
|
|
114
|
+
IndexDocument: {
|
|
115
|
+
Suffix: "index.html",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// A website endpoint serves only what the Bucket policy makes readable, and a
|
|
122
|
+
// public policy needs the Block Public Access opt-out first.
|
|
123
|
+
await simS3.putPublicAccessBlock(
|
|
124
|
+
new PutPublicAccessBlockCommand({
|
|
125
|
+
Bucket: "foo-site",
|
|
126
|
+
PublicAccessBlockConfiguration: {
|
|
127
|
+
BlockPublicAcls: true,
|
|
128
|
+
IgnorePublicAcls: true,
|
|
129
|
+
},
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
await simS3.putBucketPolicy(
|
|
133
|
+
new PutBucketPolicyCommand({
|
|
134
|
+
Bucket: "foo-site",
|
|
135
|
+
Policy: JSON.stringify({
|
|
136
|
+
Version: "2012-10-17",
|
|
137
|
+
Statement: {
|
|
138
|
+
Effect: "Allow",
|
|
139
|
+
Principal: "*",
|
|
140
|
+
Action: "s3:GetObject",
|
|
141
|
+
Resource: "arn:aws:s3:::foo-site/*",
|
|
142
|
+
},
|
|
143
|
+
}),
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const response = await simAwsHttp.fetch(simS3.getBucketWebsiteUrl("foo-site"));
|
|
148
|
+
|
|
149
|
+
console.log(response.status); // 200
|
|
150
|
+
console.log(await response.text()); // <h1>Hello, world!</h1>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`fetch(input, init)` takes what the global `fetch` takes. `handleRequest(request)` takes a `Request`
|
|
154
|
+
that is already built, for a caller that is holding one.
|
|
155
|
+
|
|
156
|
+
With no port bound there is no URL to adapt. The website URL above is fetched as it is, where a
|
|
157
|
+
served environment needs `srv.localUrl(...)` to add the port it took. A hostname a simulated Route53
|
|
158
|
+
answers for is requested by its own name. A Distribution behind `www.example.com` is reached at
|
|
159
|
+
`https://www.example.com/`. An `https` URL needs no certificate set up for it, since there is no
|
|
160
|
+
connection to encrypt.
|
|
161
|
+
|
|
162
|
+
There is also no server to close, though `simAws.close()` still lets go of what the environment
|
|
163
|
+
itself is holding, such as a watched template file. See
|
|
164
|
+
[stopping and restarting](#stopping-and-restarting).
|
|
165
|
+
|
|
166
|
+
Live reload sits either side of this interface, outside it. A response from here never carries the
|
|
167
|
+
injected script, even in a process that served one elsewhere with live reload on.
|
|
168
|
+
|
|
169
|
+
Which to reach for:
|
|
170
|
+
|
|
171
|
+
- `SimAwsHttp` for tests, and for anything else in the same process. Test files that run in parallel
|
|
172
|
+
have no port to collide over. With no socket listening there is no teardown to get wrong, and no
|
|
173
|
+
connection between the request and the service answering it.
|
|
174
|
+
- `serveSimAws` for anything outside the process, such as a browser, `curl`, or an SDK client
|
|
175
|
+
pointed at a local endpoint.
|
|
176
|
+
|
|
177
|
+
Both go through the same authentication, routing and service code. A request answered one way is
|
|
178
|
+
answered the same way the other.
|
|
179
|
+
|
|
180
|
+
## Pointing an AWS SDK or the CLI at the simulation
|
|
181
|
+
|
|
182
|
+
A served environment answers the general AWS service APIs on the same port it serves everything else. Give any AWS SDK, in any language, the server's own URL as its endpoint and it reaches the simulation.
|
|
183
|
+
|
|
184
|
+
```typescript sim-serve-aws-api-endpoint
|
|
185
|
+
/**
|
|
186
|
+
* Reaching simulated DynamoDB with an ordinary SDK client over a port.
|
|
187
|
+
*/
|
|
188
|
+
|
|
189
|
+
import {
|
|
190
|
+
CreateTableCommand,
|
|
191
|
+
DynamoDBClient,
|
|
192
|
+
PutItemCommand,
|
|
193
|
+
} from "@aws-sdk/client-dynamodb";
|
|
194
|
+
import {
|
|
195
|
+
CreateAccessKeyCommand,
|
|
196
|
+
CreateUserCommand,
|
|
197
|
+
PutUserPolicyCommand,
|
|
198
|
+
} from "@aws-sdk/client-iam";
|
|
199
|
+
import { SimAws } from "@kensio/yulin";
|
|
200
|
+
import { serveSimAws } from "@kensio/yulin/serve";
|
|
201
|
+
|
|
202
|
+
const simAws = new SimAws();
|
|
203
|
+
|
|
204
|
+
await simAws.dynamoDb().createTable(
|
|
205
|
+
new CreateTableCommand({
|
|
206
|
+
TableName: "widgets",
|
|
207
|
+
AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }],
|
|
208
|
+
KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
|
|
209
|
+
BillingMode: "PAY_PER_REQUEST",
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// A served request is authorized as whoever signed it, so the client needs
|
|
214
|
+
// credentials simulated IAM issued.
|
|
215
|
+
const simIam = simAws.iam();
|
|
216
|
+
await simIam.createUser(new CreateUserCommand({ UserName: "Widgets" }));
|
|
217
|
+
await simIam.putUserPolicy(
|
|
218
|
+
new PutUserPolicyCommand({
|
|
219
|
+
UserName: "Widgets",
|
|
220
|
+
PolicyName: "WriteWidgets",
|
|
221
|
+
PolicyDocument: JSON.stringify({
|
|
222
|
+
Version: "2012-10-17",
|
|
223
|
+
Statement: { Effect: "Allow", Action: "dynamodb:*", Resource: "*" },
|
|
224
|
+
}),
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
const created = await simIam.createAccessKey(
|
|
228
|
+
new CreateAccessKeyCommand({ UserName: "Widgets" }),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const srv = await serveSimAws({ simAws, port: 8787 });
|
|
232
|
+
|
|
233
|
+
const client = new DynamoDBClient({
|
|
234
|
+
region: simAws.defaultRegionName,
|
|
235
|
+
endpoint: `http://localhost:${srv.port}`,
|
|
236
|
+
credentials: {
|
|
237
|
+
accessKeyId: created.AccessKey.AccessKeyId,
|
|
238
|
+
secretAccessKey: created.AccessKey.SecretAccessKey,
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await client.send(
|
|
243
|
+
new PutItemCommand({
|
|
244
|
+
TableName: "widgets",
|
|
245
|
+
Item: { id: { S: "w1" } },
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
await srv.close();
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
One endpoint URL covers every service. A client sends `Host: localhost:<port>` whichever service it is talking to, which leaves no hostname to route on. Routing is on the service and Region named in the request's SigV4 credential scope, and a client cannot change either without invalidating its signature.
|
|
253
|
+
|
|
254
|
+
The same URL works for anything that speaks the AWS APIs, which includes the real `aws` CLI:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
export AWS_ENDPOINT_URL=http://localhost:8787
|
|
258
|
+
export AWS_ACCESS_KEY_ID=<key from simulated IAM>
|
|
259
|
+
export AWS_SECRET_ACCESS_KEY=<secret from simulated IAM>
|
|
260
|
+
aws dynamodb put-item --table-name widgets --item '{"id":{"S":"w1"}}'
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Who a served request is
|
|
264
|
+
|
|
265
|
+
Whoever signed it. The endpoint verifies the signature against simulated IAM and runs the operation as the principal behind the access key. An IAM policy applies exactly as it does in process, and an assumed-role session is authorized against the Role behind it.
|
|
266
|
+
|
|
267
|
+
A request carrying no signature is anonymous and reaches nothing. In process an omitted caller means "whoever owns this simulation", and over a port the same default would hand administrator rights to anyone who could reach it.
|
|
268
|
+
|
|
269
|
+
### Which services answer
|
|
270
|
+
|
|
271
|
+
S3, STS, IAM, ELBv2, SNS, CloudFormation, Lambda, and the services that speak the AWS JSON protocol. Those are DynamoDB, DynamoDB Streams, SQS, Cognito Identity Provider, EventBridge, ECS, SSM, ACM, CloudWatch, CloudWatch Logs, KMS, Secrets Manager and Rekognition.
|
|
272
|
+
|
|
273
|
+
A request to any other service is refused with `501 Not Implemented` and a body saying why. Every service is reachable in process through `SimAws` and through [SDK interception](https://yulinsim.dev/sdk/), whether or not it answers here.
|
|
274
|
+
|
|
275
|
+
CloudWatch's windowed reads, `GetMetricStatistics` and `GetMetricData`, are the exception among the operations those services implement. The JSON protocol carries a timestamp as epoch seconds, and the endpoint passes that number through as it arrives. The simulation is handed a number where it expects a date. Both reads answer in process and through SDK interception.
|
|
276
|
+
|
|
277
|
+
### Checking who the simulator thinks you are
|
|
278
|
+
|
|
279
|
+
`sts get-caller-identity` reports the principal behind the credentials that signed the request, which is the quickest way to confirm an endpoint and a set of credentials are wired up as expected:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
export AWS_ENDPOINT_URL=http://localhost:8787
|
|
283
|
+
aws sts get-caller-identity
|
|
284
|
+
{
|
|
285
|
+
"UserId": "AIDAM7J2TJYHV8BHEVIO",
|
|
286
|
+
"Account": "888888888888",
|
|
287
|
+
"Arn": "arn:aws:iam::888888888888:user/Widgets"
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
An assumed-role session reports its own session ARN, as it does in real AWS, and its user id joins the Role's id to the session name. A caller with no identity is refused, since there is nothing to answer with.
|
|
292
|
+
|
|
293
|
+
### Assuming a Role over the endpoint
|
|
294
|
+
|
|
295
|
+
`sts assume-role` answers with temporary credentials, and those credentials sign the requests that
|
|
296
|
+
follow:
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
export AWS_ENDPOINT_URL=http://localhost:8787
|
|
300
|
+
aws sts assume-role --role-arn arn:aws:iam::888888888888:role/Reader --role-session-name probe
|
|
301
|
+
{
|
|
302
|
+
"Credentials": {
|
|
303
|
+
"AccessKeyId": "ASIAQ3JZQ6XKFPWLZ4TM",
|
|
304
|
+
"SecretAccessKey": "T4rBqYbLXKsJ0nZuV9dHc2Wm1PfAeR7gSjNyIvXo",
|
|
305
|
+
"SessionToken": "IQoJb3JpZ2luX2VjEHkaCXVzLWVhc3QtMSJHMEUCIQ",
|
|
306
|
+
"Expiration": "2026-08-18T21:00:00.000Z"
|
|
307
|
+
},
|
|
308
|
+
"AssumedRoleUser": {
|
|
309
|
+
"AssumedRoleId": "AROA5KQZH2XWNDLB7YTVR:probe",
|
|
310
|
+
"Arn": "arn:aws:sts::888888888888:assumed-role/Reader/probe"
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Hand the three credential values to an SDK client, or to `AWS_ACCESS_KEY_ID`,
|
|
316
|
+
`AWS_SECRET_ACCESS_KEY` and `AWS_SESSION_TOKEN`, and the whole assume-then-work sequence runs
|
|
317
|
+
against the endpoint URL. Simulated IAM authorizes each request against the Role behind the session,
|
|
318
|
+
and `get-caller-identity` reports the session ARN above.
|
|
319
|
+
|
|
320
|
+
`DurationSeconds` and `ExternalId` are read from the request as they are in process. The expiry is
|
|
321
|
+
stamped from the simulation's own clock. `simAws.clock().advanceBy({ hours: 2 })` takes a session
|
|
322
|
+
past it, and the credentials stop authenticating. An SDK refreshes an expired session by assuming
|
|
323
|
+
again.
|
|
324
|
+
|
|
325
|
+
A Role whose trust policy refuses the caller comes back as `AccessDenied`, under the name real STS
|
|
326
|
+
raises it.
|
|
327
|
+
|
|
328
|
+
`AssumeRole` and `GetCallerIdentity` are the two operations simulated STS implements, and both are
|
|
329
|
+
served. `AssumeRoleWithWebIdentity` and `GetSessionToken` are refused as `NotImplemented`.
|
|
330
|
+
|
|
331
|
+
### IAM over the endpoint
|
|
332
|
+
|
|
333
|
+
A served request runs as whoever signed it, and the credentials to sign one used to come from the
|
|
334
|
+
process that built the simulation. `aws iam` closes that circle. A container or a shell script
|
|
335
|
+
creates its own User over the endpoint, gives it a policy, asks for an access key and signs
|
|
336
|
+
everything after that with what it was answered:
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
export AWS_ENDPOINT_URL=http://localhost:8787
|
|
340
|
+
aws iam create-user --user-name widgets
|
|
341
|
+
aws iam put-user-policy --user-name widgets --policy-name everything \
|
|
342
|
+
--policy-document '{"Version":"2012-10-17","Statement":{"Effect":"Allow","Action":"*","Resource":"*"}}'
|
|
343
|
+
aws iam create-access-key --user-name widgets
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
`create-access-key` answers with the secret, and nothing reports it again. The first request still
|
|
347
|
+
has to be signed by somebody. The [example above](#pointing-an-aws-sdk-or-the-cli-at-the-simulation)
|
|
348
|
+
builds one identity in process, and everything after it can be built over the port.
|
|
349
|
+
|
|
350
|
+
The fifteen operations simulated IAM implements are:
|
|
351
|
+
|
|
352
|
+
- **Users** — `CreateUser`, `CreateAccessKey`, `PutUserPolicy`
|
|
353
|
+
- **Roles** — `CreateRole`, `GetRole`, `ListRoles`, `DeleteRole`, `AttachRolePolicy`,
|
|
354
|
+
`DetachRolePolicy`, `PutRolePolicy`, `DeleteRolePolicy`
|
|
355
|
+
- **Managed policies** — `CreatePolicy`, `GetPolicy`, `ListPolicies`, `DeletePolicy`
|
|
356
|
+
|
|
357
|
+
Anything else is refused as `NotImplemented`, which an SDK raises under that name.
|
|
358
|
+
|
|
359
|
+
### ELBv2 over the endpoint
|
|
360
|
+
|
|
361
|
+
`aws elbv2` builds an Application Load Balancer in the simulation over the same endpoint URL:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
export AWS_ENDPOINT_URL=http://localhost:8787
|
|
365
|
+
aws elbv2 create-load-balancer --name shop-alb --subnets subnet-1 subnet-2
|
|
366
|
+
aws elbv2 create-target-group --name checkout-tg --target-type lambda
|
|
367
|
+
aws elbv2 create-listener --load-balancer-arn <arn> --protocol HTTP --port 80 \
|
|
368
|
+
--default-actions Type=forward,TargetGroupArn=<arn>
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
A load balancer built this way is the same load balancer an in-process build produces. Its DNS name
|
|
372
|
+
is served on the local port, and a request to it reaches the simulated Functions and ECS Services
|
|
373
|
+
registered behind it.
|
|
374
|
+
|
|
375
|
+
The twenty-two operations simulated ELBv2 implements are:
|
|
376
|
+
|
|
377
|
+
- **Load balancers** — `CreateLoadBalancer`, `DescribeLoadBalancers`, `DeleteLoadBalancer`
|
|
378
|
+
- **Target groups** — `CreateTargetGroup`, `DescribeTargetGroups`, `ModifyTargetGroup`,
|
|
379
|
+
`DeleteTargetGroup`
|
|
380
|
+
- **Targets** — `RegisterTargets`, `DeregisterTargets`, `DescribeTargetHealth`
|
|
381
|
+
- **Listeners** — `CreateListener`, `DescribeListeners`, `ModifyListener`, `DeleteListener`
|
|
382
|
+
- **Listener certificates** — `AddListenerCertificates`, `RemoveListenerCertificates`,
|
|
383
|
+
`DescribeListenerCertificates`
|
|
384
|
+
- **Rules** — `CreateRule`, `DescribeRules`, `ModifyRule`, `DeleteRule`, `SetRulePriorities`
|
|
385
|
+
|
|
386
|
+
A rule matches on `host-header` and `path-pattern`, in either the plain `Values` form or the
|
|
387
|
+
per-field configuration. A condition naming any other field is refused by name, as is an operation
|
|
388
|
+
outside the list above.
|
|
389
|
+
|
|
390
|
+
### S3 over the endpoint
|
|
391
|
+
|
|
392
|
+
`aws s3` and an `S3Client` reach simulated S3 through the same endpoint URL:
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
export AWS_ENDPOINT_URL=http://localhost:8787
|
|
396
|
+
aws s3api create-bucket --bucket widgets
|
|
397
|
+
aws s3api put-object --bucket widgets --key one.txt --body ./one.txt
|
|
398
|
+
aws s3 ls s3://widgets/
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
An SDK client needs `forcePathStyle`, because a virtual-host request puts the Bucket in the hostname and this endpoint routes on the credential scope rather than the host:
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
const client = new S3Client({
|
|
405
|
+
region: "us-east-1",
|
|
406
|
+
endpoint: `http://localhost:${srv.port}`,
|
|
407
|
+
forcePathStyle: true,
|
|
408
|
+
credentials,
|
|
409
|
+
});
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
The operations served are the ones simulated S3 implements: `CreateBucket`, `DeleteBucket`, `HeadBucket`, `ListBuckets`, `ListObjects`, `ListObjectsV2`, `GetObject`, `HeadObject`, `PutObject`, `DeleteObject`, `DeleteObjects`, the six multipart upload operations, and the Bucket policy, website, Block Public Access and event notification configurations. `aws s3 cp` works in both directions, and for a file above the CLI's 8MB multipart threshold. A `GetObject` carrying a `Range` is answered `206 Partial Content` with the bytes it asked for, and that is how the CLI takes a large file back out. Anything else is refused as `NotImplemented`, which an SDK raises under that name rather than leaving a client to guess.
|
|
413
|
+
|
|
414
|
+
Simulated S3 also answers its own Bucket hostnames, covered above. That path is unchanged, and it is what a website visitor uses.
|
|
415
|
+
|
|
416
|
+
A presigned URL reaches an Object either way. `getSignedUrl` signs for whatever endpoint its client was configured with, and a URL signed for this one states its credential scope in an `X-Amz-Credential` parameter. The endpoint reads the scope from there whenever a URL states one, since a presigned URL carries no `Authorization` header of its own. Set `forcePathStyle` on the presigning client for the same reason an ordinary request needs it.
|
|
417
|
+
|
|
418
|
+
### SNS over the endpoint
|
|
419
|
+
|
|
420
|
+
`aws sns` and an `SNSClient` reach simulated SNS through the same endpoint URL:
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
export AWS_ENDPOINT_URL=http://localhost:8787
|
|
424
|
+
aws sns create-topic --name orders
|
|
425
|
+
aws sns publish --topic-arn arn:aws:sns:us-east-1:888888888888:orders --message hello
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
A message published here fans out to the simulated Queues and Functions subscribed to the topic, the
|
|
429
|
+
same way an in-process publish does. Delivery runs on the background scheduler (as it does on real
|
|
430
|
+
SNS, after the publish has been answered), so a test reading the Queue afterwards waits on
|
|
431
|
+
`simAws.backgroundTasksComplete()` first.
|
|
432
|
+
|
|
433
|
+
The operations served are the sixteen simulated SNS implements:
|
|
434
|
+
|
|
435
|
+
- **Topics** — `CreateTopic`, `DeleteTopic`, `ListTopics`, `GetTopicAttributes`, `SetTopicAttributes`
|
|
436
|
+
- **Subscriptions** — `Subscribe`, `Unsubscribe`, `ListSubscriptions`, `ListSubscriptionsByTopic`,
|
|
437
|
+
`GetSubscriptionAttributes`, `SetSubscriptionAttributes`
|
|
438
|
+
- **Messages** — `Publish`, `PublishBatch`
|
|
439
|
+
- **SMS** — `CheckIfPhoneNumberIsOptedOut`, `ListPhoneNumbersOptedOut`, `OptInPhoneNumber`
|
|
440
|
+
|
|
441
|
+
Anything else is refused as `NotImplemented`, which an SDK raises under that name.
|
|
442
|
+
|
|
443
|
+
### CloudFormation over the endpoint
|
|
444
|
+
|
|
445
|
+
`aws cloudformation` deploys a template into the simulation over the same endpoint URL:
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
export AWS_ENDPOINT_URL=http://localhost:8787
|
|
449
|
+
aws cloudformation create-stack --stack-name site --template-body file://template.json
|
|
450
|
+
aws cloudformation describe-stacks
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
The four operations simulated CloudFormation implements are `CreateStack`, `UpdateStack`,
|
|
454
|
+
`DeleteStack` and `DescribeStacks`. A deployment starts in the background and the call is answered
|
|
455
|
+
before the Resources exist, as real CloudFormation answers it. `describe-stacks` reports the status
|
|
456
|
+
it reached, and `waitForStackDeployComplete` waits for it in process.
|
|
457
|
+
|
|
458
|
+
### Lambda over the endpoint
|
|
459
|
+
|
|
460
|
+
`aws lambda` and a `LambdaClient` reach simulated Lambda through the same endpoint URL:
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
export AWS_ENDPOINT_URL=http://localhost:8787
|
|
464
|
+
aws lambda invoke --function-name orders --payload '{"id":1}' /tmp/out.json
|
|
465
|
+
cat /tmp/out.json
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
The function runs in the process serving it, and what its handler returned is written to the file.
|
|
469
|
+
A handler that throws answers `200` with `FunctionError` set to `Unhandled`, and the payload holds
|
|
470
|
+
the error it threw. `--invocation-type Event` answers `202` and runs the handler on the background
|
|
471
|
+
scheduler. A test that goes on to read what the function did waits on
|
|
472
|
+
`simAws.backgroundTasksComplete()` first.
|
|
473
|
+
|
|
474
|
+
A function created over the endpoint carries its code as a zip archive, which is the shape the
|
|
475
|
+
`Code.ZipFile` member travels in over HTTP:
|
|
476
|
+
|
|
477
|
+
```bash
|
|
478
|
+
aws lambda create-function --function-name orders \
|
|
479
|
+
--role arn:aws:iam::888888888888:role/OrdersRole \
|
|
480
|
+
--handler index.handler --runtime nodejs22.x \
|
|
481
|
+
--zip-file fileb://orders.zip
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
The operations served are nineteen of the ones simulated Lambda implements:
|
|
485
|
+
|
|
486
|
+
- **Functions** — `CreateFunction`, `GetFunction`, `UpdateFunctionCode`,
|
|
487
|
+
`UpdateFunctionConfiguration`, `ListFunctions`, `DeleteFunction`, `Invoke`
|
|
488
|
+
- **Function URLs** — `CreateFunctionUrlConfig`, `GetFunctionUrlConfig`,
|
|
489
|
+
`UpdateFunctionUrlConfig`, `DeleteFunctionUrlConfig`, `ListFunctionUrlConfigs`
|
|
490
|
+
- **Permissions** — `AddPermission`, `RemovePermission`, `GetPolicy`
|
|
491
|
+
- **Event source mappings** — `CreateEventSourceMapping`, `GetEventSourceMapping`,
|
|
492
|
+
`ListEventSourceMappings`, `DeleteEventSourceMapping`
|
|
493
|
+
|
|
494
|
+
The version and alias operations have no route here yet, and reach the simulation through `SimAws`
|
|
495
|
+
or SDK interception instead.
|
|
496
|
+
|
|
497
|
+
Anything else is refused as `NotImplemented`, which an SDK raises under that name. The refusal names
|
|
498
|
+
the path it arrived at. `aws lambda get-function-configuration` reports that
|
|
499
|
+
`GET /2015-03-31/functions/{name}/configuration` is unserved, and an unimplemented operation sharing
|
|
500
|
+
a method with one that is served gets the same answer.
|
|
501
|
+
|
|
502
|
+
Simulated Lambda also answers its own Function URL hostnames, covered above. That path is unchanged,
|
|
503
|
+
and it is what a browser and a webhook reach.
|
|
504
|
+
|
|
505
|
+
## Stopping and restarting
|
|
506
|
+
|
|
507
|
+
`close()` stops serving and lets go of everything Yulin was holding, leaving the process free to
|
|
508
|
+
exit. That is the HTTP port, the DNS port and the connections the server is holding, along with the
|
|
509
|
+
simulated environment it was serving with them. The environment covers the template files a
|
|
510
|
+
deployment is [watching](https://yulinsim.dev/services/cloudformation/#watching-a-template-file) and the
|
|
511
|
+
directories a [mount](https://yulinsim.dev/services/s3/#reloading-the-browser-when-the-directory-changes) is
|
|
512
|
+
watching, in whichever Account and Region each of them lives in. One call covers all of it. A script
|
|
513
|
+
that hangs on exit is not a hunt for the handle you missed.
|
|
514
|
+
|
|
515
|
+
It returns a promise that settles once the last thing the server had to say has gone. A script that
|
|
516
|
+
means to exit under its own steam has something to wait for. Yulin installs no signal handlers,
|
|
517
|
+
since a library taking over process signals gets in the way of whatever else the process is doing.
|
|
518
|
+
Call `close()` from your own handler:
|
|
519
|
+
|
|
520
|
+
```typescript sim-serve-shutdown
|
|
521
|
+
/**
|
|
522
|
+
* Closing a served environment when the process is asked to stop.
|
|
523
|
+
*/
|
|
524
|
+
|
|
525
|
+
import { serveSimAws } from "@kensio/yulin/serve";
|
|
526
|
+
|
|
527
|
+
const srv = await serveSimAws({ port: 8787 });
|
|
528
|
+
|
|
529
|
+
async function stopServing(): Promise<void> {
|
|
530
|
+
// Waiting means anything the server still had to say has gone before the
|
|
531
|
+
// process does.
|
|
532
|
+
await srv.close();
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
process.on("SIGTERM", () => {
|
|
536
|
+
void stopServing();
|
|
537
|
+
});
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Closing twice is safe, and so is closing a server whose environment started nothing. What closes is
|
|
541
|
+
the handles that keep the process alive. Every simulated Bucket, Table and Stack is where it was,
|
|
542
|
+
and the environment goes on working. A script can close and carry on.
|
|
543
|
+
|
|
544
|
+
An unserved environment has the same call on it. `simAws.close()` lets go of its template file
|
|
545
|
+
watches and mounted directory watches, and a test with one of those closes it in one line:
|
|
546
|
+
|
|
547
|
+
```typescript sim-serve-close-environment
|
|
548
|
+
/**
|
|
549
|
+
* Letting go of what an unserved environment is holding.
|
|
550
|
+
*/
|
|
551
|
+
|
|
552
|
+
import { SimAws } from "@kensio/yulin";
|
|
553
|
+
|
|
554
|
+
const simAws = new SimAws();
|
|
555
|
+
|
|
556
|
+
await simAws.cloudFormation().deployTemplateFile({
|
|
557
|
+
templatePath: "cdk.out/TestStack.template.json",
|
|
558
|
+
watch: true,
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// The Stack, and everything the template deployed, is still there afterwards.
|
|
562
|
+
await simAws.close();
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### Asking for a signal handler
|
|
566
|
+
|
|
567
|
+
The handler above is yours to write, and that is the point. Your script decides what a signal means
|
|
568
|
+
and in what order things happen. A script that wants the usual behaviour can ask for it instead, and
|
|
569
|
+
gets the same close on `SIGINT` and `SIGTERM`:
|
|
570
|
+
|
|
571
|
+
```typescript sim-serve-close-on-signal
|
|
572
|
+
/**
|
|
573
|
+
* Asking for the signal handler rather than writing one.
|
|
574
|
+
*/
|
|
575
|
+
|
|
576
|
+
import { SimAws } from "@kensio/yulin";
|
|
577
|
+
import { serveSimAws } from "@kensio/yulin/serve";
|
|
578
|
+
|
|
579
|
+
const simAws = new SimAws();
|
|
580
|
+
const srv = await serveSimAws({ simAws, port: 8787, liveReload: true });
|
|
581
|
+
|
|
582
|
+
// Build the simulated environment the pages are served from here.
|
|
583
|
+
|
|
584
|
+
// Asking is the whole of it. The handler closes the server and the environment
|
|
585
|
+
// it serves, and the process then exits on its own.
|
|
586
|
+
const stopListening = srv.closeOnSignal();
|
|
587
|
+
|
|
588
|
+
// A script that stops wanting the handler before the process ends takes it off
|
|
589
|
+
// again:
|
|
590
|
+
stopListening();
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
The handler goes on at that call and no sooner. A process that never asks keeps its signals to
|
|
594
|
+
itself. `closeOnSignal({ signals: ["SIGHUP"] })` names other signals. The handlers come off as the
|
|
595
|
+
first one arrives, and a second Ctrl-C from someone who has waited long enough lands on Node's own
|
|
596
|
+
default and ends the process. Closing never exits the process itself. It lets go of what Yulin was
|
|
597
|
+
holding, and a process with no work left then exits on its own. `SimAws` has the same method, for an
|
|
598
|
+
unserved environment.
|
|
599
|
+
|
|
600
|
+
A restart usually overlaps the process it replaces. `listen` waits a couple of seconds for a pinned
|
|
601
|
+
port that is still held, then throws `SimAwsLocalPortInUse` naming the port. By then something other
|
|
602
|
+
than the outgoing process owns it.
|
|
603
|
+
|
|
604
|
+
## Live reload
|
|
605
|
+
|
|
606
|
+
A page served from a simulated Bucket website, CloudFront distribution, Function URL or HTTP API has
|
|
607
|
+
only Yulin in its response path. Yulin is the one thing that can tell the browser to reload. Turning
|
|
608
|
+
`liveReload` on serves a reload channel and puts a small script into the HTML pages it serves to
|
|
609
|
+
browsers:
|
|
610
|
+
|
|
611
|
+
```typescript sim-serve-live-reload
|
|
612
|
+
/**
|
|
613
|
+
* Serving with live reload, so a browser reloads itself when the process
|
|
614
|
+
* restarts.
|
|
615
|
+
*/
|
|
616
|
+
|
|
617
|
+
import { SimAws } from "@kensio/yulin";
|
|
618
|
+
import { serveSimAws } from "@kensio/yulin/serve";
|
|
619
|
+
|
|
620
|
+
const simAws = new SimAws();
|
|
621
|
+
const srv = await serveSimAws({ simAws, port: 8787, liveReload: true });
|
|
622
|
+
|
|
623
|
+
// Build the simulated environment the pages are served from here.
|
|
624
|
+
|
|
625
|
+
async function stopServing(): Promise<void> {
|
|
626
|
+
// Waiting means the browsers hear about the restart before the process goes.
|
|
627
|
+
await srv.close();
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
process.on("SIGTERM", () => {
|
|
631
|
+
void stopServing();
|
|
632
|
+
});
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
It is off by default, and every response is then byte for byte what it would otherwise be.
|
|
636
|
+
|
|
637
|
+
### Reloading on a restart
|
|
638
|
+
|
|
639
|
+
Local development means restarting the process, because a changed setup script or Lambda handler
|
|
640
|
+
needs a fresh module graph. The script survives that on its own, with no supervisor process and no
|
|
641
|
+
shared state between the outgoing and incoming process.
|
|
642
|
+
|
|
643
|
+
The channel is Server-Sent Events, and the browser reconnects by itself. Each process has a boot id
|
|
644
|
+
and sends it to every page that connects. A page that reconnects and finds a different boot id knows
|
|
645
|
+
it is showing output from a process that has gone, and reloads. A blip on a still running process
|
|
646
|
+
hands back the same boot id, and the page carries on.
|
|
647
|
+
|
|
648
|
+
`close()` sends a `reloading` event before the connections go. A page then reads the gap it is about
|
|
649
|
+
to see as a restart, and not a server that has died. The page keeps its own appearance and gets a
|
|
650
|
+
`data-sim-aws-live-reload="reloading"` attribute on its `<html>` element, ready to style:
|
|
651
|
+
|
|
652
|
+
```css
|
|
653
|
+
html[data-sim-aws-live-reload="reloading"] {
|
|
654
|
+
opacity: 0.6;
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
The event has to reach the browser before the connection goes. `close()` sees the reload streams out
|
|
659
|
+
before it destroys anything else the server was holding, and its promise settles once they have
|
|
660
|
+
gone. A browser that has stopped answering is waited on for half a second and then dropped. A page
|
|
661
|
+
nobody is looking at cannot hold up a restart. Both ports are released before any of that waiting,
|
|
662
|
+
and a replacement process can take them straight away either way.
|
|
663
|
+
|
|
664
|
+
None of this needs a supervisor process. A dev script started from an IDE debugger gets browser
|
|
665
|
+
reload with the debugger attached throughout.
|
|
666
|
+
|
|
667
|
+
### Reloading without a restart
|
|
668
|
+
|
|
669
|
+
For a change that needs no restart, such as new content in a simulated Bucket, `reload()` reloads
|
|
670
|
+
every connected browser:
|
|
671
|
+
|
|
672
|
+
```typescript sim-serve-reload
|
|
673
|
+
/**
|
|
674
|
+
* Reloading connected browsers after changing simulated content in place.
|
|
675
|
+
*/
|
|
676
|
+
|
|
677
|
+
import { CreateBucketCommand, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
678
|
+
import { SimAws } from "@kensio/yulin";
|
|
679
|
+
import { serveSimAws } from "@kensio/yulin/serve";
|
|
680
|
+
|
|
681
|
+
const simAws = new SimAws();
|
|
682
|
+
const srv = await serveSimAws({ simAws, liveReload: true });
|
|
683
|
+
|
|
684
|
+
await simAws.s3().createBucket(new CreateBucketCommand({ Bucket: "foo-site" }));
|
|
685
|
+
await simAws.s3().putObject(
|
|
686
|
+
new PutObjectCommand({
|
|
687
|
+
Bucket: "foo-site",
|
|
688
|
+
Key: "index.html",
|
|
689
|
+
Body: "<h1>Changed</h1>",
|
|
690
|
+
ContentType: "text/html; charset=utf-8",
|
|
691
|
+
}),
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
srv.reload();
|
|
695
|
+
|
|
696
|
+
await srv.close();
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
`reload()` throws when live reload is off.
|
|
700
|
+
|
|
701
|
+
You can also hand the server to something that reloads for you, either a
|
|
702
|
+
[mounted directory](#reloading-when-a-build-changes-a-mounted-directory) or a
|
|
703
|
+
[watched template file](#answering-a-change-instead-of-restarting), both as `{ reload: srv }`. A
|
|
704
|
+
watched template file refuses a server it could never reload as it is handed over, ahead of the
|
|
705
|
+
first change.
|
|
706
|
+
|
|
707
|
+
### Reloading when a build changes a mounted directory
|
|
708
|
+
|
|
709
|
+
A Bucket mounted on a local directory is already reading the files a site generator writes. A
|
|
710
|
+
rebuild needs nothing copied into it. Hand the mount the server and it watches the directory and
|
|
711
|
+
reloads for you once the writes stop:
|
|
712
|
+
|
|
713
|
+
```typescript sim-serve-mount-reload
|
|
714
|
+
/**
|
|
715
|
+
* A built site the process reloads the browser for, rather than restarting for.
|
|
716
|
+
*/
|
|
717
|
+
|
|
718
|
+
import path from "node:path";
|
|
719
|
+
|
|
720
|
+
import { CreateBucketCommand } from "@aws-sdk/client-s3";
|
|
721
|
+
import { SimAws } from "@kensio/yulin";
|
|
722
|
+
import { serveSimAws } from "@kensio/yulin/serve";
|
|
723
|
+
|
|
724
|
+
const simAws = new SimAws();
|
|
725
|
+
const srv = await serveSimAws({ simAws, port: 8787, liveReload: true });
|
|
726
|
+
|
|
727
|
+
await simAws.s3().createBucket(new CreateBucketCommand({ Bucket: "site" }));
|
|
728
|
+
|
|
729
|
+
simAws
|
|
730
|
+
.s3()
|
|
731
|
+
.mountBucketFilesystem("site", path.join(process.cwd(), "articles/public"), {
|
|
732
|
+
reload: srv,
|
|
733
|
+
});
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
One build is one reload, however many files it wrote. See
|
|
737
|
+
[filesystem-backed Bucket storage](https://yulinsim.dev/services/s3/#reloading-the-browser-when-the-directory-changes)
|
|
738
|
+
for the `settleMs` override and for stopping the watch.
|
|
739
|
+
|
|
740
|
+
### What gets the script
|
|
741
|
+
|
|
742
|
+
Yulin's own account of a request goes in headers and never in the body. A response keeps the shape
|
|
743
|
+
the real service returns. Live reload breaks that rule on purpose, and only for a page a browser is
|
|
744
|
+
about to render. A response gets the script only when all of this holds:
|
|
745
|
+
|
|
746
|
+
- the response media type is `text/html` exactly
|
|
747
|
+
- the request `accept` header asks for `text/html`
|
|
748
|
+
- the request carries no SigV4 signature, in either an `authorization` header or the query string of
|
|
749
|
+
a presigned URL, and no `x-amz-*` header
|
|
750
|
+
- the response has no `content-encoding`
|
|
751
|
+
- the response status is other than 206, and the request is not a `HEAD`
|
|
752
|
+
|
|
753
|
+
So an SDK `GetObject` for an HTML Object gets the stored bytes, byte for byte, while a browser
|
|
754
|
+
looking at the same Object through a website endpoint gets the script.
|
|
755
|
+
|
|
756
|
+
An injected response carries `x-sim-aws-live-reload: injected`. Its `content-length` is recomputed,
|
|
757
|
+
and `etag` and `last-modified` are dropped, since the bytes are no longer the ones those headers
|
|
758
|
+
describe. Its `cache-control` is set to `no-store`, replacing whatever the service said, because a
|
|
759
|
+
page held in the browser cache is a page live reload cannot reach. The script goes in before
|
|
760
|
+
`</body>`, or before `</html>` when there is no body element, or on the end when the HTML has no
|
|
761
|
+
such element.
|
|
762
|
+
|
|
763
|
+
### The reserved path
|
|
764
|
+
|
|
765
|
+
The reload channel is served at `/__sim-aws/live-reload` on every hostname the server answers on. A
|
|
766
|
+
page can ask for it relative to wherever it was served from. While live reload is on, no simulated
|
|
767
|
+
service can serve anything at that path.
|
|
768
|
+
|
|
769
|
+
## Messages on the console
|
|
770
|
+
|
|
771
|
+
A simulated Cognito user pool records the verification message it would have sent, simulated SNS
|
|
772
|
+
records the text message it would have texted, and simulated SES records the email it accepted.
|
|
773
|
+
Reading any of them back takes test code, or the pool's `/<userPoolId>/messages` listing. Either is
|
|
774
|
+
a detour when the sign-up form is open in a browser and the confirmation code is the one thing you
|
|
775
|
+
want. A served environment prints them as they are recorded:
|
|
776
|
+
|
|
777
|
+
```
|
|
778
|
+
sim Cognito eu-west-2_aBcDeFgHi: email to alice@example.com (SignUp)
|
|
779
|
+
Subject: Your verification code
|
|
780
|
+
Your confirmation code is 483920
|
|
781
|
+
sim SNS: SMS to +15550100
|
|
782
|
+
Your one-time code is 118221
|
|
783
|
+
sim SES: hello@example.com to alice@example.com
|
|
784
|
+
Subject: Welcome to Example
|
|
785
|
+
Text body:
|
|
786
|
+
Glad to have you here.
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
Nothing has to ask for that. Narrow it with `messageLogging`, which holds one property per kind of
|
|
790
|
+
message:
|
|
791
|
+
|
|
792
|
+
```typescript sim-serve-message-logging
|
|
793
|
+
/**
|
|
794
|
+
* Serving with the text messages left off, and the pool messages still
|
|
795
|
+
* printed.
|
|
796
|
+
*/
|
|
797
|
+
|
|
798
|
+
import { SimAws } from "@kensio/yulin";
|
|
799
|
+
import { serveSimAws } from "@kensio/yulin/serve";
|
|
800
|
+
|
|
801
|
+
const simAws = new SimAws();
|
|
802
|
+
const srv = await serveSimAws({
|
|
803
|
+
simAws,
|
|
804
|
+
port: 8787,
|
|
805
|
+
messageLogging: { sns: false },
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
// Serve the pages that sign a user up here.
|
|
809
|
+
|
|
810
|
+
await srv.close();
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
`messageLogging: false` prints none of them.
|
|
814
|
+
|
|
815
|
+
An SMS the opt-out list stopped is printed with the suppression named. The publish succeeded, and a
|
|
816
|
+
handset that will never see the code is worth being told about rather than waited on:
|
|
817
|
+
|
|
818
|
+
```
|
|
819
|
+
sim SNS: SMS to +15550100 (suppressed, number opted out)
|
|
820
|
+
Your one-time code is 118221
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
### What an email prints as
|
|
824
|
+
|
|
825
|
+
An email is summarised. Printing an HTML part in full would run to kilobytes of markup and push the
|
|
826
|
+
sender, the recipients and the subject off the screen, and those are what say which send this was.
|
|
827
|
+
The first line carries the sender and the three recipient lists. The subject follows indented under
|
|
828
|
+
it, then the template the message was rendered from (where a stored template was) and its data, then
|
|
829
|
+
the text part. An HTML part is measured and left out:
|
|
830
|
+
|
|
831
|
+
```
|
|
832
|
+
sim SES: hello@example.com to alice@example.com, bcc audit@example.com
|
|
833
|
+
Subject: Reset your password
|
|
834
|
+
Template: password-reset {"code":"483920"}
|
|
835
|
+
Text body:
|
|
836
|
+
Follow this link to reset your password.
|
|
837
|
+
https://app.example.com/reset?token=abc123
|
|
838
|
+
HTML body: 4.1 kB, not printed
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
A text part is printed up to 2000 characters. What runs past that is counted and left out:
|
|
842
|
+
|
|
843
|
+
```
|
|
844
|
+
Text body:
|
|
845
|
+
Here is your monthly statement in full.
|
|
846
|
+
... 4162 more characters, not printed
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
A user pool sending its email through simulated SES prints twice, once for the SES send and once for
|
|
850
|
+
the message the pool kept. Both services recorded it, and each block says what that service holds.
|
|
851
|
+
See [Sending a pool's email through SES](https://yulinsim.dev/services/cognito/#sending-a-pools-email-through-ses).
|
|
852
|
+
|
|
853
|
+
`emailTextLimit` moves that limit. It sits beside the per-kind switches on the same option:
|
|
854
|
+
|
|
855
|
+
```typescript sim-serve-email-text-limit
|
|
856
|
+
/**
|
|
857
|
+
* Serving with a shorter limit on the email text that reaches the console.
|
|
858
|
+
*/
|
|
859
|
+
|
|
860
|
+
import { SimAws } from "@kensio/yulin";
|
|
861
|
+
import { serveSimAws } from "@kensio/yulin/serve";
|
|
862
|
+
|
|
863
|
+
const simAws = new SimAws();
|
|
864
|
+
const srv = await serveSimAws({
|
|
865
|
+
simAws,
|
|
866
|
+
port: 8787,
|
|
867
|
+
messageLogging: { emailTextLimit: 500 },
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
// Serve the pages that send the email here.
|
|
871
|
+
|
|
872
|
+
await srv.close();
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
Only what happens while the server is up reaches the console. A message recorded before it started
|
|
876
|
+
listening, or after `close()`, is on the service's own record and nowhere else. `sentMessages()`,
|
|
877
|
+
`sentSmsMessages()` and `sentEmails()` are still where the whole history is.
|
|
878
|
+
|
|
879
|
+
## Restarting on a file change
|
|
880
|
+
|
|
881
|
+
Live reload gets a page back on its feet after the process restarts. `yulin watch` is what restarts
|
|
882
|
+
it. Run the dev script through it and a save is the whole loop:
|
|
883
|
+
|
|
884
|
+
```bash
|
|
885
|
+
yulin watch -- tsx dev.ts
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
Everything after `--` is the command, run as written and restarted when something changes. The CLI
|
|
889
|
+
never imports it, never looks for an exported setup function, and takes no interest in whether the
|
|
890
|
+
simulation was built from SDK commands, a CloudFormation template, or several `SimAws` instances at
|
|
891
|
+
once. The only change to a dev script is turning `liveReload` on.
|
|
892
|
+
|
|
893
|
+
Restarting is the deliberate choice, over swapping code in place. A Lambda handler is a function
|
|
894
|
+
reference out of your own module graph, and a new process re-imports it with no module cache to
|
|
895
|
+
defeat. It also keeps simulated state the same as what a fresh test run sees, since seeding is part
|
|
896
|
+
of the setup script and runs again.
|
|
897
|
+
|
|
898
|
+
### What is watched
|
|
899
|
+
|
|
900
|
+
The working directory, minus the paths nobody edits by hand. Those are `node_modules`, `.git`,
|
|
901
|
+
`dist`, `coverage`, CDK asset directories, and the working files an editor writes around a save.
|
|
902
|
+
|
|
903
|
+
On top of that, Yulin names paths it is holding that the module graph never mentions. A directory
|
|
904
|
+
given to `mountBucketFilesystem` and a template given to `deployTemplateFile` are reported to the
|
|
905
|
+
supervisor as they are registered, and watched from then on, without appearing in any list. Editing
|
|
906
|
+
a file in a mounted directory or re-synthing a stack restarts the process.
|
|
907
|
+
|
|
908
|
+
A path the process is watching itself is the exception, and is
|
|
909
|
+
[left to the process reading it](#answering-a-change-instead-of-restarting). That covers a template
|
|
910
|
+
deployed with the `watch` option, and a directory mounted with somewhere to reload. So is any other
|
|
911
|
+
path the process [says it is holding](#holding-a-path-yourself).
|
|
912
|
+
|
|
913
|
+
### One restart for a burst of writes
|
|
914
|
+
|
|
915
|
+
A burst of writes is one restart. Saving one file is several filesystem events, and changes are held
|
|
916
|
+
until they stop arriving before anything is restarted. The wait is 250ms by default. A build writing
|
|
917
|
+
hundreds of files gets one restart, however many files it wrote.
|
|
918
|
+
|
|
919
|
+
The number is set by what a build needs, and a save pays for it. macOS hands a recursive watch its
|
|
920
|
+
events in waves. A build writing several thousand files was measured arriving as tens of waves up to
|
|
921
|
+
49ms apart, and a window anywhere near that turns one build into several restarts. A build that
|
|
922
|
+
pauses between its own phases, as a tool that resolves before it writes does, pauses for longer than
|
|
923
|
+
that again. 250ms clears the waves several times over and covers the shorter of those pauses, at the
|
|
924
|
+
cost of 250ms before a plain save is acted on.
|
|
925
|
+
|
|
926
|
+
A project whose build is unusual can say so on the command line:
|
|
927
|
+
|
|
928
|
+
```bash
|
|
929
|
+
yulin watch --settle=600 -- tsx dev.ts
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
Writes that keep arriving push the wait back. A build that never goes quiet would otherwise hold the
|
|
933
|
+
restart off for as long as it ran, so a burst is acted on after five seconds however much is still
|
|
934
|
+
arriving, and the writes after that are a burst of their own. That is a backstop for a build that
|
|
935
|
+
writes continuously for minutes. An ordinary build never reaches it.
|
|
936
|
+
|
|
937
|
+
### Holding a path yourself
|
|
938
|
+
|
|
939
|
+
A process that is already watching a path and answering changes to it in place has nothing to gain
|
|
940
|
+
from a restart, and everything its simulation holds to lose. `simWatch.reportHeldPath(...)` says so,
|
|
941
|
+
and the supervisor leaves that path alone from then on:
|
|
942
|
+
|
|
943
|
+
```typescript sim-serve-hold-path
|
|
944
|
+
/**
|
|
945
|
+
* A mounted directory this process watches itself, reloading the browser
|
|
946
|
+
* rather than being restarted for it.
|
|
947
|
+
*/
|
|
948
|
+
|
|
949
|
+
import { watch } from "node:fs";
|
|
950
|
+
import path from "node:path";
|
|
951
|
+
|
|
952
|
+
import { CreateBucketCommand } from "@aws-sdk/client-s3";
|
|
953
|
+
import { SimAws } from "@kensio/yulin";
|
|
954
|
+
import { serveSimAws } from "@kensio/yulin/serve";
|
|
955
|
+
import { simWatch } from "@kensio/yulin/watch";
|
|
956
|
+
|
|
957
|
+
const built = path.join(process.cwd(), "public");
|
|
958
|
+
|
|
959
|
+
const simAws = new SimAws();
|
|
960
|
+
await simAws.s3().createBucket(new CreateBucketCommand({ Bucket: "site" }));
|
|
961
|
+
simAws.s3().mountBucketFilesystem("site", built);
|
|
962
|
+
|
|
963
|
+
const srv = await serveSimAws({ simAws, port: 8787, liveReload: true });
|
|
964
|
+
|
|
965
|
+
simWatch.reportHeldPath(built);
|
|
966
|
+
|
|
967
|
+
watch(built, { recursive: true }, () => {
|
|
968
|
+
srv.reload();
|
|
969
|
+
});
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
A static site build writing into that directory then reloads the page, where the mount alone would
|
|
973
|
+
have restarted the process and taken every simulated Bucket, Table and Stack with it. Holding a path
|
|
974
|
+
beats having reported it. Being told a path is answered in the process is more specific than being
|
|
975
|
+
told it is worth watching, so holding wins even though `mountBucketFilesystem` reported the same
|
|
976
|
+
directory first.
|
|
977
|
+
|
|
978
|
+
A held path stays held only for the run that reported it. A run that exits releases the path, and
|
|
979
|
+
the supervisor watches it again until its replacement says otherwise.
|
|
980
|
+
|
|
981
|
+
`simWatch.onStopping(...)` is the other half, for a restart this process has no say in. It runs just
|
|
982
|
+
before the supervisor kills the process. Live reload uses that moment to tell browsers a reload is
|
|
983
|
+
coming, so a page comes back on its own. A hand-written reload channel wants the same warning.
|
|
984
|
+
|
|
985
|
+
Both are best effort and need a supervisor. In a process `yulin watch` never started, such as a test
|
|
986
|
+
run or a script launched from an IDE debugger, both are no-ops.
|
|
987
|
+
|
|
988
|
+
This is the general case, for a path nothing else knows about. A mounted directory is the one Yulin
|
|
989
|
+
does know about. `mountBucketFilesystem` takes a reload target, holds the path itself and settles
|
|
990
|
+
the writes, so the example above is written for you. See
|
|
991
|
+
[reloading when a build changes a mounted directory](#reloading-when-a-build-changes-a-mounted-directory).
|
|
992
|
+
|
|
993
|
+
### Answering a change instead of restarting
|
|
994
|
+
|
|
995
|
+
A deployment that watches its own template file is left alone by the supervisor:
|
|
996
|
+
|
|
997
|
+
```typescript sim-serve-watch-template
|
|
998
|
+
/**
|
|
999
|
+
* A template the process updates its stack from, rather than restarting for.
|
|
1000
|
+
*/
|
|
1001
|
+
|
|
1002
|
+
import { SimAws } from "@kensio/yulin";
|
|
1003
|
+
import { serveSimAws } from "@kensio/yulin/serve";
|
|
1004
|
+
|
|
1005
|
+
const simAws = new SimAws();
|
|
1006
|
+
const srv = await serveSimAws({ simAws, port: 8787, liveReload: true });
|
|
1007
|
+
|
|
1008
|
+
await simAws.cloudFormation().deployTemplateFile({
|
|
1009
|
+
templatePath: "cdk.out/TestStack.template.json",
|
|
1010
|
+
watch: {
|
|
1011
|
+
reload: srv,
|
|
1012
|
+
},
|
|
1013
|
+
});
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
Re-synthing the stack then updates it in place and reloads the page. The reload waits for the update
|
|
1017
|
+
and not the write. The page comes back on the resources the new template asked for, and an update
|
|
1018
|
+
that failed reloads nothing. Whatever the change left alone keeps what it holds in simulated S3,
|
|
1019
|
+
DynamoDB and SQS, where a restart would have taken all of it. The process names the template as one
|
|
1020
|
+
it is answering itself, and the supervisor takes it off its own list. See
|
|
1021
|
+
[watching a template file](https://yulinsim.dev/services/cloudformation/#watching-a-template-file) for what
|
|
1022
|
+
an update does to the resources.
|
|
1023
|
+
|
|
1024
|
+
A directory mounted with somewhere to reload is left alone the same way. The Bucket is reading the
|
|
1025
|
+
files either way, and a rebuild has nothing to redo. The browser is reloaded, and everything else
|
|
1026
|
+
the process is holding stays where it is. A restart would have taken the whole simulated environment
|
|
1027
|
+
for the sake of a page that changed. See
|
|
1028
|
+
[reloading when a build changes a mounted directory](#reloading-when-a-build-changes-a-mounted-directory).
|
|
1029
|
+
|
|
1030
|
+
A template synthesized against a real account sometimes needs adapting before Yulin will take it.
|
|
1031
|
+
`transform` is given the parsed template and answers with the one to deploy, on the deployment and
|
|
1032
|
+
again on every change. The file the supervisor leaves alone is still the one in `cdk.out`:
|
|
1033
|
+
|
|
1034
|
+
```typescript sim-serve-transform-template
|
|
1035
|
+
/**
|
|
1036
|
+
* Adapting a watched template, so the file being watched is the real one.
|
|
1037
|
+
*/
|
|
1038
|
+
|
|
1039
|
+
import { SimAws } from "@kensio/yulin";
|
|
1040
|
+
import { serveSimAws } from "@kensio/yulin/serve";
|
|
1041
|
+
|
|
1042
|
+
const simAws = new SimAws();
|
|
1043
|
+
const srv = await serveSimAws({ simAws, port: 8787, liveReload: true });
|
|
1044
|
+
|
|
1045
|
+
await simAws.cloudFormation().deployTemplateFile({
|
|
1046
|
+
templatePath: "cdk.out/TestStack.template.json",
|
|
1047
|
+
transform: (template) => ({
|
|
1048
|
+
...template,
|
|
1049
|
+
Resources: Object.fromEntries(
|
|
1050
|
+
Object.entries(template.Resources).filter(
|
|
1051
|
+
([logicalId]) => logicalId !== "SiteAliasRecord",
|
|
1052
|
+
),
|
|
1053
|
+
),
|
|
1054
|
+
}),
|
|
1055
|
+
watch: {
|
|
1056
|
+
onUpdated: () => {
|
|
1057
|
+
srv.reload();
|
|
1058
|
+
},
|
|
1059
|
+
},
|
|
1060
|
+
});
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
A transform that throws is reported the way a failed update is. The process and the resources it is
|
|
1064
|
+
serving are left where they were. See
|
|
1065
|
+
[adapting a synthesized template](https://yulinsim.dev/services/cloudformation/#adapting-a-synthesized-template-on-the-way-in).
|
|
1066
|
+
|
|
1067
|
+
Both work with no supervisor at all. A dev script started from an IDE debugger picks up a re-synth
|
|
1068
|
+
or a rebuild with the debugger attached throughout.
|
|
1069
|
+
|
|
1070
|
+
### When a run goes wrong
|
|
1071
|
+
|
|
1072
|
+
A setup script that throws leaves the watcher up. The error is on the terminal and the next save is
|
|
1073
|
+
the retry, with no watch to start over.
|
|
1074
|
+
|
|
1075
|
+
Setup that writes into a watched path restarts the process, which writes again, which restarts it.
|
|
1076
|
+
`yulin watch` refuses to run that loop. After a few restarts caused by the same file changing
|
|
1077
|
+
straight after startup, it stops and names the file. Write generated files outside the working
|
|
1078
|
+
directory, or into a directory the watch passes over.
|
|
1079
|
+
|
|
1080
|
+
### Debugging
|
|
1081
|
+
|
|
1082
|
+
A process started by `yulin watch` can be debugged. Attach the debugger to that process, and not to
|
|
1083
|
+
the supervisor, which has nothing worth stepping through. Pass an inspector flag to the watch and it
|
|
1084
|
+
reaches each run through `NODE_OPTIONS`, whether the command is `node` or something that spawns it:
|
|
1085
|
+
|
|
1086
|
+
```bash
|
|
1087
|
+
yulin watch --inspect=9230 -- tsx dev.ts
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
Each run binds the same inspector port, because the process it replaces has fully exited by the time
|
|
1091
|
+
the replacement starts. Attaching to that port with reconnect turned on in your IDE keeps a debugger
|
|
1092
|
+
across restarts. The exact run configuration differs between IDEs.
|
|
1093
|
+
|
|
1094
|
+
Live reload works without the supervisor. A dev script launched straight from an IDE debugger still
|
|
1095
|
+
gets browser reload and still picks up a re-synthed template. A handler edit is a manual restart, as
|
|
1096
|
+
it is without watch mode.
|
|
1097
|
+
|
|
1098
|
+
## Limitations
|
|
1099
|
+
|
|
1100
|
+
- An injected page is not byte for byte what the real service would return, and its `cache-control`
|
|
1101
|
+
is the simulator's rather than the service's. That is the point of the feature, and the reason it
|
|
1102
|
+
is off by default and says so on startup.
|
|
1103
|
+
- The `Location` header is the only place a simulated hostname is rewritten for a browser. A
|
|
1104
|
+
hostname in a page body, in a JSON response or in a cookie `Domain` is left as the service wrote
|
|
1105
|
+
it.
|
|
1106
|
+
- `/__sim-aws/live-reload` is shadowed on every served hostname while live reload is on.
|
|
1107
|
+
- Console message logging covers simulated Cognito messages and simulated SNS text messages. An
|
|
1108
|
+
email accepted by simulated SES is recorded and left unprinted, because a body can run to
|
|
1109
|
+
kilobytes of HTML.
|
|
1110
|
+
- Injection decodes the HTML as UTF-8. A page stored in another encoding would be corrupted, so
|
|
1111
|
+
serve HTML as UTF-8.
|
|
1112
|
+
- An open reload connection uses one of the browser's six connections per origin per tab.
|
|
1113
|
+
- There is no overlay for a reload that failed. The terminal has the error.
|
|
1114
|
+
- `yulin watch` does not re-synth CDK. `cdk watch` is a shortcut for `deploy --watch` against real
|
|
1115
|
+
AWS and there is no synth-only watch, so run your own synth and let the watch pick up its output.
|
|
1116
|
+
- A template updated in place still replaces a changed resource, so the objects in a bucket the
|
|
1117
|
+
change touches go with it. Only the resources the template left alone keep what they hold.
|
|
1118
|
+
- Lambda and CloudFront Function code is not swapped without a restart. A fresh process picks up an
|
|
1119
|
+
edited handler correctly, and an in-process swap would have to invalidate an ESM import subgraph
|
|
1120
|
+
that the language keeps private.
|
|
1121
|
+
- Simulated state is not carried across a restart. Seeding belongs in the setup script, so it runs
|
|
1122
|
+
again and local state stays the same as what tests and CI see.
|
|
1123
|
+
- The IDE run configurations for attaching a debugger to a watched process are not documented yet.
|
|
1124
|
+
- The served AWS service API covers S3, STS and the AWS JSON protocol services. A service speaking REST-JSON, or Query other than STS, is refused with `501 Not Implemented`.
|
|
1125
|
+
- Simulated STS implements `AssumeRole` and `GetCallerIdentity`, and serves both. `AssumeRoleWithWebIdentity` and `GetSessionToken` are refused as `NotImplemented`.
|
|
1126
|
+
- A served AWS API request is routed by its SigV4 credential scope. An unsigned one reaches nothing, whatever endpoint URL it used.
|