Skip to content

Commit

Permalink
Add get promotions command to CLI (#790)
Browse files Browse the repository at this point in the history
Signed-off-by: Sunghoon Kang <[email protected]>
Co-authored-by: Kent Rancourt <[email protected]>
  • Loading branch information
devholic and krancour authored Sep 19, 2023
1 parent a5a1ec1 commit 9203c78
Show file tree
Hide file tree
Showing 7 changed files with 355 additions and 230 deletions.
160 changes: 35 additions & 125 deletions internal/cli/get/get.go
Original file line number Diff line number Diff line change
@@ -1,162 +1,72 @@
package get

import (
"strings"
"time"

"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/exp/slices"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/duration"
"k8s.io/cli-runtime/pkg/printers"
"k8s.io/utils/pointer"

kargoapi "github.com/akuity/kargo/api/v1alpha1"
"github.com/akuity/kargo/internal/cli/client"
"github.com/akuity/kargo/internal/cli/option"
)

func NewCommand(opt *option.Option) *cobra.Command {
cmd := &cobra.Command{
Use: "get [--project=project] (RESOURCE) [NAME]...",
Use: "get (RESOURCE) [NAME]...",
Short: "Display one or many resources",
Args: cobra.MinimumNArgs(1),
Example: `
# List all projects
kargo get projects
# List all stages in the project
kargo get --project= stages
kargo get stages --project=my-project
# List all stages in JSON output format
kargo get stages my-project -o json
# List all promotions for the given stage
kargo get promotions --project=my-project --stage=my-stage
`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
kargoSvcCli, err := client.GetClientFromConfig(ctx, opt)
if err != nil {
return errors.New("get client from config")
}

resource := strings.ToLower(strings.TrimSpace(args[0]))
if resource == "" {
return errors.New("resource is required")
}

var resErr error
res := &metav1.List{
TypeMeta: metav1.TypeMeta{
APIVersion: metav1.Unversioned.String(),
Kind: "List",
},
}
switch resource {
case "project", "projects":
names := slices.Compact(args[1:])
filter, err := filterProjects(ctx, kargoSvcCli)
if err != nil {
return err
}

var projects []runtime.Object
projects, resErr = filter(names...)
res.Items = make([]runtime.RawExtension, 0, len(projects))
for _, project := range projects {
res.Items = append(res.Items, runtime.RawExtension{Object: project})
}
if len(names) == 1 {
if len(res.Items) == 1 {
_ = printResult(opt, res.Items[0].Object)
}
return resErr
}
case "stage", "stages":
project := opt.Project.OrElse("")
if project == "" {
return errors.New("project is required")
}
names := slices.Compact(args[1:])
filter, err := filterStages(ctx, kargoSvcCli, project)
if err != nil {
return err
}

var stages []runtime.Object
stages, resErr = filter(names...)
res.Items = make([]runtime.RawExtension, 0, len(stages))
for _, stage := range stages {
res.Items = append(res.Items, runtime.RawExtension{Object: stage})
}
if len(names) == 1 && len(res.Items) == 1 {
_ = printStageResult(opt, res.Items[0].Object)
} else {
_ = printStageResult(opt, res)
}
return resErr
default:
return errors.Errorf("unknown resource %q", resource)
}
_ = printResult(opt, res)
return resErr
},
}
option.OptionalProject(opt.Project)(cmd.Flags())
opt.PrintFlags.AddFlags(cmd)
// Subcommands
cmd.AddCommand(newGetProjectsCommand(opt))
cmd.AddCommand(newGetStagesCommand(opt))
cmd.AddCommand(newGetPromotionsCommand(opt))
return cmd
}

func printStageResult(opt *option.Option, res runtime.Object) error {
if pointer.StringDeref(opt.PrintFlags.OutputFormat, "") != "" {
return printResult(opt, res)
func printObjects[T runtime.Object](opt *option.Option, objects []T) error {
items := make([]runtime.RawExtension, len(objects))
for i, obj := range objects {
items[i] = runtime.RawExtension{Object: obj}
}
var items []runtime.RawExtension
if list, ok := res.(*metav1.List); ok {
items = list.Items
} else {
items = []runtime.RawExtension{{Object: res}}
}
table := &metav1.Table{
ColumnDefinitions: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string"},
{Name: "Current Freight", Type: "string"},
{Name: "Health", Type: "string"},
{Name: "Age", Type: "string"},
list := &metav1.List{
TypeMeta: metav1.TypeMeta{
APIVersion: metav1.Unversioned.String(),
Kind: "List",
},
Rows: make([]metav1.TableRow, len(items)),
Items: items,
}
for i, item := range items {
// This func is only ever passed Stages
stage := item.Object.(*kargoapi.Stage) // nolint: forcetypeassert
var currentFreightID string
if stage.Status.CurrentFreight != nil {
currentFreightID = stage.Status.CurrentFreight.ID
}
var health string
if stage.Status.Health != nil {
health = string(stage.Status.Health.Status)

if pointer.StringDeref(opt.PrintFlags.OutputFormat, "") != "" {
printer, err := opt.PrintFlags.ToPrinter()
if err != nil {
return errors.Wrap(err, "new printer")
}
table.Rows[i] = metav1.TableRow{
Cells: []any{
stage.Name,
currentFreightID,
health,
duration.HumanDuration(time.Since(stage.CreationTimestamp.Time)),
},
Object: item,
if len(list.Items) == 1 {
return printer.PrintObj(list.Items[0].Object, opt.IOStreams.Out)
}
return printer.PrintObj(list, opt.IOStreams.Out)
}
return printers.NewTablePrinter(printers.PrintOptions{}).PrintObj(table, opt.IOStreams.Out)
}

func printResult(opt *option.Option, res runtime.Object) error {
if pointer.StringDeref(opt.PrintFlags.OutputFormat, "") == "" {
return printers.NewTablePrinter(printers.PrintOptions{}).PrintObj(res, opt.IOStreams.Out)
}
printer, err := opt.PrintFlags.ToPrinter()
if err != nil {
return errors.Wrap(err, "new printer")
var t T
switch any(t).(type) {
case *kargoapi.Stage:
table := newStageTable(list)
return printers.NewTablePrinter(printers.PrintOptions{}).PrintObj(table, opt.IOStreams.Out)
case *kargoapi.Promotion:
table := newPromotionTable(list)
return printers.NewTablePrinter(printers.PrintOptions{}).PrintObj(table, opt.IOStreams.Out)
default:
return printers.NewTablePrinter(printers.PrintOptions{}).PrintObj(list, opt.IOStreams.Out)
}
return printer.PrintObj(res, opt.IOStreams.Out)
}
52 changes: 0 additions & 52 deletions internal/cli/get/project.go

This file was deleted.

70 changes: 70 additions & 0 deletions internal/cli/get/projects.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package get

import (
goerrors "errors"

"connectrpc.com/connect"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/exp/slices"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

typesv1alpha1 "github.com/akuity/kargo/internal/api/types/v1alpha1"
"github.com/akuity/kargo/internal/cli/client"
"github.com/akuity/kargo/internal/cli/option"
v1alpha1 "github.com/akuity/kargo/pkg/api/service/v1alpha1"
)

func newGetProjectsCommand(opt *option.Option) *cobra.Command {
cmd := &cobra.Command{
Use: "projects [NAME...]",
Aliases: []string{"project"},
Short: "Display one or many projects",
Example: `
# List all projects
kargo get projects
# List all projects in JSON output format
kargo get projects -o json
`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

kargoSvcCli, err := client.GetClientFromConfig(ctx, opt)
if err != nil {
return errors.New("get client from config")
}
resp, err := kargoSvcCli.ListProjects(ctx, connect.NewRequest(&v1alpha1.ListProjectsRequest{}))
if err != nil {
return errors.Wrap(err, "list projects")
}

names := slices.Compact(args)
res := make([]*unstructured.Unstructured, 0, len(resp.Msg.GetProjects()))
var resErr error
if len(names) == 0 {
for _, p := range resp.Msg.GetProjects() {
res = append(res, typesv1alpha1.FromProjectProto(p))
}
} else {
projectsByName := make(map[string]*unstructured.Unstructured, len(resp.Msg.GetProjects()))
for _, p := range resp.Msg.GetProjects() {
projectsByName[p.GetName()] = typesv1alpha1.FromProjectProto(p)
}
for _, name := range names {
if promo, ok := projectsByName[name]; ok {
res = append(res, promo)
} else {
resErr = goerrors.Join(err, errors.Errorf("project %q not found", name))
}
}
}
if err := printObjects(opt, res); err != nil {
return err
}
return resErr
},
}
opt.PrintFlags.AddFlags(cmd)
return cmd
}
Loading

0 comments on commit 9203c78

Please sign in to comment.