sst 2.20.1 → 2.21.1

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,18 @@ export interface JavaProps {
507
508
  */
508
509
  experimentalUseProvidedRuntime?: "provided" | "provided.al2";
509
510
  }
511
+ export interface ContainerProps {
512
+ /**
513
+ * Specify or override the CMD on the Docker image.
514
+ * @example
515
+ * ```js
516
+ * container: {
517
+ * cmd: ["index.handler"]
518
+ * }
519
+ * ```
520
+ */
521
+ cmd?: string[];
522
+ }
510
523
  /**
511
524
  * Used to configure additional files to copy into the function bundle
512
525
  *
@@ -208,6 +208,7 @@ export class Function extends CDKFunction {
208
208
  ...(architecture?.dockerPlatform
209
209
  ? { platform: Platform.custom(architecture.dockerPlatform) }
210
210
  : {}),
211
+ ...(props.container?.cmd ? { cmd: props.container.cmd } : {}),
211
212
  }),
212
213
  handler: CDKHandler.FROM_IMAGE,
213
214
  runtime: CDKRuntime.FROM_IMAGE,
@@ -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,10 +10,34 @@ 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
+ * Specify or override the CMD on the Docker image.
16
+ * @example
17
+ * ```js
18
+ * container: {
19
+ * cmd: ["python3", "my_script.py"]
20
+ * }
21
+ * ```
22
+ */
23
+ cmd: string[];
24
+ }
12
25
  export interface JobProps {
13
26
  /**
14
- * Path to the entry point and handler function. Of the format:
15
- * `/path/to/file.function`.
27
+ * The runtime environment for the job.
28
+ * @default "nodejs"
29
+ * @example
30
+ * ```js
31
+ * new Function(stack, "Function", {
32
+ * runtime: "container",
33
+ * handler: "src/job",
34
+ * })
35
+ *```
36
+ */
37
+ runtime?: "nodejs" | "container";
38
+ /**
39
+ * For "nodejs" runtime, point to the entry point and handler function.
40
+ * Of the format: `/path/to/file.function`.
16
41
  *
17
42
  * @example
18
43
  * ```js
@@ -20,6 +45,17 @@ export interface JobProps {
20
45
  * handler: "src/job.handler",
21
46
  * })
22
47
  *```
48
+ *
49
+ * For "container" runtime, point the handler to the directory containing
50
+ * the Dockerfile.
51
+ *
52
+ * @example
53
+ * ```js
54
+ * new Job(stack, "MyJob", {
55
+ * runtime: "container",
56
+ * handler: "src/job", // Dockerfile is at "src/job/Dockerfile"
57
+ * })
58
+ *```
23
59
  */
24
60
  handler: string;
25
61
  /**
@@ -65,6 +101,10 @@ export interface JobProps {
65
101
  * Used to configure nodejs function properties
66
102
  */
67
103
  nodejs?: JobNodeJSProps;
104
+ /**
105
+ * Used to configure container properties
106
+ */
107
+ container?: JobContainerProps;
68
108
  /**
69
109
  * 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
110
  *
@@ -204,10 +244,10 @@ export interface JobProps {
204
244
  */
205
245
  export declare class Job extends Construct implements SSTConstruct {
206
246
  readonly id: string;
207
- private readonly localId;
208
247
  private readonly props;
209
248
  private readonly job;
210
- readonly _jobInvoker: Function;
249
+ private readonly liveDevJob?;
250
+ readonly _jobManager: CdkFunction;
211
251
  constructor(scope: Construct, id: string, props: JobProps);
212
252
  getConstructMetadata(): {
213
253
  type: "Job";
@@ -246,15 +286,16 @@ export declare class Job extends Construct implements SSTConstruct {
246
286
  * ```
247
287
  */
248
288
  addEnvironment(name: string, value: string): void;
249
- private createCodeBuildProject;
289
+ private createCodeBuildJob;
290
+ private createLiveDevJob;
250
291
  private createLogRetention;
251
292
  private buildCodeBuildProjectCode;
252
- private updateCodeBuildProjectCode;
253
- private createLocalInvoker;
254
- private createCodeBuildInvoker;
293
+ private createJobManager;
255
294
  private bindForCodeBuild;
256
295
  private attachPermissionsForCodeBuild;
257
296
  private addEnvironmentForCodeBuild;
297
+ private validateContainerProps;
258
298
  private normalizeMemorySize;
259
299
  private normalizeTimeout;
300
+ private convertJobRuntimeToFunctionRuntime;
260
301
  }
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,40 @@ 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
+ this.validateContainerProps();
51
+ this.job = this.createCodeBuildJob();
59
52
  if (!stack.isActive) {
60
- this._jobInvoker = this.createCodeBuildInvoker();
53
+ this._jobManager = this.createJobManager();
61
54
  }
62
55
  else if (isLiveDevEnabled) {
63
- this._jobInvoker = this.createLocalInvoker();
56
+ this.liveDevJob = this.createLiveDevJob();
57
+ this._jobManager = this.createJobManager();
64
58
  }
65
59
  else {
66
- this._jobInvoker = this.createCodeBuildInvoker();
60
+ this._jobManager = this.createJobManager();
67
61
  this.buildCodeBuildProjectCode();
68
62
  }
63
+ this.createLogRetention();
69
64
  this.attachPermissions(props.permissions || []);
70
65
  this.bind(props.bind || []);
71
66
  Object.entries(props.environment || {}).forEach(([key, value]) => {
72
67
  this.addEnvironment(key, value);
73
68
  });
69
+ useFunctions().add(this.node.addr, {
70
+ ...props,
71
+ runtime: this.convertJobRuntimeToFunctionRuntime(),
72
+ });
74
73
  }
75
74
  getConstructMetadata() {
76
75
  return {
@@ -87,11 +86,11 @@ export class Job extends Construct {
87
86
  variables: {
88
87
  functionName: {
89
88
  type: "plain",
90
- value: this._jobInvoker.functionName,
89
+ value: this._jobManager.functionName,
91
90
  },
92
91
  },
93
92
  permissions: {
94
- "lambda:*": [this._jobInvoker.functionArn],
93
+ "lambda:*": [this._jobManager.functionArn],
95
94
  },
96
95
  };
97
96
  }
@@ -104,7 +103,7 @@ export class Job extends Construct {
104
103
  * ```
105
104
  */
106
105
  bind(constructs) {
107
- this._jobInvoker.bind(constructs);
106
+ this.liveDevJob?.bind(constructs);
108
107
  this.bindForCodeBuild(constructs);
109
108
  }
110
109
  /**
@@ -116,7 +115,7 @@ export class Job extends Construct {
116
115
  * ```
117
116
  */
118
117
  attachPermissions(permissions) {
119
- this._jobInvoker.attachPermissions(permissions);
118
+ this.liveDevJob?.attachPermissions(permissions);
120
119
  this.attachPermissionsForCodeBuild(permissions);
121
120
  }
122
121
  /**
@@ -130,22 +129,15 @@ export class Job extends Construct {
130
129
  * ```
131
130
  */
132
131
  addEnvironment(name, value) {
133
- this._jobInvoker.addEnvironment(name, value);
132
+ this.liveDevJob?.addEnvironment(name, value);
134
133
  this.addEnvironmentForCodeBuild(name, value);
135
134
  }
136
- createCodeBuildProject() {
137
- const { cdk, memorySize, timeout } = this.props;
135
+ createCodeBuildJob() {
136
+ const { cdk, runtime, handler, memorySize, timeout, container } = this.props;
138
137
  const app = this.node.root;
139
138
  return new Project(this, "JobProject", {
140
139
  projectName: app.logicalPrefixedName(this.node.id),
141
140
  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
141
  computeType: this.normalizeMemorySize(memorySize || "3 GB"),
150
142
  },
151
143
  environmentVariables: {
@@ -169,6 +161,23 @@ export class Job extends Construct {
169
161
  subnetSelection: cdk?.vpcSubnets,
170
162
  });
171
163
  }
164
+ createLiveDevJob() {
165
+ // Note: make the invoker function the same ID as the Job
166
+ // construct so users can identify the invoker function
167
+ // in the Console.
168
+ const fn = new Function(this, this.node.id, {
169
+ ...this.props,
170
+ runtime: this.convertJobRuntimeToFunctionRuntime(),
171
+ memorySize: 1024,
172
+ timeout: "10 seconds",
173
+ environment: {
174
+ ...this.props.environment,
175
+ SST_DEBUG_JOB: "true",
176
+ },
177
+ });
178
+ fn._doNotAllowOthersToBind = true;
179
+ return fn;
180
+ }
172
181
  createLogRetention() {
173
182
  const { logRetention } = this.props;
174
183
  if (!logRetention)
@@ -182,17 +191,44 @@ export class Job extends Construct {
182
191
  });
183
192
  }
184
193
  buildCodeBuildProjectCode() {
185
- const app = this.node.root;
194
+ const { handler, runtime, container } = this.props;
186
195
  useDeferredTasks().add(async () => {
187
196
  // Build function
188
197
  const result = await useRuntimeHandlers().build(this.node.addr, "deploy");
189
- // create wrapper that calls the handler
190
198
  if (result.type === "error") {
191
- throw new Error([
192
- `Failed to build job "${this.props.handler}"`,
193
- ...result.errors,
194
- ].join("\n"));
199
+ throw new Error([`Failed to build job "${handler}"`, ...result.errors].join("\n"));
200
+ }
201
+ // No need to update code for container runtime
202
+ // Note: we could set the commands in `createCodeBuildJob` but
203
+ // in `sst dev`, we want to avoid changing the CodeBuild resources
204
+ // when `cmd` changes.
205
+ if (runtime === "container") {
206
+ const image = LinuxBuildImage.fromAsset(this, "ContainerImage", {
207
+ directory: handler,
208
+ platform: Platform.custom("linux/amd64"),
209
+ });
210
+ image.repository?.grantPull(this.job.role);
211
+ const project = this.job.node.defaultChild;
212
+ project.environment = {
213
+ ...project.environment,
214
+ image: image.imageId,
215
+ imagePullCredentialsType: "SERVICE_ROLE",
216
+ };
217
+ project.source = {
218
+ type: "NO_SOURCE",
219
+ buildSpec: [
220
+ "version: 0.2",
221
+ "phases:",
222
+ " build:",
223
+ " commands:",
224
+ ` - ${container.cmd
225
+ .map((arg) => (arg.includes(" ") ? `"${arg}"` : arg))
226
+ .join(" ")}`,
227
+ ].join("\n"),
228
+ };
229
+ return;
195
230
  }
231
+ // Create wrapper that calls the handler
196
232
  const parsed = path.parse(result.handler);
197
233
  const importName = parsed.ext.substring(1);
198
234
  const importPath = `./${path
@@ -219,76 +255,65 @@ export class Job extends Construct {
219
255
  `console.log("")`,
220
256
  `process.exit(0)`,
221
257
  ].join("\n"));
258
+ // Update job's commands
222
259
  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",
260
+ const codeConfig = code.bind(this);
261
+ const project = this.job.node.defaultChild;
262
+ const image = LinuxBuildImage.fromDockerRegistry("amazon/aws-lambda-nodejs:16");
263
+ project.environment = {
264
+ ...project.environment,
265
+ image: image.imageId,
266
+ };
267
+ image.repository?.grantPull(this.job.role);
268
+ project.source = {
269
+ type: "S3",
270
+ location: `${codeConfig.s3Location?.bucketName}/${codeConfig.s3Location?.objectKey}`,
271
+ buildSpec: [
272
+ "version: 0.2",
273
+ "phases:",
274
+ " build:",
275
+ " commands:",
276
+ ` - node handler-wrapper.mjs`,
277
+ ].join("\n"),
278
+ };
279
+ this.attachPermissions([
280
+ new PolicyStatement({
281
+ actions: ["s3:*"],
282
+ effect: Effect.ALLOW,
283
+ resources: [
284
+ `arn:${Stack.of(this).partition}:s3:::${codeConfig.s3Location?.bucketName}/${codeConfig.s3Location?.objectKey}`,
285
+ ],
286
+ }),
287
+ ]);
265
288
  });
266
- fn._doNotAllowOthersToBind = true;
267
- return fn;
268
289
  }
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,
290
+ createJobManager() {
291
+ return new CdkFunction(this, "Manager", {
292
+ code: Code.fromAsset(path.join(__dirname, "../support/job-manager/")),
293
+ handler: "index.handler",
294
+ runtime: Runtime.NODEJS_16_X,
295
+ timeout: CdkDuration.seconds(10),
274
296
  memorySize: 1024,
275
297
  environment: {
276
- PROJECT_NAME: this.job.projectName,
298
+ SST_JOB_PROVIDER: this.liveDevJob ? "lambda" : "codebuild",
299
+ SST_JOB_RUNNER: this.liveDevJob
300
+ ? this.liveDevJob.functionArn
301
+ : this.job.projectName,
277
302
  },
278
- permissions: [
279
- new PolicyStatement({
280
- effect: Effect.ALLOW,
281
- actions: ["codebuild:StartBuild"],
282
- resources: [this.job.projectArn],
283
- }),
303
+ initialPolicy: [
304
+ this.liveDevJob
305
+ ? new PolicyStatement({
306
+ effect: Effect.ALLOW,
307
+ actions: ["lambda:InvokeFunction"],
308
+ resources: [this.liveDevJob.functionArn],
309
+ })
310
+ : new PolicyStatement({
311
+ effect: Effect.ALLOW,
312
+ actions: ["codebuild:StartBuild", "codebuild:StopBuild"],
313
+ resources: [this.job.projectArn],
314
+ }),
284
315
  ],
285
- nodejs: {
286
- format: "esm",
287
- },
288
- enableLiveDev: false,
289
316
  });
290
- fn._doNotAllowOthersToBind = true;
291
- return fn;
292
317
  }
293
318
  bindForCodeBuild(constructs) {
294
319
  // Get referenced secrets
@@ -318,6 +343,14 @@ export class Job extends Construct {
318
343
  const envVars = env.environmentVariables;
319
344
  envVars.push({ name, value });
320
345
  }
346
+ validateContainerProps() {
347
+ const { runtime, container } = this.props;
348
+ if (runtime === "container") {
349
+ if (!container) {
350
+ throw new Error(`No commands defined for the ${this.node.id} Job.`);
351
+ }
352
+ }
353
+ }
321
354
  normalizeMemorySize(memorySize) {
322
355
  if (memorySize === "3 GB") {
323
356
  return ComputeType.SMALL;
@@ -340,4 +373,8 @@ export class Job extends Construct {
340
373
  }
341
374
  return value;
342
375
  }
376
+ convertJobRuntimeToFunctionRuntime() {
377
+ const { runtime } = this.props;
378
+ return runtime === "container" ? "container" : "nodejs16.x";
379
+ }
343
380
  }
@@ -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.1",
4
+ "version": "2.21.1",
5
5
  "bin": {
6
6
  "sst": "cli/sst.js"
7
7
  },