diff --git a/cmd/v10/main.go b/cmd/v10/main.go index 7bda0e649..c882eb413 100644 --- a/cmd/v10/main.go +++ b/cmd/v10/main.go @@ -2,21 +2,95 @@ package main import ( "fmt" - "log" "log/slog" + "os" - "github.com/sst/v10/pkg/js" "github.com/sst/v10/pkg/project" + "github.com/sst/v10/pkg/stack" + cli "github.com/urfave/cli/v2" ) func main() { - p, err := project.New() + + app := &cli.App{ + Name: "v10", + Usage: "wtf is this", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "verbose", + }, + }, + Before: func(c *cli.Context) error { + level := slog.LevelWarn + if c.Bool("verbose") { + level = slog.LevelInfo + } + slog.SetDefault( + slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: level, + })), + ) + return nil + }, + Commands: []*cli.Command{ + { + Name: "deploy", + Usage: "Deploy", + Flags: []cli.Flag{}, + Action: func(cli *cli.Context) error { + p, err := initProject() + if err != nil { + return err + } + stack.Deploy(p) + return nil + }, + }, + { + Name: "remove", + Usage: "Remove", + Flags: []cli.Flag{}, + Action: func(cli *cli.Context) error { + p, err := initProject() + if err != nil { + return err + } + stack.Remove(p) + return nil + }, + }, + { + Name: "cancel", + Usage: "Cancel", + Flags: []cli.Flag{}, + Action: func(cli *cli.Context) error { + p, err := initProject() + if err != nil { + return err + } + stack.Cancel(p) + return nil + }, + }, + }, + } + + err := app.Run(os.Args) if err != nil { panic(err) } +} + +func initProject() (*project.Project, error) { + slog.Info("initializing project") + p, err := project.New() + if err != nil { + return nil, err + } + if p.Stage() == "" { - p.StagePersonalLoad() + p.LoadPersonalStage() if p.Stage() == "" { for { var stage string @@ -28,45 +102,22 @@ func main() { if stage == "" { continue } - p.StagePersonalSet(stage) + p.SetPersonalStage(stage) break } } } - slog.Info("using", "stage", p.Stage()) - output, err := js.Eval(p.PathTemp(), fmt.Sprintf(` - console.log("here") - import mod from '%s'; - import * as aws from "@pulumi/aws"; - globalThis.aws = aws - console.log("here") - import { LocalWorkspace } from "@pulumi/pulumi/automation/index.js"; - const cfg = mod.config() - console.log("workspace", LocalWorkspace) - const stack = await LocalWorkspace.createOrSelectStack({ - program: mod.run, - projectName: "%s", - stackName: "%s", - }) - - await stack.destroy({ - onOutput: console.log, - }) - - await stack.up({ - onOutput: console.log, - }) - `, - p.PathConfig(), - p.Name(), - p.Stage(), - )) - log.Println(string(output)) - + _, err = p.GetAwsCredentials() if err != nil { - panic(err) + return nil, err + } + + missingDeps := p.CheckDeps() + if len(missingDeps) > 0 { + p.InstallDeps(missingDeps) } + return p, nil } diff --git a/examples/test/sst.config.ts b/examples/test/sst.config.ts index 081353ffb..d39d2dbf2 100644 --- a/examples/test/sst.config.ts +++ b/examples/test/sst.config.ts @@ -7,7 +7,7 @@ export default { profile: "sst-dev", }; }, - run() { - const bucket = new aws.s3.Bucket("my-bucket"); + async run() { + const a = new aws.s3.Bucket("my-bucket", {}); }, }; diff --git a/go.mod b/go.mod index 1e0dcbb12..c9252a92c 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,26 @@ module github.com/sst/v10 go 1.21.3 -require github.com/evanw/esbuild v0.19.5 +require ( + github.com/aws/aws-sdk-go-v2 v1.22.2 + github.com/aws/aws-sdk-go-v2/config v1.25.0 + github.com/evanw/esbuild v0.19.5 + github.com/urfave/cli/v2 v2.25.7 +) -require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect +require ( + github.com/aws/aws-sdk-go-v2/credentials v1.16.0 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.7.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.17.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.19.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.25.1 // indirect + github.com/aws/smithy-go v1.16.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect +) diff --git a/go.sum b/go.sum index 460cc8663..5b7ad0dcf 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,38 @@ +github.com/aws/aws-sdk-go-v2 v1.22.2 h1:lV0U8fnhAnPz8YcdmZVV60+tr6CakHzqA6P8T46ExJI= +github.com/aws/aws-sdk-go-v2 v1.22.2/go.mod h1:Kd0OJtkW3Q0M0lUWGszapWjEvrXDzRW+D21JNsroB+c= +github.com/aws/aws-sdk-go-v2/config v1.25.0 h1:WCwAqyrM/kqYi6pHjVpq/w2pLydeGKv8Af9vdtO3ciM= +github.com/aws/aws-sdk-go-v2/config v1.25.0/go.mod h1:1QMnmhoWcR6957nC1MUUhhOLx9NOGFSVNG3Mag9vLU4= +github.com/aws/aws-sdk-go-v2/credentials v1.16.0 h1:sSEHkXonpZBSPcyUBDRlZjxOi14qM/UK7/vfKhGwmTo= +github.com/aws/aws-sdk-go-v2/credentials v1.16.0/go.mod h1:tXM8wmaeAhfC7nZoCxb0FzM/aRaB1m1WQ7x0qlBLq80= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.3 h1:G5KawTAkyHH6WyKQCdHiW4h3PmAXNJpOgwKg3H7sDRE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.3/go.mod h1:hugKmSFnZB+HgNI1sYGT14BUPZkO6alC/e0AWu+0IAQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.2 h1:AaQsr5vvGR7rmeSWBtTCcw16tT9r51mWijuCQhzLnq8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.2/go.mod h1:o1IiRn7CWocIFTXJjGKJDOwxv1ibL53NpcvcqGWyRBA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.2 h1:UZx8SXZ0YtzRiALzYAWcjb9Y9hZUR7MBKaBQ5ouOjPs= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.2/go.mod h1:ipuRpcSaklmxR6C39G187TpBAO132gUfleTGccUPs8c= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.0 h1:usgqiJtamuGIBj+OvYmMq89+Z1hIKkMJToz1WpoeNUY= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.0/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.2 h1:h7j73yuAVVjic8pqswh+L/7r2IHP43QwRyOu6zcCDDE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.2/go.mod h1:H07AHdK5LSy8F7EJUQhoxyiCNkePoHj2D8P2yGTWafo= +github.com/aws/aws-sdk-go-v2/service/sso v1.17.1 h1:km+ZNjtLtpXYf42RdaDZnNHm9s7SYAuDGTafy6nd89A= +github.com/aws/aws-sdk-go-v2/service/sso v1.17.1/go.mod h1:aHBr3pvBSD5MbzOvQtYutyPLLRPbl/y9x86XyJJnUXQ= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.19.1 h1:iRFNqZH4a67IqPvK8xxtyQYnyrlsvwmpHOe9r55ggBA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.19.1/go.mod h1:pTy5WM+6sNv2tB24JNKFtn6EvciQ5k40ZJ0pq/Iaxj0= +github.com/aws/aws-sdk-go-v2/service/sts v1.25.1 h1:txgVXIXWPXyqdiVn92BV6a/rgtpX31HYdsOYj0sVQQQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.25.1/go.mod h1:VAiJiNaoP1L89STFlEMgmHX1bKixY+FaP+TpRFrmyZ4= +github.com/aws/smithy-go v1.16.0 h1:gJZEH/Fqh+RsvlJ1Zt4tVAtV6bKkp3cC+R6FCZMNzik= +github.com/aws/smithy-go v1.16.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/evanw/esbuild v0.19.5 h1:9ildZqajUJzDAwNf9MyQsLh2RdDRKTq3kcyyzhE39us= github.com/evanw/esbuild v0.19.5/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/util/fs/fs.go b/internal/fs/fs.go similarity index 100% rename from internal/util/fs/fs.go rename to internal/fs/fs.go diff --git a/pkg/js/js.go b/pkg/js/js.go index 0591b8afd..6958b2866 100644 --- a/pkg/js/js.go +++ b/pkg/js/js.go @@ -1,7 +1,10 @@ package js import ( - "log" + "bufio" + "fmt" + "log/slog" + "math/rand" "os" "os/exec" "path/filepath" @@ -9,8 +12,26 @@ import ( esbuild "github.com/evanw/esbuild/pkg/api" ) -func Eval(tmpDir string, code string) ([]byte, error) { - outfile := filepath.Join(tmpDir, "out.mjs") +type EvalOptions struct { + Dir string + Code string + Env []string +} + +type EvalResult struct { + Out *bufio.Scanner + Err *bufio.Scanner + + cmd *exec.Cmd + file string +} + +func Eval(input EvalOptions) (*EvalResult, error) { + outfile := filepath.Join(input.Dir, + "eval", + fmt.Sprintf("eval-%x.mjs", rand.Int()), + ) + slog.Info("esbuild building") esbuild.Build(esbuild.BuildOptions{ Banner: map[string]string{ "js": ` @@ -22,13 +43,13 @@ func Eval(tmpDir string, code string) ([]byte, error) { }, External: []string{ "@pulumi/pulumi", - // "@pulumi/aws", + "@pulumi/aws", }, Format: esbuild.FormatESModule, Platform: esbuild.PlatformNode, Stdin: &esbuild.StdinOptions{ - Contents: code, - ResolveDir: tmpDir, + Contents: input.Code, + ResolveDir: input.Dir, Sourcefile: "eval.ts", Loader: esbuild.LoaderTS, }, @@ -36,17 +57,36 @@ func Eval(tmpDir string, code string) ([]byte, error) { Write: true, Bundle: true, }) + slog.Info("esbuild built") cmd := exec.Command("node", outfile) - cmd.Env = append( - os.Environ(), - "PULUMI_CONFIG_PASSPHRASE=", - ) - cmd.Dir = tmpDir - cmd.Stderr = os.Stderr - output, err := cmd.Output() - log.Println(string(output)) + cmd.Env = append(os.Environ(), input.Env...) + + stdout, err := cmd.StdoutPipe() if err != nil { return nil, err } - return output, nil + outScanner := bufio.NewScanner(stdout) + + stderr, err := cmd.StderrPipe() + if err != nil { + return nil, err + } + errScanner := bufio.NewScanner(stderr) + + return &EvalResult{ + Out: outScanner, + Err: errScanner, + cmd: cmd, + file: outfile, + }, nil +} + +func (e *EvalResult) Start() error { + return e.cmd.Start() +} + +func (e *EvalResult) Wait() error { + err := e.cmd.Wait() + os.Remove(e.file) + return err } diff --git a/pkg/project/aws.go b/pkg/project/aws.go new file mode 100644 index 000000000..60e459abc --- /dev/null +++ b/pkg/project/aws.go @@ -0,0 +1,32 @@ +package project + +import ( + "context" + "log/slog" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" +) + +func (p *Project) GetAwsCredentials() (*aws.Credentials, error) { + if p.credentials != nil && !p.credentials.Expired() { + return p.credentials, nil + } + slog.Info("using", "profile", p.Profile()) + ctx := context.Background() + slog.Info("getting aws credentials") + cfg, err := config.LoadDefaultConfig( + ctx, + config.WithSharedConfigProfile(p.Profile()), + ) + if err != nil { + return nil, err + } + credentials, err := cfg.Credentials.Retrieve(ctx) + if err != nil { + return nil, err + } + p.credentials = &credentials + slog.Info("credentials found") + return &credentials, nil +} diff --git a/pkg/project/deps.go b/pkg/project/deps.go new file mode 100644 index 000000000..5f16dc156 --- /dev/null +++ b/pkg/project/deps.go @@ -0,0 +1,50 @@ +package project + +import ( + "encoding/json" + "log/slog" + "os" + "os/exec" +) + +var VERSIONS = map[string]string{ + "@pulumi/pulumi": "3.93.0", + "@pulumi/aws": "v6.8.0", +} + +func (p *Project) CheckDeps() map[string]bool { + result := map[string]bool{} + + for k, v := range VERSIONS { + slog.Info("checking", "dep", k) + path := p.getPath("node_modules", k, "package.json") + data, err := os.ReadFile(path) + if err != nil { + result[k] = true + } + + parsed := struct { + Version string `json:"version"` + }{} + err = json.Unmarshal(data, &parsed) + if err != nil { + result[k] = true + } + + slog.Info("dep", "version", parsed.Version, "wanted", v) + if parsed.Version != v { + result[k] = true + } + } + + return result +} + +func (p *Project) InstallDeps(input map[string]bool) { + for k := range input { + slog.Info("installing", "dep", k) + cmd := exec.Command("npm", "install", "--save", k+"@"+VERSIONS[k]) + cmd.Dir = p.PathTemp() + cmd.Run() + } +} diff --git a/pkg/project/project.go b/pkg/project/project.go index 554e395f0..8444cc509 100644 --- a/pkg/project/project.go +++ b/pkg/project/project.go @@ -5,18 +5,19 @@ import ( "fmt" "os" "path/filepath" - "strings" - "github.com/sst/v10/internal/util/fs" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/sst/v10/internal/fs" "github.com/sst/v10/pkg/js" ) type Project struct { - root string - config string - name string - profile string - stage string + root string + config string + name string + profile string + stage string + credentials *aws.Credentials } func New() (*Project, error) { @@ -49,33 +50,54 @@ func New() (*Project, error) { } } - evaled, err := js.Eval(tmp, fmt.Sprintf(` + eval, err := js.Eval( + js.EvalOptions{ + Dir: tmp, + Code: fmt.Sprintf(` import mod from '%s'; - console.log(JSON.stringify(mod.config())) - `, cfgPath)) + console.log(JSON.stringify(mod.config()))`, + cfgPath), + }, + ) if err != nil { return nil, err } - parsed := struct { - Name string `json:"name"` - Profile string `json:"profile"` - Stage string `json:"stage"` - }{} - err = json.Unmarshal(evaled, &parsed) + eval.Start() + + for eval.Out.Scan() { + line := eval.Out.Bytes() + parsed := struct { + Name string `json:"name"` + Profile string `json:"profile"` + Stage string `json:"stage"` + }{} + err = json.Unmarshal(line, &parsed) + if err != nil { + return nil, err + } + proj.name = parsed.Name + if proj.name == "" { + return nil, fmt.Errorf("Project name is required") + } + proj.profile = parsed.Profile + proj.stage = parsed.Stage + break + } + + err = eval.Wait() if err != nil { return nil, err } - proj.name = parsed.Name - if proj.name == "" { - return nil, fmt.Errorf("Project name is required") - } - proj.profile = parsed.Profile - proj.stage = parsed.Stage return proj, nil } +func (p *Project) getPath(path ...string) string { + paths := append([]string{p.PathTemp()}, path...) + return filepath.Join(paths...) +} + func (p *Project) PathTemp() string { return filepath.Join(p.root, ".sst") } @@ -99,28 +121,3 @@ func (p *Project) Profile() string { func (p *Project) Stage() string { return p.stage } - -func (p *Project) StageSet(input string) { - p.stage = input -} - -func (p *Project) stagePersonalPath() string { - return filepath.Join(p.PathTemp(), "stage") -} - -func (p *Project) StagePersonalLoad() { - data, err := os.ReadFile(p.stagePersonalPath()) - if err != nil { - return - } - p.stage = strings.TrimSpace(string(data)) -} - -func (p *Project) StagePersonalSet(input string) error { - err := os.WriteFile(p.stagePersonalPath(), []byte(strings.TrimSpace(input)), 0644) - if err != nil { - return err - } - p.stage = input - return nil -} diff --git a/pkg/project/stage.go b/pkg/project/stage.go new file mode 100644 index 000000000..17643a926 --- /dev/null +++ b/pkg/project/stage.go @@ -0,0 +1,28 @@ +package project + +import ( + "os" + "path/filepath" + "strings" +) + +func (p *Project) pathPersonalStage() string { + return filepath.Join(p.PathTemp(), "stage") +} + +func (p *Project) LoadPersonalStage() { + data, err := os.ReadFile(p.pathPersonalStage()) + if err != nil { + return + } + p.stage = strings.TrimSpace(string(data)) +} + +func (p *Project) SetPersonalStage(input string) error { + err := os.WriteFile(p.pathPersonalStage(), []byte(strings.TrimSpace(input)), 0644) + if err != nil { + return err + } + p.stage = input + return nil +} diff --git a/pkg/stack/stack.go b/pkg/stack/stack.go new file mode 100644 index 000000000..f72d2f786 --- /dev/null +++ b/pkg/stack/stack.go @@ -0,0 +1,144 @@ +package stack + +import ( + "fmt" + "strings" + + "github.com/sst/v10/pkg/js" + "github.com/sst/v10/pkg/project" +) + +func runtime(project *project.Project) string { + return fmt.Sprintf(` + import * as aws from "@pulumi/aws"; + globalThis.aws = aws + import { LocalWorkspace } from "@pulumi/pulumi/automation/index.js"; + + import mod from '%s'; + const stack = await LocalWorkspace.createOrSelectStack({ + program: mod.run, + projectName: "%s", + stackName: "%s", + }) + `, project.PathConfig(), project.Name(), project.Stage(), + ) +} + +func env(project *project.Project) ([]string, error) { + credentials, err := project.GetAwsCredentials() + if err != nil { + return nil, err + } + return []string{ + "PULUMI_CONFIG_PASSPHRASE=", + "AWS_ACCESS_KEY_ID=" + credentials.AccessKeyID, + "AWS_SECRET_ACCESS_KEY=" + credentials.SecretAccessKey, + "AWS_SESSION_TOKEN=" + credentials.SessionToken, + }, nil +} + +func Deploy(project *project.Project) error { + env, err := env(project) + if err != nil { + return err + } + cmd, err := js.Eval(js.EvalOptions{ + Dir: project.PathTemp(), + Code: fmt.Sprintf(` + %v + await stack.up({ + onOutput: console.log, + }) + `, runtime(project)), + Env: env, + }) + if err != nil { + return err + } + err = cmd.Start() + if err != nil { + return err + } + + for cmd.Out.Scan() { + line := strings.TrimSpace(cmd.Out.Text()) + if line == "" { + continue + } + + fmt.Println(line) + } + + cmd.Wait() + + return nil +} + +func Remove(project *project.Project) error { + env, err := env(project) + if err != nil { + return err + } + cmd, err := js.Eval(js.EvalOptions{ + Dir: project.PathTemp(), + Code: fmt.Sprintf(` + %v + await stack.destroy({ + onOutput: console.log, + }) + `, runtime(project)), + Env: env, + }) + if err != nil { + return err + } + err = cmd.Start() + if err != nil { + return err + } + + for cmd.Out.Scan() { + line := strings.TrimSpace(cmd.Out.Text()) + if line == "" { + continue + } + + fmt.Println(line) + } + + cmd.Wait() + + return nil +} + +func Cancel(project *project.Project) error { + env, err := env(project) + if err != nil { + return err + } + cmd, err := js.Eval(js.EvalOptions{ + Dir: project.PathTemp(), + Code: fmt.Sprintf(` + %v + await stack.cancel({ + onOutput: console.log, + }) + `, runtime(project)), + Env: env, + }) + if err != nil { + return err + } + err = cmd.Start() + if err != nil { + return err + } + + for cmd.Out.Scan() { + fmt.Println(cmd.Out.Text()) + } + + cmd.Wait() + + return nil +}