diff --git a/examples/internal/playground/cluster/.dockerignore b/examples/internal/playground/cluster/.dockerignore new file mode 100644 index 000000000..d98fe1711 --- /dev/null +++ b/examples/internal/playground/cluster/.dockerignore @@ -0,0 +1,3 @@ + +# sst +.sst \ No newline at end of file diff --git a/examples/internal/playground/cluster/Dockerfile b/examples/internal/playground/cluster/Dockerfile new file mode 100644 index 000000000..512465755 --- /dev/null +++ b/examples/internal/playground/cluster/Dockerfile @@ -0,0 +1,15 @@ +FROM node:18-bullseye-slim +ARG SST_RESOURCE_MyBucket + +WORKDIR /app/ + +COPY package.json /app +RUN npm install + +# Ensure linked resources are available at build time +COPY build.mjs /app +RUN node build.mjs + +COPY index.mjs /app + +ENTRYPOINT ["node", "index.mjs"] \ No newline at end of file diff --git a/examples/internal/playground/cluster/build.mjs b/examples/internal/playground/cluster/build.mjs new file mode 100644 index 000000000..2b66fb7e1 --- /dev/null +++ b/examples/internal/playground/cluster/build.mjs @@ -0,0 +1,3 @@ +import { Resource } from "sst"; + +console.log("SDK", Resource.MyBucket.name); diff --git a/examples/internal/playground/cluster/index.mjs b/examples/internal/playground/cluster/index.mjs new file mode 100644 index 000000000..f3369d880 --- /dev/null +++ b/examples/internal/playground/cluster/index.mjs @@ -0,0 +1,19 @@ +import express from "express"; +import { Resource } from "sst"; + +const PORT = 80; + +const app = express(); + +app.get("/", async (req, res) => { + res.send( + JSON.stringify({ + sdk: Resource.MyBucket.name, + env: process.env.SST_RESOURCE_MyBucket, + }) + ); +}); + +app.listen(PORT, () => { + console.log(`Server is running on http://localhost:${PORT}`); +}); diff --git a/examples/internal/playground/cluster/package.json b/examples/internal/playground/cluster/package.json new file mode 100644 index 000000000..2b120522d --- /dev/null +++ b/examples/internal/playground/cluster/package.json @@ -0,0 +1,13 @@ +{ + "name": "cluster", + "version": "1.0.0", + "description": "", + "main": "index.js", + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "express": "^4.19.2", + "sst": "latest" + } +} diff --git a/examples/internal/playground/cluster/sst-env.d.ts b/examples/internal/playground/cluster/sst-env.d.ts new file mode 100644 index 000000000..99616cc2b --- /dev/null +++ b/examples/internal/playground/cluster/sst-env.d.ts @@ -0,0 +1,21 @@ +/* This file is auto-generated by SST. Do not edit. */ +/* tslint:disable */ +/* eslint-disable */ +import "sst" +export {} +declare module "sst" { + export interface Resource { + "MyBucket": { + "name": string + "type": "sst.aws.Bucket" + } + "MyService": { + "service": string + "type": "sst.aws.Service" + "url": string + } + "MyVpc": { + "type": "sst.aws.Vpc" + } + } +} diff --git a/examples/internal/playground/functions/bundled-example/sst-env.d.ts b/examples/internal/playground/functions/bundled-example/sst-env.d.ts new file mode 100644 index 000000000..99616cc2b --- /dev/null +++ b/examples/internal/playground/functions/bundled-example/sst-env.d.ts @@ -0,0 +1,21 @@ +/* This file is auto-generated by SST. Do not edit. */ +/* tslint:disable */ +/* eslint-disable */ +import "sst" +export {} +declare module "sst" { + export interface Resource { + "MyBucket": { + "name": string + "type": "sst.aws.Bucket" + } + "MyService": { + "service": string + "type": "sst.aws.Service" + "url": string + } + "MyVpc": { + "type": "sst.aws.Vpc" + } + } +} diff --git a/examples/internal/playground/sst-env.d.ts b/examples/internal/playground/sst-env.d.ts index a77e632b2..99616cc2b 100644 --- a/examples/internal/playground/sst-env.d.ts +++ b/examples/internal/playground/sst-env.d.ts @@ -1,17 +1,21 @@ +/* This file is auto-generated by SST. Do not edit. */ /* tslint:disable */ /* eslint-disable */ import "sst" +export {} declare module "sst" { export interface Resource { - "MyApp": { - "name": string - "type": "sst.aws.Function" - "url": string - } "MyBucket": { "name": string "type": "sst.aws.Bucket" } + "MyService": { + "service": string + "type": "sst.aws.Service" + "url": string + } + "MyVpc": { + "type": "sst.aws.Vpc" + } } } -export {} diff --git a/examples/internal/playground/sst.config.ts b/examples/internal/playground/sst.config.ts index 372be351b..71f6d101c 100644 --- a/examples/internal/playground/sst.config.ts +++ b/examples/internal/playground/sst.config.ts @@ -8,43 +8,50 @@ export default $config({ home: "aws", }; }, - console: { - autodeploy: { - target(event) { - if ( - event.type === "branch" && - event.branch === "dev" && - event.action === "pushed" - ) { - return { stage: "dev" }; - } - }, - workflow(context) { - context.install(); - context.shell("cd examples/internal/playground && npm install"); - context.deploy(); - }, - }, - }, async run() { - const bucket = new sst.aws.Bucket("MyBucket", { - access: "public", - transform: { - bucket: (args) => { - args.tags = { foo: "bar" }; - }, - }, - }); + const ret: Record> = {}; - const app = new sst.aws.Function("MyApp", { - handler: "functions/handler-example/index.handler", - link: [bucket], - url: true, - }); + const vpc = addVpc(); + const bucket = addBucket(); + //const app = addFunction(); + const service = addService(); - return { - bucket: bucket.name, - app: app.url, - }; + return ret; + + function addVpc() { + return new sst.aws.Vpc("MyVpc"); + } + + function addBucket() { + const bucket = new sst.aws.Bucket("MyBucket"); + ret.bucket = bucket.name; + return bucket; + } + + function addFunction() { + const app = new sst.aws.Function("MyApp", { + handler: "functions/handler-example/index.handler", + link: [bucket], + url: true, + }); + ret.app = app.url; + return app; + } + + function addService() { + const cluster = new sst.aws.Cluster("MyCluster", { vpc }); + const service = cluster.addService("MyService", { + public: { + ports: [{ listen: "80/http" }], + }, + image: { + context: "cluster", + }, + link: [bucket], + }); + ret.service = service.url; + + return service; + } }, }); diff --git a/platform/src/components/aws/service.ts b/platform/src/components/aws/service.ts index 989a68231..377b840af 100644 --- a/platform/src/components/aws/service.ts +++ b/platform/src/components/aws/service.ts @@ -104,9 +104,6 @@ export class Service extends Component implements Link.Linkable { const containers = normalizeContainers(); const pub = normalizePublic(); - const linkData = buildLinkData(); - const linkPermissions = buildLinkPermissions(); - const taskRole = createTaskRole(); this.cloudmapNamespace = vpc.cloudmapNamespaceName; @@ -356,14 +353,6 @@ export class Service extends Component implements Link.Linkable { return { ports, domain }; } - function buildLinkData() { - return output(args.link || []).apply((links) => Link.build(links)); - } - - function buildLinkPermissions() { - return Link.getInclude("aws.permission", args.link); - } - function createLoadBalancer() { if (!pub) return {}; @@ -501,26 +490,28 @@ export class Service extends Component implements Link.Linkable { } function createTaskRole() { - const policy = all([args.permissions || [], linkPermissions]).apply( - ([argsPermissions, linkPermissions]) => - iam.getPolicyDocumentOutput({ - statements: [ - ...argsPermissions, - ...linkPermissions.map((item) => ({ - actions: item.actions, - resources: item.resources, - })), - { - actions: [ - "ssmmessages:CreateControlChannel", - "ssmmessages:CreateDataChannel", - "ssmmessages:OpenControlChannel", - "ssmmessages:OpenDataChannel", - ], - resources: ["*"], - }, - ], - }), + const policy = all([ + args.permissions || [], + Link.getInclude("aws.permission", args.link), + ]).apply(([argsPermissions, linkPermissions]) => + iam.getPolicyDocumentOutput({ + statements: [ + ...argsPermissions, + ...linkPermissions.map((item) => ({ + actions: item.actions, + resources: item.resources, + })), + { + actions: [ + "ssmmessages:CreateControlChannel", + "ssmmessages:CreateDataChannel", + "ssmmessages:OpenControlChannel", + "ssmmessages:OpenDataChannel", + ], + resources: ["*"], + }, + ], + }), ); return new iam.Role( @@ -583,7 +574,11 @@ export class Service extends Component implements Link.Linkable { executionRoleArn: executionRole.arn, taskRoleArn: taskRole.arn, containerDefinitions: $jsonStringify( - containers.apply((containers) => + all([ + containers, + args.environment ?? [], + Link.propertiesToEnv(Link.getProperties(args.link)), + ]).apply(([containers, env, linkEnvs]) => containers.map((container) => { return { name: container.name, @@ -600,24 +595,8 @@ export class Service extends Component implements Link.Linkable { "awslogs-stream-prefix": "/service", }, }, - environment: all([args.environment ?? [], linkData]).apply( - ([env, linkData]) => [ - ...Object.entries(env).map(([name, value]) => ({ - name, - value, - })), - ...linkData.map((d) => ({ - name: `SST_RESOURCE_${d.name}`, - value: JSON.stringify(d.properties), - })), - { - name: "SST_RESOURCE_App", - value: JSON.stringify({ - name: $app.name, - stage: $app.stage, - }), - }, - ], + environment: Object.entries({ ...env, ...linkEnvs }).map( + ([name, value]) => ({ name, value }), ), linuxParameters: { initProcessEnabled: true, @@ -625,74 +604,73 @@ export class Service extends Component implements Link.Linkable { }; function createImage() { - return container.image.apply((imageArgs) => { - if (typeof imageArgs === "string") - return output(imageArgs); + if (typeof container.image === "string") + return output(container.image); - const contextPath = path.join( - $cli.paths.root, - imageArgs.context, - ); - const dockerfile = imageArgs.dockerfile ?? "Dockerfile"; - const dockerfilePath = imageArgs.dockerfile - ? path.join($cli.paths.root, imageArgs.dockerfile) - : path.join( - $cli.paths.root, - imageArgs.context, - "Dockerfile", - ); - const dockerIgnorePath = fs.existsSync( - path.join(contextPath, `${dockerfile}.dockerignore`), - ) - ? path.join(contextPath, `${dockerfile}.dockerignore`) - : path.join(contextPath, ".dockerignore"); - - // add .sst to .dockerignore if not exist - const lines = fs.existsSync(dockerIgnorePath) - ? fs - .readFileSync(dockerIgnorePath) - .toString() - .split("\n") - : []; - if (!lines.find((line) => line === ".sst")) { - fs.writeFileSync( - dockerIgnorePath, - [...lines, "", "# sst", ".sst"].join("\n"), + const contextPath = path.join( + $cli.paths.root, + container.image.context, + ); + const dockerfile = + container.image.dockerfile ?? "Dockerfile"; + const dockerfilePath = container.image.dockerfile + ? path.join($cli.paths.root, container.image.dockerfile) + : path.join( + $cli.paths.root, + container.image.context, + "Dockerfile", ); - } - - // Build image - const image = new Image( - ...transform( - args.transform?.image, - `${name}Image${container.name}`, - { - context: { location: contextPath }, - dockerfile: { location: dockerfilePath }, - buildArgs: imageArgs.args ?? {}, - platforms: [imageArgs.platform], - tags: [ - interpolate`${bootstrapData.assetEcrUrl}:${container.name}`, - ], - registries: [ - ecr - .getAuthorizationTokenOutput({ - registryId: bootstrapData.assetEcrRegistryId, - }) - .apply((authToken) => ({ - address: authToken.proxyEndpoint, - password: secret(authToken.password), - username: authToken.userName, - })), - ], - push: true, - }, - { parent: self }, - ), + const dockerIgnorePath = fs.existsSync( + path.join(contextPath, `${dockerfile}.dockerignore`), + ) + ? path.join(contextPath, `${dockerfile}.dockerignore`) + : path.join(contextPath, ".dockerignore"); + + // add .sst to .dockerignore if not exist + const lines = fs.existsSync(dockerIgnorePath) + ? fs.readFileSync(dockerIgnorePath).toString().split("\n") + : []; + if (!lines.find((line) => line === ".sst")) { + fs.writeFileSync( + dockerIgnorePath, + [...lines, "", "# sst", ".sst"].join("\n"), ); + } + + // Build image + const image = new Image( + ...transform( + args.transform?.image, + `${name}Image${container.name}`, + { + context: { location: contextPath }, + dockerfile: { location: dockerfilePath }, + buildArgs: { + ...container.image.args, + ...linkEnvs, + }, + platforms: [container.image.platform], + tags: [ + interpolate`${bootstrapData.assetEcrUrl}:${container.name}`, + ], + registries: [ + ecr + .getAuthorizationTokenOutput({ + registryId: bootstrapData.assetEcrRegistryId, + }) + .apply((authToken) => ({ + address: authToken.proxyEndpoint, + password: secret(authToken.password), + username: authToken.userName, + })), + ], + push: true, + }, + { parent: self }, + ), + ); - return interpolate`${bootstrapData.assetEcrUrl}@${image.digest}`; - }); + return interpolate`${bootstrapData.assetEcrUrl}@${image.digest}`; } function createLogGroup() { @@ -702,9 +680,8 @@ export class Service extends Component implements Link.Linkable { `${name}LogGroup${container.name}`, { name: interpolate`/sst/cluster/${cluster.name}/${name}/${container.name}`, - retentionInDays: container.logging.apply( - (logging) => RETENTION[logging.retention], - ), + retentionInDays: + RETENTION[container.logging.retention], }, { parent: self }, ), diff --git a/platform/src/components/link.ts b/platform/src/components/link.ts index a42d8d67c..a3b2b39e1 100644 --- a/platform/src/components/link.ts +++ b/platform/src/components/link.ts @@ -121,6 +121,54 @@ export module Link { }); } + export function getProperties(links?: Input) { + const linkProperties = output(links ?? []).apply((links) => + links + .map((link) => { + if (!link) + throw new VisibleError( + "An undefined link was passed into a `link` array.", + ); + return link; + }) + .filter((l) => isLinkable(l)) + .map((l: Linkable) => ({ + urn: l.urn, + properties: l.getSSTLink().properties, + })), + ); + + return output(linkProperties).apply((e) => + Object.fromEntries( + e.map(({ urn, properties }) => { + const name = urn.split("::").at(-1)!; + const data = { + ...properties, + type: urn.split("::").at(-2), + }; + return [name, data]; + }), + ), + ); + } + + export function propertiesToEnv( + properties: ReturnType, + ) { + return output(properties).apply((properties) => { + const env = Object.fromEntries( + Object.entries(properties).map(([key, value]) => { + return [`SST_RESOURCE_${key}`, JSON.stringify(value)]; + }), + ); + env["SST_RESOURCE_App"] = JSON.stringify({ + name: $app.name, + stage: $app.stage, + }); + return env; + }); + } + export function getInclude( type: string, input?: Input,