@fjall/components-infrastructure 5.0.0 → 6.0.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/dist/lib/config/aws/ebsDefaultEncryption.js +1 -1
- package/dist/lib/config/aws/inspectorEnablement.js +1 -1
- package/dist/lib/config/aws/oidcConnector.js +1 -1
- package/dist/lib/config/aws/s3BlockPublicAccess.js +1 -1
- package/dist/lib/config/aws/securityServicesAdmin.js +2 -2
- package/dist/lib/lambda-assets/cert-generator/asset/index.js +31 -31
- package/dist/lib/patterns/aws/compute.js +1 -1
- package/dist/lib/patterns/aws/computeEcs.js +5 -0
- package/dist/lib/patterns/aws/computeLambda.d.ts +1 -1
- package/dist/lib/patterns/aws/devSubstrate.js +1 -1
- package/dist/lib/patterns/aws/payload.js +3 -3
- package/dist/lib/patterns/aws/staticSite.js +1 -1
- package/dist/lib/resources/aws/compute/ec2GracefulTerminationHandler.js +1 -1
- package/dist/lib/resources/aws/compute/ecsCapacityConfig.d.ts +43 -0
- package/dist/lib/resources/aws/compute/ecsCapacityConfig.js +161 -0
- package/dist/lib/resources/aws/compute/ecsConstants.d.ts +4 -0
- package/dist/lib/resources/aws/compute/ecsConstants.js +9 -0
- package/dist/lib/resources/aws/compute/ecsLifecycleHookMigration.js +1 -1
- package/dist/lib/resources/aws/compute/ecsServiceFactory.d.ts +4 -10
- package/dist/lib/resources/aws/compute/ecsServiceFactory.js +7 -101
- package/dist/lib/resources/aws/compute/ecsValidation.js +6 -0
- package/dist/lib/resources/aws/compute/persistentDataVolume.js +1 -1
- package/dist/lib/resources/aws/database/rdsInstance.js +1 -1
- package/dist/lib/resources/aws/iam/identityCenter/user.js +1 -1
- package/dist/lib/resources/aws/networking/crossAccountReturnRoutes.js +1 -1
- package/dist/lib/resources/aws/networking/ipamPool.js +1 -1
- package/dist/lib/resources/aws/organisation/costAllocationTagActivator.js +1 -1
- package/dist/lib/resources/aws/utilities/tlsCertGenerator.js +1 -1
- package/dist/lib/utils/engineCompat.d.ts +12 -5
- package/dist/lib/utils/engineCompat.js +41 -22
- package/package.json +10 -5
|
@@ -11,7 +11,7 @@ export class EbsDefaultEncryption extends Construct {
|
|
|
11
11
|
constructor(scope, id) {
|
|
12
12
|
super(scope, id);
|
|
13
13
|
new CustomResource(this, "EbsDefaultEncryption", {
|
|
14
|
-
runtime: Runtime.
|
|
14
|
+
runtime: Runtime.NODEJS_24_X,
|
|
15
15
|
timeout: Duration.minutes(5),
|
|
16
16
|
lambdaDescription: "Enables EBS default encryption at account level",
|
|
17
17
|
inlinePolicy: [
|
|
@@ -12,7 +12,7 @@ export class InspectorEnablement extends Construct {
|
|
|
12
12
|
constructor(scope, id) {
|
|
13
13
|
super(scope, id);
|
|
14
14
|
new CustomResource(this, "InspectorEnable", {
|
|
15
|
-
runtime: Runtime.
|
|
15
|
+
runtime: Runtime.NODEJS_24_X,
|
|
16
16
|
timeout: Duration.minutes(5),
|
|
17
17
|
lambdaDescription: "Enables/disables Inspector v2 for the account",
|
|
18
18
|
inlinePolicy: [
|
|
@@ -74,7 +74,7 @@ export class OidcConnector extends Construct {
|
|
|
74
74
|
const issuerDomain = FJALL_OIDC_ISSUER_DOMAIN;
|
|
75
75
|
const issuerUrl = `https://${issuerDomain}`;
|
|
76
76
|
const providerResource = new CustomResource(this, "OidcProvider", {
|
|
77
|
-
runtime: Runtime.
|
|
77
|
+
runtime: Runtime.NODEJS_24_X,
|
|
78
78
|
timeout: Duration.minutes(5),
|
|
79
79
|
lambdaDescription: "Adopt-or-create the account-global Fjall OIDC provider",
|
|
80
80
|
resourceType: "Custom::FjallOidcProvider",
|
|
@@ -25,7 +25,7 @@ export class S3BlockPublicAccess extends Construct {
|
|
|
25
25
|
const account = getConfig().providerAccounts.find((pa) => pa.id === props.accountId);
|
|
26
26
|
const enforce = (account?.s3BlockPublicAccess ?? "off") === "enforced";
|
|
27
27
|
new CustomResource(this, "S3BlockPublicAccess", {
|
|
28
|
-
runtime: Runtime.
|
|
28
|
+
runtime: Runtime.NODEJS_24_X,
|
|
29
29
|
timeout: Duration.minutes(5),
|
|
30
30
|
lambdaDescription: "Manages S3 Block Public Access at account level",
|
|
31
31
|
rolePath: "/fjall/",
|
|
@@ -35,7 +35,7 @@ export class SecurityServicesAdmin extends Construct {
|
|
|
35
35
|
}
|
|
36
36
|
createGuardDutyAdmin() {
|
|
37
37
|
new CustomResource(this, "GuardDutyOrgConfig", {
|
|
38
|
-
runtime: Runtime.
|
|
38
|
+
runtime: Runtime.NODEJS_24_X,
|
|
39
39
|
timeout: Duration.minutes(5),
|
|
40
40
|
lambdaDescription: "Configures GuardDuty organisation-wide auto-enablement",
|
|
41
41
|
inlinePolicy: [
|
|
@@ -70,7 +70,7 @@ exports.handler = async (event) => {
|
|
|
70
70
|
}
|
|
71
71
|
createSecurityHubAdmin() {
|
|
72
72
|
new CustomResource(this, "SecurityHubOrgConfig", {
|
|
73
|
-
runtime: Runtime.
|
|
73
|
+
runtime: Runtime.NODEJS_24_X,
|
|
74
74
|
timeout: Duration.minutes(5),
|
|
75
75
|
lambdaDescription: "Configures SecurityHub organisation-wide auto-enablement",
|
|
76
76
|
inlinePolicy: [
|
|
@@ -6549,15 +6549,15 @@ var require_parser = __commonJS({
|
|
|
6549
6549
|
var helper_1 = require_helper();
|
|
6550
6550
|
var storage_1 = require_storage();
|
|
6551
6551
|
var AsnParser = class {
|
|
6552
|
-
static parse(data, target) {
|
|
6553
|
-
const asn1Parsed = asn1js.fromBER((0, bytes_1.toArrayBuffer)(data));
|
|
6552
|
+
static parse(data, target, options) {
|
|
6553
|
+
const asn1Parsed = asn1js.fromBER((0, bytes_1.toArrayBuffer)(data), options?.berOptions);
|
|
6554
6554
|
if (asn1Parsed.result.error) {
|
|
6555
6555
|
throw new Error(asn1Parsed.result.error);
|
|
6556
6556
|
}
|
|
6557
|
-
const res = this.fromASN(asn1Parsed.result, target);
|
|
6557
|
+
const res = this.fromASN(asn1Parsed.result, target, options);
|
|
6558
6558
|
return res;
|
|
6559
6559
|
}
|
|
6560
|
-
static fromASN(asn1Schema, target) {
|
|
6560
|
+
static fromASN(asn1Schema, target, options) {
|
|
6561
6561
|
try {
|
|
6562
6562
|
if ((0, helper_1.isConvertible)(target)) {
|
|
6563
6563
|
const value = new target();
|
|
@@ -6566,7 +6566,7 @@ var require_parser = __commonJS({
|
|
|
6566
6566
|
const schema = storage_1.schemaStorage.get(target);
|
|
6567
6567
|
storage_1.schemaStorage.cache(target);
|
|
6568
6568
|
let targetSchema = schema.schema;
|
|
6569
|
-
const choiceResult = this.handleChoiceTypes(asn1Schema, schema, target, targetSchema);
|
|
6569
|
+
const choiceResult = this.handleChoiceTypes(asn1Schema, schema, target, targetSchema, options);
|
|
6570
6570
|
if (choiceResult?.result) {
|
|
6571
6571
|
return choiceResult.result;
|
|
6572
6572
|
}
|
|
@@ -6576,9 +6576,9 @@ var require_parser = __commonJS({
|
|
|
6576
6576
|
const sequenceResult = this.handleSequenceTypes(asn1Schema, schema, target, targetSchema);
|
|
6577
6577
|
const res = new target();
|
|
6578
6578
|
if ((0, helper_1.isTypeOfArray)(target)) {
|
|
6579
|
-
return this.handleArrayTypes(asn1Schema, schema, target);
|
|
6579
|
+
return this.handleArrayTypes(asn1Schema, schema, target, options);
|
|
6580
6580
|
}
|
|
6581
|
-
this.processSchemaItems(schema, sequenceResult, res);
|
|
6581
|
+
this.processSchemaItems(schema, sequenceResult, res, options);
|
|
6582
6582
|
return res;
|
|
6583
6583
|
} catch (error) {
|
|
6584
6584
|
if (error instanceof errors_1.AsnSchemaValidationError) {
|
|
@@ -6587,7 +6587,7 @@ var require_parser = __commonJS({
|
|
|
6587
6587
|
throw error;
|
|
6588
6588
|
}
|
|
6589
6589
|
}
|
|
6590
|
-
static handleChoiceTypes(asn1Schema, schema, target, targetSchema) {
|
|
6590
|
+
static handleChoiceTypes(asn1Schema, schema, target, targetSchema, options) {
|
|
6591
6591
|
if (asn1Schema.constructor === asn1js.Constructed && schema.type === enums_1.AsnTypeTypes.Choice && asn1Schema.idBlock.tagClass === 3) {
|
|
6592
6592
|
for (const key in schema.items) {
|
|
6593
6593
|
const schemaItem = schema.items[key];
|
|
@@ -6598,7 +6598,7 @@ var require_parser = __commonJS({
|
|
|
6598
6598
|
const newSeq = new asn1js.Sequence();
|
|
6599
6599
|
if ("value" in asn1Schema.valueBlock && Array.isArray(asn1Schema.valueBlock.value) && "value" in newSeq.valueBlock) {
|
|
6600
6600
|
newSeq.valueBlock.value = asn1Schema.valueBlock.value;
|
|
6601
|
-
const fieldValue = this.fromASN(newSeq, schemaItem.type);
|
|
6601
|
+
const fieldValue = this.fromASN(newSeq, schemaItem.type, options);
|
|
6602
6602
|
const res = new target();
|
|
6603
6603
|
res[key] = fieldValue;
|
|
6604
6604
|
return { result: res };
|
|
@@ -6689,7 +6689,7 @@ var require_parser = __commonJS({
|
|
|
6689
6689
|
throw err;
|
|
6690
6690
|
}
|
|
6691
6691
|
}
|
|
6692
|
-
static handleArrayTypes(asn1Schema, schema, target) {
|
|
6692
|
+
static handleArrayTypes(asn1Schema, schema, target, options) {
|
|
6693
6693
|
if (!("value" in asn1Schema.valueBlock && Array.isArray(asn1Schema.valueBlock.value))) {
|
|
6694
6694
|
throw new Error("Cannot get items from the ASN.1 parsed value. ASN.1 object is not constructed.");
|
|
6695
6695
|
}
|
|
@@ -6701,10 +6701,10 @@ var require_parser = __commonJS({
|
|
|
6701
6701
|
}
|
|
6702
6702
|
return target.from(asn1Schema.valueBlock.value, (element) => converter.fromASN(element));
|
|
6703
6703
|
} else {
|
|
6704
|
-
return target.from(asn1Schema.valueBlock.value, (element) => this.fromASN(element, itemType));
|
|
6704
|
+
return target.from(asn1Schema.valueBlock.value, (element) => this.fromASN(element, itemType, options));
|
|
6705
6705
|
}
|
|
6706
6706
|
}
|
|
6707
|
-
static processSchemaItems(schema, asn1ComparedSchema, res) {
|
|
6707
|
+
static processSchemaItems(schema, asn1ComparedSchema, res, options) {
|
|
6708
6708
|
for (const key in schema.items) {
|
|
6709
6709
|
const asn1SchemaValue = asn1ComparedSchema.result[key];
|
|
6710
6710
|
if (!asn1SchemaValue) {
|
|
@@ -6714,9 +6714,9 @@ var require_parser = __commonJS({
|
|
|
6714
6714
|
const schemaItemType = schemaItem.type;
|
|
6715
6715
|
let parsedValue;
|
|
6716
6716
|
if (typeof schemaItemType === "number" || (0, helper_1.isConvertible)(schemaItemType)) {
|
|
6717
|
-
parsedValue = this.processPrimitiveSchemaItem(asn1SchemaValue, schemaItem, schemaItemType);
|
|
6717
|
+
parsedValue = this.processPrimitiveSchemaItem(asn1SchemaValue, schemaItem, schemaItemType, options);
|
|
6718
6718
|
} else {
|
|
6719
|
-
parsedValue = this.processComplexSchemaItem(asn1SchemaValue, schemaItem, schemaItemType);
|
|
6719
|
+
parsedValue = this.processComplexSchemaItem(asn1SchemaValue, schemaItem, schemaItemType, options);
|
|
6720
6720
|
}
|
|
6721
6721
|
if (parsedValue && typeof parsedValue === "object" && "value" in parsedValue && "raw" in parsedValue) {
|
|
6722
6722
|
res[key] = parsedValue.value;
|
|
@@ -6726,23 +6726,23 @@ var require_parser = __commonJS({
|
|
|
6726
6726
|
}
|
|
6727
6727
|
}
|
|
6728
6728
|
}
|
|
6729
|
-
static processPrimitiveSchemaItem(asn1SchemaValue, schemaItem, schemaItemType) {
|
|
6729
|
+
static processPrimitiveSchemaItem(asn1SchemaValue, schemaItem, schemaItemType, options) {
|
|
6730
6730
|
const converter = schemaItem.converter ?? ((0, helper_1.isConvertible)(schemaItemType) ? new schemaItemType() : null);
|
|
6731
6731
|
if (!converter) {
|
|
6732
6732
|
throw new Error("Converter is empty");
|
|
6733
6733
|
}
|
|
6734
6734
|
if (schemaItem.repeated) {
|
|
6735
|
-
return this.processRepeatedPrimitiveItem(asn1SchemaValue, schemaItem, converter);
|
|
6735
|
+
return this.processRepeatedPrimitiveItem(asn1SchemaValue, schemaItem, converter, options);
|
|
6736
6736
|
} else {
|
|
6737
|
-
return this.processSinglePrimitiveItem(asn1SchemaValue, schemaItem, schemaItemType, converter);
|
|
6737
|
+
return this.processSinglePrimitiveItem(asn1SchemaValue, schemaItem, schemaItemType, converter, options);
|
|
6738
6738
|
}
|
|
6739
6739
|
}
|
|
6740
|
-
static processRepeatedPrimitiveItem(asn1SchemaValue, schemaItem, converter) {
|
|
6740
|
+
static processRepeatedPrimitiveItem(asn1SchemaValue, schemaItem, converter, options) {
|
|
6741
6741
|
if (schemaItem.implicit) {
|
|
6742
6742
|
const Container = schemaItem.repeated === "sequence" ? asn1js.Sequence : asn1js.Set;
|
|
6743
6743
|
const newItem = new Container();
|
|
6744
6744
|
newItem.valueBlock = asn1SchemaValue.valueBlock;
|
|
6745
|
-
const newItemAsn = asn1js.fromBER(newItem.toBER(false));
|
|
6745
|
+
const newItemAsn = asn1js.fromBER(newItem.toBER(false), options?.berOptions);
|
|
6746
6746
|
if (newItemAsn.offset === -1) {
|
|
6747
6747
|
throw new Error(`Cannot parse the child item. ${newItemAsn.result.error}`);
|
|
6748
6748
|
}
|
|
@@ -6755,7 +6755,7 @@ var require_parser = __commonJS({
|
|
|
6755
6755
|
return Array.from(asn1SchemaValue, (element) => converter.fromASN(element));
|
|
6756
6756
|
}
|
|
6757
6757
|
}
|
|
6758
|
-
static processSinglePrimitiveItem(asn1SchemaValue, schemaItem, schemaItemType, converter) {
|
|
6758
|
+
static processSinglePrimitiveItem(asn1SchemaValue, schemaItem, schemaItemType, converter, options) {
|
|
6759
6759
|
let value = asn1SchemaValue;
|
|
6760
6760
|
if (schemaItem.implicit) {
|
|
6761
6761
|
let newItem;
|
|
@@ -6770,21 +6770,21 @@ var require_parser = __commonJS({
|
|
|
6770
6770
|
newItem = new Asn1Type();
|
|
6771
6771
|
}
|
|
6772
6772
|
newItem.valueBlock = value.valueBlock;
|
|
6773
|
-
value = asn1js.fromBER(newItem.toBER(false)).result;
|
|
6773
|
+
value = asn1js.fromBER(newItem.toBER(false), options?.berOptions).result;
|
|
6774
6774
|
}
|
|
6775
6775
|
return converter.fromASN(value);
|
|
6776
6776
|
}
|
|
6777
|
-
static processComplexSchemaItem(asn1SchemaValue, schemaItem, schemaItemType) {
|
|
6777
|
+
static processComplexSchemaItem(asn1SchemaValue, schemaItem, schemaItemType, options) {
|
|
6778
6778
|
if (schemaItem.repeated) {
|
|
6779
6779
|
if (!Array.isArray(asn1SchemaValue)) {
|
|
6780
6780
|
throw new Error("Cannot get list of items from the ASN.1 parsed value. ASN.1 value should be iterable.");
|
|
6781
6781
|
}
|
|
6782
|
-
return Array.from(asn1SchemaValue, (element) => this.fromASN(element, schemaItemType));
|
|
6782
|
+
return Array.from(asn1SchemaValue, (element) => this.fromASN(element, schemaItemType, options));
|
|
6783
6783
|
} else {
|
|
6784
6784
|
const valueToProcess = this.handleImplicitTagging(asn1SchemaValue, schemaItem, schemaItemType);
|
|
6785
6785
|
if (this.isOptionalChoiceField(schemaItem)) {
|
|
6786
6786
|
try {
|
|
6787
|
-
return this.fromASN(valueToProcess, schemaItemType);
|
|
6787
|
+
return this.fromASN(valueToProcess, schemaItemType, options);
|
|
6788
6788
|
} catch (err) {
|
|
6789
6789
|
if (err instanceof errors_1.AsnSchemaValidationError && /Wrong values for Choice type/.test(err.message)) {
|
|
6790
6790
|
return void 0;
|
|
@@ -6792,7 +6792,7 @@ var require_parser = __commonJS({
|
|
|
6792
6792
|
throw err;
|
|
6793
6793
|
}
|
|
6794
6794
|
} else {
|
|
6795
|
-
const parsedValue = this.fromASN(valueToProcess, schemaItemType);
|
|
6795
|
+
const parsedValue = this.fromASN(valueToProcess, schemaItemType, options);
|
|
6796
6796
|
if (schemaItem.raw) {
|
|
6797
6797
|
return {
|
|
6798
6798
|
value: parsedValue,
|
|
@@ -7009,12 +7009,12 @@ var require_convert = __commonJS({
|
|
|
7009
7009
|
static serialize(obj) {
|
|
7010
7010
|
return serializer_1.AsnSerializer.serialize(obj);
|
|
7011
7011
|
}
|
|
7012
|
-
static parse(data, target) {
|
|
7013
|
-
return parser_1.AsnParser.parse(data, target);
|
|
7012
|
+
static parse(data, target, options) {
|
|
7013
|
+
return parser_1.AsnParser.parse(data, target, options);
|
|
7014
7014
|
}
|
|
7015
|
-
static toString(data) {
|
|
7015
|
+
static toString(data, options) {
|
|
7016
7016
|
const buf = (0, bytes_1.isBufferSource)(data) ? (0, bytes_1.toArrayBuffer)(data) : _AsnConvert.serialize(data);
|
|
7017
|
-
const asn = asn1js.fromBER(buf);
|
|
7017
|
+
const asn = asn1js.fromBER(buf, options?.berOptions);
|
|
7018
7018
|
if (asn.offset === -1) {
|
|
7019
7019
|
throw new Error(`Cannot decode ASN.1 data. ${asn.result.error}`);
|
|
7020
7020
|
}
|
|
@@ -14678,9 +14678,9 @@ var require_cjs11 = __commonJS({
|
|
|
14678
14678
|
}
|
|
14679
14679
|
});
|
|
14680
14680
|
|
|
14681
|
-
// node_modules/@peculiar/x509/build/x509.cjs.js
|
|
14681
|
+
// ../../node_modules/@peculiar/x509/build/x509.cjs.js
|
|
14682
14682
|
var require_x509_cjs = __commonJS({
|
|
14683
|
-
"node_modules/@peculiar/x509/build/x509.cjs.js"(exports2) {
|
|
14683
|
+
"../../node_modules/@peculiar/x509/build/x509.cjs.js"(exports2) {
|
|
14684
14684
|
"use strict";
|
|
14685
14685
|
var asn1Schema = require_cjs();
|
|
14686
14686
|
var asn1X509 = require_cjs2();
|
|
@@ -26,6 +26,7 @@ import { VALIDATION_PATTERNS } from "@fjall/generator";
|
|
|
26
26
|
import { evaluateBakeGuard } from "@fjall/util/docker";
|
|
27
27
|
import { toKebab, buildParameterPath } from "@fjall/util";
|
|
28
28
|
import { validateEc2ServiceSizing, validateEcsDomainConfig, validateSecretName } from "../../resources/aws/compute/ecsValidation.js";
|
|
29
|
+
import { validateSharedEc2CapacityConfig } from "../../resources/aws/compute/ecsCapacityConfig.js";
|
|
29
30
|
import { COMPUTE_DEFAULTS, collectImportedSecretNames } from "./compute.js";
|
|
30
31
|
import { isHookMigrations } from "./computeEcsTypes.js";
|
|
31
32
|
export { ScalingType } from "./computeEcsTypes.js";
|
|
@@ -174,6 +175,10 @@ export function validateEcsProps(props) {
|
|
|
174
175
|
validateSeparateTaskDef(service.name, service.migrations.separateTaskDef);
|
|
175
176
|
}
|
|
176
177
|
}
|
|
178
|
+
// Mirror of the resources-layer shared-ASG capacity check (drift on a reused
|
|
179
|
+
// ASG, shared persistentDataVolume singleton) — defence-in-depth per
|
|
180
|
+
// generator-standards § "Validate at the Lowest Layer the Field Belongs To".
|
|
181
|
+
validateSharedEc2CapacityConfig(props.services);
|
|
177
182
|
// Validate awaitMigrationsFrom cross-service migration ordering.
|
|
178
183
|
const serviceByName = new Map(props.services.map((s) => [s.name, s]));
|
|
179
184
|
for (const service of props.services) {
|
|
@@ -201,7 +201,7 @@ export interface CodeLambdaProps extends BaseLambdaProps {
|
|
|
201
201
|
code: Code;
|
|
202
202
|
/** Handler function. Default: "index.handler" */
|
|
203
203
|
handler?: string;
|
|
204
|
-
/** Lambda runtime. Default:
|
|
204
|
+
/** Lambda runtime. Default: NODEJS_24_X */
|
|
205
205
|
runtime?: Runtime;
|
|
206
206
|
}
|
|
207
207
|
/**
|
|
@@ -317,7 +317,7 @@ export class DevSubstrate extends Construct {
|
|
|
317
317
|
// control plane (ECS / Aurora / SSM), never over the VPC, and the ALB invokes
|
|
318
318
|
// it through the Lambda API — not the network path.
|
|
319
319
|
const waker = new LambdaFunction(this, DEV_SUBSTRATE_WAKER_ID, {
|
|
320
|
-
runtime: Runtime.
|
|
320
|
+
runtime: Runtime.NODEJS_24_X,
|
|
321
321
|
handler: "index.handler",
|
|
322
322
|
code: Code.fromInline(source),
|
|
323
323
|
lambdaDescription: "Dev substrate slot waker",
|
|
@@ -308,7 +308,7 @@ export class Payload extends Construct {
|
|
|
308
308
|
deployment: "code",
|
|
309
309
|
code: Code.fromAsset(`${PAYLOAD_DEFAULTS.SOURCE}/${PAYLOAD_DEFAULTS.OPENNEXT.SERVER}`),
|
|
310
310
|
handler: "index.handler",
|
|
311
|
-
runtime: Runtime.
|
|
311
|
+
runtime: Runtime.NODEJS_24_X,
|
|
312
312
|
architecture: Architecture.ARM_64,
|
|
313
313
|
memorySize,
|
|
314
314
|
timeout,
|
|
@@ -370,7 +370,7 @@ export class Payload extends Construct {
|
|
|
370
370
|
deployment: "code",
|
|
371
371
|
code: Code.fromAsset(`${PAYLOAD_DEFAULTS.SOURCE}/${PAYLOAD_DEFAULTS.OPENNEXT.IMAGE}`),
|
|
372
372
|
handler: "index.handler",
|
|
373
|
-
runtime: Runtime.
|
|
373
|
+
runtime: Runtime.NODEJS_24_X,
|
|
374
374
|
architecture: Architecture.ARM_64,
|
|
375
375
|
memorySize,
|
|
376
376
|
timeout,
|
|
@@ -396,7 +396,7 @@ export class Payload extends Construct {
|
|
|
396
396
|
deployment: "code",
|
|
397
397
|
code: Code.fromAsset(`${PAYLOAD_DEFAULTS.SOURCE}/${PAYLOAD_DEFAULTS.OPENNEXT.REVALIDATION}`),
|
|
398
398
|
handler: "index.handler",
|
|
399
|
-
runtime: Runtime.
|
|
399
|
+
runtime: Runtime.NODEJS_24_X,
|
|
400
400
|
architecture: Architecture.ARM_64,
|
|
401
401
|
memorySize,
|
|
402
402
|
timeout,
|
|
@@ -42,7 +42,7 @@ import { toPascalCase } from "../../utils/capitaliseString.js";
|
|
|
42
42
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
43
43
|
const FORMS_ASSET_DIR = resolve(__dirname, "../../lambda-assets/static-site-forms/asset");
|
|
44
44
|
const FORMS_DEFAULTS = {
|
|
45
|
-
RUNTIME: Runtime.
|
|
45
|
+
RUNTIME: Runtime.NODEJS_24_X,
|
|
46
46
|
ARCHITECTURE: Architecture.ARM_64,
|
|
47
47
|
TIMEOUT_SECONDS: 10,
|
|
48
48
|
MEMORY_MB: 256,
|
|
@@ -144,7 +144,7 @@ export class Ec2GracefulTerminationHandler extends Construct {
|
|
|
144
144
|
]
|
|
145
145
|
: [];
|
|
146
146
|
this.lambda = new LambdaFunction(this, `${id}Fn`, {
|
|
147
|
-
runtime: Runtime.
|
|
147
|
+
runtime: Runtime.NODEJS_24_X,
|
|
148
148
|
handler: "index.handler",
|
|
149
149
|
code: Code.fromInline(source),
|
|
150
150
|
lambdaDescription: `${id} ${EC2_GRACEFUL_TERMINATION_HANDLER_DESCRIPTION}`,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Ec2CapacityConfig, EcsCapacityProvider } from "./ecsTypes.js";
|
|
2
|
+
/** The service + ec2Config that first claimed an EC2 capacity key. */
|
|
3
|
+
export interface Ec2AsgOrigin {
|
|
4
|
+
serviceName: string;
|
|
5
|
+
ec2Config: Ec2CapacityConfig;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Rejects at synth when a service reusing a shared ASG is incompatible with
|
|
9
|
+
* it: either the origin's `persistentDataVolume` makes the ASG a singleton
|
|
10
|
+
* (two tasks would race to attach the same EBS volume), or the reusing
|
|
11
|
+
* service asks for ASG settings that differ from the ones the ASG was built
|
|
12
|
+
* with — those settings would be silently ignored (same silent-ignore class
|
|
13
|
+
* as `validateEc2ServiceSizing`).
|
|
14
|
+
*/
|
|
15
|
+
export declare function assertSharedAsgConfigMatches(serviceName: string, ec2Config: Ec2CapacityConfig, key: string, origin: Ec2AsgOrigin): void;
|
|
16
|
+
/**
|
|
17
|
+
* Generates a unique key for EC2 config so services with matching
|
|
18
|
+
* configurations share an ASG.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getEc2ConfigKey(ec2Config: Ec2CapacityConfig): string;
|
|
21
|
+
/**
|
|
22
|
+
* Minimal structural input for {@link validateSharedEc2CapacityConfig} —
|
|
23
|
+
* exactly the fields the check reads. Both the resources-layer
|
|
24
|
+
* `EcsServiceProps` and the patterns-layer `EcsServiceConfig` structurally
|
|
25
|
+
* satisfy it (typescript-standards § "Helpers consume resolved state, not the
|
|
26
|
+
* dispatch enum").
|
|
27
|
+
*/
|
|
28
|
+
export interface SharedEc2CapacityServiceInput {
|
|
29
|
+
name: string;
|
|
30
|
+
capacityProvider?: EcsCapacityProvider;
|
|
31
|
+
ec2Config?: Ec2CapacityConfig;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Cross-service shared-ASG compatibility: groups EC2 services by their
|
|
35
|
+
* capacity key and rejects a reuse that is incompatible with the ASG's origin
|
|
36
|
+
* — config drift the shared ASG would silently discard, or a
|
|
37
|
+
* `persistentDataVolume` singleton being shared. Pure; called from BOTH
|
|
38
|
+
* validation layers — resources `validateEcsClusterProps` and patterns
|
|
39
|
+
* `validateEcsProps` (generator-standards § "Validate at the Lowest Layer the
|
|
40
|
+
* Field Belongs To") — with the same check at the construction site
|
|
41
|
+
* (`getOrCreateAsgCapacityProvider`) as defence-in-depth.
|
|
42
|
+
*/
|
|
43
|
+
export declare function validateSharedEc2CapacityConfig(services: readonly SharedEc2CapacityServiceInput[]): void;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { AmiHardwareType } from "aws-cdk-lib/aws-ecs";
|
|
2
|
+
import { Token } from "aws-cdk-lib";
|
|
3
|
+
import { DEFAULT_EC2_INSTANCE_TYPE, DEFAULT_EC2_MIN_CAPACITY, DEFAULT_EC2_MAX_CAPACITY, DEFAULT_EC2_INSTANCE_MONITORING, DEFAULT_WARM_POOL_MIN_SIZE, DEFAULT_WARM_POOL_REUSE_ON_SCALE_IN, inferAmiHardwareType } from "./ecsConstants.js";
|
|
4
|
+
/**
|
|
5
|
+
* Every `Ec2CapacityConfig` field, classified. The `Record<keyof …, …>`
|
|
6
|
+
* satisfies-shape is a compile-time exhaustiveness guard in compiled `src/`
|
|
7
|
+
* (typescript-standards § "Compile-time guards must live in compiled src/"):
|
|
8
|
+
* adding a field to `Ec2CapacityConfig` without classifying it here fails
|
|
9
|
+
* `tsc`, so a new ASG-shaping field cannot silently join the "not compared,
|
|
10
|
+
* silently discarded on a shared ASG" bucket.
|
|
11
|
+
*/
|
|
12
|
+
const EC2_CONFIG_FIELD_ROLES = {
|
|
13
|
+
instanceType: "keyed",
|
|
14
|
+
amiHardwareType: "keyed",
|
|
15
|
+
warmPool: "keyed",
|
|
16
|
+
persistentDataVolume: "keyed",
|
|
17
|
+
availabilityZones: "keyed",
|
|
18
|
+
minCapacity: "scalar",
|
|
19
|
+
maxCapacity: "scalar",
|
|
20
|
+
desiredCapacity: "scalar",
|
|
21
|
+
instanceMonitoring: "scalar",
|
|
22
|
+
associatePublicIpAddress: "scalar",
|
|
23
|
+
machineImage: "opaque",
|
|
24
|
+
userData: "opaque",
|
|
25
|
+
blockDevices: "opaque",
|
|
26
|
+
tags: "tags",
|
|
27
|
+
memoryLimitMiB: "taskDefinition"
|
|
28
|
+
};
|
|
29
|
+
function fieldsWithRole(role) {
|
|
30
|
+
return Object.keys(EC2_CONFIG_FIELD_ROLES).filter((field) => EC2_CONFIG_FIELD_ROLES[field] === role);
|
|
31
|
+
}
|
|
32
|
+
const KEYED_ASG_FIELDS = fieldsWithRole("keyed");
|
|
33
|
+
const SCALAR_ASG_FIELDS = fieldsWithRole("scalar");
|
|
34
|
+
const OPAQUE_ASG_FIELDS = fieldsWithRole("opaque");
|
|
35
|
+
/**
|
|
36
|
+
* Defaults resolved before a scalar field is compared — the SAME values the
|
|
37
|
+
* ASG construction applies (`getOrCreateAsgCapacityProvider`), imported from
|
|
38
|
+
* their `ecsConstants.ts` single source, so an explicit value equal to the
|
|
39
|
+
* default compares equal to an omitted one instead of tripping the drift
|
|
40
|
+
* throw on a pair that synthesises identically. Fields with no construction
|
|
41
|
+
* default (`desiredCapacity`, `associatePublicIpAddress`) compare raw.
|
|
42
|
+
*/
|
|
43
|
+
const SCALAR_ASG_FIELD_DEFAULTS = {
|
|
44
|
+
minCapacity: DEFAULT_EC2_MIN_CAPACITY,
|
|
45
|
+
maxCapacity: DEFAULT_EC2_MAX_CAPACITY,
|
|
46
|
+
instanceMonitoring: DEFAULT_EC2_INSTANCE_MONITORING
|
|
47
|
+
};
|
|
48
|
+
function resolvedScalar(config, field) {
|
|
49
|
+
return config[field] ?? SCALAR_ASG_FIELD_DEFAULTS[field];
|
|
50
|
+
}
|
|
51
|
+
/** Deep tag-record equality, treating `{}` and absent as the same ASG shape. */
|
|
52
|
+
function tagRecordsEqual(a, b) {
|
|
53
|
+
const aKeys = a === undefined ? [] : Object.keys(a);
|
|
54
|
+
const bKeys = b === undefined ? [] : Object.keys(b);
|
|
55
|
+
if (aKeys.length !== bKeys.length)
|
|
56
|
+
return false;
|
|
57
|
+
if (a === undefined || b === undefined)
|
|
58
|
+
return true;
|
|
59
|
+
return aKeys.every((key) => Object.is(a[key], b[key]));
|
|
60
|
+
}
|
|
61
|
+
function findDifferingAsgFields(ec2Config, origin) {
|
|
62
|
+
const differing = SCALAR_ASG_FIELDS.filter((field) => !Object.is(resolvedScalar(ec2Config, field), resolvedScalar(origin, field)));
|
|
63
|
+
if (!tagRecordsEqual(ec2Config.tags, origin.tags))
|
|
64
|
+
differing.push("tags");
|
|
65
|
+
for (const field of OPAQUE_ASG_FIELDS) {
|
|
66
|
+
if ((ec2Config[field] === undefined) !== (origin[field] === undefined)) {
|
|
67
|
+
differing.push(field);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return differing;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Rejects at synth when a service reusing a shared ASG is incompatible with
|
|
74
|
+
* it: either the origin's `persistentDataVolume` makes the ASG a singleton
|
|
75
|
+
* (two tasks would race to attach the same EBS volume), or the reusing
|
|
76
|
+
* service asks for ASG settings that differ from the ones the ASG was built
|
|
77
|
+
* with — those settings would be silently ignored (same silent-ignore class
|
|
78
|
+
* as `validateEc2ServiceSizing`).
|
|
79
|
+
*/
|
|
80
|
+
export function assertSharedAsgConfigMatches(serviceName, ec2Config, key, origin) {
|
|
81
|
+
// Cross-service only: the construction path legitimately re-resolves the
|
|
82
|
+
// SAME service's provider (e.g. a second task definition riding its ASG),
|
|
83
|
+
// and that re-entry must not trip the singleton throw.
|
|
84
|
+
if (origin.ec2Config.persistentDataVolume !== undefined &&
|
|
85
|
+
serviceName !== origin.serviceName) {
|
|
86
|
+
throw new Error(`Service '${serviceName}': its ec2Config resolves to the same EC2 capacity key ('${key}') ` +
|
|
87
|
+
`as service '${origin.serviceName}', whose persistentDataVolume pairs that auto scaling ` +
|
|
88
|
+
`group with a single EBS data volume. A persistent-data-volume service is a singleton — ` +
|
|
89
|
+
`two services sharing its ASG would race to attach the same volume. Change a keyed field ` +
|
|
90
|
+
`(${KEYED_ASG_FIELDS.map((f) => `'${f}'`).join(", ")}) so this service gets its own ASG.`);
|
|
91
|
+
}
|
|
92
|
+
const differing = findDifferingAsgFields(ec2Config, origin.ec2Config);
|
|
93
|
+
if (differing.length === 0)
|
|
94
|
+
return;
|
|
95
|
+
throw new Error(`Service '${serviceName}': ec2Config ${differing.map((f) => `'${f}'`).join(", ")} ` +
|
|
96
|
+
`${differing.length === 1 ? "is" : "are"} ignored — it shares an auto scaling group with ` +
|
|
97
|
+
`service '${origin.serviceName}' (both resolve to EC2 capacity key '${key}'), and that ASG ` +
|
|
98
|
+
`is built from the first service's ec2Config. Either give the two services the same value ` +
|
|
99
|
+
`for ${differing.map((f) => `'${f}'`).join(", ")}, or give this service its own ASG by ` +
|
|
100
|
+
`changing a keyed field (${KEYED_ASG_FIELDS.map((f) => `'${f}'`).join(", ")}).`);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Generates a unique key for EC2 config so services with matching
|
|
104
|
+
* configurations share an ASG.
|
|
105
|
+
*/
|
|
106
|
+
export function getEc2ConfigKey(ec2Config) {
|
|
107
|
+
const instanceType = ec2Config.instanceType ?? DEFAULT_EC2_INSTANCE_TYPE;
|
|
108
|
+
const amiHardwareType = ec2Config.amiHardwareType ??
|
|
109
|
+
(inferAmiHardwareType(instanceType) === AmiHardwareType.ARM
|
|
110
|
+
? "ARM"
|
|
111
|
+
: "STANDARD");
|
|
112
|
+
const warmPoolKey = ec2Config.warmPool
|
|
113
|
+
? `wp${ec2Config.warmPool.minSize ?? DEFAULT_WARM_POOL_MIN_SIZE}-${ec2Config.warmPool.reuseOnScaleIn ?? DEFAULT_WARM_POOL_REUSE_ON_SCALE_IN}`
|
|
114
|
+
: "nowp";
|
|
115
|
+
const baseKey = `${instanceType}-${amiHardwareType}-${warmPoolKey}`;
|
|
116
|
+
// PDV services are implicit singletons — two identical PDV configs still
|
|
117
|
+
// collide on this key, so sharing is rejected in assertSharedAsgConfigMatches.
|
|
118
|
+
const pdv = ec2Config.persistentDataVolume;
|
|
119
|
+
const extras = [];
|
|
120
|
+
if (pdv !== undefined) {
|
|
121
|
+
extras.push(`pdv-${pdv.deviceName}-${pdv.sizeGb}-${stableAzSegment(pdv.availabilityZone)}`);
|
|
122
|
+
}
|
|
123
|
+
// Serialise actual AZ names — `az${length}` would let services pinned to different AZs collide on `az1`.
|
|
124
|
+
// CDK Tokens (env-agnostic stacks) substitute a stable sentinel to keep logical IDs deterministic.
|
|
125
|
+
if (ec2Config.availabilityZones !== undefined) {
|
|
126
|
+
const azKey = [...ec2Config.availabilityZones]
|
|
127
|
+
.map(stableAzSegment)
|
|
128
|
+
.sort()
|
|
129
|
+
.join(",");
|
|
130
|
+
extras.push(`az-${azKey}`);
|
|
131
|
+
}
|
|
132
|
+
return extras.length === 0 ? baseKey : `${baseKey}-${extras.join("-")}`;
|
|
133
|
+
}
|
|
134
|
+
function stableAzSegment(az) {
|
|
135
|
+
return Token.isUnresolved(az) ? "synthAz" : az;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Cross-service shared-ASG compatibility: groups EC2 services by their
|
|
139
|
+
* capacity key and rejects a reuse that is incompatible with the ASG's origin
|
|
140
|
+
* — config drift the shared ASG would silently discard, or a
|
|
141
|
+
* `persistentDataVolume` singleton being shared. Pure; called from BOTH
|
|
142
|
+
* validation layers — resources `validateEcsClusterProps` and patterns
|
|
143
|
+
* `validateEcsProps` (generator-standards § "Validate at the Lowest Layer the
|
|
144
|
+
* Field Belongs To") — with the same check at the construction site
|
|
145
|
+
* (`getOrCreateAsgCapacityProvider`) as defence-in-depth.
|
|
146
|
+
*/
|
|
147
|
+
export function validateSharedEc2CapacityConfig(services) {
|
|
148
|
+
const origins = new Map();
|
|
149
|
+
for (const service of services) {
|
|
150
|
+
if (service.capacityProvider !== "EC2")
|
|
151
|
+
continue;
|
|
152
|
+
const ec2Config = service.ec2Config ?? {};
|
|
153
|
+
const key = getEc2ConfigKey(ec2Config);
|
|
154
|
+
const origin = origins.get(key);
|
|
155
|
+
if (origin === undefined) {
|
|
156
|
+
origins.set(key, { serviceName: service.name, ec2Config });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
assertSharedAsgConfigMatches(service.name, ec2Config, key, origin);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Monitoring } from "aws-cdk-lib/aws-autoscaling";
|
|
1
2
|
import { AmiHardwareType } from "aws-cdk-lib/aws-ecs";
|
|
2
3
|
import { RetentionDays } from "aws-cdk-lib/aws-logs";
|
|
3
4
|
export declare const DEFAULT_EC2_INSTANCE_TYPE = "t4g.micro";
|
|
@@ -8,6 +9,9 @@ export declare const DEFAULT_LOG_RETENTION = RetentionDays.TWO_WEEKS;
|
|
|
8
9
|
export declare const DEFAULT_FARGATE_CPU = 256;
|
|
9
10
|
export declare const DEFAULT_FARGATE_MEMORY_MIB = 512;
|
|
10
11
|
export declare const DEFAULT_EC2_CONTAINER_MEMORY_MIB = 1024;
|
|
12
|
+
export declare const DEFAULT_EC2_MIN_CAPACITY = 2;
|
|
13
|
+
export declare const DEFAULT_EC2_MAX_CAPACITY = 3;
|
|
14
|
+
export declare const DEFAULT_EC2_INSTANCE_MONITORING = Monitoring.BASIC;
|
|
11
15
|
/**
|
|
12
16
|
* The container hard memory limit for an EC2-capacity ECS service.
|
|
13
17
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Monitoring } from "aws-cdk-lib/aws-autoscaling";
|
|
1
2
|
import { AmiHardwareType } from "aws-cdk-lib/aws-ecs";
|
|
2
3
|
import { RetentionDays } from "aws-cdk-lib/aws-logs";
|
|
3
4
|
// Canonical source: @fjall/generator schemas/constants.ts — keep in sync
|
|
@@ -16,6 +17,14 @@ export const DEFAULT_LOG_RETENTION = RetentionDays.TWO_WEEKS;
|
|
|
16
17
|
export const DEFAULT_FARGATE_CPU = 256;
|
|
17
18
|
export const DEFAULT_FARGATE_MEMORY_MIB = 512;
|
|
18
19
|
export const DEFAULT_EC2_CONTAINER_MEMORY_MIB = 1024;
|
|
20
|
+
// Read at two sites that must agree (code-quality § coupled values): the ASG
|
|
21
|
+
// construction (`getOrCreateAsgCapacityProvider`) and the shared-ASG drift
|
|
22
|
+
// compare (`ecsCapacityConfig.ts` SCALAR_ASG_FIELD_DEFAULTS) — the compare
|
|
23
|
+
// resolves the same defaults the construction applies, or an explicit value
|
|
24
|
+
// equal to the default would read as drift.
|
|
25
|
+
export const DEFAULT_EC2_MIN_CAPACITY = 2;
|
|
26
|
+
export const DEFAULT_EC2_MAX_CAPACITY = 3;
|
|
27
|
+
export const DEFAULT_EC2_INSTANCE_MONITORING = Monitoring.BASIC;
|
|
19
28
|
/**
|
|
20
29
|
* The container hard memory limit for an EC2-capacity ECS service.
|
|
21
30
|
*
|
|
@@ -49,7 +49,7 @@ export class EcsLifecycleHookMigration extends Construct {
|
|
|
49
49
|
: "DISABLED"
|
|
50
50
|
};
|
|
51
51
|
this.lambda = new LambdaFunction(this, `${id}Fn`, {
|
|
52
|
-
runtime: Runtime.
|
|
52
|
+
runtime: Runtime.NODEJS_24_X,
|
|
53
53
|
handler: "index.handler",
|
|
54
54
|
code: Code.fromInline(source),
|
|
55
55
|
lambdaDescription: `${id} ECS deployment lifecycle hook for migration`,
|
|
@@ -3,7 +3,8 @@ import type { FargateTaskDefinition, Ec2TaskDefinition } from "aws-cdk-lib/aws-e
|
|
|
3
3
|
import { type ISecurityGroup } from "aws-cdk-lib/aws-ec2";
|
|
4
4
|
import { TargetTrackingScalingPolicy } from "aws-cdk-lib/aws-applicationautoscaling";
|
|
5
5
|
import { type AutoScalingGroup } from "aws-cdk-lib/aws-autoscaling";
|
|
6
|
-
import { type
|
|
6
|
+
import { type Ec2AsgOrigin } from "./ecsCapacityConfig.js";
|
|
7
|
+
import { type EcsServiceProps, type QueueScalingResources } from "./ecsTypes.js";
|
|
7
8
|
import type { EcsConstructContext } from "./ecsContext.js";
|
|
8
9
|
/**
|
|
9
10
|
* Resolves the user's `circuitBreaker` config to the CDK
|
|
@@ -22,18 +23,11 @@ export interface AsgCapacityState {
|
|
|
22
23
|
* service sharing the key can be checked against it
|
|
23
24
|
* ({@link assertSharedAsgConfigMatches}).
|
|
24
25
|
*/
|
|
25
|
-
asgOrigins: Map<string,
|
|
26
|
-
serviceName: string;
|
|
27
|
-
ec2Config: Ec2CapacityConfig;
|
|
28
|
-
}>;
|
|
26
|
+
asgOrigins: Map<string, Ec2AsgOrigin>;
|
|
29
27
|
autoScalingGroup?: AutoScalingGroup;
|
|
30
28
|
asgSecurityGroup?: ISecurityGroup;
|
|
31
29
|
}
|
|
32
|
-
|
|
33
|
-
* Generates a unique key for EC2 config so services with matching
|
|
34
|
-
* configurations share an ASG.
|
|
35
|
-
*/
|
|
36
|
-
export declare function getEc2ConfigKey(ec2Config: Ec2CapacityConfig): string;
|
|
30
|
+
export { getEc2ConfigKey, validateSharedEc2CapacityConfig, type Ec2AsgOrigin, type SharedEc2CapacityServiceInput } from "./ecsCapacityConfig.js";
|
|
37
31
|
/**
|
|
38
32
|
* Gets or creates an ASG capacity provider for an EC2-backed service.
|
|
39
33
|
* Services with matching EC2 configs share the same ASG.
|
|
@@ -2,16 +2,16 @@ import { FargateService, Ec2Service, PropagatedTagSource, PlacementStrategy, Asg
|
|
|
2
2
|
import { Peer, Port, SubnetType, UserData } from "aws-cdk-lib/aws-ec2";
|
|
3
3
|
import { ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
|
4
4
|
import { Role } from "../iam/role.js";
|
|
5
|
-
import { CfnOutput, Duration
|
|
5
|
+
import { CfnOutput, Duration } from "aws-cdk-lib";
|
|
6
6
|
import { AdjustmentType, PredefinedMetric, ScalableTarget, ServiceNamespace, StepScalingAction, TargetTrackingScalingPolicy } from "aws-cdk-lib/aws-applicationautoscaling";
|
|
7
7
|
import { Alarm, ComparisonOperator, MathExpression } from "aws-cdk-lib/aws-cloudwatch";
|
|
8
8
|
import { ApplicationScalingAction } from "aws-cdk-lib/aws-cloudwatch-actions";
|
|
9
|
-
import { Monitoring } from "aws-cdk-lib/aws-autoscaling";
|
|
10
9
|
import { SecurityGroup } from "../networking/securityGroup.js";
|
|
11
10
|
import { Ec2Instance } from "./ec2.js";
|
|
12
11
|
import { vpcHasNatGateways } from "../../../utils/vpcUtils.js";
|
|
13
12
|
import { toPascalCase } from "../../../utils/capitaliseString.js";
|
|
14
|
-
import { DEFAULT_EC2_INSTANCE_TYPE, DEFAULT_WARM_POOL_MIN_SIZE, DEFAULT_WARM_POOL_REUSE_ON_SCALE_IN, DEFAULT_HEALTH_CHECK_GRACE_SECONDS, DEFAULT_MIN_HEALTHY_PERCENT, DEFAULT_MAX_HEALTHY_PERCENT, DEFAULT_DESIRED_COUNT, inferAmiHardwareType } from "./ecsConstants.js";
|
|
13
|
+
import { DEFAULT_EC2_INSTANCE_TYPE, DEFAULT_EC2_MIN_CAPACITY, DEFAULT_EC2_MAX_CAPACITY, DEFAULT_EC2_INSTANCE_MONITORING, DEFAULT_WARM_POOL_MIN_SIZE, DEFAULT_WARM_POOL_REUSE_ON_SCALE_IN, DEFAULT_HEALTH_CHECK_GRACE_SECONDS, DEFAULT_MIN_HEALTHY_PERCENT, DEFAULT_MAX_HEALTHY_PERCENT, DEFAULT_DESIRED_COUNT, inferAmiHardwareType } from "./ecsConstants.js";
|
|
14
|
+
import { assertSharedAsgConfigMatches, getEc2ConfigKey } from "./ecsCapacityConfig.js";
|
|
15
15
|
import { ScalingType } from "./ecsTypes.js";
|
|
16
16
|
import { isServiceFargate, isServiceEc2 } from "./ecsTaskDefinition.js";
|
|
17
17
|
/**
|
|
@@ -26,101 +26,7 @@ export function resolveCircuitBreaker(config) {
|
|
|
26
26
|
const rollback = config?.rollback ?? true;
|
|
27
27
|
return { enable: true, rollback };
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
* `ec2Config` fields that shape the ASG but are NOT part of
|
|
31
|
-
* {@link getEc2ConfigKey}, split by how a difference can be detected without
|
|
32
|
-
* false positives. A second service whose key matches reuses the first
|
|
33
|
-
* service's ASG, so any of these it sets is silently discarded.
|
|
34
|
-
*
|
|
35
|
-
* Scalars compare by value. `tags` is plain data, so it compares deeply.
|
|
36
|
-
* `machineImage` / `userData` / `blockDevices` are CDK objects with no stable
|
|
37
|
-
* structural identity — two equivalent literals are different references, so
|
|
38
|
-
* only a defined-vs-undefined asymmetry is safe to report. They deliberately
|
|
39
|
-
* under-report: over-reporting is a hard synth failure on config that
|
|
40
|
-
* previously worked, which is the more damaging direction.
|
|
41
|
-
*/
|
|
42
|
-
const SCALAR_ASG_FIELDS = [
|
|
43
|
-
"minCapacity",
|
|
44
|
-
"maxCapacity",
|
|
45
|
-
"desiredCapacity",
|
|
46
|
-
"instanceMonitoring",
|
|
47
|
-
"associatePublicIpAddress"
|
|
48
|
-
];
|
|
49
|
-
const OPAQUE_ASG_FIELDS = [
|
|
50
|
-
"machineImage",
|
|
51
|
-
"userData",
|
|
52
|
-
"blockDevices"
|
|
53
|
-
];
|
|
54
|
-
function tagRecordsEqual(a, b) {
|
|
55
|
-
if (a === undefined || b === undefined)
|
|
56
|
-
return a === b;
|
|
57
|
-
const aKeys = Object.keys(a);
|
|
58
|
-
if (aKeys.length !== Object.keys(b).length)
|
|
59
|
-
return false;
|
|
60
|
-
return aKeys.every((key) => Object.is(a[key], b[key]));
|
|
61
|
-
}
|
|
62
|
-
function findDifferingAsgFields(ec2Config, origin) {
|
|
63
|
-
const differing = SCALAR_ASG_FIELDS.filter((field) => !Object.is(ec2Config[field], origin[field]));
|
|
64
|
-
if (!tagRecordsEqual(ec2Config.tags, origin.tags))
|
|
65
|
-
differing.push("tags");
|
|
66
|
-
for (const field of OPAQUE_ASG_FIELDS) {
|
|
67
|
-
if ((ec2Config[field] === undefined) !== (origin[field] === undefined)) {
|
|
68
|
-
differing.push(field);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return differing;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Rejects at synth when a service reusing a shared ASG asks for ASG settings
|
|
75
|
-
* that differ from the ones the ASG was built with — those settings would be
|
|
76
|
-
* silently ignored (same silent-ignore class as `validateEc2ServiceSizing`).
|
|
77
|
-
*/
|
|
78
|
-
function assertSharedAsgConfigMatches(serviceName, ec2Config, key, origin) {
|
|
79
|
-
const differing = findDifferingAsgFields(ec2Config, origin.ec2Config);
|
|
80
|
-
if (differing.length === 0)
|
|
81
|
-
return;
|
|
82
|
-
throw new Error(`Service '${serviceName}': ec2Config ${differing.map((f) => `'${f}'`).join(", ")} ` +
|
|
83
|
-
`${differing.length === 1 ? "is" : "are"} ignored — it shares an auto scaling group with ` +
|
|
84
|
-
`service '${origin.serviceName}' (both resolve to EC2 capacity key '${key}'), and that ASG ` +
|
|
85
|
-
`is built from the first service's ec2Config. Either give the two services the same value ` +
|
|
86
|
-
`for ${differing.map((f) => `'${f}'`).join(", ")}, or give this service its own ASG by ` +
|
|
87
|
-
`changing a keyed field ('instanceType', 'amiHardwareType', 'warmPool', ` +
|
|
88
|
-
`'persistentDataVolume', 'availabilityZones').`);
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Generates a unique key for EC2 config so services with matching
|
|
92
|
-
* configurations share an ASG.
|
|
93
|
-
*/
|
|
94
|
-
export function getEc2ConfigKey(ec2Config) {
|
|
95
|
-
const instanceType = ec2Config.instanceType ?? DEFAULT_EC2_INSTANCE_TYPE;
|
|
96
|
-
const amiHardwareType = ec2Config.amiHardwareType ??
|
|
97
|
-
(inferAmiHardwareType(instanceType) === AmiHardwareType.ARM
|
|
98
|
-
? "ARM"
|
|
99
|
-
: "STANDARD");
|
|
100
|
-
const warmPoolKey = ec2Config.warmPool
|
|
101
|
-
? `wp${ec2Config.warmPool.minSize ?? DEFAULT_WARM_POOL_MIN_SIZE}-${ec2Config.warmPool.reuseOnScaleIn ?? DEFAULT_WARM_POOL_REUSE_ON_SCALE_IN}`
|
|
102
|
-
: "nowp";
|
|
103
|
-
const baseKey = `${instanceType}-${amiHardwareType}-${warmPoolKey}`;
|
|
104
|
-
// PDV services are implicit singletons — sharing an ASG would let two tasks race for the same EBS volume.
|
|
105
|
-
const pdv = ec2Config.persistentDataVolume;
|
|
106
|
-
const extras = [];
|
|
107
|
-
if (pdv !== undefined) {
|
|
108
|
-
extras.push(`pdv-${pdv.deviceName}-${pdv.sizeGb}-${stableAzSegment(pdv.availabilityZone)}`);
|
|
109
|
-
}
|
|
110
|
-
// Serialise actual AZ names — `az${length}` would let services pinned to different AZs collide on `az1`.
|
|
111
|
-
// CDK Tokens (env-agnostic stacks) substitute a stable sentinel to keep logical IDs deterministic.
|
|
112
|
-
if (ec2Config.availabilityZones !== undefined) {
|
|
113
|
-
const azKey = [...ec2Config.availabilityZones]
|
|
114
|
-
.map(stableAzSegment)
|
|
115
|
-
.sort()
|
|
116
|
-
.join(",");
|
|
117
|
-
extras.push(`az-${azKey}`);
|
|
118
|
-
}
|
|
119
|
-
return extras.length === 0 ? baseKey : `${baseKey}-${extras.join("-")}`;
|
|
120
|
-
}
|
|
121
|
-
function stableAzSegment(az) {
|
|
122
|
-
return Token.isUnresolved(az) ? "synthAz" : az;
|
|
123
|
-
}
|
|
29
|
+
export { getEc2ConfigKey, validateSharedEc2CapacityConfig } from "./ecsCapacityConfig.js";
|
|
124
30
|
/**
|
|
125
31
|
* Gets or creates an ASG capacity provider for an EC2-backed service.
|
|
126
32
|
* Services with matching EC2 configs share the same ASG.
|
|
@@ -146,8 +52,8 @@ export function getOrCreateAsgCapacityProvider(ctx, serviceProps, state) {
|
|
|
146
52
|
? AmiHardwareType.STANDARD
|
|
147
53
|
: AmiHardwareType.ARM
|
|
148
54
|
: inferAmiHardwareType(instanceType);
|
|
149
|
-
const minCapacity = ec2Config.minCapacity ??
|
|
150
|
-
const maxCapacity = ec2Config.maxCapacity ??
|
|
55
|
+
const minCapacity = ec2Config.minCapacity ?? DEFAULT_EC2_MIN_CAPACITY;
|
|
56
|
+
const maxCapacity = ec2Config.maxCapacity ?? DEFAULT_EC2_MAX_CAPACITY;
|
|
151
57
|
const asgSecurityGroup = ctx.props.cluster?.securityGroup ??
|
|
152
58
|
new SecurityGroup(ctx.scope, `${safeKey}AsgSecurityGroup`, {
|
|
153
59
|
vpc: ctx.cluster.vpc,
|
|
@@ -195,7 +101,7 @@ export function getOrCreateAsgCapacityProvider(ctx, serviceProps, state) {
|
|
|
195
101
|
EcsOptimizedImage.amazonLinux2023(amiHardwareType),
|
|
196
102
|
userData: ec2Config.userData ?? UserData.forLinux(),
|
|
197
103
|
role: instanceRole,
|
|
198
|
-
instanceMonitoring: ec2Config.instanceMonitoring ??
|
|
104
|
+
instanceMonitoring: ec2Config.instanceMonitoring ?? DEFAULT_EC2_INSTANCE_MONITORING,
|
|
199
105
|
capacityRebalance: true,
|
|
200
106
|
ecsClusterArn: ctx.cluster.clusterArn,
|
|
201
107
|
...(ec2Config.desiredCapacity !== undefined && {
|
|
@@ -3,6 +3,7 @@ import { evaluateBakeGuard } from "@fjall/util/docker";
|
|
|
3
3
|
import { toKebab, SSM_COMPONENT_PATTERN, SSM_COMPONENT_ERROR, SECRET_NAME_PATTERN, SECRET_NAME_ERROR } from "@fjall/util";
|
|
4
4
|
import { ScalingType } from "./ecsTypes.js";
|
|
5
5
|
import { DEFAULT_EC2_CONTAINER_MEMORY_MIB } from "./ecsConstants.js";
|
|
6
|
+
import { validateSharedEc2CapacityConfig } from "./ecsCapacityConfig.js";
|
|
6
7
|
/**
|
|
7
8
|
* Validates ECS cluster props before construction.
|
|
8
9
|
* Pure function — does not depend on class state.
|
|
@@ -196,6 +197,11 @@ export function validateEcsClusterProps(props) {
|
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
199
|
}
|
|
200
|
+
// Cross-service, so it runs after the per-service loop: EC2 services whose
|
|
201
|
+
// ec2Config resolves to the same capacity key share one ASG — reject config
|
|
202
|
+
// drift the shared ASG would silently discard, and any sharing of a
|
|
203
|
+
// persistentDataVolume (singleton) ASG.
|
|
204
|
+
validateSharedEc2CapacityConfig(props.services);
|
|
199
205
|
}
|
|
200
206
|
/**
|
|
201
207
|
* Rejects service-level task sizing that is silently ignored for EC2 capacity.
|
|
@@ -157,7 +157,7 @@ export class PersistentDataVolume extends Construct {
|
|
|
157
157
|
resources: [asgArnWildcard]
|
|
158
158
|
});
|
|
159
159
|
this.lambda = new LambdaFunction(this, `${id}Fn`, {
|
|
160
|
-
runtime: Runtime.
|
|
160
|
+
runtime: Runtime.NODEJS_24_X,
|
|
161
161
|
handler: "index.handler",
|
|
162
162
|
code: Code.fromInline(source),
|
|
163
163
|
lambdaDescription: `${id} ${PERSISTENT_DATA_VOLUME_LAUNCHING_DESCRIPTION}`,
|
|
@@ -252,7 +252,7 @@ export class RdsInstance extends Construct {
|
|
|
252
252
|
const instanceId = ResourceNaming.dbInstanceId(this.constructId);
|
|
253
253
|
const waiterTimeout = Duration.minutes(10);
|
|
254
254
|
const deletionWaiter = new CustomResource(this, `${this.databaseNameValue}DeletionWaiter`, {
|
|
255
|
-
runtime: Runtime.
|
|
255
|
+
runtime: Runtime.NODEJS_24_X,
|
|
256
256
|
timeout: waiterTimeout.plus(Duration.seconds(30)),
|
|
257
257
|
lambdaDescription: `${this.databaseNameValue} deletion waiter`,
|
|
258
258
|
inlineCode: `
|
|
@@ -33,7 +33,7 @@ export class IdentityStoreUserProvider extends Construct {
|
|
|
33
33
|
const identityStoreArn = `arn:${stack.partition}:identitystore::${stack.account}:identitystore/${props.identityStoreId}`;
|
|
34
34
|
const userArnPattern = identityStoreUserArnPattern(stack.partition);
|
|
35
35
|
const handler = new SingletonFunction(this, "Handler", {
|
|
36
|
-
runtime: Runtime.
|
|
36
|
+
runtime: Runtime.NODEJS_24_X,
|
|
37
37
|
code: Code.fromAsset(LAMBDA_ASSET_DIR),
|
|
38
38
|
handler: "index.handler",
|
|
39
39
|
timeout: HANDLER_TIMEOUT_SECONDS,
|
|
@@ -21,7 +21,7 @@ export class CrossAccountReturnRoutes extends Construct {
|
|
|
21
21
|
constructor(scope, id, props) {
|
|
22
22
|
super(scope, id);
|
|
23
23
|
this.customResource = new CustomResource(this, "Handler", {
|
|
24
|
-
runtime: Runtime.
|
|
24
|
+
runtime: Runtime.NODEJS_24_X,
|
|
25
25
|
timeout: Duration.minutes(5),
|
|
26
26
|
lambdaDescription: `${id} cross-account return-route manager`,
|
|
27
27
|
roleDescription: `${id} return-route Lambda execution role`,
|
|
@@ -166,7 +166,7 @@ export class IpamPool extends Construct {
|
|
|
166
166
|
// On stack deletion, VPC allocations (not managed by CloudFormation)
|
|
167
167
|
// block CIDR deprovisioning. This custom resource releases them first.
|
|
168
168
|
const allocationCleanup = new CustomResource(this, `${constructKey}IpamCleanup${regionSuffix}`, {
|
|
169
|
-
runtime: Runtime.
|
|
169
|
+
runtime: Runtime.NODEJS_24_X,
|
|
170
170
|
timeout: Duration.minutes(5),
|
|
171
171
|
lambdaDescription: `${account} IPAM pool allocation cleanup - ${region}`,
|
|
172
172
|
properties: {
|
|
@@ -36,7 +36,7 @@ export class CostAllocationTagActivator extends Construct {
|
|
|
36
36
|
this.lambda = new LambdaFunction(this, "TagActivatorFunction", {
|
|
37
37
|
// Referenced cross-environment by the App messaging stack's schedule rule.
|
|
38
38
|
functionName: PhysicalName.GENERATE_IF_NEEDED,
|
|
39
|
-
runtime: Runtime.
|
|
39
|
+
runtime: Runtime.NODEJS_24_X,
|
|
40
40
|
handler: "index.handler",
|
|
41
41
|
code: Code.fromInline(HANDLER_CODE),
|
|
42
42
|
lambdaDescription: "Auto-discovers and activates cost allocation tags in Cost Explorer",
|
|
@@ -66,7 +66,7 @@ export class TlsCertGenerator extends Construct {
|
|
|
66
66
|
resources: [caCmk.key.keyArn, serverCmk.key.keyArn]
|
|
67
67
|
});
|
|
68
68
|
const cr = new CustomResource(this, "Generator", {
|
|
69
|
-
runtime: Runtime.
|
|
69
|
+
runtime: Runtime.NODEJS_24_X,
|
|
70
70
|
timeout: LAMBDA_TIMEOUT,
|
|
71
71
|
codePath: LAMBDA_ASSET_DIR,
|
|
72
72
|
handler: "index.handler",
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { type EngineCompat } from "@fjall/util/manifest/schemas";
|
|
2
2
|
/**
|
|
3
3
|
* Normalise an npm dependency RANGE to its minimum X.Y.Z floor: strip a leading
|
|
4
|
-
* range operator (`^`, `~`, `>=`,
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* inclusive range operator (`^`, `~`, `>=`, `=`, `v`), then reconstruct
|
|
5
|
+
* `${major}.${minor}.${patch}` (missing minor/patch default to 0).
|
|
6
|
+
*
|
|
7
|
+
* Deliberately conservative — returns undefined (→ the caller omits the field,
|
|
8
|
+
* FM1 degrades to no-op) for every shape whose true floor the first-token strip
|
|
9
|
+
* would MIS-state rather than merely fail to state:
|
|
10
|
+
* - unions (`^3 || ^2` floors at 2.0.0, not the first branch's 3.0.0)
|
|
11
|
+
* - strict lower bounds (`>2.1134.0` excludes its own literal)
|
|
12
|
+
* - prerelease bounds (`^2.0.0-rc.1` floors below the stripped 2.0.0)
|
|
13
|
+
* - non-decimal segments (`1e2`, hex, `2.x`, `*`, git URLs, 4+ segments)
|
|
14
|
+
* A wrong floor is worse than no floor: over-strict refuses a valid engine,
|
|
15
|
+
* under-strict admits an incompatible one — both silently.
|
|
9
16
|
*/
|
|
10
17
|
export declare function rangeFloor(range: string | undefined): string | undefined;
|
|
11
18
|
/**
|
|
@@ -56,29 +56,39 @@ import { FjallLogger } from "./validationLogger.js";
|
|
|
56
56
|
const PACKAGE_NAME = "@fjall/components-infrastructure";
|
|
57
57
|
/**
|
|
58
58
|
* Normalise an npm dependency RANGE to its minimum X.Y.Z floor: strip a leading
|
|
59
|
-
* range operator (`^`, `~`, `>=`,
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
59
|
+
* inclusive range operator (`^`, `~`, `>=`, `=`, `v`), then reconstruct
|
|
60
|
+
* `${major}.${minor}.${patch}` (missing minor/patch default to 0).
|
|
61
|
+
*
|
|
62
|
+
* Deliberately conservative — returns undefined (→ the caller omits the field,
|
|
63
|
+
* FM1 degrades to no-op) for every shape whose true floor the first-token strip
|
|
64
|
+
* would MIS-state rather than merely fail to state:
|
|
65
|
+
* - unions (`^3 || ^2` floors at 2.0.0, not the first branch's 3.0.0)
|
|
66
|
+
* - strict lower bounds (`>2.1134.0` excludes its own literal)
|
|
67
|
+
* - prerelease bounds (`^2.0.0-rc.1` floors below the stripped 2.0.0)
|
|
68
|
+
* - non-decimal segments (`1e2`, hex, `2.x`, `*`, git URLs, 4+ segments)
|
|
69
|
+
* A wrong floor is worse than no floor: over-strict refuses a valid engine,
|
|
70
|
+
* under-strict admits an incompatible one — both silently.
|
|
64
71
|
*/
|
|
65
72
|
export function rangeFloor(range) {
|
|
66
73
|
if (range === undefined || range.length === 0)
|
|
67
74
|
return undefined;
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
const trimmed = range.trim();
|
|
76
|
+
if (trimmed.includes("||"))
|
|
77
|
+
return undefined;
|
|
78
|
+
const firstToken = trimmed.split(/\s+/)[0] ?? "";
|
|
79
|
+
if (/^>(?!=)/.test(firstToken))
|
|
80
|
+
return undefined;
|
|
81
|
+
const core = firstToken.replace(/^[\^~>=v ]+/i, "");
|
|
82
|
+
if (/[-+]/.test(core))
|
|
71
83
|
return undefined;
|
|
72
84
|
const parts = core.split(".");
|
|
85
|
+
if (parts.length === 0 || parts.length > 3)
|
|
86
|
+
return undefined;
|
|
87
|
+
if (!parts.every((segment) => /^\d+$/.test(segment)))
|
|
88
|
+
return undefined;
|
|
73
89
|
const major = Number(parts[0]);
|
|
74
90
|
const minor = parts.length > 1 ? Number(parts[1]) : 0;
|
|
75
91
|
const patch = parts.length > 2 ? Number(parts[2]) : 0;
|
|
76
|
-
if (!Number.isInteger(major) || major < 0)
|
|
77
|
-
return undefined;
|
|
78
|
-
if (!Number.isInteger(minor) || minor < 0)
|
|
79
|
-
return undefined;
|
|
80
|
-
if (!Number.isInteger(patch) || patch < 0)
|
|
81
|
-
return undefined;
|
|
82
92
|
return `${major}.${minor}.${patch}`;
|
|
83
93
|
}
|
|
84
94
|
/**
|
|
@@ -100,12 +110,20 @@ function resolveConstructsPackageMeta() {
|
|
|
100
110
|
typeof pkg.version === "string" &&
|
|
101
111
|
pkg.version.length > 0) {
|
|
102
112
|
const awsCdkPeer = pkg.peerDependencies?.["aws-cdk"];
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
113
|
+
const awsCdkCliFloor = typeof awsCdkPeer === "string" ? rangeFloor(awsCdkPeer) : undefined;
|
|
114
|
+
if (awsCdkCliFloor === undefined) {
|
|
115
|
+
// The FM1 omission is fail-safe but must never be silent — an
|
|
116
|
+
// assembly without minimumAwsCdkCli skips the deploy engine's
|
|
117
|
+
// CLI-floor check entirely.
|
|
118
|
+
FjallLogger.warn(typeof awsCdkPeer === "string"
|
|
119
|
+
? `engineCompat: aws-cdk peer range "${awsCdkPeer}" has no ` +
|
|
120
|
+
`derivable minimum floor — minimumAwsCdkCli will be ` +
|
|
121
|
+
`omitted and the deploy engine's CLI-floor check skipped`
|
|
122
|
+
: "engineCompat: no aws-cdk peerDependency found — " +
|
|
123
|
+
"minimumAwsCdkCli will be omitted and the deploy " +
|
|
124
|
+
"engine's CLI-floor check skipped");
|
|
125
|
+
}
|
|
126
|
+
return { version: pkg.version, awsCdkCliFloor };
|
|
109
127
|
}
|
|
110
128
|
}
|
|
111
129
|
if (dir === root)
|
|
@@ -150,9 +168,10 @@ export const AWS_CDK_CLI_FLOOR = CONSTRUCTS_PACKAGE_META?.awsCdkCliFloor;
|
|
|
150
168
|
export function buildEngineCompatFrom(version, awsCdkCliFloor) {
|
|
151
169
|
if (version === undefined || version.length === 0)
|
|
152
170
|
return undefined;
|
|
153
|
-
const
|
|
154
|
-
if (
|
|
171
|
+
const majorToken = version.split(".")[0] ?? "";
|
|
172
|
+
if (!/^\d+$/.test(majorToken))
|
|
155
173
|
return undefined;
|
|
174
|
+
const major = Number(majorToken);
|
|
156
175
|
return {
|
|
157
176
|
synthesisedBy: version,
|
|
158
177
|
minimumEngineVersion: `${major}.0.0`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/fjall-tech/fjall.git",
|
|
7
|
+
"directory": "components/infrastructure"
|
|
8
|
+
},
|
|
4
9
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
10
|
"type": "module",
|
|
6
11
|
"bin": {
|
|
@@ -69,8 +74,8 @@
|
|
|
69
74
|
},
|
|
70
75
|
"dependencies": {
|
|
71
76
|
"@aws-sdk/client-organizations": "^3.1098.0",
|
|
72
|
-
"@fjall/generator": "^
|
|
73
|
-
"@fjall/util": "^
|
|
77
|
+
"@fjall/generator": "^6.0.0",
|
|
78
|
+
"@fjall/util": "^6.0.0",
|
|
74
79
|
"constructs": "^10.7.2"
|
|
75
80
|
},
|
|
76
81
|
"overrides": {
|
|
@@ -78,11 +83,11 @@
|
|
|
78
83
|
},
|
|
79
84
|
"peerDependencies": {
|
|
80
85
|
"aws-cdk": "^2.1134.0",
|
|
81
|
-
"aws-cdk-lib": "^2.
|
|
86
|
+
"aws-cdk-lib": "^2.263.0",
|
|
82
87
|
"constructs": "^10.7.2"
|
|
83
88
|
},
|
|
84
89
|
"engines": {
|
|
85
90
|
"node": ">=18.0.0"
|
|
86
91
|
},
|
|
87
|
-
"gitHead": "
|
|
92
|
+
"gitHead": "7620fc7c93bbef1046580d6568546afe3cd450a7"
|
|
88
93
|
}
|