From 60160bffe7f75f6b8e908b193525461323b5ed72 Mon Sep 17 00:00:00 2001
From: James <10496761+jamesgibbons92@users.noreply.github.com>
Date: Sat, 6 Dec 2025 00:26:20 +0000
Subject: [PATCH 1/7] Provider upgrades
---
pkg/project/run.go | 95 ++-
platform/package.json | 2 +-
platform/src/components/aws/apigatewayv1.ts | 178 +++---
platform/src/components/aws/aurora.ts | 4 +-
platform/src/components/aws/auth.ts | 9 +-
platform/src/components/aws/bucket.ts | 18 +-
platform/src/components/aws/cluster.ts | 2 +-
.../components/aws/cognito-identity-pool.ts | 2 +-
platform/src/components/aws/fargate.ts | 2 +-
platform/src/components/aws/function.ts | 580 +++++++++---------
platform/src/components/aws/https-redirect.ts | 2 +-
platform/src/components/aws/mysql.ts | 128 ++--
platform/src/components/aws/nextjs.ts | 4 +-
platform/src/components/aws/open-search.ts | 22 +-
platform/src/components/aws/postgres.ts | 2 +-
platform/src/components/aws/redis-v1.ts | 3 +-
platform/src/components/aws/redis.ts | 3 +-
platform/src/components/aws/router.ts | 2 +-
platform/src/components/aws/service-v1.ts | 17 +-
platform/src/components/aws/service.ts | 2 +-
platform/src/components/aws/ssr-site.ts | 6 +-
platform/src/components/aws/static-site.ts | 100 +--
platform/src/components/aws/vpc-v1.ts | 2 +-
platform/src/components/aws/vpc.ts | 4 +-
platform/src/components/component.ts | 6 +-
25 files changed, 649 insertions(+), 546 deletions(-)
diff --git a/pkg/project/run.go b/pkg/project/run.go
index 7aa6074be4..2c4951ce5c 100644
--- a/pkg/project/run.go
+++ b/pkg/project/run.go
@@ -16,6 +16,7 @@ import (
"syscall"
"time"
+ "github.com/Masterminds/semver/v3"
"github.com/pulumi/pulumi/sdk/v3/go/auto/events"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
"github.com/sst/sst/v3/internal/util"
@@ -40,6 +41,81 @@ func (p *Project) Run(ctx context.Context, input *StackInput) error {
return p.RunNext(ctx, input)
}
+func (p *Project) checkProviderUpgrading(workdir *PulumiWorkdir) map[string]string {
+ log := slog.Default().With("service", "project.run")
+ result := make(map[string]string)
+
+ checkpoint, err := workdir.Export()
+ if err != nil {
+ log.Info("could not export checkpoint for provider version comparison", "err", err)
+ return result
+ }
+
+ if checkpoint == nil || checkpoint.Latest == nil {
+ return result
+ }
+
+ stateProviders := make(map[string]string)
+
+ for _, resource := range checkpoint.Latest.Resources {
+ // "urn:pulumi:stage::app::pulumi:providers:random::default_4_18_2::61adb215-aad5-4981-a591-ef3b50cb5dcc
+ providerUrnParts := strings.Split(string(resource.Provider), ":")
+ if len(providerUrnParts) != 13 {
+ continue
+ }
+ provider, version := providerUrnParts[8], providerUrnParts[10]
+
+ if versionStr, ok := strings.CutPrefix(version, "default_"); ok {
+ versionSemVer := strings.ReplaceAll(versionStr, "_", ".")
+ stateProviders[provider] = versionSemVer
+ log.Info("found provider in state", "provider", provider, "version", versionSemVer)
+ }
+ }
+
+ for _, lockEntry := range p.lock {
+ if stateVersion, exists := stateProviders[lockEntry.Name]; exists {
+ log.Info("comparing versions",
+ "provider", lockEntry.Name,
+ "state", stateVersion,
+ "lock", lockEntry.Version)
+
+ stateVer, err := semver.NewVersion(stateVersion)
+ lockVer, err2 := semver.NewVersion(lockEntry.Version)
+
+ if err == nil && err2 == nil {
+ upgradeType := ""
+ if stateVer.Major() < lockVer.Major() {
+ upgradeType = "major"
+ log.Warn("major provider version upgrade detected",
+ "provider", lockEntry.Name,
+ "state_version", stateVersion,
+ "lock_version", lockEntry.Version,
+ "state_major", stateVer.Major(),
+ "lock_major", lockVer.Major())
+ } else if stateVer.Minor() < lockVer.Minor() {
+ upgradeType = "minor"
+ log.Info("minor provider version upgrade detected",
+ "provider", lockEntry.Name,
+ "state_version", stateVersion,
+ "lock_version", lockEntry.Version)
+ } else if stateVer.Patch() < lockVer.Patch() {
+ upgradeType = "patch"
+ log.Info("patch provider version upgrade detected",
+ "provider", lockEntry.Name,
+ "state_version", stateVersion,
+ "lock_version", lockEntry.Version)
+ }
+
+ if upgradeType != "" {
+ result[lockEntry.Name] = upgradeType
+ }
+ }
+ }
+ }
+
+ return result
+}
+
func (p *Project) RunNext(ctx context.Context, input *StackInput) error {
log := slog.Default().With("service", "project.run")
log.Info("running stack command", "cmd", input.Command)
@@ -289,8 +365,25 @@ func (p *Project) RunNext(ctx context.Context, input *StackInput) error {
}
if input.Command == "deploy" || input.Command == "diff" {
+ // Compare provider versions between state and lock file
+ providerVersions := p.checkProviderUpgrading(workdir)
+
+ // Check for major version upgrades and collect affected providers
+ majorUpgrades := []string{}
+ for provider, upgradeType := range providerVersions {
+ if upgradeType == "major" {
+ majorUpgrades = append(majorUpgrades, provider)
+ }
+ }
+
+ // Return error if major upgrades detected
+ if len(majorUpgrades) > 0 {
+ return util.NewReadableError(nil, fmt.Sprintf("Major version upgrade detected for provider(s): %s. Run `sst refresh` to migrate your state to this version. Then run `sst diff` to verify there are no unexpected changes. For more information, visit ", strings.Join(majorUpgrades, ", ")))
+ }
+
for provider, opts := range p.app.Providers {
for key, value := range opts.(map[string]interface{}) {
+ log.Info("setting provider config", "provider", provider, "key", key, "value", value)
switch v := value.(type) {
case map[string]interface{}:
bytes, err := json.Marshal(v)
@@ -315,7 +408,7 @@ 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...)
case "remove":
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",
From a176245720b3b8c06c52de0b1c51a62d60debcd8 Mon Sep 17 00:00:00 2001
From: James <10496761+jamesgibbons92@users.noreply.github.com>
Date: Sun, 7 Dec 2025 00:04:57 +0000
Subject: [PATCH 2/7] Refactor
---
pkg/project/preflight.go | 143 +++++++++++++++++++++++++++++++++++++++
pkg/project/run.go | 84 +----------------------
2 files changed, 145 insertions(+), 82 deletions(-)
create mode 100644 pkg/project/preflight.go
diff --git a/pkg/project/preflight.go b/pkg/project/preflight.go
new file mode 100644
index 0000000000..6bc1c2f447
--- /dev/null
+++ b/pkg/project/preflight.go
@@ -0,0 +1,143 @@
+package project
+
+import (
+ "log/slog"
+ "strings"
+
+ "github.com/Masterminds/semver/v3"
+)
+
+type ProjectPreflight struct{}
+
+func (p *Project) getProvidersUpgrading(workdir *PulumiWorkdir) map[string]string {
+ log := slog.Default().With("service", "project.preflight")
+ result := make(map[string]string)
+ checkpoint, err := workdir.Export()
+
+ if err != nil || checkpoint == nil || checkpoint.Latest == nil {
+ return result
+ }
+
+ // looking at provider resources (type = "pulumi:providers:*")
+ providerVersions := make(map[string]string) // URN -> version
+ for _, resource := range checkpoint.Latest.Resources {
+ resourceType := string(resource.Type)
+ if !strings.HasPrefix(resourceType, "pulumi:providers:") {
+ continue
+ }
+
+ if resource.Inputs == nil {
+ continue
+ }
+
+ versionInterface, ok := resource.Inputs["version"]
+ if !ok {
+ continue
+ }
+
+ version, ok := versionInterface.(string)
+ if !ok {
+ continue
+ }
+
+ providerVersions[string(resource.URN)] = version
+ }
+
+ // Track which providers are actually used by resources
+ providersInUse := make(map[string]string) // provider name -> version
+
+ for _, resource := range checkpoint.Latest.Resources {
+ if resource.Provider == "" {
+ continue
+ }
+
+ // The provider field has a UUID at the end that we need to strip
+ // Resource provider: "urn:pulumi:james::aws-full::pulumi:providers:random::default_4_18_2::61adb215-aad5-4981-a591-ef3b50cb5dcc"
+ // Provider URN: "urn:pulumi:james::aws-full::pulumi:providers:random::default_4_18_2"
+ // We need to strip the last "::" and UUID
+ providerRef := string(resource.Provider)
+ providerRefParts := strings.Split(providerRef, "::")
+ if len(providerRefParts) < 4 {
+ continue
+ }
+ // Remove the UUID (last part) to get the provider URN
+ providerURN := strings.Join(providerRefParts[:len(providerRefParts)-1], "::")
+
+ // Look up the provider version from the map we built
+ version, ok := providerVersions[providerURN]
+ if !ok {
+ continue
+ }
+
+ // Extract provider type from the URN (e.g., "pulumi:providers:aws")
+ if len(providerRefParts) < 3 {
+ continue
+ }
+ providerType := providerRefParts[2]
+ if !strings.HasPrefix(providerType, "pulumi:providers:") {
+ continue
+ }
+
+ providerName := strings.TrimPrefix(providerType, "pulumi:providers:")
+
+ // Track provider usage - handle multiple versions
+ if _, ok := providersInUse[providerName]; !ok {
+ providersInUse[providerName] = version
+ }
+ }
+
+ // Step 3: Compare versions in state with lock file
+ for _, lockEntry := range p.lock {
+ currProviderVersion, exists := providersInUse[lockEntry.Name]
+ if !exists {
+ continue
+ }
+
+ // Check each version in use for this provider
+ log.Info("comparing versions",
+ "provider", lockEntry.Name,
+ "state", currProviderVersion,
+ "lock", lockEntry.Version)
+
+ stateVer, err := semver.NewVersion(currProviderVersion)
+ lockVer, err2 := semver.NewVersion(lockEntry.Version)
+
+ if err == nil && err2 == nil {
+ upgradeType := ""
+ if stateVer.Major() < lockVer.Major() {
+ upgradeType = "major"
+ log.Warn("major provider version upgrade detected",
+ "provider", lockEntry.Name,
+ "state_version", currProviderVersion,
+ "lock_version", lockEntry.Version,
+ "state_major", stateVer.Major(),
+ "lock_major", lockVer.Major())
+ } else if stateVer.Minor() < lockVer.Minor() {
+ upgradeType = "minor"
+ log.Info("minor provider version upgrade detected",
+ "provider", lockEntry.Name,
+ "state_version", currProviderVersion,
+ "lock_version", lockEntry.Version)
+ } else if stateVer.Patch() < lockVer.Patch() {
+ upgradeType = "patch"
+ log.Info("patch provider version upgrade detected",
+ "provider", lockEntry.Name,
+ "state_version", currProviderVersion,
+ "lock_version", lockEntry.Version)
+ }
+
+ if upgradeType != "" {
+ if existing, ok := result[lockEntry.Name]; ok {
+ if existing == "major" {
+ continue
+ } else if existing == "minor" && upgradeType == "patch" {
+ continue
+ }
+ }
+ result[lockEntry.Name] = upgradeType
+ }
+ }
+ }
+
+ return result
+}
diff --git a/pkg/project/run.go b/pkg/project/run.go
index 2c4951ce5c..2a323fa0d8 100644
--- a/pkg/project/run.go
+++ b/pkg/project/run.go
@@ -16,7 +16,6 @@ import (
"syscall"
"time"
- "github.com/Masterminds/semver/v3"
"github.com/pulumi/pulumi/sdk/v3/go/auto/events"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
"github.com/sst/sst/v3/internal/util"
@@ -41,81 +40,6 @@ func (p *Project) Run(ctx context.Context, input *StackInput) error {
return p.RunNext(ctx, input)
}
-func (p *Project) checkProviderUpgrading(workdir *PulumiWorkdir) map[string]string {
- log := slog.Default().With("service", "project.run")
- result := make(map[string]string)
-
- checkpoint, err := workdir.Export()
- if err != nil {
- log.Info("could not export checkpoint for provider version comparison", "err", err)
- return result
- }
-
- if checkpoint == nil || checkpoint.Latest == nil {
- return result
- }
-
- stateProviders := make(map[string]string)
-
- for _, resource := range checkpoint.Latest.Resources {
- // "urn:pulumi:stage::app::pulumi:providers:random::default_4_18_2::61adb215-aad5-4981-a591-ef3b50cb5dcc
- providerUrnParts := strings.Split(string(resource.Provider), ":")
- if len(providerUrnParts) != 13 {
- continue
- }
- provider, version := providerUrnParts[8], providerUrnParts[10]
-
- if versionStr, ok := strings.CutPrefix(version, "default_"); ok {
- versionSemVer := strings.ReplaceAll(versionStr, "_", ".")
- stateProviders[provider] = versionSemVer
- log.Info("found provider in state", "provider", provider, "version", versionSemVer)
- }
- }
-
- for _, lockEntry := range p.lock {
- if stateVersion, exists := stateProviders[lockEntry.Name]; exists {
- log.Info("comparing versions",
- "provider", lockEntry.Name,
- "state", stateVersion,
- "lock", lockEntry.Version)
-
- stateVer, err := semver.NewVersion(stateVersion)
- lockVer, err2 := semver.NewVersion(lockEntry.Version)
-
- if err == nil && err2 == nil {
- upgradeType := ""
- if stateVer.Major() < lockVer.Major() {
- upgradeType = "major"
- log.Warn("major provider version upgrade detected",
- "provider", lockEntry.Name,
- "state_version", stateVersion,
- "lock_version", lockEntry.Version,
- "state_major", stateVer.Major(),
- "lock_major", lockVer.Major())
- } else if stateVer.Minor() < lockVer.Minor() {
- upgradeType = "minor"
- log.Info("minor provider version upgrade detected",
- "provider", lockEntry.Name,
- "state_version", stateVersion,
- "lock_version", lockEntry.Version)
- } else if stateVer.Patch() < lockVer.Patch() {
- upgradeType = "patch"
- log.Info("patch provider version upgrade detected",
- "provider", lockEntry.Name,
- "state_version", stateVersion,
- "lock_version", lockEntry.Version)
- }
-
- if upgradeType != "" {
- result[lockEntry.Name] = upgradeType
- }
- }
- }
- }
-
- return result
-}
-
func (p *Project) RunNext(ctx context.Context, input *StackInput) error {
log := slog.Default().With("service", "project.run")
log.Info("running stack command", "cmd", input.Command)
@@ -365,25 +289,21 @@ func (p *Project) RunNext(ctx context.Context, input *StackInput) error {
}
if input.Command == "deploy" || input.Command == "diff" {
- // Compare provider versions between state and lock file
- providerVersions := p.checkProviderUpgrading(workdir)
+ providersUpgrading := p.getProvidersUpgrading(workdir)
- // Check for major version upgrades and collect affected providers
majorUpgrades := []string{}
- for provider, upgradeType := range providerVersions {
+ for provider, upgradeType := range providersUpgrading {
if upgradeType == "major" {
majorUpgrades = append(majorUpgrades, provider)
}
}
- // Return error if major upgrades detected
if len(majorUpgrades) > 0 {
return util.NewReadableError(nil, fmt.Sprintf("Major version upgrade detected for provider(s): %s. Run `sst refresh` to migrate your state to this version. Then run `sst diff` to verify there are no unexpected changes. For more information, visit ", strings.Join(majorUpgrades, ", ")))
}
for provider, opts := range p.app.Providers {
for key, value := range opts.(map[string]interface{}) {
- log.Info("setting provider config", "provider", provider, "key", key, "value", value)
switch v := value.(type) {
case map[string]interface{}:
bytes, err := json.Marshal(v)
From 4d27c2a80cad34f1a2e274633de54f3062c9a79d Mon Sep 17 00:00:00 2001
From: James <10496761+jamesgibbons92@users.noreply.github.com>
Date: Fri, 19 Dec 2025 12:06:29 +0000
Subject: [PATCH 3/7] Simplify upgrade check
---
pkg/project/preflight.go | 158 ++++++++++++++-------------------------
pkg/project/run.go | 13 +---
2 files changed, 58 insertions(+), 113 deletions(-)
diff --git a/pkg/project/preflight.go b/pkg/project/preflight.go
index 6bc1c2f447..7edf08868d 100644
--- a/pkg/project/preflight.go
+++ b/pkg/project/preflight.go
@@ -1,143 +1,95 @@
package project
import (
- "log/slog"
"strings"
"github.com/Masterminds/semver/v3"
+ "github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
)
-type ProjectPreflight struct{}
-
-func (p *Project) getProvidersUpgrading(workdir *PulumiWorkdir) map[string]string {
- log := slog.Default().With("service", "project.preflight")
- result := make(map[string]string)
- checkpoint, err := workdir.Export()
-
- if err != nil || checkpoint == nil || checkpoint.Latest == nil {
- return result
- }
+type migrationNotice struct {
+ providerName string
+ fromVersion string
+ toVersion string
+ message string
+}
- // looking at provider resources (type = "pulumi:providers:*")
- providerVersions := make(map[string]string) // URN -> version
- for _, resource := range checkpoint.Latest.Resources {
- resourceType := string(resource.Type)
- if !strings.HasPrefix(resourceType, "pulumi:providers:") {
- continue
- }
+var migrationNotices = []migrationNotice{
+ {
+ providerName: "aws",
+ fromVersion: "6.0.0",
+ toVersion: "7.0.0",
+ message: "Upgrading AWS provider to v7 contains some breaking changes in pulumi components and requires state migration before deploying. Refer 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",
+ },
+}
- if resource.Inputs == nil {
- continue
- }
+func (p *Project) checkProviderUpgrade(resources []apitype.ResourceV3) []string {
+ providerVersions := make(map[string]string)
- versionInterface, ok := resource.Inputs["version"]
- if !ok {
+ // 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
}
-
- version, ok := versionInterface.(string)
+ versionOutput, ok := v.Outputs["version"].(string)
if !ok {
continue
}
-
- providerVersions[string(resource.URN)] = version
+ providerVersions[name] = versionOutput
}
- // Track which providers are actually used by resources
- providersInUse := make(map[string]string) // provider name -> version
+ var messages []string
- for _, resource := range checkpoint.Latest.Resources {
- if resource.Provider == "" {
+ for _, entry := range p.lock {
+ currentVersionStr, ok := providerVersions[entry.Name]
+ if !ok {
continue
}
- // The provider field has a UUID at the end that we need to strip
- // Resource provider: "urn:pulumi:james::aws-full::pulumi:providers:random::default_4_18_2::61adb215-aad5-4981-a591-ef3b50cb5dcc"
- // Provider URN: "urn:pulumi:james::aws-full::pulumi:providers:random::default_4_18_2"
- // We need to strip the last "::" and UUID
- providerRef := string(resource.Provider)
- providerRefParts := strings.Split(providerRef, "::")
- if len(providerRefParts) < 4 {
+ currentVersion, err := semver.NewVersion(currentVersionStr)
+ if err != nil {
continue
}
- // Remove the UUID (last part) to get the provider URN
- providerURN := strings.Join(providerRefParts[:len(providerRefParts)-1], "::")
- // Look up the provider version from the map we built
- version, ok := providerVersions[providerURN]
- if !ok {
+ targetVersion, err := semver.NewVersion(entry.Version)
+ if err != nil {
continue
}
- // Extract provider type from the URN (e.g., "pulumi:providers:aws")
- if len(providerRefParts) < 3 {
- continue
- }
- providerType := providerRefParts[2]
- if !strings.HasPrefix(providerType, "pulumi:providers:") {
+ // Check if this is an upgrade
+ if !currentVersion.LessThan(targetVersion) {
continue
}
- providerName := strings.TrimPrefix(providerType, "pulumi:providers:")
-
- // Track provider usage - handle multiple versions
- if _, ok := providersInUse[providerName]; !ok {
- providersInUse[providerName] = version
- }
- }
+ // Check against all applicable upgrade rules
+ for _, rule := range migrationNotices {
+ if rule.providerName != entry.Name {
+ continue
+ }
- // Step 3: Compare versions in state with lock file
- for _, lockEntry := range p.lock {
- currProviderVersion, exists := providersInUse[lockEntry.Name]
- if !exists {
- continue
- }
+ ruleFrom, err := semver.NewVersion(rule.fromVersion)
+ if err != nil {
+ continue
+ }
- // Check each version in use for this provider
- log.Info("comparing versions",
- "provider", lockEntry.Name,
- "state", currProviderVersion,
- "lock", lockEntry.Version)
-
- stateVer, err := semver.NewVersion(currProviderVersion)
- lockVer, err2 := semver.NewVersion(lockEntry.Version)
-
- if err == nil && err2 == nil {
- upgradeType := ""
- if stateVer.Major() < lockVer.Major() {
- upgradeType = "major"
- log.Warn("major provider version upgrade detected",
- "provider", lockEntry.Name,
- "state_version", currProviderVersion,
- "lock_version", lockEntry.Version,
- "state_major", stateVer.Major(),
- "lock_major", lockVer.Major())
- } else if stateVer.Minor() < lockVer.Minor() {
- upgradeType = "minor"
- log.Info("minor provider version upgrade detected",
- "provider", lockEntry.Name,
- "state_version", currProviderVersion,
- "lock_version", lockEntry.Version)
- } else if stateVer.Patch() < lockVer.Patch() {
- upgradeType = "patch"
- log.Info("patch provider version upgrade detected",
- "provider", lockEntry.Name,
- "state_version", currProviderVersion,
- "lock_version", lockEntry.Version)
+ ruleTo, err := semver.NewVersion(rule.toVersion)
+ if err != nil {
+ continue
}
- if upgradeType != "" {
- if existing, ok := result[lockEntry.Name]; ok {
- if existing == "major" {
- continue
- } else if existing == "minor" && upgradeType == "patch" {
- continue
- }
- }
- result[lockEntry.Name] = upgradeType
+ // 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 result
+ return messages
}
diff --git a/pkg/project/run.go b/pkg/project/run.go
index 2a323fa0d8..cf7d1bda63 100644
--- a/pkg/project/run.go
+++ b/pkg/project/run.go
@@ -289,17 +289,10 @@ func (p *Project) RunNext(ctx context.Context, input *StackInput) error {
}
if input.Command == "deploy" || input.Command == "diff" {
- providersUpgrading := p.getProvidersUpgrading(workdir)
+ upgradeMsgs := p.checkProviderUpgrade(completed.Resources)
- majorUpgrades := []string{}
- for provider, upgradeType := range providersUpgrading {
- if upgradeType == "major" {
- majorUpgrades = append(majorUpgrades, provider)
- }
- }
-
- if len(majorUpgrades) > 0 {
- return util.NewReadableError(nil, fmt.Sprintf("Major version upgrade detected for provider(s): %s. Run `sst refresh` to migrate your state to this version. Then run `sst diff` to verify there are no unexpected changes. For more information, visit ", strings.Join(majorUpgrades, ", ")))
+ if len(upgradeMsgs) > 0 {
+ return util.NewReadableError(nil, strings.Join(upgradeMsgs, "\n\n"))
}
for provider, opts := range p.app.Providers {
From 5954aff1329eb58d21bddca23811ca35344f3532 Mon Sep 17 00:00:00 2001
From: James <10496761+jamesgibbons92@users.noreply.github.com>
Date: Fri, 19 Dec 2025 20:56:15 +0000
Subject: [PATCH 4/7] Add --refresh arg to deploy
---
cmd/sst/deploy.go | 9 +++++++++
pkg/project/preflight.go | 2 +-
pkg/project/run.go | 5 ++++-
pkg/project/stack.go | 1 +
4 files changed, 15 insertions(+), 2 deletions(-)
diff --git a/cmd/sst/deploy.go b/cmd/sst/deploy.go
index 6cda321c53..710da9d260 100644
--- a/cmd/sst/deploy.go
+++ b/cmd/sst/deploy.go
@@ -105,6 +105,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 +164,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
index 7edf08868d..20543418fa 100644
--- a/pkg/project/preflight.go
+++ b/pkg/project/preflight.go
@@ -19,7 +19,7 @@ var migrationNotices = []migrationNotice{
providerName: "aws",
fromVersion: "6.0.0",
toVersion: "7.0.0",
- message: "Upgrading AWS provider to v7 contains some breaking changes in pulumi components and requires state migration before deploying. Refer 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",
+ 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",
},
}
diff --git a/pkg/project/run.go b/pkg/project/run.go
index cf7d1bda63..273ed0a2a3 100644
--- a/pkg/project/run.go
+++ b/pkg/project/run.go
@@ -291,7 +291,7 @@ 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 {
+ if len(upgradeMsgs) > 0 && !input.Refresh {
return util.NewReadableError(nil, strings.Join(upgradeMsgs, "\n\n"))
}
@@ -324,6 +324,9 @@ func (p *Project) RunNext(ctx context.Context, input *StackInput) error {
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{}
From 24f85f4992d6ea7799725827a44fd73b08654e77 Mon Sep 17 00:00:00 2001
From: James <10496761+jamesgibbons92@users.noreply.github.com>
Date: Fri, 19 Dec 2025 21:16:51 +0000
Subject: [PATCH 5/7] Docs
---
cmd/sst/deploy.go | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/cmd/sst/deploy.go b/cmd/sst/deploy.go
index 710da9d260..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{
From bd4237340476d42fb60b55a579977e707b44482e Mon Sep 17 00:00:00 2001
From: James <10496761+jamesgibbons92@users.noreply.github.com>
Date: Fri, 19 Dec 2025 21:54:50 +0000
Subject: [PATCH 6/7] Fix docs generate
---
www/generate.ts | 1 +
www/src/content/docs/docs/examples.mdx | 110 +++++++++++++++++++++++--
2 files changed, 104 insertions(+), 7 deletions(-)
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).
From 9bc91fa6d1b343f561310559bad79ce3497e3e14 Mon Sep 17 00:00:00 2001
From: James <10496761+jamesgibbons92@users.noreply.github.com>
Date: Thu, 8 Jan 2026 23:38:47 +0000
Subject: [PATCH 7/7] Lock file
---
bun.lockb | Bin 537514 -> 537538 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
diff --git a/bun.lockb b/bun.lockb
index 24ee0c0470b250a3160c35bc5ab6926b4d89f9c4..dbca3c34f9754e4e150f9f5168e38322099988b6 100755
GIT binary patch
delta 4376
zcmXxm37Adi9mnx=XXeb@YahGF!ido1zK&v}$=8X}`ajpZA%+^FN>aKj%H?J@0#*omXp}xLRvf
z)7t9~|N8#CnjKeM9nq@IW4ZOzkYDhw=nbUBgQ|ucxJ}uSJwIUa-~1Lfb`K7M5naWUGT!wq3^5@smUY+ZET<
zmD~Pei$4V(@@4-4WpkR#B_lq@@T%dRD+j=K?S
zY^_527j`E`8C$!s2R4A$rj}$IOzXNQ(Sv!R)(_KLwHHwj)AAdLX=L(<>aH8)y53kt
z*A2zg?=!mpd@`-mVW94^4>8p?(S^@qt#t%#5~fc25^ZghUDprm$VN9`!1Hkv(bBGBc@RpNrd!KpY0wWJ4*L2
zYyj>xe$Rys*}i9c-qwii0o(U&dXc=m{!gNo0jQm+mCEorsrgO{u7CjI)e5m
zP!}_a=x6)f_G4_g?YwO=R@rvJHU;a-^6bd=qHQYqAWZF&Z5nx8SKW!^GN|+96SB^1
z|8ARZ>%#ViZ3g)p)OFQwKH*IA?U>qsY_rHG*zRDOGqVXjHd;Km=fXMcO(0jh@4^?z
zwFK4v>$`8iuDpKt+ogp1MAE#ta{eM*NdN4&h|2SkGS#SSIjG*
zo}DIC1s4|DG@&wWi*1@v6>Uq%cY7{W!gLKwiGSIux^5YE(-yC0Tn^pV8US392=TyYOf1oHRDIZN>C{
zS8Il;e)FU)QqucK*=}*0R#)
zweU@18;O=xOWRxIT6VHl7=J+#@j0E$XWPzoJIMcl6|ik@+evNB
z{JF4TFLB=3(}nv~$en2N^s>E2p3hKgPUPA4lk4s@lD#qA-2pDyQMJ=Q9q2o;1JQ?OJ}g_eu=4Lbv6W3#~%jQl6j(Z5F96t5=V%`$#(xsi$}si)DPZ&Fw9ShEKM{>
z4Hwo38K=@jI#GtmAYw!`xg#~qpe(sN3=@%FbFmkRmxy^poS07(5DSQf#3JHl;uWHh
zSWGM>mJ!Q|6-3Wur;IQ^5;>lHHyVygOF5aIELS#sJ0*Fne7HH19Gw|{l9K3IIlPu^
zQ6;P&DYdm~$(YQ}OEi6umX%ywH5{Fll9iUc**ct-87s;w`FUG%d|vp)*7R0Qb8?$z
zC!hN=Y`rITZxgpmo1I*@DgAULReaQ^7)(~s{Tcf6=x(Xjo)qIDN!#v|pW
z_sX}&bXu}=K{T~w%>v(_{?dw%-GtcXLwC+&M_&(&0)!C$}m*pfK~>8Uvdv93$gn%)Wb
zU5oYY@<1#SiPD=wMW55C{~xl3SUR<$f$yzJn0q}|qgykd9&o6s4>iXs^I0F8**e-X
zu;#X&VUaE|#D_9D)Y6AW+bUqKY?EyjvDUWP;fGuzS>!|NwXF{=$13r;3enEC4$~K{
zO61tq+p1yhZ5wP^SO;5)tvZ%#+gNVNNDVO0xXBH(v5vOQwwl<3w%2U6u!n40Y_+kf
zm~M8ftqyrjx7%i`i`9uI`AgXjYTD`%b=`1>8{UVJ-YYw8_4%ZEQ`u#^pIj56QfhmE
zT!XIi2Btn55E@05JxNeA)sU#8F{h+_awGC8wga}tSYSJ7Yl6jWhipx;Y};W>9XBId
z*^athvN_n)c+A)WJLKCwZfl8^+D_P7VcRi{#7RtbYhoUzL442FhI|O76@1#(mb`Jr
zNHUTjIb&=`ev9GIFrK&NkpC6aC|$6%Cx2f+`3%!scOcr@%51qrTEJ)0T|Vf!g&TUd58y
zlj+8ubO6#(pJ+p02^CmVe<0DHZ?BSr>FGO&Xn|=<
z=3?r1F#SjBxM4>(9KwO>ZkUhhW`+{8NE%Rg!t~9D5i@K9-EKJ6Stn2iVd`WA(bYD@
z?HZ1Y7{0A0iCp>Uhb2hvbClzwmofYL;Z^FXEr@tR@o-n^lVveE3oBg{?`~M8THUu
zXPZp^r1$IwOr!O4VzjNq_6tlm-GzFi&z)k^cHV4zhWsY;rLDUK%h3E!HEQqf5>ijI
zY47f}J!|Vhow7|QAEy&2$1vSsAu-%`+%^LnZ98F`i6yg*CylefKJ3}v)TeB-$wy%-
z@7m^&>zh1G{U=P9nQQAqecCq9)|dLMZ9e%{&h4l9{|BhMUO;RIRW8_mNj}B)F{Xv|
zD?;Wf%h^RFo=WWNulz67!!Ba4WMn93DQ
z9TySpZC8E4#n|6DL3{j~?FI4=brH&S+po#b+P?DjUc`=R|7kzpaKqn_&vL_WY)i0y
zmH0#X*0z+qzwM^&x0t3*i|jwP-`S>8e`kBirZtwlWn5;|BKzLnhdUa5H&d0VT`!8hQVF!hce>EqvChSE*s!
zO0KP{l5N{YuBWGpehsVN;g1kDK-gZDkqW?CvDD=9dh6f3KI}N2?
zm-Oup5gJwvo8FAn?l7U>b1Hf>k{uz6{gBd|k?bf@5z`3eWBiL8Bf7Io3aBGp-S9ZM
zI#y@hFm-%_$aE(?-0mc%o6&`P+D?({TpjD>bBF6!DP?Tx)7!BcX{p%V*mKeSOM}9+
z@OE0TD77X%h^3`A2f?anYEec|7){+w1Y={Vk(GnpY0Z;t2Z
z~^eKSe(Zt4p>iwyYdq4Gr#u73gN}P#Kr#sljzX9