@fjall/components-infrastructure 2.22.0 → 2.23.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.
@@ -1,5 +1,6 @@
1
1
  import { Token } from "aws-cdk-lib";
2
2
  import { Runtime } from "aws-cdk-lib/aws-lambda";
3
+ import { buildParameterPath } from "@fjall/util";
3
4
  import { isCompute, isEcsCompute, isLambdaCompute, isEc2Compute } from "./interfaces/compute.js";
4
5
  import { warnIfPropertiesIgnored } from "../../utils/validationLogger.js";
5
6
  import { applyInferredPublicBuildArgs } from "./computeBuildArgInference.js";
@@ -205,7 +206,11 @@ export class ComputeFactory {
205
206
  // Include ssmSecretsPath (explicit or derived)
206
207
  manifestService.ssmSecretsPath =
207
208
  service.ssmSecretsPath ??
208
- `/${appName}/${clusterName}/${service.name}`;
209
+ buildParameterPath({
210
+ app: appName,
211
+ cluster: clusterName,
212
+ service: service.name
213
+ });
209
214
  collector.addService(manifestService);
210
215
  }
211
216
  return new EcsCompute(scope, id, computeProps);
@@ -223,7 +228,10 @@ export class ComputeFactory {
223
228
  const manifestLambda = {
224
229
  name: id,
225
230
  ssmSecretsPath: lambdaComputeProps.ssmSecretsPath ||
226
- `/${lambdaComputeProps.appName}/lambda/${id}`
231
+ buildParameterPath({
232
+ app: lambdaComputeProps.appName,
233
+ lambda: id
234
+ })
227
235
  };
228
236
  if (lambdaComputeProps.secrets &&
229
237
  lambdaComputeProps.secrets.length > 0) {
@@ -23,7 +23,8 @@ import { toPascalCase } from "../../utils/capitaliseString.js";
23
23
  import { FjallLogger } from "../../utils/validationLogger.js";
24
24
  import { VALIDATION_PATTERNS } from "@fjall/generator";
25
25
  import { evaluateBakeGuard } from "@fjall/util/docker";
26
- import { toKebab } from "@fjall/util";
26
+ import { toKebab, buildParameterPath } from "@fjall/util";
27
+ import { validateSecretName } from "../../resources/aws/compute/ecsValidation.js";
27
28
  import { COMPUTE_DEFAULTS, collectImportedSecretNames } from "./compute.js";
28
29
  import { isHookMigrations } from "./computeEcsTypes.js";
29
30
  export { ScalingType } from "./computeEcsTypes.js";
@@ -1228,9 +1229,14 @@ export class EcsCompute extends Construct {
1228
1229
  `is set and EcsComputeProps.appName is missing — set one to enable ` +
1229
1230
  `SSM path derivation (/<appName>/<clusterName>/<serviceName>).`);
1230
1231
  }
1231
- ssmPath = `/${this.appName}/${this.clusterId}/${svcConfig.name}`;
1232
+ ssmPath = buildParameterPath({
1233
+ app: this.appName,
1234
+ cluster: this.clusterId,
1235
+ service: svcConfig.name
1236
+ });
1232
1237
  }
1233
1238
  for (const secretName of migrations.secrets) {
1239
+ validateSecretName(secretName, `migrations of service '${svcConfig.name}'`);
1234
1240
  const param = StringParameter.fromSecureStringParameterAttributes(this, `${idPrefix}${secretName}SsmParam`, { parameterName: `${ssmPath}/${secretName}` });
1235
1241
  secretsResolved[secretName] = EcsSecret.fromSsmParameter(param);
1236
1242
  }
@@ -2,8 +2,9 @@ import { AwsLogDriver, ContainerDependencyCondition, FargateTaskDefinition, Ec2T
2
2
  import { Duration, RemovalPolicy } from "aws-cdk-lib";
3
3
  import { Secret as EcsSecret } from "aws-cdk-lib/aws-ecs";
4
4
  import { StringParameter } from "aws-cdk-lib/aws-ssm";
5
+ import { buildParameterPath } from "@fjall/util";
5
6
  import { resolveOrgId } from "../../../utils/cdkContext.js";
6
- import { validateSsmPathComponent } from "./ecsValidation.js";
7
+ import { validateSsmPathComponent, validateSecretName } from "./ecsValidation.js";
7
8
  import { DEFAULT_LOG_RETENTION, DEFAULT_FARGATE_CPU, DEFAULT_FARGATE_MEMORY_MIB, DEFAULT_EC2_CONTAINER_MEMORY_MIB } from "./ecsConstants.js";
8
9
  import { LogGroup } from "../logging/logGroup.js";
9
10
  import { getContainerImage } from "./ecsImages.js";
@@ -46,7 +47,21 @@ export function deriveSsmSecretsPath(props, serviceName, explicitPath) {
46
47
  validateSsmPathComponent(appName, "appName");
47
48
  validateSsmPathComponent(props.clusterName, "clusterName");
48
49
  validateSsmPathComponent(serviceName, "serviceName");
49
- return `/${appName}/${props.clusterName}/${serviceName}`;
50
+ // "lambda" is the lambda-scope path marker: /<app>/lambda/<x> decodes as a
51
+ // lambda namespace everywhere (SecretNamespaceSchema reserves the name), so
52
+ // a cluster called "lambda" would derive paths the secrets tooling cannot
53
+ // address as ECS scopes.
54
+ if (props.clusterName === "lambda") {
55
+ throw new Error(`Cluster name "lambda" is reserved for lambda-scoped parameter paths. ` +
56
+ `The derived secrets path /${appName}/lambda/${serviceName} would be ` +
57
+ `read as a lambda namespace. Rename the cluster, or set ssmSecretsPath ` +
58
+ `explicitly on service '${serviceName}'.`);
59
+ }
60
+ return buildParameterPath({
61
+ app: appName,
62
+ cluster: props.clusterName,
63
+ service: serviceName
64
+ });
50
65
  }
51
66
  export function createTaskDefinition(ctx, serviceName, serviceProps, executionRole, taskRole) {
52
67
  const cpu = serviceProps.cpu ?? DEFAULT_FARGATE_CPU;
@@ -155,6 +170,7 @@ export function addContainersToTask(ctx, serviceName, serviceProps, taskDefiniti
155
170
  }
156
171
  const ssmSecretsPath = deriveSsmSecretsPath(ctx.props, serviceName, serviceProps.ssmSecretsPath);
157
172
  for (const secretName of containerConfig.secrets) {
173
+ validateSecretName(secretName, `container '${containerConfig.name}' of service '${serviceName}'`);
158
174
  const paramPath = `${ssmSecretsPath}/${secretName}`;
159
175
  const param = StringParameter.fromSecureStringParameterAttributes(ctx.scope, `${ctx.props.clusterName}${serviceName}${containerConfig.name}${secretName}SsmParam`, { parameterName: paramPath });
160
176
  secrets[secretName] = EcsSecret.fromSsmParameter(param);
@@ -32,3 +32,10 @@ export declare function validateEcsClusterProps(props: EcsClusterProps): void;
32
32
  * @throws Error if the component is invalid
33
33
  */
34
34
  export declare function validateSsmPathComponent(component: string, fieldName: string): void;
35
+ /**
36
+ * Validates a secret name wired into a container or migration task.
37
+ * The construct is the last validation line for direct-CDK consumers: a name
38
+ * failing SECRET_NAME_PATTERN (slash, digit-leading) synthesises a parameter
39
+ * reference the fjall secrets tooling can never write.
40
+ */
41
+ export declare function validateSecretName(name: string, context: string): void;
@@ -1,6 +1,6 @@
1
1
  import { NetworkMode } from "aws-cdk-lib/aws-ecs";
2
2
  import { evaluateBakeGuard } from "@fjall/util/docker";
3
- import { toKebab } from "@fjall/util";
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
  /**
6
6
  * Validates ECS cluster props before construction.
@@ -185,8 +185,10 @@ export function validateSsmPathComponent(component, fieldName) {
185
185
  if (!component || component.trim() === "") {
186
186
  throw new Error(`${fieldName} cannot be empty for SSM path derivation`);
187
187
  }
188
- if (component.includes("/")) {
189
- throw new Error(`${fieldName} cannot contain forward slashes (/). Invalid value: "${component}".`);
188
+ if (!SSM_COMPONENT_PATTERN.test(component)) {
189
+ throw new Error(`${fieldName} is not a valid SSM path component. ${SSM_COMPONENT_ERROR}. ` +
190
+ `Invalid value: "${component}". A path derived from it could not be ` +
191
+ `managed through fjall secrets — rename it, or set ssmSecretsPath explicitly.`);
190
192
  }
191
193
  // SSM parameter name hierarchy labels have a max length of 2048, but we use a more
192
194
  // reasonable limit since each component is just one part of the path
@@ -194,3 +196,14 @@ export function validateSsmPathComponent(component, fieldName) {
194
196
  throw new Error(`${fieldName} exceeds maximum length (128 characters).`);
195
197
  }
196
198
  }
199
+ /**
200
+ * Validates a secret name wired into a container or migration task.
201
+ * The construct is the last validation line for direct-CDK consumers: a name
202
+ * failing SECRET_NAME_PATTERN (slash, digit-leading) synthesises a parameter
203
+ * reference the fjall secrets tooling can never write.
204
+ */
205
+ export function validateSecretName(name, context) {
206
+ if (!SECRET_NAME_PATTERN.test(name)) {
207
+ throw new Error(`Secret name "${name}" in ${context} is invalid. ${SECRET_NAME_ERROR}.`);
208
+ }
209
+ }
@@ -10,7 +10,9 @@ import { PolicyStatement, Effect } from "aws-cdk-lib/aws-iam";
10
10
  import { RetentionDays } from "aws-cdk-lib/aws-logs";
11
11
  import { LogGroup } from "../logging/logGroup.js";
12
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ import { buildParameterPath } from "@fjall/util";
13
14
  import { resolveImportedSecret } from "../secrets/index.js";
15
+ import { validateSsmPathComponent } from "./ecsValidation.js";
14
16
  import { toPascalCase } from "../../../utils/capitaliseString.js";
15
17
  import { resolvePrivateSubnetType } from "../../../utils/vpcUtils.js";
16
18
  import { createLambdaAlarms } from "../monitoring/index.js";
@@ -369,6 +371,8 @@ export class LambdaFunction extends Function {
369
371
  "and appName/functionName are not available. " +
370
372
  "Either set ssmSecretsPath explicitly, or ensure App.getName() returns a value.");
371
373
  }
372
- return `/${appName}/lambda/${functionName}`;
374
+ validateSsmPathComponent(appName, "appName");
375
+ validateSsmPathComponent(functionName, "functionName");
376
+ return buildParameterPath({ app: appName, lambda: functionName });
373
377
  }
374
378
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/components-infrastructure",
3
- "version": "2.22.0",
3
+ "version": "2.23.0",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -63,8 +63,8 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "@aws-sdk/client-organizations": "^3.1038.0",
66
- "@fjall/generator": "^2.22.0",
67
- "@fjall/util": "^2.22.0",
66
+ "@fjall/generator": "^2.23.0",
67
+ "@fjall/util": "^2.23.0",
68
68
  "constructs": "^10.6.0"
69
69
  },
70
70
  "overrides": {
@@ -78,5 +78,5 @@
78
78
  "engines": {
79
79
  "node": ">=18.0.0"
80
80
  },
81
- "gitHead": "448c771d258e87f0535ae7698f11438e0f7c57b1"
81
+ "gitHead": "dadd84e6118f03252877974eec755e812cb5b42f"
82
82
  }