diff --git a/cmd/kubectl/go.mod b/cmd/kubectl/go.mod index 3a42432e6..44372a890 100644 --- a/cmd/kubectl/go.mod +++ b/cmd/kubectl/go.mod @@ -3,7 +3,9 @@ module sigs.k8s.io/kustomize/cmd/kubectl go 1.13 require ( + github.com/go-errors/errors v1.0.1 github.com/spf13/cobra v0.0.5 + github.com/stretchr/testify v1.4.0 k8s.io/api v0.17.0 k8s.io/apimachinery v0.17.0 k8s.io/cli-runtime v0.17.0 diff --git a/cmd/kubectl/go.sum b/cmd/kubectl/go.sum index 1b5e06aa8..1f2220181 100644 --- a/cmd/kubectl/go.sum +++ b/cmd/kubectl/go.sum @@ -83,6 +83,7 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= @@ -364,7 +365,6 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190909003024-a7b16738d86b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI= golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -504,7 +504,6 @@ k8s.io/kubectl v0.0.0-20191219154910-1528d4eea6dd h1:nZX5+wEqTu/EBIYjrZlFOA63z4+ k8s.io/kubectl v0.0.0-20191219154910-1528d4eea6dd/go.mod h1:9ehGcuUGjXVZh0qbYSB0vvofQw2JQe6c6cO0k4wu/Oo= k8s.io/metrics v0.0.0-20191214191643-6b1944c9f765/go.mod h1:5V7rewilItwK0cz4nomU0b3XCcees2Ka5EBYWS1HBeM= k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= -k8s.io/utils v0.0.0-20191030222137-2b95a09bc58d/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo= k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= diff --git a/cmd/kubectl/kubectlcobra/applier.go b/cmd/kubectl/kubectlcobra/applier.go new file mode 100644 index 000000000..b67258053 --- /dev/null +++ b/cmd/kubectl/kubectlcobra/applier.go @@ -0,0 +1,205 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package kubectlcobra + +import ( + "context" + "time" + + "github.com/go-errors/errors" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/kubectl/pkg/cmd/apply" + "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/kustomize/kstatus/wait" +) + +// newApplier returns a new Applier. It will set up the applyOptions and +// statusOptions which are responsible for capturing any command line flags. +// It currently requires IOStreams, but this is a legacy from when +// the ApplyOptions were responsible for printing progress. This is now +// handled by a separate printer with the KubectlPrinterAdapter bridging +// between the two. +func newApplier(factory util.Factory, ioStreams genericclioptions.IOStreams) *Applier { + return &Applier{ + applyOptions: apply.NewApplyOptions(ioStreams), + statusOptions: NewStatusOptions(), + factory: factory, + ioStreams: ioStreams, + } +} + +// resolver defines the interface the applier needs to observe status for resources. +type resolver interface { + WaitForStatusOfObjects(ctx context.Context, objects []wait.KubernetesObject) <-chan wait.Event +} + +// Applier performs the step of applying a set of resources into a cluster, +// conditionally waits for all of them to be fully reconciled and finally +// performs prune to clean up any resources that has been deleted. +type Applier struct { + factory util.Factory + ioStreams genericclioptions.IOStreams + + applyOptions *apply.ApplyOptions + statusOptions *StatusOptions + resolver resolver +} + +// Initialize sets up the Applier for actually doing an apply against +// a cluster. This involves validating command line inputs and configuring +// clients for communicating with the cluster. +func (a *Applier) Initialize(cmd *cobra.Command) error { + a.applyOptions.PreProcessorFn = PrependGroupingObject(a.applyOptions) + err := a.applyOptions.Complete(a.factory, cmd) + if err != nil { + return errors.WrapPrefix(err, "error setting up ApplyOptions", 1) + } + // Default PostProcessor is configured in "Complete" function, + // so the prune must happen after "Complete". + a.applyOptions.PostProcessorFn = prune(a.factory, a.applyOptions) + + resolver, err := a.newResolver(a.statusOptions.period) + if err != nil { + return errors.WrapPrefix(err, "error creating resolver", 1) + } + a.resolver = resolver + return nil +} + +// SetFlags configures the command line flags needed for apply and +// status. This is a temporary solution as we should separate the configuration +// of cobra flags from the Applier. +func (a *Applier) SetFlags(cmd *cobra.Command) { + a.applyOptions.DeleteFlags.AddFlags(cmd) + a.applyOptions.RecordFlags.AddFlags(cmd) + a.applyOptions.PrintFlags.AddFlags(cmd) + a.statusOptions.AddFlags(cmd) + a.applyOptions.Overwrite = true +} + +// newResolver sets up a new Resolver for computing status. The configuration +// needed for the resolver is taken from the Factory. +func (a *Applier) newResolver(pollInterval time.Duration) (*wait.Resolver, error) { + config, err := a.factory.ToRESTConfig() + if err != nil { + return nil, errors.WrapPrefix(err, "error getting RESTConfig", 1) + } + + mapper, err := a.factory.ToRESTMapper() + if err != nil { + return nil, errors.WrapPrefix(err, "error getting RESTMapper", 1) + } + + c, err := client.New(config, client.Options{Scheme: scheme.Scheme, Mapper: mapper}) + if err != nil { + return nil, errors.WrapPrefix(err, "error creating client", 1) + } + + return wait.NewResolver(c, mapper, pollInterval), nil +} + +// Run performs the Apply step. This happens asynchronously with updates +// on progress and any errors are reported back on the event channel. +// Cancelling the operation or setting timeout on how long to wait +// for it complete can be done with the passed in context. +// Note: There sn't currently any way to interrupt the operation +// before all the given resources have been applied to the cluster. Any +// cancellation or timeout will only affect how long we wait for the +// resources to become current. +func (a *Applier) Run(ctx context.Context) <-chan Event { + ch := make(chan Event) + + go func() { + defer close(ch) + adapter := &KubectlPrinterAdapter{ + ch: ch, + } + // The adapter is used to intercept what is meant to be printing + // in the ApplyOptions, and instead turn those into events. + a.applyOptions.ToPrinter = adapter.toPrinterFunc() + // This provides us with a slice of all the objects that will be + // applied to the cluster. + infos, _ := a.applyOptions.GetObjects() + err := a.applyOptions.Run() + if err != nil { + // If we see an error here we just report it on the channel and then + // give up. Eventually we might be able to determine which errors + // are fatal and which might allow us to continue. + ch <- Event{ + EventType: ErrorEventType, + ErrorEvent: ErrorEvent{ + Err: errors.WrapPrefix(err, "error applying resources", 1), + }, + } + return + } + + if a.statusOptions.wait { + statusChannel := a.resolver.WaitForStatusOfObjects(ctx, infosToObjects(infos)) + // As long as the statusChannel remains open, we take every statusEvent, + // wrap it in an Event and send it on the channel. + for statusEvent := range statusChannel { + ch <- Event{ + EventType: StatusEventType, + StatusEvent: statusEvent, + } + } + } + + }() + return ch +} + +func infosToObjects(infos []*resource.Info) []wait.KubernetesObject { + var objects []wait.KubernetesObject + for _, info := range infos { + u := info.Object.(*unstructured.Unstructured) + objects = append(objects, u) + } + return objects +} + +// EventType determines the type of events that are available. +type EventType string + +const ( + ErrorEventType EventType = "error" + ApplyEventType EventType = "apply" + StatusEventType EventType = "status" +) + +// Event is the type of the objects that will be returned through +// the channel that is returned from a call to Run. It contains +// information about progress and errors encountered during +// the process of doing apply, waiting for status and doing a prune. +type Event struct { + // EventType is the type of event. + EventType EventType + + // ErrorEvent contains information about any errors encountered. + ErrorEvent ErrorEvent + + // ApplyEvent contains information about progress pertaining to + // applying a resource to the cluster. + ApplyEvent ApplyEvent + + // StatusEvents contains information about the status of one of + // the applied resources. + StatusEvent wait.Event +} + +type ErrorEvent struct { + Err error +} + +type ApplyEvent struct { + Operation string + Object runtime.Object +} diff --git a/cmd/kubectl/kubectlcobra/applier_test.go b/cmd/kubectl/kubectlcobra/applier_test.go new file mode 100644 index 000000000..0e3dae046 --- /dev/null +++ b/cmd/kubectl/kubectlcobra/applier_test.go @@ -0,0 +1,48 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package kubectlcobra + +import ( + "context" + "github.com/stretchr/testify/assert" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + "testing" +) + +// The applier is currently hard to test, as the dependencies on the ApplyOptions and +// the resolver are hard to stub out. As we work to better separate the different +// responsibilities of the apply functionality, we should also make it easier to test. +// This provides some basic tests for now. + +func TestApplierWithUnknownFile(t *testing.T) { + tf := cmdtesting.NewTestFactory() + defer tf.Cleanup() + iostreams, _, _, _ := genericclioptions.NewTestIOStreams() + cmd := NewCmdApply("base", tf, iostreams) + + applier := newApplier(tf, iostreams) + filenames := []string{"file.yaml"} + applier.applyOptions.DeleteFlags.FileNameFlags.Filenames = &filenames + + err := applier.Initialize(cmd) + assert.NoError(t, err) + + ch := applier.Run(context.TODO()) + + var events []Event + for msg := range ch { + events = append(events, msg) + } + + if !assert.Equal(t, 1, len(events)) { + return + } + + event := events[0] + if !assert.Equal(t, ErrorEventType, event.EventType) { + return + } + assert.Contains(t, event.ErrorEvent.Err.Error(), "does not exist") +} diff --git a/cmd/kubectl/kubectlcobra/basic_printer.go b/cmd/kubectl/kubectlcobra/basic_printer.go new file mode 100644 index 000000000..96db9755d --- /dev/null +++ b/cmd/kubectl/kubectlcobra/basic_printer.go @@ -0,0 +1,62 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package kubectlcobra + +import ( + "fmt" + "k8s.io/apimachinery/pkg/runtime/schema" + "strings" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "sigs.k8s.io/kustomize/kstatus/wait" +) + +// BasicPrinter is a simple implementation that just prints the events +// from the channel in the default format for kubectl. +// We need to support different printers for different output formats. +type BasicPrinter struct { + ioStreams genericclioptions.IOStreams +} + +// Print outputs the events from the provided channel in a simple +// format on StdOut. As we support other printer implementations +// this should probably be an interface. +// This function will block until the channel is closed. +func (b *BasicPrinter) Print(ch <-chan Event) { + for event := range ch { + switch event.EventType { + case ErrorEventType: + cmdutil.CheckErr(event.ErrorEvent.Err) + case ApplyEventType: + obj := event.ApplyEvent.Object + gvk := obj.GetObjectKind().GroupVersionKind() + name := "" + if acc, err := meta.Accessor(obj); err == nil { + if n := acc.GetName(); len(n) > 0 { + name = n + } + } + fmt.Fprintf(b.ioStreams.Out, "%s %s\n", resourceIdToString(gvk.GroupKind(), name), event.ApplyEvent.Operation) + case StatusEventType: + statusEvent := event.StatusEvent + switch statusEvent.Type { + case wait.ResourceUpdate: + id := statusEvent.EventResource.ResourceIdentifier + gk := id.GroupKind + fmt.Fprintf(b.ioStreams.Out, "%s is %s: %s\n", resourceIdToString(gk, id.Name), statusEvent.EventResource.Status.String(), statusEvent.EventResource.Message) + case wait.Completed: + fmt.Fprint(b.ioStreams.Out, "all resources has reached the Current status\n") + case wait.Aborted: + fmt.Fprintf(b.ioStreams.Out, "resources failed to the reached Current status\n") + } + } + } +} + +// resourceIdToString returns the string representation of a GroupKind and a resource name. +func resourceIdToString(gk schema.GroupKind, name string) string { + return fmt.Sprintf("%s/%s", strings.ToLower(gk.String()), name) +} diff --git a/cmd/kubectl/kubectlcobra/commands.go b/cmd/kubectl/kubectlcobra/commands.go index 8fe5146f8..d4da23e1a 100644 --- a/cmd/kubectl/kubectlcobra/commands.go +++ b/cmd/kubectl/kubectlcobra/commands.go @@ -5,6 +5,7 @@ package kubectlcobra import ( + "context" "flag" "fmt" "os" @@ -79,10 +80,10 @@ func updateHelp(names []string, c *cobra.Command) { // NewCmdApply creates the `apply` command func NewCmdApply(baseName string, f util.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { - o := apply.NewApplyOptions(ioStreams) - so := newStatusOptions(f, ioStreams) - // Set up grouping object for this apply; used in subsequent prune. - o.PreProcessorFn = PrependGroupingObject(o) + applier := newApplier(f, ioStreams) + printer := &BasicPrinter{ + ioStreams: ioStreams, + } cmd := &cobra.Command{ Use: "apply (-f FILENAME | -k DIRECTORY)", @@ -94,31 +95,28 @@ func NewCmdApply(baseName string, f util.Factory, ioStreams genericclioptions.IO Run: func(cmd *cobra.Command, args []string) { if len(args) > 0 { // check is kustomize, if so update - o.DeleteFlags.FileNameFlags.Kustomize = &args[0] + applier.applyOptions.DeleteFlags.FileNameFlags.Kustomize = &args[0] } - cmdutil.CheckErr(o.Complete(f, cmd)) - // Default PostProcessor is configured in "Complete" function, - // so the prune must happen after "Complete". - o.PostProcessorFn = prune(f, o) - cmdutil.CheckErr(o.Run()) - infos, _ := o.GetObjects() - if so.wait { - cmdutil.CheckErr(so.waitForStatus(infos)) - } + cmdutil.CheckErr(applier.Initialize(cmd)) + + // Create a context with the provided timout from the cobra parameter. + ctx, cancel := context.WithTimeout(context.Background(), applier.statusOptions.timeout) + defer cancel() + // Run the applier. It will return a channel where we can receive updates + // to keep track of progress and any issues. + ch := applier.Run(ctx) + + // The printer will print updates from the channel. It will block + // until the channel is closed. + printer.Print(ch) }, } - // bind flag structs - o.DeleteFlags.AddFlags(cmd) - o.RecordFlags.AddFlags(cmd) - o.PrintFlags.AddFlags(cmd) - so.AddFlags(cmd) - - o.Overwrite = true + applier.SetFlags(cmd) cmdutil.AddValidateFlags(cmd) - cmd.Flags().BoolVar(&o.ServerDryRun, "server-dry-run", o.ServerDryRun, "If true, request will be sent to server with dry-run flag, which means the modifications won't be persisted. This is an alpha feature and flag.") + cmd.Flags().BoolVar(&applier.applyOptions.ServerDryRun, "server-dry-run", applier.applyOptions.ServerDryRun, "If true, request will be sent to server with dry-run flag, which means the modifications won't be persisted. This is an alpha feature and flag.") cmd.Flags().Bool("dry-run", false, "If true, only print the object that would be sent, without sending it. Warning: --dry-run cannot accurately output the result of merging the local manifest and the server-side data. Use --server-dry-run to get the merged result instead.") cmdutil.AddServerSideApplyFlags(cmd) diff --git a/cmd/kubectl/kubectlcobra/printer_adapter.go b/cmd/kubectl/kubectlcobra/printer_adapter.go new file mode 100644 index 000000000..0836dad74 --- /dev/null +++ b/cmd/kubectl/kubectlcobra/printer_adapter.go @@ -0,0 +1,53 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package kubectlcobra + +import ( + "io" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/printers" +) + +// KubectlPrinterAdapter is a workaround for capturing progress from +// ApplyOptions. ApplyOptions were originally meant to print progress +// directly using a configurable printer. The KubectlPrinterAdapter +// plugs into ApplyOptions as a ToPrinter function, but instead of +// printing the info, it emits it as an event on the provided channel. +type KubectlPrinterAdapter struct { + ch chan<- Event +} + +// resourcePrinterImpl implements the ResourcePrinter interface. But +// instead of printing, it emits information on the provided channel. +type resourcePrinterImpl struct { + operation string + ch chan<- Event +} + +// PrintObj takes the provided object and operation and emits +// it on the channel. +func (r *resourcePrinterImpl) PrintObj(obj runtime.Object, _ io.Writer) error { + r.ch <- Event{ + EventType: ApplyEventType, + ApplyEvent: ApplyEvent{ + Operation: r.operation, + Object: obj, + }, + } + return nil +} + +type toPrinterFunc func(string) (printers.ResourcePrinter, error) + +// toPrinterFunc returns a function of type toPrinterFunc. This +// is the type required by the ApplyOptions. +func (p *KubectlPrinterAdapter) toPrinterFunc() toPrinterFunc { + return func(operation string) (printers.ResourcePrinter, error) { + return &resourcePrinterImpl{ + ch: p.ch, + operation: operation, + }, nil + } +} diff --git a/cmd/kubectl/kubectlcobra/printer_adapter_test.go b/cmd/kubectl/kubectlcobra/printer_adapter_test.go new file mode 100644 index 000000000..b7375b5a0 --- /dev/null +++ b/cmd/kubectl/kubectlcobra/printer_adapter_test.go @@ -0,0 +1,49 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package kubectlcobra + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestKubectlPrinterAdapter(t *testing.T) { + ch := make(chan Event) + buffer := bytes.Buffer{} + operation := "operation" + + adapter := KubectlPrinterAdapter{ + ch: ch, + } + + toPrinterFunc := adapter.toPrinterFunc() + resourcePrinter, err := toPrinterFunc(operation) + assert.NoError(t, err) + + deployment := appsv1.Deployment{ + TypeMeta: v1.TypeMeta{ + APIVersion: "apps/v1", + Kind: "Deployment", + }, + ObjectMeta: v1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + }, + } + + // Need to run this in a separate gorutine since go channels + // are blocking. + go func() { + err = resourcePrinter.PrintObj(&deployment, &buffer) + }() + msg := <-ch + + assert.NoError(t, err) + assert.Equal(t, operation, msg.ApplyEvent.Operation) + assert.Equal(t, &deployment, msg.ApplyEvent.Object) +} diff --git a/cmd/kubectl/kubectlcobra/status.go b/cmd/kubectl/kubectlcobra/status.go index 3db008362..d69393fd9 100644 --- a/cmd/kubectl/kubectlcobra/status.go +++ b/cmd/kubectl/kubectlcobra/status.go @@ -4,102 +4,27 @@ package kubectlcobra import ( - "context" - "fmt" - "strings" "time" "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/cli-runtime/pkg/resource" - "k8s.io/kubectl/pkg/cmd/util" - "k8s.io/kubectl/pkg/scheme" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/kustomize/kstatus/wait" ) -type StatusOptions struct { - factory util.Factory - ioStreams genericclioptions.IOStreams +func NewStatusOptions() *StatusOptions { + return &StatusOptions{ + wait: false, + period: 2 * time.Second, + timeout: time.Minute, + } +} +type StatusOptions struct { wait bool period time.Duration timeout time.Duration } -func newStatusOptions(factory util.Factory, ioStreams genericclioptions.IOStreams) *StatusOptions { - return &StatusOptions{ - factory: factory, - ioStreams: ioStreams, - - wait: false, - period: 2 * time.Second, - timeout: 1 * time.Minute, - } -} - func (s *StatusOptions) AddFlags(c *cobra.Command) { c.Flags().BoolVar(&s.wait, "status", s.wait, "Wait for all applied resources to reach the Current status.") c.Flags().DurationVar(&s.period, "status-period", s.period, "Polling period for resource statuses.") c.Flags().DurationVar(&s.timeout, "status-timeout", s.timeout, "Timeout threshold for waiting for all resources to reach the Current status.") } - -func (s *StatusOptions) waitForStatus(infos []*resource.Info) error { - mapper, err := getRESTMapper(s.factory) - if err != nil { - return err - } - - c, err := getClient(s.factory, mapper) - if err != nil { - return err - } - - ctx, cancel := context.WithTimeout(context.Background(), s.timeout) - defer cancel() - - resolver := wait.NewResolver(c, mapper, s.period) - ch := resolver.WaitForStatus(ctx, infosToResourceIdentifiers(infos)) - - for msg := range ch { - switch msg.Type { - case wait.ResourceUpdate: - id := msg.EventResource.ResourceIdentifier - gk := id.GroupKind - fmt.Fprintf(s.ioStreams.Out, "%s/%s is %s: %s\n", strings.ToLower(gk.String()), id.Name, msg.EventResource.Status.String(), msg.EventResource.Message) - case wait.Completed: - fmt.Fprint(s.ioStreams.Out, "all resources has reached the Current status\n") - case wait.Aborted: - fmt.Fprintf(s.ioStreams.Out, "resources failed to the reached Current status after %s\n", s.timeout.String()) - } - } - return nil -} - -func infosToResourceIdentifiers(infos []*resource.Info) []wait.ResourceIdentifier { - var resources []wait.ResourceIdentifier - for _, info := range infos { - u := info.Object.(*unstructured.Unstructured) - resources = append(resources, wait.ResourceIdentifier{ - GroupKind: u.GroupVersionKind().GroupKind(), - Namespace: u.GetNamespace(), - Name: u.GetName(), - }) - } - return resources -} - -func getRESTMapper(f util.Factory) (meta.RESTMapper, error) { - return f.ToRESTMapper() -} - -func getClient(f util.Factory, mapper meta.RESTMapper) (client.Reader, error) { - config, err := f.ToRESTConfig() - if err != nil { - return nil, err - } - - return client.New(config, client.Options{Scheme: scheme.Scheme, Mapper: mapper}) -}