sst 2.0.0-rc.25 → 2.0.0-rc.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.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: never;
3
+ bucket: never;
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,121 @@ 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();
19
- if (!ret.version ||
20
- !ret.bucket ||
21
- !ret.stack ||
22
- ret.version !== LATEST_VERSION) {
26
+ let status = await loadBootstrapStatus();
27
+ if (!status || status.version !== LATEST_VERSION) {
23
28
  const project = useProject();
24
29
  const spinner = createSpinner("Deploying bootstrap stack, this only needs to happen once").start();
30
+ // Create bootstrap stack
25
31
  const app = new App();
26
- const stack = new Stack(app, "SSTBootstrap", {
32
+ const stack = new Stack(app, STACK_NAME, {
27
33
  env: {
28
34
  region: project.config.region,
29
35
  },
30
36
  });
37
+ // Add tags to stack
31
38
  const tags = {};
32
39
  for (const [key, value] of Object.entries(tags)) {
33
40
  Tags.of(app).add(key, value);
34
41
  }
42
+ // Create S3 bucket to store stacks metadata
35
43
  const bucket = new Bucket(stack, project.config.region, {
36
44
  encryption: BucketEncryption.S3_MANAGED,
37
45
  removalPolicy: RemovalPolicy.DESTROY,
38
46
  autoDeleteObjects: true,
39
47
  blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
40
48
  });
41
- new StringParameter(stack, SSM_NAME_VERSION, {
42
- parameterName: SSM_NAME_VERSION,
43
- stringValue: LATEST_VERSION,
44
- tier: ParameterTier.STANDARD,
49
+ // Create Function and subscribe to CloudFormation events
50
+ const fn = new Function(stack, "MetadataHandler", {
51
+ code: Code.fromAsset(path.resolve(__dirname, "support/bootstrap-metadata-function")),
52
+ handler: "index.handler",
53
+ runtime: Runtime.NODEJS_16_X,
54
+ initialPolicy: [
55
+ new PolicyStatement({
56
+ actions: ["cloudformation:DescribeStacks"],
57
+ resources: ["*"],
58
+ }),
59
+ new PolicyStatement({
60
+ actions: ["s3:PutObject", "s3:DeleteObject"],
61
+ resources: [bucket.bucketArn + "/*"],
62
+ }),
63
+ ],
45
64
  });
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,
65
+ const queue = new Queue(stack, "MetadataQueue");
66
+ fn.addEventSource(new SqsEventSource(queue));
67
+ const rule = new Rule(stack, "MetadataRule", {
68
+ eventPattern: {
69
+ source: ["aws.cloudformation"],
70
+ detailType: ["CloudFormation Stack Status Change"],
71
+ detail: {
72
+ "status-details": {
73
+ status: [
74
+ "CREATE_COMPLETE",
75
+ "UPDATE_COMPLETE",
76
+ "UPDATE_ROLLBACK_COMPLETE",
77
+ "ROLLBACK_COMPLETE",
78
+ "DELETE_COMPLETE",
79
+ ],
80
+ }
81
+ }
82
+ }
55
83
  });
84
+ rule.addTarget(new SqsQueue(queue, {
85
+ retryAttempts: 10,
86
+ }));
87
+ // Create stack outputs to store bootstrap stack info
88
+ new CfnOutput(stack, OUTPUT_VERSION, { value: LATEST_VERSION });
89
+ new CfnOutput(stack, OUTPUT_BUCKET, { value: bucket.bucketName });
90
+ // Deploy bootstrap stack
56
91
  const asm = app.synth();
57
92
  const result = await Stacks.deploy(asm.stacks[0]);
58
93
  if (Stacks.isFailed(result.status)) {
59
94
  throw new VisibleError(`Failed to deploy bootstrap stack:\n${JSON.stringify(result.errors, null, 4)}`);
60
95
  }
61
96
  spinner.succeed();
62
- return ssm();
97
+ // Fetch bootstrap status
98
+ status = await loadBootstrapStatus();
99
+ if (!status) {
100
+ throw new VisibleError("Failed to deploy bootstrap stack");
101
+ }
63
102
  }
64
- Logger.debug("Loaded bootstrap info: ", JSON.stringify(ret));
65
- return ret;
103
+ Logger.debug("Loaded bootstrap info: ", JSON.stringify(status));
104
+ return status;
66
105
  });
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],
71
- }));
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
- };
106
+ async function loadBootstrapStatus() {
107
+ // Get bootstrap CloudFormation stack
108
+ const cf = useAWSClient(CloudFormationClient);
109
+ let result;
110
+ try {
111
+ result = await cf.send(new DescribeStacksCommand({
112
+ StackName: STACK_NAME,
113
+ }));
114
+ }
115
+ catch (e) {
116
+ if (e.Code === "ValidationError"
117
+ && e.message === `Stack with id ${STACK_NAME} does not exist`) {
118
+ return null;
119
+ }
120
+ throw e;
121
+ }
122
+ // Parse stack outputs
123
+ let version, bucket;
124
+ (result.Stacks[0].Outputs || []).forEach((o) => {
125
+ if (o.OutputKey === OUTPUT_VERSION) {
126
+ version = o.OutputValue;
127
+ }
128
+ else if (o.OutputKey === OUTPUT_BUCKET) {
129
+ bucket = o.OutputValue;
130
+ }
131
+ });
132
+ if (!version || !bucket) {
133
+ return null;
134
+ }
135
+ return { version, bucket };
79
136
  }
@@ -37,15 +37,20 @@ export const deploy = (program) => program.command("deploy [filter]", "Work on y
37
37
  return;
38
38
  }
39
39
  console.log(`Deploying ${bold(target.length + " stacks")} for stage ${blue(project.config.stage)}...`);
40
- let component = undefined;
41
- if (args.fullscreen) {
42
- process.stdout.write("\x1b[?1049h");
43
- component = render(React.createElement(DeploymentUI, { stacks: assembly.stacks.map((s) => s.stackName) }));
44
- }
40
+ const cleanup = (() => {
41
+ if (args.fullscreen) {
42
+ process.stdout.write("\x1b[?1049h");
43
+ const component = render(React.createElement(DeploymentUI, { stacks: assembly.stacks.map((s) => s.stackName) }));
44
+ return () => {
45
+ component.unmount();
46
+ process.stdout.write("\x1b[?1049l");
47
+ };
48
+ }
49
+ const spinner = createSpinner("Deploying stacks");
50
+ return () => spinner.succeed();
51
+ })();
45
52
  const results = await Stacks.deployMany(target);
46
- if (component)
47
- component.unmount();
48
- process.stdout.write("\x1b[?1049l");
53
+ cleanup();
49
54
  printDeploymentResults(results);
50
55
  if (Object.values(results).some((stack) => Stacks.isFailed(stack.status)))
51
56
  process.exit(1);
@@ -108,15 +108,20 @@ export const dev = (program) => program.command(["start", "dev"], "Work on your
108
108
  const assembly = pending;
109
109
  const nextChecksum = await checksum(assembly.directory);
110
110
  pending = undefined;
111
- let component = undefined;
112
- if (args.fullscreen) {
113
- process.stdout.write("\x1b[?1049h");
114
- component = render(React.createElement(DeploymentUI, { stacks: assembly.stacks.map((s) => s.stackName) }));
115
- }
111
+ const cleanup = (() => {
112
+ if (args.fullscreen) {
113
+ process.stdout.write("\x1b[?1049h");
114
+ const component = render(React.createElement(DeploymentUI, { stacks: assembly.stacks.map((s) => s.stackName) }));
115
+ return () => {
116
+ component.unmount();
117
+ process.stdout.write("\x1b[?1049l");
118
+ };
119
+ }
120
+ const spinner = createSpinner("Deploying stacks");
121
+ return () => spinner.succeed();
122
+ })();
116
123
  const results = await Stacks.deployMany(assembly.stacks);
117
- if (component)
118
- component.unmount();
119
- process.stdout.write("\x1b[?1049l");
124
+ cleanup();
120
125
  lastDeployed = nextChecksum;
121
126
  printDeploymentResults(results);
122
127
  const keys = await SiteEnv.keys();
@@ -8,6 +8,8 @@ export declare const remove: (program: Program) => import("yargs").Argv<{
8
8
  region: string | undefined;
9
9
  } & {
10
10
  from: string | undefined;
11
+ } & {
12
+ fullscreen: boolean;
11
13
  } & {
12
14
  filter: string | undefined;
13
15
  }>;
@@ -1,6 +1,12 @@
1
+ import { createSpinner } from "../spinner.js";
1
2
  import { printDeploymentResults } from "../ui/deploy.js";
2
3
  export const remove = (program) => program.command("remove [filter]", "Remove all stacks for this app", (yargs) => yargs
3
4
  .option("from", { type: "string" })
5
+ .option("fullscreen", {
6
+ type: "boolean",
7
+ describe: "Disable full screen UI",
8
+ default: true,
9
+ })
4
10
  .positional("filter", { type: "string" }), async (args) => {
5
11
  const React = await import("react");
6
12
  const { CloudAssembly } = await import("aws-cdk-lib/cx-api");
@@ -28,11 +34,22 @@ export const remove = (program) => program.command("remove [filter]", "Remove al
28
34
  process.exit(1);
29
35
  }
30
36
  console.log(`Removing ${bold(target.length + " stacks")} for stage ${blue(project.config.stage)}...`);
31
- process.stdout.write("\x1b[?1049h");
32
- const component = render(React.createElement(DeploymentUI, { stacks: target.map((s) => s.stackName) }));
37
+ const cleanup = (() => {
38
+ if (args.fullscreen) {
39
+ process.stdout.write("\x1b[?1049h");
40
+ const component = render(React.createElement(DeploymentUI, { stacks: assembly.stacks.map((s) => s.stackName) }));
41
+ return () => {
42
+ component.unmount();
43
+ process.stdout.write("\x1b[?1049l");
44
+ };
45
+ }
46
+ const spinner = createSpinner("Removing stacks");
47
+ return () => spinner.succeed();
48
+ })();
33
49
  const results = await Stacks.removeMany(target);
34
- component.unmount();
35
- process.stdout.write("\x1b[?1049l");
50
+ cleanup();
36
51
  printDeploymentResults(results);
52
+ if (Object.values(results).some((stack) => Stacks.isFailed(stack.status)))
53
+ process.exit(1);
37
54
  process.exit(0);
38
55
  });
package/cli/ui/deploy.js CHANGED
@@ -76,7 +76,10 @@ export const DeploymentUI = (props) => {
76
76
  React.createElement(Text, null,
77
77
  "Deploying ",
78
78
  React.createElement(Text, { color: "bold" }, props.stacks.length),
79
- " stacks for stage ",
79
+ " stack",
80
+ props.stacks.length > 1 && "s",
81
+ " for stage",
82
+ " ",
80
83
  React.createElement(Text, { color: "blue" }, useProject().config.stage)),
81
84
  Object.entries(stacks).map(([stackID, status]) => {
82
85
  return (React.createElement(React.Fragment, { key: stackID },
@@ -106,9 +109,9 @@ export const DeploymentUI = (props) => {
106
109
  };
107
110
  export function printDeploymentResults(results) {
108
111
  console.log();
109
- console.log(`----------------------------`);
110
- console.log(`| Stack deployment results |`);
111
- console.log(`----------------------------`);
112
+ console.log(`-----------`);
113
+ console.log(`| Summary |`);
114
+ console.log(`-----------`);
112
115
  for (const [stack, result] of Object.entries(results)) {
113
116
  const icon = (() => {
114
117
  if (Stacks.isSuccess(result.status))
@@ -122,6 +125,8 @@ export function printDeploymentResults(results) {
122
125
  return false;
123
126
  if (key.includes("SstSiteEnv"))
124
127
  return false;
128
+ if (key === "SstMetadata")
129
+ return false;
125
130
  return true;
126
131
  });
127
132
  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.25",
3
+ "version": "2.0.0-rc.28",
4
4
  "bin": {
5
5
  "sst": "cli/sst.js"
6
6
  },