diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 68da63f3e..f30616d03 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -8,7 +8,6 @@ import ( "github.com/pkg/errors" "github.com/akuity/kargo/internal/cli/config" - "github.com/akuity/kargo/internal/cli/option" ) func main() { @@ -21,11 +20,7 @@ func main() { } cfg = config.NewDefaultCLIConfig() } - cmd, err := NewRootCommand(cfg, option.NewOption(cfg), &rootState{}) - if err != nil { - fmt.Fprintln(os.Stderr, errors.Wrap(err, "new root command")) - os.Exit(1) - } + cmd := NewRootCommand(cfg) if err := cmd.ExecuteContext(ctx); err != nil { os.Exit(1) } diff --git a/cmd/cli/root.go b/cmd/cli/root.go index 5de067774..4fb02c2f9 100644 --- a/cmd/cli/root.go +++ b/cmd/cli/root.go @@ -1,21 +1,12 @@ package main import ( - "context" - "fmt" - "net" "os" - "github.com/pkg/errors" "github.com/spf13/cobra" cobracompletefig "github.com/withfig/autocomplete-tools/integrations/cobra" - "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" - "sigs.k8s.io/controller-runtime/pkg/client/config" - "github.com/akuity/kargo/internal/api" - apiconfig "github.com/akuity/kargo/internal/api/config" - "github.com/akuity/kargo/internal/api/kubernetes" "github.com/akuity/kargo/internal/cli/cmd/apply" "github.com/akuity/kargo/internal/cli/cmd/approve" cliconfigcmd "github.com/akuity/kargo/internal/cli/cmd/config" @@ -27,91 +18,42 @@ import ( "github.com/akuity/kargo/internal/cli/cmd/logout" "github.com/akuity/kargo/internal/cli/cmd/promote" "github.com/akuity/kargo/internal/cli/cmd/refresh" + "github.com/akuity/kargo/internal/cli/cmd/server" "github.com/akuity/kargo/internal/cli/cmd/update" "github.com/akuity/kargo/internal/cli/cmd/version" clicfg "github.com/akuity/kargo/internal/cli/config" - "github.com/akuity/kargo/internal/cli/option" + "github.com/akuity/kargo/internal/cli/io" ) -// rootState holds state used internally by the root command. -type rootState struct { - localServerListener net.Listener -} - -func NewRootCommand( - cfg clicfg.CLIConfig, - opt *option.Option, - rs *rootState, -) (*cobra.Command, error) { +func NewRootCommand(cfg clicfg.CLIConfig) *cobra.Command { cmd := &cobra.Command{ Use: "kargo", DisableAutoGenTag: true, SilenceUsage: true, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - ctx := buildRootContext(cmd.Context()) - - if opt.UseLocalServer { - restCfg, err := config.GetConfig() - if err != nil { - return errors.Wrap(err, "get REST config") - } - client, err := - kubernetes.NewClient(ctx, restCfg, kubernetes.ClientOptions{}) - if err != nil { - return errors.Wrap(err, "error creating Kubernetes client") - } - l, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - return errors.Wrap(err, "start local server") - } - rs.localServerListener = l - srv := api.NewServer( - apiconfig.ServerConfig{ - LocalMode: true, - }, - client, - client, - ) - go srv.Serve(ctx, l) // nolint: errcheck - opt.LocalServerAddress = fmt.Sprintf("http://%s", l.Addr()) - } - return nil - }, Run: func(cmd *cobra.Command, args []string) { cmd.HelpFunc()(cmd, args) }, - PersistentPostRunE: func(cmd *cobra.Command, args []string) error { - if rs.localServerListener != nil { - return rs.localServerListener.Close() - } - return nil - }, } - opt.IOStreams = &genericiooptions.IOStreams{ - In: cmd.InOrStdin(), - Out: os.Stdout, - ErrOut: os.Stderr, - } - scheme, err := option.NewScheme() - if err != nil { - return nil, err - } - opt.PrintFlags = genericclioptions.NewPrintFlags("").WithTypeSetter(scheme) + // Set up the IOStreams for the commands to use. + streams := genericiooptions.IOStreams{Out: os.Stdout, ErrOut: os.Stderr, In: os.Stdin} + io.SetIOStreams(cmd, streams) - cmd.AddCommand(apply.NewCommand(cfg, opt)) - cmd.AddCommand(approve.NewCommand(cfg, opt)) + // Register the subcommands. + cmd.AddCommand(apply.NewCommand(cfg, streams)) + cmd.AddCommand(approve.NewCommand(cfg)) cmd.AddCommand(cliconfigcmd.NewCommand(cfg)) - cmd.AddCommand(create.NewCommand(cfg, opt)) - cmd.AddCommand(delete.NewCommand(cfg, opt)) - cmd.AddCommand(get.NewCommand(cfg, opt)) - cmd.AddCommand(login.NewCommand(opt)) + cmd.AddCommand(create.NewCommand(cfg, streams)) + cmd.AddCommand(delete.NewCommand(cfg, streams)) + cmd.AddCommand(get.NewCommand(cfg, streams)) + cmd.AddCommand(login.NewCommand()) cmd.AddCommand(logout.NewCommand()) - cmd.AddCommand(refresh.NewCommand(cfg, opt)) - cmd.AddCommand(update.NewCommand(cfg, opt)) + cmd.AddCommand(refresh.NewCommand(cfg)) + cmd.AddCommand(update.NewCommand(cfg)) cmd.AddCommand(dashboard.NewCommand(cfg)) - cmd.AddCommand(promote.NewCommand(cfg, opt)) - cmd.AddCommand(version.NewCommand(cfg, opt)) + cmd.AddCommand(promote.NewCommand(cfg, streams)) + cmd.AddCommand(version.NewCommand(cfg, streams)) + cmd.AddCommand(server.NewCommand()) cmd.AddCommand( cobracompletefig.CreateCompletionSpecCommand( cobracompletefig.Opts{ @@ -119,10 +61,6 @@ func NewRootCommand( }, ), ) - return cmd, nil -} -func buildRootContext(ctx context.Context) context.Context { - // TODO: Inject console printer or logger - return ctx + return cmd } diff --git a/internal/cli/client/client.go b/internal/cli/client/client.go index 8550da977..cf8d05f5b 100644 --- a/internal/cli/client/client.go +++ b/internal/cli/client/client.go @@ -7,33 +7,39 @@ import ( "connectrpc.com/connect" "github.com/pkg/errors" + "github.com/spf13/pflag" "github.com/akuity/kargo/internal/cli/config" "github.com/akuity/kargo/internal/cli/option" "github.com/akuity/kargo/pkg/api/service/v1alpha1/svcv1alpha1connect" ) +type Options struct { + InsecureTLS bool +} + +// AddFlags adds the flags for the client options to the provided flag set. +func (o *Options) AddFlags(flags *pflag.FlagSet) { + option.InsecureTLS(flags, &o.InsecureTLS) +} + // GetClientFromConfig returns a new client for the Kargo API server located at // the address specified in local configuration, using credentials also -// specified in local configuration UNLESS the specified options indicates that -// the local server should be used instead. +// specified in the local configuration. func GetClientFromConfig( ctx context.Context, cfg config.CLIConfig, - opt *option.Option, + opts Options, ) ( svcv1alpha1connect.KargoServiceClient, error, ) { - if opt.UseLocalServer { - return GetClient(opt.LocalServerAddress, "", opt.InsecureTLS), nil - } if cfg.APIAddress == "" || cfg.BearerToken == "" { return nil, errors.New( "seems like you are not logged in; please use `kargo login` to authenticate", ) } - skipTLSVerify := opt.InsecureTLS || cfg.InsecureSkipTLSVerify + skipTLSVerify := opts.InsecureTLS || cfg.InsecureSkipTLSVerify cfg, err := newTokenRefresher().refreshToken(ctx, cfg, skipTLSVerify) if err != nil { return nil, errors.Wrap(err, "error refreshing token") diff --git a/internal/cli/cmd/apply/apply.go b/internal/cli/cmd/apply/apply.go index 3542e2be8..335277253 100644 --- a/internal/cli/cmd/apply/apply.go +++ b/internal/cli/cmd/apply/apply.go @@ -9,29 +9,35 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/types" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" - "k8s.io/utils/ptr" sigyaml "sigs.k8s.io/yaml" "github.com/akuity/kargo/internal/cli/client" "github.com/akuity/kargo/internal/cli/config" + "github.com/akuity/kargo/internal/cli/io" + "github.com/akuity/kargo/internal/cli/kubernetes" "github.com/akuity/kargo/internal/cli/option" "github.com/akuity/kargo/internal/yaml" kargosvcapi "github.com/akuity/kargo/pkg/api/service/v1alpha1" ) type applyOptions struct { - *option.Option - Config config.CLIConfig + genericiooptions.IOStreams + *genericclioptions.PrintFlags + + Config config.CLIConfig + ClientOptions client.Options Filenames []string } -func NewCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { +func NewCommand(cfg config.CLIConfig, streams genericiooptions.IOStreams) *cobra.Command { cmdOpts := &applyOptions{ - Option: opt, - Config: cfg, + Config: cfg, + IOStreams: streams, + PrintFlags: genericclioptions.NewPrintFlags("").WithTypeSetter(kubernetes.GetScheme()), } cmd := &cobra.Command{ @@ -54,6 +60,9 @@ kargo apply -f stages/ }, } + // Set the input/output streams for the command. + io.SetIOStreams(cmd, cmdOpts.IOStreams) + // Register the option flags on the command. cmdOpts.addFlags(cmd) @@ -62,13 +71,9 @@ kargo apply -f stages/ // addFlags adds the flags for the apply options to the provided command. func (o *applyOptions) addFlags(cmd *cobra.Command) { + o.ClientOptions.AddFlags(cmd.PersistentFlags()) o.PrintFlags.AddFlags(cmd) - // TODO: Factor out server flags to a higher level (root?) as they are - // common to almost all commands. - option.InsecureTLS(cmd.PersistentFlags(), o.Option) - option.LocalServer(cmd.PersistentFlags(), o.Option) - option.Filenames(cmd.Flags(), &o.Filenames, "Filename or directory to use to apply the resource(s)") if err := cmd.MarkFlagRequired(option.FilenameFlag); err != nil { @@ -101,15 +106,7 @@ func (o *applyOptions) run(ctx context.Context) error { return errors.Wrap(err, "read manifests") } - var printer printers.ResourcePrinter - if ptr.Deref(o.PrintFlags.OutputFormat, "") != "" { - printer, err = o.PrintFlags.ToPrinter() - if err != nil { - return errors.Wrap(err, "new printer") - } - } - - kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.Option) + kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.ClientOptions) if err != nil { return errors.Wrap(err, "get client from config") } @@ -140,41 +137,39 @@ func (o *applyOptions) run(ctx context.Context) error { } } + printer, err := o.toPrinter("created") + if err != nil { + return errors.Wrap(err, "new printer") + } + for _, res := range createdRes { var obj unstructured.Unstructured - if err := sigyaml.Unmarshal(res.CreatedResourceManifest, &obj); err != nil { + if err = sigyaml.Unmarshal(res.CreatedResourceManifest, &obj); err != nil { fmt.Fprintf(o.IOStreams.ErrOut, "%s", errors.Wrap(err, "Error: unmarshal created manifest")) continue } - if printer == nil { - name := types.NamespacedName{ - Namespace: obj.GetNamespace(), - Name: obj.GetName(), - }.String() - fmt.Fprintf(o.IOStreams.Out, "%s Created: %q\n", obj.GetKind(), name) - continue - } _ = printer.PrintObj(&obj, o.IOStreams.Out) } + printer, err = o.toPrinter("updated") + if err != nil { + return errors.Wrap(err, "new printer") + } + for _, res := range updatedRes { var obj unstructured.Unstructured - if err := sigyaml.Unmarshal(res.UpdatedResourceManifest, &obj); err != nil { + if err = sigyaml.Unmarshal(res.UpdatedResourceManifest, &obj); err != nil { fmt.Fprintf(o.IOStreams.ErrOut, "%s", errors.Wrap(err, "Error: unmarshal updated manifest")) continue } - if printer == nil { - name := types.NamespacedName{ - Namespace: obj.GetNamespace(), - Name: obj.GetName(), - }.String() - fmt.Fprintf(o.IOStreams.Out, "%s Updated: %q\n", obj.GetKind(), name) - continue - } _ = printer.PrintObj(&obj, o.IOStreams.Out) } - return goerrors.Join(errs...) } + +func (o *applyOptions) toPrinter(operation string) (printers.ResourcePrinter, error) { + o.PrintFlags.NamePrintFlags.Operation = operation + return o.PrintFlags.ToPrinter() +} diff --git a/internal/cli/cmd/approve/approve.go b/internal/cli/cmd/approve/approve.go index 60f776d83..7a28f3fce 100644 --- a/internal/cli/cmd/approve/approve.go +++ b/internal/cli/cmd/approve/approve.go @@ -15,17 +15,17 @@ import ( ) type approvalOptions struct { - *option.Option - Config config.CLIConfig + Config config.CLIConfig + ClientOptions client.Options + Project string FreightName string FreightAlias string Stage string } -func NewCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { +func NewCommand(cfg config.CLIConfig) *cobra.Command { cmdOpts := &approvalOptions{ - Option: opt, Config: cfg, } @@ -65,13 +65,10 @@ kargo approve --freight-alias=wonky-wombat --stage=qa // addFlags adds the flags for the approval options to the provided command. func (o *approvalOptions) addFlags(cmd *cobra.Command) { - // TODO: Factor out server flags to a higher level (root?) as they are - // common to almost all commands. - option.InsecureTLS(cmd.PersistentFlags(), o.Option) - option.LocalServer(cmd.PersistentFlags(), o.Option) + o.ClientOptions.AddFlags(cmd.PersistentFlags()) option.Project( - cmd.Flags(), &o.Project, o.Project, + cmd.Flags(), &o.Project, o.Config.Project, "The project the freight belongs to. If not set, the default project will be used.", ) option.Freight(cmd.Flags(), &o.FreightName, "The name of the freight to approve.") @@ -109,7 +106,7 @@ func (o *approvalOptions) validate() error { // run performs the approval of a freight based on the options. func (o *approvalOptions) run(ctx context.Context) error { - kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.Option) + kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.ClientOptions) if err != nil { return errors.Wrap(err, "get client from config") } diff --git a/internal/cli/cmd/create/create.go b/internal/cli/cmd/create/create.go index d1efc8cfe..66725000d 100644 --- a/internal/cli/cmd/create/create.go +++ b/internal/cli/cmd/create/create.go @@ -9,29 +9,34 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/types" - "k8s.io/cli-runtime/pkg/printers" - "k8s.io/utils/ptr" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/genericiooptions" sigyaml "sigs.k8s.io/yaml" "github.com/akuity/kargo/internal/cli/client" "github.com/akuity/kargo/internal/cli/config" + "github.com/akuity/kargo/internal/cli/io" + "github.com/akuity/kargo/internal/cli/kubernetes" "github.com/akuity/kargo/internal/cli/option" "github.com/akuity/kargo/internal/yaml" kargosvcapi "github.com/akuity/kargo/pkg/api/service/v1alpha1" ) type createOptions struct { - *option.Option - Config config.CLIConfig + genericiooptions.IOStreams + *genericclioptions.PrintFlags + + Config config.CLIConfig + ClientOptions client.Options Filenames []string } -func NewCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { +func NewCommand(cfg config.CLIConfig, streams genericiooptions.IOStreams) *cobra.Command { cmdOpts := &createOptions{ - Option: opt, - Config: cfg, + Config: cfg, + IOStreams: streams, + PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(kubernetes.GetScheme()), } cmd := &cobra.Command{ @@ -57,21 +62,20 @@ kargo create project my-project // Register the option flags on the command. cmdOpts.addFlags(cmd) + // Set the input/output streams for the command. + io.SetIOStreams(cmd, cmdOpts.IOStreams) + // Register subcommands. - cmd.AddCommand(newProjectCommand(cfg, opt)) + cmd.AddCommand(newProjectCommand(cfg, streams)) return cmd } // addFlags adds the flags for the create options to the provided command. func (o *createOptions) addFlags(cmd *cobra.Command) { + o.ClientOptions.AddFlags(cmd.PersistentFlags()) o.PrintFlags.AddFlags(cmd) - // TODO: Factor out server flags to a higher level (root?) as they are - // common to almost all commands. - option.InsecureTLS(cmd.PersistentFlags(), o.Option) - option.LocalServer(cmd.PersistentFlags(), o.Option) - option.Filenames(cmd.Flags(), &o.Filenames, "Filename or directory to use to create resource(s).") if err := cmd.MarkFlagRequired(option.FilenameFlag); err != nil { @@ -104,18 +108,16 @@ func (o *createOptions) run(ctx context.Context) error { return errors.Wrap(err, "read manifests") } - var printer printers.ResourcePrinter - if ptr.Deref(o.PrintFlags.OutputFormat, "") != "" { - printer, err = o.PrintFlags.ToPrinter() - if err != nil { - return errors.Wrap(err, "new printer") - } + printer, err := o.PrintFlags.ToPrinter() + if err != nil { + return errors.Wrap(err, "create printer") } - kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.Option) + kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.ClientOptions) if err != nil { return errors.Wrap(err, "get client from config") } + resp, err := kargoSvcCli.CreateResource(ctx, connect.NewRequest(&kargosvcapi.CreateResourceRequest{ Manifest: manifest, })) @@ -141,14 +143,6 @@ func (o *createOptions) run(ctx context.Context) error { errors.Wrap(err, "Error: unmarshal created manifest")) continue } - if printer == nil { - name := types.NamespacedName{ - Namespace: obj.GetNamespace(), - Name: obj.GetName(), - }.String() - fmt.Fprintf(o.IOStreams.Out, "%s Created: %q\n", obj.GetKind(), name) - continue - } _ = printer.PrintObj(&obj, o.IOStreams.Out) } return goerrors.Join(createErrs...) diff --git a/internal/cli/cmd/create/project.go b/internal/cli/cmd/create/project.go index d5826b573..17858ff27 100644 --- a/internal/cli/cmd/create/project.go +++ b/internal/cli/cmd/create/project.go @@ -2,34 +2,40 @@ package create import ( "context" - "fmt" "strings" "connectrpc.com/connect" "github.com/pkg/errors" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/ptr" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/genericiooptions" sigyaml "sigs.k8s.io/yaml" kargoapi "github.com/akuity/kargo/api/v1alpha1" "github.com/akuity/kargo/internal/cli/client" "github.com/akuity/kargo/internal/cli/config" + "github.com/akuity/kargo/internal/cli/io" + "github.com/akuity/kargo/internal/cli/kubernetes" "github.com/akuity/kargo/internal/cli/option" kargosvcapi "github.com/akuity/kargo/pkg/api/service/v1alpha1" ) type createProjectOptions struct { - *option.Option - Config config.CLIConfig + genericiooptions.IOStreams + *genericclioptions.PrintFlags + + Config config.CLIConfig + ClientOptions client.Options Name string } -func newProjectCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { +func newProjectCommand(cfg config.CLIConfig, streams genericiooptions.IOStreams) *cobra.Command { cmdOpts := &createProjectOptions{ - Option: opt, - Config: cfg, + Config: cfg, + IOStreams: streams, + PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(kubernetes.GetScheme()), } cmd := &cobra.Command{ @@ -54,11 +60,15 @@ kargo create project my-project // Register the option flags on the command. cmdOpts.addFlags(cmd) + // Set the input/output streams for the command. + io.SetIOStreams(cmd, cmdOpts.IOStreams) + return cmd } // addFlags adds the flags for the create project options to the provided command. func (o *createProjectOptions) addFlags(cmd *cobra.Command) { + o.ClientOptions.AddFlags(cmd.PersistentFlags()) o.PrintFlags.AddFlags(cmd) } @@ -78,7 +88,7 @@ func (o *createProjectOptions) validate() error { // run creates a project using the provided options. func (o *createProjectOptions) run(ctx context.Context) error { - kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.Option) + kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.ClientOptions) if err != nil { return errors.Wrap(err, "get client from config") } @@ -115,11 +125,6 @@ func (o *createProjectOptions) run(ctx context.Context) error { return errors.Wrap(err, "unmarshal project") } - if ptr.Deref(o.PrintFlags.OutputFormat, "") == "" { - _, _ = fmt.Fprintf(o.IOStreams.Out, "Project Created: %q\n", o.Name) - return nil - } - printer, err := o.PrintFlags.ToPrinter() if err != nil { return errors.Wrap(err, "new printer") diff --git a/internal/cli/cmd/delete/delete.go b/internal/cli/cmd/delete/delete.go index 443c384a4..ff6460e47 100644 --- a/internal/cli/cmd/delete/delete.go +++ b/internal/cli/cmd/delete/delete.go @@ -4,33 +4,39 @@ import ( "context" goerrors "errors" "fmt" - "strings" "connectrpc.com/connect" "github.com/pkg/errors" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/types" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/genericiooptions" sigyaml "sigs.k8s.io/yaml" "github.com/akuity/kargo/internal/cli/client" "github.com/akuity/kargo/internal/cli/config" + "github.com/akuity/kargo/internal/cli/io" + "github.com/akuity/kargo/internal/cli/kubernetes" "github.com/akuity/kargo/internal/cli/option" "github.com/akuity/kargo/internal/yaml" kargosvcapi "github.com/akuity/kargo/pkg/api/service/v1alpha1" ) type deleteOptions struct { - *option.Option - Config config.CLIConfig + genericiooptions.IOStreams + *genericclioptions.PrintFlags + + Config config.CLIConfig + ClientOptions client.Options Filenames []string } -func NewCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { +func NewCommand(cfg config.CLIConfig, streams genericiooptions.IOStreams) *cobra.Command { cmdOpts := &deleteOptions{ - Option: opt, - Config: cfg, + Config: cfg, + IOStreams: streams, + PrintFlags: genericclioptions.NewPrintFlags("deleted").WithTypeSetter(kubernetes.GetScheme()), } cmd := &cobra.Command{ @@ -65,23 +71,22 @@ kargo delete warehouse --project=my-project my-warehouse // Register the option flags on the command. cmdOpts.addFlags(cmd) + // Set the input/output streams for the command. + io.SetIOStreams(cmd, cmdOpts.IOStreams) + // Register subcommands. - cmd.AddCommand(newProjectCommand(cfg, opt)) - cmd.AddCommand(newStageCommand(cfg, opt)) - cmd.AddCommand(newWarehouseCommand(cfg, opt)) + cmd.AddCommand(newProjectCommand(cfg, streams)) + cmd.AddCommand(newStageCommand(cfg, streams)) + cmd.AddCommand(newWarehouseCommand(cfg, streams)) return cmd } // addFlags adds the flags for the delete options to the provided command. func (o *deleteOptions) addFlags(cmd *cobra.Command) { + o.ClientOptions.AddFlags(cmd.PersistentFlags()) o.PrintFlags.AddFlags(cmd) - // TODO: Factor out server flags to a higher level (root?) as they are - // common to almost all commands. - option.InsecureTLS(cmd.PersistentFlags(), o.Option) - option.LocalServer(cmd.PersistentFlags(), o.Option) - option.Filenames(cmd.Flags(), &o.Filenames, "Filename or directory to use to delete resource(s).") if err := cmd.MarkFlagRequired(option.FilenameFlag); err != nil { @@ -114,7 +119,7 @@ func (o *deleteOptions) run(ctx context.Context) error { return errors.Wrap(err, "read manifests") } - kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.Option) + kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.ClientOptions) if err != nil { return errors.Wrap(err, "get client from config") } @@ -137,6 +142,12 @@ func (o *deleteOptions) run(ctx context.Context) error { deleteErrs = append(deleteErrs, errors.New(typedRes.Error)) } } + + printer, err := o.PrintFlags.ToPrinter() + if err != nil { + return errors.Wrap(err, "create printer") + } + for _, r := range successRes { var obj unstructured.Unstructured if err := sigyaml.Unmarshal(r.DeletedResourceManifest, &obj); err != nil { @@ -144,11 +155,7 @@ func (o *deleteOptions) run(ctx context.Context) error { errors.Wrap(err, "Error: unmarshal deleted manifest")) continue } - name := strings.TrimLeft(types.NamespacedName{ - Namespace: obj.GetNamespace(), - Name: obj.GetName(), - }.String(), "/") - fmt.Fprintf(o.IOStreams.Out, "%s Deleted: %q\n", obj.GetKind(), name) + _ = printer.PrintObj(&obj, o.IOStreams.Out) } return goerrors.Join(deleteErrs...) } diff --git a/internal/cli/cmd/delete/project.go b/internal/cli/cmd/delete/project.go index 567e870b0..c27556d8c 100644 --- a/internal/cli/cmd/delete/project.go +++ b/internal/cli/cmd/delete/project.go @@ -3,30 +3,39 @@ package delete import ( "context" goerrors "errors" - "fmt" "slices" "connectrpc.com/connect" "github.com/pkg/errors" "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/genericiooptions" + kargoapi "github.com/akuity/kargo/api/v1alpha1" "github.com/akuity/kargo/internal/cli/client" "github.com/akuity/kargo/internal/cli/config" + "github.com/akuity/kargo/internal/cli/io" + "github.com/akuity/kargo/internal/cli/kubernetes" "github.com/akuity/kargo/internal/cli/option" v1alpha1 "github.com/akuity/kargo/pkg/api/service/v1alpha1" ) type deleteProjectOptions struct { - *option.Option - Config config.CLIConfig + genericiooptions.IOStreams + *genericclioptions.PrintFlags + + Config config.CLIConfig + ClientOptions client.Options Names []string } -func newProjectCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { +func newProjectCommand(cfg config.CLIConfig, streams genericiooptions.IOStreams) *cobra.Command { cmdOpts := &deleteProjectOptions{ - Option: opt, - Config: cfg, + Config: cfg, + IOStreams: streams, + PrintFlags: genericclioptions.NewPrintFlags("deleted").WithTypeSetter(kubernetes.GetScheme()), } cmd := &cobra.Command{ @@ -54,12 +63,16 @@ kargo delete project my-project1 my-project2 // Register the option flags on the command. cmdOpts.addFlags(cmd) + // Set the input/output streams for the command. + io.SetIOStreams(cmd, cmdOpts.IOStreams) + return cmd } // addFlags adds the flags for the delete project options to the provided // command. func (o *deleteProjectOptions) addFlags(cmd *cobra.Command) { + o.ClientOptions.AddFlags(cmd.PersistentFlags()) o.PrintFlags.AddFlags(cmd) } @@ -79,11 +92,16 @@ func (o *deleteProjectOptions) validate() error { // run removes the project(s) based on the options. func (o *deleteProjectOptions) run(ctx context.Context) error { - kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.Option) + kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.ClientOptions) if err != nil { return errors.Wrap(err, "get client from config") } + printer, err := o.PrintFlags.ToPrinter() + if err != nil { + return errors.Wrap(err, "create printer") + } + var resErr error for _, name := range o.Names { if _, err := kargoSvcCli.DeleteProject(ctx, connect.NewRequest(&v1alpha1.DeleteProjectRequest{ @@ -92,7 +110,11 @@ func (o *deleteProjectOptions) run(ctx context.Context) error { resErr = goerrors.Join(resErr, errors.Wrap(err, "Error")) continue } - _, _ = fmt.Fprintf(o.IOStreams.Out, "Project Deleted: %q\n", name) + _ = printer.PrintObj(&kargoapi.Project{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + }, o.IOStreams.Out) } return resErr } diff --git a/internal/cli/cmd/delete/stage.go b/internal/cli/cmd/delete/stage.go index 05fde5200..1a8234d50 100644 --- a/internal/cli/cmd/delete/stage.go +++ b/internal/cli/cmd/delete/stage.go @@ -3,30 +3,40 @@ package delete import ( "context" goerrors "errors" - "fmt" "slices" "connectrpc.com/connect" "github.com/pkg/errors" "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/genericiooptions" + kargoapi "github.com/akuity/kargo/api/v1alpha1" "github.com/akuity/kargo/internal/cli/client" "github.com/akuity/kargo/internal/cli/config" + "github.com/akuity/kargo/internal/cli/io" + "github.com/akuity/kargo/internal/cli/kubernetes" "github.com/akuity/kargo/internal/cli/option" v1alpha1 "github.com/akuity/kargo/pkg/api/service/v1alpha1" ) type deleteStageOptions struct { - *option.Option - Config config.CLIConfig + genericiooptions.IOStreams + *genericclioptions.PrintFlags - Names []string + Config config.CLIConfig + ClientOptions client.Options + + Project string + Names []string } -func newStageCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { +func newStageCommand(cfg config.CLIConfig, streams genericiooptions.IOStreams) *cobra.Command { cmdOpts := &deleteStageOptions{ - Option: opt, - Config: cfg, + Config: cfg, + IOStreams: streams, + PrintFlags: genericclioptions.NewPrintFlags("deleted").WithTypeSetter(kubernetes.GetScheme()), } cmd := &cobra.Command{ @@ -58,14 +68,18 @@ kargo delete stage my-stage // Register the option flags on the command. cmdOpts.addFlags(cmd) + // Set the input/output streams for the command. + io.SetIOStreams(cmd, cmdOpts.IOStreams) + return cmd } // addFlags adds the flags for the delete stage options to the provided command. func (o *deleteStageOptions) addFlags(cmd *cobra.Command) { + o.ClientOptions.AddFlags(cmd.PersistentFlags()) o.PrintFlags.AddFlags(cmd) - option.Project(cmd.Flags(), &o.Project, o.Project, + option.Project(cmd.Flags(), &o.Project, o.Config.Project, "The Project for which to delete Stages. If not set, the default project will be used.") } @@ -92,11 +106,16 @@ func (o *deleteStageOptions) validate() error { // run removes the stage(s) from the project based on the options. func (o *deleteStageOptions) run(ctx context.Context) error { - kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.Option) + kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.ClientOptions) if err != nil { return errors.Wrap(err, "get client from config") } + printer, err := o.PrintFlags.ToPrinter() + if err != nil { + return errors.Wrap(err, "create printer") + } + var resErr error for _, name := range o.Names { if _, err := kargoSvcCli.DeleteStage(ctx, connect.NewRequest(&v1alpha1.DeleteStageRequest{ @@ -106,7 +125,12 @@ func (o *deleteStageOptions) run(ctx context.Context) error { resErr = goerrors.Join(resErr, errors.Wrap(err, "Error")) continue } - _, _ = fmt.Fprintf(o.IOStreams.Out, "Stage Deleted: %q\n", name) + _ = printer.PrintObj(&kargoapi.Stage{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: o.Project, + }, + }, o.IOStreams.Out) } return resErr } diff --git a/internal/cli/cmd/delete/warehouse.go b/internal/cli/cmd/delete/warehouse.go index e2533b1fa..c1e8b3d0e 100644 --- a/internal/cli/cmd/delete/warehouse.go +++ b/internal/cli/cmd/delete/warehouse.go @@ -3,13 +3,17 @@ package delete import ( "context" goerrors "errors" - "fmt" "slices" "connectrpc.com/connect" "github.com/pkg/errors" "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/genericiooptions" + kargoapi "github.com/akuity/kargo/api/v1alpha1" "github.com/akuity/kargo/internal/cli/client" "github.com/akuity/kargo/internal/cli/config" "github.com/akuity/kargo/internal/cli/option" @@ -17,16 +21,21 @@ import ( ) type deleteWarehouseOptions struct { - *option.Option - Config config.CLIConfig + genericiooptions.IOStreams + *genericclioptions.PrintFlags - Names []string + Config config.CLIConfig + ClientOptions client.Options + + Project string + Names []string } -func newWarehouseCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { +func newWarehouseCommand(cfg config.CLIConfig, streams genericiooptions.IOStreams) *cobra.Command { cmdOpts := &deleteWarehouseOptions{ - Option: opt, - Config: cfg, + Config: cfg, + IOStreams: streams, + PrintFlags: genericclioptions.NewPrintFlags("deleted").WithTypeSetter(runtime.NewScheme()), } cmd := &cobra.Command{ @@ -64,9 +73,10 @@ kargo delete warehouse my-warehouse // addFlags adds the flags for the delete warehouse options to the provided // command. func (o *deleteWarehouseOptions) addFlags(cmd *cobra.Command) { + o.ClientOptions.AddFlags(cmd.PersistentFlags()) o.PrintFlags.AddFlags(cmd) - option.Project(cmd.Flags(), &o.Project, o.Project, + option.Project(cmd.Flags(), &o.Project, o.Config.Project, "The Project for which to delete Warehouses. If not set, the default project will be used.") } @@ -93,11 +103,16 @@ func (o *deleteWarehouseOptions) validate() error { // run removes the warehouse(s) based on the options. func (o *deleteWarehouseOptions) run(ctx context.Context) error { - kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.Option) + kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.ClientOptions) if err != nil { return errors.Wrap(err, "get client from config") } + printer, err := o.PrintFlags.ToPrinter() + if err != nil { + return errors.Wrap(err, "create printer") + } + var resErr error for _, name := range o.Names { if _, err := kargoSvcCli.DeleteWarehouse( @@ -112,7 +127,12 @@ func (o *deleteWarehouseOptions) run(ctx context.Context) error { resErr = goerrors.Join(resErr, errors.Wrap(err, "Error")) continue } - _, _ = fmt.Fprintf(o.IOStreams.Out, "Warehouse Deleted: %q\n", name) + _ = printer.PrintObj(&kargoapi.Warehouse{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: o.Project, + }, + }, o.IOStreams.Out) } return resErr } diff --git a/internal/cli/cmd/get/freight.go b/internal/cli/cmd/get/freight.go index 1df046f5e..e0c4ad9f5 100644 --- a/internal/cli/cmd/get/freight.go +++ b/internal/cli/cmd/get/freight.go @@ -10,27 +10,36 @@ import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/duration" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/genericiooptions" kargoapi "github.com/akuity/kargo/api/v1alpha1" typesv1alpha1 "github.com/akuity/kargo/internal/api/types/v1alpha1" "github.com/akuity/kargo/internal/cli/client" "github.com/akuity/kargo/internal/cli/config" + "github.com/akuity/kargo/internal/cli/io" + "github.com/akuity/kargo/internal/cli/kubernetes" "github.com/akuity/kargo/internal/cli/option" v1alpha1 "github.com/akuity/kargo/pkg/api/service/v1alpha1" ) type getFreightOptions struct { - *option.Option - Config config.CLIConfig + genericiooptions.IOStreams + *genericclioptions.PrintFlags + Config config.CLIConfig + ClientOptions client.Options + + Project string Names []string Aliases []string } -func newGetFreightCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { +func newGetFreightCommand(cfg config.CLIConfig, streams genericiooptions.IOStreams) *cobra.Command { cmdOpts := &getFreightOptions{ - Option: opt, - Config: cfg, + Config: cfg, + IOStreams: streams, + PrintFlags: genericclioptions.NewPrintFlags("").WithTypeSetter(kubernetes.GetScheme()), } cmd := &cobra.Command{ @@ -62,7 +71,7 @@ kargo get freight --name=abc1234 kargo config set-project my-project kargo get freight --alias=wonky-wombat `, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { if err := cmdOpts.validate(); err != nil { return err } @@ -74,15 +83,19 @@ kargo get freight --alias=wonky-wombat // Register the option flags on the command. cmdOpts.addFlags(cmd) + // Set the input/output streams for the command. + io.SetIOStreams(cmd, cmdOpts.IOStreams) + return cmd } // addFlags adds the flags for the get freight options to the provided command. func (o *getFreightOptions) addFlags(cmd *cobra.Command) { + o.ClientOptions.AddFlags(cmd.PersistentFlags()) o.PrintFlags.AddFlags(cmd) option.Project( - cmd.Flags(), &o.Project, o.Project, + cmd.Flags(), &o.Project, o.Config.Project, "The project for which to get freight. If not set, the default project will be used.", ) option.Names(cmd.Flags(), &o.Names, "The name of a piece of freight to get.") @@ -102,13 +115,12 @@ func (o *getFreightOptions) validate() error { // run gets the freight from the server and prints it to the console. func (o *getFreightOptions) run(ctx context.Context) error { - kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.Option) + kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.ClientOptions) if err != nil { return errors.Wrap(err, "get client from config") } if len(o.Names) == 0 && len(o.Aliases) == 0 { - var resp *connect.Response[v1alpha1.QueryFreightResponse] if resp, err = kargoSvcCli.QueryFreight( ctx, @@ -120,6 +132,7 @@ func (o *getFreightOptions) run(ctx context.Context) error { ); err != nil { return errors.Wrap(err, "query freight") } + // We didn't specify any groupBy, so there should be one group with an // empty key freight := resp.Msg.GetGroups()[""] @@ -127,10 +140,10 @@ func (o *getFreightOptions) run(ctx context.Context) error { for _, f := range freight.Freight { res = append(res, typesv1alpha1.FromFreightProto(f)) } - return printObjects(o.Option, res) + return printObjects(res, o.PrintFlags, o.IOStreams) } - freight := make([]*kargoapi.Freight, 0, len(o.Names)+len(o.Aliases)) + res := make([]*kargoapi.Freight, 0, len(o.Names)+len(o.Aliases)) errs := make([]error, 0, len(o.Names)) for _, name := range o.Names { var resp *connect.Response[v1alpha1.GetFreightResponse] @@ -146,7 +159,7 @@ func (o *getFreightOptions) run(ctx context.Context) error { errs = append(errs, errors.Wrapf(err, "get freight %s", name)) continue } - freight = append(freight, typesv1alpha1.FromFreightProto(resp.Msg.GetFreight())) + res = append(res, typesv1alpha1.FromFreightProto(resp.Msg.GetFreight())) } for _, alias := range o.Aliases { var resp *connect.Response[v1alpha1.GetFreightResponse] @@ -162,17 +175,12 @@ func (o *getFreightOptions) run(ctx context.Context) error { errs = append(errs, errors.Wrapf(err, "get freight %s", alias)) continue } - freight = append(freight, typesv1alpha1.FromFreightProto(resp.Msg.GetFreight())) + res = append(res, typesv1alpha1.FromFreightProto(resp.Msg.GetFreight())) } - if err = printObjects(o.Option, freight); err != nil { - return errors.Wrap(err, "print stages") + if err = printObjects(res, o.PrintFlags, o.IOStreams); err != nil { + return errors.Wrap(err, "print freight") } - - if len(errs) == 0 { - return nil - } - return goerrors.Join(errs...) } diff --git a/internal/cli/cmd/get/get.go b/internal/cli/cmd/get/get.go index 58bac70d4..e82faf11e 100644 --- a/internal/cli/cmd/get/get.go +++ b/internal/cli/cmd/get/get.go @@ -5,15 +5,16 @@ import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" - "k8s.io/utils/ptr" kargoapi "github.com/akuity/kargo/api/v1alpha1" "github.com/akuity/kargo/internal/cli/config" "github.com/akuity/kargo/internal/cli/option" ) -func NewCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { +func NewCommand(cfg config.CLIConfig, streams genericiooptions.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "get TYPE [NAME ...]", Short: "Display one or many resources", @@ -30,21 +31,20 @@ kargo get promotions --project=my-project --stage=my-stage `, } - // TODO: Factor out server flags to a higher level (root?) as they are - // common to almost all commands. - option.InsecureTLS(cmd.PersistentFlags(), opt) - option.LocalServer(cmd.PersistentFlags(), opt) - // Register subcommands. - cmd.AddCommand(newGetFreightCommand(cfg, opt)) - cmd.AddCommand(newGetProjectsCommand(cfg, opt)) - cmd.AddCommand(newGetPromotionsCommand(cfg, opt)) - cmd.AddCommand(newGetStagesCommand(cfg, opt)) - cmd.AddCommand(newGetWarehousesCommand(cfg, opt)) + cmd.AddCommand(newGetFreightCommand(cfg, streams)) + cmd.AddCommand(newGetProjectsCommand(cfg, streams)) + cmd.AddCommand(newGetPromotionsCommand(cfg, streams)) + cmd.AddCommand(newGetStagesCommand(cfg, streams)) + cmd.AddCommand(newGetWarehousesCommand(cfg, streams)) return cmd } -func printObjects[T runtime.Object](opt *option.Option, objects []T) error { +func printObjects[T runtime.Object]( + objects []T, + flags *genericclioptions.PrintFlags, + streams genericiooptions.IOStreams, +) error { items := make([]runtime.RawExtension, len(objects)) for i, obj := range objects { items[i] = runtime.RawExtension{Object: obj} @@ -57,15 +57,14 @@ func printObjects[T runtime.Object](opt *option.Option, objects []T) error { Items: items, } - if ptr.Deref(opt.PrintFlags.OutputFormat, "") != "" { - printer, err := opt.PrintFlags.ToPrinter() + if flags.OutputFlagSpecified != nil && flags.OutputFlagSpecified() { + printer, err := flags.ToPrinter() if err != nil { return errors.Wrap(err, "new printer") } if len(list.Items) == 1 { - return printer.PrintObj(list.Items[0].Object, opt.IOStreams.Out) + return printer.PrintObj(list.Items[0].Object, streams.Out) } - return printer.PrintObj(list, opt.IOStreams.Out) } var t T @@ -82,5 +81,5 @@ func printObjects[T runtime.Object](opt *option.Option, objects []T) error { default: printObj = list } - return printers.NewTablePrinter(printers.PrintOptions{}).PrintObj(printObj, opt.IOStreams.Out) + return printers.NewTablePrinter(printers.PrintOptions{}).PrintObj(printObj, streams.Out) } diff --git a/internal/cli/cmd/get/projects.go b/internal/cli/cmd/get/projects.go index ac8eace77..86af0bd57 100644 --- a/internal/cli/cmd/get/projects.go +++ b/internal/cli/cmd/get/projects.go @@ -11,26 +11,33 @@ import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/duration" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/genericiooptions" kargoapi "github.com/akuity/kargo/api/v1alpha1" typesv1alpha1 "github.com/akuity/kargo/internal/api/types/v1alpha1" "github.com/akuity/kargo/internal/cli/client" "github.com/akuity/kargo/internal/cli/config" - "github.com/akuity/kargo/internal/cli/option" + "github.com/akuity/kargo/internal/cli/io" + "github.com/akuity/kargo/internal/cli/kubernetes" v1alpha1 "github.com/akuity/kargo/pkg/api/service/v1alpha1" ) type getProjectsOptions struct { - *option.Option - Config config.CLIConfig + genericiooptions.IOStreams + *genericclioptions.PrintFlags + + Config config.CLIConfig + ClientOptions client.Options Names []string } -func newGetProjectsCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { +func newGetProjectsCommand(cfg config.CLIConfig, streams genericiooptions.IOStreams) *cobra.Command { cmdOpts := &getProjectsOptions{ - Option: opt, - Config: cfg, + Config: cfg, + IOStreams: streams, + PrintFlags: genericclioptions.NewPrintFlags("").WithTypeSetter(kubernetes.GetScheme()), } cmd := &cobra.Command{ @@ -57,11 +64,15 @@ kargo get project my-project // Register the option flags on the command. cmdOpts.addFlags(cmd) + // Set the input/output streams for the command. + io.SetIOStreams(cmd, cmdOpts.IOStreams) + return cmd } // addFlags adds the flags for the get projects options to the provided command. func (o *getProjectsOptions) addFlags(cmd *cobra.Command) { + o.ClientOptions.AddFlags(cmd.PersistentFlags()) o.PrintFlags.AddFlags(cmd) } @@ -72,13 +83,12 @@ func (o *getProjectsOptions) complete(args []string) { // run gets the projects from the server and prints them to the console. func (o *getProjectsOptions) run(ctx context.Context) error { - kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.Option) + kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.ClientOptions) if err != nil { return errors.Wrap(err, "get client from config") } if len(o.Names) == 0 { - var resp *connect.Response[v1alpha1.ListProjectsResponse] if resp, err = kargoSvcCli.ListProjects( ctx, @@ -86,12 +96,12 @@ func (o *getProjectsOptions) run(ctx context.Context) error { ); err != nil { return errors.Wrap(err, "list projects") } + res := make([]*kargoapi.Project, 0, len(resp.Msg.GetProjects())) for _, project := range resp.Msg.GetProjects() { res = append(res, typesv1alpha1.FromProjectProto(project)) } - return printObjects(o.Option, res) - + return printObjects(res, o.PrintFlags, o.IOStreams) } res := make([]*kargoapi.Project, 0, len(o.Names)) @@ -112,14 +122,9 @@ func (o *getProjectsOptions) run(ctx context.Context) error { res = append(res, typesv1alpha1.FromProjectProto(resp.Msg.GetProject())) } - if err = printObjects(o.Option, res); err != nil { - return errors.Wrap(err, "print credentials") - } - - if len(errs) == 0 { - return nil + if err = printObjects(res, o.PrintFlags, o.IOStreams); err != nil { + return errors.Wrap(err, "print projects") } - return goerrors.Join(errs...) } diff --git a/internal/cli/cmd/get/promotions.go b/internal/cli/cmd/get/promotions.go index 61470c4cf..c9c993552 100644 --- a/internal/cli/cmd/get/promotions.go +++ b/internal/cli/cmd/get/promotions.go @@ -11,27 +11,36 @@ import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/duration" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/genericiooptions" kargoapi "github.com/akuity/kargo/api/v1alpha1" typesv1alpha1 "github.com/akuity/kargo/internal/api/types/v1alpha1" "github.com/akuity/kargo/internal/cli/client" "github.com/akuity/kargo/internal/cli/config" + "github.com/akuity/kargo/internal/cli/io" + "github.com/akuity/kargo/internal/cli/kubernetes" "github.com/akuity/kargo/internal/cli/option" v1alpha1 "github.com/akuity/kargo/pkg/api/service/v1alpha1" ) type getPromotionsOptions struct { - *option.Option - Config config.CLIConfig + genericiooptions.IOStreams + *genericclioptions.PrintFlags - Stage string - Names []string + Config config.CLIConfig + ClientOptions client.Options + + Project string + Stage string + Names []string } -func newGetPromotionsCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { +func newGetPromotionsCommand(cfg config.CLIConfig, streams genericiooptions.IOStreams) *cobra.Command { cmdOpts := &getPromotionsOptions{ - Option: opt, - Config: cfg, + Config: cfg, + IOStreams: streams, + PrintFlags: genericclioptions.NewPrintFlags("").WithTypeSetter(kubernetes.GetScheme()), } cmd := &cobra.Command{ @@ -77,15 +86,19 @@ kargo get promotion abc1234 // Register the option flags on the command. cmdOpts.addFlags(cmd) + // Set the input/output streams for the command. + io.SetIOStreams(cmd, cmdOpts.IOStreams) + return cmd } // addFlags adds the flags for the get promotions options to the provided command. func (o *getPromotionsOptions) addFlags(cmd *cobra.Command) { + o.ClientOptions.AddFlags(cmd.PersistentFlags()) o.PrintFlags.AddFlags(cmd) option.Project( - cmd.Flags(), &o.Project, o.Project, + cmd.Flags(), &o.Project, o.Config.Project, "The project for which to list promotions. If not set, the default project will be used.", ) option.Stage( @@ -110,13 +123,12 @@ func (o *getPromotionsOptions) validate() error { // run gets the promotions from the server and prints them to the console. func (o *getPromotionsOptions) run(ctx context.Context) error { - kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.Option) + kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.ClientOptions) if err != nil { return errors.Wrap(err, "get client from config") } if len(o.Names) == 0 { - var resp *connect.Response[v1alpha1.ListPromotionsResponse] if resp, err = kargoSvcCli.ListPromotions( ctx, @@ -129,12 +141,12 @@ func (o *getPromotionsOptions) run(ctx context.Context) error { ); err != nil { return errors.Wrap(err, "list promotions") } + res := make([]*kargoapi.Promotion, 0, len(resp.Msg.GetPromotions())) for _, promotion := range resp.Msg.GetPromotions() { res = append(res, typesv1alpha1.FromPromotionProto(promotion)) } - return printObjects(o.Option, res) - + return printObjects(res, o.PrintFlags, o.IOStreams) } res := make([]*kargoapi.Promotion, 0, len(o.Names)) @@ -156,14 +168,9 @@ func (o *getPromotionsOptions) run(ctx context.Context) error { res = append(res, typesv1alpha1.FromPromotionProto(resp.Msg.GetPromotion())) } - if err = printObjects(o.Option, res); err != nil { + if err = printObjects(res, o.PrintFlags, o.IOStreams); err != nil { return errors.Wrap(err, "print promotions") } - - if len(errs) == 0 { - return nil - } - return goerrors.Join(errs...) } diff --git a/internal/cli/cmd/get/stages.go b/internal/cli/cmd/get/stages.go index b650a64be..c5090a247 100644 --- a/internal/cli/cmd/get/stages.go +++ b/internal/cli/cmd/get/stages.go @@ -11,26 +11,35 @@ import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/duration" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/genericiooptions" kargoapi "github.com/akuity/kargo/api/v1alpha1" typesv1alpha1 "github.com/akuity/kargo/internal/api/types/v1alpha1" "github.com/akuity/kargo/internal/cli/client" "github.com/akuity/kargo/internal/cli/config" + "github.com/akuity/kargo/internal/cli/io" + "github.com/akuity/kargo/internal/cli/kubernetes" "github.com/akuity/kargo/internal/cli/option" v1alpha1 "github.com/akuity/kargo/pkg/api/service/v1alpha1" ) type getStagesOptions struct { - *option.Option - Config config.CLIConfig + genericiooptions.IOStreams + *genericclioptions.PrintFlags - Names []string + Config config.CLIConfig + ClientOptions client.Options + + Project string + Names []string } -func newGetStagesCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { +func newGetStagesCommand(cfg config.CLIConfig, streams genericiooptions.IOStreams) *cobra.Command { cmdOpts := &getStagesOptions{ - Option: opt, - Config: cfg, + Config: cfg, + IOStreams: streams, + PrintFlags: genericclioptions.NewPrintFlags("").WithTypeSetter(kubernetes.GetScheme()), } cmd := &cobra.Command{ @@ -69,15 +78,19 @@ kargo get stage qa // Register the option flags on the command. cmdOpts.addFlags(cmd) + // Set the input/output streams for the command. + io.SetIOStreams(cmd, cmdOpts.IOStreams) + return cmd } // addFlags adds the flags for the get stages options to the provided command. func (o *getStagesOptions) addFlags(cmd *cobra.Command) { + o.ClientOptions.AddFlags(cmd.PersistentFlags()) o.PrintFlags.AddFlags(cmd) option.Project( - cmd.Flags(), &o.Project, o.Project, + cmd.Flags(), &o.Project, o.Config.Project, "The project for which to list stages. If not set, the default project will be used.", ) } @@ -98,13 +111,12 @@ func (o *getStagesOptions) validate() error { // run gets the stages from the server and prints them to the console. func (o *getStagesOptions) run(ctx context.Context) error { - kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.Option) + kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.ClientOptions) if err != nil { return errors.Wrap(err, "get client from config") } if len(o.Names) == 0 { - var resp *connect.Response[v1alpha1.ListStagesResponse] if resp, err = kargoSvcCli.ListStages( ctx, @@ -116,12 +128,12 @@ func (o *getStagesOptions) run(ctx context.Context) error { ); err != nil { return errors.Wrap(err, "list stages") } + res := make([]*kargoapi.Stage, 0, len(resp.Msg.GetStages())) for _, stage := range resp.Msg.GetStages() { res = append(res, typesv1alpha1.FromStageProto(stage)) } - return printObjects(o.Option, res) - + return printObjects(res, o.PrintFlags, o.IOStreams) } res := make([]*kargoapi.Stage, 0, len(o.Names)) @@ -143,14 +155,9 @@ func (o *getStagesOptions) run(ctx context.Context) error { res = append(res, typesv1alpha1.FromStageProto(resp.Msg.GetStage())) } - if err = printObjects(o.Option, res); err != nil { + if err = printObjects(res, o.PrintFlags, o.IOStreams); err != nil { return errors.Wrap(err, "print stages") } - - if len(errs) == 0 { - return nil - } - return goerrors.Join(errs...) } diff --git a/internal/cli/cmd/get/warehouse.go b/internal/cli/cmd/get/warehouse.go index c7c70f9b8..7d7ea8ef7 100644 --- a/internal/cli/cmd/get/warehouse.go +++ b/internal/cli/cmd/get/warehouse.go @@ -8,26 +8,38 @@ import ( "connectrpc.com/connect" "github.com/pkg/errors" "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/genericiooptions" kargoapi "github.com/akuity/kargo/api/v1alpha1" typesv1alpha1 "github.com/akuity/kargo/internal/api/types/v1alpha1" "github.com/akuity/kargo/internal/cli/client" "github.com/akuity/kargo/internal/cli/config" + "github.com/akuity/kargo/internal/cli/io" + "github.com/akuity/kargo/internal/cli/kubernetes" "github.com/akuity/kargo/internal/cli/option" v1alpha1 "github.com/akuity/kargo/pkg/api/service/v1alpha1" ) type getWarehousesOptions struct { - *option.Option - Config config.CLIConfig + genericiooptions.IOStreams + *genericclioptions.PrintFlags - Names []string + Config config.CLIConfig + ClientOptions client.Options + + Project string + Names []string } -func newGetWarehousesCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { +func newGetWarehousesCommand( + cfg config.CLIConfig, + streams genericiooptions.IOStreams, +) *cobra.Command { cmdOpts := &getWarehousesOptions{ - Option: opt, - Config: cfg, + Config: cfg, + IOStreams: streams, + PrintFlags: genericclioptions.NewPrintFlags("").WithTypeSetter(kubernetes.GetScheme()), } cmd := &cobra.Command{ @@ -66,16 +78,20 @@ kargo get warehouse my-warehouse // Register the option flags on the command. cmdOpts.addFlags(cmd) + // Set the input/output streams for the command. + io.SetIOStreams(cmd, cmdOpts.IOStreams) + return cmd } // addFlags adds the flags for the get warehouses options to the provided // command. func (o *getWarehousesOptions) addFlags(cmd *cobra.Command) { + o.ClientOptions.AddFlags(cmd.PersistentFlags()) o.PrintFlags.AddFlags(cmd) option.Project( - cmd.Flags(), &o.Project, o.Project, + cmd.Flags(), &o.Project, o.Config.Project, "The project for which to list Warehouses. If not set, the default project will be used.", ) } @@ -96,13 +112,12 @@ func (o *getWarehousesOptions) validate() error { // run gets the warehouses from the server and prints them to the console. func (o *getWarehousesOptions) run(ctx context.Context) error { - kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.Option) + kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.ClientOptions) if err != nil { return errors.Wrap(err, "get client from config") } if len(o.Names) == 0 { - var resp *connect.Response[v1alpha1.ListWarehousesResponse] if resp, err = kargoSvcCli.ListWarehouses( ctx, @@ -114,11 +129,12 @@ func (o *getWarehousesOptions) run(ctx context.Context) error { ); err != nil { return errors.Wrap(err, "list warehouses") } + res := make([]*kargoapi.Warehouse, 0, len(resp.Msg.GetWarehouses())) for _, warehouse := range resp.Msg.GetWarehouses() { res = append(res, typesv1alpha1.FromWarehouseProto(warehouse)) } - return printObjects(o.Option, res) + return printObjects(res, o.PrintFlags, o.IOStreams) } @@ -141,13 +157,8 @@ func (o *getWarehousesOptions) run(ctx context.Context) error { res = append(res, typesv1alpha1.FromWarehouseProto(resp.Msg.GetWarehouse())) } - if err = printObjects(o.Option, res); err != nil { + if err = printObjects(res, o.PrintFlags, o.IOStreams); err != nil { return errors.Wrap(err, "print warehouses") } - - if len(errs) == 0 { - return nil - } - return goerrors.Join(errs...) } diff --git a/internal/cli/cmd/login/login.go b/internal/cli/cmd/login/login.go index b657f10cc..9514f55ee 100644 --- a/internal/cli/cmd/login/login.go +++ b/internal/cli/cmd/login/login.go @@ -37,8 +37,7 @@ const defaultRandStringCharSet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRST var assets embed.FS type loginOptions struct { - *option.Option - + InsecureTLS bool UseAdmin bool UseKubeconfig bool UseSSO bool @@ -47,8 +46,8 @@ type loginOptions struct { ServerAddress string } -func NewCommand(opt *option.Option) *cobra.Command { - cmdOpts := &loginOptions{Option: opt} +func NewCommand() *cobra.Command { + cmdOpts := &loginOptions{} cmd := &cobra.Command{ Use: "login SERVER_ADDRESS (--admin | --kubeconfig | --sso)", @@ -86,7 +85,7 @@ kargo login https://kargo.example.com --kubeconfig --insecure-tls // addFlags adds the flags for the login options to the provided command. func (o *loginOptions) addFlags(cmd *cobra.Command) { - option.InsecureTLS(cmd.PersistentFlags(), o.Option) + option.InsecureTLS(cmd.PersistentFlags(), &o.InsecureTLS) cmd.Flags().BoolVar(&o.UseAdmin, "admin", false, "Log in as the Kargo admin user. If set, --kubeconfig and --sso must not be set.") diff --git a/internal/cli/cmd/promote/promote.go b/internal/cli/cmd/promote/promote.go index 273528d34..13c1d7773 100644 --- a/internal/cli/cmd/promote/promote.go +++ b/internal/cli/cmd/promote/promote.go @@ -8,29 +8,37 @@ import ( "connectrpc.com/connect" "github.com/pkg/errors" "github.com/spf13/cobra" - "k8s.io/utils/ptr" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/genericiooptions" typesv1alpha1 "github.com/akuity/kargo/internal/api/types/v1alpha1" "github.com/akuity/kargo/internal/cli/client" "github.com/akuity/kargo/internal/cli/config" + "github.com/akuity/kargo/internal/cli/io" + "github.com/akuity/kargo/internal/cli/kubernetes" "github.com/akuity/kargo/internal/cli/option" v1alpha1 "github.com/akuity/kargo/pkg/api/service/v1alpha1" ) type promotionOptions struct { - *option.Option - Config config.CLIConfig + genericiooptions.IOStreams + *genericclioptions.PrintFlags + Config config.CLIConfig + ClientOptions client.Options + + Project string FreightName string FreightAlias string Stage string SubscribersOf string } -func NewCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { +func NewCommand(cfg config.CLIConfig, streams genericiooptions.IOStreams) *cobra.Command { cmdOpts := &promotionOptions{ - Option: opt, - Config: cfg, + Config: cfg, + IOStreams: streams, + PrintFlags: genericclioptions.NewPrintFlags("promotion created").WithTypeSetter(kubernetes.GetScheme()), } cmd := &cobra.Command{ @@ -67,7 +75,7 @@ kargo promote --freight=abc123 --subscribers-of=qa kargo config set-project my-project kargo promote --freight-alias=wonky-wombat --subscribers-of=qas `, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { if err := cmdOpts.validate(); err != nil { return err } @@ -79,20 +87,19 @@ kargo promote --freight-alias=wonky-wombat --subscribers-of=qas // Register the option flags on the command. cmdOpts.addFlags(cmd) + // Set the input/output streams for the command. + io.SetIOStreams(cmd, cmdOpts.IOStreams) + return cmd } // addFlags adds the flags for the promotion options to the provided command. func (o *promotionOptions) addFlags(cmd *cobra.Command) { + o.ClientOptions.AddFlags(cmd.PersistentFlags()) o.PrintFlags.AddFlags(cmd) - // TODO: Factor out server flags to a higher level (root?) as they are - // common to almost all commands. - option.InsecureTLS(cmd.PersistentFlags(), o.Option) - option.LocalServer(cmd.PersistentFlags(), o.Option) - option.Project( - cmd.Flags(), &o.Project, o.Project, + cmd.Flags(), &o.Project, o.Config.Project, "The project the freight belongs to. If not set, the default project will be used.", ) option.Freight(cmd.Flags(), &o.FreightName, "The name of piece of freight to promote.") @@ -145,9 +152,14 @@ func (o *promotionOptions) validate() error { // run performs the promotion of the freight using the options. func (o *promotionOptions) run(ctx context.Context) error { - kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.Option) + kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.ClientOptions) if err != nil { - return err + return errors.Wrap(err, "get client from config") + } + + printer, err := o.PrintFlags.ToPrinter() + if err != nil { + return errors.Wrap(err, "new printer") } switch { @@ -166,15 +178,7 @@ func (o *promotionOptions) run(ctx context.Context) error { if err != nil { return errors.Wrap(err, "promote stage") } - if ptr.Deref(o.PrintFlags.OutputFormat, "") == "" { - fmt.Fprintf(o.IOStreams.Out, - "Promotion Created: %q\n", res.Msg.GetPromotion().GetMetadata().GetName()) - return nil - } - printer, err := o.PrintFlags.ToPrinter() - if err != nil { - return errors.Wrap(err, "new printer") - } + promo := typesv1alpha1.FromPromotionProto(res.Msg.GetPromotion()) _ = printer.PrintObj(promo, o.IOStreams.Out) return nil @@ -190,25 +194,10 @@ func (o *promotionOptions) run(ctx context.Context) error { }, ), ) - if ptr.Deref(o.PrintFlags.OutputFormat, "") == "" { - if res != nil && res.Msg != nil { - for _, p := range res.Msg.GetPromotions() { - fmt.Fprintf(o.IOStreams.Out, "Promotion Created: %q\n", *p.Metadata.Name) - } - } - if promoteErr != nil { - return errors.Wrap(promoteErr, "promote subscribers") - } - return nil - } - printer, printerErr := o.PrintFlags.ToPrinter() - if printerErr != nil { - return errors.Wrap(printerErr, "new printer") - } for _, p := range res.Msg.GetPromotions() { - kubeP := typesv1alpha1.FromPromotionProto(p) - _ = printer.PrintObj(kubeP, o.IOStreams.Out) + promo := typesv1alpha1.FromPromotionProto(p) + _ = printer.PrintObj(promo, o.IOStreams.Out) } return promoteErr } diff --git a/internal/cli/cmd/refresh/refresh.go b/internal/cli/cmd/refresh/refresh.go index 96918d825..26d44bb0f 100644 --- a/internal/cli/cmd/refresh/refresh.go +++ b/internal/cli/cmd/refresh/refresh.go @@ -22,15 +22,16 @@ const ( ) type refreshOptions struct { - *option.Option - Config config.CLIConfig + Config config.CLIConfig + ClientOptions client.Options + Project string ResourceType string Name string Wait bool } -func NewCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { +func NewCommand(cfg config.CLIConfig) *cobra.Command { cmd := &cobra.Command{ Use: "refresh TYPE NAME [--wait]", Short: "Refresh a stage or warehouse", @@ -45,20 +46,17 @@ kargo refresh stage --project=my-project my-stage } // Register subcommands. - cmd.AddCommand(newRefreshWarehouseCommand(cfg, opt)) - cmd.AddCommand(newRefreshStageCommand(cfg, opt)) + cmd.AddCommand(newRefreshWarehouseCommand(cfg)) + cmd.AddCommand(newRefreshStageCommand(cfg)) return cmd } // addFlags adds the flags for the refresh options to the provided command. func (o *refreshOptions) addFlags(cmd *cobra.Command) { - // TODO: Factor out server flags to a higher level (root?) as they are - // common to almost all commands. - option.InsecureTLS(cmd.PersistentFlags(), o.Option) - option.LocalServer(cmd.PersistentFlags(), o.Option) + o.ClientOptions.AddFlags(cmd.PersistentFlags()) - option.Project(cmd.Flags(), &o.Project, o.Project, + option.Project(cmd.Flags(), &o.Project, o.Config.Project, "The Project the resource belongs to. If not set, the default project will be used.") option.Wait(cmd.Flags(), &o.Wait, false, "Wait for the refresh to complete.") } @@ -92,9 +90,9 @@ func (o *refreshOptions) validate() error { // run performs the refresh operation based on the provided options. func (o *refreshOptions) run(ctx context.Context) error { - kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.Option) + kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.ClientOptions) if err != nil { - return err + return errors.Wrap(err, "get client from config") } switch o.ResourceType { diff --git a/internal/cli/cmd/refresh/stage.go b/internal/cli/cmd/refresh/stage.go index 05ea60e58..003cb8fa1 100644 --- a/internal/cli/cmd/refresh/stage.go +++ b/internal/cli/cmd/refresh/stage.go @@ -14,9 +14,8 @@ import ( "github.com/akuity/kargo/pkg/api/service/v1alpha1/svcv1alpha1connect" ) -func newRefreshStageCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { +func newRefreshStageCommand(cfg config.CLIConfig) *cobra.Command { cmdOpts := &refreshOptions{ - Option: opt, Config: cfg, } diff --git a/internal/cli/cmd/refresh/warehouse.go b/internal/cli/cmd/refresh/warehouse.go index 7f33e2805..113e99d54 100644 --- a/internal/cli/cmd/refresh/warehouse.go +++ b/internal/cli/cmd/refresh/warehouse.go @@ -14,9 +14,8 @@ import ( "github.com/akuity/kargo/pkg/api/service/v1alpha1/svcv1alpha1connect" ) -func newRefreshWarehouseCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { +func newRefreshWarehouseCommand(cfg config.CLIConfig) *cobra.Command { cmdOpts := &refreshOptions{ - Option: opt, Config: cfg, } diff --git a/internal/cli/cmd/server/server.go b/internal/cli/cmd/server/server.go new file mode 100644 index 000000000..24faec43d --- /dev/null +++ b/internal/cli/cmd/server/server.go @@ -0,0 +1,105 @@ +package server + +import ( + "context" + "net" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "sigs.k8s.io/controller-runtime/pkg/client/config" + + "github.com/akuity/kargo/internal/api" + apiconfig "github.com/akuity/kargo/internal/api/config" + "github.com/akuity/kargo/internal/api/kubernetes" + "github.com/akuity/kargo/internal/cli/option" +) + +type serverOptions struct { + address string +} + +func NewCommand() *cobra.Command { + cmdOpts := &serverOptions{} + + cmd := &cobra.Command{ + Use: "server", + Short: "Start a local Kargo API server", + Args: option.NoArgs, + Example: ` +# Start a local Kargo API server on a random port +kargo server + +# Start a local Kargo API server on a specific address +kargo server --address=127.0.0.1:3000 +`, + RunE: func(cmd *cobra.Command, _ []string) error { + if err := cmdOpts.validate(); err != nil { + return err + } + return cmdOpts.run(cmd.Context()) + }, + } + + // Register the option flags on the command. + cmdOpts.addFlags(cmd) + + return cmd +} + +// addFlags adds the flags for the server options to the provided command. +func (o *serverOptions) addFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.address, "address", "127.0.0.1:0", + "Address to bind the server to. Defaults to binding to a random port on localhost.") +} + +// validate performs validation of the options. If the options are invalid, an +// error is returned. +func (o *serverOptions) validate() error { + if o.address == "" { + return errors.New("address cannot be empty") + } + return nil +} + +// run starts a local server on the provided address. +func (o *serverOptions) run(ctx context.Context) error { + // TODO: This is at present incomplete, and is a placeholder for future work. + // + // - The server should be started with a Kubernetes client which does NOT + // make use of an authorization wrapper. + // - It should allow the user to visit the UI in their browser. + // - It should allow the user to interact with the API through `kargo` + // commands, but _without_ needing to authenticate, as the server is + // running locally using the user's kubeconfig. + // - It should properly handle signals and clean up after itself. + // + // xref: https://github.com/akuity/kargo/issues/1569 + + restCfg, err := config.GetConfig() + if err != nil { + return errors.Wrap(err, "get REST config") + } + + client, err := kubernetes.NewClient(ctx, restCfg, kubernetes.ClientOptions{}) + if err != nil { + return errors.Wrap(err, "error creating Kubernetes client") + } + + l, err := net.Listen("tcp", o.address) + if err != nil { + return errors.Wrap(err, "start local server") + } + defer l.Close() // nolint: errcheck + + srv := api.NewServer( + apiconfig.ServerConfig{ + LocalMode: true, + }, + client, + client, + ) + if err := srv.Serve(ctx, l); err != nil { + return errors.Wrap(err, "serve error") + } + return nil +} diff --git a/internal/cli/cmd/update/freight.go b/internal/cli/cmd/update/freight.go index b78d9cb73..768392a79 100644 --- a/internal/cli/cmd/update/freight.go +++ b/internal/cli/cmd/update/freight.go @@ -15,17 +15,17 @@ import ( ) type updateFreightAliasOptions struct { - *option.Option - Config config.CLIConfig + Config config.CLIConfig + ClientOptions client.Options + Project string Name string OldAlias string NewAlias string } -func newUpdateFreightAliasCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { +func newUpdateFreightAliasCommand(cfg config.CLIConfig) *cobra.Command { cmdOpts := &updateFreightAliasOptions{ - Option: opt, Config: cfg, } @@ -48,7 +48,7 @@ kargo update freight --name=abc123 --new-alias=frozen-fox kargo config set-project my-project kargo update freight --old-alias=wonky-wombat --new-alias=frozen-fox `, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { if err := cmdOpts.validate(); err != nil { return err } @@ -66,8 +66,10 @@ kargo update freight --old-alias=wonky-wombat --new-alias=frozen-fox // addFlags adds the flags for the update freight alias options to the provided // command. func (o *updateFreightAliasOptions) addFlags(cmd *cobra.Command) { + o.ClientOptions.AddFlags(cmd.PersistentFlags()) + option.Project( - cmd.Flags(), &o.Project, o.Project, + cmd.Flags(), &o.Project, o.Config.Project, "The project the freight belongs to. If not set, the default project will be used.", ) option.Name(cmd.Flags(), &o.Name, "The name of the freight to to be updated.") @@ -105,7 +107,7 @@ func (o *updateFreightAliasOptions) validate() error { // run updates the freight alias using the options. func (o *updateFreightAliasOptions) run(ctx context.Context) error { - kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.Option) + kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.ClientOptions) if err != nil { return errors.Wrap(err, "get client from config") } diff --git a/internal/cli/cmd/update/update.go b/internal/cli/cmd/update/update.go index a82c16b37..a580ac28a 100644 --- a/internal/cli/cmd/update/update.go +++ b/internal/cli/cmd/update/update.go @@ -7,7 +7,7 @@ import ( "github.com/akuity/kargo/internal/cli/option" ) -func NewCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { +func NewCommand(cfg config.CLIConfig) *cobra.Command { cmd := &cobra.Command{ Use: "update SUBCOMMAND", Short: "Update a resource", @@ -17,11 +17,9 @@ func NewCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { kargo update freight --project=my-project abc123 --alias=my-new-alias `, } - option.InsecureTLS(cmd.PersistentFlags(), opt) - option.LocalServer(cmd.PersistentFlags(), opt) // Register subcommands. - cmd.AddCommand(newUpdateFreightAliasCommand(cfg, opt)) + cmd.AddCommand(newUpdateFreightAliasCommand(cfg)) return cmd } diff --git a/internal/cli/cmd/version/version.go b/internal/cli/cmd/version/version.go index 6401e07ee..b0738d535 100644 --- a/internal/cli/cmd/version/version.go +++ b/internal/cli/cmd/version/version.go @@ -11,28 +11,35 @@ import ( "google.golang.org/protobuf/encoding/protojson" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/utils/ptr" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/genericiooptions" kargoapi "github.com/akuity/kargo/api/v1alpha1" typesv1alpha1 "github.com/akuity/kargo/internal/api/types/v1alpha1" "github.com/akuity/kargo/internal/cli/client" "github.com/akuity/kargo/internal/cli/config" + "github.com/akuity/kargo/internal/cli/io" + "github.com/akuity/kargo/internal/cli/kubernetes" "github.com/akuity/kargo/internal/cli/option" versionpkg "github.com/akuity/kargo/internal/version" svcv1alpha1 "github.com/akuity/kargo/pkg/api/service/v1alpha1" ) type versionOptions struct { - *option.Option - Config config.CLIConfig + genericiooptions.IOStreams + *genericclioptions.PrintFlags + + Config config.CLIConfig + ClientOptions client.Options ClientOnly bool } -func NewCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command { +func NewCommand(cfg config.CLIConfig, streams genericiooptions.IOStreams) *cobra.Command { cmdOpts := &versionOptions{ - Option: opt, - Config: cfg, + Config: cfg, + IOStreams: streams, + PrintFlags: genericclioptions.NewPrintFlags("").WithTypeSetter(kubernetes.GetScheme()), } cmd := &cobra.Command{ @@ -46,7 +53,7 @@ kargo version # Print the client version information only kargo version --client `, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { return cmdOpts.run(cmd.Context()) }, } @@ -54,37 +61,38 @@ kargo version --client // Register the option flags on the command. cmdOpts.addFlags(cmd) + // Set the input/output streams for the command. + io.SetIOStreams(cmd, cmdOpts.IOStreams) + return cmd } // addFlags adds the flags for the version options to the provided command. func (o *versionOptions) addFlags(cmd *cobra.Command) { + o.ClientOptions.AddFlags(cmd.PersistentFlags()) o.PrintFlags.AddFlags(cmd) - option.InsecureTLS(cmd.PersistentFlags(), o.Option) - option.LocalServer(cmd.PersistentFlags(), o.Option) - cmd.Flags().BoolVar(&o.ClientOnly, "client", o.ClientOnly, "If true, shows client version only (no server required)") } // run prints the client and server version information. func (o *versionOptions) run(ctx context.Context) error { - printToStdout := ptr.Deref(o.PrintFlags.OutputFormat, "") == "" + printToStdout := o.PrintFlags.OutputFlagSpecified == nil || !o.PrintFlags.OutputFlagSpecified() cliVersion := typesv1alpha1.ToVersionProto(versionpkg.GetVersion()) if printToStdout { - fmt.Println("Client Version:", cliVersion.GetVersion()) + _, _ = fmt.Fprintln(o.IOStreams.Out, "Client Version:", cliVersion.GetVersion()) } var serverVersion *svcv1alpha1.VersionInfo var serverErr error - if !o.UseLocalServer && !o.ClientOnly { - serverVersion, serverErr = getServerVersion(ctx, o.Config, o.Option) + if !o.ClientOnly { + serverVersion, serverErr = getServerVersion(ctx, o.Config, o.ClientOptions) } if printToStdout { if serverVersion != nil { - fmt.Println("Server Version:", serverVersion.GetVersion()) + _, _ = fmt.Fprintln(o.IOStreams.Out, "Server Version:", serverVersion.GetVersion()) } return serverErr } @@ -107,15 +115,20 @@ func (o *versionOptions) run(ctx context.Context) error { return serverErr } -func getServerVersion(ctx context.Context, cfg config.CLIConfig, opt *option.Option) (*svcv1alpha1.VersionInfo, error) { +func getServerVersion( + ctx context.Context, + cfg config.CLIConfig, + opts client.Options, +) (*svcv1alpha1.VersionInfo, error) { if cfg.APIAddress == "" || cfg.BearerToken == "" { return nil, nil } - kargoSvcCli, err := client.GetClientFromConfig(ctx, cfg, opt) + kargoSvcCli, err := client.GetClientFromConfig(ctx, cfg, opts) if err != nil { return nil, errors.Wrap(err, "get client from config") } + resp, err := kargoSvcCli.GetVersionInfo( ctx, connect.NewRequest(&svcv1alpha1.GetVersionInfoRequest{}), diff --git a/internal/cli/io/util.go b/internal/cli/io/util.go new file mode 100644 index 000000000..7a0ae05e6 --- /dev/null +++ b/internal/cli/io/util.go @@ -0,0 +1,13 @@ +package io + +import ( + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericiooptions" +) + +// SetIOStreams sets the input/output streams on the provided command. +func SetIOStreams(cmd *cobra.Command, streams genericiooptions.IOStreams) { + cmd.SetIn(streams.In) + cmd.SetOut(streams.Out) + cmd.SetErr(streams.ErrOut) +} diff --git a/internal/cli/kubernetes/scheme.go b/internal/cli/kubernetes/scheme.go new file mode 100644 index 000000000..6e3a8fcc3 --- /dev/null +++ b/internal/cli/kubernetes/scheme.go @@ -0,0 +1,20 @@ +package kubernetes + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + + kargoapi "github.com/akuity/kargo/api/v1alpha1" +) + +var scheme = runtime.NewScheme() + +func init() { + _ = corev1.AddToScheme(scheme) + _ = kargoapi.AddToScheme(scheme) +} + +// GetScheme returns a runtime.Scheme with the types of the Kargo API. +func GetScheme() *runtime.Scheme { + return scheme +} diff --git a/internal/cli/option/flag.go b/internal/cli/option/flag.go index fb6b41fe3..cb5d29267 100644 --- a/internal/cli/option/flag.go +++ b/internal/cli/option/flag.go @@ -21,6 +21,9 @@ const ( // FreightAliasFlag is the flag name for the freight-alias flag. FreightAliasFlag = "freight-alias" + // InsecureTLSFlag is the flag name for the insecure-tls flag. + InsecureTLSFlag = "insecure-skip-tls-verify" + // NameFlag is the flag name for the name flag. NameFlag = "name" @@ -70,12 +73,9 @@ func FreightAlias(fs *pflag.FlagSet, stage *string, usage string) { fs.StringVar(stage, FreightAliasFlag, "", usage) } -func InsecureTLS(fs *pflag.FlagSet, opt *Option) { - fs.BoolVar(&opt.InsecureTLS, "insecure-skip-tls-verify", false, "Skip TLS certificate verification") -} - -func LocalServer(fs *pflag.FlagSet, opt *Option) { - fs.BoolVar(&opt.UseLocalServer, "local-server", false, "Use local server") +// InsecureTLS adds the InsecureTLSFlag to the provided flag set. +func InsecureTLS(fs *pflag.FlagSet, insecure *bool) { + fs.BoolVar(insecure, InsecureTLSFlag, false, "Skip TLS certificate verification") } // Name adds the NameFlag to the provided flag set. diff --git a/internal/cli/option/option.go b/internal/cli/option/option.go index ca6587fdc..9acc529ad 100644 --- a/internal/cli/option/option.go +++ b/internal/cli/option/option.go @@ -3,45 +3,9 @@ package option import ( "fmt" - "github.com/pkg/errors" "github.com/spf13/cobra" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/cli-runtime/pkg/genericiooptions" - - kargoapi "github.com/akuity/kargo/api/v1alpha1" - "github.com/akuity/kargo/internal/cli/config" ) -type Option struct { - InsecureTLS bool - LocalServerAddress string - UseLocalServer bool - - Project string - - IOStreams *genericiooptions.IOStreams - PrintFlags *genericclioptions.PrintFlags -} - -func NewOption(cfg config.CLIConfig) *Option { - return &Option{ - Project: cfg.Project, - } -} - -func NewScheme() (*runtime.Scheme, error) { - scheme := runtime.NewScheme() - if err := corev1.AddToScheme(scheme); err != nil { - return nil, errors.Wrap(err, "add core v1 scheme") - } - if err := kargoapi.AddToScheme(scheme); err != nil { - return nil, errors.Wrap(err, "add kargo v1alpha1 scheme") - } - return scheme, nil -} - // ExactArgs is a wrapper around cobra.ExactArgs to additionally print usage string func ExactArgs(n int) cobra.PositionalArgs { exactArgs := cobra.ExactArgs(n)