diff --git a/internal/cmd/blueprint/blueprint.go b/internal/cmd/blueprint/blueprint.go new file mode 100644 index 0000000..e375424 --- /dev/null +++ b/internal/cmd/blueprint/blueprint.go @@ -0,0 +1,87 @@ +package blueprint + +import ( + "fmt" + "math" + + "github.com/spacelift-io/spacectl/internal/cmd" + "github.com/spacelift-io/spacectl/internal/cmd/authenticated" + "github.com/urfave/cli/v2" +) + +// Command encapsulates the blueprintNode command subtree. +func Command() *cli.Command { + return &cli.Command{ + Name: "blueprint", + Usage: "Manage a Spacelift blueprints", + Subcommands: []*cli.Command{ + { + Name: "list", + Usage: "List the blueprints you have access to", + Flags: []cli.Flag{ + cmd.FlagShowLabels, + cmd.FlagOutputFormat, + cmd.FlagNoColor, + cmd.FlagLimit, + cmd.FlagSearch, + }, + Action: listBlueprints(), + Before: cmd.PerformAllBefore( + cmd.HandleNoColor, + authenticated.Ensure, + validateLimit, + validateSearch, + ), + ArgsUsage: cmd.EmptyArgsUsage, + }, + { + Name: "show", + Usage: "Shows detailed information about a specific blueprint", + Flags: []cli.Flag{ + flagRequiredBlueprintID, + cmd.FlagOutputFormat, + cmd.FlagNoColor, + }, + Action: (&showCommand{}).show, + Before: cmd.PerformAllBefore(cmd.HandleNoColor, authenticated.Ensure), + ArgsUsage: cmd.EmptyArgsUsage, + }, + { + Name: "deploy", + Usage: "Deploy a stack from the blueprint", + Flags: []cli.Flag{ + flagRequiredBlueprintID, + cmd.FlagNoColor, + }, + Action: (&deployCommand{}).deploy, + Before: cmd.PerformAllBefore(cmd.HandleNoColor, authenticated.Ensure), + ArgsUsage: cmd.EmptyArgsUsage, + }, + }, + } +} + +func validateLimit(cliCtx *cli.Context) error { + if cliCtx.IsSet(cmd.FlagLimit.Name) { + if cliCtx.Uint(cmd.FlagLimit.Name) == 0 { + return fmt.Errorf("limit must be greater than 0") + } + + if cliCtx.Uint(cmd.FlagLimit.Name) >= math.MaxInt32 { + return fmt.Errorf("limit must be less than %d", math.MaxInt32) + } + } + + return nil +} + +func validateSearch(cliCtx *cli.Context) error { + if cliCtx.IsSet(cmd.FlagSearch.Name) { + if cliCtx.String(cmd.FlagSearch.Name) == "" { + return fmt.Errorf("search must be non-empty") + } + + } + + return nil +} diff --git a/internal/cmd/blueprint/deploy.go b/internal/cmd/blueprint/deploy.go new file mode 100644 index 0000000..0c81cc3 --- /dev/null +++ b/internal/cmd/blueprint/deploy.go @@ -0,0 +1,192 @@ +package blueprint + +import ( + "fmt" + "slices" + "strconv" + "strings" + + "github.com/manifoldco/promptui" + "github.com/pkg/errors" + "github.com/spacelift-io/spacectl/internal/cmd/authenticated" + "github.com/urfave/cli/v2" +) + +type deployCommand struct{} + +func (c *deployCommand) deploy(cliCtx *cli.Context) error { + blueprintID := cliCtx.String(flagRequiredBlueprintID.Name) + + b, found, err := getBlueprintByID(cliCtx.Context, blueprintID) + if err != nil { + return errors.Wrapf(err, "failed to query for blueprint ID %q", blueprintID) + } + + if !found { + return fmt.Errorf("blueprint with ID %q not found", blueprintID) + } + + templateInputs := make([]BlueprintStackCreateInputPair, 0, len(b.Inputs)) + + for _, input := range b.Inputs { + var value string + switch strings.ToLower(input.Type) { + case "", "short_text", "long_text": + value, err = promptForTextInput(input) + if err != nil { + return err + } + case "secret": + value, err = promptForSecretInput(input) + if err != nil { + return err + } + case "number": + value, err = promptForIntegerInput(input) + if err != nil { + return err + } + case "float": + value, err = promptForFloatInput(input) + if err != nil { + return err + } + case "boolean": + value, err = promptForSelectInput(input, []string{"true", "false"}) + if err != nil { + return err + } + case "select": + value, err = promptForSelectInput(input, input.Options) + if err != nil { + return err + } + } + + templateInputs = append(templateInputs, BlueprintStackCreateInputPair{ + ID: input.ID, + Value: value, + }) + } + + var mutation struct { + BlueprintCreateStack struct { + StackID string `graphql:"stackID"` + } `graphql:"blueprintCreateStack(id: $id, input: $input)"` + } + + err = authenticated.Client.Mutate( + cliCtx.Context, + &mutation, + map[string]any{ + "id": blueprintID, + "input": BlueprintStackCreateInput{ + TemplateInputs: templateInputs, + }, + }, + ) + if err != nil { + return fmt.Errorf("failed to deploy stack from the blueprint: %w", err) + } + + url := authenticated.Client.URL("/stack/%s", mutation.BlueprintCreateStack.StackID) + fmt.Printf("\nCreated stack: %q", url) + + return nil +} + +func formatLabel(input blueprintInput) string { + if input.Description != "" { + return fmt.Sprintf("%s (%s) - %s", input.Name, input.ID, input.Description) + } + return fmt.Sprintf("%s (%s)", input.Name, input.ID) +} + +func promptForTextInput(input blueprintInput) (string, error) { + prompt := promptui.Prompt{ + Label: formatLabel(input), + Default: input.Default, + } + result, err := prompt.Run() + if err != nil { + return "", fmt.Errorf("failed to read text input for %q: %w", input.Name, err) + } + + return result, nil +} + +func promptForSecretInput(input blueprintInput) (string, error) { + prompt := promptui.Prompt{ + Label: formatLabel(input), + Default: input.Default, + Mask: '*', + } + result, err := prompt.Run() + if err != nil { + return "", fmt.Errorf("failed to read secret input for %q: %w", input.Name, err) + } + + return result, nil +} + +func promptForIntegerInput(input blueprintInput) (string, error) { + prompt := promptui.Prompt{ + Label: formatLabel(input), + Default: input.Default, + Validate: func(s string) error { + _, err := strconv.Atoi(s) + if err != nil { + return fmt.Errorf("input must be an integer") + } + + return nil + }, + } + result, err := prompt.Run() + if err != nil { + return "", fmt.Errorf("failed to read integer input for %q: %w", input.Name, err) + } + + return result, nil +} + +func promptForFloatInput(input blueprintInput) (string, error) { + prompt := promptui.Prompt{ + Label: formatLabel(input), + Default: input.Default, + Validate: func(s string) error { + _, err := strconv.ParseFloat(s, 64) + if err != nil { + return fmt.Errorf("input must be a float") + } + + return nil + }, + } + result, err := prompt.Run() + if err != nil { + return "", fmt.Errorf("failed to read float input for %q: %w", input.Name, err) + } + + return result, nil +} + +func promptForSelectInput(input blueprintInput, options []string) (string, error) { + cursorPosition := 0 + if input.Default != "" { + cursorPosition = slices.Index(options, input.Default) + } + + sel := promptui.Select{ + Label: formatLabel(input), + Items: options, + CursorPos: cursorPosition, + } + + _, result, err := sel.Run() + if err != nil { + return "", fmt.Errorf("failed to read selected input for %q: %w", input.Name, err) + } + + return result, nil +} diff --git a/internal/cmd/blueprint/flags.go b/internal/cmd/blueprint/flags.go new file mode 100644 index 0000000..a8ca690 --- /dev/null +++ b/internal/cmd/blueprint/flags.go @@ -0,0 +1,10 @@ +package blueprint + +import "github.com/urfave/cli/v2" + +var flagRequiredBlueprintID = &cli.StringFlag{ + Name: "blueprint-id", + Aliases: []string{"b-id"}, + Usage: "[Required] `ID` of the blueprint", + Required: true, +} diff --git a/internal/cmd/blueprint/inputs.go b/internal/cmd/blueprint/inputs.go new file mode 100644 index 0000000..bde06de --- /dev/null +++ b/internal/cmd/blueprint/inputs.go @@ -0,0 +1,12 @@ +package blueprint + +// BlueprintStackCreateInputPair represents a key-value pair for a blueprint input. +type BlueprintStackCreateInputPair struct { + ID string `json:"id"` + Value string `json:"value"` +} + +// BlueprintStackCreateInput represents the input for creating a new stack from a blueprint. +type BlueprintStackCreateInput struct { + TemplateInputs []BlueprintStackCreateInputPair `json:"templateInputs"` +} diff --git a/internal/cmd/blueprint/list.go b/internal/cmd/blueprint/list.go new file mode 100644 index 0000000..e8fc217 --- /dev/null +++ b/internal/cmd/blueprint/list.go @@ -0,0 +1,225 @@ +package blueprint + +import ( + "context" + "fmt" + "slices" + "strings" + + "github.com/pkg/errors" + "github.com/shurcooL/graphql" + "github.com/spacelift-io/spacectl/client/structs" + "github.com/spacelift-io/spacectl/internal" + "github.com/spacelift-io/spacectl/internal/cmd" + "github.com/spacelift-io/spacectl/internal/cmd/authenticated" + "github.com/urfave/cli/v2" +) + +func listBlueprints() cli.ActionFunc { + return func(cliCtx *cli.Context) error { + outputFormat, err := cmd.GetOutputFormat(cliCtx) + if err != nil { + return err + } + + var limit *uint + if cliCtx.IsSet(cmd.FlagLimit.Name) { + limit = internal.Ptr(cliCtx.Uint(cmd.FlagLimit.Name)) + } + + var search *string + if cliCtx.IsSet(cmd.FlagSearch.Name) { + search = internal.Ptr(cliCtx.String(cmd.FlagSearch.Name)) + } + + switch outputFormat { + case cmd.OutputFormatTable: + return listBlueprintsTable(cliCtx, search, limit) + case cmd.OutputFormatJSON: + return listBlueprintsJSON(cliCtx, search, limit) + } + + return fmt.Errorf("unknown output format: %v", outputFormat) + } +} + +func listBlueprintsJSON( + ctx *cli.Context, + search *string, + limit *uint, +) error { + var first *graphql.Int + if limit != nil { + first = graphql.NewInt(graphql.Int(*limit)) //nolint: gosec + } + + var fullTextSearch *graphql.String + if search != nil { + fullTextSearch = graphql.NewString(graphql.String(*search)) + } + + blueprints, err := searchAllBlueprints(ctx.Context, structs.SearchInput{ + First: first, + FullTextSearch: fullTextSearch, + }) + if err != nil { + return err + } + + return cmd.OutputJSON(blueprints) +} + +func listBlueprintsTable( + ctx *cli.Context, + search *string, + limit *uint, +) error { + var first *graphql.Int + if limit != nil { + first = graphql.NewInt(graphql.Int(*limit)) //nolint: gosec + } + + var fullTextSearch *graphql.String + if search != nil { + fullTextSearch = graphql.NewString(graphql.String(*search)) + } + + input := structs.SearchInput{ + First: first, + FullTextSearch: fullTextSearch, + OrderBy: &structs.QueryOrder{ + Field: "name", + Direction: "DESC", + }, + } + + blueprints, err := searchAllBlueprints(ctx.Context, input) + if err != nil { + return err + } + + columns := []string{"Name", "ID", "Description", "State", "Space", "Updated At"} + if ctx.Bool(cmd.FlagShowLabels.Name) { + columns = append(columns, "Labels") + } + + tableData := [][]string{columns} + for _, b := range blueprints { + row := []string{ + b.Name, + b.ID, + b.Description, + cmd.HumanizeBlueprintState(b.State), + b.Space.Name, + cmd.HumanizeUnixSeconds(b.UpdatedAt), + } + if ctx.Bool(cmd.FlagShowLabels.Name) { + row = append(row, strings.Join(b.Labels, ", ")) + } + + tableData = append(tableData, row) + } + + return cmd.OutputTable(tableData, true) +} + +// searchAllBlueprints returns a list of stacks based on the provided search input. +// input.First limits the total number of returned stacks, if not provided all stacks are returned. +func searchAllBlueprints(ctx context.Context, input structs.SearchInput) ([]blueprintNode, error) { + const maxPageSize = 50 + + var limit int + if input.First != nil { + limit = int(*input.First) + } + fetchAll := limit == 0 + + out := []blueprintNode{} + pageInput := structs.SearchInput{ + First: graphql.NewInt(maxPageSize), + FullTextSearch: input.FullTextSearch, + } + for { + if !fetchAll { + // Fetch exactly the number of items requested + pageInput.First = graphql.NewInt( + //nolint: gosec + graphql.Int( + slices.Min([]int{maxPageSize, limit - len(out)}), + ), + ) + } + + result, err := searchBlueprints(ctx, pageInput) + if err != nil { + return nil, err + } + + out = append(out, result.Blueprints...) + + if result.PageInfo.HasNextPage && (fetchAll || limit > len(out)) { + pageInput.After = graphql.NewString(graphql.String(result.PageInfo.EndCursor)) + } else { + break + } + } + + return out, nil +} + +type blueprintNode struct { + ID string `graphql:"id" json:"id,omitempty"` + Name string `graphql:"name" json:"name,omitempty"` + State string `graphql:"state" json:"state,omitempty"` + Description string `graphql:"description" json:"description,omitempty"` + Labels []string `graphql:"labels" json:"labels,omitempty"` + CreatedAt int `graphql:"createdAt" json:"createdAt,omitempty"` + UpdatedAt int `graphql:"updatedAt" json:"updatedAt,omitempty"` + RawTemplate string `graphql:"rawTemplate" json:"rawTemplate,omitempty"` + Inputs []struct { + ID string `graphql:"id" json:"id,omitempty"` + Name string `graphql:"name" json:"name,omitempty"` + Default string `graphql:"default" json:"default,omitempty"` + Options []string `graphql:"options" json:"options,omitempty"` + Type string `graphql:"type" json:"type,omitempty"` + } `graphql:"inputs" json:"inputs,omitempty"` + Space struct { + ID string `graphql:"id" json:"id,omitempty"` + Name string `graphql:"name" json:"name,omitempty"` + AccessLevel string `graphql:"accessLevel" json:"accessLevel,omitempty"` + } `graphql:"space" json:"space,omitempty"` +} + +type searchBlueprintsResult struct { + Blueprints []blueprintNode + PageInfo structs.PageInfo +} + +func searchBlueprints(ctx context.Context, input structs.SearchInput) (searchBlueprintsResult, error) { + var query struct { + SearchBlueprintsOutput struct { + Edges []struct { + Node blueprintNode `graphql:"node"` + } `graphql:"edges"` + PageInfo structs.PageInfo `graphql:"pageInfo"` + } `graphql:"searchBlueprints(input: $input)"` + } + + if err := authenticated.Client.Query( + ctx, + &query, + map[string]interface{}{"input": input}, + ); err != nil { + return searchBlueprintsResult{}, errors.Wrap(err, "failed search for blueprints") + } + + nodes := make([]blueprintNode, 0) + for _, q := range query.SearchBlueprintsOutput.Edges { + nodes = append(nodes, q.Node) + } + + return searchBlueprintsResult{ + Blueprints: nodes, + PageInfo: query.SearchBlueprintsOutput.PageInfo, + }, nil +} diff --git a/internal/cmd/blueprint/show.go b/internal/cmd/blueprint/show.go new file mode 100644 index 0000000..079cf00 --- /dev/null +++ b/internal/cmd/blueprint/show.go @@ -0,0 +1,175 @@ +package blueprint + +import ( + "context" + "fmt" + "strings" + + "github.com/pkg/errors" + "github.com/pterm/pterm" + "github.com/shurcooL/graphql" + "github.com/spacelift-io/spacectl/internal/cmd" + "github.com/spacelift-io/spacectl/internal/cmd/authenticated" + "github.com/urfave/cli/v2" +) + +type blueprintInput struct { + ID string `graphql:"id" json:"id,omitempty"` + Name string `graphql:"name" json:"name,omitempty"` + Default string `graphql:"default" json:"default,omitempty"` + Description string `graphql:"description" json:"description,omitempty"` + Options []string `graphql:"options" json:"options,omitempty"` + Type string `graphql:"type" json:"type,omitempty"` +} + +type blueprint struct { + ID string `graphql:"id" json:"id,omitempty"` + Deleted bool `graphql:"deleted" json:"deleted,omitempty"` + Name string `graphql:"name" json:"name,omitempty"` + Description string `graphql:"description" json:"description,omitempty"` + CreatedAt int `graphql:"createdAt" json:"createdAt,omitempty"` + UpdatedAt int `graphql:"updatedAt" json:"updatedAt,omitempty"` + State string `graphql:"state" json:"state,omitempty"` + Inputs []blueprintInput + Space struct { + ID string `graphql:"id" json:"id,omitempty"` + Name string `graphql:"name" json:"name,omitempty"` + AccessLevel string `graphql:"accessLevel" json:"accessLevel,omitempty"` + } + Labels []string `graphql:"labels" json:"labels,omitempty"` + RawTemplate string `graphql:"rawTemplate" json:"rawTemplate,omitempty"` +} + +type showCommand struct{} + +func (c *showCommand) show(cliCtx *cli.Context) error { + blueprintID := cliCtx.String(flagRequiredBlueprintID.Name) + + outputFormat, err := cmd.GetOutputFormat(cliCtx) + if err != nil { + return err + } + + b, found, err := getBlueprintByID(cliCtx.Context, blueprintID) + if err != nil { + return errors.Wrapf(err, "failed to query for blueprint ID %q", blueprintID) + } + + if !found { + return fmt.Errorf("blueprint with ID %q not found", blueprintID) + } + + switch outputFormat { + case cmd.OutputFormatTable: + return c.showBlueprintTable(b) + case cmd.OutputFormatJSON: + return cmd.OutputJSON(b) + } + + return fmt.Errorf("unknown output format: %v", outputFormat) +} + +func (c *showCommand) showBlueprintTable(b blueprint) error { + c.outputBlueprintNameSection(b) + + if err := c.outputInputs(b); err != nil { + return err + } + + if err := c.outputSpace(b); err != nil { + return err + } + + c.outputRawTemplate(b) + + return nil +} + +func (c *showCommand) outputBlueprintNameSection(b blueprint) { + pterm.DefaultSection.WithLevel(1).Print(b.Name) + + if len(b.Labels) > 0 { + pterm.DefaultSection.WithLevel(2).Println("Labels") + pterm.DefaultParagraph.Println(fmt.Sprintf("[%s]", strings.Join(b.Labels, "], ["))) + } + + if b.Description != "" { + pterm.DefaultSection.WithLevel(2).Println("Description") + pterm.DefaultParagraph.Println(b.Description) + } + + if b.CreatedAt != 0 { + pterm.DefaultSection.WithLevel(2).Println("Created at") + pterm.DefaultParagraph.Println(cmd.HumanizeUnixSeconds(b.CreatedAt)) + } + + if b.UpdatedAt != 0 { + pterm.DefaultSection.WithLevel(2).Println("Updated at") + pterm.DefaultParagraph.Println(cmd.HumanizeUnixSeconds(b.UpdatedAt)) + } + + if b.State != "" { + pterm.DefaultSection.WithLevel(2).Println("State") + pterm.DefaultParagraph.Println(cmd.HumanizeBlueprintState(b.State)) + } +} + +func (c *showCommand) outputInputs(b blueprint) error { + if len(b.Inputs) == 0 { + return nil + } + + pterm.DefaultSection.WithLevel(2).Println("Inputs") + + tableData := [][]string{{"Name", "ID", "Description", "Default", "Options", "Type"}} + for _, input := range b.Inputs { + + tableData = append(tableData, []string{ + input.Name, + input.ID, + input.Description, + input.Default, + strings.Join(input.Options, ", "), + input.Type, + }) + } + + return cmd.OutputTable(tableData, true) +} + +func (c *showCommand) outputSpace(b blueprint) error { + pterm.DefaultSection.WithLevel(2).Println("Space") + tableData := [][]string{ + {"Name", b.Space.Name}, + {"ID", b.Space.ID}, + {"Access Level", b.Space.AccessLevel}, + } + + return cmd.OutputTable(tableData, false) +} + +func (c *showCommand) outputRawTemplate(b blueprint) { + pterm.DefaultSection.WithLevel(2).Println("Template") + + pterm.Println(b.RawTemplate) +} + +func getBlueprintByID(ctx context.Context, blueprintID string) (blueprint, bool, error) { + var query struct { + Blueprint *blueprint `graphql:"blueprint(id: $blueprintId)" json:"blueprint,omitempty"` + } + + variables := map[string]interface{}{ + "blueprintId": graphql.ID(blueprintID), + } + + if err := authenticated.Client.Query(ctx, &query, variables); err != nil { + return blueprint{}, false, errors.Wrapf(err, "failed to query for blueprint ID %q", blueprintID) + } + + if query.Blueprint == nil { + return blueprint{}, false, nil + } + + return *query.Blueprint, true, nil +} diff --git a/internal/cmd/flags.go b/internal/cmd/flags.go index a87174a..0a5c651 100644 --- a/internal/cmd/flags.go +++ b/internal/cmd/flags.go @@ -20,3 +20,21 @@ var FlagNoColor = &cli.BoolFlag{ Name: "no-color", Usage: "Disables coloring for the console output. Automatically enabled when the output is not a terminal.", } + +// FlagLimit is a flag used for limiting the number of items to return. +var FlagLimit = &cli.UintFlag{ + Name: "limit", + Usage: "[Optional] Limit the number of items to return", +} + +// FlagSearch is a flag used for performing a full-text search. +var FlagSearch = &cli.StringFlag{ + Name: "search", + Usage: "[Optional] Performs a full-text search.", +} + +// FlagShowLabels is a flag used for indicating that labels should be printed when outputting data in the table format. +var FlagShowLabels = &cli.BoolFlag{ + Name: "show-labels", + Usage: "[Optional] Indicates that labels should be printed when outputting data in the table format", +} diff --git a/internal/cmd/humanize.go b/internal/cmd/humanize.go index 8cb5e89..ec67f91 100644 --- a/internal/cmd/humanize.go +++ b/internal/cmd/humanize.go @@ -1,5 +1,9 @@ package cmd +import ( + "time" +) + // HumanizeVCSProvider converts the GraphQL VCSProvider enum to a human readable string. func HumanizeVCSProvider(provider string) string { switch provider { @@ -52,3 +56,18 @@ func HumanizeGitHash(hash string) string { return hash } + +func HumanizeBlueprintState(state string) string { + switch state { + case "DRAFT": + return "Draft" + case "PUBLISHED": + return "Published" + } + + return state +} + +func HumanizeUnixSeconds(seconds int) string { + return time.Unix(int64(seconds), 0).Format(time.RFC3339) +} diff --git a/internal/cmd/profile/login_command.go b/internal/cmd/profile/login_command.go index e2e928b..445f63e 100644 --- a/internal/cmd/profile/login_command.go +++ b/internal/cmd/profile/login_command.go @@ -100,8 +100,7 @@ func getCredentialsType(ctx *cli.Context) (session.CredentialsType, error) { return 0, err } - //nolint: gosec - return session.CredentialsType(result + 1), nil + return session.CredentialsType(result + 1), nil //nolint: gosec } func readEndpoint(ctx *cli.Context, reader *bufio.Reader) (string, error) { diff --git a/internal/cmd/stack/flags.go b/internal/cmd/stack/flags.go index 5a1926e..4359214 100644 --- a/internal/cmd/stack/flags.go +++ b/internal/cmd/stack/flags.go @@ -120,13 +120,6 @@ var flagSearchCount = &cli.IntFlag{ Value: 30, } -var flagShowLabels = &cli.BoolFlag{ - Name: "show-labels", - Usage: "[Optional] Indicates that stack labels should be printed when outputting stack data in the table format", - Required: false, - Value: false, -} - var flagOverrideEnvVarsTF = &cli.StringSliceFlag{ Name: "tf-env-var-override", Usage: "[Optional] Terraform environment variables injected into the run at runtime, they will be prefixed with TF_ by default, example: --tf-env-var-override 'foo=bar,bar=baz'", @@ -157,13 +150,3 @@ var flagInteractive = &cli.BoolFlag{ Aliases: []string{"i"}, Usage: "[Optional] Whether to run the command in interactive mode", } - -var flagLimit = &cli.UintFlag{ - Name: "limit", - Usage: "[Optional] Limit the number of items to return", -} - -var flagSearch = &cli.StringFlag{ - Name: "search", - Usage: "[Optional] Performs a full-text search.", -} diff --git a/internal/cmd/stack/list.go b/internal/cmd/stack/list.go index 9386038..d753625 100644 --- a/internal/cmd/stack/list.go +++ b/internal/cmd/stack/list.go @@ -22,25 +22,25 @@ func listStacks() cli.ActionFunc { } var limit *uint - if cliCtx.IsSet(flagLimit.Name) { - if cliCtx.Uint(flagLimit.Name) == 0 { + if cliCtx.IsSet(cmd.FlagLimit.Name) { + if cliCtx.Uint(cmd.FlagLimit.Name) == 0 { return fmt.Errorf("limit must be greater than 0") } - if cliCtx.Uint(flagLimit.Name) >= math.MaxInt32 { + if cliCtx.Uint(cmd.FlagLimit.Name) >= math.MaxInt32 { return fmt.Errorf("limit must be less than %d", math.MaxInt32) } - limit = internal.Ptr(cliCtx.Uint(flagLimit.Name)) + limit = internal.Ptr(cliCtx.Uint(cmd.FlagLimit.Name)) } var search *string - if cliCtx.IsSet(flagSearch.Name) { - if cliCtx.String(flagSearch.Name) == "" { + if cliCtx.IsSet(cmd.FlagSearch.Name) { + if cliCtx.String(cmd.FlagSearch.Name) == "" { return fmt.Errorf("search must be non-empty") } - search = internal.Ptr(cliCtx.String(flagSearch.Name)) + search = internal.Ptr(cliCtx.String(cmd.FlagSearch.Name)) } switch outputFormat { @@ -110,7 +110,7 @@ func listStacksTable( } columns := []string{"Name", "ID", "Commit", "Author", "State", "Worker Pool", "Locked By"} - if ctx.Bool(flagShowLabels.Name) { + if ctx.Bool(cmd.FlagShowLabels.Name) { columns = append(columns, "Labels") } @@ -125,7 +125,7 @@ func listStacksTable( s.WorkerPool.Name, s.LockedBy, } - if ctx.Bool(flagShowLabels.Name) { + if ctx.Bool(cmd.FlagShowLabels.Name) { row = append(row, strings.Join(s.Labels, ", ")) } diff --git a/internal/cmd/stack/stack.go b/internal/cmd/stack/stack.go index 775f241..cff7b78 100644 --- a/internal/cmd/stack/stack.go +++ b/internal/cmd/stack/stack.go @@ -137,11 +137,11 @@ func Command() *cli.Command { Name: "list", Usage: "List the stacks you have access to", Flags: []cli.Flag{ - flagShowLabels, + cmd.FlagShowLabels, cmd.FlagOutputFormat, cmd.FlagNoColor, - flagLimit, - flagSearch, + cmd.FlagLimit, + cmd.FlagSearch, }, Action: listStacks(), Before: cmd.PerformAllBefore(cmd.HandleNoColor, authenticated.Ensure), diff --git a/main.go b/main.go index d7604aa..68d9099 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "os" "time" + "github.com/spacelift-io/spacectl/internal/cmd/blueprint" "github.com/spacelift-io/spacectl/internal/cmd/completion" "github.com/spacelift-io/spacectl/internal/cmd/module" "github.com/spacelift-io/spacectl/internal/cmd/profile" @@ -41,6 +42,7 @@ func main() { versioncmd.Command(version), workerpools.Command(), completion.Command(), + blueprint.Command(), }, }