@aws-blocks/core 0.4.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 +12 -0
- package/dist/cdk/blocks-backend.d.ts +4 -1
- package/dist/cdk/blocks-backend.d.ts.map +1 -1
- package/dist/cdk/blocks-backend.js +56 -10
- package/dist/cdk/blocks-backend.test.js +71 -1
- package/dist/cdk/blocks-defaults.d.ts +11 -0
- package/dist/cdk/blocks-defaults.d.ts.map +1 -1
- package/dist/cdk/blocks-stack.test.js +23 -3
- package/dist/cdk/compute/compute.d.ts +80 -2
- package/dist/cdk/compute/compute.d.ts.map +1 -1
- package/dist/cdk/compute/compute.js +57 -3
- package/dist/cdk/config-registry.test.js +12 -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 +67 -11
- package/dist/cdk/index.d.ts.map +1 -1
- package/dist/cdk/index.js +100 -18
- 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/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +5 -0
- package/dist/hosting.d.ts.map +1 -1
- package/dist/hosting.js +2 -0
- package/dist/hosting.test.js +39 -1
- package/dist/index.cdk.d.ts +2 -1
- package/dist/index.cdk.d.ts.map +1 -1
- package/dist/index.cdk.js +1 -1
- package/dist/lambda-handler.js +9 -2
- package/dist/lambda-handler.test.js +61 -1
- 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/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +8 -1
- package/src/cdk/blocks-backend.test.ts +144 -60
- package/src/cdk/blocks-backend.ts +298 -239
- package/src/cdk/blocks-defaults.ts +12 -0
- package/src/cdk/blocks-stack.test.ts +32 -13
- package/src/cdk/compute/compute.ts +105 -3
- package/src/cdk/config-registry.test.ts +14 -0
- package/src/cdk/dashboard-registry.ts +68 -0
- package/src/cdk/index.ts +426 -298
- 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/errors.ts +5 -0
- package/src/hosting.test.ts +59 -1
- package/src/hosting.ts +3 -0
- package/src/index.cdk.ts +7 -0
- package/src/lambda-handler.test.ts +79 -1
- package/src/lambda-handler.ts +11 -2
- 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/version.ts +1 -1
package/src/cdk/internal.ts
CHANGED
|
@@ -23,8 +23,12 @@
|
|
|
23
23
|
* @internal
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
export { Compute } from './compute/compute.js';
|
|
27
|
-
export type { DefaultComputeFactory } from './compute/default-compute-factory.js';
|
|
28
26
|
// Reserved `/aws-blocks` path segment, needed by concrete computes (e.g.
|
|
29
27
|
// LambdaCompute in @aws-blocks/bb-lambda-compute) to build their API route tree.
|
|
30
28
|
export { BLOCKS_NAMESPACE } from '../constants.js';
|
|
29
|
+
export type { ComputeDashboardSection } from './compute/compute.js';
|
|
30
|
+
export { Compute } from './compute/compute.js';
|
|
31
|
+
// Enumerate the computes registered on a stack — the Dashboard BB's default
|
|
32
|
+
// compute selection resolves through this at finalize.
|
|
33
|
+
export { getComputes } from './compute/compute-registry.js';
|
|
34
|
+
export type { DefaultComputeFactory } from './compute/default-compute-factory.js';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import * as cdk from 'aws-cdk-lib';
|
|
4
|
+
import { type IRole, PolicyStatement } from 'aws-cdk-lib/aws-iam';
|
|
5
|
+
import type { Construct } from 'constructs';
|
|
6
|
+
import { getComputes } from './compute/compute-registry.js';
|
|
7
|
+
|
|
8
|
+
const REGISTRY_KEY = Symbol.for('BLOCKS_TRACER_PRESENCE');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Mark that the app contains a `Tracer`. Tracing is **presence-gated**: a Tracer
|
|
12
|
+
* anywhere in the app means every compute should be traced (X-Ray provisions
|
|
13
|
+
* real, costed infra, so it's off unless the app opts in by creating a Tracer).
|
|
14
|
+
* Multiple Tracers are fine — this just records the boolean. Stored per stack
|
|
15
|
+
* (keyed by a Symbol), like the config/compute registries.
|
|
16
|
+
*
|
|
17
|
+
* @param scope - Any construct in the stack (used to locate the stack).
|
|
18
|
+
*/
|
|
19
|
+
export function registerTracer(scope: Construct): void {
|
|
20
|
+
(cdk.Stack.of(scope) as unknown as Record<symbol, boolean>)[REGISTRY_KEY] = true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hasTracer(stack: cdk.Stack): boolean {
|
|
24
|
+
return (stack as unknown as Record<symbol, boolean | undefined>)[REGISTRY_KEY] === true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* If the app contains a `Tracer`, enable tracing on **every** compute in the
|
|
29
|
+
* stack and grant X-Ray publish **once** on the shared execution role. Runs at
|
|
30
|
+
* the end of `create()` (after the backend module has imported, so all computes
|
|
31
|
+
* are registered). `Compute.enableTracing()` is idempotent, so this is safe
|
|
32
|
+
* regardless of how many Tracers exist.
|
|
33
|
+
*
|
|
34
|
+
* The IAM grant lives here — at the framework level, once on the shared role —
|
|
35
|
+
* rather than in each compute's `applyTracing()`: every compute assumes the same
|
|
36
|
+
* execution role, so a per-compute grant would add N identical statements. The
|
|
37
|
+
* compute only flips its own tracing mode (e.g. Lambda `TracingConfig: Active`);
|
|
38
|
+
* the permission to publish segments is a single stack-level concern.
|
|
39
|
+
*
|
|
40
|
+
* @param scope - Any construct in the stack (used to locate the stack + computes).
|
|
41
|
+
* @param executionRole - The shared execution role every compute assumes; granted
|
|
42
|
+
* X-Ray publish once when tracing is enabled.
|
|
43
|
+
*/
|
|
44
|
+
export function finalizeTracing(scope: Construct, executionRole: IRole): void {
|
|
45
|
+
if (!hasTracer(cdk.Stack.of(scope))) return;
|
|
46
|
+
for (const compute of getComputes(scope)) compute.enableTracing();
|
|
47
|
+
// One grant on the shared role rather than one per traced compute.
|
|
48
|
+
executionRole.addToPrincipalPolicy(
|
|
49
|
+
new PolicyStatement({
|
|
50
|
+
actions: ['xray:PutTraceSegments', 'xray:PutTelemetryRecords'],
|
|
51
|
+
resources: ['*'],
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import * as cdk from 'aws-cdk-lib';
|
|
5
|
+
import type { Construct } from 'constructs';
|
|
6
|
+
import type { VpcRequirements } from './vpc-types.js';
|
|
7
|
+
|
|
8
|
+
const REGISTRY_KEY = Symbol.for('BLOCKS_VPC_REQUIREMENTS_REGISTRY');
|
|
9
|
+
|
|
10
|
+
/** A registered requirement plus the fullId of the BB that declared it (for errors). */
|
|
11
|
+
export interface RegisteredVpcRequirement {
|
|
12
|
+
readonly fullId: string;
|
|
13
|
+
readonly requirements: VpcRequirements;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get or create the VPC-requirements list for a given stack. Stored on the stack
|
|
18
|
+
* object (keyed by a Symbol), so each stack in a multi-stack synth gets its own —
|
|
19
|
+
* a requirement never leaks into another stack's list. Mirrors the compute and
|
|
20
|
+
* config registries, which scope their state the same way.
|
|
21
|
+
*/
|
|
22
|
+
function getRegistry(stack: cdk.Stack): RegisteredVpcRequirement[] {
|
|
23
|
+
let list = (stack as any)[REGISTRY_KEY] as RegisteredVpcRequirement[] | undefined;
|
|
24
|
+
if (!list) {
|
|
25
|
+
list = [];
|
|
26
|
+
(stack as any)[REGISTRY_KEY] = list;
|
|
27
|
+
}
|
|
28
|
+
return list;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Register a Building Block's VPC requirements on its owning stack. Called from
|
|
33
|
+
* the {@link BuildingBlockScope} base constructor, so every BB self-registers the
|
|
34
|
+
* moment it is constructed — `finalizeVpc` then enumerates them without a separate
|
|
35
|
+
* discovery pass (mirrors how `registerCompute`/`registerConfig` accumulate during
|
|
36
|
+
* the backend import). Because it's driven by the base constructor, a BB **cannot**
|
|
37
|
+
* silently skip declaring its requirements: the constructor won't compile without
|
|
38
|
+
* supplying them (see `BuildingBlockScope`).
|
|
39
|
+
*
|
|
40
|
+
* @param bb - The construct declaring the requirement (used to locate its stack and name it).
|
|
41
|
+
* @param requirements - What the BB needs from the VPC.
|
|
42
|
+
*/
|
|
43
|
+
export function registerVpcRequirements(
|
|
44
|
+
bb: Construct & { readonly fullId: string },
|
|
45
|
+
requirements: VpcRequirements,
|
|
46
|
+
): void {
|
|
47
|
+
getRegistry(cdk.Stack.of(bb)).push({ fullId: bb.fullId, requirements });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The VPC requirements registered on the stack that owns `scope`, in construction
|
|
52
|
+
* order. Returns an empty array before any BB is constructed.
|
|
53
|
+
*
|
|
54
|
+
* @param scope - Any construct in the stack (used to locate the stack).
|
|
55
|
+
*/
|
|
56
|
+
export function getVpcRequirements(scope: Construct): readonly RegisteredVpcRequirement[] {
|
|
57
|
+
return getRegistry(cdk.Stack.of(scope));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Clear the registry. **For test cleanup only.** */
|
|
61
|
+
export function _resetVpcRequirementsRegistry(stack: cdk.Stack): void {
|
|
62
|
+
(stack as any)[REGISTRY_KEY] = [];
|
|
63
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type * as ec2 from 'aws-cdk-lib/aws-ec2';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A subnet role — the kind of subnet a Building Block needs, expressed as an
|
|
8
|
+
* intent the VPC resolves to a concrete {@link ec2.SubnetSelection}:
|
|
9
|
+
*
|
|
10
|
+
* - `'private-with-egress'` — private subnets with outbound internet access via
|
|
11
|
+
* a NAT gateway. Required by anything that must reach a public AWS endpoint at
|
|
12
|
+
* runtime (e.g. a service with no interface endpoint).
|
|
13
|
+
* - `'isolated'` — private subnets with no internet route at all. Best for
|
|
14
|
+
* resources reached entirely over VPC endpoints or in-VPC (e.g. a database).
|
|
15
|
+
* - `'public'` — subnets with a direct internet gateway route.
|
|
16
|
+
*/
|
|
17
|
+
export type SubnetRole = 'private-with-egress' | 'isolated' | 'public';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Options for VPC integration on BlocksStack / BlocksBackend.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const vpc = new ec2.Vpc(app, 'AppVpc', { maxAzs: 2, natGateways: 1 });
|
|
25
|
+
* await BlocksStack.create(app, stackName, {
|
|
26
|
+
* backendHandlerPath: join(__dirname, 'index.handler.ts'),
|
|
27
|
+
* backendCDKPath: join(__dirname, 'index.ts'),
|
|
28
|
+
* defaults: { ...BlocksPresets.production, vpc: { network: vpc } },
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export interface BlocksVpcOptions {
|
|
33
|
+
/**
|
|
34
|
+
* The VPC to place Lambdas and VPC-resident resources into.
|
|
35
|
+
* Create this however you like — standard CDK:
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* const vpc = new ec2.Vpc(stack, 'AppVpc', { maxAzs: 2, natGateways: 1 });
|
|
39
|
+
* // or
|
|
40
|
+
* const vpc = ec2.Vpc.fromLookup(stack, 'SharedVpc', { vpcId: 'vpc-abc123' });
|
|
41
|
+
*/
|
|
42
|
+
network: ec2.IVpc;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Subnet selection for Lambda and Blocks-managed compute placement.
|
|
46
|
+
*
|
|
47
|
+
* Any subnets you select must belong to {@link network}. This is not
|
|
48
|
+
* validated at synth — CDK can't check it for an imported subnet (which
|
|
49
|
+
* carries no `vpcId`) — so a mismatch surfaces as a CloudFormation error at
|
|
50
|
+
* deploy, not a synth-time failure.
|
|
51
|
+
*
|
|
52
|
+
* @default { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }
|
|
53
|
+
*/
|
|
54
|
+
subnets?: ec2.SubnetSelection;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Whether to auto-provision VPC endpoints based on BB registrations.
|
|
58
|
+
* Set to `false` to disable (e.g., when using a shared VPC that already has endpoints).
|
|
59
|
+
*
|
|
60
|
+
* @default true
|
|
61
|
+
*/
|
|
62
|
+
provisionEndpoints?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* VPC requirements declared by a Building Block.
|
|
67
|
+
* Supplied to the `BuildingBlockScope` constructor and collected in the central
|
|
68
|
+
* requirements registry; `finalizeVpc` pulls them to provision endpoints, and the
|
|
69
|
+
* lazy VPC uses `requiresVpc` to decide whether to derive a VPC when none was
|
|
70
|
+
* provided.
|
|
71
|
+
*/
|
|
72
|
+
export interface VpcRequirements {
|
|
73
|
+
/** Gateway VPC endpoints this BB needs (e.g., S3, DynamoDB). */
|
|
74
|
+
gatewayEndpoints?: ec2.GatewayVpcEndpointAwsService[];
|
|
75
|
+
/** Interface VPC endpoints this BB needs (e.g., SQS, SSM, Secrets Manager). */
|
|
76
|
+
interfaceEndpoints?: ec2.InterfaceVpcEndpointAwsService[];
|
|
77
|
+
/**
|
|
78
|
+
* Whether this BB **cannot function without a VPC** — i.e. it must run in (or
|
|
79
|
+
* provision resources into) a VPC regardless of whether the customer asked
|
|
80
|
+
* for one. When any registered requirement sets this and no `vpc` was
|
|
81
|
+
* provided, the framework **lazily derives** a default VPC (see
|
|
82
|
+
* `getOrCreateVpc`) rather than erroring — mirroring how bb-data creates its
|
|
83
|
+
* own VPC when no shared one exists.
|
|
84
|
+
*
|
|
85
|
+
* Most BBs leave this unset: they reach AWS services from the AWS-managed
|
|
86
|
+
* Lambda network and only *benefit* from a VPC (endpoints) when one is present.
|
|
87
|
+
* Set it only for a BB whose resources are intrinsically VPC-resident.
|
|
88
|
+
*
|
|
89
|
+
* @default false
|
|
90
|
+
*/
|
|
91
|
+
requiresVpc?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Whether the BB's **parent runtime** — the shared Blocks handler Lambda (or,
|
|
94
|
+
* in future, a container) that executes this BB's operations — must be able to
|
|
95
|
+
* reach the internet (outbound egress) for the BB to work at runtime.
|
|
96
|
+
*
|
|
97
|
+
* Set this when your BB's runtime code calls a service it can only reach over
|
|
98
|
+
* the public internet. The canonical case: a service with no VPC endpoint
|
|
99
|
+
* (e.g. Aurora DSQL) is reached over a public HTTPS endpoint, so the runtime
|
|
100
|
+
* must sit in a subnet with an egress route; if it's placed in isolated
|
|
101
|
+
* subnets the deploy still succeeds but every call times out at runtime.
|
|
102
|
+
*
|
|
103
|
+
* The framework **validates** this at synth against the runtime's resolved
|
|
104
|
+
* placement (egress is satisfied by a `private-with-egress` or `public`
|
|
105
|
+
* selection) and fails the build with an actionable message on a mismatch. It
|
|
106
|
+
* never moves the runtime — reassigning a customer's explicit placement is not
|
|
107
|
+
* a BB's responsibility, so an unsatisfiable requirement is an error, not a
|
|
108
|
+
* silent relocation.
|
|
109
|
+
*
|
|
110
|
+
* This constrains the BB's **host**. To place compute the BB provisions
|
|
111
|
+
* **itself** (e.g. an Aurora cluster), resolve a subnet inline in your
|
|
112
|
+
* constructor via {@link VpcContext.selectSubnets} instead.
|
|
113
|
+
*
|
|
114
|
+
* @default false
|
|
115
|
+
*/
|
|
116
|
+
requiresEgress?: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The minimal shape {@link VpcContext.selectSubnets} needs from a Building
|
|
121
|
+
* Block to produce an instructive, BB-named error — just its `fullId`. Typed
|
|
122
|
+
* structurally so `vpc-types.ts` stays type-only and free of a dependency on
|
|
123
|
+
* the `Scope` class.
|
|
124
|
+
*/
|
|
125
|
+
export interface SubnetScope {
|
|
126
|
+
readonly fullId: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Internal VPC context propagated through the construct tree.
|
|
131
|
+
* Set by the CDK-level VPC option. BBs read this to determine their VPC placement.
|
|
132
|
+
* @internal
|
|
133
|
+
*/
|
|
134
|
+
export interface VpcContext {
|
|
135
|
+
readonly vpc: ec2.IVpc;
|
|
136
|
+
readonly computeSecurityGroup: ec2.ISecurityGroup;
|
|
137
|
+
readonly computeSubnets: ec2.SubnetSelection;
|
|
138
|
+
/**
|
|
139
|
+
* Resolve a {@link SubnetRole} to a concrete {@link ec2.SubnetSelection} for a
|
|
140
|
+
* resource this Building Block provisions itself, verifying the VPC actually
|
|
141
|
+
* has subnets of that role.
|
|
142
|
+
*
|
|
143
|
+
* Prefer this over building an `ec2.SubnetSelection` by hand: if the VPC has
|
|
144
|
+
* no matching subnet, it throws an actionable, BB-named error at synth
|
|
145
|
+
* ("`KVStore 'app/cache'` needs an 'isolated' subnet, but VPC 'vpc-…' has
|
|
146
|
+
* none …") instead of the opaque CDK "no subnet groups" error thrown later.
|
|
147
|
+
*
|
|
148
|
+
* @param scope the BB requesting the subnet (its `fullId` names the error)
|
|
149
|
+
* @param role the subnet role the BB's own resource needs
|
|
150
|
+
* @param opts.fallback an alternate role to use when `role` is absent from
|
|
151
|
+
* the VPC. Provide it to **explicitly** allow graceful degradation (e.g.
|
|
152
|
+
* Aurora over the Data API works from `'private-with-egress'` when there is
|
|
153
|
+
* no isolated tier); omit it to require `role` strictly and fail otherwise.
|
|
154
|
+
* The downgrade is never silent — it only happens when you opt in here.
|
|
155
|
+
* @throws if the VPC has neither `role` nor (when given) `opts.fallback`
|
|
156
|
+
*/
|
|
157
|
+
selectSubnets(scope: SubnetScope, role: SubnetRole, opts?: { fallback?: SubnetRole }): ec2.SubnetSelection;
|
|
158
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import * as assert from 'node:assert/strict';
|
|
5
|
+
import { describe, it } from 'node:test';
|
|
6
|
+
import * as cdk from 'aws-cdk-lib';
|
|
7
|
+
import { Template } from 'aws-cdk-lib/assertions';
|
|
8
|
+
import * as ec2 from 'aws-cdk-lib/aws-ec2';
|
|
9
|
+
import { Construct } from 'constructs';
|
|
10
|
+
import {
|
|
11
|
+
anyRequirementNeedsVpc,
|
|
12
|
+
finalizeVpc,
|
|
13
|
+
getOrCreateVpc,
|
|
14
|
+
getVpcContext,
|
|
15
|
+
initializeVpc,
|
|
16
|
+
setVpcContext,
|
|
17
|
+
} from './vpc.js';
|
|
18
|
+
import { registerVpcRequirements } from './vpc-requirements-registry.js';
|
|
19
|
+
import type { BlocksVpcOptions, VpcRequirements } from './vpc-types.js';
|
|
20
|
+
|
|
21
|
+
// A minimal stand-in for a BuildingBlockScope: finalizeVpc reads requirements
|
|
22
|
+
// from the central registry, and the real BuildingBlockScope base registers in
|
|
23
|
+
// its constructor. This fake does the same (register on construction) so it
|
|
24
|
+
// exercises the real pull-and-provision path without pulling every BB package
|
|
25
|
+
// into core's test graph. A `fullId` getter mirrors the real Scope so error
|
|
26
|
+
// messages that name the BB can be asserted.
|
|
27
|
+
class FakeBB extends Construct {
|
|
28
|
+
constructor(scope: Construct, id: string, reqs: VpcRequirements) {
|
|
29
|
+
super(scope, id);
|
|
30
|
+
registerVpcRequirements(this, reqs);
|
|
31
|
+
}
|
|
32
|
+
get fullId(): string {
|
|
33
|
+
return this.node.id;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build a stack + VPC, optionally initialize the VPC context (so the endpoint
|
|
39
|
+
* SG can scope to the Lambda SG), add fake BBs, then run finalizeVpc.
|
|
40
|
+
*/
|
|
41
|
+
function synthWith(
|
|
42
|
+
bbs: (scope: Construct) => void,
|
|
43
|
+
opts?: Partial<BlocksVpcOptions> & { natGateways?: number; withContext?: boolean },
|
|
44
|
+
): Template {
|
|
45
|
+
const app = new cdk.App();
|
|
46
|
+
const stack = new cdk.Stack(app, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' } });
|
|
47
|
+
const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 2, natGateways: opts?.natGateways ?? 1 });
|
|
48
|
+
const options: BlocksVpcOptions = {
|
|
49
|
+
network: vpc,
|
|
50
|
+
subnets: opts?.subnets,
|
|
51
|
+
provisionEndpoints: opts?.provisionEndpoints,
|
|
52
|
+
};
|
|
53
|
+
if (opts?.withContext !== false) initializeVpc(stack, options);
|
|
54
|
+
bbs(stack);
|
|
55
|
+
finalizeVpc(stack, options);
|
|
56
|
+
return Template.fromStack(stack);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const IFACE = 'AWS::EC2::VPCEndpoint';
|
|
60
|
+
|
|
61
|
+
/** Count interface vs gateway endpoints in a synthesized template. */
|
|
62
|
+
function endpointCounts(template: Template): { iface: number; gateway: number } {
|
|
63
|
+
const eps = template.findResources(IFACE);
|
|
64
|
+
let iface = 0;
|
|
65
|
+
let gateway = 0;
|
|
66
|
+
for (const ep of Object.values(eps)) {
|
|
67
|
+
const type = (ep as { Properties?: { VpcEndpointType?: string } }).Properties?.VpcEndpointType;
|
|
68
|
+
// Interface endpoints set VpcEndpointType: 'Interface'; gateway endpoints
|
|
69
|
+
// default the property (Gateway) and omit it in the synthesized template.
|
|
70
|
+
if (type === 'Interface') iface += 1;
|
|
71
|
+
else gateway += 1;
|
|
72
|
+
}
|
|
73
|
+
return { iface, gateway };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe('finalizeVpc — endpoint provisioning', () => {
|
|
77
|
+
it('always provisions CloudWatch Logs (interface) and S3 (gateway), even with no BBs', () => {
|
|
78
|
+
const template = synthWith(() => {});
|
|
79
|
+
const { iface, gateway } = endpointCounts(template);
|
|
80
|
+
assert.equal(gateway, 1); // S3 gateway, always-on (free)
|
|
81
|
+
assert.equal(iface, 1); // Logs, always-on
|
|
82
|
+
template.hasResourceProperties(IFACE, {
|
|
83
|
+
VpcEndpointType: 'Interface',
|
|
84
|
+
ServiceName: 'com.amazonaws.us-east-1.logs',
|
|
85
|
+
});
|
|
86
|
+
// Exactly one gateway endpoint (S3). Its ServiceName synthesizes as a
|
|
87
|
+
// region-tokenized Fn::Join, so assert by type/count rather than a literal.
|
|
88
|
+
const gateways = template.findResources(IFACE, { Properties: { VpcEndpointType: 'Gateway' } });
|
|
89
|
+
assert.equal(Object.keys(gateways).length, 1, 'exactly one gateway endpoint (S3)');
|
|
90
|
+
// SSM is NOT always-on — it flows from BB requirements only.
|
|
91
|
+
const ssm = template.findResources(IFACE, {
|
|
92
|
+
Properties: { ServiceName: 'com.amazonaws.us-east-1.ssm' },
|
|
93
|
+
});
|
|
94
|
+
assert.equal(Object.keys(ssm).length, 0, 'no SSM endpoint without a BB that needs it');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('provisions the union of BB-declared endpoints plus the always-on Logs + S3', () => {
|
|
98
|
+
const template = synthWith((scope) => {
|
|
99
|
+
new FakeBB(scope, 'Kv', { gatewayEndpoints: [ec2.GatewayVpcEndpointAwsService.DYNAMODB] });
|
|
100
|
+
new FakeBB(scope, 'Bucket', { gatewayEndpoints: [ec2.GatewayVpcEndpointAwsService.S3] });
|
|
101
|
+
new FakeBB(scope, 'Job', { interfaceEndpoints: [ec2.InterfaceVpcEndpointAwsService.SQS] });
|
|
102
|
+
new FakeBB(scope, 'Db', {
|
|
103
|
+
interfaceEndpoints: [
|
|
104
|
+
ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
|
|
105
|
+
ec2.InterfaceVpcEndpointAwsService.RDS_DATA,
|
|
106
|
+
],
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
const { iface, gateway } = endpointCounts(template);
|
|
110
|
+
// Gateways: DynamoDB + S3 (BB's S3 dedups with the always-on S3).
|
|
111
|
+
assert.equal(gateway, 2);
|
|
112
|
+
// Interfaces: SQS + Secrets Manager + RDS Data + always-on Logs.
|
|
113
|
+
assert.equal(iface, 4);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('deduplicates endpoints requested by multiple BBs', () => {
|
|
117
|
+
const template = synthWith((scope) => {
|
|
118
|
+
new FakeBB(scope, 'Kv1', { gatewayEndpoints: [ec2.GatewayVpcEndpointAwsService.DYNAMODB] });
|
|
119
|
+
new FakeBB(scope, 'Kv2', { gatewayEndpoints: [ec2.GatewayVpcEndpointAwsService.DYNAMODB] });
|
|
120
|
+
new FakeBB(scope, 'Job1', { interfaceEndpoints: [ec2.InterfaceVpcEndpointAwsService.SQS] });
|
|
121
|
+
new FakeBB(scope, 'Job2', { interfaceEndpoints: [ec2.InterfaceVpcEndpointAwsService.SQS] });
|
|
122
|
+
});
|
|
123
|
+
const { iface, gateway } = endpointCounts(template);
|
|
124
|
+
// Two DynamoDB requests collapse to one; plus always-on S3 = 2 gateways.
|
|
125
|
+
assert.equal(gateway, 2);
|
|
126
|
+
// Two SQS requests collapse to one; plus always-on Logs = 2 interfaces.
|
|
127
|
+
assert.equal(iface, 2);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('provisions SSM only when a BB declares it (and dedups multiple declarations)', () => {
|
|
131
|
+
const template = synthWith((scope) => {
|
|
132
|
+
new FakeBB(scope, 'AppSetting', { interfaceEndpoints: [ec2.InterfaceVpcEndpointAwsService.SSM] });
|
|
133
|
+
new FakeBB(scope, 'Auth', { interfaceEndpoints: [ec2.InterfaceVpcEndpointAwsService.SSM] });
|
|
134
|
+
});
|
|
135
|
+
const { iface } = endpointCounts(template);
|
|
136
|
+
// Two SSM requests collapse to one; plus always-on Logs = 2 interfaces.
|
|
137
|
+
assert.equal(iface, 2);
|
|
138
|
+
template.hasResourceProperties(IFACE, {
|
|
139
|
+
VpcEndpointType: 'Interface',
|
|
140
|
+
ServiceName: 'com.amazonaws.us-east-1.ssm',
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('provisions nothing when provisionEndpoints is false (but still validates requiresEgress)', () => {
|
|
145
|
+
const template = synthWith(
|
|
146
|
+
(scope) => {
|
|
147
|
+
new FakeBB(scope, 'Kv', { gatewayEndpoints: [ec2.GatewayVpcEndpointAwsService.DYNAMODB] });
|
|
148
|
+
},
|
|
149
|
+
{ provisionEndpoints: false },
|
|
150
|
+
);
|
|
151
|
+
template.resourceCountIs(IFACE, 0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('scopes the interface-endpoint SG to 443 from the Blocks Lambda SG only', () => {
|
|
155
|
+
const template = synthWith((scope) => {
|
|
156
|
+
new FakeBB(scope, 'Job', { interfaceEndpoints: [ec2.InterfaceVpcEndpointAwsService.SQS] });
|
|
157
|
+
});
|
|
158
|
+
// A dedicated endpoint SG exists, described as Lambda-scoped, with a 443
|
|
159
|
+
// ingress rule sourced from another SG (the Lambda SG) rather than the whole
|
|
160
|
+
// VPC CIDR. We assert the rule is present (arrayWith) rather than the exact
|
|
161
|
+
// array, since CDK may add its own paired rule for the endpoint association.
|
|
162
|
+
const sgs = template.findResources('AWS::EC2::SecurityGroup', {
|
|
163
|
+
Properties: { GroupDescription: 'Blocks interface VPC endpoints — 443 from the Blocks Lambda only' },
|
|
164
|
+
});
|
|
165
|
+
assert.equal(Object.keys(sgs).length, 1, 'exactly one dedicated endpoint SG');
|
|
166
|
+
const sg = Object.values(sgs)[0] as {
|
|
167
|
+
Properties: { SecurityGroupIngress: Array<{ FromPort?: number; ToPort?: number; CidrIp?: string }> };
|
|
168
|
+
};
|
|
169
|
+
const ingress = sg.Properties.SecurityGroupIngress ?? [];
|
|
170
|
+
const https = ingress.filter((r) => r.FromPort === 443 && r.ToPort === 443);
|
|
171
|
+
assert.equal(https.length, 1, 'exactly one 443 ingress rule');
|
|
172
|
+
// Must NOT be open to a CIDR (that would be the broad default we're avoiding).
|
|
173
|
+
assert.equal(https[0].CidrIp, undefined, '443 ingress is SG-sourced, not CIDR-open');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('finalizeVpc — requiresEgress validation', () => {
|
|
178
|
+
it('passes when the runtime placement provides egress', () => {
|
|
179
|
+
// Default placement is private-with-egress, which provides egress.
|
|
180
|
+
assert.doesNotThrow(() =>
|
|
181
|
+
synthWith((scope) => {
|
|
182
|
+
new FakeBB(scope, 'Dsql', { requiresEgress: true });
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('throws an actionable, BB-named error when the placement has no egress', () => {
|
|
188
|
+
// Place the runtime in isolated subnets; a BB needing egress cannot be served.
|
|
189
|
+
assert.throws(
|
|
190
|
+
() =>
|
|
191
|
+
synthWith(
|
|
192
|
+
(scope) => {
|
|
193
|
+
new FakeBB(scope, 'Dsql', { requiresEgress: true });
|
|
194
|
+
},
|
|
195
|
+
{ subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED } },
|
|
196
|
+
),
|
|
197
|
+
/Dsql requires its runtime to reach the internet.*no egress/s,
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('does not validate a BB that does not require egress', () => {
|
|
202
|
+
// A BB with no egress need is fine in isolated subnets.
|
|
203
|
+
assert.doesNotThrow(() =>
|
|
204
|
+
synthWith(
|
|
205
|
+
(scope) => {
|
|
206
|
+
new FakeBB(scope, 'Bb', {});
|
|
207
|
+
},
|
|
208
|
+
{ subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED } },
|
|
209
|
+
),
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('initializeVpc — Lambda placement guard', () => {
|
|
215
|
+
function init(natGateways: number, subnets?: ec2.SubnetSelection) {
|
|
216
|
+
const app = new cdk.App();
|
|
217
|
+
const stack = new cdk.Stack(app, 'S', { env: { account: '123456789012', region: 'us-east-1' } });
|
|
218
|
+
const subnetConfiguration =
|
|
219
|
+
natGateways === 0
|
|
220
|
+
? [{ name: 'isolated', subnetType: ec2.SubnetType.PRIVATE_ISOLATED, cidrMask: 24 }]
|
|
221
|
+
: undefined;
|
|
222
|
+
const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 2, natGateways, subnetConfiguration });
|
|
223
|
+
return () => initializeVpc(stack, { network: vpc, subnets });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
it('throws an actionable error when defaulting to private-with-egress but the VPC has none', () => {
|
|
227
|
+
assert.throws(init(0), /no private-with-egress subnets.*NAT gateway/s);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('does not throw when the VPC has a private-with-egress tier', () => {
|
|
231
|
+
assert.doesNotThrow(init(1));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('honors an explicit isolated placement without the private-with-egress guard', () => {
|
|
235
|
+
// Caller explicitly chose isolated; the default-path guard must not fire.
|
|
236
|
+
assert.doesNotThrow(init(0, { subnetType: ec2.SubnetType.PRIVATE_ISOLATED }));
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('VpcContext.selectSubnets — instructive resolution', () => {
|
|
241
|
+
function contextFor(natGateways: number) {
|
|
242
|
+
const app = new cdk.App();
|
|
243
|
+
const stack = new cdk.Stack(app, 'S', { env: { account: '123456789012', region: 'us-east-1' } });
|
|
244
|
+
const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 2, natGateways });
|
|
245
|
+
// With natGateways: 0 CDK still creates the default public+private config,
|
|
246
|
+
// but the private tier becomes isolated (no NAT). Build an explicit
|
|
247
|
+
// isolated-only VPC to exercise the "no isolated" and "no egress" cases.
|
|
248
|
+
return initializeVpc(stack, { network: vpc, subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS } });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
it('returns the requested role when the VPC has it', () => {
|
|
252
|
+
const ctx = contextFor(1);
|
|
253
|
+
const sel = ctx.selectSubnets({ fullId: 'app/db' }, 'private-with-egress');
|
|
254
|
+
assert.equal(sel.subnetType, ec2.SubnetType.PRIVATE_WITH_EGRESS);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('falls back to an explicit fallback role when the requested role is absent', () => {
|
|
258
|
+
const ctx = contextFor(1); // has public + private-with-egress, no isolated
|
|
259
|
+
const sel = ctx.selectSubnets({ fullId: 'app/db' }, 'isolated', { fallback: 'private-with-egress' });
|
|
260
|
+
assert.equal(sel.subnetType, ec2.SubnetType.PRIVATE_WITH_EGRESS);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('throws a BB-named error when neither the role nor its fallback exists', () => {
|
|
264
|
+
const ctx = contextFor(1); // no isolated tier, and no public fallback requested that helps
|
|
265
|
+
assert.throws(
|
|
266
|
+
() => ctx.selectSubnets({ fullId: 'app/db' }, 'isolated'),
|
|
267
|
+
/app\/db needs a 'isolated' subnet.*has none/s,
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// We can't fully test CDK constructs without a Stack, but we can test
|
|
273
|
+
// the getVpcContext logic with mock constructs.
|
|
274
|
+
|
|
275
|
+
describe('VPC utilities', () => {
|
|
276
|
+
it('getVpcContext returns undefined when no context set', () => {
|
|
277
|
+
const fakeScope: any = { node: { scope: undefined } };
|
|
278
|
+
assert.equal(getVpcContext(fakeScope), undefined);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('getVpcContext walks up the scope tree', () => {
|
|
282
|
+
const vpcContext = { vpc: 'mock-vpc', computeSecurityGroup: 'mock-sg', computeSubnets: {} };
|
|
283
|
+
const parent: any = { node: { scope: undefined } };
|
|
284
|
+
setVpcContext(parent, vpcContext as any);
|
|
285
|
+
|
|
286
|
+
const child: any = { node: { scope: parent } };
|
|
287
|
+
assert.equal(getVpcContext(child), vpcContext);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('setVpcContext stores context on the scope', () => {
|
|
291
|
+
const fakeScope: any = { node: { scope: undefined } };
|
|
292
|
+
const vpcContext = { vpc: 'mock-vpc', computeSecurityGroup: 'mock-sg', computeSubnets: {} };
|
|
293
|
+
setVpcContext(fakeScope, vpcContext as any);
|
|
294
|
+
assert.equal(getVpcContext(fakeScope), vpcContext);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('lazy VPC derivation', () => {
|
|
299
|
+
function stackWith(bbs: (scope: Construct) => void): { stack: cdk.Stack; scope: Construct } {
|
|
300
|
+
const app = new cdk.App();
|
|
301
|
+
const stack = new cdk.Stack(app, 'LazyStack', { env: { account: '123456789012', region: 'us-east-1' } });
|
|
302
|
+
bbs(stack);
|
|
303
|
+
return { stack, scope: stack };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
it('anyRequirementNeedsVpc is false for BBs that only declare endpoints', () => {
|
|
307
|
+
const { scope } = stackWith((s) => {
|
|
308
|
+
new FakeBB(s, 'Kv', { gatewayEndpoints: [ec2.GatewayVpcEndpointAwsService.DYNAMODB] });
|
|
309
|
+
new FakeBB(s, 'Job', { interfaceEndpoints: [ec2.InterfaceVpcEndpointAwsService.SQS] });
|
|
310
|
+
});
|
|
311
|
+
assert.equal(anyRequirementNeedsVpc(scope), false);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('anyRequirementNeedsVpc is true when a BB declares requiresVpc', () => {
|
|
315
|
+
const { scope } = stackWith((s) => {
|
|
316
|
+
new FakeBB(s, 'Kv', { gatewayEndpoints: [ec2.GatewayVpcEndpointAwsService.DYNAMODB] });
|
|
317
|
+
new FakeBB(s, 'Container', { requiresVpc: true });
|
|
318
|
+
});
|
|
319
|
+
assert.equal(anyRequirementNeedsVpc(scope), true);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('getOrCreateVpc returns the same VPC on repeated calls (singleton)', () => {
|
|
323
|
+
const { scope } = stackWith(() => {});
|
|
324
|
+
const a = getOrCreateVpc(scope);
|
|
325
|
+
const b = getOrCreateVpc(scope);
|
|
326
|
+
assert.equal(a, b, 'lazy VPC is a per-stack singleton');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('a derived VPC + finalizeVpc provisions endpoints for a requiresVpc app', () => {
|
|
330
|
+
const app = new cdk.App();
|
|
331
|
+
const stack = new cdk.Stack(app, 'DerivedStack', { env: { account: '123456789012', region: 'us-east-1' } });
|
|
332
|
+
new FakeBB(stack, 'Container', {
|
|
333
|
+
requiresVpc: true,
|
|
334
|
+
interfaceEndpoints: [ec2.InterfaceVpcEndpointAwsService.SQS],
|
|
335
|
+
});
|
|
336
|
+
assert.equal(anyRequirementNeedsVpc(stack), true);
|
|
337
|
+
const vpc = getOrCreateVpc(stack);
|
|
338
|
+
const options = { network: vpc };
|
|
339
|
+
initializeVpc(stack, options);
|
|
340
|
+
finalizeVpc(stack, options);
|
|
341
|
+
const template = Template.fromStack(stack);
|
|
342
|
+
// SQS (from the BB) + always-on CloudWatch Logs interface endpoints, plus
|
|
343
|
+
// the always-on S3 gateway endpoint.
|
|
344
|
+
const { iface, gateway } = endpointCounts(template);
|
|
345
|
+
assert.equal(iface, 2, 'SQS + Logs');
|
|
346
|
+
assert.equal(gateway, 1, 'S3 gateway');
|
|
347
|
+
});
|
|
348
|
+
});
|