Skip to content
Open
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
75 changes: 75 additions & 0 deletions cmd/sst/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"

Expand All @@ -12,6 +13,7 @@ import (
"github.com/sst/sst/v3/internal/util"
"github.com/sst/sst/v3/pkg/id"
"github.com/sst/sst/v3/pkg/process"
"github.com/sst/sst/v3/pkg/project"
"github.com/sst/sst/v3/pkg/project/provider"
"github.com/sst/sst/v3/pkg/state"
)
Expand Down Expand Up @@ -149,6 +151,79 @@ var CmdState = &cli.Command{
return encoder.Encode(exported)
},
},
{
Name: "graph",
Flags: []cli.Flag{
{
Name: "file",
Type: "string",
Description: cli.Description{
Short: "Path to write the graph file",
Long: "Path to write the output DOT file. Accepts an absolute path or a path relative to the current working directory. Defaults to `<app>-<stage>.dot` in the project root.",
},
},
},
Description: cli.Description{
Short: "Generate a graph of your app's resource dependencies",
Long: strings.Join([]string{
"Generates a dependency graph of your app's deployed resources.",
"",
"This exports the resource dependency graph from your most recent deployment",
"as a [DOT format](https://graphviz.org/doc/info/lang.html) file, which can",
"be visualized with tools like [Graphviz](https://graphviz.org/).",
"",
"By default the file is written to `<app>-<stage>.dot` in your project root",
"(the directory containing `sst.config.ts`).",
"",
"```bash frame=\"none\"",
"sst state graph",
"```",
"",
"You can specify a custom output path with `--file`.",
"",
"```bash frame=\"none\"",
"sst state graph --file ./my-graph.dot",
"```",
"",
"You can also run this for a specific stage.",
"",
"```bash frame=\"none\"",
"sst state graph --stage production",
"```",
"",
"By default, it runs on your personal stage.",
}, "\n"),
},
Run: func(c *cli.Cli) error {
p, err := c.InitProject()
if err != nil {
return err
}
defer p.Cleanup()

outputFile := c.String("file")
if outputFile == "" {
outputFile = filepath.Join(p.PathRoot(), p.App().Name+"-"+p.App().Stage+".dot")
} else if !filepath.IsAbs(outputFile) {
cwd, err := os.Getwd()
if err != nil {
return util.NewReadableError(err, "Could not determine working directory")
}
outputFile = filepath.Join(cwd, outputFile)
}

err = p.Run(c.Context, &project.StackInput{
Command: "graph",
OutputFile: outputFile,
})
if err != nil {
return util.NewReadableError(err, "Could not generate graph")
}

ui.Success("Graph written to " + outputFile)
return nil
},
},
{
Name: "list",
Description: cli.Description{
Expand Down
19 changes: 12 additions & 7 deletions pkg/project/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func (p *Project) RunNext(ctx context.Context, input *StackInput) error {
ID: id.Descending(),
}
var err error
if input.Command != "diff" {
if input.Command != "diff" && input.Command != "graph" {
update, err = p.Lock(input.Command)
if err != nil {
if err == provider.ErrLockExists {
Expand Down Expand Up @@ -292,7 +292,10 @@ func (p *Project) RunNext(ctx context.Context, input *StackInput) error {
args := []string{
"--stack", fmt.Sprintf("organization/%v/%v", p.app.Name, p.app.Stage),
"--non-interactive",
"--event-log", eventlogPath,
}
// Graph command does not support event log
if input.Command != "graph" {
args = append(args, "--event-log", eventlogPath)
}

if input.Command == "deploy" {
Expand Down Expand Up @@ -338,9 +341,11 @@ func (p *Project) RunNext(ctx context.Context, input *StackInput) error {
args = append([]string{"up", "--yes", "-f"}, args...)
case "remove":
args = append([]string{"destroy", "--yes", "-f"}, args...)
case "graph":
args = append([]string{"stack", "graph", input.OutputFile}, args...)
}

if (input.Command == "diff" || input.Command == "deploy") && input.PolicyPath != "" {
if (input.Command == "diff" || input.Command == "deploy" || input.Command == "graph") && input.PolicyPath != "" {
policyPath, err := p.ResolvePolicyPackPath(input.PolicyPath)
if err != nil {
return util.NewReadableError(nil, err.Error())
Expand Down Expand Up @@ -398,7 +403,7 @@ func (p *Project) RunNext(ctx context.Context, input *StackInput) error {
defer partialCancel()
partialDone := make(chan error)
go func() {
if input.Command == "diff" {
if input.Command == "diff" || input.Command == "graph" {
return
}
for {
Expand Down Expand Up @@ -579,7 +584,7 @@ loop:
}
}

if input.Command != "diff" && (event.ResOutputsEvent != nil || event.CancelEvent != nil || event.SummaryEvent != nil) {
if input.Command != "diff" && input.Command != "graph" && (event.ResOutputsEvent != nil || event.CancelEvent != nil || event.SummaryEvent != nil) {
partial <- 1
}

Expand Down Expand Up @@ -645,7 +650,7 @@ loop:
types.Generate(p.PathConfig(), complete.Links)
defer bus.Publish(complete)

if input.Command != "diff" {
if input.Command != "diff" && input.Command != "graph" {
log.Info("canceling partial")
partialCancel()
log.Info("waiting for partial to exit")
Expand All @@ -662,7 +667,7 @@ loop:
defer outputsFile.Close()
json.NewEncoder(outputsFile).Encode(complete.Outputs)

if input.Command != "diff " {
if input.Command != "diff " && input.Command != "graph" {
update.TimeCompleted = time.Now().Format(time.RFC3339)
for _, err := range errors {
update.Errors = append(update.Errors, provider.SummaryError{
Expand Down
1 change: 1 addition & 0 deletions pkg/project/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type StackInput struct {
Continue bool
SkipHash string
PolicyPath string
OutputFile string
}

type ConcurrentUpdateEvent struct{}
Expand Down
Loading