From ab4151907c92e5c367434e56545aea847bf0799e Mon Sep 17 00:00:00 2001 From: Tomas <40318863+tomasmik@users.noreply.github.com> Date: Fri, 19 Jul 2024 11:04:42 +0300 Subject: [PATCH] feat: Add additional module commands for versions (#235) --- internal/cmd/module/list.go | 119 ++++++++++++++++++++++++++ internal/cmd/module/module.go | 23 +++++ internal/cmd/module/search_version.go | 105 +++++++++++++++++++++++ 3 files changed, 247 insertions(+) create mode 100644 internal/cmd/module/list.go create mode 100644 internal/cmd/module/search_version.go diff --git a/internal/cmd/module/list.go b/internal/cmd/module/list.go new file mode 100644 index 0000000..1ca8e71 --- /dev/null +++ b/internal/cmd/module/list.go @@ -0,0 +1,119 @@ +package module + +import ( + "context" + "fmt" + "sort" + "strings" + + "github.com/pkg/errors" + "github.com/urfave/cli/v2" + + "github.com/spacelift-io/spacectl/internal/cmd" + "github.com/spacelift-io/spacectl/internal/cmd/authenticated" +) + +func listModules() cli.ActionFunc { + return func(cliCtx *cli.Context) error { + outputFormat, err := cmd.GetOutputFormat(cliCtx) + if err != nil { + return err + } + + switch outputFormat { + case cmd.OutputFormatTable: + return listModulesTable(cliCtx) + case cmd.OutputFormatJSON: + return listModulesJSON(cliCtx.Context) + } + + return fmt.Errorf("unknown output format: %v", outputFormat) + } +} + +func listModulesJSON(ctx context.Context) error { + var query struct { + Modules []module `graphql:"modules" json:"modules,omitempty"` + } + + if err := authenticated.Client.Query(ctx, &query, map[string]interface{}{}); err != nil { + return errors.Wrap(err, "failed to query list of modules") + } + return cmd.OutputJSON(query.Modules) +} + +func listModulesTable(ctx *cli.Context) error { + var query struct { + Modules []struct { + ID string `graphql:"id" json:"id,omitempty"` + Name string `graphql:"name"` + Current struct { + ID string `graphql:"id"` + Number string `graphql:"number"` + State string `graphql:"state"` + Yanked bool `graphql:"yanked"` + } `graphql:"current"` + } `graphql:"modules"` + } + + if err := authenticated.Client.Query(ctx.Context, &query, map[string]interface{}{}); err != nil { + return errors.Wrap(err, "failed to query list of modules") + } + + sort.SliceStable(query.Modules, func(i, j int) bool { + return strings.Compare(strings.ToLower(query.Modules[i].Name), strings.ToLower(query.Modules[j].Name)) < 0 + }) + + columns := []string{"Name", "ID", "Current Version", "Number", "State", "Yanked"} + + tableData := [][]string{columns} + for _, module := range query.Modules { + row := []string{ + module.Name, + module.ID, + module.Current.ID, + module.Current.Number, + module.Current.State, + fmt.Sprintf("%t", module.Current.Yanked), + } + + tableData = append(tableData, row) + } + + return cmd.OutputTable(tableData, true) +} + +type module struct { + ID string `json:"id" graphql:"id"` + Administrative bool `json:"administrative" graphql:"administrative"` + APIHost string `json:"apiHost" graphql:"apiHost"` + Branch string `json:"branch" graphql:"branch"` + Description string `json:"description" graphql:"description"` + CanWrite bool `json:"canWrite" graphql:"canWrite"` + IsDisabled bool `json:"isDisabled" graphql:"isDisabled"` + CreatedAt int `json:"createdAt" graphql:"createdAt"` + Labels []string `json:"labels" graphql:"labels"` + Name string `json:"name" graphql:"name"` + Namespace string `json:"namespace" graphql:"namespace"` + ProjectRoot string `json:"projectRoot" graphql:"projectRoot"` + Provider string `json:"provider" graphql:"provider"` + Repository string `json:"repository" graphql:"repository"` + TerraformProvider string `json:"terraformProvider" graphql:"terraformProvider"` + LocalPreviewEnabled bool `json:"localPreviewEnabled,omitempty" graphql:"localPreviewEnabled"` + Current struct { + ID string `json:"id" graphql:"id"` + Number string `json:"number" graphql:"number"` + State string `json:"state" graphql:"state"` + Yanked bool `json:"yanked" graphql:"yanked"` + } `json:"current" graphql:"current"` + SpaceDetails struct { + ID string `json:"id" graphql:"id"` + Name string `json:"name" graphql:"name"` + AccessLevel string `json:"accessLevel" graphql:"accessLevel"` + } `json:"spaceDetails" graphql:"spaceDetails"` + WorkerPool struct { + ID string `graphql:"id" json:"id,omitempty"` + Name string `graphql:"name" json:"name,omitempty"` + } `graphql:"workerPool" json:"workerPool,omitempty"` + Starred bool `json:"starred" graphql:"starred"` +} diff --git a/internal/cmd/module/module.go b/internal/cmd/module/module.go index d25f1d4..83aea0a 100644 --- a/internal/cmd/module/module.go +++ b/internal/cmd/module/module.go @@ -42,6 +42,29 @@ func Command() *cli.Command { Before: authenticated.Ensure, ArgsUsage: cmd.EmptyArgsUsage, }, + { + Category: "Module management", + Name: "list", + Usage: "List all modules available and their current version", + Flags: []cli.Flag{ + cmd.FlagOutputFormat, + }, + Action: listModules(), + Before: authenticated.Ensure, + ArgsUsage: cmd.EmptyArgsUsage, + }, + { + Category: "Module management", + Name: "list-versions", + Usage: "List 20 latest non failed versions for a module", + Flags: []cli.Flag{ + flagModuleID, + cmd.FlagOutputFormat, + }, + Action: listVersions(), + Before: authenticated.Ensure, + ArgsUsage: cmd.EmptyArgsUsage, + }, }, } } diff --git a/internal/cmd/module/search_version.go b/internal/cmd/module/search_version.go new file mode 100644 index 0000000..177af14 --- /dev/null +++ b/internal/cmd/module/search_version.go @@ -0,0 +1,105 @@ +package module + +import ( + "fmt" + + "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 listVersions() cli.ActionFunc { + return func(cliCtx *cli.Context) error { + outputFormat, err := cmd.GetOutputFormat(cliCtx) + if err != nil { + return err + } + + switch outputFormat { + case cmd.OutputFormatTable: + return listVersionsTable(cliCtx) + case cmd.OutputFormatJSON: + return listVersionsJSON(cliCtx) + } + + return fmt.Errorf("unknown output format: %v", outputFormat) + } +} + +func listVersionsJSON(cliCtx *cli.Context) error { + var query struct { + Module struct { + Verions []version `graphql:"versions(includeFailed: $includeFailed)"` + } `graphql:"module(id: $id)"` + } + + if err := authenticated.Client.Query(cliCtx.Context, &query, map[string]interface{}{ + "id": cliCtx.String(flagModuleID.Name), + "includeFailed": graphql.Boolean(false), + }); err != nil { + return errors.Wrap(err, "failed to query list of modules") + } + return cmd.OutputJSON(query.Module.Verions) +} + +func listVersionsTable(cliCtx *cli.Context) error { + var query struct { + Module struct { + Verions []version `graphql:"versions(includeFailed: $includeFailed)"` + } `graphql:"module(id: $id)"` + } + + if err := authenticated.Client.Query(cliCtx.Context, &query, map[string]interface{}{ + "id": cliCtx.String(flagModuleID.Name), + "includeFailed": graphql.Boolean(false), + }); err != nil { + return errors.Wrap(err, "failed to query list of modules") + } + + columns := []string{"ID", "Author", "Message", "Number", "State", "Tests", "Timestamp"} + tableData := [][]string{columns} + + if len(query.Module.Verions) > 20 { + query.Module.Verions = query.Module.Verions[:20] + } + + // We print the versions in reverse order + // so the latest version is at the bottom, much easier to read in terminal. + for i := len(query.Module.Verions) - 1; i >= 0; i-- { + module := query.Module.Verions[i] + row := []string{ + module.ID, + module.Commit.AuthorName, + module.Commit.Message, + module.Number, + module.State, + fmt.Sprintf("%d", module.VersionCount), + fmt.Sprintf("%d", module.Commit.Timestamp), + } + + tableData = append(tableData, row) + } + + return cmd.OutputTable(tableData, true) +} + +type version struct { + ID string `json:"id" graphql:"id"` + Commit struct { + AuthorLogin string `json:"authorLogin" graphql:"authorLogin"` + AuthorName string `json:"authorName" graphql:"authorName"` + Hash string `json:"hash" graphql:"hash"` + Message string `json:"message" graphql:"message"` + Timestamp int `json:"timestamp" graphql:"timestamp"` + URL string `json:"url" graphql:"url"` + } `json:"commit" graphql:"commit"` + DownloadLink interface{} `json:"downloadLink" graphql:"downloadLink"` + Number string `json:"number" graphql:"number"` + SourceURL string `json:"sourceURL" graphql:"sourceURL"` + State string `json:"state" graphql:"state"` + VersionCount int `json:"versionCount" graphql:"versionCount"` + Yanked bool `json:"yanked" graphql:"yanked"` +}