Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

refactor(cli): restructure commands, option flags and validation #1547

Merged
merged 3 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cmd/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"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"
Expand Down Expand Up @@ -86,7 +87,7 @@
},
}

opt.IOStreams = &genericclioptions.IOStreams{
opt.IOStreams = &genericiooptions.IOStreams{

Check warning on line 90 in cmd/cli/root.go

View check run for this annotation

Codecov / codecov/patch

cmd/cli/root.go#L90

Added line #L90 was not covered by tests
In: cmd.InOrStdin(),
Out: os.Stdout,
ErrOut: os.Stderr,
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ require (
github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1
go.uber.org/ratelimit v0.3.0
golang.org/x/crypto v0.20.0
golang.org/x/exp v0.0.0-20230807204917-050eac23e9de
golang.org/x/net v0.21.0
golang.org/x/oauth2 v0.17.0
golang.org/x/sync v0.6.0
Expand All @@ -54,6 +53,7 @@ require (
require (
github.com/distribution/reference v0.5.0 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
golang.org/x/exp v0.0.0-20230807204917-050eac23e9de // indirect
gopkg.in/evanphx/json-patch.v5 v5.6.0 // indirect
sigs.k8s.io/kustomize/api v0.16.0 // indirect
sigs.k8s.io/kustomize/kyaml v0.16.0 // indirect
Expand Down
223 changes: 138 additions & 85 deletions internal/cli/cmd/apply/apply.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package apply

import (
"context"
goerrors "errors"
"fmt"

Expand All @@ -20,104 +21,156 @@
kargosvcapi "github.com/akuity/kargo/pkg/api/service/v1alpha1"
)

type applyOptions struct {
*option.Option
Config config.CLIConfig

Filenames []string
}

func NewCommand(cfg config.CLIConfig, opt *option.Option) *cobra.Command {
var filenames []string
cmdOpts := &applyOptions{
Option: opt,
Config: cfg,

Check warning on line 34 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L32-L34

Added lines #L32 - L34 were not covered by tests
}

cmd := &cobra.Command{
Use: "apply [--project=project] -f (FILENAME)",
Short: "Apply a resource from a file or from stdin",
Example: `
# Apply a stage using the data in stage.yaml
kargo apply -f stage.yaml
`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if len(filenames) == 0 {
return errors.New("filename is required")
RunE: func(cmd *cobra.Command, _ []string) error {
if err := cmdOpts.validate(); err != nil {
return err

Check warning on line 46 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L44-L46

Added lines #L44 - L46 were not covered by tests
}

rawManifest, err := yaml.Read(filenames)
if err != nil {
return errors.Wrap(err, "read manifests")
}
return cmdOpts.run(cmd.Context())

Check warning on line 49 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L49

Added line #L49 was not covered by tests
},
}

var printer printers.ResourcePrinter
if ptr.Deref(opt.PrintFlags.OutputFormat, "") != "" {
printer, err = opt.PrintFlags.ToPrinter()
if err != nil {
return errors.Wrap(err, "new printer")
}
}
// Register the option flags on the command.
cmdOpts.addFlags(cmd)

Check warning on line 54 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L54

Added line #L54 was not covered by tests

kargoSvcCli, err := client.GetClientFromConfig(ctx, cfg, opt)
if err != nil {
return errors.Wrap(err, "get client from config")
}
return cmd

Check warning on line 56 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L56

Added line #L56 was not covered by tests
}

// TODO: Current implementation of apply is not the same as `kubectl` does.
// It actually "replaces" resource with the given file.
// We should provide the same implementation as `kubectl` does.
resp, err := kargoSvcCli.CreateOrUpdateResource(ctx,
connect.NewRequest(&kargosvcapi.CreateOrUpdateResourceRequest{
Manifest: rawManifest,
}))
if err != nil {
return errors.Wrap(err, "apply resource")
}
// addFlags adds the flags for the apply options to the provided command.
func (o *applyOptions) addFlags(cmd *cobra.Command) {
o.PrintFlags.AddFlags(cmd)

Check warning on line 61 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L60-L61

Added lines #L60 - L61 were not covered by tests

resCap := len(resp.Msg.GetResults())
createdRes := make([]*kargosvcapi.CreateOrUpdateResourceResult_CreatedResourceManifest, 0, resCap)
updatedRes := make([]*kargosvcapi.CreateOrUpdateResourceResult_UpdatedResourceManifest, 0, resCap)
errs := make([]error, 0, resCap)
for _, r := range resp.Msg.GetResults() {
switch typedRes := r.GetResult().(type) {
case *kargosvcapi.CreateOrUpdateResourceResult_CreatedResourceManifest:
createdRes = append(createdRes, typedRes)
case *kargosvcapi.CreateOrUpdateResourceResult_UpdatedResourceManifest:
updatedRes = append(updatedRes, typedRes)
case *kargosvcapi.CreateOrUpdateResourceResult_Error:
errs = append(errs, errors.New(typedRes.Error))
}
}
for _, r := range createdRes {
var obj unstructured.Unstructured
if err := sigyaml.Unmarshal(r.CreatedResourceManifest, &obj); err != nil {
fmt.Fprintf(opt.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(opt.IOStreams.Out, "%s Created: %q\n", obj.GetKind(), name)
continue
}
_ = printer.PrintObj(&obj, opt.IOStreams.Out)
}
for _, r := range updatedRes {
var obj unstructured.Unstructured
if err := sigyaml.Unmarshal(r.UpdatedResourceManifest, &obj); err != nil {
fmt.Fprintf(opt.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(opt.IOStreams.Out, "%s Updated: %q\n", obj.GetKind(), name)
continue
}
_ = printer.PrintObj(&obj, opt.IOStreams.Out)
}
return goerrors.Join(errs...)
},
// TODO: Factor out server flags to a higher level (root?) as they are
// common to almost all commands.
Comment on lines +63 to +64
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya... it's tricky. Almost but not all commands use them.

Personally, I'd rather repeat ourselves sometimes that have inapplicable flags show up on commands. i.e. If we had to choose, UX wins over DRY, imho.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A problem I have observed though is that we sometimes ourselves forget to actually wire them in (see e.g. https://github.com/akuity/kargo/blob/5912e5a9dfd2912fdd6a46e1b35655b6225c4454/internal/cli/cmd/approve/approve.go in current main, which AFAIK should have the flags set).

By registering them once at root and loading them into some config which is passed to downward commands, this could be avoided.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to do it at root and unregister them in spots?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can theoretically use MarkHidden to hide them from the help menu for specific commands.

option.InsecureTLS(cmd.PersistentFlags(), o.Option)
option.LocalServer(cmd.PersistentFlags(), o.Option)

Check warning on line 66 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L65-L66

Added lines #L65 - L66 were not covered by tests

option.Filenames(cmd.Flags(), &o.Filenames, "Filename or directory to use to apply the resource(s)")

Check warning on line 68 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L68

Added line #L68 was not covered by tests

if err := cmd.MarkFlagRequired(option.FilenameFlag); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are new since the last time I went deep on Cobra. I am super excited about how much this cleans things up.

panic(errors.Wrap(err, "could not mark filename flag as required"))

Check warning on line 71 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L70-L71

Added lines #L70 - L71 were not covered by tests
}
opt.PrintFlags.AddFlags(cmd)
option.Filenames(cmd.Flags(), &filenames, "apply")
option.InsecureTLS(cmd.PersistentFlags(), opt)
option.LocalServer(cmd.PersistentFlags(), opt)
return cmd
if err := cmd.MarkFlagFilename(option.FilenameFlag, ".yaml", ".yml"); err != nil {
panic(errors.Wrap(err, "could not mark filename flag as filename"))

Check warning on line 74 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L73-L74

Added lines #L73 - L74 were not covered by tests
}
if err := cmd.MarkFlagDirname(option.FilenameFlag); err != nil {
panic(errors.Wrap(err, "could not mark filename flag as dirname"))

Check warning on line 77 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L76-L77

Added lines #L76 - L77 were not covered by tests
}
}

// validate performs validation of the options. If the options are invalid, an
// error is returned.
func (o *applyOptions) validate() error {

Check warning on line 83 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L83

Added line #L83 was not covered by tests
// While the filename flag is marked as required, a user could still
// provide an empty string. This is a check to ensure that the flag is
// not empty.
if len(o.Filenames) == 0 {
return errors.New("filename is required")

Check warning on line 88 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L87-L88

Added lines #L87 - L88 were not covered by tests
}
return nil

Check warning on line 90 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L90

Added line #L90 was not covered by tests
}

// run performs the apply operation using the provided options.
func (o *applyOptions) run(ctx context.Context) error {
rawManifest, err := yaml.Read(o.Filenames)
if err != nil {
return errors.Wrap(err, "read manifests")

Check warning on line 97 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L94-L97

Added lines #L94 - L97 were not covered by tests
}

var printer printers.ResourcePrinter
if ptr.Deref(o.PrintFlags.OutputFormat, "") != "" {
printer, err = o.PrintFlags.ToPrinter()
if err != nil {
return errors.Wrap(err, "new printer")

Check warning on line 104 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L100-L104

Added lines #L100 - L104 were not covered by tests
}
}

kargoSvcCli, err := client.GetClientFromConfig(ctx, o.Config, o.Option)
if err != nil {
return errors.Wrap(err, "get client from config")

Check warning on line 110 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L108-L110

Added lines #L108 - L110 were not covered by tests
}

// TODO: Current implementation of apply is not the same as `kubectl` does.
// It actually "replaces" resource with the given file.
// We should provide the same implementation as `kubectl` does.
resp, err := kargoSvcCli.CreateOrUpdateResource(ctx,
connect.NewRequest(&kargosvcapi.CreateOrUpdateResourceRequest{
Manifest: rawManifest,
}))
if err != nil {
return errors.Wrap(err, "apply resource")

Check warning on line 121 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L116-L121

Added lines #L116 - L121 were not covered by tests
}

resCap := len(resp.Msg.GetResults())
createdRes := make([]*kargosvcapi.CreateOrUpdateResourceResult_CreatedResourceManifest, 0, resCap)
updatedRes := make([]*kargosvcapi.CreateOrUpdateResourceResult_UpdatedResourceManifest, 0, resCap)
errs := make([]error, 0, resCap)
for _, r := range resp.Msg.GetResults() {
switch typedRes := r.GetResult().(type) {
case *kargosvcapi.CreateOrUpdateResourceResult_CreatedResourceManifest:
createdRes = append(createdRes, typedRes)
case *kargosvcapi.CreateOrUpdateResourceResult_UpdatedResourceManifest:
updatedRes = append(updatedRes, typedRes)
case *kargosvcapi.CreateOrUpdateResourceResult_Error:
errs = append(errs, errors.New(typedRes.Error))

Check warning on line 135 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L124-L135

Added lines #L124 - L135 were not covered by tests
}
}

for _, res := range createdRes {
var obj unstructured.Unstructured
if err := sigyaml.Unmarshal(res.CreatedResourceManifest, &obj); err != nil {
fmt.Fprintf(o.IOStreams.ErrOut, "%s",
errors.Wrap(err, "Error: unmarshal created manifest"))
continue

Check warning on line 144 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L139-L144

Added lines #L139 - L144 were not covered by tests
}
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

Check warning on line 152 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L146-L152

Added lines #L146 - L152 were not covered by tests
}
_ = printer.PrintObj(&obj, o.IOStreams.Out)

Check warning on line 154 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L154

Added line #L154 was not covered by tests
}

for _, res := range updatedRes {
var obj unstructured.Unstructured
if err := sigyaml.Unmarshal(res.UpdatedResourceManifest, &obj); err != nil {
fmt.Fprintf(o.IOStreams.ErrOut, "%s",
errors.Wrap(err, "Error: unmarshal updated manifest"))
continue

Check warning on line 162 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L157-L162

Added lines #L157 - L162 were not covered by tests
}
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

Check warning on line 170 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L164-L170

Added lines #L164 - L170 were not covered by tests
}
_ = printer.PrintObj(&obj, o.IOStreams.Out)

Check warning on line 172 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L172

Added line #L172 was not covered by tests
}

return goerrors.Join(errs...)

Check warning on line 175 in internal/cli/cmd/apply/apply.go

View check run for this annotation

Codecov / codecov/patch

internal/cli/cmd/apply/apply.go#L175

Added line #L175 was not covered by tests
}
Loading
Loading