Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions examples/aws-group/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const handler = async () => {
return {
statusCode: 200,
body: "hello from group example",
};
};
7 changes: 7 additions & 0 deletions examples/aws-group/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "aws-group",
"version": "0.0.0",
"dependencies": {
"sst": "^4"
}
}
35 changes: 35 additions & 0 deletions examples/aws-group/sst.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/// <reference path="./.sst/platform/config.d.ts" />

/**
* ## Group
*
* Use `sst.x.Group` to target or exclude a set of components together.
*
* ```bash
* sst deploy --target Api
* ```
*/
export default $config({
app(input) {
return {
name: "aws-group",
home: "aws",
removal: input?.stage === "production" ? "retain" : "remove",
};
},
async run() {
const bucket = new sst.aws.Bucket("MyBucket");
const fn = new sst.aws.Function("MyFunction", {
handler: "index.handler",
url: true,
});

const api = new sst.x.Group("Api");
api.add(bucket, fn);

return {
url: fn.url,
bucket: bucket.name,
};
},
});
1 change: 1 addition & 0 deletions examples/aws-group/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
96 changes: 94 additions & 2 deletions pkg/project/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,14 @@ func (p *Project) RunNext(ctx context.Context, input *StackInput) error {
if index == -1 {
return util.NewReadableError(nil, fmt.Sprintf("Target not found: %v", item))
}
args = append(args, "--target", string(completed.Resources[index].URN))
resource := completed.Resources[index]
if resource.Type == "sst:sst:Group" {
for _, memberURN := range resolveGroupMembers(resource, completed.Resources) {
args = append(args, "--target", memberURN)
}
} else {
args = append(args, "--target", string(resource.URN))
}
}
if len(input.Target) > 0 {
args = append(args, "--target-dependents")
Expand All @@ -371,7 +378,14 @@ func (p *Project) RunNext(ctx context.Context, input *StackInput) error {
if index == -1 {
return util.NewReadableError(nil, fmt.Sprintf("Exclude target not found: %v", item))
}
args = append(args, "--exclude", string(completed.Resources[index].URN))
resource := completed.Resources[index]
if resource.Type == "sst:sst:Group" {
for _, memberURN := range resolveGroupMembers(resource, completed.Resources) {
args = append(args, "--exclude", memberURN)
}
} else {
args = append(args, "--exclude", string(resource.URN))
}
}
if len(input.Exclude) > 0 {
args = append(args, "--exclude-dependents")
Expand Down Expand Up @@ -668,3 +682,81 @@ loop:
}
return nil
}

func resolveGroupMembers(resource apitype.ResourceV3, allResources []apitype.ResourceV3) []string {
outputs, ok := parsePlaintext(resource.Outputs).(map[string]interface{})
if !ok {
return nil
}
group, ok := outputs["_group"].(map[string]interface{})
if !ok {
return nil
}
members, ok := group["members"].([]interface{})
if !ok {
return nil
}

// Collect direct member URNs
var directMembers []string
memberSet := make(map[string]bool)
for _, m := range members {
if urn, ok := m.(string); ok {
directMembers = append(directMembers, urn)
memberSet[urn] = true
}
}

// For each direct member, find linked dependencies by walking the
// children's dependency graph back up to top-level SST components
for _, urn := range directMembers {
for _, desc := range findDescendants(urn, allResources) {
for _, dep := range desc.Dependencies {
if parent := findTopLevelSSTParent(string(dep), allResources); parent != "" {
memberSet[parent] = true
}
}
for _, deps := range desc.PropertyDependencies {
for _, dep := range deps {
if parent := findTopLevelSSTParent(string(dep), allResources); parent != "" {
memberSet[parent] = true
}
}
}
}
}

result := make([]string, 0, len(memberSet))
for urn := range memberSet {
result = append(result, urn)
}
return result
}

func findDescendants(parentURN string, allResources []apitype.ResourceV3) []apitype.ResourceV3 {
var result []apitype.ResourceV3
for _, res := range allResources {
if string(res.Parent) == parentURN {
result = append(result, res)
result = append(result, findDescendants(string(res.URN), allResources)...)
}
}
return result
}

func findTopLevelSSTParent(urn string, allResources []apitype.ResourceV3) string {
idx := slices.IndexFunc(allResources, func(r apitype.ResourceV3) bool {
return string(r.URN) == urn
})
if idx == -1 {
return ""
}
res := allResources[idx]
if res.Parent == "" || res.Parent.Type() == "pulumi:pulumi:Stack" {
if strings.HasPrefix(string(res.Type), "sst:") {
return string(res.URN)
}
return ""
}
return findTopLevelSSTParent(string(res.Parent), allResources)
}
50 changes: 50 additions & 0 deletions platform/src/components/experimental/group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ComponentResourceOptions, all } from "@pulumi/pulumi";
import { Component } from "../component";

/**
* The `Group` component lets you define a named set of components that can be
* targeted together using `sst deploy --target` or excluded with `--exclude`.
*
* @example
*
* ```ts
* const bucket = new sst.aws.Bucket("MyBucket");
* const fn = new sst.aws.Function("MyFn", { handler: "src/handler.ts" });
*
* const api = new sst.x.Group("Api");
* api.add(bucket, fn);
* ```
*
* Now you can deploy just the `Api` group:
* ```bash
* sst deploy --target Api
* ```
*/
export class Group extends Component {
private members: Component[] = [];

constructor(name: string, opts?: ComponentResourceOptions) {
super("sst:sst:Group", name, {}, opts);
}

/**
* Add components to this group.
*
* @example
* ```ts
* const bucket = new sst.aws.Bucket("MyBucket");
* const fn = new sst.aws.Function("MyFn", { handler: "src/handler.ts" });
*
* const api = new sst.x.Group("Api");
* api.add(bucket, fn);
* ```
*/
add(...members: Component[]) {
this.members.push(...members);
this.registerOutputs({
_group: {
members: all(this.members.map((m) => m.urn)),
},
});
}
}
1 change: 1 addition & 0 deletions platform/src/components/experimental/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./dev-command.js";
export * from "./group.js";