From 6f2462699c586af8b76aa16cbe388f27e0449f7e Mon Sep 17 00:00:00 2001 From: Brad Sickles Date: Thu, 11 Jul 2024 09:08:22 -0400 Subject: [PATCH] Adjusted CLI commands to work with new version of the Nullstone engine. (#195) --- CHANGELOG.md | 3 ++ app_urls/base_url.go | 20 ++++++++ app_urls/intent_workflow.go | 14 ++++++ app_urls/run.go | 14 ++++++ app_urls/workspace_workflow.go | 18 +++++++ aws/beanstalk/remoter.go | 2 +- aws/ec2/remoter.go | 2 +- aws/ecs/remoter.go | 2 +- aws/ecs/statuser.go | 2 +- cmd/apply.go | 22 +++------ cmd/create_deploy.go | 9 ++-- cmd/deploy.go | 53 +++++++++++++++++--- cmd/envs.go | 63 +++--------------------- cmd/launch.go | 19 ++++--- cmd/perform_env_run.go | 90 ++++++++++++++++++++++++++++++++++ cmd/perform_run.go | 61 +++++++++++++++++++++++ cmd/plan.go | 27 +++------- cmd/up.go | 21 +++----- cmd/wait.go | 3 +- cmd/wait_for.go | 64 ++++++++++++++++++++++++ gcp/gke/remoter.go | 2 +- go.mod | 5 +- go.sum | 10 ++-- runs/app_url.go | 25 ---------- runs/create.go | 22 --------- runs/stream_logs.go | 3 +- runs/wait_for_terminal_run.go | 3 +- 27 files changed, 398 insertions(+), 181 deletions(-) create mode 100644 app_urls/base_url.go create mode 100644 app_urls/intent_workflow.go create mode 100644 app_urls/run.go create mode 100644 app_urls/workspace_workflow.go create mode 100644 cmd/perform_env_run.go create mode 100644 cmd/perform_run.go create mode 100644 cmd/wait_for.go delete mode 100644 runs/app_url.go delete mode 100644 runs/create.go diff --git a/CHANGELOG.md b/CHANGELOG.md index d67cddd..a8bb3a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 0.0.121 (Jul 11, 2024) +* Updated CLI commands (`launch`, `deploy`, `plan`, `apply`, `up`, `envs up`, `envs down`) to interop with workflows. + # 0.0.120 (Apr 09, 2024) * Added `ingress` as an option for category when generating a new module. diff --git a/app_urls/base_url.go b/app_urls/base_url.go new file mode 100644 index 0000000..b8840e9 --- /dev/null +++ b/app_urls/base_url.go @@ -0,0 +1,20 @@ +package app_urls + +import ( + "gopkg.in/nullstone-io/go-api-client.v0" + "net/url" + "strings" +) + +func GetBaseUrl(cfg api.Config) *url.URL { + u, err := url.Parse(cfg.BaseAddress) + if err != nil { + u = &url.URL{Scheme: "https", Host: "app.nullstone.io"} + } + u.Host = strings.Replace(u.Host, "api", "app", 1) + if u.Host == "localhost:8443" { + u.Scheme = "http" + u.Host = "localhost:8090" + } + return u +} diff --git a/app_urls/intent_workflow.go b/app_urls/intent_workflow.go new file mode 100644 index 0000000..cd5604c --- /dev/null +++ b/app_urls/intent_workflow.go @@ -0,0 +1,14 @@ +package app_urls + +import ( + "fmt" + "gopkg.in/nullstone-io/go-api-client.v0" + "gopkg.in/nullstone-io/go-api-client.v0/types" +) + +func GetIntentWorkflow(cfg api.Config, iw types.IntentWorkflow) string { + u := GetBaseUrl(cfg) + u.Path = fmt.Sprintf("orgs/%s/stacks/%d/envs/%d/activity/workflows/%d", + iw.OrgName, iw.StackId, iw.EnvId, iw.Id) + return u.String() +} diff --git a/app_urls/run.go b/app_urls/run.go new file mode 100644 index 0000000..8599680 --- /dev/null +++ b/app_urls/run.go @@ -0,0 +1,14 @@ +package app_urls + +import ( + "fmt" + "gopkg.in/nullstone-io/go-api-client.v0" + "gopkg.in/nullstone-io/go-api-client.v0/types" +) + +func GetRun(cfg api.Config, workspace types.Workspace, run types.Run) string { + u := GetBaseUrl(cfg) + u.Path = fmt.Sprintf("orgs/%s/stacks/%d/envs/%d/blocks/%d/activity/runs/%s", + workspace.OrgName, workspace.StackId, workspace.EnvId, workspace.BlockId, run.Uid) + return u.String() +} diff --git a/app_urls/workspace_workflow.go b/app_urls/workspace_workflow.go new file mode 100644 index 0000000..f516e71 --- /dev/null +++ b/app_urls/workspace_workflow.go @@ -0,0 +1,18 @@ +package app_urls + +import ( + "fmt" + "gopkg.in/nullstone-io/go-api-client.v0" + "gopkg.in/nullstone-io/go-api-client.v0/types" +) + +func GetWorkspaceWorkflow(cfg api.Config, ww types.WorkspaceWorkflow, isApp bool) string { + u := GetBaseUrl(cfg) + blockType := "blocks" + if isApp { + blockType = "apps" + } + u.Path = fmt.Sprintf("orgs/%s/stacks/%d/envs/%d/%s/%d/activity/workflows/%d", + ww.OrgName, ww.StackId, ww.EnvId, blockType, ww.BlockId, ww.Id) + return u.String() +} diff --git a/aws/beanstalk/remoter.go b/aws/beanstalk/remoter.go index 1c593e5..b3b09a3 100644 --- a/aws/beanstalk/remoter.go +++ b/aws/beanstalk/remoter.go @@ -11,7 +11,7 @@ import ( ) func NewRemoter(ctx context.Context, osWriters logging.OsWriters, source outputs.RetrieverSource, appDetails app.Details) (admin.Remoter, error) { - outs, err := outputs.Retrieve[Outputs](ctx, source, appDetails.Workspace) + outs, err := outputs.Retrieve[Outputs](ctx, source, appDetails.Workspace, appDetails.WorkspaceConfig) if err != nil { return nil, err } diff --git a/aws/ec2/remoter.go b/aws/ec2/remoter.go index 7d72132..2c25403 100644 --- a/aws/ec2/remoter.go +++ b/aws/ec2/remoter.go @@ -10,7 +10,7 @@ import ( ) func NewRemoter(ctx context.Context, osWriters logging.OsWriters, source outputs.RetrieverSource, appDetails app.Details) (admin.Remoter, error) { - outs, err := outputs.Retrieve[Outputs](ctx, source, appDetails.Workspace) + outs, err := outputs.Retrieve[Outputs](ctx, source, appDetails.Workspace, appDetails.WorkspaceConfig) if err != nil { return nil, err } diff --git a/aws/ecs/remoter.go b/aws/ecs/remoter.go index d991e72..dfad296 100644 --- a/aws/ecs/remoter.go +++ b/aws/ecs/remoter.go @@ -10,7 +10,7 @@ import ( ) func NewRemoter(ctx context.Context, osWriters logging.OsWriters, source outputs.RetrieverSource, appDetails app.Details) (admin.Remoter, error) { - outs, err := outputs.Retrieve[Outputs](ctx, source, appDetails.Workspace) + outs, err := outputs.Retrieve[Outputs](ctx, source, appDetails.Workspace, appDetails.WorkspaceConfig) if err != nil { return nil, err } diff --git a/aws/ecs/statuser.go b/aws/ecs/statuser.go index e16a2f5..8c11022 100644 --- a/aws/ecs/statuser.go +++ b/aws/ecs/statuser.go @@ -11,7 +11,7 @@ import ( ) func NewStatuser(ctx context.Context, osWriters logging.OsWriters, source outputs.RetrieverSource, appDetails app.Details) (admin.Statuser, error) { - outs, err := outputs.Retrieve[Outputs](ctx, source, appDetails.Workspace) + outs, err := outputs.Retrieve[Outputs](ctx, source, appDetails.Workspace, appDetails.WorkspaceConfig) if err != nil { return nil, err } diff --git a/cmd/apply.go b/cmd/apply.go index fd343fc..e052972 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -2,12 +2,10 @@ package cmd import ( "context" - "fmt" "github.com/urfave/cli/v2" "gopkg.in/nullstone-io/go-api-client.v0" "gopkg.in/nullstone-io/go-api-client.v0/types" "gopkg.in/nullstone-io/nullstone.v0/runs" - "os" ) var Apply = func() *cli.Command { @@ -66,19 +64,15 @@ var Apply = func() *cli.Command { return err } - newRun, err := runs.Create(ctx, cfg, workspace, autoApprove, false) - if err != nil { - return fmt.Errorf("error creating run: %w", err) - } else if newRun == nil { - return fmt.Errorf("unable to create run") - } - fmt.Fprintf(os.Stdout, "created apply run %q\n", newRun.Uid) - fmt.Fprintln(os.Stdout, runs.GetBrowserUrl(cfg, workspace, *newRun)) - - if c.IsSet("wait") { - return runs.StreamLogs(ctx, cfg, workspace, newRun) + input := PerformRunInput{ + Workspace: workspace, + CommitSha: "", + IsApproved: autoApprove, + IsDestroy: false, + BlockType: types.BlockType(block.Type), + StreamLogs: c.IsSet("wait"), } - return nil + return PerformRun(ctx, cfg, input) }) }, } diff --git a/cmd/create_deploy.go b/cmd/create_deploy.go index b09e45a..0aa53a4 100644 --- a/cmd/create_deploy.go +++ b/cmd/create_deploy.go @@ -5,10 +5,9 @@ import ( "fmt" "github.com/nullstone-io/deployment-sdk/app" "gopkg.in/nullstone-io/go-api-client.v0" - "gopkg.in/nullstone-io/go-api-client.v0/types" ) -func CreateDeploy(nsConfig api.Config, appDetails app.Details, commitSha, version string) (*types.Deploy, error) { +func CreateDeploy(nsConfig api.Config, appDetails app.Details, commitSha, version string) (*api.DeployCreateResult, error) { ctx := context.TODO() client := api.Client{Config: nsConfig} payload := api.DeployCreatePayload{ @@ -16,11 +15,11 @@ func CreateDeploy(nsConfig api.Config, appDetails app.Details, commitSha, versio Version: version, CommitSha: commitSha, } - newDeploy, err := client.Deploys().Create(ctx, appDetails.App.StackId, appDetails.App.Id, appDetails.Env.Id, payload) + result, err := client.Deploys().Create(ctx, appDetails.App.StackId, appDetails.App.Id, appDetails.Env.Id, payload) if err != nil { return nil, fmt.Errorf("error creating deploy: %w", err) - } else if newDeploy == nil { + } else if result == nil { return nil, fmt.Errorf("unable to create deploy") } - return newDeploy, nil + return result, nil } diff --git a/cmd/deploy.go b/cmd/deploy.go index 383cb12..4210515 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -52,13 +52,18 @@ var Deploy = func(providers app.Providers) *cli.Command { } fmt.Fprintln(osWriters.Stderr(), "Creating deploy...") - deploy, err := CreateDeploy(cfg, appDetails, commitSha, version) + result, err := CreateDeploy(cfg, appDetails, commitSha, version) if err != nil { return err } - fmt.Fprintln(osWriters.Stderr()) - return streamDeployLogs(ctx, osWriters, cfg, *deploy, wait) + + if result.Deploy != nil { + return streamDeployLogs(ctx, osWriters, cfg, *result.Deploy, wait) + } else if result.IntentWorkflow != nil { + return streamDeployIntentLogs(ctx, osWriters, cfg, appDetails, *result.IntentWorkflow, wait) + } + return nil }) }, } @@ -93,8 +98,10 @@ func getCurrentVersion(ctx context.Context, pusher app.Pusher) (string, string, } func streamDeployLogs(ctx context.Context, osWriters logging.OsWriters, cfg api.Config, deploy types.Deploy, wait bool) error { - fmt.Fprintln(osWriters.Stderr(), "Waiting for deploy logs...") + stdout, stderr := osWriters.Stdout(), osWriters.Stderr() client := api.Client{Config: cfg} + + fmt.Fprintln(stderr, "Waiting for deploy logs...") msgs, err := client.DeployLogs().Watch(ctx, deploy.StackId, deploy.Id, ws.RetryInfinite(2*time.Second)) if err != nil { return fmt.Errorf("error connecting to deploy logs: %w", err) @@ -107,14 +114,14 @@ func streamDeployLogs(ctx context.Context, osWriters logging.OsWriters, cfg api. // Stop streaming logs if we receive a log message from wait-healthy and no --wait break } - fmt.Fprint(osWriters.Stderr(), msg.Content) + fmt.Fprint(stderr, msg.Content) } updated, err := client.Deploys().Get(ctx, deploy.StackId, deploy.AppId, deploy.EnvId, deploy.Id) if err != nil { return fmt.Errorf("error retrieving deploy status: %w", err) } - fmt.Fprintln(osWriters.Stdout(), updated.Reference) + fmt.Fprintln(stdout, updated.Reference) switch updated.Status { case types.DeployStatusCancelled: return fmt.Errorf("Deploy was cancelled.") @@ -123,3 +130,37 @@ func streamDeployLogs(ctx context.Context, osWriters logging.OsWriters, cfg api. } return nil } + +func streamDeployIntentLogs(ctx context.Context, osWriters logging.OsWriters, cfg api.Config, appDetails app.Details, iw types.IntentWorkflow, wait bool) error { + _, stderr := osWriters.Stdout(), osWriters.Stderr() + client := api.Client{Config: cfg} + + fmt.Fprintln(stderr, "Starting deployment...") + var err error + if iw, err = waitForRunningIntentWorkflow(ctx, cfg, iw); err != nil { + return err + } else if iw.Status == types.IntentWorkflowStatusCompleted { + fmt.Fprintln(stderr, "Deployment completed.") + return nil + } + + var wflow types.WorkspaceWorkflow + for _, ww := range iw.WorkspaceWorkflows { + if ww.BlockId == appDetails.App.Id && ww.EnvId == appDetails.Env.Id && ww.StackId == appDetails.App.StackId { + wflow = ww + break + } + } + if wflow.Id == 0 { + return fmt.Errorf("deployment workflow is missing") + } + + activities, err := client.WorkspaceWorkflows().GetActivities(ctx, wflow.StackId, wflow.BlockId, wflow.EnvId, wflow.Id) + if err != nil { + return fmt.Errorf("unable to find deployment: %w", err) + } else if activities == nil || activities.Deploy == nil { + return fmt.Errorf("deployment is missing") + } + + return streamDeployLogs(ctx, osWriters, cfg, *activities.Deploy, wait) +} diff --git a/cmd/envs.go b/cmd/envs.go index 5952e5b..9060985 100644 --- a/cmd/envs.go +++ b/cmd/envs.go @@ -9,7 +9,6 @@ import ( "gopkg.in/nullstone-io/go-api-client.v0" "gopkg.in/nullstone-io/go-api-client.v0/find" "gopkg.in/nullstone-io/go-api-client.v0/types" - "gopkg.in/nullstone-io/nullstone.v0/runs" "math" "os" "regexp" @@ -372,11 +371,6 @@ func createEnvRun(c *cli.Context, cfg api.Config, isDestroy bool) error { stackName := c.String("stack") envName := c.String("env") - action := "launch" - if isDestroy { - action = "destroy" - } - stack, err := client.StacksByName().Get(ctx, stackName) if err != nil { return fmt.Errorf("error looking for stack %q: %w", stackName, err) @@ -391,56 +385,11 @@ func createEnvRun(c *cli.Context, cfg api.Config, isDestroy bool) error { return fmt.Errorf("environment %q does not exist in stack %d", envName, stack.Id) } - body := types.CreateEnvRunInput{IsDestroy: isDestroy} - newRuns, err := client.EnvRuns().Create(ctx, stack.Id, env.Id, body) - if err != nil { - return fmt.Errorf("error creating run: %w", err) - } - - if len(newRuns) <= 0 { - fmt.Fprintf(os.Stdout, "no runs created to %s the %q environment\n", action, envName) - return nil - } - - workspaces, err := client.Workspaces().List(ctx, stack.Id) - if err != nil { - return fmt.Errorf("error retrieving list of workspaces: %w", err) - } - blocks, err := client.Blocks().List(ctx, stack.Id) - if err != nil { - return fmt.Errorf("error retrieving list of blocks: %w", err) + input := PerformEnvRunInput{ + CommitSha: "", + Stack: *stack, + Env: *env, + IsDestroy: isDestroy, } - - findWorkspace := func(run types.Run) *types.Workspace { - for _, workspace := range workspaces { - if workspace.Uid == run.WorkspaceUid { - return &workspace - } - } - return nil - } - findBlock := func(workspace *types.Workspace) *types.Block { - if workspace == nil { - return nil - } - for _, block := range blocks { - if workspace.BlockId == block.Id { - return &block - } - } - return nil - } - for _, run := range newRuns { - blockName := "(unknown)" - workspace := findWorkspace(run) - if block := findBlock(workspace); block != nil { - blockName = block.Name - } - browserUrl := "" - if workspace != nil { - browserUrl = fmt.Sprintf(" Logs: %s", runs.GetBrowserUrl(cfg, *workspace, run)) - } - fmt.Fprintf(os.Stdout, "created run to %s %s and dependencies in %q environment.%s\n", action, blockName, envName, browserUrl) - } - return nil + return PerformEnvRun(ctx, cfg, input) } diff --git a/cmd/launch.go b/cmd/launch.go index 8692a46..4622287 100644 --- a/cmd/launch.go +++ b/cmd/launch.go @@ -26,6 +26,7 @@ var Launch = func(providers app.Providers) *cli.Command { Action: func(c *cli.Context) error { return AppWorkspaceAction(c, func(ctx context.Context, cfg api.Config, appDetails app.Details) error { osWriters := CliOsWriters{Context: c} + stderr := osWriters.Stderr() source, version := c.String("source"), c.String("version") pusher, err := getPusher(providers, cfg, appDetails) @@ -35,12 +36,12 @@ var Launch = func(providers app.Providers) *cli.Command { commitSha := "" if version == "" { - fmt.Fprintf(osWriters.Stderr(), "No version specified. Defaulting version based on current git commit sha...\n") + fmt.Fprintf(stderr, "No version specified. Defaulting version based on current git commit sha...\n") commitSha, version, err = calcNewVersion(ctx, pusher) if err != nil { return err } - fmt.Fprintf(osWriters.Stderr(), "Version defaulted to: %s\n", version) + fmt.Fprintf(stderr, "Version defaulted to: %s\n", version) } err = push(ctx, osWriters, pusher, source, version) @@ -48,14 +49,20 @@ var Launch = func(providers app.Providers) *cli.Command { return err } - fmt.Fprintln(osWriters.Stderr(), "Creating deploy...") - deploy, err := CreateDeploy(cfg, appDetails, commitSha, version) + fmt.Fprintln(stderr, "Creating deploy...") + result, err := CreateDeploy(cfg, appDetails, commitSha, version) if err != nil { return err } - fmt.Fprintln(osWriters.Stderr()) - return streamDeployLogs(ctx, osWriters, cfg, *deploy, true) + fmt.Fprintln(stderr) + if result.Deploy != nil { + return streamDeployLogs(ctx, osWriters, cfg, *result.Deploy, true) + } else if result.IntentWorkflow != nil { + return streamDeployIntentLogs(ctx, osWriters, cfg, appDetails, *result.IntentWorkflow, true) + } + fmt.Fprintln(stderr, "Unable to stream deployment logs") + return nil }) }, } diff --git a/cmd/perform_env_run.go b/cmd/perform_env_run.go new file mode 100644 index 0000000..2c8b401 --- /dev/null +++ b/cmd/perform_env_run.go @@ -0,0 +1,90 @@ +package cmd + +import ( + "context" + "fmt" + "gopkg.in/nullstone-io/go-api-client.v0" + "gopkg.in/nullstone-io/go-api-client.v0/types" + "gopkg.in/nullstone-io/nullstone.v0/app_urls" + "os" +) + +type PerformEnvRunInput struct { + CommitSha string + Stack types.Stack + Env types.Environment + IsDestroy bool +} + +func PerformEnvRun(ctx context.Context, cfg api.Config, input PerformEnvRunInput) error { + stdout := os.Stdout + action := "launch" + if input.IsDestroy { + action = "destroy" + } + + client := api.Client{Config: cfg} + body := types.CreateEnvRunInput{IsDestroy: input.IsDestroy} + result, err := client.EnvRuns().Create(ctx, input.Stack.Id, input.Env.Id, body) + if err != nil { + return fmt.Errorf("error creating run: %w", err) + } else if result == nil { + fmt.Fprintf(stdout, "no runs created to %s the %q environment\n", action, input.Env.Name) + return nil + } + + if result.IntentWorkflow.Intent != "" { + fmt.Fprintf(stdout, "created workflow to %s %q environment.\n", action, input.Env.Name) + fmt.Fprintln(stdout, app_urls.GetIntentWorkflow(cfg, result.IntentWorkflow)) + return nil + } else if result.Runs == nil { + return fmt.Errorf("workflow to %q environment was not created", action) + } + + if len(result.Runs) < 1 { + fmt.Fprintf(stdout, "no runs created to %s the %q environment\n", action, input.Env.Name) + return nil + } + + workspaces, err := client.Workspaces().List(ctx, input.Env.Id) + if err != nil { + return fmt.Errorf("error retrieving list of workspaces: %w", err) + } + blocks, err := client.Blocks().List(ctx, input.Stack.Id) + if err != nil { + return fmt.Errorf("error retrieving list of blocks: %w", err) + } + + findWorkspace := func(run types.Run) *types.Workspace { + for _, workspace := range workspaces { + if workspace.Uid == run.WorkspaceUid { + return &workspace + } + } + return nil + } + findBlock := func(workspace *types.Workspace) *types.Block { + if workspace == nil { + return nil + } + for _, block := range blocks { + if workspace.BlockId == block.Id { + return &block + } + } + return nil + } + for _, run := range result.Runs { + blockName := "(unknown)" + workspace := findWorkspace(run) + if block := findBlock(workspace); block != nil { + blockName = block.Name + } + browserUrl := "" + if workspace != nil { + browserUrl = fmt.Sprintf(" Logs: %s", app_urls.GetRun(cfg, *workspace, run)) + } + fmt.Fprintf(os.Stdout, "created run to %s %s and dependencies in %q environment. %s\n", action, blockName, input.Env.Name, browserUrl) + } + return nil +} diff --git a/cmd/perform_run.go b/cmd/perform_run.go new file mode 100644 index 0000000..0fddb6d --- /dev/null +++ b/cmd/perform_run.go @@ -0,0 +1,61 @@ +package cmd + +import ( + "context" + "errors" + "fmt" + "gopkg.in/nullstone-io/go-api-client.v0" + api_runs "gopkg.in/nullstone-io/go-api-client.v0/runs" + "gopkg.in/nullstone-io/go-api-client.v0/types" + "gopkg.in/nullstone-io/nullstone.v0/app_urls" + "gopkg.in/nullstone-io/nullstone.v0/runs" + "os" +) + +type PerformRunInput struct { + Workspace types.Workspace + CommitSha string + IsApproved *bool + IsDestroy bool + BlockType types.BlockType + StreamLogs bool +} + +func PerformRun(ctx context.Context, cfg api.Config, input PerformRunInput) error { + result, err := api_runs.Create(ctx, cfg, input.Workspace, input.CommitSha, input.IsApproved, input.IsDestroy, "") + if err != nil { + return fmt.Errorf("error creating run: %w", err) + } else if result == nil { + return fmt.Errorf("unable to create run") + } + + var newRun types.Run + if result.IntentWorkflow != nil { + // When creating runs, we should have a primary workflow already + pw := result.IntentWorkflow.PrimaryWorkflow + if pw == nil { + return fmt.Errorf("no primary workflow found") + } + fmt.Fprintf(os.Stdout, "created workflow run (id = %d)\n", pw.Id) + fmt.Fprintln(os.Stdout, app_urls.GetWorkspaceWorkflow(cfg, *pw, input.BlockType == types.BlockTypeApplication)) + if newRun, err = waitForWorkspaceWorkflowRun(ctx, cfg, *pw); err != nil { + return fmt.Errorf("error waiting for workflow run: %w", err) + } + } else if result.Run != nil { + newRun = *result.Run + fmt.Fprintf(os.Stdout, "created run %q\n", newRun.Uid) + fmt.Fprintln(os.Stdout, app_urls.GetRun(cfg, input.Workspace, newRun)) + } else { + return fmt.Errorf("run was not created") + } + + if input.StreamLogs { + err := runs.StreamLogs(ctx, cfg, input.Workspace, &newRun) + if errors.Is(err, runs.ErrRunDisapproved) { + // Disapproved plans are expected, return no error + return nil + } + return err + } + return nil +} diff --git a/cmd/plan.go b/cmd/plan.go index 9180af5..06b3a35 100644 --- a/cmd/plan.go +++ b/cmd/plan.go @@ -2,12 +2,10 @@ package cmd import ( "context" - "fmt" "github.com/urfave/cli/v2" "gopkg.in/nullstone-io/go-api-client.v0" "gopkg.in/nullstone-io/go-api-client.v0/types" "gopkg.in/nullstone-io/nullstone.v0/runs" - "os" ) var Plan = func() *cli.Command { @@ -56,24 +54,15 @@ var Plan = func() *cli.Command { } f := false - newRun, err := runs.Create(ctx, cfg, workspace, &f, false) - if err != nil { - return fmt.Errorf("error creating run: %w", err) - } else if newRun == nil { - return fmt.Errorf("unable to create run") - } - fmt.Fprintf(os.Stdout, "created plan run %q\n", newRun.Uid) - fmt.Fprintln(os.Stdout, runs.GetBrowserUrl(cfg, workspace, *newRun)) - - if c.IsSet("wait") { - err := runs.StreamLogs(ctx, cfg, workspace, newRun) - if err == runs.ErrRunDisapproved { - // Disapproved plans are expected, return no error - return nil - } - return err + input := PerformRunInput{ + Workspace: workspace, + CommitSha: "", + IsApproved: &f, + IsDestroy: false, + BlockType: types.BlockType(block.Type), + StreamLogs: c.IsSet("wait"), } - return nil + return PerformRun(ctx, cfg, input) }) }, } diff --git a/cmd/up.go b/cmd/up.go index b790f45..9dec899 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -7,7 +7,6 @@ import ( "gopkg.in/nullstone-io/go-api-client.v0" "gopkg.in/nullstone-io/go-api-client.v0/types" "gopkg.in/nullstone-io/nullstone.v0/runs" - "os" ) var Up = func() *cli.Command { @@ -45,19 +44,15 @@ var Up = func() *cli.Command { } t := true - newRun, err := runs.Create(ctx, cfg, workspace, &t, false) - if err != nil { - return fmt.Errorf("error creating run: %w", err) - } else if newRun == nil { - return fmt.Errorf("unable to create run") - } - fmt.Printf("created run %q\n", newRun.Uid) - fmt.Fprintln(os.Stdout, runs.GetBrowserUrl(cfg, workspace, *newRun)) - - if c.IsSet("wait") { - return runs.StreamLogs(ctx, cfg, workspace, newRun) + input := PerformRunInput{ + Workspace: workspace, + CommitSha: "", + IsApproved: &t, + IsDestroy: false, + BlockType: types.BlockType(block.Type), + StreamLogs: c.IsSet("wait"), } - return nil + return PerformRun(ctx, cfg, input) }) }, } diff --git a/cmd/wait.go b/cmd/wait.go index dd4feb6..d8d48b7 100644 --- a/cmd/wait.go +++ b/cmd/wait.go @@ -9,6 +9,7 @@ import ( "gopkg.in/nullstone-io/go-api-client.v0" "gopkg.in/nullstone-io/go-api-client.v0/find" "gopkg.in/nullstone-io/go-api-client.v0/types" + "gopkg.in/nullstone-io/nullstone.v0/app_urls" "gopkg.in/nullstone-io/nullstone.v0/runs" "strings" "time" @@ -98,7 +99,7 @@ func WaitForLaunch(ctx context.Context, osWriters logging.OsWriters, cfg api.Con } fmt.Fprintf(stderr, "Waiting for %q to launch in %q environment...\n", details.Block.Name, details.Env.Name) - fmt.Fprintf(stderr, "Watching run for launch: %s\n", runs.GetBrowserUrl(cfg, *details.Workspace, *launchRun)) + fmt.Fprintf(stderr, "Watching run for launch: %s\n", app_urls.GetRun(cfg, *details.Workspace, *launchRun)) fmt.Fprintf(stderr, "Timeout = %s, Approval Timeout = %s\n", timeout, approvalTimeout) result, err := runs.WaitForTerminalRun(ctx, osWriters, cfg, *details.Workspace, *launchRun, timeout, approvalTimeout) diff --git a/cmd/wait_for.go b/cmd/wait_for.go new file mode 100644 index 0000000..8db3250 --- /dev/null +++ b/cmd/wait_for.go @@ -0,0 +1,64 @@ +package cmd + +import ( + "context" + "fmt" + "gopkg.in/nullstone-io/go-api-client.v0" + "gopkg.in/nullstone-io/go-api-client.v0/types" + "gopkg.in/nullstone-io/go-api-client.v0/ws" + "time" +) + +func waitForRunningIntentWorkflow(ctx context.Context, cfg api.Config, iw types.IntentWorkflow) (types.IntentWorkflow, error) { + client := api.Client{Config: cfg} + intentWorkflow, ch, err := client.IntentWorkflows().WatchGet(ctx, iw.StackId, iw.Id, ws.RetryInfinite(time.Second)) + if err != nil { + return iw, fmt.Errorf("error waiting for deployment: %w", err) + } else if intentWorkflow == nil { + return iw, context.Canceled + } + + cur := *intentWorkflow + for { + switch cur.Status { + case types.IntentWorkflowStatusRunning: + return cur, nil + case types.IntentWorkflowStatusCompleted: + return cur, nil + case types.IntentWorkflowStatusFailed: + return cur, fmt.Errorf("Deployment failed: %s", cur.StatusMessage) + case types.IntentWorkflowStatusCancelled: + return cur, fmt.Errorf("Deployment was cancelled.") + } + so := <-ch + if so.Err != nil { + return cur, fmt.Errorf("error waiting for deployment: %w", so.Err) + } + cur = so.Object.ApplyTo(cur) + } +} + +func waitForWorkspaceWorkflowRun(ctx context.Context, cfg api.Config, ww types.WorkspaceWorkflow) (types.Run, error) { + client := api.Client{Config: cfg} + workspaceWorkflow, ch, err := client.WorkspaceWorkflows().WatchGet(ctx, ww.StackId, ww.BlockId, ww.EnvId, ww.Id, ws.RetryInfinite(time.Second)) + if err != nil { + return types.Run{}, fmt.Errorf("error waiting for run: %w", err) + } else if workspaceWorkflow == nil { + return types.Run{}, context.Canceled + } + + cur := *workspaceWorkflow + for { + if cur.Run != nil { + return *cur.Run, nil + } + if types.IsTerminalWorkspaceWorkflow(cur.Status) { + return types.Run{}, fmt.Errorf("workflow reached %s status before a run could be found", cur.Status) + } + so := <-ch + if so.Err != nil { + return types.Run{}, fmt.Errorf("error waiting for run: %w", so.Err) + } + cur = so.Object.ApplyTo(cur) + } +} diff --git a/gcp/gke/remoter.go b/gcp/gke/remoter.go index adee3fe..9a2b2b2 100644 --- a/gcp/gke/remoter.go +++ b/gcp/gke/remoter.go @@ -12,7 +12,7 @@ import ( ) func NewRemoter(ctx context.Context, osWriters logging.OsWriters, source outputs.RetrieverSource, appDetails app.Details) (admin.Remoter, error) { - outs, err := outputs.Retrieve[Outputs](ctx, source, appDetails.Workspace) + outs, err := outputs.Retrieve[Outputs](ctx, source, appDetails.Workspace, appDetails.WorkspaceConfig) if err != nil { return nil, err } diff --git a/go.mod b/go.mod index 74ec738..ea1c3bb 100644 --- a/go.mod +++ b/go.mod @@ -10,14 +10,14 @@ require ( github.com/cristalhq/jwt/v3 v3.1.0 github.com/go-git/go-git/v5 v5.4.2 github.com/gosuri/uilive v0.0.4 - github.com/nullstone-io/deployment-sdk v0.0.0-20240325201158-9a2365f3f12d + github.com/nullstone-io/deployment-sdk v0.0.0-20240704122950-a55160b619cd github.com/nullstone-io/module v0.2.9 github.com/ryanuber/columnize v2.1.2+incompatible github.com/stretchr/testify v1.8.4 github.com/urfave/cli/v2 v2.3.0 golang.org/x/crypto v0.18.0 golang.org/x/sync v0.5.0 - gopkg.in/nullstone-io/go-api-client.v0 v0.0.0-20240409171401-6a698b5ac0bc + gopkg.in/nullstone-io/go-api-client.v0 v0.0.0-20240704122801-fa8b47b44f84 k8s.io/api v0.27.2 k8s.io/apimachinery v0.27.2 k8s.io/client-go v0.27.2 @@ -130,6 +130,7 @@ require ( github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/miekg/pkcs11 v1.0.3 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/moby/spdystream v0.2.0 // indirect github.com/moby/sys/mount v0.2.0 // indirect github.com/moby/sys/mountinfo v0.4.1 // indirect diff --git a/go.sum b/go.sum index 5a60a20..4206cf2 100644 --- a/go.sum +++ b/go.sum @@ -717,6 +717,8 @@ github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WT github.com/miekg/pkcs11 v1.0.3 h1:iMwmD7I5225wv84WxIG/bmxz9AXjWvTWIbM/TYHvWtw= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= @@ -759,8 +761,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nullstone-io/deployment-sdk v0.0.0-20240325201158-9a2365f3f12d h1:yIGvT7mlrEJ+5ehwwxI6IOWNnYmbdl3v9Cw9AChkqro= -github.com/nullstone-io/deployment-sdk v0.0.0-20240325201158-9a2365f3f12d/go.mod h1:zPeD7DYDnsv0IO2Gpo2/WC1zH6WleNQUbS+Eot1NIYg= +github.com/nullstone-io/deployment-sdk v0.0.0-20240704122950-a55160b619cd h1:3DWDfyQ9UGUfkfrK0PshA43fCp7PZuC/BjqYUBTZhi8= +github.com/nullstone-io/deployment-sdk v0.0.0-20240704122950-a55160b619cd/go.mod h1:7wc/0tmIELFoBTjNB5+hsyvTQD60mnt+BJ9/AU2/Cb8= github.com/nullstone-io/module v0.2.9 h1:PcYhPEemBbc+RdP+Q/DF0+XlwJkkNb5R17Hfv8qaYyc= github.com/nullstone-io/module v0.2.9/go.mod h1:btQiO0giVWDvvaQ7CLnPmuPPakJc55lAr8OlE1LK6hg= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -1444,8 +1446,8 @@ gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKW gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/nullstone-io/go-api-client.v0 v0.0.0-20240409171401-6a698b5ac0bc h1:bVsbmDx7Uo1YIGqvaDA4mfEbJFdQfP7pMkrvtiywI1w= -gopkg.in/nullstone-io/go-api-client.v0 v0.0.0-20240409171401-6a698b5ac0bc/go.mod h1:+cdH05EoXHHcFjSvRMXaaOhHSAmEj7tId0b+7R+N7qE= +gopkg.in/nullstone-io/go-api-client.v0 v0.0.0-20240704122801-fa8b47b44f84 h1:4O26uSJcXHvkzuSggSqEW5iJZSv/6ep8bg1bJJkkvi4= +gopkg.in/nullstone-io/go-api-client.v0 v0.0.0-20240704122801-fa8b47b44f84/go.mod h1:+cdH05EoXHHcFjSvRMXaaOhHSAmEj7tId0b+7R+N7qE= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 h1:d4KQkxAaAiRY2h5Zqis161Pv91A37uZyJOx73duwUwM= gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw= diff --git a/runs/app_url.go b/runs/app_url.go deleted file mode 100644 index 6ddfeb9..0000000 --- a/runs/app_url.go +++ /dev/null @@ -1,25 +0,0 @@ -package runs - -import ( - "fmt" - "gopkg.in/nullstone-io/go-api-client.v0" - "gopkg.in/nullstone-io/go-api-client.v0/types" - "net/url" - "strings" -) - -func GetBrowserUrl(cfg api.Config, workspace types.Workspace, run types.Run) string { - u, err := url.Parse(cfg.BaseAddress) - if err != nil { - u = &url.URL{Scheme: "https", Host: "app.nullstone.io"} - } - u.Host = strings.Replace(u.Host, "api", "app", 1) - if u.Host == "localhost:8443" { - u.Scheme = "http" - u.Host = "localhost:8090" - } - - u.Path = fmt.Sprintf("orgs/%s/stacks/%d/envs/%d/blocks/%d/activity/runs/%s", - workspace.OrgName, workspace.StackId, workspace.EnvId, workspace.BlockId, run.Uid) - return u.String() -} diff --git a/runs/create.go b/runs/create.go deleted file mode 100644 index 2fc219f..0000000 --- a/runs/create.go +++ /dev/null @@ -1,22 +0,0 @@ -package runs - -import ( - "context" - "fmt" - "gopkg.in/nullstone-io/go-api-client.v0" - "gopkg.in/nullstone-io/go-api-client.v0/types" -) - -func Create(ctx context.Context, cfg api.Config, workspace types.Workspace, isApproved *bool, isDestroy bool) (*types.Run, error) { - input := types.CreateRunInput{ - IsDestroy: isDestroy, - IsApproved: isApproved, - } - - client := api.Client{Config: cfg} - newRun, err := client.Runs().Create(ctx, workspace.StackId, workspace.Uid, input) - if err != nil { - return nil, fmt.Errorf("error creating run: %w", err) - } - return newRun, nil -} diff --git a/runs/stream_logs.go b/runs/stream_logs.go index e49ddb4..1b0850c 100644 --- a/runs/stream_logs.go +++ b/runs/stream_logs.go @@ -8,6 +8,7 @@ import ( "gopkg.in/nullstone-io/go-api-client.v0" "gopkg.in/nullstone-io/go-api-client.v0/types" "gopkg.in/nullstone-io/go-api-client.v0/ws" + "gopkg.in/nullstone-io/nullstone.v0/app_urls" "os" "sync" "time" @@ -57,7 +58,7 @@ func StreamLogs(ctx context.Context, cfg api.Config, workspace types.Workspace, printApprovalMsg.Do(func() { fmt.Fprintln(os.Stdout, "Nullstone requires approval before applying infrastructure changes.") fmt.Fprintln(os.Stdout, "Visit the infrastructure logs in a browser to approve/reject.") - fmt.Fprintln(os.Stdout, GetBrowserUrl(cfg, workspace, run)) + fmt.Fprintln(os.Stdout, app_urls.GetRun(cfg, workspace, run)) }) } case <-ctx.Done(): diff --git a/runs/wait_for_terminal_run.go b/runs/wait_for_terminal_run.go index 82c1304..3f9af0d 100644 --- a/runs/wait_for_terminal_run.go +++ b/runs/wait_for_terminal_run.go @@ -6,6 +6,7 @@ import ( "github.com/nullstone-io/deployment-sdk/logging" "gopkg.in/nullstone-io/go-api-client.v0" "gopkg.in/nullstone-io/go-api-client.v0/types" + "gopkg.in/nullstone-io/nullstone.v0/app_urls" "sync" "time" ) @@ -35,7 +36,7 @@ func WaitForTerminalRun(ctx context.Context, osWriters logging.OsWriters, cfg ap printApprovalMsg.Do(func() { fmt.Fprintln(stderr, "Nullstone requires approval before applying infrastructure changes.") fmt.Fprintln(stderr, "Visit the infrastructure logs in a browser to approve/reject.") - fmt.Fprintln(stderr, GetBrowserUrl(cfg, ws, updatedRun)) + fmt.Fprintln(stderr, app_urls.GetRun(cfg, ws, updatedRun)) }) approvalTimer.Reset(approvalTimeout) }