Skip to content

Commit

Permalink
Allow to get run changes and replan a run (#213)
Browse files Browse the repository at this point in the history
* Allow to get run changes and replan a run
* Fix rocket emoji for good
  • Loading branch information
tomasmik authored Jan 23, 2024
1 parent 10a716e commit 16bff7c
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 0 deletions.
11 changes: 11 additions & 0 deletions internal/cmd/stack/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,14 @@ var flagDisregardGitignore = &cli.BoolFlag{
Name: "disregard-gitignore",
Usage: "[Optional] Disregard the .gitignore file when reading files in a directory",
}

var flagResources = &cli.StringSliceFlag{
Name: "resources",
Usage: "[Optional] A comma separeted list of resources to be used when applying, example: 'aws_instance.foo'",
}

var flagInteractive = &cli.BoolFlag{
Name: "interactive",
Aliases: []string{"i"},
Usage: "[Optional] Whether to run the command in interactive mode",
}
59 changes: 59 additions & 0 deletions internal/cmd/stack/run_changes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package stack

import (
"github.com/pkg/errors"
"github.com/shurcooL/graphql"
"github.com/urfave/cli/v2"

"github.com/spacelift-io/spacectl/internal/cmd"
"github.com/spacelift-io/spacectl/internal/cmd/authenticated"
)

func runChanges(cliCtx *cli.Context) error {
stackID, err := getStackID(cliCtx)
if err != nil {
return err
}
run := cliCtx.String(flagRequiredRun.Name)

result, err := getRunChanges(cliCtx, stackID, run)
if err != nil {
return err
}

return cmd.OutputJSON(result)
}

func getRunChanges(cliCtx *cli.Context, stackID, runID string) ([]runChangesData, error) {
var query struct {
Stack struct {
Run struct {
ChangesV3 []runChangesData `graphql:"changesV3(input: {})"`
} `graphql:"run(id: $run)"`
} `graphql:"stack(id: $stack)"`
}

variables := map[string]any{
"stack": graphql.ID(stackID),
"run": graphql.ID(runID),
}
if err := authenticated.Client.Query(cliCtx.Context, &query, variables); err != nil {
return nil, errors.Wrap(err, "failed to query one stack")
}

return query.Stack.Run.ChangesV3, nil
}

type runChangesData struct {
Resources []runChangesResource `graphql:"resources"`
}

type runChangesResource struct {
Address string `graphql:"address"`
PreviousAddress string `graphql:"previousAddress"`
Metadata runChangesMetadata `graphql:"metadata"`
}

type runChangesMetadata struct {
Type string `graphql:"type"`
}
143 changes: 143 additions & 0 deletions internal/cmd/stack/run_replan.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package stack

import (
"fmt"
"strings"

"github.com/manifoldco/promptui"
"github.com/shurcooL/graphql"
"github.com/urfave/cli/v2"

"github.com/spacelift-io/spacectl/internal/cmd/authenticated"
)

const rocketEmoji = "\U0001F680"

func runReplan(cliCtx *cli.Context) error {
stackID, err := getStackID(cliCtx)
if err != nil {
return err
}

runID := cliCtx.String(flagRequiredRun.Name)

var resources []string
if cliCtx.Bool(flagInteractive.Name) {
var err error
resources, err = interactiveResourceSelection(cliCtx, stackID, runID)
if err != nil {
return err
}
} else {
resources = cliCtx.StringSlice(flagResources.Name)
}

if len(resources) == 0 {
return fmt.Errorf("no resources targeted for replanning: at least one resource must be specified")
}

var mutation struct {
RunTargetedReplan struct {
ID string `graphql:"id"`
} `graphql:"runTargetedReplan(stack: $stack, run: $run, targets: $targets)"`
}

targets := make([]graphql.String, len(resources))
for i, resource := range resources {
targets[i] = graphql.String(resource)
}

variables := map[string]interface{}{
"stack": graphql.ID(stackID),
"run": graphql.ID(runID),
"targets": targets,
}

if err := authenticated.Client.Mutate(cliCtx.Context, &mutation, variables); err != nil {
return err
}

fmt.Printf("Run ID %q is being replanned\n", runID)
fmt.Println("The live run can be visited at", authenticated.Client.URL(
"/stack/%s/run/%s",
stackID,
mutation.RunTargetedReplan.ID,
))

if !cliCtx.Bool(flagTail.Name) {
return nil
}

terminal, err := runLogsWithAction(cliCtx.Context, stackID, mutation.RunTargetedReplan.ID, nil)
if err != nil {
return err
}

return terminal.Error()
}

func interactiveResourceSelection(cliCtx *cli.Context, stackID, runID string) ([]string, error) {
resources, err := getRunChanges(cliCtx, stackID, runID)
if err != nil {
return nil, err
}

templates := &promptui.SelectTemplates{
Label: "{{ . }}?",
Active: fmt.Sprintf("%s {{ .Address | cyan }} %s", rocketEmoji, rocketEmoji),
Inactive: " {{ .Address | cyan }}",
Selected: fmt.Sprintf("%s {{ .Address cyan }} %s", rocketEmoji, rocketEmoji),
Details: `
----------- Details ------------
{{ "Address:" | faint }} {{ .Address }}
{{ "PreviousAddress:" | faint }} {{ .PreviousAddress }}
{{ "Type:" | faint }} {{ .Metadata.Type }}
`,
}

values := make([]runChangesResource, 0)
selected := make([]string, 0)

for _, r := range resources {
values = append(values, r.Resources...)
}

for {
prompt := promptui.Select{
Label: "Which resource should be added to the replan",
Items: values,
Templates: templates,
Size: 20,
StartInSearchMode: len(values) > 10,
Searcher: func(input string, index int) bool {
return strings.Contains(values[index].Address, input)
},
}

index, _, err := prompt.Run()
if err != nil {
return nil, err
}

selected = append(selected, values[index].Address)
values = append(values[:index], values[index+1:]...)

if !shouldPickMore() || len(values) == 0 {
break
}
}

return selected, nil
}

func shouldPickMore() bool {
prompt := promptui.Prompt{
Label: "Pick more",
IsConfirm: true,
Default: "y",
}

result, _ := prompt.Run()

return result == "y" || result == ""
}
27 changes: 27 additions & 0 deletions internal/cmd/stack/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,33 @@ func Command() *cli.Command {
Before: authenticated.Ensure,
ArgsUsage: cmd.EmptyArgsUsage,
},
{
Category: "Run management",
Name: "replan",
Usage: "Replan an unconfirmed tracked run",
Flags: []cli.Flag{
flagStackID,
flagRequiredRun,
flagTail,
flagResources,
flagInteractive,
},
Action: runReplan,
Before: authenticated.Ensure,
ArgsUsage: cmd.EmptyArgsUsage,
},
{
Category: "Run management",
Name: "changes",
Usage: "Show a list of changes for a given run",
Flags: []cli.Flag{
flagStackID,
flagRequiredRun,
},
Action: runChanges,
Before: authenticated.Ensure,
ArgsUsage: cmd.EmptyArgsUsage,
},
{
Name: "list",
Usage: "List the stacks you have access to",
Expand Down

0 comments on commit 16bff7c

Please sign in to comment.