@fjall/components-infrastructure 2.32.0 → 2.34.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/app.js CHANGED
@@ -20,6 +20,7 @@ import { FjallLogger } from "./utils/validationLogger.js";
20
20
  import { getManifestCollector, writeManifest } from "./utils/manifestWriter.js";
21
21
  import { toPascalCase, toKebab } from "./utils/capitaliseString.js";
22
22
  import { COST_ALLOCATION_TAGS } from "./utils/costAllocationTags.js";
23
+ import { DEFAULT_ORG_ID, resolveOrgId } from "./utils/cdkContext.js";
23
24
  /**
24
25
  * The basic corner-stone of all Fjall-hosted applications.
25
26
  * This class is a singleton and should be used to create and manage
@@ -702,6 +703,15 @@ export class App extends CdkApp {
702
703
  }
703
704
  applyTagsAspect() {
704
705
  if (!this.aspectApplied && Object.keys(this.globalTags).length > 0) {
706
+ // Owner fallback resolves lazily HERE, not in initialiseStandardTags:
707
+ // at construction the orgId context may not be set yet, and an explicit
708
+ // addTags owner must win — by this point it is already in globalTags.
709
+ if (this.globalTags[COST_ALLOCATION_TAGS.OWNER] === undefined) {
710
+ const orgId = resolveOrgId(this.node);
711
+ if (orgId !== undefined && orgId !== DEFAULT_ORG_ID) {
712
+ this.globalTags[COST_ALLOCATION_TAGS.OWNER] = orgId;
713
+ }
714
+ }
705
715
  // Apply standard tags using Tags.of(this).add()
706
716
  for (const [key, value] of Object.entries(this.globalTags)) {
707
717
  Tags.of(this).add(key, value);
@@ -748,7 +758,11 @@ export class App extends CdkApp {
748
758
  return this.manifestCollector;
749
759
  }
750
760
  synth(options) {
751
- // Call parent synth first
761
+ // Tagging is an invariant of synthesis, not of getStack(): governance
762
+ // stacks (Organisation/Account/Platform) and raw `extends Stack` classes
763
+ // attach directly to the App and never call getStack(), so without this
764
+ // they synthesise untagged. Idempotent via the aspectApplied guard.
765
+ this.applyTagsAspect();
752
766
  const assembly = super.synth(options);
753
767
  // After synthesis, write Fjall manifest to cdk.out
754
768
  try {
@@ -1,4 +1,4 @@
1
- import { CfnOutput, Stack } from "aws-cdk-lib";
1
+ import { CfnOutput, Stack, Tags } from "aws-cdk-lib";
2
2
  import { EcrDefaultImage, SharedAlarmTopic } from "../../config/aws/index.js";
3
3
  import { ManagementEventsTrail } from "../../config/aws/cloudTrail.js";
4
4
  import { OidcConnector } from "../../config/aws/oidcConnector.js";
@@ -6,6 +6,7 @@ import { AccountMonitoringRole } from "../../config/aws/accountMonitoringRole.js
6
6
  import { AccountAuditRole } from "../../config/aws/accountAuditRole.js";
7
7
  import { CDK_CONTEXT_KEYS, resolveAccountTrailState } from "../../utils/cdkContext.js";
8
8
  import { getConfig } from "../../utils/getConfig.js";
9
+ import { COST_ALLOCATION_TAGS } from "../../utils/costAllocationTags.js";
9
10
  import { DisasterRecovery } from "../../config/aws/disasterRecovery.js";
10
11
  import { S3BlockPublicAccess } from "../../config/aws/s3BlockPublicAccess.js";
11
12
  import { EbsDefaultEncryption } from "../../config/aws/ebsDefaultEncryption.js";
@@ -28,6 +29,10 @@ export class Account extends Stack {
28
29
  }
29
30
  const env = props.env ?? { region, account: accountId };
30
31
  super(scope, id, { ...props, env });
32
+ // Governance apps never name the App, so the App-level service tag would
33
+ // read "FjallApp"; the stack-scope tag wins over App scope and names the
34
+ // governance tier instead ("Organisation"/"Account"/"Platform").
35
+ Tags.of(this).add(COST_ALLOCATION_TAGS.SERVICE, id);
31
36
  this.resolvedRegion = region ?? this.region;
32
37
  const orgId = this.node.tryGetContext(CDK_CONTEXT_KEYS.ORG_ID);
33
38
  if (orgId) {
@@ -13,6 +13,7 @@ import { resolveImportedSecret } from "../../resources/aws/secrets/index.js";
13
13
  import App from "../../app.js";
14
14
  import EcsCluster from "../../resources/aws/compute/ecs.js";
15
15
  import { createScheduledTaskDefinition, createMigrationTaskDefinition } from "../../resources/aws/compute/ecsTaskDefinition.js";
16
+ import { DEFAULT_LOG_RETENTION } from "../../resources/aws/compute/ecsConstants.js";
16
17
  import { EcsLifecycleHookMigration } from "../../resources/aws/compute/ecsLifecycleHookMigration.js";
17
18
  import { Role } from "../../resources/aws/iam/role.js";
18
19
  import { LogGroup } from "../../resources/aws/logging/logGroup.js";
@@ -924,13 +925,16 @@ export class EcsCompute extends Construct {
924
925
  });
925
926
  }
926
927
  }
928
+ // `logRetention` must not reach the driver: AwsLogDriver.bind() creates
929
+ // a raw RETAIN LogGroup inside aws-cdk-lib, escaping the env-aware
930
+ // removal default (D17).
931
+ const logGroup = entry.logGroup ??
932
+ new LogGroup(this, `${id}${toPascalCase(entry.name)}LogGroup`, {
933
+ retention: entry.logRetention ?? DEFAULT_LOG_RETENTION
934
+ });
927
935
  const logging = new AwsLogDriver({
928
936
  streamPrefix: `/ecs-scheduled/${id}/${entry.name}`,
929
- ...(entry.logGroup !== undefined && { logGroup: entry.logGroup }),
930
- ...(entry.logGroup === undefined &&
931
- entry.logRetention !== undefined && {
932
- logRetention: entry.logRetention
933
- })
937
+ logGroup
934
938
  });
935
939
  const container = taskDef.addContainer(`${id}${toPascalCase(entry.name)}Container`, {
936
940
  image: entry.image,
@@ -501,7 +501,11 @@ export interface EcsScheduledTaskConfig {
501
501
  secrets?: Record<string, EcsSecret>;
502
502
  /** Pre-existing CDK log group. Mutually exclusive with `logRetention`. */
503
503
  logGroup?: ILogGroup;
504
- /** When `logGroup` is omitted, the awsLogs driver creates one with this retention. */
504
+ /**
505
+ * When `logGroup` is omitted, an explicit framework log group is created
506
+ * with this retention (default: `DEFAULT_LOG_RETENTION`) and the env-aware
507
+ * removal default (D17).
508
+ */
505
509
  logRetention?: RetentionDays;
506
510
  securityGroups?: ISecurityGroup[];
507
511
  subnetSelection?: SubnetSelection;
@@ -453,7 +453,9 @@ export type IPatternProps = IPayloadProps | IStaticSiteProps;
453
453
  * Note the vocabulary is wider than `IPatternProps`: `nextjs` is declared in
454
454
  * `PATTERN_REGISTRY` but has no construct, so it is deliberately absent from the
455
455
  * union above and `PatternFactory.build` will not accept it — the omission here
456
- * is the single enforcement of that gap.
456
+ * is the compile-time enforcement of that gap. The registry's `deployable`
457
+ * flag mirrors it for runtime consumers (the CLI picker/validation cannot read
458
+ * this union); when adding the construct, extend the union AND flip the flag.
457
459
  */
458
460
  export type { PatternType };
459
461
  /**
@@ -1,5 +1,5 @@
1
1
  import { AwsLogDriver, ContainerDependencyCondition, FargateTaskDefinition, Ec2TaskDefinition, NetworkMode, CpuArchitecture, OperatingSystemFamily } from "aws-cdk-lib/aws-ecs";
2
- import { Duration, RemovalPolicy } from "aws-cdk-lib";
2
+ import { Duration } 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
5
  import { buildParameterPath } from "@fjall/util";
@@ -143,10 +143,10 @@ export function addContainersToTask(ctx, serviceName, serviceProps, taskDefiniti
143
143
  // Explicit so the cluster can attach metric filters (createLogPatternAlarms).
144
144
  // No logGroupName: CDK auto-names from the logical ID so it cannot collide
145
145
  // with the orphaned-retained old group during the implicit→explicit replace.
146
- // RETAIN (never DESTROY): preserves fail-closed evidence and supports rollback.
146
+ // Removal: env-aware wrapper default (D17) production RETAIN keeps
147
+ // fail-closed rollback evidence; non-production DESTROY avoids orphans.
147
148
  const logGroup = new LogGroup(ctx.scope, `${ctx.props.clusterName}${serviceName}LogGroup`, {
148
- retention: DEFAULT_LOG_RETENTION,
149
- removalPolicy: RemovalPolicy.RETAIN
149
+ retention: DEFAULT_LOG_RETENTION
150
150
  });
151
151
  for (const containerConfig of serviceProps.containers) {
152
152
  const image = getContainerImage(ctx, serviceName, containerConfig, serviceProps);
@@ -7,6 +7,12 @@ import { type Construct } from "constructs";
7
7
  * would silently give sibling framework lambdas different retention.
8
8
  */
9
9
  export declare const DEFAULT_FRAMEWORK_LOG_RETENTION = Logs.RetentionDays.ONE_WEEK;
10
+ /**
11
+ * Framework log group with an env-aware removal-policy default (D17).
12
+ * When no `removalPolicy` is passed, the default resolves via
13
+ * `envAwareRemovalPolicyDefault()` — production → RETAIN, other recognised
14
+ * stages → DESTROY, unrecognised values fail synth. An explicit prop wins.
15
+ */
10
16
  export declare class LogGroup extends Logs.LogGroup {
11
17
  constructor(scope: Construct, id: string, props?: Logs.LogGroupProps);
12
18
  }
@@ -1,4 +1,5 @@
1
1
  import * as Logs from "aws-cdk-lib/aws-logs";
2
+ import { envAwareRemovalPolicyDefault, toRemovalPolicy } from "../../../utils/removalPolicy.js";
2
3
  /**
3
4
  * Default retention for framework-owned lambda log groups. Coupled across
4
5
  * every framework lambda construct (LambdaFunction, SingletonFunction, the
@@ -6,8 +7,17 @@ import * as Logs from "aws-cdk-lib/aws-logs";
6
7
  * would silently give sibling framework lambdas different retention.
7
8
  */
8
9
  export const DEFAULT_FRAMEWORK_LOG_RETENTION = Logs.RetentionDays.ONE_WEEK;
10
+ /**
11
+ * Framework log group with an env-aware removal-policy default (D17).
12
+ * When no `removalPolicy` is passed, the default resolves via
13
+ * `envAwareRemovalPolicyDefault()` — production → RETAIN, other recognised
14
+ * stages → DESTROY, unrecognised values fail synth. An explicit prop wins.
15
+ */
9
16
  export class LogGroup extends Logs.LogGroup {
10
17
  constructor(scope, id, props) {
11
- super(scope, id, props);
18
+ super(scope, id, {
19
+ ...props,
20
+ removalPolicy: props?.removalPolicy ?? toRemovalPolicy(envAwareRemovalPolicyDefault())
21
+ });
12
22
  }
13
23
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/components-infrastructure",
3
- "version": "2.32.0",
3
+ "version": "2.34.0",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -67,8 +67,8 @@
67
67
  },
68
68
  "dependencies": {
69
69
  "@aws-sdk/client-organizations": "^3.1038.0",
70
- "@fjall/generator": "^2.32.0",
71
- "@fjall/util": "^2.32.0",
70
+ "@fjall/generator": "^2.34.0",
71
+ "@fjall/util": "^2.34.0",
72
72
  "constructs": "^10.6.0"
73
73
  },
74
74
  "overrides": {
@@ -82,5 +82,5 @@
82
82
  "engines": {
83
83
  "node": ">=18.0.0"
84
84
  },
85
- "gitHead": "876a79a9ad0031a5919019c86867c28c1ab9dc92"
85
+ "gitHead": "8c553fca8f9737d445061ea75cf6c6c7db0d24c9"
86
86
  }