sst 2.20.0 → 2.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -79,8 +79,9 @@ export interface FunctionProps extends Omit<FunctionOptions, "functionName" | "m
79
79
  */
80
80
  python?: PythonProps;
81
81
  /**
82
- * Used to configure image function properties
82
+ * Used to configure container function properties
83
83
  */
84
+ container?: ContainerProps;
84
85
  /**
85
86
  * Hooks to run before and after function builds
86
87
  */
@@ -507,6 +508,23 @@ export interface JavaProps {
507
508
  */
508
509
  experimentalUseProvidedRuntime?: "provided" | "provided.al2";
509
510
  }
511
+ export interface ContainerProps {
512
+ /**
513
+ * Configure docker build options
514
+ *
515
+ * @example
516
+ * ```js
517
+ * container: {
518
+ * docker: {
519
+ * cmd: ["executable", "param1", "param2"]
520
+ * }
521
+ * }
522
+ * ```
523
+ */
524
+ docker: {
525
+ cmd?: string[];
526
+ };
527
+ }
510
528
  /**
511
529
  * Used to configure additional files to copy into the function bundle
512
530
  *
@@ -1,7 +1,8 @@
1
1
  import { Construct } from "constructs";
2
+ import { Function as CdkFunction } from "aws-cdk-lib/aws-lambda";
2
3
  import { RetentionDays } from "aws-cdk-lib/aws-logs";
3
4
  import { SSTConstruct } from "./Construct.js";
4
- import { Function, NodeJSProps, FunctionCopyFilesProps } from "./Function.js";
5
+ import { NodeJSProps, FunctionCopyFilesProps } from "./Function.js";
5
6
  import { Duration } from "./util/duration.js";
6
7
  import { Permissions } from "./util/permission.js";
7
8
  import { FunctionBindingProps } from "./util/functionBinding.js";
@@ -9,7 +10,25 @@ import { ISecurityGroup, IVpc, SubnetSelection } from "aws-cdk-lib/aws-ec2";
9
10
  export type JobMemorySize = "3 GB" | "7 GB" | "15 GB" | "145 GB";
10
11
  export interface JobNodeJSProps extends NodeJSProps {
11
12
  }
13
+ export interface JobContainerProps {
14
+ /**
15
+ * Configure docker build options
16
+ *
17
+ * @example
18
+ * ```js
19
+ * container: {
20
+ * docker: {
21
+ * cmd: ["executable", "param1", "param2"]
22
+ * }
23
+ * }
24
+ * ```
25
+ */
26
+ docker: {
27
+ cmd: string[];
28
+ };
29
+ }
12
30
  export interface JobProps {
31
+ runtime?: "nodejs" | "container";
13
32
  /**
14
33
  * Path to the entry point and handler function. Of the format:
15
34
  * `/path/to/file.function`.
@@ -65,6 +84,10 @@ export interface JobProps {
65
84
  * Used to configure nodejs function properties
66
85
  */
67
86
  nodejs?: JobNodeJSProps;
87
+ /**
88
+ * Used to configure container properties
89
+ */
90
+ container?: JobContainerProps;
68
91
  /**
69
92
  * Can be used to disable Live Lambda Development when using `sst start`. Useful for things like Custom Resources that need to execute during deployment.
70
93
  *
@@ -204,10 +227,10 @@ export interface JobProps {
204
227
  */
205
228
  export declare class Job extends Construct implements SSTConstruct {
206
229
  readonly id: string;
207
- private readonly localId;
208
230
  private readonly props;
209
231
  private readonly job;
210
- readonly _jobInvoker: Function;
232
+ private readonly liveDevJob?;
233
+ readonly _jobManager: CdkFunction;
211
234
  constructor(scope: Construct, id: string, props: JobProps);
212
235
  getConstructMetadata(): {
213
236
  type: "Job";
@@ -246,15 +269,17 @@ export declare class Job extends Construct implements SSTConstruct {
246
269
  * ```
247
270
  */
248
271
  addEnvironment(name: string, value: string): void;
249
- private createCodeBuildProject;
272
+ private createCodeBuildJob;
273
+ private createLiveDevJob;
250
274
  private createLogRetention;
251
275
  private buildCodeBuildProjectCode;
252
- private updateCodeBuildProjectCode;
253
- private createLocalInvoker;
254
- private createCodeBuildInvoker;
276
+ private createJobManager;
255
277
  private bindForCodeBuild;
256
278
  private attachPermissionsForCodeBuild;
257
279
  private addEnvironmentForCodeBuild;
280
+ private validateContainerProps;
258
281
  private normalizeMemorySize;
259
282
  private normalizeTimeout;
283
+ private convertJobRuntimeToFunctionRuntime;
284
+ private convertJobContainerToFunctionContainer;
260
285
  }
package/constructs/Job.js CHANGED
@@ -2,8 +2,9 @@ import url from "url";
2
2
  import path from "path";
3
3
  import fs from "fs/promises";
4
4
  import { Construct } from "constructs";
5
+ import { Duration as CdkDuration } from "aws-cdk-lib/core";
5
6
  import { PolicyStatement, Effect } from "aws-cdk-lib/aws-iam";
6
- import { AssetCode } from "aws-cdk-lib/aws-lambda";
7
+ import { AssetCode, Code, Runtime, Function as CdkFunction, } from "aws-cdk-lib/aws-lambda";
7
8
  import { Project, LinuxBuildImage, BuildSpec, ComputeType, } from "aws-cdk-lib/aws-codebuild";
8
9
  import { RetentionDays, LogRetention } from "aws-cdk-lib/aws-logs";
9
10
  import { Stack } from "./Stack.js";
@@ -14,6 +15,7 @@ import { bindEnvironment, bindPermissions, getReferencedSecrets, } from "./util/
14
15
  import { useDeferredTasks } from "./deferred_task.js";
15
16
  import { useProject } from "../project.js";
16
17
  import { useRuntimeHandlers } from "../runtime/handlers.js";
18
+ import { Platform } from "aws-cdk-lib/aws-ecr-assets";
17
19
  const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
18
20
  /////////////////////
19
21
  // Construct
@@ -34,43 +36,41 @@ const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
34
36
  */
35
37
  export class Job extends Construct {
36
38
  id;
37
- localId;
38
39
  props;
39
40
  job;
40
- _jobInvoker;
41
+ liveDevJob;
42
+ _jobManager;
41
43
  constructor(scope, id, props) {
42
44
  super(scope, props.cdk?.id || id);
43
45
  const app = this.node.root;
44
46
  const stack = Stack.of(scope);
45
47
  this.id = id;
46
48
  this.props = props;
47
- useFunctions().add(this.node.addr, {
48
- ...props,
49
- runtime: "nodejs16.x",
50
- });
51
- this.localId = path.posix
52
- .join(scope.node.path, id)
53
- .replace(/\$/g, "-")
54
- .replace(/\//g, "-")
55
- .replace(/\./g, "-");
56
49
  const isLiveDevEnabled = app.mode === "dev" && (this.props.enableLiveDev === false ? false : true);
57
- this.job = this.createCodeBuildProject();
58
- this.createLogRetention();
50
+ // TODO add test
51
+ this.validateContainerProps();
52
+ this.job = this.createCodeBuildJob();
59
53
  if (!stack.isActive) {
60
- this._jobInvoker = this.createCodeBuildInvoker();
54
+ this._jobManager = this.createJobManager();
61
55
  }
62
56
  else if (isLiveDevEnabled) {
63
- this._jobInvoker = this.createLocalInvoker();
57
+ this.liveDevJob = this.createLiveDevJob();
58
+ this._jobManager = this.createJobManager();
64
59
  }
65
60
  else {
66
- this._jobInvoker = this.createCodeBuildInvoker();
61
+ this._jobManager = this.createJobManager();
67
62
  this.buildCodeBuildProjectCode();
68
63
  }
64
+ this.createLogRetention();
69
65
  this.attachPermissions(props.permissions || []);
70
66
  this.bind(props.bind || []);
71
67
  Object.entries(props.environment || {}).forEach(([key, value]) => {
72
68
  this.addEnvironment(key, value);
73
69
  });
70
+ useFunctions().add(this.node.addr, {
71
+ ...props,
72
+ runtime: this.convertJobRuntimeToFunctionRuntime(),
73
+ });
74
74
  }
75
75
  getConstructMetadata() {
76
76
  return {
@@ -87,11 +87,11 @@ export class Job extends Construct {
87
87
  variables: {
88
88
  functionName: {
89
89
  type: "plain",
90
- value: this._jobInvoker.functionName,
90
+ value: this._jobManager.functionName,
91
91
  },
92
92
  },
93
93
  permissions: {
94
- "lambda:*": [this._jobInvoker.functionArn],
94
+ "lambda:*": [this._jobManager.functionArn],
95
95
  },
96
96
  };
97
97
  }
@@ -104,7 +104,7 @@ export class Job extends Construct {
104
104
  * ```
105
105
  */
106
106
  bind(constructs) {
107
- this._jobInvoker.bind(constructs);
107
+ this.liveDevJob?.bind(constructs);
108
108
  this.bindForCodeBuild(constructs);
109
109
  }
110
110
  /**
@@ -116,7 +116,7 @@ export class Job extends Construct {
116
116
  * ```
117
117
  */
118
118
  attachPermissions(permissions) {
119
- this._jobInvoker.attachPermissions(permissions);
119
+ this.liveDevJob?.attachPermissions(permissions);
120
120
  this.attachPermissionsForCodeBuild(permissions);
121
121
  }
122
122
  /**
@@ -130,22 +130,15 @@ export class Job extends Construct {
130
130
  * ```
131
131
  */
132
132
  addEnvironment(name, value) {
133
- this._jobInvoker.addEnvironment(name, value);
133
+ this.liveDevJob?.addEnvironment(name, value);
134
134
  this.addEnvironmentForCodeBuild(name, value);
135
135
  }
136
- createCodeBuildProject() {
137
- const { cdk, memorySize, timeout } = this.props;
136
+ createCodeBuildJob() {
137
+ const { cdk, runtime, handler, memorySize, timeout, container } = this.props;
138
138
  const app = this.node.root;
139
139
  return new Project(this, "JobProject", {
140
140
  projectName: app.logicalPrefixedName(this.node.id),
141
141
  environment: {
142
- // CodeBuild offers different build images. The newer ones have much quicker
143
- // boot time. The latest build image is STANDARD_6_0, which support Node.js 16.
144
- // But while testing, I found STANDARD_6_0 took 100s to boot. So for the
145
- // purpose of this demo, I use STANDARD_5_0. It takes 30s to boot.
146
- //buildImage: codebuild.LinuxBuildImage.STANDARD_6_0,
147
- //buildImage: codebuild.LinuxBuildImage.STANDARD_5_0,
148
- buildImage: LinuxBuildImage.fromDockerRegistry("amazon/aws-lambda-nodejs:16"),
149
142
  computeType: this.normalizeMemorySize(memorySize || "3 GB"),
150
143
  },
151
144
  environmentVariables: {
@@ -169,6 +162,24 @@ export class Job extends Construct {
169
162
  subnetSelection: cdk?.vpcSubnets,
170
163
  });
171
164
  }
165
+ createLiveDevJob() {
166
+ // Note: make the invoker function the same ID as the Job
167
+ // construct so users can identify the invoker function
168
+ // in the Console.
169
+ const fn = new Function(this, this.node.id, {
170
+ ...this.props,
171
+ runtime: this.convertJobRuntimeToFunctionRuntime(),
172
+ container: this.convertJobContainerToFunctionContainer(),
173
+ memorySize: 1024,
174
+ timeout: "10 seconds",
175
+ environment: {
176
+ ...this.props.environment,
177
+ SST_DEBUG_JOB: "true",
178
+ },
179
+ });
180
+ fn._doNotAllowOthersToBind = true;
181
+ return fn;
182
+ }
172
183
  createLogRetention() {
173
184
  const { logRetention } = this.props;
174
185
  if (!logRetention)
@@ -182,17 +193,44 @@ export class Job extends Construct {
182
193
  });
183
194
  }
184
195
  buildCodeBuildProjectCode() {
185
- const app = this.node.root;
196
+ const { handler, runtime, container } = this.props;
186
197
  useDeferredTasks().add(async () => {
187
198
  // Build function
188
199
  const result = await useRuntimeHandlers().build(this.node.addr, "deploy");
189
- // create wrapper that calls the handler
190
200
  if (result.type === "error") {
191
- throw new Error([
192
- `Failed to build job "${this.props.handler}"`,
193
- ...result.errors,
194
- ].join("\n"));
201
+ throw new Error([`Failed to build job "${handler}"`, ...result.errors].join("\n"));
202
+ }
203
+ // No need to update code for container runtime
204
+ // Note: we could set the commands in `createCodeBuildJob` but
205
+ // in `sst dev`, we want to avoid changing the CodeBuild resources
206
+ // when `cmd` changes.
207
+ if (runtime === "container") {
208
+ const image = LinuxBuildImage.fromAsset(this, "ContainerImage", {
209
+ directory: handler,
210
+ platform: Platform.custom("linux/amd64"),
211
+ });
212
+ image.repository?.grantPull(this.job.role);
213
+ const project = this.job.node.defaultChild;
214
+ project.environment = {
215
+ ...project.environment,
216
+ image: image.imageId,
217
+ imagePullCredentialsType: "SERVICE_ROLE",
218
+ };
219
+ project.source = {
220
+ type: "NO_SOURCE",
221
+ buildSpec: [
222
+ "version: 0.2",
223
+ "phases:",
224
+ " build:",
225
+ " commands:",
226
+ ` - ${container.docker.cmd
227
+ .map((arg) => (arg.includes(" ") ? `"${arg}"` : arg))
228
+ .join(" ")}`,
229
+ ].join("\n"),
230
+ };
231
+ return;
195
232
  }
233
+ // Create wrapper that calls the handler
196
234
  const parsed = path.parse(result.handler);
197
235
  const importName = parsed.ext.substring(1);
198
236
  const importPath = `./${path
@@ -219,76 +257,65 @@ export class Job extends Construct {
219
257
  `console.log("")`,
220
258
  `process.exit(0)`,
221
259
  ].join("\n"));
260
+ // Update job's commands
222
261
  const code = AssetCode.fromAsset(result.out);
223
- this.updateCodeBuildProjectCode(code, "handler-wrapper.mjs");
224
- // This should always be true b/c runtime is always Node.js
225
- });
226
- }
227
- updateCodeBuildProjectCode(code, script) {
228
- // Update job's commands
229
- const codeConfig = code.bind(this);
230
- const project = this.job.node.defaultChild;
231
- project.source = {
232
- type: "S3",
233
- location: `${codeConfig.s3Location?.bucketName}/${codeConfig.s3Location?.objectKey}`,
234
- buildSpec: [
235
- "version: 0.2",
236
- "phases:",
237
- " build:",
238
- " commands:",
239
- ` - node ${script}`,
240
- ].join("\n"),
241
- };
242
- this.attachPermissions([
243
- new PolicyStatement({
244
- actions: ["s3:*"],
245
- effect: Effect.ALLOW,
246
- resources: [
247
- `arn:${Stack.of(this).partition}:s3:::${codeConfig.s3Location?.bucketName}/${codeConfig.s3Location?.objectKey}`,
248
- ],
249
- }),
250
- ]);
251
- }
252
- createLocalInvoker() {
253
- // Note: make the invoker function the same ID as the Job
254
- // construct so users can identify the invoker function
255
- // in the Console.
256
- const fn = new Function(this, this.node.id, {
257
- runtime: "nodejs16.x",
258
- memorySize: 1024,
259
- environment: {
260
- ...this.props.environment,
261
- SST_DEBUG_TYPE: "job",
262
- },
263
- ...this.props,
264
- timeout: "15 minutes",
262
+ const codeConfig = code.bind(this);
263
+ const project = this.job.node.defaultChild;
264
+ const image = LinuxBuildImage.fromDockerRegistry("amazon/aws-lambda-nodejs:16");
265
+ project.environment = {
266
+ ...project.environment,
267
+ image: image.imageId,
268
+ };
269
+ image.repository?.grantPull(this.job.role);
270
+ project.source = {
271
+ type: "S3",
272
+ location: `${codeConfig.s3Location?.bucketName}/${codeConfig.s3Location?.objectKey}`,
273
+ buildSpec: [
274
+ "version: 0.2",
275
+ "phases:",
276
+ " build:",
277
+ " commands:",
278
+ ` - node handler-wrapper.mjs`,
279
+ ].join("\n"),
280
+ };
281
+ this.attachPermissions([
282
+ new PolicyStatement({
283
+ actions: ["s3:*"],
284
+ effect: Effect.ALLOW,
285
+ resources: [
286
+ `arn:${Stack.of(this).partition}:s3:::${codeConfig.s3Location?.bucketName}/${codeConfig.s3Location?.objectKey}`,
287
+ ],
288
+ }),
289
+ ]);
265
290
  });
266
- fn._doNotAllowOthersToBind = true;
267
- return fn;
268
291
  }
269
- createCodeBuildInvoker() {
270
- const fn = new Function(this, this.node.id, {
271
- handler: path.join(__dirname, "../support/job-invoker/index.main"),
272
- runtime: "nodejs18.x",
273
- timeout: 10,
292
+ createJobManager() {
293
+ return new CdkFunction(this, "Manager", {
294
+ code: Code.fromAsset(path.join(__dirname, "../support/job-manager/")),
295
+ handler: "index.handler",
296
+ runtime: Runtime.NODEJS_16_X,
297
+ timeout: CdkDuration.seconds(10),
274
298
  memorySize: 1024,
275
299
  environment: {
276
- PROJECT_NAME: this.job.projectName,
300
+ SST_JOB_PROVIDER: this.liveDevJob ? "lambda" : "codebuild",
301
+ SST_JOB_RUNNER: this.liveDevJob
302
+ ? this.liveDevJob.functionArn
303
+ : this.job.projectName,
277
304
  },
278
- permissions: [
279
- new PolicyStatement({
280
- effect: Effect.ALLOW,
281
- actions: ["codebuild:StartBuild"],
282
- resources: [this.job.projectArn],
283
- }),
305
+ initialPolicy: [
306
+ this.liveDevJob
307
+ ? new PolicyStatement({
308
+ effect: Effect.ALLOW,
309
+ actions: ["lambda:InvokeFunction"],
310
+ resources: [this.liveDevJob.functionArn],
311
+ })
312
+ : new PolicyStatement({
313
+ effect: Effect.ALLOW,
314
+ actions: ["codebuild:StartBuild", "codebuild:StopBuild"],
315
+ resources: [this.job.projectArn],
316
+ }),
284
317
  ],
285
- nodejs: {
286
- format: "esm",
287
- },
288
- enableLiveDev: false,
289
318
  });
290
- fn._doNotAllowOthersToBind = true;
291
- return fn;
292
319
  }
293
320
  bindForCodeBuild(constructs) {
294
321
  // Get referenced secrets
@@ -318,6 +345,14 @@ export class Job extends Construct {
318
345
  const envVars = env.environmentVariables;
319
346
  envVars.push({ name, value });
320
347
  }
348
+ validateContainerProps() {
349
+ const { runtime, container } = this.props;
350
+ if (runtime === "container") {
351
+ if (!container) {
352
+ throw new Error(`No commands defined for the ${this.node.id} Job.`);
353
+ }
354
+ }
355
+ }
321
356
  normalizeMemorySize(memorySize) {
322
357
  if (memorySize === "3 GB") {
323
358
  return ComputeType.SMALL;
@@ -340,4 +375,18 @@ export class Job extends Construct {
340
375
  }
341
376
  return value;
342
377
  }
378
+ convertJobRuntimeToFunctionRuntime() {
379
+ const { runtime } = this.props;
380
+ return runtime === "container" ? "container" : "nodejs16.x";
381
+ }
382
+ convertJobContainerToFunctionContainer() {
383
+ const { runtime, container } = this.props;
384
+ if (runtime !== "container")
385
+ return;
386
+ return {
387
+ docker: {
388
+ cmd: container?.docker.cmd,
389
+ },
390
+ };
391
+ }
343
392
  }
@@ -143,7 +143,7 @@ function permissionsToStatementsAndGrants(permissions) {
143
143
  statements.push(buildPolicyStatement("lambda:*", [permission.functionArn]));
144
144
  }
145
145
  else if (permission instanceof Job) {
146
- statements.push(buildPolicyStatement("lambda:*", [permission._jobInvoker.functionArn]));
146
+ statements.push(buildPolicyStatement("lambda:*", [permission._jobManager.functionArn]));
147
147
  }
148
148
  ////////////////////////////////////
149
149
  // Case: CDK constructs
@@ -10,7 +10,10 @@ export type JobType = {
10
10
  };
11
11
  export declare const Job: JobType;
12
12
  declare function JobControl<Name extends keyof JobResources>(name: Name, vars: Record<string, string>): {
13
- run(props: JobRunProps<Name>): Promise<void>;
13
+ run(props: JobRunProps<Name>): Promise<{
14
+ jobId: string;
15
+ }>;
16
+ cancel(jobId: string): Promise<void>;
14
17
  };
15
18
  /**
16
19
  * Create a new job handler.
package/node/job/index.js CHANGED
@@ -11,19 +11,30 @@ export const Job = /* @__PURE__ */ (() => {
11
11
  return result;
12
12
  })();
13
13
  function JobControl(name, vars) {
14
+ const functionName = vars.functionName;
14
15
  return {
15
16
  async run(props) {
16
- // Handle job permission not granted
17
- const functionName = vars.functionName;
18
17
  // Invoke the Lambda function
19
18
  const ret = await lambda.send(new InvokeCommand({
20
19
  FunctionName: functionName,
21
- Payload: props?.payload === undefined
22
- ? undefined
23
- : Buffer.from(JSON.stringify(props?.payload)),
20
+ Payload: Buffer.from(JSON.stringify({ action: "run", payload: props?.payload })),
24
21
  }));
25
22
  if (ret.FunctionError) {
26
- throw new Error(`Failed to invoke the ${name} Job. Error: ${ret.FunctionError}`);
23
+ throw new Error(`Failed to invoke the "${name}" job. Error: ${ret.FunctionError}`);
24
+ }
25
+ const resp = JSON.parse(Buffer.from(ret.Payload).toString());
26
+ return {
27
+ jobId: resp.jobId,
28
+ };
29
+ },
30
+ async cancel(jobId) {
31
+ // Invoke the Lambda function
32
+ const ret = await lambda.send(new InvokeCommand({
33
+ FunctionName: functionName,
34
+ Payload: Buffer.from(JSON.stringify({ action: "cancel", jobId })),
35
+ }));
36
+ if (ret.FunctionError) {
37
+ throw new Error(`Failed to cancel the "${name}" job id ${jobId}. Error: ${ret.FunctionError}`);
27
38
  }
28
39
  },
29
40
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "sideEffects": false,
3
3
  "name": "sst",
4
- "version": "2.20.0",
4
+ "version": "2.21.0",
5
5
  "bin": {
6
6
  "sst": "cli/sst.js"
7
7
  },