@aws-blocks/core 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +32 -0
- package/dist/api.test.js +22 -0
- package/dist/bb-utils.d.ts +1 -0
- package/dist/bb-utils.d.ts.map +1 -1
- package/dist/bb-utils.js +3 -0
- package/dist/cdk/apigateway-account.d.ts +33 -0
- package/dist/cdk/apigateway-account.d.ts.map +1 -0
- package/dist/cdk/apigateway-account.js +60 -0
- package/dist/cdk/blocks-backend.d.ts +11 -0
- package/dist/cdk/blocks-backend.d.ts.map +1 -1
- package/dist/cdk/blocks-backend.js +72 -13
- package/dist/cdk/blocks-backend.test.js +88 -1
- package/dist/cdk/blocks-defaults.d.ts +76 -1
- package/dist/cdk/blocks-defaults.d.ts.map +1 -1
- package/dist/cdk/blocks-defaults.js +17 -1
- package/dist/cdk/blocks-defaults.test.js +17 -0
- package/dist/cdk/blocks-stack.test.js +44 -3
- package/dist/cdk/compute/compute-registry.d.ts +19 -0
- package/dist/cdk/compute/compute-registry.d.ts.map +1 -0
- package/dist/cdk/compute/compute-registry.js +38 -0
- package/dist/cdk/compute/compute.d.ts +82 -2
- package/dist/cdk/compute/compute.d.ts.map +1 -1
- package/dist/cdk/compute/compute.js +64 -2
- package/dist/cdk/compute/default-compute-factory.d.ts +1 -0
- package/dist/cdk/compute/default-compute-factory.d.ts.map +1 -1
- package/dist/cdk/config-registry.d.ts +34 -4
- package/dist/cdk/config-registry.d.ts.map +1 -1
- package/dist/cdk/config-registry.js +83 -25
- package/dist/cdk/config-registry.test.d.ts +2 -0
- package/dist/cdk/config-registry.test.d.ts.map +1 -0
- package/dist/cdk/config-registry.test.js +127 -0
- package/dist/cdk/dashboard-registry.d.ts +41 -0
- package/dist/cdk/dashboard-registry.d.ts.map +1 -0
- package/dist/cdk/dashboard-registry.js +61 -0
- package/dist/cdk/index.d.ts +90 -6
- package/dist/cdk/index.d.ts.map +1 -1
- package/dist/cdk/index.js +135 -13
- package/dist/cdk/internal.d.ts +3 -1
- package/dist/cdk/internal.d.ts.map +1 -1
- package/dist/cdk/internal.js +4 -1
- package/dist/cdk/tracer-registry.d.ts +31 -0
- package/dist/cdk/tracer-registry.d.ts.map +1 -0
- package/dist/cdk/tracer-registry.js +49 -0
- package/dist/cdk/vpc-requirements-registry.d.ts +33 -0
- package/dist/cdk/vpc-requirements-registry.d.ts.map +1 -0
- package/dist/cdk/vpc-requirements-registry.js +46 -0
- package/dist/cdk/vpc-types.d.ts +151 -0
- package/dist/cdk/vpc-types.d.ts.map +1 -0
- package/dist/cdk/vpc-types.js +3 -0
- package/dist/cdk/vpc.d.ts +59 -0
- package/dist/cdk/vpc.d.ts.map +1 -0
- package/dist/cdk/vpc.js +298 -0
- package/dist/cdk/vpc.test.d.ts +2 -0
- package/dist/cdk/vpc.test.d.ts.map +1 -0
- package/dist/cdk/vpc.test.js +285 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/common/config.d.ts +34 -0
- package/dist/common/config.d.ts.map +1 -1
- package/dist/common/config.js +45 -3
- package/dist/common/config.test.js +19 -0
- package/dist/common/index.d.ts +8 -0
- package/dist/common/index.d.ts.map +1 -1
- package/dist/errors.d.ts +21 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +25 -0
- package/dist/hosting.d.ts +9 -0
- package/dist/hosting.d.ts.map +1 -1
- package/dist/hosting.js +12 -1
- package/dist/hosting.test.js +63 -1
- package/dist/index.cdk.d.ts +3 -2
- package/dist/index.cdk.d.ts.map +1 -1
- package/dist/index.cdk.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/lambda-handler.d.ts +17 -0
- package/dist/lambda-handler.d.ts.map +1 -1
- package/dist/lambda-handler.js +61 -5
- package/dist/lambda-handler.test.js +191 -2
- package/dist/raw-route.d.ts +15 -1
- package/dist/raw-route.d.ts.map +1 -1
- package/dist/raw-route.js +96 -12
- package/dist/raw-route.test.js +332 -1
- package/dist/scripts/dev-server.d.ts.map +1 -1
- package/dist/scripts/dev-server.js +11 -0
- package/dist/scripts/extract-ts-types.d.ts.map +1 -1
- package/dist/scripts/extract-ts-types.js +107 -23
- package/dist/scripts/extract-ts-types.test.js +225 -26
- package/dist/scripts/generate-spec.d.ts.map +1 -1
- package/dist/scripts/generate-spec.js +14 -5
- package/dist/scripts/generate-spec.test.js +93 -0
- package/dist/scripts/sandbox-empty-buckets.test.d.ts +2 -0
- package/dist/scripts/sandbox-empty-buckets.test.d.ts.map +1 -0
- package/dist/scripts/sandbox-empty-buckets.test.js +171 -0
- package/dist/scripts/sandbox.d.ts +54 -0
- package/dist/scripts/sandbox.d.ts.map +1 -1
- package/dist/scripts/sandbox.js +163 -25
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +10 -1
- package/src/api.test.ts +25 -0
- package/src/api.ts +39 -0
- package/src/bb-utils.ts +3 -0
- package/src/cdk/apigateway-account.ts +66 -0
- package/src/cdk/blocks-backend.test.ts +168 -60
- package/src/cdk/blocks-backend.ts +299 -227
- package/src/cdk/blocks-defaults.test.ts +21 -0
- package/src/cdk/blocks-defaults.ts +79 -1
- package/src/cdk/blocks-stack.test.ts +57 -13
- package/src/cdk/compute/compute-registry.ts +45 -0
- package/src/cdk/compute/compute.ts +114 -2
- package/src/cdk/compute/default-compute-factory.ts +1 -0
- package/src/cdk/config-registry.test.ts +149 -0
- package/src/cdk/config-registry.ts +92 -34
- package/src/cdk/dashboard-registry.ts +68 -0
- package/src/cdk/index.ts +427 -256
- package/src/cdk/internal.ts +6 -2
- package/src/cdk/tracer-registry.ts +54 -0
- package/src/cdk/vpc-requirements-registry.ts +63 -0
- package/src/cdk/vpc-types.ts +158 -0
- package/src/cdk/vpc.test.ts +348 -0
- package/src/cdk/vpc.ts +336 -0
- package/src/client/index.ts +1 -1
- package/src/common/config.test.ts +21 -0
- package/src/common/config.ts +47 -3
- package/src/common/index.ts +8 -0
- package/src/errors.ts +26 -0
- package/src/hosting.test.ts +87 -1
- package/src/hosting.ts +24 -1
- package/src/index.cdk.ts +11 -1
- package/src/index.ts +1 -1
- package/src/lambda-handler.test.ts +220 -2
- package/src/lambda-handler.ts +65 -4
- package/src/raw-route.test.ts +427 -1
- package/src/raw-route.ts +125 -12
- package/src/scripts/dev-server.ts +12 -1
- package/src/scripts/extract-ts-types.test.ts +228 -26
- package/src/scripts/extract-ts-types.ts +104 -20
- package/src/scripts/generate-spec.test.ts +101 -0
- package/src/scripts/generate-spec.ts +15 -5
- package/src/scripts/sandbox-empty-buckets.test.ts +191 -0
- package/src/scripts/sandbox.ts +185 -24
- package/src/version.ts +1 -1
package/dist/cdk/vpc.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { Annotations } from 'aws-cdk-lib';
|
|
4
|
+
import * as ec2 from 'aws-cdk-lib/aws-ec2';
|
|
5
|
+
import { getVpcRequirements as getRegisteredVpcRequirements } from './vpc-requirements-registry.js';
|
|
6
|
+
const VPC_CONTEXT_KEY = Symbol.for('BLOCKS_VPC_CONTEXT');
|
|
7
|
+
/** Map a subnet role to its concrete CDK subnet type. */
|
|
8
|
+
function subnetTypeForRole(role) {
|
|
9
|
+
switch (role) {
|
|
10
|
+
case 'isolated':
|
|
11
|
+
return ec2.SubnetType.PRIVATE_ISOLATED;
|
|
12
|
+
case 'public':
|
|
13
|
+
return ec2.SubnetType.PUBLIC;
|
|
14
|
+
case 'private-with-egress':
|
|
15
|
+
return ec2.SubnetType.PRIVATE_WITH_EGRESS;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Does the VPC actually contain at least one subnet of the given role? */
|
|
19
|
+
function vpcHasRole(vpc, role) {
|
|
20
|
+
switch (role) {
|
|
21
|
+
case 'isolated':
|
|
22
|
+
return vpc.isolatedSubnets.length > 0;
|
|
23
|
+
case 'public':
|
|
24
|
+
return vpc.publicSubnets.length > 0;
|
|
25
|
+
case 'private-with-egress':
|
|
26
|
+
return vpc.privateSubnets.length > 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Set the VPC context on a scope (BlocksStack or BlocksBackend).
|
|
31
|
+
* Called during stack creation when `vpc` prop is provided.
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
export function setVpcContext(scope, context) {
|
|
35
|
+
scope[VPC_CONTEXT_KEY] = context;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get the VPC context from a scope by walking up the construct tree.
|
|
39
|
+
* Used by BBs (e.g., bb-data) to discover the shared VPC.
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export function getVpcContext(scope) {
|
|
43
|
+
let current = scope;
|
|
44
|
+
while (current) {
|
|
45
|
+
const ctx = current[VPC_CONTEXT_KEY];
|
|
46
|
+
if (ctx)
|
|
47
|
+
return ctx;
|
|
48
|
+
current = current.node.scope;
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const LAZY_VPC_KEY = Symbol.for('BLOCKS_LAZY_VPC');
|
|
53
|
+
/**
|
|
54
|
+
* Does any registered Building Block *require* a VPC (i.e. cannot function
|
|
55
|
+
* without one)? Used to decide whether to lazily derive a VPC when the customer
|
|
56
|
+
* didn't provide one. A BB that merely benefits from endpoints does NOT count —
|
|
57
|
+
* only `requiresVpc: true`.
|
|
58
|
+
* @internal
|
|
59
|
+
*/
|
|
60
|
+
export function anyRequirementNeedsVpc(scope) {
|
|
61
|
+
return getRegisteredVpcRequirements(scope).some((r) => r.requirements.requiresVpc === true);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get the framework-owned VPC for a stack, creating one on first use.
|
|
65
|
+
*
|
|
66
|
+
* VPC is a **derived** resource: when a Building Block (or, later, a container
|
|
67
|
+
* compute) requires one and the customer didn't bring their own, Blocks
|
|
68
|
+
* materializes a sensible default here — the same create-if-absent pattern
|
|
69
|
+
* `bb-data` uses for Aurora, lifted to the framework so the whole app shares a
|
|
70
|
+
* single VPC. Keyed by a Symbol on the stack so it's a true singleton: the first
|
|
71
|
+
* caller creates, everyone else reuses. Passing a customer VPC (`vpc:` prop)
|
|
72
|
+
* pre-seeds this via {@link initializeVpc}, so this default is only built when
|
|
73
|
+
* nothing was provided.
|
|
74
|
+
*
|
|
75
|
+
* The default has both a `private-with-egress` tier (for a runtime that needs
|
|
76
|
+
* outbound access — the derivation trigger) and public subnets for the NAT
|
|
77
|
+
* gateway. NAT has a real cost, so this is only created on genuine need, and
|
|
78
|
+
* `create()` emits a notice when it does.
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
export function getOrCreateVpc(scope) {
|
|
82
|
+
const holder = scope;
|
|
83
|
+
const existing = holder[LAZY_VPC_KEY];
|
|
84
|
+
if (existing)
|
|
85
|
+
return existing;
|
|
86
|
+
const vpc = new ec2.Vpc(scope, 'BlocksVpc', { maxAzs: 2, natGateways: 1 });
|
|
87
|
+
holder[LAZY_VPC_KEY] = vpc;
|
|
88
|
+
return vpc;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Initialize VPC support on a Blocks scope (BlocksStack or BlocksBackend).
|
|
92
|
+
* Creates the security group, sets VPC context, and returns the VpcContext
|
|
93
|
+
* that is used for Lambda placement configuration.
|
|
94
|
+
*
|
|
95
|
+
* Called during setupBlocksInfra when `vpc` prop is present.
|
|
96
|
+
* @internal
|
|
97
|
+
*/
|
|
98
|
+
export function initializeVpc(scope, options) {
|
|
99
|
+
const { network: vpc, subnets } = options;
|
|
100
|
+
// Default Lambda placement to private-with-egress. Guard the default: if the
|
|
101
|
+
// caller didn't specify `subnets` and the VPC has no such tier, fail now with
|
|
102
|
+
// an actionable message instead of letting Lambda placement throw a cryptic
|
|
103
|
+
// CDK error later. (A caller who *explicitly* passes an isolated selection is
|
|
104
|
+
// honored — per-BB `requiresEgress` validation catches BBs that can't run there.)
|
|
105
|
+
if (!subnets && !vpcHasRole(vpc, 'private-with-egress')) {
|
|
106
|
+
throw new Error(`VPC '${vpc.vpcId}' has no private-with-egress subnets, which Blocks uses for Lambda ` +
|
|
107
|
+
`placement by default. Add a PRIVATE_WITH_EGRESS subnet tier (a private subnet with a ` +
|
|
108
|
+
`NAT gateway), or pass 'vpc.subnets' to choose a different placement explicitly. ` +
|
|
109
|
+
`See packages/blocks/VPC.md.`);
|
|
110
|
+
}
|
|
111
|
+
const resolvedSubnets = subnets ?? { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS };
|
|
112
|
+
const computeSecurityGroup = new ec2.SecurityGroup(scope, 'BlocksLambdaSg', {
|
|
113
|
+
vpc,
|
|
114
|
+
description: 'Security group for Blocks Lambda functions in VPC',
|
|
115
|
+
allowAllOutbound: true,
|
|
116
|
+
});
|
|
117
|
+
const context = {
|
|
118
|
+
vpc,
|
|
119
|
+
computeSecurityGroup,
|
|
120
|
+
computeSubnets: resolvedSubnets,
|
|
121
|
+
selectSubnets(scope, role, opts) {
|
|
122
|
+
if (vpcHasRole(vpc, role)) {
|
|
123
|
+
return { subnetType: subnetTypeForRole(role) };
|
|
124
|
+
}
|
|
125
|
+
// Requested role is absent. Fall back only if the BB explicitly opted in
|
|
126
|
+
// AND the fallback role actually exists — never downgrade silently.
|
|
127
|
+
if (opts?.fallback && vpcHasRole(vpc, opts.fallback)) {
|
|
128
|
+
return { subnetType: subnetTypeForRole(opts.fallback) };
|
|
129
|
+
}
|
|
130
|
+
const wanted = opts?.fallback ? `'${role}' (or '${opts.fallback}')` : `'${role}'`;
|
|
131
|
+
throw new Error(`${scope.fullId} needs a ${wanted} subnet, but VPC '${vpc.vpcId}' has none. ` +
|
|
132
|
+
`Add a matching subnet tier to your VPC (e.g. a 'subnetConfiguration' entry of the ` +
|
|
133
|
+
`required type), or place the Building Block differently. ` +
|
|
134
|
+
`See packages/blocks/VPC.md for guidance.`);
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
setVpcContext(scope, context);
|
|
138
|
+
return context;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Finalize VPC: query all BuildingBlockScope children for their VPC requirements,
|
|
142
|
+
* deduplicate, and provision endpoints.
|
|
143
|
+
* Called after all BBs are constructed (alongside finalizeConfigRegistry).
|
|
144
|
+
* @internal
|
|
145
|
+
*/
|
|
146
|
+
export function finalizeVpc(scope, options) {
|
|
147
|
+
const { network: vpc, subnets } = options;
|
|
148
|
+
// Whether the runtime's placement provides egress (outbound internet). This is
|
|
149
|
+
// the only capability the requirement check needs — a BB either needs egress
|
|
150
|
+
// or it doesn't. Resolved from the actual selected subnets rather than guessed
|
|
151
|
+
// from a subnet role, so an explicit subnet *list* (no subnetType) is handled
|
|
152
|
+
// correctly. `undefined` = couldn't determine (e.g. an imported VPC whose
|
|
153
|
+
// subnets aren't known at synth) → we skip validation with a warning rather
|
|
154
|
+
// than fabricate an answer.
|
|
155
|
+
const placementHasEgress = placementProvidesEgress(vpc, subnets);
|
|
156
|
+
const gatewayEndpoints = [];
|
|
157
|
+
const interfaceEndpoints = [];
|
|
158
|
+
// Pull requirements from the central registry — every BuildingBlockScope
|
|
159
|
+
// self-registers in its constructor, so this is a faithful census without a
|
|
160
|
+
// separate tree walk (mirrors how config/compute registries are consumed).
|
|
161
|
+
for (const { fullId, requirements: reqs } of getRegisteredVpcRequirements(scope)) {
|
|
162
|
+
if (reqs.gatewayEndpoints) {
|
|
163
|
+
gatewayEndpoints.push(...reqs.gatewayEndpoints);
|
|
164
|
+
}
|
|
165
|
+
if (reqs.interfaceEndpoints) {
|
|
166
|
+
interfaceEndpoints.push(...reqs.interfaceEndpoints);
|
|
167
|
+
}
|
|
168
|
+
// Validate the BB's runtime egress need against where the runtime is
|
|
169
|
+
// actually placed. We never move the runtime (that's the customer's
|
|
170
|
+
// explicit choice) — an unsatisfiable requirement is a synth error, not a
|
|
171
|
+
// silent relocation. This turns an otherwise-silent runtime failure (e.g.
|
|
172
|
+
// DSQL in an isolated Lambda: deploys clean, times out on every call) into
|
|
173
|
+
// an actionable build error.
|
|
174
|
+
if (reqs.requiresEgress) {
|
|
175
|
+
if (placementHasEgress === false) {
|
|
176
|
+
throw new Error(`${fullId} requires its runtime to reach the internet (outbound egress), but the ` +
|
|
177
|
+
`Blocks runtime is placed in subnets with no egress route. ` +
|
|
178
|
+
`Set 'vpc.subnets' to a 'private-with-egress' (or 'public') selection, or remove ` +
|
|
179
|
+
`the Building Block that needs it. See packages/blocks/VPC.md.`);
|
|
180
|
+
}
|
|
181
|
+
if (placementHasEgress === undefined) {
|
|
182
|
+
// Couldn't determine egress (e.g. imported VPC with unknown subnets).
|
|
183
|
+
// Don't fabricate a pass/fail — warn so a real mismatch isn't silent.
|
|
184
|
+
Annotations.of(scope).addWarningV2('blocks:vpc:egress-unverified', `${fullId} requires runtime egress, but Blocks couldn't determine whether the ` +
|
|
185
|
+
`configured subnets provide it (e.g. an imported VPC). Ensure the runtime's subnets ` +
|
|
186
|
+
`have an outbound internet route. See packages/blocks/VPC.md.`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (options.provisionEndpoints === false) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// Always add the S3 gateway endpoint. The runtime pulls config/secrets and
|
|
194
|
+
// migrations from S3 at cold start, so an in-VPC runtime needs it. Gateway
|
|
195
|
+
// endpoints are free (route-table entries, no ENI), so this is unconditional.
|
|
196
|
+
gatewayEndpoints.push(ec2.GatewayVpcEndpointAwsService.S3);
|
|
197
|
+
// Provision gateway endpoints (deduplicated). Gateway endpoints attach to
|
|
198
|
+
// route tables, not ENIs, so they have no security-group layer.
|
|
199
|
+
const provisionedGateway = new Set();
|
|
200
|
+
for (const service of gatewayEndpoints) {
|
|
201
|
+
const key = service.name;
|
|
202
|
+
if (provisionedGateway.has(key))
|
|
203
|
+
continue;
|
|
204
|
+
provisionedGateway.add(key);
|
|
205
|
+
const constructId = `VpcGw${key.replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
206
|
+
new ec2.GatewayVpcEndpoint(scope, constructId, { vpc, service });
|
|
207
|
+
}
|
|
208
|
+
// Always add CloudWatch Logs — every in-VPC Lambda needs it for log delivery,
|
|
209
|
+
// regardless of which Building Blocks are present. SSM is NOT added here: it
|
|
210
|
+
// flows from BB requirements (AppSetting and the auth blocks, which compose
|
|
211
|
+
// AppSetting, declare it), so an app that uses neither doesn't pay for an
|
|
212
|
+
// unused interface endpoint.
|
|
213
|
+
interfaceEndpoints.push(ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS);
|
|
214
|
+
// Dedicated security group for the interface endpoints. Without an explicit
|
|
215
|
+
// SG, CDK creates a default that allows 443 from the entire VPC CIDR — on a
|
|
216
|
+
// bring-your-own VPC that exposes every endpoint to unrelated workloads. Scope
|
|
217
|
+
// ingress to just the Blocks Lambda SG so only our functions can reach them.
|
|
218
|
+
const ctx = getVpcContext(scope);
|
|
219
|
+
const endpointSecurityGroup = new ec2.SecurityGroup(scope, 'BlocksVpcEndpointSg', {
|
|
220
|
+
vpc,
|
|
221
|
+
description: 'Blocks interface VPC endpoints — 443 from the Blocks Lambda only',
|
|
222
|
+
allowAllOutbound: true,
|
|
223
|
+
});
|
|
224
|
+
if (ctx) {
|
|
225
|
+
endpointSecurityGroup.addIngressRule(ec2.Peer.securityGroupId(ctx.computeSecurityGroup.securityGroupId), ec2.Port.tcp(443), 'HTTPS from Blocks Lambda');
|
|
226
|
+
}
|
|
227
|
+
// Provision interface endpoints (deduplicated)
|
|
228
|
+
const provisionedInterface = new Set();
|
|
229
|
+
for (const service of interfaceEndpoints) {
|
|
230
|
+
const key = service.name;
|
|
231
|
+
if (provisionedInterface.has(key))
|
|
232
|
+
continue;
|
|
233
|
+
provisionedInterface.add(key);
|
|
234
|
+
const constructId = `VpcIf${key.replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
235
|
+
new ec2.InterfaceVpcEndpoint(scope, constructId, {
|
|
236
|
+
vpc,
|
|
237
|
+
service,
|
|
238
|
+
privateDnsEnabled: true,
|
|
239
|
+
securityGroups: [endpointSecurityGroup],
|
|
240
|
+
// `open: false` suppresses CDK's default "allow 443 from the whole VPC
|
|
241
|
+
// CIDR" ingress rule. Our dedicated SG already allows 443 from just the
|
|
242
|
+
// Blocks Lambda SG; without this, CDK would re-widen access to the entire
|
|
243
|
+
// VPC — the exact broadening this dedicated SG exists to prevent.
|
|
244
|
+
open: false,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Does the runtime's subnet placement provide egress (an outbound internet
|
|
250
|
+
* route)? Returns `true`/`false` when determinable, or `undefined` when it
|
|
251
|
+
* can't be determined at synth (e.g. an imported VPC whose subnets aren't
|
|
252
|
+
* known) — callers should warn rather than assume.
|
|
253
|
+
*
|
|
254
|
+
* Resolved from the actual subnets, not from a subnet *role*, so an explicit
|
|
255
|
+
* subnet **list** (which carries no `subnetType`) is handled correctly. For a
|
|
256
|
+
* multi-subnet selection, egress is only reported when **every** selected
|
|
257
|
+
* subnet has it (a runtime may land in any of them).
|
|
258
|
+
*/
|
|
259
|
+
function placementProvidesEgress(vpc, selection) {
|
|
260
|
+
// Default placement (no selection) is PRIVATE_WITH_EGRESS — see initializeVpc.
|
|
261
|
+
if (!selection) {
|
|
262
|
+
return vpc.privateSubnets.length > 0 ? true : undefined;
|
|
263
|
+
}
|
|
264
|
+
// Explicit type: egress iff it's the egress tier; isolated/public are known.
|
|
265
|
+
if (selection.subnetType !== undefined) {
|
|
266
|
+
switch (selection.subnetType) {
|
|
267
|
+
case ec2.SubnetType.PRIVATE_WITH_EGRESS:
|
|
268
|
+
return true;
|
|
269
|
+
case ec2.SubnetType.PUBLIC:
|
|
270
|
+
return true; // public subnets route to an internet gateway
|
|
271
|
+
case ec2.SubnetType.PRIVATE_ISOLATED:
|
|
272
|
+
return false;
|
|
273
|
+
default:
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Explicit subnet list (or filter): resolve the concrete subnets and check
|
|
278
|
+
// that all of them are egress-capable. If resolution yields nothing usable,
|
|
279
|
+
// we can't tell.
|
|
280
|
+
try {
|
|
281
|
+
const { subnets: selected } = vpc.selectSubnets(selection);
|
|
282
|
+
if (selected.length === 0)
|
|
283
|
+
return undefined;
|
|
284
|
+
// A subnet is egress-capable if it's one of the VPC's private (with-egress)
|
|
285
|
+
// or public subnets. Isolated subnets are in neither list.
|
|
286
|
+
const egressCapable = new Set([
|
|
287
|
+
...vpc.privateSubnets.map((s) => s.subnetId),
|
|
288
|
+
...vpc.publicSubnets.map((s) => s.subnetId),
|
|
289
|
+
]);
|
|
290
|
+
// If the VPC exposes no subnet inventory (imported VPC), we can't classify.
|
|
291
|
+
if (egressCapable.size === 0 && vpc.isolatedSubnets.length === 0)
|
|
292
|
+
return undefined;
|
|
293
|
+
return selected.every((s) => egressCapable.has(s.subnetId));
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vpc.test.d.ts","sourceRoot":"","sources":["../../src/cdk/vpc.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import * as assert from 'node:assert/strict';
|
|
4
|
+
import { describe, it } from 'node:test';
|
|
5
|
+
import * as cdk from 'aws-cdk-lib';
|
|
6
|
+
import { Template } from 'aws-cdk-lib/assertions';
|
|
7
|
+
import * as ec2 from 'aws-cdk-lib/aws-ec2';
|
|
8
|
+
import { Construct } from 'constructs';
|
|
9
|
+
import { anyRequirementNeedsVpc, finalizeVpc, getOrCreateVpc, getVpcContext, initializeVpc, setVpcContext, } from './vpc.js';
|
|
10
|
+
import { registerVpcRequirements } from './vpc-requirements-registry.js';
|
|
11
|
+
// A minimal stand-in for a BuildingBlockScope: finalizeVpc reads requirements
|
|
12
|
+
// from the central registry, and the real BuildingBlockScope base registers in
|
|
13
|
+
// its constructor. This fake does the same (register on construction) so it
|
|
14
|
+
// exercises the real pull-and-provision path without pulling every BB package
|
|
15
|
+
// into core's test graph. A `fullId` getter mirrors the real Scope so error
|
|
16
|
+
// messages that name the BB can be asserted.
|
|
17
|
+
class FakeBB extends Construct {
|
|
18
|
+
constructor(scope, id, reqs) {
|
|
19
|
+
super(scope, id);
|
|
20
|
+
registerVpcRequirements(this, reqs);
|
|
21
|
+
}
|
|
22
|
+
get fullId() {
|
|
23
|
+
return this.node.id;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build a stack + VPC, optionally initialize the VPC context (so the endpoint
|
|
28
|
+
* SG can scope to the Lambda SG), add fake BBs, then run finalizeVpc.
|
|
29
|
+
*/
|
|
30
|
+
function synthWith(bbs, opts) {
|
|
31
|
+
const app = new cdk.App();
|
|
32
|
+
const stack = new cdk.Stack(app, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' } });
|
|
33
|
+
const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 2, natGateways: opts?.natGateways ?? 1 });
|
|
34
|
+
const options = {
|
|
35
|
+
network: vpc,
|
|
36
|
+
subnets: opts?.subnets,
|
|
37
|
+
provisionEndpoints: opts?.provisionEndpoints,
|
|
38
|
+
};
|
|
39
|
+
if (opts?.withContext !== false)
|
|
40
|
+
initializeVpc(stack, options);
|
|
41
|
+
bbs(stack);
|
|
42
|
+
finalizeVpc(stack, options);
|
|
43
|
+
return Template.fromStack(stack);
|
|
44
|
+
}
|
|
45
|
+
const IFACE = 'AWS::EC2::VPCEndpoint';
|
|
46
|
+
/** Count interface vs gateway endpoints in a synthesized template. */
|
|
47
|
+
function endpointCounts(template) {
|
|
48
|
+
const eps = template.findResources(IFACE);
|
|
49
|
+
let iface = 0;
|
|
50
|
+
let gateway = 0;
|
|
51
|
+
for (const ep of Object.values(eps)) {
|
|
52
|
+
const type = ep.Properties?.VpcEndpointType;
|
|
53
|
+
// Interface endpoints set VpcEndpointType: 'Interface'; gateway endpoints
|
|
54
|
+
// default the property (Gateway) and omit it in the synthesized template.
|
|
55
|
+
if (type === 'Interface')
|
|
56
|
+
iface += 1;
|
|
57
|
+
else
|
|
58
|
+
gateway += 1;
|
|
59
|
+
}
|
|
60
|
+
return { iface, gateway };
|
|
61
|
+
}
|
|
62
|
+
describe('finalizeVpc — endpoint provisioning', () => {
|
|
63
|
+
it('always provisions CloudWatch Logs (interface) and S3 (gateway), even with no BBs', () => {
|
|
64
|
+
const template = synthWith(() => { });
|
|
65
|
+
const { iface, gateway } = endpointCounts(template);
|
|
66
|
+
assert.equal(gateway, 1); // S3 gateway, always-on (free)
|
|
67
|
+
assert.equal(iface, 1); // Logs, always-on
|
|
68
|
+
template.hasResourceProperties(IFACE, {
|
|
69
|
+
VpcEndpointType: 'Interface',
|
|
70
|
+
ServiceName: 'com.amazonaws.us-east-1.logs',
|
|
71
|
+
});
|
|
72
|
+
// Exactly one gateway endpoint (S3). Its ServiceName synthesizes as a
|
|
73
|
+
// region-tokenized Fn::Join, so assert by type/count rather than a literal.
|
|
74
|
+
const gateways = template.findResources(IFACE, { Properties: { VpcEndpointType: 'Gateway' } });
|
|
75
|
+
assert.equal(Object.keys(gateways).length, 1, 'exactly one gateway endpoint (S3)');
|
|
76
|
+
// SSM is NOT always-on — it flows from BB requirements only.
|
|
77
|
+
const ssm = template.findResources(IFACE, {
|
|
78
|
+
Properties: { ServiceName: 'com.amazonaws.us-east-1.ssm' },
|
|
79
|
+
});
|
|
80
|
+
assert.equal(Object.keys(ssm).length, 0, 'no SSM endpoint without a BB that needs it');
|
|
81
|
+
});
|
|
82
|
+
it('provisions the union of BB-declared endpoints plus the always-on Logs + S3', () => {
|
|
83
|
+
const template = synthWith((scope) => {
|
|
84
|
+
new FakeBB(scope, 'Kv', { gatewayEndpoints: [ec2.GatewayVpcEndpointAwsService.DYNAMODB] });
|
|
85
|
+
new FakeBB(scope, 'Bucket', { gatewayEndpoints: [ec2.GatewayVpcEndpointAwsService.S3] });
|
|
86
|
+
new FakeBB(scope, 'Job', { interfaceEndpoints: [ec2.InterfaceVpcEndpointAwsService.SQS] });
|
|
87
|
+
new FakeBB(scope, 'Db', {
|
|
88
|
+
interfaceEndpoints: [
|
|
89
|
+
ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
|
|
90
|
+
ec2.InterfaceVpcEndpointAwsService.RDS_DATA,
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
const { iface, gateway } = endpointCounts(template);
|
|
95
|
+
// Gateways: DynamoDB + S3 (BB's S3 dedups with the always-on S3).
|
|
96
|
+
assert.equal(gateway, 2);
|
|
97
|
+
// Interfaces: SQS + Secrets Manager + RDS Data + always-on Logs.
|
|
98
|
+
assert.equal(iface, 4);
|
|
99
|
+
});
|
|
100
|
+
it('deduplicates endpoints requested by multiple BBs', () => {
|
|
101
|
+
const template = synthWith((scope) => {
|
|
102
|
+
new FakeBB(scope, 'Kv1', { gatewayEndpoints: [ec2.GatewayVpcEndpointAwsService.DYNAMODB] });
|
|
103
|
+
new FakeBB(scope, 'Kv2', { gatewayEndpoints: [ec2.GatewayVpcEndpointAwsService.DYNAMODB] });
|
|
104
|
+
new FakeBB(scope, 'Job1', { interfaceEndpoints: [ec2.InterfaceVpcEndpointAwsService.SQS] });
|
|
105
|
+
new FakeBB(scope, 'Job2', { interfaceEndpoints: [ec2.InterfaceVpcEndpointAwsService.SQS] });
|
|
106
|
+
});
|
|
107
|
+
const { iface, gateway } = endpointCounts(template);
|
|
108
|
+
// Two DynamoDB requests collapse to one; plus always-on S3 = 2 gateways.
|
|
109
|
+
assert.equal(gateway, 2);
|
|
110
|
+
// Two SQS requests collapse to one; plus always-on Logs = 2 interfaces.
|
|
111
|
+
assert.equal(iface, 2);
|
|
112
|
+
});
|
|
113
|
+
it('provisions SSM only when a BB declares it (and dedups multiple declarations)', () => {
|
|
114
|
+
const template = synthWith((scope) => {
|
|
115
|
+
new FakeBB(scope, 'AppSetting', { interfaceEndpoints: [ec2.InterfaceVpcEndpointAwsService.SSM] });
|
|
116
|
+
new FakeBB(scope, 'Auth', { interfaceEndpoints: [ec2.InterfaceVpcEndpointAwsService.SSM] });
|
|
117
|
+
});
|
|
118
|
+
const { iface } = endpointCounts(template);
|
|
119
|
+
// Two SSM requests collapse to one; plus always-on Logs = 2 interfaces.
|
|
120
|
+
assert.equal(iface, 2);
|
|
121
|
+
template.hasResourceProperties(IFACE, {
|
|
122
|
+
VpcEndpointType: 'Interface',
|
|
123
|
+
ServiceName: 'com.amazonaws.us-east-1.ssm',
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
it('provisions nothing when provisionEndpoints is false (but still validates requiresEgress)', () => {
|
|
127
|
+
const template = synthWith((scope) => {
|
|
128
|
+
new FakeBB(scope, 'Kv', { gatewayEndpoints: [ec2.GatewayVpcEndpointAwsService.DYNAMODB] });
|
|
129
|
+
}, { provisionEndpoints: false });
|
|
130
|
+
template.resourceCountIs(IFACE, 0);
|
|
131
|
+
});
|
|
132
|
+
it('scopes the interface-endpoint SG to 443 from the Blocks Lambda SG only', () => {
|
|
133
|
+
const template = synthWith((scope) => {
|
|
134
|
+
new FakeBB(scope, 'Job', { interfaceEndpoints: [ec2.InterfaceVpcEndpointAwsService.SQS] });
|
|
135
|
+
});
|
|
136
|
+
// A dedicated endpoint SG exists, described as Lambda-scoped, with a 443
|
|
137
|
+
// ingress rule sourced from another SG (the Lambda SG) rather than the whole
|
|
138
|
+
// VPC CIDR. We assert the rule is present (arrayWith) rather than the exact
|
|
139
|
+
// array, since CDK may add its own paired rule for the endpoint association.
|
|
140
|
+
const sgs = template.findResources('AWS::EC2::SecurityGroup', {
|
|
141
|
+
Properties: { GroupDescription: 'Blocks interface VPC endpoints — 443 from the Blocks Lambda only' },
|
|
142
|
+
});
|
|
143
|
+
assert.equal(Object.keys(sgs).length, 1, 'exactly one dedicated endpoint SG');
|
|
144
|
+
const sg = Object.values(sgs)[0];
|
|
145
|
+
const ingress = sg.Properties.SecurityGroupIngress ?? [];
|
|
146
|
+
const https = ingress.filter((r) => r.FromPort === 443 && r.ToPort === 443);
|
|
147
|
+
assert.equal(https.length, 1, 'exactly one 443 ingress rule');
|
|
148
|
+
// Must NOT be open to a CIDR (that would be the broad default we're avoiding).
|
|
149
|
+
assert.equal(https[0].CidrIp, undefined, '443 ingress is SG-sourced, not CIDR-open');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
describe('finalizeVpc — requiresEgress validation', () => {
|
|
153
|
+
it('passes when the runtime placement provides egress', () => {
|
|
154
|
+
// Default placement is private-with-egress, which provides egress.
|
|
155
|
+
assert.doesNotThrow(() => synthWith((scope) => {
|
|
156
|
+
new FakeBB(scope, 'Dsql', { requiresEgress: true });
|
|
157
|
+
}));
|
|
158
|
+
});
|
|
159
|
+
it('throws an actionable, BB-named error when the placement has no egress', () => {
|
|
160
|
+
// Place the runtime in isolated subnets; a BB needing egress cannot be served.
|
|
161
|
+
assert.throws(() => synthWith((scope) => {
|
|
162
|
+
new FakeBB(scope, 'Dsql', { requiresEgress: true });
|
|
163
|
+
}, { subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED } }), /Dsql requires its runtime to reach the internet.*no egress/s);
|
|
164
|
+
});
|
|
165
|
+
it('does not validate a BB that does not require egress', () => {
|
|
166
|
+
// A BB with no egress need is fine in isolated subnets.
|
|
167
|
+
assert.doesNotThrow(() => synthWith((scope) => {
|
|
168
|
+
new FakeBB(scope, 'Bb', {});
|
|
169
|
+
}, { subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED } }));
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
describe('initializeVpc — Lambda placement guard', () => {
|
|
173
|
+
function init(natGateways, subnets) {
|
|
174
|
+
const app = new cdk.App();
|
|
175
|
+
const stack = new cdk.Stack(app, 'S', { env: { account: '123456789012', region: 'us-east-1' } });
|
|
176
|
+
const subnetConfiguration = natGateways === 0
|
|
177
|
+
? [{ name: 'isolated', subnetType: ec2.SubnetType.PRIVATE_ISOLATED, cidrMask: 24 }]
|
|
178
|
+
: undefined;
|
|
179
|
+
const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 2, natGateways, subnetConfiguration });
|
|
180
|
+
return () => initializeVpc(stack, { network: vpc, subnets });
|
|
181
|
+
}
|
|
182
|
+
it('throws an actionable error when defaulting to private-with-egress but the VPC has none', () => {
|
|
183
|
+
assert.throws(init(0), /no private-with-egress subnets.*NAT gateway/s);
|
|
184
|
+
});
|
|
185
|
+
it('does not throw when the VPC has a private-with-egress tier', () => {
|
|
186
|
+
assert.doesNotThrow(init(1));
|
|
187
|
+
});
|
|
188
|
+
it('honors an explicit isolated placement without the private-with-egress guard', () => {
|
|
189
|
+
// Caller explicitly chose isolated; the default-path guard must not fire.
|
|
190
|
+
assert.doesNotThrow(init(0, { subnetType: ec2.SubnetType.PRIVATE_ISOLATED }));
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
describe('VpcContext.selectSubnets — instructive resolution', () => {
|
|
194
|
+
function contextFor(natGateways) {
|
|
195
|
+
const app = new cdk.App();
|
|
196
|
+
const stack = new cdk.Stack(app, 'S', { env: { account: '123456789012', region: 'us-east-1' } });
|
|
197
|
+
const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 2, natGateways });
|
|
198
|
+
// With natGateways: 0 CDK still creates the default public+private config,
|
|
199
|
+
// but the private tier becomes isolated (no NAT). Build an explicit
|
|
200
|
+
// isolated-only VPC to exercise the "no isolated" and "no egress" cases.
|
|
201
|
+
return initializeVpc(stack, { network: vpc, subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS } });
|
|
202
|
+
}
|
|
203
|
+
it('returns the requested role when the VPC has it', () => {
|
|
204
|
+
const ctx = contextFor(1);
|
|
205
|
+
const sel = ctx.selectSubnets({ fullId: 'app/db' }, 'private-with-egress');
|
|
206
|
+
assert.equal(sel.subnetType, ec2.SubnetType.PRIVATE_WITH_EGRESS);
|
|
207
|
+
});
|
|
208
|
+
it('falls back to an explicit fallback role when the requested role is absent', () => {
|
|
209
|
+
const ctx = contextFor(1); // has public + private-with-egress, no isolated
|
|
210
|
+
const sel = ctx.selectSubnets({ fullId: 'app/db' }, 'isolated', { fallback: 'private-with-egress' });
|
|
211
|
+
assert.equal(sel.subnetType, ec2.SubnetType.PRIVATE_WITH_EGRESS);
|
|
212
|
+
});
|
|
213
|
+
it('throws a BB-named error when neither the role nor its fallback exists', () => {
|
|
214
|
+
const ctx = contextFor(1); // no isolated tier, and no public fallback requested that helps
|
|
215
|
+
assert.throws(() => ctx.selectSubnets({ fullId: 'app/db' }, 'isolated'), /app\/db needs a 'isolated' subnet.*has none/s);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
// We can't fully test CDK constructs without a Stack, but we can test
|
|
219
|
+
// the getVpcContext logic with mock constructs.
|
|
220
|
+
describe('VPC utilities', () => {
|
|
221
|
+
it('getVpcContext returns undefined when no context set', () => {
|
|
222
|
+
const fakeScope = { node: { scope: undefined } };
|
|
223
|
+
assert.equal(getVpcContext(fakeScope), undefined);
|
|
224
|
+
});
|
|
225
|
+
it('getVpcContext walks up the scope tree', () => {
|
|
226
|
+
const vpcContext = { vpc: 'mock-vpc', computeSecurityGroup: 'mock-sg', computeSubnets: {} };
|
|
227
|
+
const parent = { node: { scope: undefined } };
|
|
228
|
+
setVpcContext(parent, vpcContext);
|
|
229
|
+
const child = { node: { scope: parent } };
|
|
230
|
+
assert.equal(getVpcContext(child), vpcContext);
|
|
231
|
+
});
|
|
232
|
+
it('setVpcContext stores context on the scope', () => {
|
|
233
|
+
const fakeScope = { node: { scope: undefined } };
|
|
234
|
+
const vpcContext = { vpc: 'mock-vpc', computeSecurityGroup: 'mock-sg', computeSubnets: {} };
|
|
235
|
+
setVpcContext(fakeScope, vpcContext);
|
|
236
|
+
assert.equal(getVpcContext(fakeScope), vpcContext);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
describe('lazy VPC derivation', () => {
|
|
240
|
+
function stackWith(bbs) {
|
|
241
|
+
const app = new cdk.App();
|
|
242
|
+
const stack = new cdk.Stack(app, 'LazyStack', { env: { account: '123456789012', region: 'us-east-1' } });
|
|
243
|
+
bbs(stack);
|
|
244
|
+
return { stack, scope: stack };
|
|
245
|
+
}
|
|
246
|
+
it('anyRequirementNeedsVpc is false for BBs that only declare endpoints', () => {
|
|
247
|
+
const { scope } = stackWith((s) => {
|
|
248
|
+
new FakeBB(s, 'Kv', { gatewayEndpoints: [ec2.GatewayVpcEndpointAwsService.DYNAMODB] });
|
|
249
|
+
new FakeBB(s, 'Job', { interfaceEndpoints: [ec2.InterfaceVpcEndpointAwsService.SQS] });
|
|
250
|
+
});
|
|
251
|
+
assert.equal(anyRequirementNeedsVpc(scope), false);
|
|
252
|
+
});
|
|
253
|
+
it('anyRequirementNeedsVpc is true when a BB declares requiresVpc', () => {
|
|
254
|
+
const { scope } = stackWith((s) => {
|
|
255
|
+
new FakeBB(s, 'Kv', { gatewayEndpoints: [ec2.GatewayVpcEndpointAwsService.DYNAMODB] });
|
|
256
|
+
new FakeBB(s, 'Container', { requiresVpc: true });
|
|
257
|
+
});
|
|
258
|
+
assert.equal(anyRequirementNeedsVpc(scope), true);
|
|
259
|
+
});
|
|
260
|
+
it('getOrCreateVpc returns the same VPC on repeated calls (singleton)', () => {
|
|
261
|
+
const { scope } = stackWith(() => { });
|
|
262
|
+
const a = getOrCreateVpc(scope);
|
|
263
|
+
const b = getOrCreateVpc(scope);
|
|
264
|
+
assert.equal(a, b, 'lazy VPC is a per-stack singleton');
|
|
265
|
+
});
|
|
266
|
+
it('a derived VPC + finalizeVpc provisions endpoints for a requiresVpc app', () => {
|
|
267
|
+
const app = new cdk.App();
|
|
268
|
+
const stack = new cdk.Stack(app, 'DerivedStack', { env: { account: '123456789012', region: 'us-east-1' } });
|
|
269
|
+
new FakeBB(stack, 'Container', {
|
|
270
|
+
requiresVpc: true,
|
|
271
|
+
interfaceEndpoints: [ec2.InterfaceVpcEndpointAwsService.SQS],
|
|
272
|
+
});
|
|
273
|
+
assert.equal(anyRequirementNeedsVpc(stack), true);
|
|
274
|
+
const vpc = getOrCreateVpc(stack);
|
|
275
|
+
const options = { network: vpc };
|
|
276
|
+
initializeVpc(stack, options);
|
|
277
|
+
finalizeVpc(stack, options);
|
|
278
|
+
const template = Template.fromStack(stack);
|
|
279
|
+
// SQS (from the BB) + always-on CloudWatch Logs interface endpoints, plus
|
|
280
|
+
// the always-on S3 gateway endpoint.
|
|
281
|
+
const { iface, gateway } = endpointCounts(template);
|
|
282
|
+
assert.equal(iface, 2, 'SQS + Logs');
|
|
283
|
+
assert.equal(gateway, 1, 'S3 gateway');
|
|
284
|
+
});
|
|
285
|
+
});
|
package/dist/client/index.d.ts
CHANGED
|
@@ -92,6 +92,6 @@ export interface ApiNamespaceClientOptions {
|
|
|
92
92
|
* ```
|
|
93
93
|
*/
|
|
94
94
|
export declare function ApiNamespaceClient<T extends Record<string, (...args: any[]) => any>>(name: string, options?: ApiNamespaceClientOptions): AsyncAPI<T>;
|
|
95
|
-
export { ApiError, isBlocksError, hasAuthError, DEFAULT_API_ERROR_NAME } from '../errors.js';
|
|
95
|
+
export { ApiError, blocksError, isBlocksError, hasAuthError, DEFAULT_API_ERROR_NAME } from '../errors.js';
|
|
96
96
|
export { Scope, type ScopeOptions } from '../common/index.js';
|
|
97
97
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC;AAChC,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC,IAAI,GAAG,CAAC;AAiBhF,KAAK,QAAQ,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC,IAAI;KAChE,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,MAAM,IAAI,KAAK,MAAM,CAAC,GACzD,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GACtC,KAAK;CACV,CAAC;AAoHF;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,iKAAiK;IACjK,YAAY,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,wHAAwH;IACxH,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,gBAAgB;IAC/B,6GAA6G;IAC7G,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,aAAa,GAAG,IAAI,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IAC7F,6FAA6F;IAC7F,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC;CACzC;AAID;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,gBAAgB,GAAG,IAAI,CAErE;AAqBD;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,8EAA8E;IAC9E,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC,EAClF,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,yBAAyB,GAClC,QAAQ,CAAC,CAAC,CAAC,CAyDb;AAED,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,YAAY,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC;AAChC,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC,IAAI,GAAG,CAAC;AAiBhF,KAAK,QAAQ,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC,IAAI;KAChE,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,MAAM,IAAI,KAAK,MAAM,CAAC,GACzD,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GACtC,KAAK;CACV,CAAC;AAoHF;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,iKAAiK;IACjK,YAAY,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,wHAAwH;IACxH,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,gBAAgB;IAC/B,6GAA6G;IAC7G,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,aAAa,GAAG,IAAI,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IAC7F,6FAA6F;IAC7F,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC;CACzC;AAID;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,gBAAgB,GAAG,IAAI,CAErE;AAqBD;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,8EAA8E;IAC9E,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC,EAClF,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,yBAAyB,GAClC,QAAQ,CAAC,CAAC,CAAC,CAyDb;AAED,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAC1G,OAAO,EAAE,KAAK,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/client/index.js
CHANGED
|
@@ -234,5 +234,5 @@ export function ApiNamespaceClient(name, options) {
|
|
|
234
234
|
}
|
|
235
235
|
});
|
|
236
236
|
}
|
|
237
|
-
export { ApiError, isBlocksError, hasAuthError, DEFAULT_API_ERROR_NAME } from '../errors.js';
|
|
237
|
+
export { ApiError, blocksError, isBlocksError, hasAuthError, DEFAULT_API_ERROR_NAME } from '../errors.js';
|
|
238
238
|
export { Scope } from '../common/index.js';
|
package/dist/common/config.d.ts
CHANGED
|
@@ -34,6 +34,40 @@ export declare function preloadConfig(): Promise<void>;
|
|
|
34
34
|
* Only sets env vars that aren't already set (won't override explicit env vars).
|
|
35
35
|
*/
|
|
36
36
|
export declare function loadConfigToProcessEnv(): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Whether a config load has resolved and cached a result.
|
|
39
|
+
*
|
|
40
|
+
* Returns `true` once `loadConfigFromS3()` has cached a config object — either a
|
|
41
|
+
* successful S3 load, a genuinely-empty `{}` config, or the no-bucket local-dev
|
|
42
|
+
* path (both of which cache `{}`). Returns `false` in the initial state AND
|
|
43
|
+
* after a transient not-found miss: the 404 path deliberately does NOT cache its
|
|
44
|
+
* empty result (see `loadConfigFromS3`), so a `false` return immediately after
|
|
45
|
+
* awaiting a load uniquely identifies the post-deploy S3 window where
|
|
46
|
+
* blocks-config.json isn't readable yet.
|
|
47
|
+
*
|
|
48
|
+
* Callers use this to tell a transient-empty load (retry on the next request)
|
|
49
|
+
* apart from a legitimately-config-less app (do nothing) without reaching into
|
|
50
|
+
* the module's private cache.
|
|
51
|
+
*/
|
|
52
|
+
export declare function isConfigResolved(): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Sanitize a Blocks identifier into a valid config/env-var key segment:
|
|
55
|
+
* uppercase, with every non-alphanumeric character replaced by `_`.
|
|
56
|
+
*
|
|
57
|
+
* Config entries are loaded into `process.env` at runtime (see
|
|
58
|
+
* {@link loadConfigToProcessEnv}), so a key must be a valid env-var name. This
|
|
59
|
+
* is the single rule both sides of a key share: whoever *writes* a config key
|
|
60
|
+
* (`registerConfig(\`PREFIX_${sanitizeConfigKey(id)}\`, …)`) and whoever *reads*
|
|
61
|
+
* it (`getConfigSync(\`PREFIX_${sanitizeConfigKey(id)}\`)`) MUST go through this
|
|
62
|
+
* function so they reconstruct a byte-identical key — a hand-inlined variant
|
|
63
|
+
* that drifts silently misses the lookup at runtime.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* registerConfig(this, `BLOCKS_QUEUE_URL_${sanitizeConfigKey(this.fullId)}`, url);
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export declare function sanitizeConfigKey(id: string): string;
|
|
37
71
|
/**
|
|
38
72
|
* Reset the config cache. **For testing only.**
|
|
39
73
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/common/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/common/config.ts"],"names":[],"mappings":"AAgGA;;;;;;;;GAQG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAM5D;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAOjD;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnD;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC,CAO5D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAIxC;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,IAAI,GAAG,IAAI,CAEtG"}
|