sst 2.0.0-rc.24 → 2.0.0-rc.26

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.d.ts CHANGED
@@ -1,6 +1,4 @@
1
- export declare const LATEST_VERSION = "3";
2
1
  export declare const useBootstrap: () => Promise<{
3
- version: string | undefined;
4
- bucket: string | undefined;
5
- stack: string | undefined;
2
+ version: undefined;
3
+ bucket: undefined;
6
4
  }>;
package/bootstrap.js CHANGED
@@ -1,7 +1,14 @@
1
- import { GetParametersCommand, SSMClient } from "@aws-sdk/client-ssm";
2
- import { Tags, Stack, RemovalPolicy, App } from "aws-cdk-lib";
1
+ import url from "url";
2
+ import path from "path";
3
+ import { DescribeStacksCommand, CloudFormationClient, } from "@aws-sdk/client-cloudformation";
4
+ import { Tags, Stack, RemovalPolicy, App, CfnOutput } from "aws-cdk-lib";
5
+ import { Function, Runtime, Code } from "aws-cdk-lib/aws-lambda";
6
+ import { SqsEventSource } from "aws-cdk-lib/aws-lambda-event-sources";
7
+ import { PolicyStatement } from "aws-cdk-lib/aws-iam";
8
+ import { Queue } from "aws-cdk-lib/aws-sqs";
9
+ import { Rule } from "aws-cdk-lib/aws-events";
10
+ import { SqsQueue } from "aws-cdk-lib/aws-events-targets";
3
11
  import { BlockPublicAccess, Bucket, BucketEncryption, } from "aws-cdk-lib/aws-s3";
4
- import { ParameterTier, StringParameter } from "aws-cdk-lib/aws-ssm";
5
12
  import { useProject } from "./project.js";
6
13
  import { createSpinner } from "./cli/spinner.js";
7
14
  import { Context } from "./context/context.js";
@@ -9,71 +16,103 @@ import { useAWSClient } from "./credentials.js";
9
16
  import { VisibleError } from "./error.js";
10
17
  import { Logger } from "./logger.js";
11
18
  import { Stacks } from "./stacks/index.js";
12
- const SSM_NAME_VERSION = `/sst/bootstrap/version`;
13
- const SSM_NAME_STACK_NAME = `/sst/bootstrap/stack-name`;
14
- const SSM_NAME_BUCKET_NAME = `/sst/bootstrap/bucket-name`;
15
- export const LATEST_VERSION = "3";
19
+ const STACK_NAME = "SSTBootstrap";
20
+ const OUTPUT_VERSION = "Version";
21
+ const OUTPUT_BUCKET = "BucketName";
22
+ const LATEST_VERSION = "4";
23
+ const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
16
24
  export const useBootstrap = Context.memo(async () => {
17
25
  Logger.debug("Initializing bootstrap context");
18
- const ret = await ssm();
26
+ const ret = await loadBootstrapStatus();
19
27
  if (!ret.version ||
20
28
  !ret.bucket ||
21
- !ret.stack ||
22
29
  ret.version !== LATEST_VERSION) {
23
30
  const project = useProject();
24
31
  const spinner = createSpinner("Deploying bootstrap stack, this only needs to happen once").start();
32
+ // Create bootstrap stack
25
33
  const app = new App();
26
- const stack = new Stack(app, "SSTBootstrap", {
34
+ const stack = new Stack(app, STACK_NAME, {
27
35
  env: {
28
36
  region: project.config.region,
29
37
  },
30
38
  });
39
+ // Add tags to stack
31
40
  const tags = {};
32
41
  for (const [key, value] of Object.entries(tags)) {
33
42
  Tags.of(app).add(key, value);
34
43
  }
44
+ // Create S3 bucket to store stacks metadata
35
45
  const bucket = new Bucket(stack, project.config.region, {
36
46
  encryption: BucketEncryption.S3_MANAGED,
37
47
  removalPolicy: RemovalPolicy.DESTROY,
38
48
  autoDeleteObjects: true,
39
49
  blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
40
50
  });
41
- new StringParameter(stack, SSM_NAME_VERSION, {
42
- parameterName: SSM_NAME_VERSION,
43
- stringValue: LATEST_VERSION,
44
- tier: ParameterTier.STANDARD,
51
+ // Create Function and subscribe to CloudFormation events
52
+ const fn = new Function(stack, "MetadataHandler", {
53
+ code: Code.fromAsset(path.resolve(__dirname, "support/bootstrap-metadata-function")),
54
+ handler: "index.handler",
55
+ runtime: Runtime.NODEJS_16_X,
56
+ initialPolicy: [
57
+ new PolicyStatement({
58
+ actions: ["cloudformation:DescribeStacks"],
59
+ resources: ["*"],
60
+ }),
61
+ new PolicyStatement({
62
+ actions: ["s3:PutObject", "s3:DeleteObject"],
63
+ resources: [bucket.bucketArn + "/*"],
64
+ }),
65
+ ],
45
66
  });
46
- new StringParameter(stack, SSM_NAME_STACK_NAME, {
47
- parameterName: SSM_NAME_STACK_NAME,
48
- stringValue: stack.stackName,
49
- tier: ParameterTier.STANDARD,
50
- });
51
- new StringParameter(stack, SSM_NAME_BUCKET_NAME, {
52
- parameterName: SSM_NAME_BUCKET_NAME,
53
- stringValue: bucket.bucketName,
54
- tier: ParameterTier.STANDARD,
67
+ const queue = new Queue(stack, "MetadataQueue");
68
+ fn.addEventSource(new SqsEventSource(queue));
69
+ const rule = new Rule(stack, "MetadataRule", {
70
+ eventPattern: {
71
+ source: ["aws.cloudformation"],
72
+ detailType: ["CloudFormation Stack Status Change"],
73
+ detail: {
74
+ "status-details": {
75
+ status: [
76
+ "CREATE_COMPLETE",
77
+ "UPDATE_COMPLETE",
78
+ "UPDATE_ROLLBACK_COMPLETE",
79
+ "ROLLBACK_COMPLETE",
80
+ "DELETE_COMPLETE",
81
+ ],
82
+ }
83
+ }
84
+ }
55
85
  });
86
+ rule.addTarget(new SqsQueue(queue, {
87
+ retryAttempts: 10,
88
+ }));
89
+ // Create stack outputs to store bootstrap stack info
90
+ new CfnOutput(stack, OUTPUT_VERSION, { value: LATEST_VERSION });
91
+ new CfnOutput(stack, OUTPUT_BUCKET, { value: bucket.bucketName });
56
92
  const asm = app.synth();
57
93
  const result = await Stacks.deploy(asm.stacks[0]);
58
94
  if (Stacks.isFailed(result.status)) {
59
95
  throw new VisibleError(`Failed to deploy bootstrap stack:\n${JSON.stringify(result.errors, null, 4)}`);
60
96
  }
61
97
  spinner.succeed();
62
- return ssm();
98
+ return loadBootstrapStatus();
63
99
  }
64
100
  Logger.debug("Loaded bootstrap info: ", JSON.stringify(ret));
65
101
  return ret;
66
102
  });
67
- async function ssm() {
68
- const ssm = useAWSClient(SSMClient);
69
- const result = await ssm.send(new GetParametersCommand({
70
- Names: [SSM_NAME_VERSION, SSM_NAME_STACK_NAME, SSM_NAME_BUCKET_NAME],
103
+ async function loadBootstrapStatus() {
104
+ const cf = useAWSClient(CloudFormationClient);
105
+ const result = await cf.send(new DescribeStacksCommand({
106
+ StackName: STACK_NAME,
71
107
  }));
72
- return {
73
- version: result.Parameters.find((p) => p.Name === SSM_NAME_VERSION)?.Value,
74
- bucket: result.Parameters.find((p) => p.Name === SSM_NAME_BUCKET_NAME)
75
- ?.Value,
76
- stack: result.Parameters.find((p) => p.Name === SSM_NAME_STACK_NAME)
77
- ?.Value,
78
- };
108
+ let version, bucket;
109
+ (result.Stacks[0].Outputs || []).forEach((o) => {
110
+ if (o.OutputKey === OUTPUT_VERSION) {
111
+ version = o.OutputValue;
112
+ }
113
+ else if (o.OutputKey === OUTPUT_BUCKET) {
114
+ bucket = o.OutputValue;
115
+ }
116
+ });
117
+ return { version, bucket };
79
118
  }
package/cli/ui/deploy.js CHANGED
@@ -122,6 +122,8 @@ export function printDeploymentResults(results) {
122
122
  return false;
123
123
  if (key.includes("SstSiteEnv"))
124
124
  return false;
125
+ if (key === "SstMetadata")
126
+ return false;
125
127
  return true;
126
128
  });
127
129
  if (outputs.length > 0) {
package/constructs/App.js CHANGED
@@ -304,7 +304,15 @@ export class App extends cdk.App {
304
304
  for (const child of this.node.children) {
305
305
  if (child instanceof Stack) {
306
306
  const stackName = child.node.id;
307
- child.setStackMetadata(byStack[stackName] || []);
307
+ child.addOutputs({
308
+ SstMetadata: JSON.stringify({
309
+ app: this.name,
310
+ stage: this.stage,
311
+ version: useProject().version,
312
+ bootstrapBucket: this.bootstrap.bucket,
313
+ metadata: byStack[stackName] || [],
314
+ }),
315
+ });
308
316
  }
309
317
  }
310
318
  }
@@ -32,7 +32,6 @@ export declare class Stack extends cdk.Stack {
32
32
  * @internal
33
33
  */
34
34
  readonly customResourceHandler: lambda.Function;
35
- private readonly metadata;
36
35
  constructor(scope: Construct, id: string, props?: StackProps);
37
36
  /**
38
37
  * The default function props to be applied to all the Lambda functions in the stack.
@@ -129,9 +128,8 @@ export declare class Stack extends cdk.Stack {
129
128
  * ```
130
129
  */
131
130
  addOutputs(outputs: Record<string, string | cdk.CfnOutputProps>): void;
132
- setStackMetadata(metadata: any): void;
133
131
  private createCustomResourceHandler;
134
- private createStackMetadataResource;
132
+ private createSecretsMigrationResource;
135
133
  private static checkForPropsIsConstruct;
136
134
  private static checkForEnvInProps;
137
135
  }
@@ -5,10 +5,8 @@ import * as iam from "aws-cdk-lib/aws-iam";
5
5
  import * as lambda from "aws-cdk-lib/aws-lambda";
6
6
  import { Function as Fn } from "./Function.js";
7
7
  import { isConstruct } from "./Construct.js";
8
- import { createRequire } from "module";
9
8
  import { useApp } from "./context.js";
10
9
  import { useProject } from "../project.js";
11
- const require = createRequire(import.meta.url);
12
10
  const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
13
11
  /**
14
12
  * The Stack construct extends cdk.Stack. It automatically prefixes the stack names with the stage and app name to ensure that they can be deployed to multiple regions in the same AWS account. It also ensure that the stack uses the same AWS profile and region as the app. They're defined using functions that return resources that can be imported by other stacks.
@@ -36,7 +34,6 @@ export class Stack extends cdk.Stack {
36
34
  * @internal
37
35
  */
38
36
  customResourceHandler;
39
- metadata;
40
37
  constructor(scope, id, props) {
41
38
  const root = scope.node.root;
42
39
  const stackId = root.logicalPrefixedName(id);
@@ -51,15 +48,10 @@ export class Stack extends cdk.Stack {
51
48
  });
52
49
  this.stage = root.stage;
53
50
  this.defaultFunctionProps = root.defaultFunctionProps.map((dfp) => typeof dfp === "function" ? dfp(this) : dfp);
54
- // We created the Metadata resource first with empty metadata, and on
55
- // app synthesis we'll update it with the actual metadata.
56
- // We do this two step process because we call the "Template.fromStack"
57
- // method in the tests. And the call triggers an app synethsis. And we
58
- // end up synthesize an app multiple times. If we created the Metadata
59
- // resource on app synth, the tests would fail because the resource
60
- // would already exist.
51
+ // Create a custom resource handler per stack. This handler will
52
+ // be used by all the custom resources in the stack.
61
53
  this.customResourceHandler = this.createCustomResourceHandler();
62
- this.metadata = this.createStackMetadataResource();
54
+ this.createSecretsMigrationResource();
63
55
  }
64
56
  /**
65
57
  * The default function props to be applied to all the Lambda functions in the stack.
@@ -200,9 +192,6 @@ export class Stack extends cdk.Stack {
200
192
  }
201
193
  });
202
194
  }
203
- setStackMetadata(metadata) {
204
- this.metadata.node.defaultChild.addPropertyOverride("Metadata", metadata);
205
- }
206
195
  createCustomResourceHandler() {
207
196
  return new lambda.Function(this, "CustomResourceHandler", {
208
197
  code: lambda.Code.fromAsset(path.join(__dirname, "../support/custom-resources/"), {
@@ -214,14 +203,9 @@ export class Stack extends cdk.Stack {
214
203
  memorySize: 1024,
215
204
  });
216
205
  }
217
- createStackMetadataResource() {
206
+ createSecretsMigrationResource() {
218
207
  const app = useApp();
219
- // Create execution policy
220
- this.customResourceHandler.addToRolePolicy(new iam.PolicyStatement({
221
- actions: ["s3:PutObject", "s3:DeleteObject"],
222
- resources: [`arn:aws:s3:::${app.bootstrap.bucket}/*`],
223
- }));
224
- // Temporary: Add permissions to migrate SSM paths for secrets (piggybacking on the stack metadata custom resource handler)
208
+ // Add permissions to migrate SSM paths for secrets
225
209
  this.customResourceHandler.addToRolePolicy(new iam.PolicyStatement({
226
210
  actions: ["ssm:GetParametersByPath", "ssm:PutParameter"],
227
211
  resources: [
@@ -229,18 +213,13 @@ export class Stack extends cdk.Stack {
229
213
  `arn:aws:ssm:${app.region}:${app.account}:parameter/sst/${app.name}/.fallback/*`,
230
214
  ],
231
215
  }));
232
- return new cdk.CustomResource(this, "StackMetadata", {
216
+ new cdk.CustomResource(this, "SecretsMigration", {
233
217
  serviceToken: this.customResourceHandler.functionArn,
234
- resourceType: "Custom::StackMetadata",
218
+ resourceType: "Custom::SecretsMigration",
235
219
  properties: {
236
220
  App: app.name,
237
221
  Stage: this.stage,
238
- Stack: this.stackName,
239
222
  SSTVersion: useProject().version,
240
- BootstrapBucketName: app.bootstrap.bucket,
241
- ForceUpdate: process.env.SST_FORCE_UPDATE_METADATA
242
- ? Date.now().toString()
243
- : undefined,
244
223
  },
245
224
  });
246
225
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sst",
3
- "version": "2.0.0-rc.24",
3
+ "version": "2.0.0-rc.26",
4
4
  "bin": {
5
5
  "sst": "cli/sst.js"
6
6
  },
package/site-env.js CHANGED
@@ -37,7 +37,7 @@ export async function writeValues(input) {
37
37
  await fs.promises.writeFile(file, JSON.stringify(input));
38
38
  }
39
39
  export function append(input) {
40
- input.path = path.join(useProject().paths.root, input.path);
40
+ input.path = path.resolve(useProject().paths.root, input.path);
41
41
  fs.appendFileSync(keysFile(), JSON.stringify(input) + "\n");
42
42
  }
43
43
  export function reset() {