@fjall/deploy-core 0.89.2
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/LICENSE +21 -0
- package/dist/src/aws/AwsProvider.d.ts +39 -0
- package/dist/src/aws/AwsProvider.js +1 -0
- package/dist/src/aws/SimpleAwsProvider.d.ts +22 -0
- package/dist/src/aws/SimpleAwsProvider.js +73 -0
- package/dist/src/aws/index.d.ts +4 -0
- package/dist/src/aws/index.js +3 -0
- package/dist/src/aws/organisations/accounts.d.ts +21 -0
- package/dist/src/aws/organisations/accounts.js +99 -0
- package/dist/src/aws/organisations/backup.d.ts +12 -0
- package/dist/src/aws/organisations/backup.js +28 -0
- package/dist/src/aws/organisations/costAllocation.d.ts +12 -0
- package/dist/src/aws/organisations/costAllocation.js +26 -0
- package/dist/src/aws/organisations/identityCentre.d.ts +8 -0
- package/dist/src/aws/organisations/identityCentre.js +19 -0
- package/dist/src/aws/organisations/index.d.ts +16 -0
- package/dist/src/aws/organisations/index.js +12 -0
- package/dist/src/aws/organisations/ipam.d.ts +7 -0
- package/dist/src/aws/organisations/ipam.js +18 -0
- package/dist/src/aws/organisations/organisation.d.ts +12 -0
- package/dist/src/aws/organisations/organisation.js +94 -0
- package/dist/src/aws/organisations/organisationalUnits.d.ts +19 -0
- package/dist/src/aws/organisations/organisationalUnits.js +125 -0
- package/dist/src/aws/organisations/policies.d.ts +7 -0
- package/dist/src/aws/organisations/policies.js +36 -0
- package/dist/src/aws/organisations/ram.d.ts +7 -0
- package/dist/src/aws/organisations/ram.js +15 -0
- package/dist/src/aws/organisations/serviceAccess.d.ts +7 -0
- package/dist/src/aws/organisations/serviceAccess.js +38 -0
- package/dist/src/aws/organisations/trustedAccess.d.ts +7 -0
- package/dist/src/aws/organisations/trustedAccess.js +15 -0
- package/dist/src/aws/organisations/types.d.ts +29 -0
- package/dist/src/aws/organisations/types.js +16 -0
- package/dist/src/aws/utils/CloudFormationFailureAnalyser.d.ts +32 -0
- package/dist/src/aws/utils/CloudFormationFailureAnalyser.js +228 -0
- package/dist/src/aws/utils/cloudformationEvents.d.ts +98 -0
- package/dist/src/aws/utils/cloudformationEvents.js +596 -0
- package/dist/src/aws/utils/errors.d.ts +26 -0
- package/dist/src/aws/utils/errors.js +59 -0
- package/dist/src/aws/utils/regions.d.ts +1 -0
- package/dist/src/aws/utils/regions.js +1 -0
- package/dist/src/aws/utils/stackStatus.d.ts +23 -0
- package/dist/src/aws/utils/stackStatus.js +90 -0
- package/dist/src/index.d.ts +35 -0
- package/dist/src/index.js +45 -0
- package/dist/src/orchestration/applicationDeploy.d.ts +11 -0
- package/dist/src/orchestration/applicationDeploy.js +327 -0
- package/dist/src/orchestration/contextHelpers.d.ts +9 -0
- package/dist/src/orchestration/contextHelpers.js +14 -0
- package/dist/src/orchestration/deploy.d.ts +10 -0
- package/dist/src/orchestration/deploy.js +42 -0
- package/dist/src/orchestration/detectionPipeline.d.ts +23 -0
- package/dist/src/orchestration/detectionPipeline.js +65 -0
- package/dist/src/orchestration/dockerInterface.d.ts +56 -0
- package/dist/src/orchestration/dockerInterface.js +1 -0
- package/dist/src/orchestration/domainInterface.d.ts +37 -0
- package/dist/src/orchestration/domainInterface.js +1 -0
- package/dist/src/orchestration/index.d.ts +8 -0
- package/dist/src/orchestration/index.js +3 -0
- package/dist/src/orchestration/organisationDeploy.d.ts +16 -0
- package/dist/src/orchestration/organisationDeploy.js +382 -0
- package/dist/src/orchestration/organisationSetup.d.ts +42 -0
- package/dist/src/orchestration/organisationSetup.js +227 -0
- package/dist/src/orchestration/resolveOperation.d.ts +10 -0
- package/dist/src/orchestration/resolveOperation.js +53 -0
- package/dist/src/orchestration/serviceFactory.d.ts +15 -0
- package/dist/src/orchestration/serviceFactory.js +16 -0
- package/dist/src/services/application/ApplicationStackService.d.ts +93 -0
- package/dist/src/services/application/ApplicationStackService.js +436 -0
- package/dist/src/services/application/index.d.ts +1 -0
- package/dist/src/services/application/index.js +1 -0
- package/dist/src/services/infrastructure/CdkArgumentBuilder.d.ts +12 -0
- package/dist/src/services/infrastructure/CdkArgumentBuilder.js +67 -0
- package/dist/src/services/infrastructure/CdkCommandRunner.d.ts +30 -0
- package/dist/src/services/infrastructure/CdkCommandRunner.js +241 -0
- package/dist/src/services/infrastructure/CdkErrorFormatter.d.ts +4 -0
- package/dist/src/services/infrastructure/CdkErrorFormatter.js +194 -0
- package/dist/src/services/infrastructure/CdkEventMonitoring.d.ts +19 -0
- package/dist/src/services/infrastructure/CdkEventMonitoring.js +41 -0
- package/dist/src/services/infrastructure/CdkOutputAnalyser.d.ts +43 -0
- package/dist/src/services/infrastructure/CdkOutputAnalyser.js +125 -0
- package/dist/src/services/infrastructure/CdkOutputParser.d.ts +8 -0
- package/dist/src/services/infrastructure/CdkOutputParser.js +33 -0
- package/dist/src/services/infrastructure/CdkProcessManager.d.ts +20 -0
- package/dist/src/services/infrastructure/CdkProcessManager.js +244 -0
- package/dist/src/services/infrastructure/CdkService.d.ts +71 -0
- package/dist/src/services/infrastructure/CdkService.js +254 -0
- package/dist/src/services/infrastructure/CloudFormationService.d.ts +79 -0
- package/dist/src/services/infrastructure/CloudFormationService.js +249 -0
- package/dist/src/services/infrastructure/index.d.ts +8 -0
- package/dist/src/services/infrastructure/index.js +7 -0
- package/dist/src/services/supporting/CdkContextBuilder.d.ts +49 -0
- package/dist/src/services/supporting/CdkContextBuilder.js +44 -0
- package/dist/src/services/supporting/TemplateHashService.d.ts +67 -0
- package/dist/src/services/supporting/TemplateHashService.js +152 -0
- package/dist/src/services/supporting/helpers.d.ts +46 -0
- package/dist/src/services/supporting/helpers.js +81 -0
- package/dist/src/services/supporting/index.d.ts +3 -0
- package/dist/src/services/supporting/index.js +3 -0
- package/dist/src/types/FjallState.d.ts +50 -0
- package/dist/src/types/FjallState.js +118 -0
- package/dist/src/types/ProgressEvent.d.ts +35 -0
- package/dist/src/types/ProgressEvent.js +48 -0
- package/dist/src/types/apiClient.d.ts +34 -0
- package/dist/src/types/apiClient.js +1 -0
- package/dist/src/types/application/ApplicationServiceTypes.d.ts +56 -0
- package/dist/src/types/application/ApplicationServiceTypes.js +30 -0
- package/dist/src/types/application/index.d.ts +1 -0
- package/dist/src/types/application/index.js +1 -0
- package/dist/src/types/callbacks.d.ts +36 -0
- package/dist/src/types/callbacks.js +1 -0
- package/dist/src/types/constants.d.ts +6 -0
- package/dist/src/types/constants.js +6 -0
- package/dist/src/types/credentials.d.ts +30 -0
- package/dist/src/types/credentials.js +1 -0
- package/dist/src/types/deployment/DeploymentServiceTypes.d.ts +23 -0
- package/dist/src/types/deployment/DeploymentServiceTypes.js +1 -0
- package/dist/src/types/deployment/DeploymentTypes.d.ts +29 -0
- package/dist/src/types/deployment/DeploymentTypes.js +1 -0
- package/dist/src/types/deployment/cloudformation.d.ts +14 -0
- package/dist/src/types/deployment/cloudformation.js +1 -0
- package/dist/src/types/deployment/index.d.ts +5 -0
- package/dist/src/types/deployment/index.js +1 -0
- package/dist/src/types/deployment/parallel.d.ts +46 -0
- package/dist/src/types/deployment/parallel.js +10 -0
- package/dist/src/types/errors/CdkError.d.ts +14 -0
- package/dist/src/types/errors/CdkError.js +20 -0
- package/dist/src/types/errors/ServiceError.d.ts +86 -0
- package/dist/src/types/errors/ServiceError.js +119 -0
- package/dist/src/types/events.d.ts +40 -0
- package/dist/src/types/events.js +5 -0
- package/dist/src/types/index.d.ts +20 -0
- package/dist/src/types/index.js +9 -0
- package/dist/src/types/operations.d.ts +193 -0
- package/dist/src/types/operations.js +285 -0
- package/dist/src/types/orgConfig.d.ts +28 -0
- package/dist/src/types/orgConfig.js +11 -0
- package/dist/src/types/params.d.ts +74 -0
- package/dist/src/types/params.js +1 -0
- package/dist/src/types/patternDetection.d.ts +43 -0
- package/dist/src/types/patternDetection.js +92 -0
- package/dist/src/types/validation.d.ts +12 -0
- package/dist/src/types/validation.js +1 -0
- package/dist/src/util/fsHelpers.d.ts +4 -0
- package/dist/src/util/fsHelpers.js +16 -0
- package/dist/src/util/index.d.ts +3 -0
- package/dist/src/util/index.js +3 -0
- package/dist/src/util/securityHelpers.d.ts +31 -0
- package/dist/src/util/securityHelpers.js +124 -0
- package/dist/src/util/singleton.d.ts +2 -0
- package/dist/src/util/singleton.js +9 -0
- package/dist/src/util/sleep.d.ts +4 -0
- package/dist/src/util/sleep.js +4 -0
- package/package.json +42 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { success, failure } from "@fjall/generator";
|
|
3
|
+
import { logger } from "@fjall/util";
|
|
4
|
+
import { detectPattern, deriveResourcesFromManifestStacks } from "../types/patternDetection.js";
|
|
5
|
+
import { emitProgress, PROGRESS_MESSAGES } from "../services/supporting/helpers.js";
|
|
6
|
+
/**
|
|
7
|
+
* Pre-deployment analysis pipeline.
|
|
8
|
+
*
|
|
9
|
+
* Detects the application pattern, synthesises infrastructure, computes
|
|
10
|
+
* template hashes, and determines which stacks have changed.
|
|
11
|
+
*/
|
|
12
|
+
export async function runDetectionPipeline(operation, services, context, callbacks) {
|
|
13
|
+
// 1. Detect application pattern
|
|
14
|
+
const { pattern, hasDatabase } = detectPattern(operation.path);
|
|
15
|
+
callbacks.onLog?.(`Pattern detected: ${pattern ?? "standard"}`, "info");
|
|
16
|
+
// 2. Synthesise infrastructure
|
|
17
|
+
emitProgress(callbacks, PROGRESS_MESSAGES.SYNTH);
|
|
18
|
+
const synthResult = await services.cdkService.runCdkSynth(context, (chunk) => callbacks.onCdkOutput?.(chunk, "synth"));
|
|
19
|
+
if (!synthResult.success) {
|
|
20
|
+
return failure(new Error(`CDK synthesis failed: ${synthResult.error}`));
|
|
21
|
+
}
|
|
22
|
+
// 3. Compute template hashes
|
|
23
|
+
emitProgress(callbacks, PROGRESS_MESSAGES.HASH);
|
|
24
|
+
const cdkOutPath = join(operation.path, "cdk.out");
|
|
25
|
+
const hashResult = await services.hashService.getTemplateHashes(cdkOutPath);
|
|
26
|
+
if (!hashResult.success) {
|
|
27
|
+
return failure(new Error(`Template hash computation failed: ${hashResult.error.message}`));
|
|
28
|
+
}
|
|
29
|
+
const currentHashes = hashResult.data;
|
|
30
|
+
// 4. Compare with stored state
|
|
31
|
+
const comparisonResult = await services.hashService.compareWithState(currentHashes, operation.path);
|
|
32
|
+
if (!comparisonResult.success) {
|
|
33
|
+
return failure(new Error(`Hash comparison failed: ${comparisonResult.error.message}`));
|
|
34
|
+
}
|
|
35
|
+
const comparison = comparisonResult.data;
|
|
36
|
+
// 5. Stale hash detection: unchanged templates whose stacks don't exist in CFN
|
|
37
|
+
const stackChanges = new Map(comparison.stackChanges);
|
|
38
|
+
for (const [stackName, hasChanged] of comparison.stackChanges) {
|
|
39
|
+
if (!hasChanged) {
|
|
40
|
+
const exists = await services.cfnService.stackExists(stackName);
|
|
41
|
+
if (!exists) {
|
|
42
|
+
logger.debug("detectionPipeline", "Stale hash detected — stack missing in CFN", {
|
|
43
|
+
stackName
|
|
44
|
+
});
|
|
45
|
+
stackChanges.set(stackName, true);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// 6. Derive resource flags from manifest stacks
|
|
50
|
+
const stackNames = Array.from(currentHashes.keys());
|
|
51
|
+
const resources = deriveResourcesFromManifestStacks(stackNames);
|
|
52
|
+
// 7. Detect Dockerfile (Compute stack presence implies Docker)
|
|
53
|
+
const hasDockerfile = resources.hasCompute;
|
|
54
|
+
const hasDifferences = Array.from(stackChanges.values()).some(Boolean);
|
|
55
|
+
callbacks.onLog?.(`Detection complete: ${comparison.changedCount} changed, ${comparison.unchangedCount} unchanged`, "info");
|
|
56
|
+
return success({
|
|
57
|
+
pattern,
|
|
58
|
+
hasDatabase,
|
|
59
|
+
hasDifferences,
|
|
60
|
+
stackChanges,
|
|
61
|
+
currentHashes,
|
|
62
|
+
resources,
|
|
63
|
+
hasDockerfile
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Result } from "@fjall/generator";
|
|
2
|
+
export interface DockerServiceConfig {
|
|
3
|
+
name: string;
|
|
4
|
+
dockerfilePath: string;
|
|
5
|
+
dockerTarget?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface DockerBuildParams {
|
|
8
|
+
appName: string;
|
|
9
|
+
appPath: string;
|
|
10
|
+
region: string;
|
|
11
|
+
accountId: string;
|
|
12
|
+
imageTag?: string;
|
|
13
|
+
services?: DockerServiceConfig[];
|
|
14
|
+
platform?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface DockerBuildResult {
|
|
17
|
+
imageUri: string;
|
|
18
|
+
imageTag: string;
|
|
19
|
+
services?: Array<{
|
|
20
|
+
name: string;
|
|
21
|
+
imageUri: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
export interface ECRInitParams {
|
|
25
|
+
appName: string;
|
|
26
|
+
region: string;
|
|
27
|
+
accountId: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ECRInitResult {
|
|
30
|
+
repositoryUri: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Parameters for tagging existing ECR images for ECS services.
|
|
34
|
+
* Used for non-Dockerfile apps where CodeBuild pushes :latest but
|
|
35
|
+
* ECS expects :<serviceName>-latest tags.
|
|
36
|
+
*/
|
|
37
|
+
export interface TagImagesParams {
|
|
38
|
+
appName: string;
|
|
39
|
+
appPath: string;
|
|
40
|
+
region: string;
|
|
41
|
+
accountId: string;
|
|
42
|
+
}
|
|
43
|
+
export interface TagImagesResult {
|
|
44
|
+
taggedServices: string[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Interface for Docker operations (build, push, ECR initialisation).
|
|
48
|
+
* CLI provides CliDockerProvider (wraps dockerode-based services).
|
|
49
|
+
* Worker passes undefined (Docker ops skipped until container deployments enabled).
|
|
50
|
+
*/
|
|
51
|
+
export interface DockerProvider {
|
|
52
|
+
buildAndPush(params: DockerBuildParams): Promise<Result<DockerBuildResult>>;
|
|
53
|
+
initialiseECR(params: ECRInitParams): Promise<Result<ECRInitResult>>;
|
|
54
|
+
/** Tag existing ECR images for ECS services (non-Dockerfile apps). Optional. */
|
|
55
|
+
tagImages?(params: TagImagesParams): Promise<Result<TagImagesResult>>;
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Result } from "@fjall/generator";
|
|
2
|
+
import type { DeployCallbacks } from "../types/callbacks.js";
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for a single domain to deploy.
|
|
5
|
+
*/
|
|
6
|
+
export interface DomainConfig {
|
|
7
|
+
name: string;
|
|
8
|
+
type: "apex" | "delegated";
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Result of a domain deployment phase.
|
|
12
|
+
*/
|
|
13
|
+
export interface DomainDeployResult {
|
|
14
|
+
domainsDeployed: number;
|
|
15
|
+
errors: string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Provider interface for domain deployment operations.
|
|
19
|
+
*
|
|
20
|
+
* Domain deployment requires CLI-specific services (zone file parsing, CDK
|
|
21
|
+
* synthesis in domain component directories). deploy-core defines the cascade
|
|
22
|
+
* structure; the caller provides the implementation.
|
|
23
|
+
*
|
|
24
|
+
* If no provider is given, the domains phase is skipped.
|
|
25
|
+
*/
|
|
26
|
+
export interface DomainDeployProvider {
|
|
27
|
+
/**
|
|
28
|
+
* Get the list of configured domains for this organisation.
|
|
29
|
+
* Returns an empty array if no domains are configured.
|
|
30
|
+
*/
|
|
31
|
+
getDomains(): DomainConfig[];
|
|
32
|
+
/**
|
|
33
|
+
* Deploy a single domain. Apex domains create hosted zones and delegation
|
|
34
|
+
* roles; delegated domains create NS records in the parent zone.
|
|
35
|
+
*/
|
|
36
|
+
deployDomain(domainName: string, callbacks: DeployCallbacks): Promise<Result<void>>;
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { deploy } from "./deploy.js";
|
|
2
|
+
export { deployOrganisation } from "./organisationDeploy.js";
|
|
3
|
+
export type { DockerProvider, DockerServiceConfig, DockerBuildParams, DockerBuildResult, ECRInitParams, ECRInitResult, TagImagesParams, TagImagesResult } from "./dockerInterface.js";
|
|
4
|
+
export type { DomainDeployProvider, DomainConfig, DomainDeployResult } from "./domainInterface.js";
|
|
5
|
+
export type { DeployServices } from "./serviceFactory.js";
|
|
6
|
+
export type { DetectionResult } from "./detectionPipeline.js";
|
|
7
|
+
export { runOrganisationSetup } from "./organisationSetup.js";
|
|
8
|
+
export type { OrgSetupPhase, OrgSetupCallbacks, OrgSetupConfig, OrgSetupResult } from "./organisationSetup.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Result } from "@fjall/generator";
|
|
2
|
+
import type { DeployParams, DeployResult } from "../types/params.js";
|
|
3
|
+
import type { OrganisationOperation } from "../types/operations.js";
|
|
4
|
+
import type { DeployServices } from "./serviceFactory.js";
|
|
5
|
+
/**
|
|
6
|
+
* Organisation deployment orchestration.
|
|
7
|
+
*
|
|
8
|
+
* Handles three target types:
|
|
9
|
+
* - organisation: deploy org infra + cascade to platform + all accounts
|
|
10
|
+
* - platform: deploy platform stack only
|
|
11
|
+
* - account: deploy single account stack
|
|
12
|
+
*
|
|
13
|
+
* Auth and org setup (creating AWS accounts/OUs) are the caller's
|
|
14
|
+
* responsibility. deploy-core receives credentials and deploys.
|
|
15
|
+
*/
|
|
16
|
+
export declare function deployOrganisation(params: DeployParams, services: DeployServices, operation: OrganisationOperation): Promise<Result<DeployResult>>;
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { success, failure } from "@fjall/generator";
|
|
2
|
+
import { logger } from "@fjall/util";
|
|
3
|
+
import { ORGANISATION_TYPES, getOrganisationStackName } from "../types/operations.js";
|
|
4
|
+
import { CdkContextBuilder } from "../services/supporting/CdkContextBuilder.js";
|
|
5
|
+
import { CloudFormationService } from "../services/infrastructure/CloudFormationService.js";
|
|
6
|
+
import { SimpleAwsProvider } from "../aws/SimpleAwsProvider.js";
|
|
7
|
+
import { buildParamsContext } from "./contextHelpers.js";
|
|
8
|
+
/**
|
|
9
|
+
* Organisation deployment orchestration.
|
|
10
|
+
*
|
|
11
|
+
* Handles three target types:
|
|
12
|
+
* - organisation: deploy org infra + cascade to platform + all accounts
|
|
13
|
+
* - platform: deploy platform stack only
|
|
14
|
+
* - account: deploy single account stack
|
|
15
|
+
*
|
|
16
|
+
* Auth and org setup (creating AWS accounts/OUs) are the caller's
|
|
17
|
+
* responsibility. deploy-core receives credentials and deploys.
|
|
18
|
+
*/
|
|
19
|
+
export async function deployOrganisation(params, services, operation) {
|
|
20
|
+
const startTime = Date.now();
|
|
21
|
+
switch (operation.type) {
|
|
22
|
+
case ORGANISATION_TYPES.ORGANISATION:
|
|
23
|
+
return deployOrgWithCascade(params, services, operation, startTime);
|
|
24
|
+
case ORGANISATION_TYPES.PLATFORM:
|
|
25
|
+
return deploySingleComponent(params, services, operation, "platform", startTime);
|
|
26
|
+
case ORGANISATION_TYPES.ACCOUNT:
|
|
27
|
+
return deploySingleComponent(params, services, operation, "account", startTime);
|
|
28
|
+
default: {
|
|
29
|
+
const _exhaustive = operation.type;
|
|
30
|
+
return failure(new Error(`Unsupported organisation type: ${String(_exhaustive)}`));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Build a deployment context for an organisation component.
|
|
36
|
+
*/
|
|
37
|
+
function buildOrgContext(params, services, operation, deployType, accountName) {
|
|
38
|
+
return CdkContextBuilder.buildDeploymentContext({
|
|
39
|
+
deployType,
|
|
40
|
+
target: operation.target,
|
|
41
|
+
path: operation.path,
|
|
42
|
+
region: services.awsProvider.getRegion(),
|
|
43
|
+
accountName,
|
|
44
|
+
callerIdentity: {
|
|
45
|
+
Account: services.awsProvider.getAccountId() ?? "",
|
|
46
|
+
Arn: "",
|
|
47
|
+
UserId: ""
|
|
48
|
+
},
|
|
49
|
+
...buildParamsContext(params)
|
|
50
|
+
}, {
|
|
51
|
+
verbose: params.options?.verbose,
|
|
52
|
+
infraOnly: params.options?.infraOnly
|
|
53
|
+
}, params.orgConfig);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Deploy a single organisation component (platform or account).
|
|
57
|
+
*/
|
|
58
|
+
async function deploySingleComponent(params, services, operation, deployType, startTime) {
|
|
59
|
+
const { callbacks } = params;
|
|
60
|
+
const context = buildOrgContext(params, services, operation, deployType, deployType === "account" ? operation.target : undefined);
|
|
61
|
+
// Synth
|
|
62
|
+
callbacks.onLog?.(`Synthesising ${deployType} infrastructure…`, "info");
|
|
63
|
+
const synthResult = await services.cdkService.runCdkSynth(context, (chunk) => callbacks.onCdkOutput?.(chunk, "synth"));
|
|
64
|
+
if (!synthResult.success) {
|
|
65
|
+
const error = new Error(`CDK synthesis failed: ${synthResult.error}`);
|
|
66
|
+
callbacks.onError?.(error);
|
|
67
|
+
return failure(error);
|
|
68
|
+
}
|
|
69
|
+
// Bootstrap
|
|
70
|
+
callbacks.onCDKBootstrap?.("bootstrapping");
|
|
71
|
+
const bootstrapResult = await services.cdkService.runCdkBootstrap(context, (chunk) => callbacks.onOutput?.(chunk));
|
|
72
|
+
if (!bootstrapResult.success) {
|
|
73
|
+
callbacks.onCDKBootstrap?.("failed");
|
|
74
|
+
const error = new Error(`Bootstrap failed: ${bootstrapResult.error}`);
|
|
75
|
+
callbacks.onError?.(error);
|
|
76
|
+
return failure(error);
|
|
77
|
+
}
|
|
78
|
+
callbacks.onCDKBootstrap?.("complete");
|
|
79
|
+
// Deploy
|
|
80
|
+
const stackName = getOrganisationStackName(operation.type);
|
|
81
|
+
const stepId = `${deployType}-deploy`;
|
|
82
|
+
const stepName = `Deploying ${deployType} infrastructure`;
|
|
83
|
+
callbacks.onStepStart?.(stepId, stepName, 0, 1);
|
|
84
|
+
const deployResult = await services.cdkService.runCdkDeploy(context, stackName, (chunk) => callbacks.onOutput?.(chunk), (event) => callbacks.onResourceProgress?.(event), services.awsProvider);
|
|
85
|
+
if (!deployResult.success) {
|
|
86
|
+
callbacks.onStepComplete?.(stepId, stepName, "error", 0, 1);
|
|
87
|
+
const error = new Error(deployResult.error);
|
|
88
|
+
callbacks.onError?.(error);
|
|
89
|
+
return failure(error);
|
|
90
|
+
}
|
|
91
|
+
callbacks.onStepComplete?.(stepId, stepName, "completed", 0, 1);
|
|
92
|
+
return success({
|
|
93
|
+
target: operation.target,
|
|
94
|
+
deploymentType: "organisation",
|
|
95
|
+
durationMs: Date.now() - startTime
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Full organisation deployment with cascade to platform + member accounts.
|
|
100
|
+
*/
|
|
101
|
+
async function deployOrgWithCascade(params, services, operation, startTime) {
|
|
102
|
+
const { callbacks, options } = params;
|
|
103
|
+
const providerAccounts = params.orgConfig?.providerAccounts ?? [];
|
|
104
|
+
const context = buildOrgContext(params, services, operation, "organisation");
|
|
105
|
+
// Synth
|
|
106
|
+
callbacks.onLog?.("Synthesising organisation infrastructure…", "info");
|
|
107
|
+
const synthResult = await services.cdkService.runCdkSynth(context, (chunk) => callbacks.onCdkOutput?.(chunk, "synth"));
|
|
108
|
+
if (!synthResult.success) {
|
|
109
|
+
const error = new Error(`CDK synthesis failed: ${synthResult.error}`);
|
|
110
|
+
callbacks.onError?.(error);
|
|
111
|
+
return failure(error);
|
|
112
|
+
}
|
|
113
|
+
// Bootstrap org account
|
|
114
|
+
callbacks.onCDKBootstrap?.("bootstrapping");
|
|
115
|
+
const bootstrapResult = await services.cdkService.runCdkBootstrap(context, (chunk) => callbacks.onOutput?.(chunk));
|
|
116
|
+
if (!bootstrapResult.success) {
|
|
117
|
+
callbacks.onCDKBootstrap?.("failed");
|
|
118
|
+
const error = new Error(`Bootstrap failed: ${bootstrapResult.error}`);
|
|
119
|
+
callbacks.onError?.(error);
|
|
120
|
+
return failure(error);
|
|
121
|
+
}
|
|
122
|
+
callbacks.onCDKBootstrap?.("complete");
|
|
123
|
+
// Deploy org infrastructure
|
|
124
|
+
const orgStepId = "organisation-deploy";
|
|
125
|
+
const orgStepName = "Deploying organisation infrastructure";
|
|
126
|
+
const cascadeEnabled = options?.cascade !== false;
|
|
127
|
+
const cascadeAccountCount = cascadeEnabled ? providerAccounts.length : 0;
|
|
128
|
+
const totalSteps = 1 + cascadeAccountCount;
|
|
129
|
+
callbacks.onStepStart?.(orgStepId, orgStepName, 0, totalSteps);
|
|
130
|
+
const orgStackName = getOrganisationStackName(ORGANISATION_TYPES.ORGANISATION);
|
|
131
|
+
const orgResult = await services.cdkService.runCdkDeploy(context, orgStackName, (chunk) => callbacks.onOutput?.(chunk), (event) => callbacks.onResourceProgress?.(event), services.awsProvider);
|
|
132
|
+
if (!orgResult.success) {
|
|
133
|
+
callbacks.onStepComplete?.(orgStepId, orgStepName, "error", 0, totalSteps);
|
|
134
|
+
const error = new Error(orgResult.error);
|
|
135
|
+
callbacks.onError?.(error);
|
|
136
|
+
return failure(error);
|
|
137
|
+
}
|
|
138
|
+
callbacks.onStepComplete?.(orgStepId, orgStepName, "completed", 0, totalSteps);
|
|
139
|
+
// Cascade to platform + domains + member accounts
|
|
140
|
+
if (cascadeEnabled && providerAccounts.length > 0) {
|
|
141
|
+
callbacks.onCascadeStart?.();
|
|
142
|
+
const cascadeErrors = [];
|
|
143
|
+
let accountsDeployed = 0;
|
|
144
|
+
let platformDeployed = false;
|
|
145
|
+
let domainsDeployed = false;
|
|
146
|
+
// Phase 1: Deploy platform account
|
|
147
|
+
const platformAccount = providerAccounts.find((acc) => acc.environment === "platform");
|
|
148
|
+
if (platformAccount) {
|
|
149
|
+
callbacks.onCascadePhaseStart?.("platform");
|
|
150
|
+
const platformResult = await deployCascadeAccount(params, services, operation, platformAccount, "platform", callbacks);
|
|
151
|
+
if (platformResult.success) {
|
|
152
|
+
platformDeployed = true;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
cascadeErrors.push({
|
|
156
|
+
accountId: platformAccount.id,
|
|
157
|
+
error: platformResult.error.message
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Phase 1.5: Read Platform stack outputs for IPAM pool IDs
|
|
162
|
+
let ipamPoolIds = new Map();
|
|
163
|
+
if (platformDeployed && platformAccount) {
|
|
164
|
+
ipamPoolIds = await readPlatformIpamPoolIds(services, platformAccount, callbacks);
|
|
165
|
+
}
|
|
166
|
+
// Phase 2: Deploy domains (apex sequential, delegated parallel)
|
|
167
|
+
if (params.domainProvider) {
|
|
168
|
+
const domainResult = await deployDomains(params.domainProvider, callbacks);
|
|
169
|
+
domainsDeployed = domainResult.domainsDeployed > 0;
|
|
170
|
+
for (const err of domainResult.errors) {
|
|
171
|
+
cascadeErrors.push({ accountId: "domains", error: err });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Phase 3: Deploy member accounts in parallel
|
|
175
|
+
const memberAccounts = providerAccounts.filter((acc) => acc.environment !== "root" && acc.environment !== "platform");
|
|
176
|
+
if (memberAccounts.length > 0) {
|
|
177
|
+
callbacks.onCascadePhaseStart?.("accounts");
|
|
178
|
+
const region = services.awsProvider.getRegion();
|
|
179
|
+
const memberResults = await Promise.all(memberAccounts.map((account) => {
|
|
180
|
+
const regionSuffix = region.replace(/-/g, "");
|
|
181
|
+
const ipamPoolId = ipamPoolIds.get(`${account.id}-${regionSuffix}`);
|
|
182
|
+
return deployCascadeAccount(params, services, operation, account, "account", callbacks, ipamPoolId);
|
|
183
|
+
}));
|
|
184
|
+
for (let j = 0; j < memberResults.length; j++) {
|
|
185
|
+
const result = memberResults[j];
|
|
186
|
+
const account = memberAccounts[j];
|
|
187
|
+
if (result.success) {
|
|
188
|
+
accountsDeployed++;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
cascadeErrors.push({
|
|
192
|
+
accountId: account.id,
|
|
193
|
+
error: result.error.message
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
callbacks.onCascadeComplete?.({
|
|
199
|
+
platformDeployed,
|
|
200
|
+
domainsDeployed,
|
|
201
|
+
accountsDeployed,
|
|
202
|
+
accountsFailed: cascadeErrors.length,
|
|
203
|
+
errors: cascadeErrors
|
|
204
|
+
});
|
|
205
|
+
if (cascadeErrors.length > 0) {
|
|
206
|
+
const errorSummary = cascadeErrors
|
|
207
|
+
.map((e) => ` ${e.accountId}: ${e.error}`)
|
|
208
|
+
.join("\n");
|
|
209
|
+
callbacks.onLog?.(`Cascade completed with ${cascadeErrors.length} failure(s):\n${errorSummary}`, "warn");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return success({
|
|
213
|
+
target: operation.target,
|
|
214
|
+
deploymentType: "organisation",
|
|
215
|
+
durationMs: Date.now() - startTime
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Deploy a single cascade account (platform or member).
|
|
220
|
+
* Assumes the target account's role, sets env credentials, and deploys.
|
|
221
|
+
*/
|
|
222
|
+
async function deployCascadeAccount(params, services, operation, account, deployType, callbacks, ipamPoolId) {
|
|
223
|
+
const operationKey = `${account.name}-${account.id}`;
|
|
224
|
+
const region = services.awsProvider.getRegion();
|
|
225
|
+
callbacks.onCascadeAccountStart?.(operationKey, account.id, region);
|
|
226
|
+
// Assume role in target account
|
|
227
|
+
if (!services.awsProvider.assumeRole) {
|
|
228
|
+
const error = new Error(`Cannot cascade to account ${account.name}: AwsProvider does not support assumeRole`);
|
|
229
|
+
callbacks.onCascadeAccountComplete?.(operationKey, false, error.message, region);
|
|
230
|
+
return failure(error);
|
|
231
|
+
}
|
|
232
|
+
const roleArn = `arn:aws:iam::${account.id}:role/fjall-deployment-role`;
|
|
233
|
+
let assumedCredentials;
|
|
234
|
+
try {
|
|
235
|
+
assumedCredentials = await services.awsProvider.assumeRole(roleArn, `fjall-cascade-${account.name}`);
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
239
|
+
callbacks.onCascadeAccountComplete?.(operationKey, false, message, region);
|
|
240
|
+
return failure(new Error(`Failed to assume role for ${account.name}: ${message}`));
|
|
241
|
+
}
|
|
242
|
+
// Build context for this account (includes IPAM pool ID if available)
|
|
243
|
+
const accountContext = CdkContextBuilder.buildDeploymentContext({
|
|
244
|
+
deployType,
|
|
245
|
+
target: operation.target,
|
|
246
|
+
path: operation.path,
|
|
247
|
+
region,
|
|
248
|
+
accountName: account.name,
|
|
249
|
+
callerIdentity: { Account: account.id, Arn: "", UserId: "" },
|
|
250
|
+
ipamPoolId,
|
|
251
|
+
...buildParamsContext(params)
|
|
252
|
+
}, { verbose: params.options?.verbose }, params.orgConfig);
|
|
253
|
+
// Create account-scoped provider for CDK monitoring
|
|
254
|
+
const accountProvider = new SimpleAwsProvider({
|
|
255
|
+
accessKeyId: assumedCredentials.accessKeyId,
|
|
256
|
+
secretAccessKey: assumedCredentials.secretAccessKey,
|
|
257
|
+
sessionToken: assumedCredentials.sessionToken,
|
|
258
|
+
region,
|
|
259
|
+
accountId: account.id
|
|
260
|
+
});
|
|
261
|
+
// Export account credentials for CDK subprocess
|
|
262
|
+
accountProvider.exportToEnv();
|
|
263
|
+
// Bootstrap the target account
|
|
264
|
+
callbacks.onCascadeAccountPhaseChange?.(operationKey, "synth", region);
|
|
265
|
+
const bootstrapResult = await services.cdkService.runCdkBootstrap(accountContext, (chunk) => callbacks.onOutput?.(chunk));
|
|
266
|
+
if (!bootstrapResult.success) {
|
|
267
|
+
services.awsProvider.exportToEnv();
|
|
268
|
+
callbacks.onCascadeAccountComplete?.(operationKey, false, `Bootstrap failed: ${bootstrapResult.error}`, region);
|
|
269
|
+
return failure(new Error(`Bootstrap failed for ${account.name}: ${bootstrapResult.error}`));
|
|
270
|
+
}
|
|
271
|
+
// Deploy the account stack
|
|
272
|
+
callbacks.onCascadeAccountPhaseChange?.(operationKey, "deploy", region);
|
|
273
|
+
const stackName = getOrganisationStackName(deployType === "platform"
|
|
274
|
+
? ORGANISATION_TYPES.PLATFORM
|
|
275
|
+
: ORGANISATION_TYPES.ACCOUNT);
|
|
276
|
+
const deployResult = await services.cdkService.runCdkDeploy(accountContext, stackName, (chunk) => callbacks.onOutput?.(chunk), (event) => callbacks.onCascadeAccountResourceProgress?.(operationKey, event, region), accountProvider);
|
|
277
|
+
// Restore parent credentials
|
|
278
|
+
services.awsProvider.exportToEnv();
|
|
279
|
+
if (!deployResult.success) {
|
|
280
|
+
callbacks.onCascadeAccountComplete?.(operationKey, false, deployResult.error, region);
|
|
281
|
+
return failure(new Error(deployResult.error));
|
|
282
|
+
}
|
|
283
|
+
callbacks.onCascadeAccountComplete?.(operationKey, true, undefined, region);
|
|
284
|
+
return success(undefined);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Read Platform stack outputs to extract IPAM pool IDs for member accounts.
|
|
288
|
+
* Output keys follow the pattern `IpamPoolId{12-digit-accountId}{regionSuffix}`.
|
|
289
|
+
* Returns a map keyed by `{accountId}-{regionSuffix}` → pool ID.
|
|
290
|
+
* Non-fatal: returns empty map on any error.
|
|
291
|
+
*/
|
|
292
|
+
async function readPlatformIpamPoolIds(services, platformAccount, callbacks) {
|
|
293
|
+
const poolIds = new Map();
|
|
294
|
+
// Assume role in platform account to read its stack outputs
|
|
295
|
+
if (!services.awsProvider.assumeRole) {
|
|
296
|
+
logger.debug("organisationDeploy", "Cannot read Platform outputs: assumeRole not available");
|
|
297
|
+
return poolIds;
|
|
298
|
+
}
|
|
299
|
+
const region = services.awsProvider.getRegion();
|
|
300
|
+
const roleArn = `arn:aws:iam::${platformAccount.id}:role/fjall-deployment-role`;
|
|
301
|
+
let assumedCredentials;
|
|
302
|
+
try {
|
|
303
|
+
assumedCredentials = await services.awsProvider.assumeRole(roleArn, `fjall-ipam-read-${platformAccount.name}`);
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
307
|
+
logger.debug("organisationDeploy", `Cannot read Platform outputs: AssumeRole failed: ${message}`);
|
|
308
|
+
return poolIds;
|
|
309
|
+
}
|
|
310
|
+
// Create a temporary provider scoped to the platform account
|
|
311
|
+
const platformProvider = new SimpleAwsProvider({
|
|
312
|
+
accessKeyId: assumedCredentials.accessKeyId,
|
|
313
|
+
secretAccessKey: assumedCredentials.secretAccessKey,
|
|
314
|
+
sessionToken: assumedCredentials.sessionToken,
|
|
315
|
+
region,
|
|
316
|
+
accountId: platformAccount.id
|
|
317
|
+
});
|
|
318
|
+
const platformCfn = new CloudFormationService(platformProvider);
|
|
319
|
+
const outputsResult = await platformCfn.getStackOutputs("Platform");
|
|
320
|
+
if (!outputsResult.success) {
|
|
321
|
+
logger.debug("organisationDeploy", `Failed to read Platform stack outputs: ${outputsResult.error.message}`);
|
|
322
|
+
return poolIds;
|
|
323
|
+
}
|
|
324
|
+
const ipamPattern = /^IpamPoolId(\d{12})(\w+)$/;
|
|
325
|
+
for (const output of outputsResult.data) {
|
|
326
|
+
const match = output.OutputKey?.match(ipamPattern);
|
|
327
|
+
if (match && output.OutputValue) {
|
|
328
|
+
const key = `${match[1]}-${match[2]}`;
|
|
329
|
+
poolIds.set(key, output.OutputValue);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (poolIds.size > 0) {
|
|
333
|
+
callbacks.onLog?.(`Read ${poolIds.size} IPAM pool ID(s) from Platform stack`, "info");
|
|
334
|
+
}
|
|
335
|
+
return poolIds;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Deploy configured domains: apex domains sequentially, then delegated
|
|
339
|
+
* domains in parallel. Delegates to the caller-provided DomainDeployProvider.
|
|
340
|
+
*/
|
|
341
|
+
async function deployDomains(provider, callbacks) {
|
|
342
|
+
const domains = provider.getDomains();
|
|
343
|
+
if (domains.length === 0) {
|
|
344
|
+
return { domainsDeployed: 0, errors: [] };
|
|
345
|
+
}
|
|
346
|
+
callbacks.onCascadePhaseStart?.("domains");
|
|
347
|
+
const apexDomains = domains.filter((d) => d.type === "apex");
|
|
348
|
+
const delegatedDomains = domains.filter((d) => d.type === "delegated");
|
|
349
|
+
let domainsDeployed = 0;
|
|
350
|
+
const errors = [];
|
|
351
|
+
// Phase A: Apex domains (sequential — delegation roles must exist first)
|
|
352
|
+
for (const domain of apexDomains) {
|
|
353
|
+
const result = await provider.deployDomain(domain.name, callbacks);
|
|
354
|
+
if (result.success) {
|
|
355
|
+
domainsDeployed++;
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
errors.push(`${domain.name}: ${result.error.message}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// Phase B: Delegated subdomains (parallel — independent of each other)
|
|
362
|
+
if (delegatedDomains.length > 0) {
|
|
363
|
+
const subdomainResults = await Promise.allSettled(delegatedDomains.map(async (domain) => {
|
|
364
|
+
const result = await provider.deployDomain(domain.name, callbacks);
|
|
365
|
+
if (result.success) {
|
|
366
|
+
domainsDeployed++;
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
errors.push(`${domain.name}: ${result.error.message}`);
|
|
370
|
+
}
|
|
371
|
+
}));
|
|
372
|
+
for (const result of subdomainResults) {
|
|
373
|
+
if (result.status === "rejected") {
|
|
374
|
+
const message = result.reason instanceof Error
|
|
375
|
+
? result.reason.message
|
|
376
|
+
: String(result.reason);
|
|
377
|
+
errors.push(`Subdomain deploy error: ${message}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return { domainsDeployed, errors };
|
|
382
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type Result } from "@fjall/generator";
|
|
2
|
+
import type { AwsProvider } from "../aws/AwsProvider.js";
|
|
3
|
+
export type OrgSetupPhase = "create-organisation" | "enable-policies" | "enable-service-access" | "enable-ram-sharing" | "activate-trusted-access" | "enable-ipam" | "configure-backup" | "create-accounts" | "create-organisational-units" | "place-accounts" | "activate-cost-tags" | "check-identity-centre";
|
|
4
|
+
export interface OrgSetupCallbacks {
|
|
5
|
+
onPhaseStart?(phase: OrgSetupPhase): void;
|
|
6
|
+
onPhaseComplete?(phase: OrgSetupPhase, result: "completed" | "skipped" | "error"): void;
|
|
7
|
+
onProgress?(message: string): void;
|
|
8
|
+
onError?(phase: OrgSetupPhase, error: Error): void;
|
|
9
|
+
}
|
|
10
|
+
export interface OrgSetupConfig {
|
|
11
|
+
accounts: Array<{
|
|
12
|
+
name: string;
|
|
13
|
+
email: string;
|
|
14
|
+
}>;
|
|
15
|
+
platformAccountId: string;
|
|
16
|
+
organisationalUnits: string[];
|
|
17
|
+
accountPlacements?: Record<string, string>;
|
|
18
|
+
costAllocationTags?: string[];
|
|
19
|
+
skipIdentityCentre?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface OrgSetupResult {
|
|
22
|
+
organisationId: string;
|
|
23
|
+
createdAccounts: Array<{
|
|
24
|
+
name: string;
|
|
25
|
+
accountId: string;
|
|
26
|
+
}>;
|
|
27
|
+
identityCentreStatus?: string;
|
|
28
|
+
phasesCompleted: OrgSetupPhase[];
|
|
29
|
+
phasesSkipped: OrgSetupPhase[];
|
|
30
|
+
errors: Array<{
|
|
31
|
+
phase: OrgSetupPhase;
|
|
32
|
+
error: string;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Orchestrate the full AWS Organisation setup sequence.
|
|
37
|
+
*
|
|
38
|
+
* Runs 12 phases sequentially. Non-fatal phase failures are recorded
|
|
39
|
+
* and execution continues. The only fatal failure is phase 1
|
|
40
|
+
* (create-organisation) since all subsequent phases depend on the org ID.
|
|
41
|
+
*/
|
|
42
|
+
export declare function runOrganisationSetup(awsProvider: AwsProvider, config: OrgSetupConfig, callbacks?: OrgSetupCallbacks): Promise<Result<OrgSetupResult>>;
|