diff --git a/bun.lockb b/bun.lockb index 24ee0c0470..dbca3c34f9 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/cmd/sst/deploy.go b/cmd/sst/deploy.go index 6cda321c53..4c966b1728 100644 --- a/cmd/sst/deploy.go +++ b/cmd/sst/deploy.go @@ -79,6 +79,12 @@ var CmdDeploy = &cli.Command{ "sst deploy --dev", "```", "The `--dev` flag will deploy your resources as if you were running `sst dev`.", + "", + "```bash frame=\"none\"", + "sst deploy --refresh", + "```", + "Pass --refresh flag to refresh your state resources before deploying any changes.", + "This is useful for making sure your state reflects your cloud resources accurately before applying updates.", }, "\n"), }, Flags: []cli.Flag{ @@ -105,6 +111,14 @@ var CmdDeploy = &cli.Command{ Long: "Deploy resources like `sst dev` would.", }, }, + { + Name: "refresh", + Type: "bool", + Description: cli.Description{ + Short: "Refresh state before deploying", + Long: "Refresh your state before deploying.", + }, + }, }, Examples: []cli.Example{ { @@ -156,6 +170,7 @@ var CmdDeploy = &cli.Command{ ServerPort: s.Port, Verbose: c.Bool("verbose"), Continue: c.Bool("continue"), + Refresh: c.Bool("refresh"), }) if err != nil { return err diff --git a/pkg/project/preflight.go b/pkg/project/preflight.go new file mode 100644 index 0000000000..20543418fa --- /dev/null +++ b/pkg/project/preflight.go @@ -0,0 +1,95 @@ +package project + +import ( + "strings" + + "github.com/Masterminds/semver/v3" + "github.com/pulumi/pulumi/sdk/v3/go/common/apitype" +) + +type migrationNotice struct { + providerName string + fromVersion string + toVersion string + message string +} + +var migrationNotices = []migrationNotice{ + { + providerName: "aws", + fromVersion: "6.0.0", + toVersion: "7.0.0", + message: "Detected AWS provider upgrade to v7.\n\nUpgrading from v6 to v7 introduces some breaking changes and requires state migration before deploying. Refer to the SST release notes and the pulumi migration guide to make the relevent changes before deploying: https://www.pulumi.com/registry/packages/aws/how-to-guides/7-0-migration \n\nThis notice will clear after migrating your state", + }, +} + +func (p *Project) checkProviderUpgrade(resources []apitype.ResourceV3) []string { + providerVersions := make(map[string]string) + + // We iterate backwards over the slice b/c when multiple provider versions are present (i.e. just after refreshing but before deploying) + // the old provider version appears at the end of the array, and we want to override the version checkpoint with the new version. + for i := len(resources) - 1; i >= 0; i-- { + v := resources[i] + name := strings.TrimPrefix(string(v.Type), "pulumi:providers:") + if name == string(v.Type) { + continue + } + versionOutput, ok := v.Outputs["version"].(string) + if !ok { + continue + } + providerVersions[name] = versionOutput + } + + var messages []string + + for _, entry := range p.lock { + currentVersionStr, ok := providerVersions[entry.Name] + if !ok { + continue + } + + currentVersion, err := semver.NewVersion(currentVersionStr) + if err != nil { + continue + } + + targetVersion, err := semver.NewVersion(entry.Version) + if err != nil { + continue + } + + // Check if this is an upgrade + if !currentVersion.LessThan(targetVersion) { + continue + } + + // Check against all applicable upgrade rules + for _, rule := range migrationNotices { + if rule.providerName != entry.Name { + continue + } + + ruleFrom, err := semver.NewVersion(rule.fromVersion) + if err != nil { + continue + } + + ruleTo, err := semver.NewVersion(rule.toVersion) + if err != nil { + continue + } + + // Check if the upgrade path crosses this rule's version range + // Current version must be >= fromVersion AND < toVersion + // Target version must be >= toVersion + if currentVersion.Compare(ruleFrom) >= 0 && + currentVersion.Compare(ruleTo) < 0 && + targetVersion.Compare(ruleTo) >= 0 { + messages = append(messages, rule.message) + } + } + } + + return messages +} diff --git a/pkg/project/run.go b/pkg/project/run.go index 7aa6074be4..273ed0a2a3 100644 --- a/pkg/project/run.go +++ b/pkg/project/run.go @@ -289,6 +289,12 @@ func (p *Project) RunNext(ctx context.Context, input *StackInput) error { } if input.Command == "deploy" || input.Command == "diff" { + upgradeMsgs := p.checkProviderUpgrade(completed.Resources) + + if len(upgradeMsgs) > 0 && !input.Refresh { + return util.NewReadableError(nil, strings.Join(upgradeMsgs, "\n\n")) + } + for provider, opts := range p.app.Providers { for key, value := range opts.(map[string]interface{}) { switch v := value.(type) { @@ -315,9 +321,12 @@ func (p *Project) RunNext(ctx context.Context, input *StackInput) error { case "diff": args = append([]string{"preview"}, args...) case "refresh": - args = append([]string{"refresh", "--yes"}, args...) + args = append([]string{"refresh", "--yes", "--run-program"}, args...) case "deploy": args = append([]string{"up", "--yes", "-f"}, args...) + if input.Refresh { + args = append(args, "--refresh", "--run-program") + } case "remove": args = append([]string{"destroy", "--yes", "-f"}, args...) } diff --git a/pkg/project/stack.go b/pkg/project/stack.go index d9e5d47189..99e5bf1e45 100644 --- a/pkg/project/stack.go +++ b/pkg/project/stack.go @@ -21,6 +21,7 @@ type StackInput struct { Verbose bool Continue bool SkipHash string + Refresh bool } type ConcurrentUpdateEvent struct{} diff --git a/platform/package.json b/platform/package.json index aff278f44e..2a32f5ffaa 100644 --- a/platform/package.json +++ b/platform/package.json @@ -24,7 +24,7 @@ "@aws-sdk/client-ssm": "3.478.0", "@aws-sdk/client-sts": "3.478.0", "@aws-sdk/middleware-retry": "3.374.0", - "@pulumi/aws": "6.66.2", + "@pulumi/aws": "7.12.0", "@pulumi/cloudflare": "6.10.0", "@pulumi/command": "1.0.1", "@pulumi/docker-build": "0.0.8", diff --git a/platform/src/components/aws/apigatewayv1.ts b/platform/src/components/aws/apigatewayv1.ts index d51b04e921..633d1176c1 100644 --- a/platform/src/components/aws/apigatewayv1.ts +++ b/platform/src/components/aws/apigatewayv1.ts @@ -539,73 +539,73 @@ export interface ApiGatewayV1RouteArgs { auth?: Input< | false | { - /** - * Enable IAM authorization for a given API route. - * - * When IAM auth is enabled, clients need to use Signature Version 4 to sign their requests with their AWS credentials. - */ - iam?: Input; - /** - * Enable custom Lambda authorization for a given API route. Pass in the authorizer ID. - * @example - * ```js - * { - * auth: { - * custom: myAuthorizer.id - * } - * } - * ``` - * - * Where `myAuthorizer` is: - * - * ```js - * const userPool = new aws.cognito.UserPool(); - * const myAuthorizer = api.addAuthorizer({ - * name: "MyAuthorizer", - * userPools: [userPool.arn] - * }); - * ``` - */ - custom?: Input; - /** - * Enable Cognito User Pool authorization for a given API route. - * - * @example - * You can configure JWT auth. - * - * ```js - * { - * auth: { - * cognito: { - * authorizer: myAuthorizer.id, - * scopes: ["read:profile", "write:profile"] - * } - * } - * } - * ``` - * - * Where `myAuthorizer` is: - * - * ```js - * const userPool = new aws.cognito.UserPool(); - * - * const myAuthorizer = api.addAuthorizer({ - * name: "MyAuthorizer", - * userPools: [userPool.arn] - * }); - * ``` - */ - cognito?: Input<{ /** - * Authorizer ID of the Cognito User Pool authorizer. + * Enable IAM authorization for a given API route. + * + * When IAM auth is enabled, clients need to use Signature Version 4 to sign their requests with their AWS credentials. */ - authorizer: Input; + iam?: Input; /** - * Defines the permissions or access levels that the authorization token grants. + * Enable custom Lambda authorization for a given API route. Pass in the authorizer ID. + * @example + * ```js + * { + * auth: { + * custom: myAuthorizer.id + * } + * } + * ``` + * + * Where `myAuthorizer` is: + * + * ```js + * const userPool = new aws.cognito.UserPool(); + * const myAuthorizer = api.addAuthorizer({ + * name: "MyAuthorizer", + * userPools: [userPool.arn] + * }); + * ``` */ - scopes?: Input[]>; - }>; - } + custom?: Input; + /** + * Enable Cognito User Pool authorization for a given API route. + * + * @example + * You can configure JWT auth. + * + * ```js + * { + * auth: { + * cognito: { + * authorizer: myAuthorizer.id, + * scopes: ["read:profile", "write:profile"] + * } + * } + * } + * ``` + * + * Where `myAuthorizer` is: + * + * ```js + * const userPool = new aws.cognito.UserPool(); + * + * const myAuthorizer = api.addAuthorizer({ + * name: "MyAuthorizer", + * userPools: [userPool.arn] + * }); + * ``` + */ + cognito?: Input<{ + /** + * Authorizer ID of the Cognito User Pool authorizer. + */ + authorizer: Input; + /** + * Defines the permissions or access levels that the authorization token grants. + */ + scopes?: Input[]>; + }>; + } >; /** * Specify if an API key is required for the route. By default, an API key is not @@ -782,7 +782,7 @@ export class ApiGatewayV1 extends Component implements Link.Linkable { this.endpointType = endpoint.types; function normalizeRegion() { - return getRegionOutput(undefined, { parent }).name; + return getRegionOutput(undefined, { parent }).region; } function normalizeEndpoint() { @@ -798,9 +798,9 @@ export class ApiGatewayV1 extends Component implements Link.Linkable { ? { types: "REGIONAL" as const } : endpoint.type === "private" ? { - types: "PRIVATE" as const, - vpcEndpointIds: endpoint.vpcEndpointIds, - } + types: "PRIVATE" as const, + vpcEndpointIds: endpoint.vpcEndpointIds, + } : { types: "EDGE" as const }; }); } @@ -825,9 +825,9 @@ export class ApiGatewayV1 extends Component implements Link.Linkable { public get url() { return this.apigDomain && this.apiMapping ? all([this.apigDomain.domainName, this.apiMapping.basePath]).apply( - ([domain, key]) => - key ? `https://${domain}/${key}/` : `https://${domain}`, - ) + ([domain, key]) => + key ? `https://${domain}/${key}/` : `https://${domain}`, + ) : interpolate`https://${this.api.id}.execute-api.${this.region}.amazonaws.com/${$app.stage}/`; } @@ -1577,28 +1577,28 @@ export class ApiGatewayV1 extends Component implements Link.Linkable { return all([domain, endpointType]).apply(([domain, endpointType]) => domain.nameId ? apigateway.DomainName.get( - `${name}DomainName`, - domain.nameId, - {}, - { parent }, - ) - : new apigateway.DomainName( - ...transform( - args.transform?.domainName, `${name}DomainName`, - { - domainName: domain?.name, - endpointConfiguration: { types: endpointType }, - ...(endpointType === "REGIONAL" - ? { - regionalCertificateArn: - certificateArn as Output, - } - : { certificateArn: certificateArn as Output }), - }, + domain.nameId, + {}, { parent }, + ) + : new apigateway.DomainName( + ...transform( + args.transform?.domainName, + `${name}DomainName`, + { + domainName: domain?.name, + endpointConfiguration: { types: endpointType }, + ...(endpointType === "REGIONAL" + ? { + regionalCertificateArn: + certificateArn as Output, + } + : { certificateArn: certificateArn as Output }), + }, + { parent }, + ), ), - ), ); } diff --git a/platform/src/components/aws/aurora.ts b/platform/src/components/aws/aurora.ts index 75a95acc7f..58f6ffd034 100644 --- a/platform/src/components/aws/aurora.ts +++ b/platform/src/components/aws/aurora.ts @@ -735,7 +735,7 @@ export class Aurora extends Component implements Link.Linkable { { parent: self }, ); - const secretId = cluster.tags + const secretId = cluster.tagsAll .apply((tags) => tags?.["sst:ref:password"]) .apply((passwordTag) => { if (!passwordTag) @@ -759,7 +759,7 @@ export class Aurora extends Component implements Link.Linkable { (v) => v.password as string, ); - const proxy = cluster.tags + const proxy = cluster.tagsAll .apply((tags) => tags?.["sst:ref:proxy"]) .apply((proxyTag) => proxyTag diff --git a/platform/src/components/aws/auth.ts b/platform/src/components/aws/auth.ts index de7782290e..b6fc8ce911 100644 --- a/platform/src/components/aws/auth.ts +++ b/platform/src/components/aws/auth.ts @@ -312,7 +312,10 @@ export class Auth extends Component implements Link.Linkable { }, { parent: self }, ); - router.route("/", issuer.url); + router.route( + "/", + issuer.url.apply((url) => url!), + ); return router; } @@ -328,7 +331,7 @@ export class Auth extends Component implements Link.Linkable { public get url() { return ( this._router?.url ?? - this._issuer.url.apply((v) => (v.endsWith("/") ? v.slice(0, -1) : v)) + this._issuer.url.apply((v) => (v?.endsWith("/") ? v.slice(0, -1) : v)) ); } @@ -365,7 +368,7 @@ export class Auth extends Component implements Link.Linkable { }, include: [ env({ - OPENAUTH_ISSUER: this.url, + OPENAUTH_ISSUER: this.url.apply((url) => url!), }), ], }; diff --git a/platform/src/components/aws/bucket.ts b/platform/src/components/aws/bucket.ts index 62c9230e0d..12f0477d8a 100644 --- a/platform/src/components/aws/bucket.ts +++ b/platform/src/components/aws/bucket.ts @@ -468,11 +468,11 @@ export interface BucketArgs { /** * Transform the S3 Bucket resource. */ - bucket?: Transform; + bucket?: Transform; /** * Transform the S3 Bucket CORS configuration resource. */ - cors?: Transform; + cors?: Transform; /** * Transform the S3 Bucket Policy resource. */ @@ -480,7 +480,7 @@ export interface BucketArgs { /** * Transform the S3 Bucket versioning resource. */ - versioning?: Transform; + versioning?: Transform; /** * Transform the S3 Bucket lifecycle resource. * */ @@ -755,7 +755,7 @@ export interface BucketSubscriberArgs { interface BucketRef { ref: boolean; - bucket: s3.BucketV2; + bucket: s3.Bucket; } /** @@ -821,7 +821,7 @@ export class Bucket extends Component implements Link.Linkable { private constructorName: string; private constructorOpts: ComponentResourceOptions; private isSubscribed: boolean = false; - private bucket: Output; + private bucket: Output; constructor( name: string, @@ -888,7 +888,7 @@ export class Bucket extends Component implements Link.Linkable { } function createBucket() { - return new s3.BucketV2( + return new s3.Bucket( ...transform( args.transform?.bucket, `${name}Bucket`, @@ -904,7 +904,7 @@ export class Bucket extends Component implements Link.Linkable { return output(args.versioning).apply((versioning) => { if (!versioning) return; - return new s3.BucketVersioningV2( + return new s3.BucketVersioning( ...transform( args.transform?.versioning, `${name}Versioning`, @@ -1039,7 +1039,7 @@ export class Bucket extends Component implements Link.Linkable { return output(args.cors).apply((cors) => { if (cors === false) return; - return new s3.BucketCorsConfigurationV2( + return new s3.BucketCorsConfiguration( ...transform( args.transform?.cors, `${name}Cors`, @@ -1140,7 +1140,7 @@ export class Bucket extends Component implements Link.Linkable { ) { return new Bucket(name, { ref: true, - bucket: s3.BucketV2.get(`${name}Bucket`, bucketName, undefined, opts), + bucket: s3.Bucket.get(`${name}Bucket`, bucketName, undefined, opts), } as BucketArgs); } diff --git a/platform/src/components/aws/cluster.ts b/platform/src/components/aws/cluster.ts index 83f2c20d79..c40e9199cf 100644 --- a/platform/src/components/aws/cluster.ts +++ b/platform/src/components/aws/cluster.ts @@ -182,7 +182,7 @@ export class Cluster extends Component { const cluster = ecs.Cluster.get(`${name}Cluster`, ref.id, undefined, { parent: self, }); - const clusterValidated = cluster.tags.apply((tags) => { + const clusterValidated = cluster.tagsAll.apply((tags) => { const refVersion = tags?.["sst:ref:version"] ? parseComponentVersion(tags["sst:ref:version"]) : undefined; diff --git a/platform/src/components/aws/cognito-identity-pool.ts b/platform/src/components/aws/cognito-identity-pool.ts index dad2ab640a..ff1f94faa1 100644 --- a/platform/src/components/aws/cognito-identity-pool.ts +++ b/platform/src/components/aws/cognito-identity-pool.ts @@ -165,7 +165,7 @@ export class CognitoIdentityPool extends Component implements Link.Linkable { this.unauthRole = unauthRole; function getRegion() { - return getRegionOutput(undefined, { parent }).name; + return getRegionOutput(undefined, { parent }).region; } function createIdentityPool() { diff --git a/platform/src/components/aws/fargate.ts b/platform/src/components/aws/fargate.ts index 772be2af21..df2c218dae 100644 --- a/platform/src/components/aws/fargate.ts +++ b/platform/src/components/aws/fargate.ts @@ -993,7 +993,7 @@ export function createTaskDefinition( executionRole: ReturnType, ) { const clusterName = args.cluster.nodes.cluster.name; - const region = getRegionOutput({}, opts).name; + const region = getRegionOutput({}, opts).region; const bootstrapData = region.apply((region) => bootstrap.forRegion(region)); const linkEnvs = Link.propertiesToEnv(Link.getProperties(args.link)); const containerDefinitions = output(containers).apply((containers) => diff --git a/platform/src/components/aws/function.ts b/platform/src/components/aws/function.ts index a6b0fab41d..add0242341 100644 --- a/platform/src/components/aws/function.ts +++ b/platform/src/components/aws/function.ts @@ -667,53 +667,53 @@ export interface FunctionArgs { logging?: Input< | false | { - /** - * The duration the function logs are kept in CloudWatch. - * - * Not application when an existing log group is provided. - * - * @default `1 month` - * @example - * ```js - * { - * logging: { - * retention: "forever" - * } - * } - * ``` - */ - retention?: Input; - /** - * Assigns the given CloudWatch log group name to the function. This allows you to pass in a previously created log group. - * - * By default, the function creates a new log group when it's created. - * - * @default Creates a log group - * @example - * ```js - * { - * logging: { - * logGroup: "/existing/log-group" - * } - * } - * ``` - */ - logGroup?: Input; - /** - * The [log format](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs-advanced.html) - * of the Lambda function. - * @default `"text"` - * @example - * ```js - * { - * logging: { - * format: "json" - * } - * } - * ``` - */ - format?: Input<"text" | "json">; - } + /** + * The duration the function logs are kept in CloudWatch. + * + * Not application when an existing log group is provided. + * + * @default `1 month` + * @example + * ```js + * { + * logging: { + * retention: "forever" + * } + * } + * ``` + */ + retention?: Input; + /** + * Assigns the given CloudWatch log group name to the function. This allows you to pass in a previously created log group. + * + * By default, the function creates a new log group when it's created. + * + * @default Creates a log group + * @example + * ```js + * { + * logging: { + * logGroup: "/existing/log-group" + * } + * } + * ``` + */ + logGroup?: Input; + /** + * The [log format](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs-advanced.html) + * of the Lambda function. + * @default `"text"` + * @example + * ```js + * { + * logging: { + * format: "json" + * } + * } + * ``` + */ + format?: Input<"text" | "json">; + } >; /** * The [architecture](https://docs.aws.amazon.com/lambda/latest/dg/foundation-arch.html) @@ -775,137 +775,137 @@ export interface FunctionArgs { url?: Input< | boolean | { - /** - * @deprecated The `url.router` prop is now the recommended way to serve your - * function URL through a `Router` component. - */ - route?: Prettify; - /** - * Serve your function URL through a `Router` instead of a standalone Function URL. - * - * By default, this component creates a direct function URL endpoint. But you might - * want to serve it through the distribution of your `Router` as a: - * - * - A path like `/api/users` - * - A subdomain like `api.example.com` - * - Or a combined pattern like `dev.example.com/api` - * - * @example - * - * To serve your function **from a path**, you'll need to configure the root domain - * in your `Router` component. - * - * ```ts title="sst.config.ts" {2} - * const router = new sst.aws.Router("Router", { - * domain: "example.com" - * }); - * ``` - * - * Now set the `router` and the `path` in the `url` prop. - * - * ```ts {4,5} - * { - * url: { - * router: { - * instance: router, - * path: "/api/users" - * } - * } - * } - * ``` - * - * To serve your function **from a subdomain**, you'll need to configure the - * domain in your `Router` component to match both the root and the subdomain. - * - * ```ts title="sst.config.ts" {3,4} - * const router = new sst.aws.Router("Router", { - * domain: { - * name: "example.com", - * aliases: ["*.example.com"] - * } - * }); - * ``` - * - * Now set the `domain` in the `router` prop. - * - * ```ts {5} - * { - * url: { - * router: { - * instance: router, - * domain: "api.example.com" - * } - * } - * } - * ``` - * - * Finally, to serve your function **from a combined pattern** like - * `dev.example.com/api`, you'll need to configure the domain in your `Router` to - * match the subdomain. - * - * ```ts title="sst.config.ts" {3,4} - * const router = new sst.aws.Router("Router", { - * domain: { - * name: "example.com", - * aliases: ["*.example.com"] - * } - * }); - * ``` - * - * And set the `domain` and the `path`. - * - * ```ts {5,6} - * { - * url: { - * router: { - * instance: router, - * domain: "dev.example.com", - * path: "/api/users" - * } - * } - * } - * ``` - */ - router?: Prettify; - /** - * The authorization used for the function URL. Supports [IAM authorization](https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html). - * @default `"none"` - * @example - * ```js - * { - * url: { - * authorization: "iam" - * } - * } - * ``` - */ - authorization?: Input<"none" | "iam">; - /** - * Customize the CORS (Cross-origin resource sharing) settings for the function URL. - * @default `true` - * @example - * Disable CORS. - * ```js - * { - * url: { - * cors: false - * } - * } - * ``` - * Only enable the `GET` and `POST` methods for `https://example.com`. - * ```js - * { - * url: { - * cors: { - * allowMethods: ["GET", "POST"], - * allowOrigins: ["https://example.com"] - * } - * } - * } - * ``` - */ - cors?: Input>; - } + /** + * @deprecated The `url.router` prop is now the recommended way to serve your + * function URL through a `Router` component. + */ + route?: Prettify; + /** + * Serve your function URL through a `Router` instead of a standalone Function URL. + * + * By default, this component creates a direct function URL endpoint. But you might + * want to serve it through the distribution of your `Router` as a: + * + * - A path like `/api/users` + * - A subdomain like `api.example.com` + * - Or a combined pattern like `dev.example.com/api` + * + * @example + * + * To serve your function **from a path**, you'll need to configure the root domain + * in your `Router` component. + * + * ```ts title="sst.config.ts" {2} + * const router = new sst.aws.Router("Router", { + * domain: "example.com" + * }); + * ``` + * + * Now set the `router` and the `path` in the `url` prop. + * + * ```ts {4,5} + * { + * url: { + * router: { + * instance: router, + * path: "/api/users" + * } + * } + * } + * ``` + * + * To serve your function **from a subdomain**, you'll need to configure the + * domain in your `Router` component to match both the root and the subdomain. + * + * ```ts title="sst.config.ts" {3,4} + * const router = new sst.aws.Router("Router", { + * domain: { + * name: "example.com", + * aliases: ["*.example.com"] + * } + * }); + * ``` + * + * Now set the `domain` in the `router` prop. + * + * ```ts {5} + * { + * url: { + * router: { + * instance: router, + * domain: "api.example.com" + * } + * } + * } + * ``` + * + * Finally, to serve your function **from a combined pattern** like + * `dev.example.com/api`, you'll need to configure the domain in your `Router` to + * match the subdomain. + * + * ```ts title="sst.config.ts" {3,4} + * const router = new sst.aws.Router("Router", { + * domain: { + * name: "example.com", + * aliases: ["*.example.com"] + * } + * }); + * ``` + * + * And set the `domain` and the `path`. + * + * ```ts {5,6} + * { + * url: { + * router: { + * instance: router, + * domain: "dev.example.com", + * path: "/api/users" + * } + * } + * } + * ``` + */ + router?: Prettify; + /** + * The authorization used for the function URL. Supports [IAM authorization](https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html). + * @default `"none"` + * @example + * ```js + * { + * url: { + * authorization: "iam" + * } + * } + * ``` + */ + authorization?: Input<"none" | "iam">; + /** + * Customize the CORS (Cross-origin resource sharing) settings for the function URL. + * @default `true` + * @example + * Disable CORS. + * ```js + * { + * url: { + * cors: false + * } + * } + * ``` + * Only enable the `GET` and `POST` methods for `https://example.com`. + * ```js + * { + * url: { + * cors: { + * allowMethods: ["GET", "POST"], + * allowOrigins: ["https://example.com"] + * } + * } + * } + * ``` + */ + cors?: Input>; + } >; /** * Configure how your function is bundled. @@ -1365,22 +1365,22 @@ export interface FunctionArgs { * ``` */ vpc?: - | Vpc - | Input<{ - /** - * A list of VPC security group IDs. - */ - securityGroups: Input[]>; - /** - * A list of VPC subnet IDs. - */ - privateSubnets: Input[]>; - /** - * A list of VPC subnet IDs. - * @deprecated Use `privateSubnets` instead. - */ - subnets?: Input[]>; - }>; + | Vpc + | Input<{ + /** + * A list of VPC security group IDs. + */ + securityGroups: Input[]>; + /** + * A list of VPC subnet IDs. + */ + privateSubnets: Input[]>; + /** + * A list of VPC subnet IDs. + * @deprecated Use `privateSubnets` instead. + */ + subnets?: Input[]>; + }>; /** * Hook into the Lambda function build process. @@ -1647,7 +1647,7 @@ export class Function extends Component implements Link.Linkable { ([python, dev]) => !dev && (python?.container ?? false), ); const partition = getPartitionOutput({}, opts).partition; - const region = getRegionOutput({}, opts).name; + const region = getRegionOutput({}, opts).region; const bootstrapData = region.apply((region) => bootstrap.forRegion(region)); const injections = normalizeInjections(); const runtime = output(args.runtime ?? "nodejs20.x"); @@ -1859,10 +1859,10 @@ export class Function extends Component implements Link.Linkable { : url.cors === true || url.cors === undefined ? defaultCors : { - ...defaultCors, - ...url.cors, - maxAge: url.cors.maxAge && toSeconds(url.cors.maxAge), - }; + ...defaultCors, + ...url.cors, + maxAge: url.cors.maxAge && toSeconds(url.cors.maxAge), + }; return { authorization, @@ -2038,21 +2038,21 @@ export class Function extends Component implements Link.Linkable { name: path.posix.join(handlerDir, `${newHandlerFileName}.mjs`), content: streaming ? [ - ...split.outer, - `export const ${newHandlerFunction} = awslambda.streamifyResponse(async (event, responseStream, context) => {`, - ...split.inner, - ` const { ${oldHandlerFunction}: rawHandler} = await import("./${oldHandlerFileName}${newHandlerFileExt}");`, - ` return rawHandler(event, responseStream, context);`, - `});`, - ].join("\n") + ...split.outer, + `export const ${newHandlerFunction} = awslambda.streamifyResponse(async (event, responseStream, context) => {`, + ...split.inner, + ` const { ${oldHandlerFunction}: rawHandler} = await import("./${oldHandlerFileName}${newHandlerFileExt}");`, + ` return rawHandler(event, responseStream, context);`, + `});`, + ].join("\n") : [ - ...split.outer, - `export const ${newHandlerFunction} = async (event, context) => {`, - ...split.inner, - ` const { ${oldHandlerFunction}: rawHandler} = await import("./${oldHandlerFileName}${newHandlerFileExt}");`, - ` return rawHandler(event, context);`, - `};`, - ].join("\n"), + ...split.outer, + `export const ${newHandlerFunction} = async (event, context) => {`, + ...split.inner, + ` const { ${oldHandlerFunction}: rawHandler} = await import("./${oldHandlerFileName}${newHandlerFileExt}");`, + ` return rawHandler(event, context);`, + `};`, + ].join("\n"), }, }; }, @@ -2081,20 +2081,20 @@ export class Function extends Component implements Link.Linkable { ...linkPermissions, ...(dev ? [ - { - effect: "allow", - actions: ["appsync:*"], - resources: ["*"], - }, - { - effect: "allow", - actions: ["s3:*"], - resources: [ - interpolate`arn:${partition}:s3:::${bootstrapData.asset}`, - interpolate`arn:${partition}:s3:::${bootstrapData.asset}/*`, - ], - }, - ] + { + effect: "allow", + actions: ["appsync:*"], + resources: ["*"], + }, + { + effect: "allow", + actions: ["s3:*"], + resources: [ + interpolate`arn:${partition}:s3:::${bootstrapData.asset}`, + interpolate`arn:${partition}:s3:::${bootstrapData.asset}/*`, + ], + }, + ] : []), ].map((item) => ({ effect: (() => { @@ -2114,28 +2114,29 @@ export class Function extends Component implements Link.Linkable { { assumeRolePolicy: !dev ? iam.assumeRolePolicyForPrincipal({ - Service: "lambda.amazonaws.com", - }) + Service: "lambda.amazonaws.com", + }) : iam.getPolicyDocumentOutput({ - statements: [ - { - actions: ["sts:AssumeRole"], - principals: [ - { - type: "Service", - identifiers: ["lambda.amazonaws.com"], - }, - { - type: "AWS", - identifiers: [ - interpolate`arn:${partition}:iam::${getCallerIdentityOutput({}, opts).accountId + statements: [ + { + actions: ["sts:AssumeRole"], + principals: [ + { + type: "Service", + identifiers: ["lambda.amazonaws.com"], + }, + { + type: "AWS", + identifiers: [ + interpolate`arn:${partition}:iam::${ + getCallerIdentityOutput({}, opts).accountId }:root`, - ], - }, - ], - }, - ], - }).json, + ], + }, + ], + }, + ], + }).json, // if there are no statements, do not add an inline policy. // adding an inline policy with no statements will cause an error. inlinePolicies: policy.apply(({ statements }) => @@ -2146,13 +2147,13 @@ export class Function extends Component implements Link.Linkable { ...policies, ...(logging ? [ - interpolate`arn:${partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole`, - ] + interpolate`arn:${partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole`, + ] : []), ...(vpc ? [ - interpolate`arn:${partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole`, - ] + interpolate`arn:${partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole`, + ] : []), ], ), @@ -2374,8 +2375,9 @@ export class Function extends Component implements Link.Linkable { args.transform?.logGroup, `${name}LogGroup`, { - name: interpolate`/aws/lambda/${args.name ?? physicalName(64, `${name}Function`) - }`, + name: interpolate`/aws/lambda/${ + args.name ?? physicalName(64, `${name}Function`) + }`, retentionInDays: RETENTION[logging.retention], }, { parent, ignoreChanges: ["name"] }, @@ -2439,34 +2441,34 @@ export class Function extends Component implements Link.Linkable { reservedConcurrentExecutions: concurrency?.reserved, ...(isContainer ? { - packageType: "Image", - imageUri: imageAsset!.ref.apply( - (ref) => ref?.replace(":latest", ""), - ), - imageConfig: { - commands: [ - all([handler, runtime]).apply(([handler, runtime]) => { - // If a python container image we have to rewrite the handler path so lambdaric is happy - // This means no leading . and replace all / with . - if (isContainer && runtime.includes("python")) { - return handler - .replace(/\.\//g, "") - .replace(/\//g, "."); - } - return handler; - }), - ], - }, - } + packageType: "Image", + imageUri: imageAsset!.ref.apply( + (ref) => ref?.replace(":latest", ""), + ), + imageConfig: { + commands: [ + all([handler, runtime]).apply(([handler, runtime]) => { + // If a python container image we have to rewrite the handler path so lambdaric is happy + // This means no leading . and replace all / with . + if (isContainer && runtime.includes("python")) { + return handler + .replace(/\.\//g, "") + .replace(/\//g, "."); + } + return handler; + }), + ], + }, + } : { - packageType: "Zip", - s3Bucket: zipAsset!.bucket, - s3Key: zipAsset!.key, - handler: unsecret(handler), - runtime: runtime.apply((v) => - v === "go" || v === "rust" ? "provided.al2023" : v, - ), - }), + packageType: "Zip", + s3Bucket: zipAsset!.bucket, + s3Key: zipAsset!.key, + handler: unsecret(handler), + runtime: runtime.apply((v) => + v === "go" || v === "rust" ? "provided.al2023" : v, + ), + }), }, { parent }, ); @@ -2476,14 +2478,14 @@ export class Function extends Component implements Link.Linkable { ...transformed[1], ...(dev ? { - description: transformed[1].description - ? output(transformed[1].description).apply( - (v) => `${v.substring(0, 240)} (live)`, - ) - : "live", - runtime: "provided.al2023", - architectures: ["x86_64"], - } + description: transformed[1].description + ? output(transformed[1].description).apply( + (v) => `${v.substring(0, 240)} (live)`, + ) + : "live", + runtime: "provided.al2023", + architectures: ["x86_64"], + } : {}), }, transformed[2], @@ -2674,7 +2676,7 @@ export class Function extends Component implements Link.Linkable { { functionName: this.name, environment, - region: getRegionOutput(undefined, { parent: this }).name, + region: getRegionOutput(undefined, { parent: this }).region, }, { parent: this }, ); diff --git a/platform/src/components/aws/https-redirect.ts b/platform/src/components/aws/https-redirect.ts index 6551142f5f..8b1e48272b 100644 --- a/platform/src/components/aws/https-redirect.ts +++ b/platform/src/components/aws/https-redirect.ts @@ -84,7 +84,7 @@ export class HttpsRedirect extends Component { } function createBucketWebsite() { - return new s3.BucketWebsiteConfigurationV2( + return new s3.BucketWebsiteConfiguration( `${name}BucketWebsite`, { bucket: bucket.name, diff --git a/platform/src/components/aws/mysql.ts b/platform/src/components/aws/mysql.ts index 627301e6da..bf42ed43c9 100644 --- a/platform/src/components/aws/mysql.ts +++ b/platform/src/components/aws/mysql.ts @@ -135,53 +135,53 @@ export interface MysqlArgs { proxy?: Input< | boolean | { - /** - * Additional credentials the proxy can use to connect to the database. You don't - * need to specify the master user credentials as they are always added by default. - * - * :::note - * This component will not create the MySQL users listed here. You need to - * create them manually in the database. - * ::: - * - * @example - * ```js - * { - * credentials: [ - * { - * username: "metabase", - * password: "Passw0rd!" - * } - * ] - * } - * ``` - * - * You can use a `Secret` to manage the password. - * - * ```js - * { - * credentials: [ - * { - * username: "metabase", - * password: new sst.Secret("MyDBPassword").value - * } - * ] - * } - * ``` - */ - credentials?: Input< - Input<{ - /** - * The username of the user. - */ - username: Input; - /** - * The password of the user. - */ - password: Input; - }>[] - >; - } + /** + * Additional credentials the proxy can use to connect to the database. You don't + * need to specify the master user credentials as they are always added by default. + * + * :::note + * This component will not create the MySQL users listed here. You need to + * create them manually in the database. + * ::: + * + * @example + * ```js + * { + * credentials: [ + * { + * username: "metabase", + * password: "Passw0rd!" + * } + * ] + * } + * ``` + * + * You can use a `Secret` to manage the password. + * + * ```js + * { + * credentials: [ + * { + * username: "metabase", + * password: new sst.Secret("MyDBPassword").value + * } + * ] + * } + * ``` + */ + credentials?: Input< + Input<{ + /** + * The username of the user. + */ + username: Input; + /** + * The password of the user. + */ + password: Input; + }>[] + >; + } >; /** * Enable [Multi-AZ](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html) @@ -237,13 +237,13 @@ export interface MysqlArgs { * ``` */ vpc: - | Vpc - | Input<{ - /** - * A list of subnet IDs in the VPC. - */ - subnets: Input[]>; - }>; + | Vpc + | Input<{ + /** + * A list of subnet IDs in the VPC. + */ + subnets: Input[]>; + }>; /** * Configure how this component works in `sst dev`. * @@ -497,7 +497,7 @@ export class Mysql extends Component implements Link.Linkable { parent: self, }); - const input = instance.tags.apply((tags) => { + const input = instance.tagsAll.apply((tags) => { return { proxyId: output(ref.proxyId), passwordTag: tags?.["sst:ref:password"], @@ -507,8 +507,8 @@ export class Mysql extends Component implements Link.Linkable { const proxy = input.proxyId.apply((proxyId) => proxyId ? rds.Proxy.get(`${name}Proxy`, proxyId, undefined, { - parent: self, - }) + parent: self, + }) : undefined, ); @@ -610,13 +610,13 @@ Listening on "${dev.host}:${dev.port}"...`, return args.password ? output(args.password) : new RandomPassword( - `${name}Password`, - { - length: 32, - special: false, - }, - { parent: self }, - ).result; + `${name}Password`, + { + length: 32, + special: false, + }, + { parent: self }, + ).result; } function createSubnetGroup() { diff --git a/platform/src/components/aws/nextjs.ts b/platform/src/components/aws/nextjs.ts index 7fdf883416..dddd959c18 100644 --- a/platform/src/components/aws/nextjs.ts +++ b/platform/src/components/aws/nextjs.ts @@ -604,10 +604,10 @@ export class Nextjs extends SsrSite { revalidationTable?.name, bucket.arn, bucket.name, - getRegionOutput(undefined, { parent: bucket }).name, + getRegionOutput(undefined, { parent: bucket }).region, revalidationQueue?.arn, revalidationQueue?.url, - getRegionOutput(undefined, { parent: revalidationQueue }).name, + getRegionOutput(undefined, { parent: revalidationQueue }).region, ]).apply( ([ tableArn, diff --git a/platform/src/components/aws/open-search.ts b/platform/src/components/aws/open-search.ts index cdb3a43e31..16659781e0 100644 --- a/platform/src/components/aws/open-search.ts +++ b/platform/src/components/aws/open-search.ts @@ -306,7 +306,7 @@ export class OpenSearch extends Component implements Link.Linkable { //}); const domain = opensearch.Domain.get(`${name}Domain`, ref.id); - const input = domain.tags.apply((tags) => { + const input = domain.tagsAll.apply((tags) => { if (!tags?.["sst:ref:username"]) throw new VisibleError( `Failed to get username for OpenSearch ${name}.`, @@ -388,16 +388,16 @@ Listening on "${dev.url}"...`, return args.password ? output(args.password) : new RandomPassword( - `${name}Password`, - { - length: 32, - minLower: 1, - minUpper: 1, - minNumeric: 1, - minSpecial: 1, - }, - { parent: self }, - ).result; + `${name}Password`, + { + length: 32, + minLower: 1, + minUpper: 1, + minNumeric: 1, + minSpecial: 1, + }, + { parent: self }, + ).result; } function createSecret() { diff --git a/platform/src/components/aws/postgres.ts b/platform/src/components/aws/postgres.ts index eb110df869..4e565dce0e 100644 --- a/platform/src/components/aws/postgres.ts +++ b/platform/src/components/aws/postgres.ts @@ -506,7 +506,7 @@ export class Postgres extends Component implements Link.Linkable { parent: self, }); - const input = instance.tags.apply((tags) => { + const input = instance.tagsAll.apply((tags) => { registerVersion( tags?.["sst:component-version"] ? parseInt(tags["sst:component-version"]) diff --git a/platform/src/components/aws/redis-v1.ts b/platform/src/components/aws/redis-v1.ts index 48cd76863f..a90cd0d70b 100644 --- a/platform/src/components/aws/redis-v1.ts +++ b/platform/src/components/aws/redis-v1.ts @@ -432,6 +432,7 @@ Listening on "${dev.host}:${dev.port}"...`, transitEncryptionEnabled: true, transitEncryptionMode: "required", authToken, + authTokenUpdateStrategy: "ROTATE", subnetGroupName: subnetGroup.name, securityGroupIds: vpc.securityGroups, tags: { @@ -553,7 +554,7 @@ Listening on "${dev.host}:${dev.port}"...`, undefined, opts, ); - const secret = cluster.tags.apply((tags) => + const secret = cluster.tagsAll.apply((tags) => tags?.["sst:auth-token-ref"] ? secretsmanager.getSecretVersionOutput( { diff --git a/platform/src/components/aws/redis.ts b/platform/src/components/aws/redis.ts index f36736c85e..b8ccce8ce0 100644 --- a/platform/src/components/aws/redis.ts +++ b/platform/src/components/aws/redis.ts @@ -361,7 +361,7 @@ export class Redis extends Component implements Link.Linkable { { parent: self }, ); - const input = cluster.tags.apply((tags) => { + const input = cluster.tagsAll.apply((tags) => { registerVersion( tags?.["sst:component-version"] ? parseInt(tags["sst:component-version"]) @@ -569,6 +569,7 @@ Listening on "${dev.host}:${dev.port}"...`, atRestEncryptionEnabled: true, transitEncryptionEnabled: true, transitEncryptionMode: "required", + authTokenUpdateStrategy: "ROTATE", authToken, subnetGroupName: subnetGroup.name, parameterGroupName: parameterGroup.name, diff --git a/platform/src/components/aws/router.ts b/platform/src/components/aws/router.ts index b6a665e55f..6aba20f0f5 100644 --- a/platform/src/components/aws/router.ts +++ b/platform/src/components/aws/router.ts @@ -1046,7 +1046,7 @@ export class Router extends Component implements Link.Linkable { function reference() { const ref = args as unknown as RouterRef; const cdn = Cdn.get(`${name}Cdn`, ref.distributionID, { parent: self }); - const tags = cdn.nodes.distribution.tags.apply((tags) => { + const tags = cdn.nodes.distribution.tagsAll.apply((tags) => { if (tags?.["sst:ref:version"] !== _refVersion.toString()) { throw new VisibleError( [ diff --git a/platform/src/components/aws/service-v1.ts b/platform/src/components/aws/service-v1.ts index cbaca246ab..aafdf4f976 100644 --- a/platform/src/components/aws/service-v1.ts +++ b/platform/src/components/aws/service-v1.ts @@ -132,9 +132,9 @@ export class Service extends Component implements Link.Linkable { this._url = !self.loadBalancer ? undefined : all([self.domain, self.loadBalancer?.dnsName]).apply( - ([domain, loadBalancer]) => - domain ? `https://${domain}/` : `http://${loadBalancer}`, - ); + ([domain, loadBalancer]) => + domain ? `https://${domain}/` : `http://${loadBalancer}`, + ); registerHint(); registerReceiver(); @@ -162,7 +162,7 @@ export class Service extends Component implements Link.Linkable { } function normalizeRegion() { - return getRegionOutput(undefined, { parent: self }).name; + return getRegionOutput(undefined, { parent: self }).region; } function normalizeArchitecture() { @@ -544,12 +544,13 @@ export class Service extends Component implements Link.Linkable { { assumeRolePolicy: !$dev ? iam.assumeRolePolicyForPrincipal({ - Service: "ecs-tasks.amazonaws.com", - }) + Service: "ecs-tasks.amazonaws.com", + }) : iam.assumeRolePolicyForPrincipal({ - AWS: interpolate`arn:aws:iam::${getCallerIdentityOutput().accountId + AWS: interpolate`arn:aws:iam::${ + getCallerIdentityOutput().accountId }:root`, - }), + }), inlinePolicies: policy.apply(({ statements }) => statements ? [{ name: "inline", policy: policy.json }] : [], ), diff --git a/platform/src/components/aws/service.ts b/platform/src/components/aws/service.ts index a8dda7c2ab..6c9a23c011 100644 --- a/platform/src/components/aws/service.ts +++ b/platform/src/components/aws/service.ts @@ -1640,7 +1640,7 @@ export class Service extends Component implements Link.Linkable { const self = this; const clusterArn = args.cluster.nodes.cluster.arn; const clusterName = args.cluster.nodes.cluster.name; - const region = getRegionOutput({}, opts).name; + const region = getRegionOutput({}, opts).region; const dev = normalizeDev(); const wait = output(args.wait ?? false); const architecture = normalizeArchitecture(args); diff --git a/platform/src/components/aws/ssr-site.ts b/platform/src/components/aws/ssr-site.ts index dcf5e43819..757d059aa7 100644 --- a/platform/src/components/aws/ssr-site.ts +++ b/platform/src/components/aws/ssr-site.ts @@ -948,7 +948,7 @@ async function handler(event) { function normalizeRegions() { return output( - args.regions ?? [getRegionOutput(undefined, { parent: self }).name], + args.regions ?? [getRegionOutput(undefined, { parent: self }).region], ).apply((regions) => { if (regions.length === 0) throw new VisibleError( @@ -1356,7 +1356,7 @@ async function handler(event) { bucketName: bucket.name, files: bucketFiles, purge, - region: getRegionOutput(undefined, { parent: self }).name, + region: getRegionOutput(undefined, { parent: self }).region, }, { parent: self }, ); @@ -1443,7 +1443,7 @@ async function handler(event) { } : undefined, servers: servers.map((s) => [ - new URL(s.url).host, + new URL(s.url!).host, supportedRegions[s.region as keyof typeof supportedRegions].lat, supportedRegions[s.region as keyof typeof supportedRegions].lon, ]), diff --git a/platform/src/components/aws/static-site.ts b/platform/src/components/aws/static-site.ts index c6498701b1..f40b803353 100644 --- a/platform/src/components/aws/static-site.ts +++ b/platform/src/components/aws/static-site.ts @@ -526,46 +526,46 @@ export interface StaticSiteArgs extends BaseStaticSiteArgs { invalidation?: Input< | false | { - /** - * Configure if `sst deploy` should wait for the CloudFront cache invalidation to finish. - * - * :::tip - * For non-prod environments it might make sense to pass in `false`. - * ::: - * - * Waiting for the CloudFront cache invalidation process to finish ensures that the new content will be served once the deploy finishes. However, this process can sometimes take more than 5 mins. - * @default `false` - * @example - * ```js - * { - * invalidation: { - * wait: true - * } - * } - * ``` - */ - wait?: Input; - /** - * The paths to invalidate. - * - * You can either pass in an array of glob patterns to invalidate specific files. Or you can use the built-in option `all` to invalidation all files when any file changes. - * - * :::note - * Invalidating `all` counts as one invalidation, while each glob pattern counts as a single invalidation path. - * ::: - * @default `"all"` - * @example - * Invalidate the `index.html` and all files under the `products/` route. - * ```js - * { - * invalidation: { - * paths: ["/index.html", "/products/*"] - * } - * } - * ``` - */ - paths?: Input<"all" | string[]>; - } + /** + * Configure if `sst deploy` should wait for the CloudFront cache invalidation to finish. + * + * :::tip + * For non-prod environments it might make sense to pass in `false`. + * ::: + * + * Waiting for the CloudFront cache invalidation process to finish ensures that the new content will be served once the deploy finishes. However, this process can sometimes take more than 5 mins. + * @default `false` + * @example + * ```js + * { + * invalidation: { + * wait: true + * } + * } + * ``` + */ + wait?: Input; + /** + * The paths to invalidate. + * + * You can either pass in an array of glob patterns to invalidate specific files. Or you can use the built-in option `all` to invalidation all files when any file changes. + * + * :::note + * Invalidating `all` counts as one invalidation, while each glob pattern counts as a single invalidation path. + * ::: + * @default `"all"` + * @example + * Invalidate the `index.html` and all files under the `products/` route. + * ```js + * { + * invalidation: { + * paths: ["/index.html", "/products/*"] + * } + * } + * ``` + */ + paths?: Input<"all" | string[]>; + } >; /** * @deprecated The `route.path` prop is now the recommended way to configure the base @@ -876,17 +876,17 @@ export class StaticSite extends Component implements Link.Linkable { // remove leading and trailing slashes from the path path: args.assets?.path ? output(args.assets?.path).apply((v) => - v.replace(/^\//, "").replace(/\/$/, ""), - ) + v.replace(/^\//, "").replace(/\/$/, ""), + ) : undefined, purge: output(args.assets?.purge ?? true), // normalize to /path format routes: args.assets?.routes ? output(args.assets?.routes).apply((v) => - v.map( - (route) => "/" + route.replace(/^\//, "").replace(/\/$/, ""), - ), - ) + v.map( + (route) => "/" + route.replace(/^\//, "").replace(/\/$/, ""), + ), + ) : [], }; } @@ -907,9 +907,9 @@ export class StaticSite extends Component implements Link.Linkable { function getBucketDetails() { const s3Bucket = bucket ? bucket.nodes.bucket - : s3.BucketV2.get(`${name}Assets`, assets.bucket!, undefined, { - parent: self, - }); + : s3.Bucket.get(`${name}Assets`, assets.bucket!, undefined, { + parent: self, + }); return { bucketName: s3Bucket.bucket, @@ -984,7 +984,7 @@ export class StaticSite extends Component implements Link.Linkable { bucketName, files: bucketFiles, purge: assets.purge, - region: getRegionOutput(undefined, { parent: self }).name, + region: getRegionOutput(undefined, { parent: self }).region, }, { parent: self }, ); diff --git a/platform/src/components/aws/vpc-v1.ts b/platform/src/components/aws/vpc-v1.ts index 7bda017e49..df0470d100 100644 --- a/platform/src/components/aws/vpc-v1.ts +++ b/platform/src/components/aws/vpc-v1.ts @@ -244,7 +244,7 @@ export class Vpc extends Component { args?.transform?.elasticIp, `${name}ElasticIp${i + 1}`, { - vpc: true, + domain: "vpc", }, { parent }, ), diff --git a/platform/src/components/aws/vpc.ts b/platform/src/components/aws/vpc.ts index 9391cc5e30..03e1530c9f 100644 --- a/platform/src/components/aws/vpc.ts +++ b/platform/src/components/aws/vpc.ts @@ -424,7 +424,7 @@ export class Vpc extends Component implements Link.Linkable { parent: self, }); - const vpcId = vpc.tags.apply((tags) => { + const vpcId = vpc.tagsAll.apply((tags) => { registerVersion( tags?.["sst:component-version"] ? parseInt(tags["sst:component-version"]) @@ -880,7 +880,7 @@ export class Vpc extends Component implements Link.Linkable { args.transform?.elasticIp, `${name}ElasticIp${i + 1}`, { - vpc: true, + domain: "vpc", }, { parent: self }, ), diff --git a/platform/src/components/component.ts b/platform/src/components/component.ts index ddf322e8d5..78ea304cf3 100644 --- a/platform/src/components/component.ts +++ b/platform/src/components/component.ts @@ -159,7 +159,7 @@ export class Component extends ComponentResource { "aws:rds/proxyDefaultTargetGroup:ProxyDefaultTargetGroup", "aws:rds/proxyTarget:ProxyTarget", "aws:route53/record:Record", - "aws:s3/bucketCorsConfigurationV2:BucketCorsConfigurationV2", + "aws:s3/bucketCorsConfiguration:BucketCorsConfiguration", "aws:s3/bucketNotification:BucketNotification", "aws:s3/bucketObject:BucketObject", "aws:s3/bucketObjectv2:BucketObjectv2", @@ -168,6 +168,8 @@ export class Component extends ComponentResource { "aws:s3/bucketVersioningV2:BucketVersioningV2", "aws:s3/bucketLifecycleConfigurationV2:BucketLifecycleConfigurationV2", "aws:s3/bucketWebsiteConfigurationV2:BucketWebsiteConfigurationV2", + "aws:s3/bucketVersioning:BucketVersioning", + "aws:s3/bucketWebsiteConfiguration:BucketWebsiteConfiguration", "aws:secretsmanager/secretVersion:SecretVersion", "aws:ses/domainIdentityVerification:DomainIdentityVerification", "aws:sesv2/configurationSetEventDestination:ConfigurationSetEventDestination", @@ -277,7 +279,7 @@ export class Component extends ComponentResource { { lower: true }, ], "aws:rds/subnetGroup:SubnetGroup": ["name", 255, { lower: true }], - "aws:s3/bucketV2:BucketV2": ["bucket", 63, { lower: true }], + "aws:s3/bucket:Bucket": ["bucket", 63, { lower: true }], "aws:secretsmanager/secret:Secret": ["name", 512], "aws:sesv2/configurationSet:ConfigurationSet": [ "configurationSetName", diff --git a/www/generate.ts b/www/generate.ts index 31d3075ea5..0ce0f84f05 100644 --- a/www/generate.ts +++ b/www/generate.ts @@ -1076,6 +1076,7 @@ function renderType( DistributionCustomErrorResponse: "cloudfront/distribution", DistributionDefaultCacheBehavior: "cloudfront/distribution", DistributionOrderedCacheBehavior: "cloudfront/distribution", + PolicyDocument: "iam/getpolicydocument", }[type.name]; if (!link) { // @ts-expect-error diff --git a/www/src/content/docs/docs/examples.mdx b/www/src/content/docs/docs/examples.mdx index f8f685e5c5..35de89ebc2 100644 --- a/www/src/content/docs/docs/examples.mdx +++ b/www/src/content/docs/docs/examples.mdx @@ -764,8 +764,8 @@ View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-bun-red --- ## AWS Cluster custom autoscaling -In this example, we'll create a cluster that can autoscales based on a custom -metric; in this case, the number of messages in a queue. +In this example, we'll create a cluster that autoscales based on a custom +metric. In this case, the number of messages in a queue. We'll create a queue, and two functions that'll seed and purge the queue. We'll also create two policies. @@ -1843,14 +1843,14 @@ View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-hono-re An example on how to enable streaming for Lambda functions using Hono. +While `sst dev` doesn't support streaming, we can conditionally enable it on deploy. + ```ts title="sst.config.ts" { - streaming: true + streaming: $dev ? false : true } ``` -While `sst dev` doesn't support streaming, we can conditionally enable it on deploy. - ```ts title="index.ts" export const handler = process.env.SST_LIVE ? handle(app) : streamHandle(app); ``` @@ -1871,7 +1871,7 @@ Here we are using a Function URL directly because API Gateway doesn't support st ```ts title="sst.config.ts" const hono = new sst.aws.Function("Hono", { url: true, - streaming: true, + streaming: $dev ? false : true, timeout: "15 minutes", handler: "index.handler", }); @@ -1880,7 +1880,7 @@ return { }; ``` -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-hono-steam). +View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-hono-stream). --- @@ -2533,6 +2533,82 @@ return { View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-lambda-vpc). +--- +## AWS Load Balancer Web Application Firewall (WAF) + +Enable WAF for an AWS Load Balancer. + +The WAF is configured to enable a rate limit and enables AWS managed rules. +```ts title="sst.config.ts" +const vpc = new sst.aws.Vpc("MyVpc"); +const cluster = new sst.aws.Cluster("MyCluster", { vpc }); +const service = cluster.addService("MyAppService", { + image: { + context: "./", + dockerfile: "packages/server/Dockerfile", + }, +}); + +const rateLimitRule = { + name: "RateLimitRule", + statement: { + rateBasedStatement: { + limit: 200, + aggregateKeyType: "IP", + }, + }, + priority: 1, + action: { block: {} }, + visibilityConfig: { + cloudwatchMetricsEnabled: true, + sampledRequestsEnabled: true, + metricName: "MyAppRateLimitRule", + }, +}; + +const awsManagedRules = { + name: "AWSManagedRules", + statement: { + managedRuleGroupStatement: { + name: "AWSManagedRulesCommonRuleSet", + vendorName: "AWS", + }, + }, + priority: 2, + overrideAction: { + none: {}, + }, + visibilityConfig: { + cloudwatchMetricsEnabled: true, + sampledRequestsEnabled: true, + metricName: "MyAppAWSManagedRules", + }, +}; + +const webAcl = new aws.wafv2.WebAcl("AppAlbWebAcl", { + defaultAction: { allow: {} }, + scope: "REGIONAL", + visibilityConfig: { + cloudwatchMetricsEnabled: true, + sampledRequestsEnabled: true, + metricName: "AppAlbWebAcl", + }, + rules: [rateLimitRule, awsManagedRules], +}); + +service.nodes.loadBalancer.arn.apply((arn) => { + new aws.wafv2.WebAclAssociation("MyAppAlbWebAclAssociation", { + resourceArn: arn, + webAclArn: webAcl.arn, + }); +}); + +return {}; +``` + +View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-load-balancer-waf). + + --- ## AWS multi-region @@ -3666,6 +3742,7 @@ View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-python) ## Subscribe to queues Create an SQS queue, subscribe to it, and publish to it from a function. + ```ts title="sst.config.ts" const queue = new sst.aws.Queue("MyQueue"); queue.subscribe("subscriber.handler"); @@ -3731,6 +3808,25 @@ export const handler: SQSHandler = async (event: SQSEvent) => { return { batchItemFailures }; }; ``` +```ts title="sst.config.ts" +const queue = new sst.aws.Queue("MyQueue"); +queue.subscribe("subscriber.handler", { + batch: { + partialResponses: true, + } +}); + +const app = new sst.aws.Function("MyApp", { + handler: "publisher.handler", + link: [queue], + url: true, +}); + +return { + app: app.url, + queue: queue.url, +}; +``` View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-queue).