@continuous-excellence/ze-great-dashboard-aws 0.1.26 → 0.1.28
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/bootstrap/core-v1.yml +111 -0
- package/bootstrap/dashboard-bootstrap.example.json +15 -0
- package/bootstrap/github-oidc-v1.yml +55 -0
- package/dist/bootstrap.d.ts +40 -0
- package/dist/cli.js +241 -21
- package/dist/index.d.ts +1 -0
- package/dist/index.js +92 -11
- package/package.json +2 -1
- package/template.yml +2 -12
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
AWSTemplateFormatVersion: '2010-09-09'
|
|
2
|
+
Description: Ze Great Dashboard bootstrap core v1. Administrator-managed; do not grant this authority to CI.
|
|
3
|
+
|
|
4
|
+
Parameters:
|
|
5
|
+
ArtifactBucketName:
|
|
6
|
+
Type: String
|
|
7
|
+
Description: Globally unique bucket for private Lambda artifacts
|
|
8
|
+
ApplicationStackName:
|
|
9
|
+
Type: String
|
|
10
|
+
Description: Stable name of the one dashboard application stack
|
|
11
|
+
DashboardFunctionName:
|
|
12
|
+
Type: String
|
|
13
|
+
Description: Stable Lambda function name used by the application stack
|
|
14
|
+
RuntimeSecretArn:
|
|
15
|
+
Type: String
|
|
16
|
+
Default: ''
|
|
17
|
+
Description: Optional explicitly configured Secrets Manager secret ARN
|
|
18
|
+
ArtifactKmsKeyArn:
|
|
19
|
+
Type: String
|
|
20
|
+
Default: ''
|
|
21
|
+
Description: Optional customer-managed KMS key ARN; SSE-S3 is used when blank
|
|
22
|
+
|
|
23
|
+
Conditions:
|
|
24
|
+
HasRuntimeSecret: !Not [!Equals [!Ref RuntimeSecretArn, '']]
|
|
25
|
+
HasArtifactKmsKey: !Not [!Equals [!Ref ArtifactKmsKeyArn, '']]
|
|
26
|
+
|
|
27
|
+
Resources:
|
|
28
|
+
ArtifactBucket:
|
|
29
|
+
Type: AWS::S3::Bucket
|
|
30
|
+
DeletionPolicy: Retain
|
|
31
|
+
UpdateReplacePolicy: Retain
|
|
32
|
+
Properties:
|
|
33
|
+
BucketName: !Ref ArtifactBucketName
|
|
34
|
+
BucketEncryption:
|
|
35
|
+
ServerSideEncryptionConfiguration:
|
|
36
|
+
- ServerSideEncryptionByDefault:
|
|
37
|
+
SSEAlgorithm: !If [HasArtifactKmsKey, aws:kms, AES256]
|
|
38
|
+
KMSMasterKeyID: !If [HasArtifactKmsKey, !Ref ArtifactKmsKeyArn, !Ref AWS::NoValue]
|
|
39
|
+
BucketKeyEnabled: !If [HasArtifactKmsKey, true, !Ref AWS::NoValue]
|
|
40
|
+
OwnershipControls:
|
|
41
|
+
Rules: [{ ObjectOwnership: BucketOwnerEnforced }]
|
|
42
|
+
PublicAccessBlockConfiguration:
|
|
43
|
+
BlockPublicAcls: true
|
|
44
|
+
BlockPublicPolicy: true
|
|
45
|
+
IgnorePublicAcls: true
|
|
46
|
+
RestrictPublicBuckets: true
|
|
47
|
+
|
|
48
|
+
ArtifactBucketPolicy:
|
|
49
|
+
Type: AWS::S3::BucketPolicy
|
|
50
|
+
Properties:
|
|
51
|
+
Bucket: !Ref ArtifactBucket
|
|
52
|
+
PolicyDocument:
|
|
53
|
+
Version: '2012-10-17'
|
|
54
|
+
Statement:
|
|
55
|
+
- Sid: DenyInsecureTransport
|
|
56
|
+
Effect: Deny
|
|
57
|
+
Principal: '*'
|
|
58
|
+
Action: s3:*
|
|
59
|
+
Resource: [!GetAtt ArtifactBucket.Arn, !Sub '${ArtifactBucket.Arn}/*']
|
|
60
|
+
Condition: { Bool: { 'aws:SecureTransport': false } }
|
|
61
|
+
|
|
62
|
+
CloudFormationExecutionRole:
|
|
63
|
+
Type: AWS::IAM::Role
|
|
64
|
+
DeletionPolicy: Retain
|
|
65
|
+
UpdateReplacePolicy: Retain
|
|
66
|
+
Properties:
|
|
67
|
+
RoleName: !Sub '${ApplicationStackName}-execution'
|
|
68
|
+
Description: !Sub 'Restricted CloudFormation execution role for ${ApplicationStackName}'
|
|
69
|
+
AssumeRolePolicyDocument:
|
|
70
|
+
Version: '2012-10-17'
|
|
71
|
+
Statement: [{ Effect: Allow, Principal: { Service: cloudformation.amazonaws.com }, Action: sts:AssumeRole }]
|
|
72
|
+
Policies:
|
|
73
|
+
- PolicyName: DashboardApplicationResources
|
|
74
|
+
PolicyDocument:
|
|
75
|
+
Version: '2012-10-17'
|
|
76
|
+
Statement:
|
|
77
|
+
- Sid: DashboardLambda
|
|
78
|
+
Effect: Allow
|
|
79
|
+
Action: [lambda:CreateFunction, lambda:GetFunction, lambda:UpdateFunctionCode, lambda:UpdateFunctionConfiguration, lambda:DeleteFunction, lambda:TagResource, lambda:UntagResource]
|
|
80
|
+
Resource: !Sub 'arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:${DashboardFunctionName}'
|
|
81
|
+
- Sid: DashboardLogs
|
|
82
|
+
Effect: Allow
|
|
83
|
+
Action: [logs:CreateLogGroup, logs:DeleteLogGroup, logs:DescribeLogGroups, logs:PutRetentionPolicy, logs:TagResource, logs:UntagResource]
|
|
84
|
+
Resource: !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${DashboardFunctionName}:*'
|
|
85
|
+
- Sid: ReadLambdaArtifact
|
|
86
|
+
Effect: Allow
|
|
87
|
+
Action: s3:GetObject
|
|
88
|
+
Resource: !Sub '${ArtifactBucket.Arn}/lambda/*'
|
|
89
|
+
- Sid: ManageRuntimeRole
|
|
90
|
+
Effect: Allow
|
|
91
|
+
Action: [iam:CreateRole, iam:GetRole, iam:DeleteRole, iam:UpdateAssumeRolePolicy, iam:PutRolePolicy, iam:GetRolePolicy, iam:DeleteRolePolicy, iam:AttachRolePolicy, iam:DetachRolePolicy, iam:ListRolePolicies, iam:ListAttachedRolePolicies, iam:TagRole, iam:UntagRole]
|
|
92
|
+
Resource: !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${DashboardFunctionName}-server'
|
|
93
|
+
- Sid: PassRuntimeRoleToLambdaOnly
|
|
94
|
+
Effect: Allow
|
|
95
|
+
Action: iam:PassRole
|
|
96
|
+
Resource: !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${DashboardFunctionName}-server'
|
|
97
|
+
Condition: { StringEquals: { 'iam:PassedToService': lambda.amazonaws.com } }
|
|
98
|
+
- !If
|
|
99
|
+
- HasRuntimeSecret
|
|
100
|
+
- Sid: ReadConfiguredRuntimeSecret
|
|
101
|
+
Effect: Allow
|
|
102
|
+
Action: [secretsmanager:DescribeSecret, secretsmanager:GetSecretValue]
|
|
103
|
+
Resource: !Ref RuntimeSecretArn
|
|
104
|
+
- !Ref AWS::NoValue
|
|
105
|
+
|
|
106
|
+
Outputs:
|
|
107
|
+
BootstrapContractVersion: { Value: '1' }
|
|
108
|
+
ArtifactBucketName: { Value: !Ref ArtifactBucket }
|
|
109
|
+
ArtifactBucketArn: { Value: !GetAtt ArtifactBucket.Arn }
|
|
110
|
+
CloudFormationExecutionRoleArn: { Value: !GetAtt CloudFormationExecutionRole.Arn }
|
|
111
|
+
ApplicationStackName: { Value: !Ref ApplicationStackName }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"region": "us-east-1",
|
|
3
|
+
"core": {
|
|
4
|
+
"stackName": "team-dashboard-bootstrap",
|
|
5
|
+
"artifactBucketName": "team-dashboard-lambda-artifacts-123456789012",
|
|
6
|
+
"applicationStackName": "team-dashboard",
|
|
7
|
+
"dashboardFunctionName": "team-dashboard"
|
|
8
|
+
},
|
|
9
|
+
"githubOidc": {
|
|
10
|
+
"stackName": "team-dashboard-github-bootstrap",
|
|
11
|
+
"providerArn": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com",
|
|
12
|
+
"repository": "example/team-dashboard",
|
|
13
|
+
"environment": "production"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
AWSTemplateFormatVersion: '2010-09-09'
|
|
2
|
+
Description: Ze Great Dashboard GitHub OIDC adapter v1. Administrator-managed; requires a central OIDC provider.
|
|
3
|
+
|
|
4
|
+
Parameters:
|
|
5
|
+
GitHubOidcProviderArn: { Type: String }
|
|
6
|
+
GitHubRepository: { Type: String, Description: owner/repository }
|
|
7
|
+
GitHubEnvironment: { Type: String, Description: Protected GitHub Environment name }
|
|
8
|
+
ApplicationStackName: { Type: String }
|
|
9
|
+
ArtifactBucketName: { Type: String }
|
|
10
|
+
CloudFormationExecutionRoleArn: { Type: String }
|
|
11
|
+
|
|
12
|
+
Resources:
|
|
13
|
+
GitHubDeployRole:
|
|
14
|
+
Type: AWS::IAM::Role
|
|
15
|
+
DeletionPolicy: Retain
|
|
16
|
+
UpdateReplacePolicy: Retain
|
|
17
|
+
Properties:
|
|
18
|
+
RoleName: !Sub '${ApplicationStackName}-github-deploy'
|
|
19
|
+
MaxSessionDuration: 3600
|
|
20
|
+
AssumeRolePolicyDocument:
|
|
21
|
+
Version: '2012-10-17'
|
|
22
|
+
Statement:
|
|
23
|
+
- Effect: Allow
|
|
24
|
+
Principal: { Federated: !Ref GitHubOidcProviderArn }
|
|
25
|
+
Action: sts:AssumeRoleWithWebIdentity
|
|
26
|
+
Condition:
|
|
27
|
+
StringEquals:
|
|
28
|
+
'token.actions.githubusercontent.com:aud': sts.amazonaws.com
|
|
29
|
+
'token.actions.githubusercontent.com:sub': !Sub 'repo:${GitHubRepository}:environment:${GitHubEnvironment}'
|
|
30
|
+
Policies:
|
|
31
|
+
- PolicyName: DeployOneDashboard
|
|
32
|
+
PolicyDocument:
|
|
33
|
+
Version: '2012-10-17'
|
|
34
|
+
Statement:
|
|
35
|
+
- Sid: UploadLambdaArtifacts
|
|
36
|
+
Effect: Allow
|
|
37
|
+
Action: [s3:PutObject, s3:GetObject]
|
|
38
|
+
Resource: !Sub 'arn:${AWS::Partition}:s3:::${ArtifactBucketName}/lambda/*'
|
|
39
|
+
- Sid: InspectArtifactBucket
|
|
40
|
+
Effect: Allow
|
|
41
|
+
Action: [s3:GetBucketLocation, s3:ListBucket]
|
|
42
|
+
Resource: !Sub 'arn:${AWS::Partition}:s3:::${ArtifactBucketName}'
|
|
43
|
+
- Sid: OperateOneApplicationStack
|
|
44
|
+
Effect: Allow
|
|
45
|
+
Action: [cloudformation:CreateChangeSet, cloudformation:DescribeChangeSet, cloudformation:ExecuteChangeSet, cloudformation:DeleteChangeSet, cloudformation:DescribeStacks, cloudformation:DescribeStackEvents, cloudformation:GetTemplate, cloudformation:GetTemplateSummary]
|
|
46
|
+
Resource: !Sub 'arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${ApplicationStackName}/*'
|
|
47
|
+
- Sid: PassCoreExecutionRole
|
|
48
|
+
Effect: Allow
|
|
49
|
+
Action: iam:PassRole
|
|
50
|
+
Resource: !Ref CloudFormationExecutionRoleArn
|
|
51
|
+
Condition: { StringEquals: { 'iam:PassedToService': cloudformation.amazonaws.com } }
|
|
52
|
+
|
|
53
|
+
Outputs:
|
|
54
|
+
BootstrapContractVersion: { Value: '1' }
|
|
55
|
+
GitHubDeployRoleArn: { Value: !GetAtt GitHubDeployRole.Arn }
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type BootstrapKind = 'core' | 'github-oidc';
|
|
2
|
+
export type CloudFormationParameterValue = {
|
|
3
|
+
ParameterKey: string;
|
|
4
|
+
ParameterValue: string;
|
|
5
|
+
UsePreviousValue?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type DeployedBootstrapStack = {
|
|
8
|
+
Parameters?: CloudFormationParameterValue[];
|
|
9
|
+
Outputs?: {
|
|
10
|
+
OutputKey: string;
|
|
11
|
+
OutputValue?: string;
|
|
12
|
+
}[];
|
|
13
|
+
};
|
|
14
|
+
export type BootstrapConfig = {
|
|
15
|
+
region?: string;
|
|
16
|
+
core?: {
|
|
17
|
+
stackName?: string;
|
|
18
|
+
artifactBucketName?: string;
|
|
19
|
+
applicationStackName?: string;
|
|
20
|
+
dashboardFunctionName?: string;
|
|
21
|
+
runtimeSecretArn?: string;
|
|
22
|
+
artifactKmsKeyArn?: string;
|
|
23
|
+
};
|
|
24
|
+
githubOidc?: {
|
|
25
|
+
stackName?: string;
|
|
26
|
+
providerArn?: string;
|
|
27
|
+
repository?: string;
|
|
28
|
+
environment?: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
export declare function bootstrapTemplatePath(kind: BootstrapKind): Promise<string>;
|
|
32
|
+
export declare function bootstrapTemplate(kind: BootstrapKind): Promise<string>;
|
|
33
|
+
export declare function bootstrapContractVersion(template: string): string;
|
|
34
|
+
/** Merges a new parameter set with deployed values without silently dropping configuration. */
|
|
35
|
+
export declare function mergeBootstrapParameters(requested: CloudFormationParameterValue[], deployed: CloudFormationParameterValue[]): CloudFormationParameterValue[];
|
|
36
|
+
/** Validates a caller-captured `describe-stacks` result before an upgrade merges it locally. */
|
|
37
|
+
export declare function deployedBootstrapStack(input: unknown, expectedContractVersion: string): DeployedBootstrapStack;
|
|
38
|
+
/** Extracts the adapter contract from a caller-captured core `describe-stacks` result. */
|
|
39
|
+
export declare function coreBootstrapOutputs(stack: DeployedBootstrapStack): Record<string, string>;
|
|
40
|
+
export declare function requiredBootstrapParameters(kind: BootstrapKind, values: Record<string, string | undefined>): CloudFormationParameterValue[];
|
package/dist/cli.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// packages/aws/src/cli.ts
|
|
4
|
-
import { readFile as
|
|
5
|
-
import { fileURLToPath as
|
|
4
|
+
import { readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
|
|
5
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
6
6
|
import { parse as parse3 } from "yaml";
|
|
7
7
|
|
|
8
8
|
// packages/aws/src/doctor.ts
|
|
9
9
|
import { execFile as execFile2 } from "node:child_process";
|
|
10
|
-
import { readFile as
|
|
10
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
11
11
|
import { promisify as promisify2 } from "node:util";
|
|
12
12
|
import { parse as parse2 } from "yaml";
|
|
13
13
|
|
|
14
14
|
// packages/aws/src/index.ts
|
|
15
15
|
import { execFile } from "node:child_process";
|
|
16
|
-
import { cp, mkdir as mkdir2, readFile as
|
|
16
|
+
import { cp, mkdir as mkdir2, readFile as readFile3, rm, writeFile as writeFile2 } from "node:fs/promises";
|
|
17
17
|
import { join as join2, resolve as resolve2 } from "node:path";
|
|
18
|
-
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
19
19
|
import { promisify } from "node:util";
|
|
20
20
|
|
|
21
21
|
// node_modules/fflate/esm/index.mjs
|
|
@@ -838,6 +838,80 @@ function sha256(value) {
|
|
|
838
838
|
return createHash("sha256").update(value).digest("hex");
|
|
839
839
|
}
|
|
840
840
|
|
|
841
|
+
// packages/aws/src/bootstrap.ts
|
|
842
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
843
|
+
import { fileURLToPath } from "node:url";
|
|
844
|
+
var templates = {
|
|
845
|
+
core: "../bootstrap/core-v1.yml",
|
|
846
|
+
"github-oidc": "../bootstrap/github-oidc-v1.yml"
|
|
847
|
+
};
|
|
848
|
+
async function bootstrapTemplatePath(kind) {
|
|
849
|
+
if (!Object.hasOwn(templates, kind)) throw new Error(`Unknown bootstrap template: ${kind}`);
|
|
850
|
+
return fileURLToPath(new URL(templates[kind], import.meta.url));
|
|
851
|
+
}
|
|
852
|
+
async function bootstrapTemplate(kind) {
|
|
853
|
+
return readFile2(await bootstrapTemplatePath(kind), "utf8");
|
|
854
|
+
}
|
|
855
|
+
function bootstrapContractVersion(template) {
|
|
856
|
+
const version = template.match(/BootstrapContractVersion:\s*\{\s*Value:\s*'([^']+)'\s*}/)?.[1];
|
|
857
|
+
if (!version) throw new Error("Bootstrap template has no BootstrapContractVersion output");
|
|
858
|
+
return version;
|
|
859
|
+
}
|
|
860
|
+
function mergeBootstrapParameters(requested, deployed) {
|
|
861
|
+
const supplied = new Map(requested.map((entry) => [entry.ParameterKey, entry.ParameterValue]));
|
|
862
|
+
const duplicates = requested.map((entry) => entry.ParameterKey);
|
|
863
|
+
if (new Set(duplicates).size !== duplicates.length)
|
|
864
|
+
throw new Error("Bootstrap parameter file contains duplicate parameters");
|
|
865
|
+
return deployed.map(({ ParameterKey, ParameterValue }) => ({
|
|
866
|
+
ParameterKey,
|
|
867
|
+
ParameterValue: supplied.get(ParameterKey) ?? ParameterValue
|
|
868
|
+
})).concat(
|
|
869
|
+
requested.filter(
|
|
870
|
+
({ ParameterKey }) => !deployed.some((entry) => entry.ParameterKey === ParameterKey)
|
|
871
|
+
)
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
function deployedBootstrapStack(input, expectedContractVersion) {
|
|
875
|
+
const stack = input && typeof input === "object" && "Stacks" in input ? input.Stacks?.[0] : input;
|
|
876
|
+
if (!stack || typeof stack !== "object") throw new Error("Deployed stack JSON contains no stack");
|
|
877
|
+
const deployed = stack;
|
|
878
|
+
const contract = deployed.Outputs?.find(
|
|
879
|
+
(output) => output.OutputKey === "BootstrapContractVersion"
|
|
880
|
+
)?.OutputValue;
|
|
881
|
+
if (contract !== expectedContractVersion)
|
|
882
|
+
throw new Error(
|
|
883
|
+
`Bootstrap contract mismatch (deployed: ${contract ?? "missing"}; expected: ${expectedContractVersion}); follow the documented migration procedure`
|
|
884
|
+
);
|
|
885
|
+
if (deployed.Parameters && !Array.isArray(deployed.Parameters))
|
|
886
|
+
throw new Error("Deployed stack JSON has invalid Parameters");
|
|
887
|
+
return deployed;
|
|
888
|
+
}
|
|
889
|
+
function coreBootstrapOutputs(stack) {
|
|
890
|
+
const values = Object.fromEntries(
|
|
891
|
+
(stack.Outputs ?? []).filter(
|
|
892
|
+
(output) => Boolean(output.OutputValue)
|
|
893
|
+
).map((output) => [output.OutputKey, output.OutputValue])
|
|
894
|
+
);
|
|
895
|
+
const required = ["ArtifactBucketName", "ApplicationStackName", "CloudFormationExecutionRoleArn"];
|
|
896
|
+
const missing = required.filter((key) => !values[key]);
|
|
897
|
+
if (missing.length) throw new Error(`Core stack JSON is missing outputs: ${missing.join(", ")}`);
|
|
898
|
+
return values;
|
|
899
|
+
}
|
|
900
|
+
function requiredBootstrapParameters(kind, values) {
|
|
901
|
+
const keys = kind === "core" ? ["ArtifactBucketName", "ApplicationStackName", "DashboardFunctionName"] : [
|
|
902
|
+
"GitHubOidcProviderArn",
|
|
903
|
+
"GitHubRepository",
|
|
904
|
+
"GitHubEnvironment",
|
|
905
|
+
"ApplicationStackName",
|
|
906
|
+
"ArtifactBucketName",
|
|
907
|
+
"CloudFormationExecutionRoleArn"
|
|
908
|
+
];
|
|
909
|
+
const missing = keys.filter((key) => !values[key]);
|
|
910
|
+
if (missing.length) throw new Error(`Missing required bootstrap values: ${missing.join(", ")}`);
|
|
911
|
+
const optional = kind === "core" ? ["RuntimeSecretArn", "ArtifactKmsKeyArn"] : [];
|
|
912
|
+
return [...keys, ...optional].filter((key) => values[key] !== void 0).map((ParameterKey) => ({ ParameterKey, ParameterValue: values[ParameterKey] ?? "" }));
|
|
913
|
+
}
|
|
914
|
+
|
|
841
915
|
// packages/aws/src/index.ts
|
|
842
916
|
var run = promisify(execFile);
|
|
843
917
|
function deploymentTemplate(template, values) {
|
|
@@ -867,7 +941,7 @@ async function packageLambda(options) {
|
|
|
867
941
|
await mkdir2(outputDir, { recursive: true });
|
|
868
942
|
const runtimeDir = join2(outputDir, "lambda");
|
|
869
943
|
await mkdir2(runtimeDir, { recursive: true });
|
|
870
|
-
const lambdaSource =
|
|
944
|
+
const lambdaSource = fileURLToPath2(new URL("../dist/lambda.mjs", import.meta.url));
|
|
871
945
|
await cp(lambdaSource, join2(runtimeDir, "index.mjs"));
|
|
872
946
|
const release = await assembleRelease({
|
|
873
947
|
boardConfigPath: options.boardConfigPath,
|
|
@@ -880,7 +954,7 @@ async function packageLambda(options) {
|
|
|
880
954
|
...release.metadata,
|
|
881
955
|
artifactChecksums: {
|
|
882
956
|
...release.metadata.artifactChecksums,
|
|
883
|
-
"index.mjs": sha256(await
|
|
957
|
+
"index.mjs": sha256(await readFile3(join2(runtimeDir, "index.mjs")))
|
|
884
958
|
}
|
|
885
959
|
};
|
|
886
960
|
await writeFile2(join2(runtimeDir, "release.json"), `${JSON.stringify(runtimeMetadata, null, 2)}
|
|
@@ -895,7 +969,7 @@ async function packageLambda(options) {
|
|
|
895
969
|
archiveFiles.map(async (name) => [
|
|
896
970
|
name,
|
|
897
971
|
[
|
|
898
|
-
strToU8(await
|
|
972
|
+
strToU8(await readFile3(join2(runtimeDir, name), "utf8")),
|
|
899
973
|
{ mtime: new Date(1980, 0, 1, 0, 0, 0), level: 9 }
|
|
900
974
|
]
|
|
901
975
|
])
|
|
@@ -903,7 +977,7 @@ async function packageLambda(options) {
|
|
|
903
977
|
);
|
|
904
978
|
await writeFile2(lambdaPath, zipSync(archive));
|
|
905
979
|
await rm(runtimeDir, { recursive: true, force: true });
|
|
906
|
-
const lambdaChecksum = sha256(await
|
|
980
|
+
const lambdaChecksum = sha256(await readFile3(lambdaPath));
|
|
907
981
|
const deploymentChecksum = sha256(JSON.stringify(runtimeMetadata));
|
|
908
982
|
const packagedRelease = {
|
|
909
983
|
...runtimeMetadata,
|
|
@@ -923,7 +997,7 @@ async function packageLambda(options) {
|
|
|
923
997
|
}
|
|
924
998
|
async function publishClientAssets(options) {
|
|
925
999
|
const assetsDir = resolve2(options.assetsDir);
|
|
926
|
-
await
|
|
1000
|
+
await readFile3(join2(assetsDir, "index.html"));
|
|
927
1001
|
const assetPath = `${options.assetsBaseUrl.replace(/\/+$/, "")}/dashboard/${options.version}`;
|
|
928
1002
|
await run("aws", [
|
|
929
1003
|
"s3",
|
|
@@ -948,8 +1022,8 @@ async function publishClientAssets(options) {
|
|
|
948
1022
|
async function deployLambda(options) {
|
|
949
1023
|
const artifactDir = resolve2(options.artifactDir);
|
|
950
1024
|
const assetsDir = resolve2(options.assetsDir);
|
|
951
|
-
await
|
|
952
|
-
await
|
|
1025
|
+
await readFile3(join2(artifactDir, "lambda.zip"));
|
|
1026
|
+
await readFile3(join2(assetsDir, "index.html"));
|
|
953
1027
|
if (options.dryRun) return;
|
|
954
1028
|
const assetPath = await publishClientAssets(options);
|
|
955
1029
|
await run("aws", [
|
|
@@ -974,7 +1048,7 @@ async function deployLambda(options) {
|
|
|
974
1048
|
await run("aws", ["lambda", "wait", "function-updated", "--function-name", options.functionName]);
|
|
975
1049
|
}
|
|
976
1050
|
async function cloudFormationTemplate() {
|
|
977
|
-
return
|
|
1051
|
+
return readFile3(fileURLToPath2(new URL("../template.yml", import.meta.url)), "utf8");
|
|
978
1052
|
}
|
|
979
1053
|
|
|
980
1054
|
// packages/aws/src/doctor.ts
|
|
@@ -1073,7 +1147,7 @@ async function runDoctor(options, dependencies = actualDependencies) {
|
|
|
1073
1147
|
let parameterData;
|
|
1074
1148
|
await check("Parameters/template", async () => {
|
|
1075
1149
|
const parameters = readParameterValues(
|
|
1076
|
-
JSON.parse(await
|
|
1150
|
+
JSON.parse(await readFile4(options.parametersPath, "utf8"))
|
|
1077
1151
|
);
|
|
1078
1152
|
parameterData = await templateContract(parameters);
|
|
1079
1153
|
return `${options.parametersPath} is compatible`;
|
|
@@ -1119,7 +1193,7 @@ var option = (name, fallback) => {
|
|
|
1119
1193
|
};
|
|
1120
1194
|
async function installedPackageVersion() {
|
|
1121
1195
|
const packageManifest = JSON.parse(
|
|
1122
|
-
await
|
|
1196
|
+
await readFile5(new URL("../package.json", import.meta.url), "utf8")
|
|
1123
1197
|
);
|
|
1124
1198
|
return typeof packageManifest.version === "string" ? packageManifest.version : "";
|
|
1125
1199
|
}
|
|
@@ -1133,7 +1207,7 @@ function parameter(key, value) {
|
|
|
1133
1207
|
}
|
|
1134
1208
|
async function existingParameters(path) {
|
|
1135
1209
|
try {
|
|
1136
|
-
const parsed = JSON.parse(await
|
|
1210
|
+
const parsed = JSON.parse(await readFile5(path, "utf8"));
|
|
1137
1211
|
if (!Array.isArray(parsed)) throw new Error(`${path} must contain a JSON parameter array`);
|
|
1138
1212
|
const values = parsed.map((value) => {
|
|
1139
1213
|
if (!value || typeof value !== "object" || typeof value.ParameterKey !== "string" || typeof value.ParameterValue !== "string")
|
|
@@ -1150,6 +1224,49 @@ async function existingParameters(path) {
|
|
|
1150
1224
|
throw error;
|
|
1151
1225
|
}
|
|
1152
1226
|
}
|
|
1227
|
+
function bootstrapKind() {
|
|
1228
|
+
const kind = option("--kind");
|
|
1229
|
+
if (kind === "core" || kind === "github-oidc") return kind;
|
|
1230
|
+
throw new Error("--kind must be core or github-oidc");
|
|
1231
|
+
}
|
|
1232
|
+
async function readBootstrapConfig(path) {
|
|
1233
|
+
const parsed = JSON.parse(await readFile5(path, "utf8"));
|
|
1234
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
1235
|
+
throw new Error("--config must contain a JSON object");
|
|
1236
|
+
return parsed;
|
|
1237
|
+
}
|
|
1238
|
+
async function bootstrapConfig() {
|
|
1239
|
+
const path = option("--config");
|
|
1240
|
+
return path ? readBootstrapConfig(path) : {};
|
|
1241
|
+
}
|
|
1242
|
+
function bootstrapValues(config, coreOutputs = {}) {
|
|
1243
|
+
return {
|
|
1244
|
+
ArtifactBucketName: option(
|
|
1245
|
+
"--artifact-bucket",
|
|
1246
|
+
coreOutputs.ArtifactBucketName ?? config.core?.artifactBucketName
|
|
1247
|
+
),
|
|
1248
|
+
ApplicationStackName: option(
|
|
1249
|
+
"--application-stack",
|
|
1250
|
+
coreOutputs.ApplicationStackName ?? config.core?.applicationStackName
|
|
1251
|
+
),
|
|
1252
|
+
DashboardFunctionName: option("--function-name", config.core?.dashboardFunctionName),
|
|
1253
|
+
RuntimeSecretArn: option("--runtime-secret-arn", config.core?.runtimeSecretArn),
|
|
1254
|
+
ArtifactKmsKeyArn: option("--artifact-kms-key-arn", config.core?.artifactKmsKeyArn),
|
|
1255
|
+
GitHubOidcProviderArn: option("--github-oidc-provider-arn", config.githubOidc?.providerArn),
|
|
1256
|
+
GitHubRepository: option("--github-repository", config.githubOidc?.repository),
|
|
1257
|
+
GitHubEnvironment: option("--github-environment", config.githubOidc?.environment),
|
|
1258
|
+
CloudFormationExecutionRoleArn: option(
|
|
1259
|
+
"--execution-role-arn",
|
|
1260
|
+
coreOutputs.CloudFormationExecutionRoleArn
|
|
1261
|
+
)
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
function bootstrapStackName(kind, config) {
|
|
1265
|
+
return kind === "core" ? config.core?.stackName : config.githubOidc?.stackName;
|
|
1266
|
+
}
|
|
1267
|
+
function shellCommand(command) {
|
|
1268
|
+
return command.map((argument) => `'${argument.replaceAll("'", `'\\"'\\"'`)}'`).join(" ");
|
|
1269
|
+
}
|
|
1153
1270
|
async function templateParameters() {
|
|
1154
1271
|
const template = await cloudFormationTemplate();
|
|
1155
1272
|
const block = template.match(/^Parameters:\n[\s\S]*?(?=^[A-Za-z][A-Za-z0-9]*:\s*$)/m)?.[0];
|
|
@@ -1172,7 +1289,7 @@ async function templateParameters() {
|
|
|
1172
1289
|
}
|
|
1173
1290
|
try {
|
|
1174
1291
|
const packageVersion = await installedPackageVersion();
|
|
1175
|
-
const bundledAssets =
|
|
1292
|
+
const bundledAssets = fileURLToPath3(new URL("../client", import.meta.url));
|
|
1176
1293
|
if (args[0] === "deploy") {
|
|
1177
1294
|
const artifactDir = requiredOption("--artifact-dir");
|
|
1178
1295
|
const version = requiredOption("--version", packageVersion);
|
|
@@ -1219,10 +1336,15 @@ try {
|
|
|
1219
1336
|
} else if (args[0] === "parameters") {
|
|
1220
1337
|
const output = option("--output", "aws-dashboard-parameters.json") ?? "aws-dashboard-parameters.json";
|
|
1221
1338
|
const existing = await existingParameters(output);
|
|
1339
|
+
const consumerConfigPath = option("--bootstrap-config");
|
|
1340
|
+
const consumerConfig = consumerConfigPath ? await readBootstrapConfig(consumerConfigPath) : void 0;
|
|
1222
1341
|
const existingValues = Object.fromEntries(
|
|
1223
1342
|
existing.map(({ ParameterKey, ParameterValue }) => [ParameterKey, ParameterValue])
|
|
1224
1343
|
);
|
|
1225
|
-
const artifactBucket = option(
|
|
1344
|
+
const artifactBucket = option(
|
|
1345
|
+
"--artifact-bucket",
|
|
1346
|
+
existingValues.LambdaArtifactBucket ?? consumerConfig?.core?.artifactBucketName
|
|
1347
|
+
);
|
|
1226
1348
|
if (!artifactBucket) throw new Error("--artifact-bucket is required");
|
|
1227
1349
|
const { definitions, packageManaged } = await templateParameters();
|
|
1228
1350
|
const templateKeys = Object.keys(definitions);
|
|
@@ -1240,20 +1362,118 @@ try {
|
|
|
1240
1362
|
);
|
|
1241
1363
|
}
|
|
1242
1364
|
const includeDefaults = args.includes("--include-defaults");
|
|
1365
|
+
const functionName = option("--function-name", consumerConfig?.core?.dashboardFunctionName);
|
|
1243
1366
|
const parameters = templateKeys.filter(
|
|
1244
|
-
(key) => !packageManaged.has(key) && (includeDefaults || !hasDefault(key) || Object.hasOwn(existingValues, key))
|
|
1367
|
+
(key) => !packageManaged.has(key) && (includeDefaults || !hasDefault(key) || Object.hasOwn(existingValues, key) || key === "Name" && Boolean(functionName))
|
|
1245
1368
|
).map((key) => {
|
|
1246
1369
|
const definition = definitions[key];
|
|
1247
|
-
const value = key === "LambdaArtifactBucket" ? artifactBucket : existingValues[key] ?? definition?.Default;
|
|
1370
|
+
const value = key === "LambdaArtifactBucket" ? artifactBucket : key === "Name" && functionName ? functionName : existingValues[key] ?? definition?.Default;
|
|
1248
1371
|
if (value === void 0) throw new Error(`No value for CloudFormation parameter ${key}`);
|
|
1249
1372
|
return parameter(key, String(value));
|
|
1250
1373
|
});
|
|
1251
1374
|
await writeFile3(output, `${JSON.stringify(parameters, null, 2)}
|
|
1252
1375
|
`);
|
|
1253
1376
|
console.log(JSON.stringify({ output }));
|
|
1377
|
+
} else if (args[0] === "bootstrap") {
|
|
1378
|
+
const action = args[1];
|
|
1379
|
+
const kind = bootstrapKind();
|
|
1380
|
+
const config = await bootstrapConfig();
|
|
1381
|
+
if (action === "template") {
|
|
1382
|
+
console.log(
|
|
1383
|
+
JSON.stringify({
|
|
1384
|
+
kind,
|
|
1385
|
+
template: await bootstrapTemplatePath(kind),
|
|
1386
|
+
contractVersion: bootstrapContractVersion(await bootstrapTemplate(kind))
|
|
1387
|
+
})
|
|
1388
|
+
);
|
|
1389
|
+
} else if (action === "parameters") {
|
|
1390
|
+
const output = option("--output", `aws-dashboard-bootstrap-${kind}.json`) ?? `aws-dashboard-bootstrap-${kind}.json`;
|
|
1391
|
+
let coreOutputs = {};
|
|
1392
|
+
const coreStackPath = option("--core-stack-json");
|
|
1393
|
+
if (kind === "github-oidc" && coreStackPath) {
|
|
1394
|
+
coreOutputs = coreBootstrapOutputs(
|
|
1395
|
+
deployedBootstrapStack(
|
|
1396
|
+
JSON.parse(await readFile5(coreStackPath, "utf8")),
|
|
1397
|
+
bootstrapContractVersion(await bootstrapTemplate("core"))
|
|
1398
|
+
)
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
const supplied = requiredBootstrapParameters(kind, bootstrapValues(config, coreOutputs));
|
|
1402
|
+
let parameters = supplied;
|
|
1403
|
+
const deployedStackPath = option("--deployed-stack-json");
|
|
1404
|
+
if (deployedStackPath) {
|
|
1405
|
+
const stack = deployedBootstrapStack(
|
|
1406
|
+
JSON.parse(await readFile5(deployedStackPath, "utf8")),
|
|
1407
|
+
bootstrapContractVersion(await bootstrapTemplate(kind))
|
|
1408
|
+
);
|
|
1409
|
+
parameters = mergeBootstrapParameters(supplied, stack.Parameters ?? []);
|
|
1410
|
+
}
|
|
1411
|
+
await writeFile3(output, `${JSON.stringify(parameters, null, 2)}
|
|
1412
|
+
`);
|
|
1413
|
+
console.log(
|
|
1414
|
+
JSON.stringify({ output, kind, preservedDeployedValues: Boolean(deployedStackPath) })
|
|
1415
|
+
);
|
|
1416
|
+
} else if (action === "status") {
|
|
1417
|
+
const stackName = requiredOption("--stack-name", bootstrapStackName(kind, config));
|
|
1418
|
+
const region = option("--region", config.region);
|
|
1419
|
+
const awsCommand = [
|
|
1420
|
+
"aws",
|
|
1421
|
+
"cloudformation",
|
|
1422
|
+
"describe-stacks",
|
|
1423
|
+
"--stack-name",
|
|
1424
|
+
stackName,
|
|
1425
|
+
...region ? ["--region", region] : [],
|
|
1426
|
+
"--no-cli-pager"
|
|
1427
|
+
];
|
|
1428
|
+
console.log(
|
|
1429
|
+
JSON.stringify({
|
|
1430
|
+
kind,
|
|
1431
|
+
contractVersion: bootstrapContractVersion(await bootstrapTemplate(kind)),
|
|
1432
|
+
awsCommand,
|
|
1433
|
+
shellCommand: args.includes("--format-shell") ? shellCommand(awsCommand) : void 0
|
|
1434
|
+
})
|
|
1435
|
+
);
|
|
1436
|
+
} else if (action === "change-set") {
|
|
1437
|
+
const stackName = requiredOption("--stack-name", bootstrapStackName(kind, config));
|
|
1438
|
+
const changeSetName = requiredOption("--change-set-name");
|
|
1439
|
+
const parametersPath = requiredOption("--parameters");
|
|
1440
|
+
const region = option("--region", config.region);
|
|
1441
|
+
await existingParameters(parametersPath);
|
|
1442
|
+
const awsCommand = [
|
|
1443
|
+
"aws",
|
|
1444
|
+
"cloudformation",
|
|
1445
|
+
"create-change-set",
|
|
1446
|
+
"--stack-name",
|
|
1447
|
+
stackName,
|
|
1448
|
+
"--change-set-name",
|
|
1449
|
+
changeSetName,
|
|
1450
|
+
"--change-set-type",
|
|
1451
|
+
option("--change-set-type", "UPDATE") ?? "UPDATE",
|
|
1452
|
+
"--template-body",
|
|
1453
|
+
`file://${await bootstrapTemplatePath(kind)}`,
|
|
1454
|
+
"--parameters",
|
|
1455
|
+
`file://${parametersPath}`,
|
|
1456
|
+
"--capabilities",
|
|
1457
|
+
"CAPABILITY_NAMED_IAM",
|
|
1458
|
+
...region ? ["--region", region] : [],
|
|
1459
|
+
"--no-cli-pager"
|
|
1460
|
+
];
|
|
1461
|
+
console.log(
|
|
1462
|
+
JSON.stringify({
|
|
1463
|
+
kind,
|
|
1464
|
+
reviewRequired: true,
|
|
1465
|
+
awsCommand,
|
|
1466
|
+
shellCommand: args.includes("--format-shell") ? shellCommand(awsCommand) : void 0
|
|
1467
|
+
})
|
|
1468
|
+
);
|
|
1469
|
+
} else {
|
|
1470
|
+
throw new Error(
|
|
1471
|
+
"Usage: ze-great-dashboard-aws bootstrap template|parameters|status|change-set --kind core|github-oidc [options]"
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1254
1474
|
} else if (args[0] !== "package")
|
|
1255
1475
|
throw new Error(
|
|
1256
|
-
"Usage: ze-great-dashboard-aws package|parameters|publish-assets|deploy|doctor [options]"
|
|
1476
|
+
"Usage: ze-great-dashboard-aws package|parameters|bootstrap|publish-assets|deploy|doctor [options]"
|
|
1257
1477
|
);
|
|
1258
1478
|
else {
|
|
1259
1479
|
const boardConfig = option("--board-config");
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { type BootstrapConfig, type BootstrapKind, bootstrapContractVersion, bootstrapTemplate, bootstrapTemplatePath, type CloudFormationParameterValue, coreBootstrapOutputs, type DeployedBootstrapStack, deployedBootstrapStack, mergeBootstrapParameters, requiredBootstrapParameters, } from './bootstrap.js';
|
|
1
2
|
export type ReleaseMetadata = {
|
|
2
3
|
dashboardVersion: string;
|
|
3
4
|
clientAssetUrl: string;
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// packages/aws/src/index.ts
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
|
-
import { cp, mkdir as mkdir2, readFile as
|
|
3
|
+
import { cp, mkdir as mkdir2, readFile as readFile3, rm, writeFile as writeFile2 } from "node:fs/promises";
|
|
4
4
|
import { join as join2, resolve as resolve2 } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
|
|
8
8
|
// node_modules/fflate/esm/index.mjs
|
|
@@ -825,6 +825,80 @@ function sha256(value) {
|
|
|
825
825
|
return createHash("sha256").update(value).digest("hex");
|
|
826
826
|
}
|
|
827
827
|
|
|
828
|
+
// packages/aws/src/bootstrap.ts
|
|
829
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
830
|
+
import { fileURLToPath } from "node:url";
|
|
831
|
+
var templates = {
|
|
832
|
+
core: "../bootstrap/core-v1.yml",
|
|
833
|
+
"github-oidc": "../bootstrap/github-oidc-v1.yml"
|
|
834
|
+
};
|
|
835
|
+
async function bootstrapTemplatePath(kind) {
|
|
836
|
+
if (!Object.hasOwn(templates, kind)) throw new Error(`Unknown bootstrap template: ${kind}`);
|
|
837
|
+
return fileURLToPath(new URL(templates[kind], import.meta.url));
|
|
838
|
+
}
|
|
839
|
+
async function bootstrapTemplate(kind) {
|
|
840
|
+
return readFile2(await bootstrapTemplatePath(kind), "utf8");
|
|
841
|
+
}
|
|
842
|
+
function bootstrapContractVersion(template) {
|
|
843
|
+
const version = template.match(/BootstrapContractVersion:\s*\{\s*Value:\s*'([^']+)'\s*}/)?.[1];
|
|
844
|
+
if (!version) throw new Error("Bootstrap template has no BootstrapContractVersion output");
|
|
845
|
+
return version;
|
|
846
|
+
}
|
|
847
|
+
function mergeBootstrapParameters(requested, deployed) {
|
|
848
|
+
const supplied = new Map(requested.map((entry) => [entry.ParameterKey, entry.ParameterValue]));
|
|
849
|
+
const duplicates = requested.map((entry) => entry.ParameterKey);
|
|
850
|
+
if (new Set(duplicates).size !== duplicates.length)
|
|
851
|
+
throw new Error("Bootstrap parameter file contains duplicate parameters");
|
|
852
|
+
return deployed.map(({ ParameterKey, ParameterValue }) => ({
|
|
853
|
+
ParameterKey,
|
|
854
|
+
ParameterValue: supplied.get(ParameterKey) ?? ParameterValue
|
|
855
|
+
})).concat(
|
|
856
|
+
requested.filter(
|
|
857
|
+
({ ParameterKey }) => !deployed.some((entry) => entry.ParameterKey === ParameterKey)
|
|
858
|
+
)
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
function deployedBootstrapStack(input, expectedContractVersion) {
|
|
862
|
+
const stack = input && typeof input === "object" && "Stacks" in input ? input.Stacks?.[0] : input;
|
|
863
|
+
if (!stack || typeof stack !== "object") throw new Error("Deployed stack JSON contains no stack");
|
|
864
|
+
const deployed = stack;
|
|
865
|
+
const contract = deployed.Outputs?.find(
|
|
866
|
+
(output) => output.OutputKey === "BootstrapContractVersion"
|
|
867
|
+
)?.OutputValue;
|
|
868
|
+
if (contract !== expectedContractVersion)
|
|
869
|
+
throw new Error(
|
|
870
|
+
`Bootstrap contract mismatch (deployed: ${contract ?? "missing"}; expected: ${expectedContractVersion}); follow the documented migration procedure`
|
|
871
|
+
);
|
|
872
|
+
if (deployed.Parameters && !Array.isArray(deployed.Parameters))
|
|
873
|
+
throw new Error("Deployed stack JSON has invalid Parameters");
|
|
874
|
+
return deployed;
|
|
875
|
+
}
|
|
876
|
+
function coreBootstrapOutputs(stack) {
|
|
877
|
+
const values = Object.fromEntries(
|
|
878
|
+
(stack.Outputs ?? []).filter(
|
|
879
|
+
(output) => Boolean(output.OutputValue)
|
|
880
|
+
).map((output) => [output.OutputKey, output.OutputValue])
|
|
881
|
+
);
|
|
882
|
+
const required = ["ArtifactBucketName", "ApplicationStackName", "CloudFormationExecutionRoleArn"];
|
|
883
|
+
const missing = required.filter((key) => !values[key]);
|
|
884
|
+
if (missing.length) throw new Error(`Core stack JSON is missing outputs: ${missing.join(", ")}`);
|
|
885
|
+
return values;
|
|
886
|
+
}
|
|
887
|
+
function requiredBootstrapParameters(kind, values) {
|
|
888
|
+
const keys = kind === "core" ? ["ArtifactBucketName", "ApplicationStackName", "DashboardFunctionName"] : [
|
|
889
|
+
"GitHubOidcProviderArn",
|
|
890
|
+
"GitHubRepository",
|
|
891
|
+
"GitHubEnvironment",
|
|
892
|
+
"ApplicationStackName",
|
|
893
|
+
"ArtifactBucketName",
|
|
894
|
+
"CloudFormationExecutionRoleArn"
|
|
895
|
+
];
|
|
896
|
+
const missing = keys.filter((key) => !values[key]);
|
|
897
|
+
if (missing.length) throw new Error(`Missing required bootstrap values: ${missing.join(", ")}`);
|
|
898
|
+
const optional = kind === "core" ? ["RuntimeSecretArn", "ArtifactKmsKeyArn"] : [];
|
|
899
|
+
return [...keys, ...optional].filter((key) => values[key] !== void 0).map((ParameterKey) => ({ ParameterKey, ParameterValue: values[ParameterKey] ?? "" }));
|
|
900
|
+
}
|
|
901
|
+
|
|
828
902
|
// packages/aws/src/index.ts
|
|
829
903
|
var run = promisify(execFile);
|
|
830
904
|
function deploymentTemplate(template, values) {
|
|
@@ -854,7 +928,7 @@ async function packageLambda(options) {
|
|
|
854
928
|
await mkdir2(outputDir, { recursive: true });
|
|
855
929
|
const runtimeDir = join2(outputDir, "lambda");
|
|
856
930
|
await mkdir2(runtimeDir, { recursive: true });
|
|
857
|
-
const lambdaSource =
|
|
931
|
+
const lambdaSource = fileURLToPath2(new URL("../dist/lambda.mjs", import.meta.url));
|
|
858
932
|
await cp(lambdaSource, join2(runtimeDir, "index.mjs"));
|
|
859
933
|
const release = await assembleRelease({
|
|
860
934
|
boardConfigPath: options.boardConfigPath,
|
|
@@ -867,7 +941,7 @@ async function packageLambda(options) {
|
|
|
867
941
|
...release.metadata,
|
|
868
942
|
artifactChecksums: {
|
|
869
943
|
...release.metadata.artifactChecksums,
|
|
870
|
-
"index.mjs": sha256(await
|
|
944
|
+
"index.mjs": sha256(await readFile3(join2(runtimeDir, "index.mjs")))
|
|
871
945
|
}
|
|
872
946
|
};
|
|
873
947
|
await writeFile2(join2(runtimeDir, "release.json"), `${JSON.stringify(runtimeMetadata, null, 2)}
|
|
@@ -882,7 +956,7 @@ async function packageLambda(options) {
|
|
|
882
956
|
archiveFiles.map(async (name) => [
|
|
883
957
|
name,
|
|
884
958
|
[
|
|
885
|
-
strToU8(await
|
|
959
|
+
strToU8(await readFile3(join2(runtimeDir, name), "utf8")),
|
|
886
960
|
{ mtime: new Date(1980, 0, 1, 0, 0, 0), level: 9 }
|
|
887
961
|
]
|
|
888
962
|
])
|
|
@@ -890,7 +964,7 @@ async function packageLambda(options) {
|
|
|
890
964
|
);
|
|
891
965
|
await writeFile2(lambdaPath, zipSync(archive));
|
|
892
966
|
await rm(runtimeDir, { recursive: true, force: true });
|
|
893
|
-
const lambdaChecksum = sha256(await
|
|
967
|
+
const lambdaChecksum = sha256(await readFile3(lambdaPath));
|
|
894
968
|
const deploymentChecksum = sha256(JSON.stringify(runtimeMetadata));
|
|
895
969
|
const packagedRelease = {
|
|
896
970
|
...runtimeMetadata,
|
|
@@ -910,7 +984,7 @@ async function packageLambda(options) {
|
|
|
910
984
|
}
|
|
911
985
|
async function publishClientAssets(options) {
|
|
912
986
|
const assetsDir = resolve2(options.assetsDir);
|
|
913
|
-
await
|
|
987
|
+
await readFile3(join2(assetsDir, "index.html"));
|
|
914
988
|
const assetPath = `${options.assetsBaseUrl.replace(/\/+$/, "")}/dashboard/${options.version}`;
|
|
915
989
|
await run("aws", [
|
|
916
990
|
"s3",
|
|
@@ -935,8 +1009,8 @@ async function publishClientAssets(options) {
|
|
|
935
1009
|
async function deployLambda(options) {
|
|
936
1010
|
const artifactDir = resolve2(options.artifactDir);
|
|
937
1011
|
const assetsDir = resolve2(options.assetsDir);
|
|
938
|
-
await
|
|
939
|
-
await
|
|
1012
|
+
await readFile3(join2(artifactDir, "lambda.zip"));
|
|
1013
|
+
await readFile3(join2(assetsDir, "index.html"));
|
|
940
1014
|
if (options.dryRun) return;
|
|
941
1015
|
const assetPath = await publishClientAssets(options);
|
|
942
1016
|
await run("aws", [
|
|
@@ -961,11 +1035,18 @@ async function deployLambda(options) {
|
|
|
961
1035
|
await run("aws", ["lambda", "wait", "function-updated", "--function-name", options.functionName]);
|
|
962
1036
|
}
|
|
963
1037
|
async function cloudFormationTemplate() {
|
|
964
|
-
return
|
|
1038
|
+
return readFile3(fileURLToPath2(new URL("../template.yml", import.meta.url)), "utf8");
|
|
965
1039
|
}
|
|
966
1040
|
export {
|
|
1041
|
+
bootstrapContractVersion,
|
|
1042
|
+
bootstrapTemplate,
|
|
1043
|
+
bootstrapTemplatePath,
|
|
967
1044
|
cloudFormationTemplate,
|
|
1045
|
+
coreBootstrapOutputs,
|
|
968
1046
|
deployLambda,
|
|
1047
|
+
deployedBootstrapStack,
|
|
1048
|
+
mergeBootstrapParameters,
|
|
969
1049
|
packageLambda,
|
|
970
|
-
publishClientAssets
|
|
1050
|
+
publishClientAssets,
|
|
1051
|
+
requiredBootstrapParameters
|
|
971
1052
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@continuous-excellence/ze-great-dashboard-aws",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AWS Lambda and CloudFormation adapter for Ze Great Dashboard.",
|
|
6
6
|
"keywords": [
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
"dist",
|
|
31
31
|
"client",
|
|
32
|
+
"bootstrap",
|
|
32
33
|
"template.yml",
|
|
33
34
|
"LICENSE"
|
|
34
35
|
],
|
package/template.yml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
AWSTemplateFormatVersion: '2010-09-09'
|
|
2
|
-
Description:
|
|
2
|
+
Description: Ze Great Dashboard private Lambda application (integrate through a customer-managed gateway)
|
|
3
3
|
|
|
4
4
|
Metadata:
|
|
5
5
|
PackageManagedParameters: [LambdaArtifactKey, DashboardVersion]
|
|
@@ -67,17 +67,7 @@ Resources:
|
|
|
67
67
|
BOARD_CONFIG_URL: !Ref BoardConfigPath
|
|
68
68
|
HOST: 0.0.0.0
|
|
69
69
|
SECRET_REFERENCE: !If [HasSecretReference, !Ref SecretReference, !Ref AWS::NoValue]
|
|
70
|
-
ServerUrl:
|
|
71
|
-
Type: AWS::Lambda::Url
|
|
72
|
-
Properties: { TargetFunctionArn: !GetAtt ServerFunction.Arn, AuthType: NONE }
|
|
73
|
-
ServerUrlPermission:
|
|
74
|
-
Type: AWS::Lambda::Permission
|
|
75
|
-
Properties: { FunctionName: !Ref ServerFunction, Action: lambda:InvokeFunctionUrl, Principal: '*', FunctionUrlAuthType: NONE }
|
|
76
|
-
ServerInvokePermission:
|
|
77
|
-
Type: AWS::Lambda::Permission
|
|
78
|
-
Properties: { FunctionName: !Ref ServerFunction, Action: lambda:InvokeFunction, Principal: '*', InvokedViaFunctionUrl: true }
|
|
79
|
-
|
|
80
70
|
Outputs:
|
|
81
|
-
ServerUrl: { Value: !GetAtt ServerUrl.FunctionUrl }
|
|
82
71
|
ServerFunctionName: { Value: !Ref ServerFunction }
|
|
72
|
+
ServerFunctionArn: { Value: !GetAtt ServerFunction.Arn }
|
|
83
73
|
AssetPath: { Value: !Sub '${AssetBaseUrl}/dashboard/${DashboardVersion}' }
|