Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to get run changes and replan a run #213

Merged
merged 2 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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: "\U0001F680 {{ .Address cyan }} \U0001F680",
tomasmik marked this conversation as resolved.
Show resolved Hide resolved
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