@fjall/components-infrastructure 3.2.1 → 3.3.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.d.ts +12 -12
- package/dist/lib/app.js +6 -42
- package/dist/lib/config/aws/scpPreset.js +8 -1
- package/dist/lib/patterns/aws/buildkite/buildkite.js +7 -0
- package/dist/lib/patterns/aws/buildkite/factory.d.ts +27 -0
- package/dist/lib/patterns/aws/buildkite/factory.js +39 -0
- package/dist/lib/patterns/aws/buildkite/iam.js +4 -0
- package/dist/lib/patterns/aws/buildkite/schema.d.ts +6 -79
- package/dist/lib/patterns/aws/buildkite/schema.js +6 -153
- package/dist/lib/patterns/aws/buildkite/userData.d.ts +12 -1
- package/dist/lib/patterns/aws/buildkite/userData.js +14 -3
- package/dist/lib/patterns/aws/buildkite.d.ts +1 -0
- package/dist/lib/patterns/aws/buildkite.js +1 -0
- package/package.json +4 -4
package/dist/lib/app.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { type RemovalPolicyString } from "./resources/aws/messaging/utils.js";
|
|
|
16
16
|
import { type ServiceRegistrationProps } from "./resources/aws/networking/serviceDiscovery.js";
|
|
17
17
|
import { type IPrivateDnsNamespace, type IService } from "aws-cdk-lib/aws-servicediscovery";
|
|
18
18
|
import { type AnyCompute } from "./patterns/aws/compute.js";
|
|
19
|
-
import { Buildkite, type BuildkitePropsInput } from "./patterns/aws/buildkite.js";
|
|
19
|
+
import { type Buildkite, type BuildkiteFactoryFn, type BuildkitePropsInput } from "./patterns/aws/buildkite.js";
|
|
20
20
|
import { DevSubstrate, type IDevSubstrateProps } from "./patterns/aws/devSubstrate.js";
|
|
21
21
|
import { type Storage, type StorageFactoryFn } from "./patterns/aws/storage.js";
|
|
22
22
|
import { type AnyPattern, type PatternFactoryFn } from "./patterns/aws/pattern.js";
|
|
@@ -288,19 +288,19 @@ export declare class App extends CdkApp {
|
|
|
288
288
|
* same registry posture — Fjall-managed platform infrastructure,
|
|
289
289
|
* deliberately NOT on the customer create/scaffold surface (design § D3).
|
|
290
290
|
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
* `props
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
* pages to the application; an explicit prop still wins.
|
|
291
|
+
* App-level defaults (cost-allocation environment from `getConfig()`,
|
|
292
|
+
* alarm actions to the account's `SharedAlarmTopicArn` export,
|
|
293
|
+
* `applicationId` to the app name — designs § D10/§ D14) are resolved by
|
|
294
|
+
* `resolveBuildkiteConstructProps`; explicit props always win.
|
|
295
|
+
*
|
|
296
|
+
* The props overload pins construct id `"Buildkite"` (byte-identical
|
|
297
|
+
* logical IDs for pre-factory consumers); the factory overload accepts
|
|
298
|
+
* `BuildkiteFactory.build(id, props)` — the shape the codemod add path
|
|
299
|
+
* emits. Both overloads resolve placement here, so the deploy-core
|
|
300
|
+
* fixed-stack contract cannot be bypassed from an emitted statement.
|
|
302
301
|
*/
|
|
303
302
|
addBuildkite(props: BuildkitePropsInput): Buildkite;
|
|
303
|
+
addBuildkite(fn: BuildkiteFactoryFn): Buildkite;
|
|
304
304
|
/**
|
|
305
305
|
* Add a database resource to the default database stack using the factory pattern.
|
|
306
306
|
* Returns the appropriate database type based on the factory used.
|
package/dist/lib/app.js
CHANGED
|
@@ -9,14 +9,12 @@ import { NetworkFactory } from "./patterns/aws/network.js";
|
|
|
9
9
|
import { MessagingFactory } from "./patterns/aws/messaging.js";
|
|
10
10
|
import { Schedule } from "./resources/aws/messaging/schedule.js";
|
|
11
11
|
import { ServiceDiscoveryNamespace } from "./resources/aws/networking/serviceDiscovery.js";
|
|
12
|
-
import {
|
|
12
|
+
import { BuildkiteFactory } from "./patterns/aws/buildkite.js";
|
|
13
13
|
import { DevSubstrate } from "./patterns/aws/devSubstrate.js";
|
|
14
14
|
import { StandardTagsAspect } from "./utils/standardTagsAspect.js";
|
|
15
15
|
import { BACKUP_TIER_TAG_KEY, BACKUP_TIER_TAG_MAP } from "./utils/backupTierMapping.js";
|
|
16
16
|
import { randomBytes } from "crypto";
|
|
17
17
|
import { getConfig } from "./utils/getConfig.js";
|
|
18
|
-
import { UNKNOWN_ENVIRONMENT } from "./utils/env.js";
|
|
19
|
-
import { SHARED_ALARM_TOPIC_EXPORT_NAME } from "./config/aws/alarmTopic.js";
|
|
20
18
|
import AuditRoleFactory from "./resources/aws/audit/auditRole.js";
|
|
21
19
|
import { FJALL_AUDIT_CONFIG } from "./config/audit.js";
|
|
22
20
|
import { FjallLogger } from "./utils/validationLogger.js";
|
|
@@ -483,46 +481,12 @@ export class App extends CdkApp {
|
|
|
483
481
|
computeStack.addConstruct(substrate);
|
|
484
482
|
return substrate;
|
|
485
483
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
* Same placement contract as `addDevSubstrate` above: deploy-core selects
|
|
491
|
-
* stacks by the fixed six-category enum, so scoping under
|
|
492
|
-
* `${stackPrefix}Compute` is what makes the fleet actually deploy. Also the
|
|
493
|
-
* same registry posture — Fjall-managed platform infrastructure,
|
|
494
|
-
* deliberately NOT on the customer create/scaffold surface (design § D3).
|
|
495
|
-
*
|
|
496
|
-
* Cost-allocation environment resolves from `getConfig()` (design § D10) so
|
|
497
|
-
* the fleet's environment tag matches the deploying config rather than the
|
|
498
|
-
* construct's `management` fallback; an explicit
|
|
499
|
-
* `props.costAllocationEnvironment` still wins.
|
|
500
|
-
*
|
|
501
|
-
* Alarm actions default to the account's `SharedAlarmTopicArn` export
|
|
502
|
-
* (design § D14 — alerting ships WITH the fleet): every fjall-governed
|
|
503
|
-
* account's Account/Platform stack produces that export, so alarms page
|
|
504
|
-
* out of the box. An explicit `props.alarmSnsTopicArn` still wins.
|
|
505
|
-
* `applicationId` defaults to the app name so the alarm webhook can map
|
|
506
|
-
* pages to the application; an explicit prop still wins.
|
|
507
|
-
*/
|
|
508
|
-
addBuildkite(props) {
|
|
484
|
+
addBuildkite(input) {
|
|
485
|
+
const fn = typeof input === "function"
|
|
486
|
+
? input
|
|
487
|
+
: BuildkiteFactory.build("Buildkite", input);
|
|
509
488
|
const computeStack = this.getDefaultComputeStack();
|
|
510
|
-
const
|
|
511
|
-
const constructProps = { ...props, vpc: this.getVpc() };
|
|
512
|
-
// Post-spread injection so a caller-materialised `costAllocationEnvironment:
|
|
513
|
-
// undefined` counts as absent rather than clobbering the resolved value.
|
|
514
|
-
if (constructProps.costAllocationEnvironment === undefined &&
|
|
515
|
-
configEnvironment !== "" &&
|
|
516
|
-
configEnvironment !== UNKNOWN_ENVIRONMENT) {
|
|
517
|
-
constructProps.costAllocationEnvironment = configEnvironment;
|
|
518
|
-
}
|
|
519
|
-
if (constructProps.alarmSnsTopicArn === undefined) {
|
|
520
|
-
constructProps.alarmSnsTopicArn = `import:${SHARED_ALARM_TOPIC_EXPORT_NAME}`;
|
|
521
|
-
}
|
|
522
|
-
if (constructProps.applicationId === undefined) {
|
|
523
|
-
constructProps.applicationId = this.getName();
|
|
524
|
-
}
|
|
525
|
-
const fleet = new Buildkite(computeStack.getStack(), "Buildkite", constructProps);
|
|
489
|
+
const fleet = fn(this, computeStack.getStack());
|
|
526
490
|
computeStack.addConstruct(fleet);
|
|
527
491
|
return fleet;
|
|
528
492
|
}
|
|
@@ -307,7 +307,14 @@ function buildDevIsolationGuardrails() {
|
|
|
307
307
|
Sid: "DenyCrossAccountAssumeRole",
|
|
308
308
|
Effect: "Deny",
|
|
309
309
|
Action: ["sts:AssumeRole", "sts:AssumeRoot"],
|
|
310
|
-
|
|
310
|
+
// NotResource carves delegation roles (the DelegationRole construct's
|
|
311
|
+
// fixed `<label>DelegateHostedZoneRole` naming contract) out of the
|
|
312
|
+
// deny — a delegated child domain deployed into a dev account must
|
|
313
|
+
// assume the parent zone's delegation role cross-account to write its
|
|
314
|
+
// NS. Those roles are NS-write-scoped by construction
|
|
315
|
+
// (delegationRole.ts). AssumeRoot stays denied everywhere: root-user
|
|
316
|
+
// ARNs never match the role pattern.
|
|
317
|
+
NotResource: "arn:aws:iam::*:role/*DelegateHostedZoneRole",
|
|
311
318
|
Condition: {
|
|
312
319
|
StringNotEquals: {
|
|
313
320
|
// IAM policy variable, substituted per-request by AWS — the
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CfnOutput, Duration, RemovalPolicy, Stack, Token } from "aws-cdk-lib";
|
|
2
|
+
import { Monitoring } from "aws-cdk-lib/aws-autoscaling";
|
|
2
3
|
import { InstanceArchitecture, InstanceType, MachineImage, SubnetType } from "aws-cdk-lib/aws-ec2";
|
|
3
4
|
import { PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
4
5
|
import { Source } from "aws-cdk-lib/aws-s3-deployment";
|
|
@@ -106,6 +107,12 @@ export class Buildkite extends Construct {
|
|
|
106
107
|
userData,
|
|
107
108
|
role: agentRole,
|
|
108
109
|
ssmSessionPermissions: false,
|
|
110
|
+
// Upstream parity: EnableDetailedMonitoring defaults "false" in the
|
|
111
|
+
// pinned v6.68.1 template, and the wrapper's absent-default is DETAILED
|
|
112
|
+
// — omitting this silently deviated. The fleet's alarms and scaler run
|
|
113
|
+
// on Buildkite queue metrics, not EC2 metrics, so 1-minute granularity
|
|
114
|
+
// buys nothing here.
|
|
115
|
+
instanceMonitoring: Monitoring.BASIC,
|
|
109
116
|
blockDevices: [
|
|
110
117
|
{
|
|
111
118
|
deviceName: "/dev/xvda",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Construct } from "constructs";
|
|
2
|
+
import type App from "../../../app.js";
|
|
3
|
+
import { Buildkite, type BuildkiteConstructProps } from "./buildkite.js";
|
|
4
|
+
import type { BuildkitePropsInput } from "./schema.js";
|
|
5
|
+
/**
|
|
6
|
+
* Thunk returned by `BuildkiteFactory.build` — materialised only by
|
|
7
|
+
* `App.addBuildkite`, which owns stack placement (the fleet has exactly one
|
|
8
|
+
* legal home, `${stackPrefix}Compute`; design
|
|
9
|
+
* `aiDocs/designs/2026-07-18-fjall-add-buildkite.md` § D2).
|
|
10
|
+
*/
|
|
11
|
+
export type BuildkiteFactoryFn = (app: App, scope: Construct) => Buildkite;
|
|
12
|
+
/**
|
|
13
|
+
* Resolve construct props from plain-data input: VPC injection plus the three
|
|
14
|
+
* app-level defaults (cost-allocation environment, shared alarm topic,
|
|
15
|
+
* applicationId). Single source for both `App.addBuildkite` entry paths
|
|
16
|
+
* (design § D2) — explicit props always win.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveBuildkiteConstructProps(app: App, props: BuildkitePropsInput): BuildkiteConstructProps;
|
|
19
|
+
/**
|
|
20
|
+
* Factory for the self-hosted Buildkite agent fleet — the codemod add path
|
|
21
|
+
* emits `app.addBuildkite(BuildkiteFactory.build("Buildkite", { … }))`
|
|
22
|
+
* (design § D2/§ D3). Validation, the region guard, cost tags, and pinned-AMI
|
|
23
|
+
* resolution all live in the `Buildkite` constructor and fire on every path.
|
|
24
|
+
*/
|
|
25
|
+
export declare class BuildkiteFactory {
|
|
26
|
+
static build(id: string, props: BuildkitePropsInput): BuildkiteFactoryFn;
|
|
27
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { SHARED_ALARM_TOPIC_EXPORT_NAME } from "../../../config/aws/alarmTopic.js";
|
|
2
|
+
import { UNKNOWN_ENVIRONMENT } from "../../../utils/env.js";
|
|
3
|
+
import { getConfig } from "../../../utils/getConfig.js";
|
|
4
|
+
import { Buildkite } from "./buildkite.js";
|
|
5
|
+
/**
|
|
6
|
+
* Resolve construct props from plain-data input: VPC injection plus the three
|
|
7
|
+
* app-level defaults (cost-allocation environment, shared alarm topic,
|
|
8
|
+
* applicationId). Single source for both `App.addBuildkite` entry paths
|
|
9
|
+
* (design § D2) — explicit props always win.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveBuildkiteConstructProps(app, props) {
|
|
12
|
+
const configEnvironment = getConfig().environment;
|
|
13
|
+
const constructProps = { ...props, vpc: app.getVpc() };
|
|
14
|
+
// Post-spread injection so a caller-materialised `costAllocationEnvironment:
|
|
15
|
+
// undefined` counts as absent rather than clobbering the resolved value.
|
|
16
|
+
if (constructProps.costAllocationEnvironment === undefined &&
|
|
17
|
+
configEnvironment !== "" &&
|
|
18
|
+
configEnvironment !== UNKNOWN_ENVIRONMENT) {
|
|
19
|
+
constructProps.costAllocationEnvironment = configEnvironment;
|
|
20
|
+
}
|
|
21
|
+
if (constructProps.alarmSnsTopicArn === undefined) {
|
|
22
|
+
constructProps.alarmSnsTopicArn = `import:${SHARED_ALARM_TOPIC_EXPORT_NAME}`;
|
|
23
|
+
}
|
|
24
|
+
if (constructProps.applicationId === undefined) {
|
|
25
|
+
constructProps.applicationId = app.getName();
|
|
26
|
+
}
|
|
27
|
+
return constructProps;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Factory for the self-hosted Buildkite agent fleet — the codemod add path
|
|
31
|
+
* emits `app.addBuildkite(BuildkiteFactory.build("Buildkite", { … }))`
|
|
32
|
+
* (design § D2/§ D3). Validation, the region guard, cost tags, and pinned-AMI
|
|
33
|
+
* resolution all live in the `Buildkite` constructor and fire on every path.
|
|
34
|
+
*/
|
|
35
|
+
export class BuildkiteFactory {
|
|
36
|
+
static build(id, props) {
|
|
37
|
+
return (app, scope) => new Buildkite(scope, id, resolveBuildkiteConstructProps(app, props));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -116,6 +116,10 @@ function buildAgentPolicyDocuments(params) {
|
|
|
116
116
|
logging: new PolicyDocument({
|
|
117
117
|
statements: [
|
|
118
118
|
new PolicyStatement({
|
|
119
|
+
// PutRetentionPolicy pairs with ENABLE_EC2_LOG_RETENTION_POLICY
|
|
120
|
+
// ='true' in the install part (userData.ts) — the booting instance
|
|
121
|
+
// applies agentLogRetentionDays to the /buildkite/* groups. Remove
|
|
122
|
+
// only together with that flag.
|
|
119
123
|
actions: [
|
|
120
124
|
"logs:CreateLogGroup",
|
|
121
125
|
"logs:CreateLogStream",
|
|
@@ -1,81 +1,8 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* (design §
|
|
7
|
-
*
|
|
8
|
-
* Defaults encode the design's deploy-fleet posture
|
|
9
|
-
* (`aiDocs/designs/2026-07-18-buildkite-selfhosted-agents.md`):
|
|
10
|
-
* on-demand-only (`spotCapacityPercentage` 0 — ASG mixed-instances has no
|
|
11
|
-
* spot→on-demand fallback, and a spot reclaim mid-deploy is a rollback
|
|
12
|
-
* incident, § D7), scale-to-zero, whole-instance agents, bounded instance
|
|
13
|
-
* staleness, and no ECR / docker-login instance-level credentials (§ D9 —
|
|
14
|
-
* deploy AWS access stays server-minted; the instance profile confers none).
|
|
2
|
+
* Buildkite props schema — re-export barrel. The canonical declaration lives
|
|
3
|
+
* in `@fjall/generator` (`src/schemas/buildkiteSchemas.ts`) so construct
|
|
4
|
+
* validation, the codemod fragment, and the LLM JSON schema all consume one
|
|
5
|
+
* source (design `aiDocs/designs/2026-07-18-fjall-add-buildkite.md` § D4).
|
|
6
|
+
* Do NOT redeclare here — the cross-package reference-identity test pins it.
|
|
15
7
|
*/
|
|
16
|
-
export
|
|
17
|
-
buildkiteQueue: z.ZodString;
|
|
18
|
-
buildkiteOrgSlug: z.ZodString;
|
|
19
|
-
agentTokenSsmParameterName: z.ZodString;
|
|
20
|
-
agentTokenKmsKeyArn: z.ZodOptional<z.ZodString>;
|
|
21
|
-
fjallApiKeySsmParameterName: z.ZodOptional<z.ZodString>;
|
|
22
|
-
instanceType: z.ZodDefault<z.ZodString>;
|
|
23
|
-
agentVolumeSizeGib: z.ZodDefault<z.ZodNumber>;
|
|
24
|
-
agentMinInstances: z.ZodDefault<z.ZodNumber>;
|
|
25
|
-
agentMaxInstances: z.ZodDefault<z.ZodNumber>;
|
|
26
|
-
agentsPerInstance: z.ZodDefault<z.ZodNumber>;
|
|
27
|
-
spotCapacityPercentage: z.ZodDefault<z.ZodNumber>;
|
|
28
|
-
scaleInIdlePeriodSeconds: z.ZodDefault<z.ZodNumber>;
|
|
29
|
-
disconnectAfterUptimeSeconds: z.ZodDefault<z.ZodNumber>;
|
|
30
|
-
maxInstanceLifetimeDays: z.ZodDefault<z.ZodNumber>;
|
|
31
|
-
terminateInstanceAfterJob: z.ZodDefault<z.ZodBoolean>;
|
|
32
|
-
purgeBuildsOnDiskFull: z.ZodDefault<z.ZodBoolean>;
|
|
33
|
-
terminateInstanceOnDiskFull: z.ZodDefault<z.ZodBoolean>;
|
|
34
|
-
logRetentionDays: z.ZodDefault<z.ZodNumber>;
|
|
35
|
-
buildkiteAgentRelease: z.ZodDefault<z.ZodEnum<{
|
|
36
|
-
stable: "stable";
|
|
37
|
-
beta: "beta";
|
|
38
|
-
edge: "edge";
|
|
39
|
-
}>>;
|
|
40
|
-
buildkiteAgentTags: z.ZodDefault<z.ZodString>;
|
|
41
|
-
buildkiteAgentTimestampLines: z.ZodDefault<z.ZodBoolean>;
|
|
42
|
-
buildkiteAgentExperiments: z.ZodDefault<z.ZodString>;
|
|
43
|
-
buildkiteAgentTracingBackend: z.ZodDefault<z.ZodEnum<{
|
|
44
|
-
"": "";
|
|
45
|
-
datadog: "datadog";
|
|
46
|
-
opentelemetry: "opentelemetry";
|
|
47
|
-
}>>;
|
|
48
|
-
buildkiteAgentCancelGracePeriodSeconds: z.ZodDefault<z.ZodNumber>;
|
|
49
|
-
enableSecretsPlugin: z.ZodDefault<z.ZodBoolean>;
|
|
50
|
-
enableEcrPlugin: z.ZodDefault<z.ZodBoolean>;
|
|
51
|
-
enableDockerLoginPlugin: z.ZodDefault<z.ZodBoolean>;
|
|
52
|
-
enableDockerUserNamespaceRemap: z.ZodDefault<z.ZodBoolean>;
|
|
53
|
-
enableDockerExperimental: z.ZodDefault<z.ZodBoolean>;
|
|
54
|
-
dockerNetworkingProtocol: z.ZodDefault<z.ZodEnum<{
|
|
55
|
-
ipv4: "ipv4";
|
|
56
|
-
dualstack: "dualstack";
|
|
57
|
-
}>>;
|
|
58
|
-
enableInstanceStorage: z.ZodDefault<z.ZodBoolean>;
|
|
59
|
-
mountTmpfsAtTmp: z.ZodDefault<z.ZodBoolean>;
|
|
60
|
-
buildkiteAgentEnableGitMirrors: z.ZodDefault<z.ZodBoolean>;
|
|
61
|
-
bootstrapScriptUrl: z.ZodDefault<z.ZodString>;
|
|
62
|
-
agentEnvFileUrl: z.ZodDefault<z.ZodString>;
|
|
63
|
-
scalerEventSchedulePeriod: z.ZodDefault<z.ZodString>;
|
|
64
|
-
scalerMinPollInterval: z.ZodDefault<z.ZodString>;
|
|
65
|
-
scaleOutFactor: z.ZodDefault<z.ZodString>;
|
|
66
|
-
scaleOutWaitingForJobs: z.ZodDefault<z.ZodBoolean>;
|
|
67
|
-
rolePermissionsBoundaryArn: z.ZodOptional<z.ZodString>;
|
|
68
|
-
alarmSnsTopicArn: z.ZodOptional<z.ZodString>;
|
|
69
|
-
applicationId: z.ZodOptional<z.ZodString>;
|
|
70
|
-
costAllocationEnvironment: z.ZodOptional<z.ZodString>;
|
|
71
|
-
costAllocationOwner: z.ZodOptional<z.ZodString>;
|
|
72
|
-
}, z.core.$strict>;
|
|
73
|
-
export type BuildkiteProps = z.infer<typeof BuildkitePropsSchema>;
|
|
74
|
-
/** Caller-facing shape: fields with defaults are optional at the call site. */
|
|
75
|
-
export type BuildkitePropsInput = z.input<typeof BuildkitePropsSchema>;
|
|
76
|
-
/**
|
|
77
|
-
* Validate + default the plain-data props at construct time. Throws a
|
|
78
|
-
* synth-time error listing every violation — the pattern's constructor is the
|
|
79
|
-
* validation boundary, mirroring `ClickHouseDatabase`'s Stage-1 shape.
|
|
80
|
-
*/
|
|
81
|
-
export declare function validateBuildkiteProps(props: BuildkitePropsInput): BuildkiteProps;
|
|
8
|
+
export { BuildkitePropsObjectSchema, BuildkitePropsSchema, validateBuildkiteProps, type BuildkiteProps, type BuildkitePropsInput, type BuildkitePropsObject } from "@fjall/generator";
|
|
@@ -1,155 +1,8 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* (design §
|
|
7
|
-
*
|
|
8
|
-
* Defaults encode the design's deploy-fleet posture
|
|
9
|
-
* (`aiDocs/designs/2026-07-18-buildkite-selfhosted-agents.md`):
|
|
10
|
-
* on-demand-only (`spotCapacityPercentage` 0 — ASG mixed-instances has no
|
|
11
|
-
* spot→on-demand fallback, and a spot reclaim mid-deploy is a rollback
|
|
12
|
-
* incident, § D7), scale-to-zero, whole-instance agents, bounded instance
|
|
13
|
-
* staleness, and no ECR / docker-login instance-level credentials (§ D9 —
|
|
14
|
-
* deploy AWS access stays server-minted; the instance profile confers none).
|
|
2
|
+
* Buildkite props schema — re-export barrel. The canonical declaration lives
|
|
3
|
+
* in `@fjall/generator` (`src/schemas/buildkiteSchemas.ts`) so construct
|
|
4
|
+
* validation, the codemod fragment, and the LLM JSON schema all consume one
|
|
5
|
+
* source (design `aiDocs/designs/2026-07-18-fjall-add-buildkite.md` § D4).
|
|
6
|
+
* Do NOT redeclare here — the cross-package reference-identity test pins it.
|
|
15
7
|
*/
|
|
16
|
-
export
|
|
17
|
-
.object({
|
|
18
|
-
/** Buildkite queue this fleet serves (cluster-scoped via the token). */
|
|
19
|
-
buildkiteQueue: z
|
|
20
|
-
.string()
|
|
21
|
-
.min(1, "buildkiteQueue cannot be empty")
|
|
22
|
-
.max(100)
|
|
23
|
-
.regex(/^[a-zA-Z0-9-_]+$/, "buildkiteQueue must be alphanumeric with hyphens/underscores"),
|
|
24
|
-
/**
|
|
25
|
-
* Buildkite organisation slug (e.g. `fjall-tech`). The
|
|
26
|
-
* buildkite-agent-scaler publishes its CloudWatch metrics dimensioned by
|
|
27
|
-
* {Org, Queue} — the fleet's alarms must query the same pair or they read
|
|
28
|
-
* no data at all (heartbeat permanently ALARM, queued-with-zero-capacity
|
|
29
|
-
* permanently inert).
|
|
30
|
-
*/
|
|
31
|
-
buildkiteOrgSlug: z
|
|
32
|
-
.string()
|
|
33
|
-
.min(1, "buildkiteOrgSlug cannot be empty")
|
|
34
|
-
.max(100)
|
|
35
|
-
.regex(/^[a-z0-9-]+$/, "buildkiteOrgSlug must be a lowercase Buildkite organisation slug"),
|
|
36
|
-
/**
|
|
37
|
-
* Name of the pre-provisioned SSM SecureString holding the cluster-scoped
|
|
38
|
-
* agent token (e.g. `/Buildkite/agents/agent-token`). Provisioned
|
|
39
|
-
* out-of-band via `fjall secrets` tooling — the construct receives the
|
|
40
|
-
* identifier only; no secret value ever transits synth (design § D4).
|
|
41
|
-
*/
|
|
42
|
-
agentTokenSsmParameterName: z
|
|
43
|
-
.string()
|
|
44
|
-
.min(2)
|
|
45
|
-
.regex(/^\//, "agentTokenSsmParameterName must be a full path (leading /)"),
|
|
46
|
-
/**
|
|
47
|
-
* KMS key ARN encrypting the agent-token parameter, when a customer
|
|
48
|
-
* managed key is used. Omit for the AWS-managed `aws/ssm` key.
|
|
49
|
-
*/
|
|
50
|
-
agentTokenKmsKeyArn: z.string().min(1).optional(),
|
|
51
|
-
/**
|
|
52
|
-
* Name of the SSM SecureString holding FJALL_API_KEY. When set, the
|
|
53
|
-
* construct ships a per-job `env` hook into the managed secrets bucket
|
|
54
|
-
* that reads the parameter at job start (rotation takes effect on the
|
|
55
|
-
* next job, no instance replacement — design § D4(iii)) and grants the
|
|
56
|
-
* instance profile read on exactly this parameter.
|
|
57
|
-
*/
|
|
58
|
-
fjallApiKeySsmParameterName: z
|
|
59
|
-
.string()
|
|
60
|
-
.min(2)
|
|
61
|
-
.regex(/^\//, "fjallApiKeySsmParameterName must be a full path (leading /)")
|
|
62
|
-
.optional(),
|
|
63
|
-
/** EC2 instance type. Graviton default per design § D7. */
|
|
64
|
-
instanceType: z.string().min(1).default("c8g.xlarge"),
|
|
65
|
-
agentVolumeSizeGib: z.number().int().min(20).max(1000).default(250),
|
|
66
|
-
agentMinInstances: z.number().int().min(0).default(0),
|
|
67
|
-
agentMaxInstances: z.number().int().min(1).default(2),
|
|
68
|
-
agentsPerInstance: z.number().int().min(1).default(1),
|
|
69
|
-
/**
|
|
70
|
-
* Percentage of capacity on spot. Deploy fleets MUST stay 0 (on-demand
|
|
71
|
-
* only): no spot→on-demand fallback mechanism exists, and a reclaim
|
|
72
|
-
* mid-deploy lands in the rollback-wedge class (design § D7). Non-zero is
|
|
73
|
-
* for Phase-2 CI fleets running idempotent, auto-retried jobs.
|
|
74
|
-
*/
|
|
75
|
-
spotCapacityPercentage: z.number().int().min(0).max(100).default(0),
|
|
76
|
-
/** Agent-driven scale-in: idle seconds before an instance self-terminates. */
|
|
77
|
-
scaleInIdlePeriodSeconds: z.number().int().min(60).default(600),
|
|
78
|
-
/** Bounded instance staleness; also caps stale-token propagation (§ D5). */
|
|
79
|
-
disconnectAfterUptimeSeconds: z.number().int().min(3600).default(86_400),
|
|
80
|
-
maxInstanceLifetimeDays: z.number().int().min(1).max(365).default(7),
|
|
81
|
-
terminateInstanceAfterJob: z.boolean().default(false),
|
|
82
|
-
purgeBuildsOnDiskFull: z.boolean().default(true),
|
|
83
|
-
terminateInstanceOnDiskFull: z.boolean().default(false),
|
|
84
|
-
/** CloudWatch retention for the scaler's log group (days). */
|
|
85
|
-
logRetentionDays: z.number().int().min(1).default(30),
|
|
86
|
-
buildkiteAgentRelease: z.enum(["stable", "beta", "edge"]).default("stable"),
|
|
87
|
-
buildkiteAgentTags: z.string().default(""),
|
|
88
|
-
buildkiteAgentTimestampLines: z.boolean().default(false),
|
|
89
|
-
buildkiteAgentExperiments: z.string().default(""),
|
|
90
|
-
buildkiteAgentTracingBackend: z
|
|
91
|
-
.enum(["", "datadog", "opentelemetry"])
|
|
92
|
-
.default(""),
|
|
93
|
-
buildkiteAgentCancelGracePeriodSeconds: z
|
|
94
|
-
.number()
|
|
95
|
-
.int()
|
|
96
|
-
.min(10)
|
|
97
|
-
.default(60),
|
|
98
|
-
/** S3 secrets-hooks plugin — required for the FJALL_API_KEY env hook. */
|
|
99
|
-
enableSecretsPlugin: z.boolean().default(true),
|
|
100
|
-
/**
|
|
101
|
-
* ECR + docker-login plugins default OFF: the instance profile carries no
|
|
102
|
-
* registry credentials (design § D9); deploy jobs authenticate through
|
|
103
|
-
* server-minted credentials exactly as on hosted agents.
|
|
104
|
-
*/
|
|
105
|
-
enableEcrPlugin: z.boolean().default(false),
|
|
106
|
-
enableDockerLoginPlugin: z.boolean().default(false),
|
|
107
|
-
enableDockerUserNamespaceRemap: z.boolean().default(true),
|
|
108
|
-
enableDockerExperimental: z.boolean().default(false),
|
|
109
|
-
dockerNetworkingProtocol: z.enum(["ipv4", "dualstack"]).default("ipv4"),
|
|
110
|
-
enableInstanceStorage: z.boolean().default(false),
|
|
111
|
-
mountTmpfsAtTmp: z.boolean().default(true),
|
|
112
|
-
buildkiteAgentEnableGitMirrors: z.boolean().default(false),
|
|
113
|
-
bootstrapScriptUrl: z.string().default(""),
|
|
114
|
-
agentEnvFileUrl: z.string().default(""),
|
|
115
|
-
scalerEventSchedulePeriod: z.string().min(1).default("1 minute"),
|
|
116
|
-
scalerMinPollInterval: z.string().min(1).default("10s"),
|
|
117
|
-
scaleOutFactor: z.string().min(1).default("1.0"),
|
|
118
|
-
scaleOutWaitingForJobs: z.boolean().default(false),
|
|
119
|
-
/** IAM permissions boundary for the scaler's roles; emitted only when set. */
|
|
120
|
-
rolePermissionsBoundaryArn: z.string().min(1).optional(),
|
|
121
|
-
/**
|
|
122
|
-
* SNS topic receiving the fleet's two alarms (scaler heartbeat + queued
|
|
123
|
-
* with zero capacity — design § D14). Omit to create the alarms without
|
|
124
|
-
* actions (visible on the console, silent).
|
|
125
|
-
*/
|
|
126
|
-
alarmSnsTopicArn: z.string().min(1).optional(),
|
|
127
|
-
/**
|
|
128
|
-
* Fjall application the fleet belongs to — appended to alarm
|
|
129
|
-
* descriptions and tagged (`fjall:applicationId`) so the alarm webhook
|
|
130
|
-
* can map pages to the application. `App.addBuildkite` injects the app
|
|
131
|
-
* name automatically; set only to override.
|
|
132
|
-
*/
|
|
133
|
-
applicationId: z.string().min(1).optional(),
|
|
134
|
-
costAllocationEnvironment: z.string().min(1).optional(),
|
|
135
|
-
costAllocationOwner: z.string().min(1).optional()
|
|
136
|
-
})
|
|
137
|
-
.strict()
|
|
138
|
-
.refine((props) => props.agentMinInstances <= props.agentMaxInstances, {
|
|
139
|
-
message: "agentMinInstances must be <= agentMaxInstances"
|
|
140
|
-
});
|
|
141
|
-
/**
|
|
142
|
-
* Validate + default the plain-data props at construct time. Throws a
|
|
143
|
-
* synth-time error listing every violation — the pattern's constructor is the
|
|
144
|
-
* validation boundary, mirroring `ClickHouseDatabase`'s Stage-1 shape.
|
|
145
|
-
*/
|
|
146
|
-
export function validateBuildkiteProps(props) {
|
|
147
|
-
const result = BuildkitePropsSchema.safeParse(props);
|
|
148
|
-
if (!result.success) {
|
|
149
|
-
const details = result.error.issues
|
|
150
|
-
.map((issue) => ` - ${issue.path.join(".") || "(root)"}: ${issue.message}`)
|
|
151
|
-
.join("\n");
|
|
152
|
-
throw new Error(`Buildkite: invalid props:\n${details}`);
|
|
153
|
-
}
|
|
154
|
-
return result.data;
|
|
155
|
-
}
|
|
8
|
+
export { BuildkitePropsObjectSchema, BuildkitePropsSchema, validateBuildkiteProps } from "@fjall/generator";
|
|
@@ -42,7 +42,7 @@ export declare function buildBuildkiteDockerCommands(props: BuildkiteProps): str
|
|
|
42
42
|
* `bk-install-elastic-stack.sh`, mirroring the upstream block key-for-key in
|
|
43
43
|
* upstream order (so a fixture-bump diff reads line-by-line). Keys with no
|
|
44
44
|
* schema knob carry the upstream v6.68.1 parameter default verbatim, with
|
|
45
|
-
*
|
|
45
|
+
* four deliberate deviations:
|
|
46
46
|
*
|
|
47
47
|
* - `BUILDKITE_S3_ACL`: our artifact bucket is BucketOwnerEnforced (ACLs
|
|
48
48
|
* disabled), where S3 rejects every canned ACL EXCEPT
|
|
@@ -54,5 +54,16 @@ export declare function buildBuildkiteDockerCommands(props: BuildkiteProps): str
|
|
|
54
54
|
* is out of scope for the Phase-1 deploy fleet; a future signing knob must
|
|
55
55
|
* land WITH the kms:Sign/Verify/GetPublicKey grants it needs (the prior
|
|
56
56
|
* knob shipped without them — a dead toggle that broke agents when set).
|
|
57
|
+
* - `ENABLE_EC2_LOG_RETENTION_POLICY`: hard-pinned `true` (upstream default
|
|
58
|
+
* `false`, "preserve all logs"). Fjall's log-group lifecycle posture is
|
|
59
|
+
* bounded retention everywhere: each booting instance applies
|
|
60
|
+
* `agentLogRetentionDays` to the agent-created `/buildkite/*` groups via
|
|
61
|
+
* the instance role's `logs:PutRetentionPolicy` grant (which this flag
|
|
62
|
+
* makes live — it was stranded while `false`). Retro-applies to existing
|
|
63
|
+
* groups on the next boot after enabling, expiring events older than the
|
|
64
|
+
* window. Residual: runtime-created groups stay untagged — the namespace
|
|
65
|
+
* is account-global (shared across fleets and stacks), so CFN pre-creation
|
|
66
|
+
* would fight cross-stack ownership; cost exposure is bounded by the
|
|
67
|
+
* retention itself.
|
|
57
68
|
*/
|
|
58
69
|
export declare function buildBuildkiteInstallCommands(props: BuildkiteProps, context: BuildkiteUserDataContext): string[];
|
|
@@ -149,7 +149,7 @@ export function buildBuildkiteDockerCommands(props) {
|
|
|
149
149
|
* `bk-install-elastic-stack.sh`, mirroring the upstream block key-for-key in
|
|
150
150
|
* upstream order (so a fixture-bump diff reads line-by-line). Keys with no
|
|
151
151
|
* schema knob carry the upstream v6.68.1 parameter default verbatim, with
|
|
152
|
-
*
|
|
152
|
+
* four deliberate deviations:
|
|
153
153
|
*
|
|
154
154
|
* - `BUILDKITE_S3_ACL`: our artifact bucket is BucketOwnerEnforced (ACLs
|
|
155
155
|
* disabled), where S3 rejects every canned ACL EXCEPT
|
|
@@ -161,6 +161,17 @@ export function buildBuildkiteDockerCommands(props) {
|
|
|
161
161
|
* is out of scope for the Phase-1 deploy fleet; a future signing knob must
|
|
162
162
|
* land WITH the kms:Sign/Verify/GetPublicKey grants it needs (the prior
|
|
163
163
|
* knob shipped without them — a dead toggle that broke agents when set).
|
|
164
|
+
* - `ENABLE_EC2_LOG_RETENTION_POLICY`: hard-pinned `true` (upstream default
|
|
165
|
+
* `false`, "preserve all logs"). Fjall's log-group lifecycle posture is
|
|
166
|
+
* bounded retention everywhere: each booting instance applies
|
|
167
|
+
* `agentLogRetentionDays` to the agent-created `/buildkite/*` groups via
|
|
168
|
+
* the instance role's `logs:PutRetentionPolicy` grant (which this flag
|
|
169
|
+
* makes live — it was stranded while `false`). Retro-applies to existing
|
|
170
|
+
* groups on the next boot after enabling, expiring events older than the
|
|
171
|
+
* window. Residual: runtime-created groups stay untagged — the namespace
|
|
172
|
+
* is account-global (shared across fleets and stacks), so CFN pre-creation
|
|
173
|
+
* would fight cross-stack ownership; cost exposure is bounded by the
|
|
174
|
+
* retention itself.
|
|
164
175
|
*/
|
|
165
176
|
export function buildBuildkiteInstallCommands(props, context) {
|
|
166
177
|
return [
|
|
@@ -219,8 +230,8 @@ export function buildBuildkiteInstallCommands(props, context) {
|
|
|
219
230
|
"RESOURCE_LIMITS_CPU_WEIGHT='100' \\",
|
|
220
231
|
"RESOURCE_LIMITS_CPU_QUOTA='90%' \\",
|
|
221
232
|
"RESOURCE_LIMITS_IO_WEIGHT='80' \\",
|
|
222
|
-
"ENABLE_EC2_LOG_RETENTION_POLICY='
|
|
223
|
-
|
|
233
|
+
"ENABLE_EC2_LOG_RETENTION_POLICY='true' \\",
|
|
234
|
+
`EC2_LOG_RETENTION_DAYS='${props.agentLogRetentionDays}' \\`,
|
|
224
235
|
"/usr/local/bin/bk-install-elastic-stack.sh"
|
|
225
236
|
];
|
|
226
237
|
}
|
|
@@ -7,5 +7,6 @@
|
|
|
7
7
|
export { Buildkite, FJALL_ENV_HOOK_OBJECT_KEY, buildFjallApiKeyEnvHookScript, type BuildkiteConstructProps } from "./buildkite/buildkite.js";
|
|
8
8
|
export { BUILDKITE_AMI_OWNER_ACCOUNT_ID, BUILDKITE_CPU_ARCHITECTURES, BUILDKITE_STACK_PINS, resolvePinnedAmiId, resolveScalerSarApplicationArn, type BuildkiteCpuArchitecture } from "./buildkite/pins.js";
|
|
9
9
|
export { BuildkitePropsSchema, validateBuildkiteProps, type BuildkiteProps, type BuildkitePropsInput } from "./buildkite/schema.js";
|
|
10
|
+
export { BuildkiteFactory, type BuildkiteFactoryFn } from "./buildkite/factory.js";
|
|
10
11
|
export { BUILDKITE_AGENT_IAM_ACTION_ALLOWLIST, buildAgentRole, type BuildkiteAgentRoleParams } from "./buildkite/iam.js";
|
|
11
12
|
export { BUILDKITE_DOCKER_PART_ENV_KEYS, BUILDKITE_MOUNT_PART_ENV_KEYS, BUILDKITE_USER_DATA_ENV_KEYS, buildBuildkiteDockerCommands, buildBuildkiteInstallCommands, buildBuildkiteMountCommands, buildBuildkiteUserData, type BuildkiteUserDataContext } from "./buildkite/userData.js";
|
|
@@ -7,5 +7,6 @@
|
|
|
7
7
|
export { Buildkite, FJALL_ENV_HOOK_OBJECT_KEY, buildFjallApiKeyEnvHookScript } from "./buildkite/buildkite.js";
|
|
8
8
|
export { BUILDKITE_AMI_OWNER_ACCOUNT_ID, BUILDKITE_CPU_ARCHITECTURES, BUILDKITE_STACK_PINS, resolvePinnedAmiId, resolveScalerSarApplicationArn } from "./buildkite/pins.js";
|
|
9
9
|
export { BuildkitePropsSchema, validateBuildkiteProps } from "./buildkite/schema.js";
|
|
10
|
+
export { BuildkiteFactory } from "./buildkite/factory.js";
|
|
10
11
|
export { BUILDKITE_AGENT_IAM_ACTION_ALLOWLIST, buildAgentRole } from "./buildkite/iam.js";
|
|
11
12
|
export { BUILDKITE_DOCKER_PART_ENV_KEYS, BUILDKITE_MOUNT_PART_ENV_KEYS, BUILDKITE_USER_DATA_ENV_KEYS, buildBuildkiteDockerCommands, buildBuildkiteInstallCommands, buildBuildkiteMountCommands, buildBuildkiteUserData } from "./buildkite/userData.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.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": "^3.
|
|
71
|
-
"@fjall/util": "^3.
|
|
70
|
+
"@fjall/generator": "^3.3.0",
|
|
71
|
+
"@fjall/util": "^3.3.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": "
|
|
85
|
+
"gitHead": "cc1e0f520f4b6b5312e16df3751d60ea746dfb7d"
|
|
86
86
|
}
|