From 0a58e43125724784c77ecd2774e2860687f14403 Mon Sep 17 00:00:00 2001 From: Mengqi Yu Date: Wed, 20 Apr 2022 18:20:50 -0700 Subject: [PATCH 1/2] vendor some of cli-utils/pkg/apply pkgs --- thirdparty/README.md | 8 +- thirdparty/cli-utils/apply/applier.go | 353 +++ thirdparty/cli-utils/apply/applier_builder.go | 160 ++ thirdparty/cli-utils/apply/applier_test.go | 2081 +++++++++++++++++ thirdparty/cli-utils/apply/common_test.go | 475 ++++ thirdparty/cli-utils/apply/destroyer.go | 228 ++ thirdparty/cli-utils/apply/destroyer_test.go | 391 ++++ thirdparty/cli-utils/apply/main_test.go | 19 + .../cli-utils/apply/solver/main_test.go | 19 + thirdparty/cli-utils/apply/solver/solver.go | 294 +++ .../cli-utils/apply/solver/solver_test.go | 1904 +++++++++++++++ thirdparty/cli-utils/apply/task/apply_task.go | 287 +++ .../cli-utils/apply/task/apply_task_test.go | 578 +++++ .../cli-utils/apply/task/delete_inv_task.go | 57 + .../apply/task/delete_inv_task_test.go | 68 + .../cli-utils/apply/task/inv_add_task.go | 98 + .../cli-utils/apply/task/inv_add_task_test.go | 196 ++ .../cli-utils/apply/task/inv_set_task.go | 132 ++ .../cli-utils/apply/task/inv_set_task_test.go | 214 ++ .../cli-utils/apply/task/printer_adapter.go | 82 + .../apply/task/printer_adapter_test.go | 55 + thirdparty/cli-utils/apply/task/prune_task.go | 85 + .../cli-utils/apply/task/send_event_task.go | 30 + 23 files changed, 7811 insertions(+), 3 deletions(-) create mode 100644 thirdparty/cli-utils/apply/applier.go create mode 100644 thirdparty/cli-utils/apply/applier_builder.go create mode 100644 thirdparty/cli-utils/apply/applier_test.go create mode 100644 thirdparty/cli-utils/apply/common_test.go create mode 100644 thirdparty/cli-utils/apply/destroyer.go create mode 100644 thirdparty/cli-utils/apply/destroyer_test.go create mode 100644 thirdparty/cli-utils/apply/main_test.go create mode 100644 thirdparty/cli-utils/apply/solver/main_test.go create mode 100644 thirdparty/cli-utils/apply/solver/solver.go create mode 100644 thirdparty/cli-utils/apply/solver/solver_test.go create mode 100644 thirdparty/cli-utils/apply/task/apply_task.go create mode 100644 thirdparty/cli-utils/apply/task/apply_task_test.go create mode 100644 thirdparty/cli-utils/apply/task/delete_inv_task.go create mode 100644 thirdparty/cli-utils/apply/task/delete_inv_task_test.go create mode 100644 thirdparty/cli-utils/apply/task/inv_add_task.go create mode 100644 thirdparty/cli-utils/apply/task/inv_add_task_test.go create mode 100644 thirdparty/cli-utils/apply/task/inv_set_task.go create mode 100644 thirdparty/cli-utils/apply/task/inv_set_task_test.go create mode 100644 thirdparty/cli-utils/apply/task/printer_adapter.go create mode 100644 thirdparty/cli-utils/apply/task/printer_adapter_test.go create mode 100644 thirdparty/cli-utils/apply/task/prune_task.go create mode 100644 thirdparty/cli-utils/apply/task/send_event_task.go diff --git a/thirdparty/README.md b/thirdparty/README.md index 558997208a..7d1227a237 100644 --- a/thirdparty/README.md +++ b/thirdparty/README.md @@ -8,8 +8,9 @@ This directory contains the files that are copied from 3rd-party projects and mo - `runfn`: KRM function runner - `cmdconfig`: Files copied from [cmd/config] v0.9.9 library - `commands`: Command files copied from [cmd/config] -- `cli-utils`: Files copied from [cli-utils] v0.26.0 library - - `commands`: Command files copied from [cli-utils/cmd] +- `cli-utils`: Files copied from [cli-utils] + - `status`: Command files copied from [cli-utils/cmd/status] v0.26.0 + - `apply`: apply library copied from [cli-utils/pkg/apply] v0.29.2 + the change in [this PR](https://github.com/kubernetes-sigs/cli-utils/pull/577) # Copyright and Licenses @@ -24,4 +25,5 @@ The modifications made in the 3rd-party files may be contributed to upstream. Th [kyaml]: https://github.com/kubernetes-sigs/kustomize/tree/8d72528eb5c73df80b20aae0a5e584c056879387/kyaml [cmd/config]: https://github.com/kubernetes-sigs/kustomize/tree/b9c36caa1c5c6ee64926021841ea441773d0767c/cmd/config [cli-utils]: https://github.com/kubernetes-sigs/cli-utils -[cli-utils/cmd]: https://github.com/kubernetes-sigs/cli-utils/tree/8b12ecd594e0bc4fa4b85ee6af3911ff33efd2aa/cmd +[cli-utils/cmd/status]: https://github.com/kubernetes-sigs/cli-utils/tree/v0.26.0/cmd/status +[cli-utils/pkg/apply]: https://github.com/kubernetes-sigs/cli-utils/tree/v0.29.2/pkg/apply diff --git a/thirdparty/cli-utils/apply/applier.go b/thirdparty/cli-utils/apply/applier.go new file mode 100644 index 0000000000..032a641ab1 --- /dev/null +++ b/thirdparty/cli-utils/apply/applier.go @@ -0,0 +1,353 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package apply + +import ( + "context" + "fmt" + "time" + + "github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/apply/solver" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + "k8s.io/klog/v2" + "sigs.k8s.io/cli-utils/pkg/apis/actuation" + "sigs.k8s.io/cli-utils/pkg/apply/cache" + "sigs.k8s.io/cli-utils/pkg/apply/event" + "sigs.k8s.io/cli-utils/pkg/apply/filter" + "sigs.k8s.io/cli-utils/pkg/apply/info" + "sigs.k8s.io/cli-utils/pkg/apply/mutator" + "sigs.k8s.io/cli-utils/pkg/apply/poller" + "sigs.k8s.io/cli-utils/pkg/apply/prune" + "sigs.k8s.io/cli-utils/pkg/apply/taskrunner" + "sigs.k8s.io/cli-utils/pkg/common" + "sigs.k8s.io/cli-utils/pkg/inventory" + "sigs.k8s.io/cli-utils/pkg/object" + "sigs.k8s.io/cli-utils/pkg/object/validation" +) + +const defaultPollInterval = 2 * time.Second + +// 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. +// The applier performs its function by executing a list queue of tasks, +// each of which is one of the steps in the process of applying a set +// of resources to the cluster. The actual execution of these tasks are +// handled by a StatusRunner. So the taskqueue is effectively a +// specification that is executed by the StatusRunner. Based on input +// parameters and/or the set of resources that needs to be applied to the +// cluster, different sets of tasks might be needed. +type Applier struct { + pruner *prune.Pruner + statusPoller poller.Poller + invClient inventory.Client + client dynamic.Interface + openAPIGetter discovery.OpenAPISchemaInterface + mapper meta.RESTMapper + infoHelper info.Helper +} + +// prepareObjects returns the set of objects to apply and to prune or +// an error if one occurred. +func (a *Applier) prepareObjects(localInv inventory.Info, localObjs object.UnstructuredSet, + o ApplierOptions) (object.UnstructuredSet, object.UnstructuredSet, error) { + if localInv == nil { + return nil, nil, fmt.Errorf("the local inventory can't be nil") + } + if err := inventory.ValidateNoInventory(localObjs); err != nil { + return nil, nil, err + } + // Add the inventory annotation to the resources being applied. + for _, localObj := range localObjs { + inventory.AddInventoryIDAnnotation(localObj, localInv) + } + // If the inventory uses the Name strategy and an inventory ID is provided, + // verify that the existing inventory object (if there is one) has an ID + // label that matches. + // TODO(seans): This inventory id validation should happen in destroy and status. + if localInv.Strategy() == inventory.NameStrategy && localInv.ID() != "" { + prevInvObjs, err := a.invClient.GetClusterInventoryObjs(localInv) + if err != nil { + return nil, nil, err + } + if len(prevInvObjs) > 1 { + panic(fmt.Errorf("found %d inv objects with Name strategy", len(prevInvObjs))) + } + if len(prevInvObjs) == 1 { + invObj := prevInvObjs[0] + val := invObj.GetLabels()[common.InventoryLabel] + if val != localInv.ID() { + return nil, nil, fmt.Errorf("inventory-id of inventory object in cluster doesn't match provided id %q", localInv.ID()) + } + } + } + pruneObjs, err := a.pruner.GetPruneObjs(localInv, localObjs, prune.Options{ + DryRunStrategy: o.DryRunStrategy, + }) + if err != nil { + return nil, nil, err + } + return localObjs, pruneObjs, nil +} + +// Run performs the Apply step. This happens asynchronously with updates +// on progress and any errors 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 isn'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, invInfo inventory.Info, objects object.UnstructuredSet, options ApplierOptions) <-chan event.Event { + klog.V(4).Infof("apply run for %d objects", len(objects)) + eventChannel := make(chan event.Event) + setDefaults(&options) + go func() { + defer close(eventChannel) + // Validate the resources to make sure we catch those problems early + // before anything has been updated in the cluster. + vCollector := &validation.Collector{} + validator := &validation.Validator{ + Collector: vCollector, + Mapper: a.mapper, + } + validator.Validate(objects) + + // Decide which objects to apply and which to prune + applyObjs, pruneObjs, err := a.prepareObjects(invInfo, objects, options) + if err != nil { + handleError(eventChannel, err) + return + } + klog.V(4).Infof("calculated %d apply objs; %d prune objs", len(applyObjs), len(pruneObjs)) + + // Build a TaskContext for passing info between tasks + resourceCache := cache.NewResourceCacheMap() + taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache) + + // Fetch the queue (channel) of tasks that should be executed. + klog.V(4).Infoln("applier building task queue...") + // Build list of apply validation filters. + applyFilters := []filter.ValidationFilter{ + filter.InventoryPolicyApplyFilter{ + Client: a.client, + Mapper: a.mapper, + Inv: invInfo, + InvPolicy: options.InventoryPolicy, + }, + filter.DependencyFilter{ + TaskContext: taskContext, + ActuationStrategy: actuation.ActuationStrategyApply, + DryRunStrategy: options.DryRunStrategy, + }, + } + // Build list of prune validation filters. + pruneFilters := []filter.ValidationFilter{ + filter.PreventRemoveFilter{}, + filter.InventoryPolicyFilter{ + Inv: invInfo, + InvPolicy: options.InventoryPolicy, + }, + filter.LocalNamespacesFilter{ + LocalNamespaces: localNamespaces(invInfo, object.UnstructuredSetToObjMetadataSet(objects)), + }, + filter.DependencyFilter{ + TaskContext: taskContext, + ActuationStrategy: actuation.ActuationStrategyDelete, + DryRunStrategy: options.DryRunStrategy, + }, + } + // Build list of apply mutators. + applyMutators := []mutator.Interface{ + &mutator.ApplyTimeMutator{ + Client: a.client, + Mapper: a.mapper, + ResourceCache: resourceCache, + }, + } + taskBuilder := &solver.TaskQueueBuilder{ + Pruner: a.pruner, + DynamicClient: a.client, + OpenAPIGetter: a.openAPIGetter, + InfoHelper: a.infoHelper, + Mapper: a.mapper, + InvClient: a.invClient, + Collector: vCollector, + ApplyFilters: applyFilters, + ApplyMutators: applyMutators, + PruneFilters: pruneFilters, + } + opts := solver.Options{ + ServerSideOptions: options.ServerSideOptions, + ReconcileTimeout: options.ReconcileTimeout, + Destroy: false, + Prune: !options.NoPrune, + DryRunStrategy: options.DryRunStrategy, + PrunePropagationPolicy: options.PrunePropagationPolicy, + PruneTimeout: options.PruneTimeout, + InventoryPolicy: options.InventoryPolicy, + } + + // Build the ordered set of tasks to execute. + taskQueue := taskBuilder. + WithApplyObjects(applyObjs). + WithPruneObjects(pruneObjs). + WithInventory(invInfo). + Build(taskContext, opts) + + klog.V(4).Infof("validation errors: %d", len(vCollector.Errors)) + klog.V(4).Infof("invalid objects: %d", len(vCollector.InvalidIds)) + + // Handle validation errors + switch options.ValidationPolicy { + case validation.ExitEarly: + err = vCollector.ToError() + if err != nil { + handleError(eventChannel, err) + return + } + case validation.SkipInvalid: + for _, err := range vCollector.Errors { + handleValidationError(eventChannel, err) + } + default: + handleError(eventChannel, fmt.Errorf("invalid ValidationPolicy: %q", options.ValidationPolicy)) + return + } + + // Register invalid objects to be retained in the inventory, if present. + for _, id := range vCollector.InvalidIds { + taskContext.AddInvalidObject(id) + } + + // Send event to inform the caller about the resources that + // will be applied/pruned. + eventChannel <- event.Event{ + Type: event.InitType, + InitEvent: event.InitEvent{ + ActionGroups: taskQueue.ToActionGroups(), + }, + } + // Create a new TaskStatusRunner to execute the taskQueue. + klog.V(4).Infoln("applier building TaskStatusRunner...") + allIds := object.UnstructuredSetToObjMetadataSet(append(applyObjs, pruneObjs...)) + runner := taskrunner.NewTaskStatusRunner(allIds, a.statusPoller) + klog.V(4).Infoln("applier running TaskStatusRunner...") + err = runner.Run(ctx, taskContext, taskQueue.ToChannel(), taskrunner.Options{ + PollInterval: options.PollInterval, + EmitStatusEvents: options.EmitStatusEvents, + }) + if err != nil { + handleError(eventChannel, err) + return + } + }() + return eventChannel +} + +type ApplierOptions struct { + // Encapsulates the fields for server-side apply. + ServerSideOptions common.ServerSideOptions + + // ReconcileTimeout defines whether the applier should wait + // until all applied resources have been reconciled, and if so, + // how long to wait. + ReconcileTimeout time.Duration + + // PollInterval defines how often we should poll for the status + // of resources. + PollInterval time.Duration + + // EmitStatusEvents defines whether status events should be + // emitted on the eventChannel to the caller. + EmitStatusEvents bool + + // NoPrune defines whether pruning of previously applied + // objects should happen after apply. + NoPrune bool + + // DryRunStrategy defines whether changes should actually be performed, + // or if it is just talk and no action. + DryRunStrategy common.DryRunStrategy + + // PrunePropagationPolicy defines the deletion propagation policy + // that should be used for pruning. If this is not provided, the + // default is to use the Background policy. + PrunePropagationPolicy metav1.DeletionPropagation + + // PruneTimeout defines whether we should wait for all resources + // to be fully deleted after pruning, and if so, how long we should + // wait. + PruneTimeout time.Duration + + // InventoryPolicy defines the inventory policy of apply. + InventoryPolicy inventory.Policy + + // ValidationPolicy defines how to handle invalid objects. + ValidationPolicy validation.Policy +} + +// setDefaults set the options to the default values if they +// have not been provided. +func setDefaults(o *ApplierOptions) { + if o.PollInterval == 0 { + o.PollInterval = defaultPollInterval + } + if o.PrunePropagationPolicy == "" { + o.PrunePropagationPolicy = metav1.DeletePropagationBackground + } +} + +func handleError(eventChannel chan event.Event, err error) { + eventChannel <- event.Event{ + Type: event.ErrorType, + ErrorEvent: event.ErrorEvent{ + Err: err, + }, + } +} + +// localNamespaces stores a set of strings of all the namespaces +// for the passed non cluster-scoped localObjs, plus the namespace +// of the passed inventory object. This is used to skip deleting +// namespaces which have currently applied objects in them. +func localNamespaces(localInv inventory.Info, localObjs []object.ObjMetadata) sets.String { + namespaces := sets.NewString() + for _, obj := range localObjs { + if obj.Namespace != "" { + namespaces.Insert(obj.Namespace) + } + } + invNamespace := localInv.Namespace() + if invNamespace != "" { + namespaces.Insert(invNamespace) + } + return namespaces +} + +func handleValidationError(eventChannel chan<- event.Event, err error) { + switch tErr := err.(type) { + case *validation.Error: + // handle validation error about one or more specific objects + eventChannel <- event.Event{ + Type: event.ValidationType, + ValidationEvent: event.ValidationEvent{ + Identifiers: tErr.Identifiers(), + Error: tErr, + }, + } + default: + // handle general validation error (no specific object) + eventChannel <- event.Event{ + Type: event.ValidationType, + ValidationEvent: event.ValidationEvent{ + Error: tErr, + }, + } + } +} diff --git a/thirdparty/cli-utils/apply/applier_builder.go b/thirdparty/cli-utils/apply/applier_builder.go new file mode 100644 index 0000000000..a052253144 --- /dev/null +++ b/thirdparty/cli-utils/apply/applier_builder.go @@ -0,0 +1,160 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package apply + +import ( + "errors" + "fmt" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" + "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/scheme" + "sigs.k8s.io/cli-utils/pkg/apply/info" + "sigs.k8s.io/cli-utils/pkg/apply/poller" + "sigs.k8s.io/cli-utils/pkg/apply/prune" + "sigs.k8s.io/cli-utils/pkg/inventory" + "sigs.k8s.io/cli-utils/pkg/kstatus/polling" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type ApplierBuilder struct { + // factory is only used to retrieve things that have not been provided explicitly. + factory util.Factory + invClient inventory.Client + client dynamic.Interface + discoClient discovery.CachedDiscoveryInterface + mapper meta.RESTMapper + restConfig *rest.Config + unstructuredClientForMapping func(*meta.RESTMapping) (resource.RESTClient, error) + statusPoller poller.Poller +} + +// NewApplierBuilder returns a new ApplierBuilder. +func NewApplierBuilder() *ApplierBuilder { + return &ApplierBuilder{ + // Defaults, if any, go here. + } +} + +func (b *ApplierBuilder) Build() (*Applier, error) { + bx, err := b.finalize() + if err != nil { + return nil, err + } + return &Applier{ + pruner: &prune.Pruner{ + InvClient: bx.invClient, + Client: bx.client, + Mapper: bx.mapper, + }, + statusPoller: bx.statusPoller, + invClient: bx.invClient, + client: bx.client, + openAPIGetter: bx.discoClient, + mapper: bx.mapper, + infoHelper: info.NewHelper(bx.mapper, bx.unstructuredClientForMapping), + }, nil +} + +func (b *ApplierBuilder) finalize() (*ApplierBuilder, error) { + bx := *b // make a copy before mutating any fields. Shallow copy is good enough. + var err error + if bx.invClient == nil { + return nil, errors.New("inventory client must be provided") + } + if bx.client == nil { + if bx.factory == nil { + return nil, fmt.Errorf("a factory must be provided or all other options: %v", err) + } + bx.client, err = bx.factory.DynamicClient() + if err != nil { + return nil, fmt.Errorf("error getting dynamic client: %v", err) + } + } + if bx.discoClient == nil { + if bx.factory == nil { + return nil, fmt.Errorf("a factory must be provided or all other options: %v", err) + } + bx.discoClient, err = bx.factory.ToDiscoveryClient() + if err != nil { + return nil, fmt.Errorf("error getting discovery client: %v", err) + } + } + if bx.mapper == nil { + if bx.factory == nil { + return nil, fmt.Errorf("a factory must be provided or all other options: %v", err) + } + bx.mapper, err = bx.factory.ToRESTMapper() + if err != nil { + return nil, fmt.Errorf("error getting rest mapper: %v", err) + } + } + if bx.restConfig == nil { + if bx.factory == nil { + return nil, fmt.Errorf("a factory must be provided or all other options: %v", err) + } + bx.restConfig, err = bx.factory.ToRESTConfig() + if err != nil { + return nil, fmt.Errorf("error getting rest config: %v", err) + } + } + if bx.unstructuredClientForMapping == nil { + if bx.factory == nil { + return nil, fmt.Errorf("a factory must be provided or all other options: %v", err) + } + bx.unstructuredClientForMapping = bx.factory.UnstructuredClientForMapping + } + if bx.statusPoller == nil { + c, err := client.New(bx.restConfig, client.Options{Scheme: scheme.Scheme, Mapper: bx.mapper}) + if err != nil { + return nil, fmt.Errorf("error creating client: %v", err) + } + bx.statusPoller = polling.NewStatusPoller(c, bx.mapper, polling.Options{}) + } + return &bx, nil +} + +func (b *ApplierBuilder) WithFactory(factory util.Factory) *ApplierBuilder { + b.factory = factory + return b +} + +func (b *ApplierBuilder) WithInventoryClient(invClient inventory.Client) *ApplierBuilder { + b.invClient = invClient + return b +} + +func (b *ApplierBuilder) WithDynamicClient(client dynamic.Interface) *ApplierBuilder { + b.client = client + return b +} + +func (b *ApplierBuilder) WithDiscoveryClient(discoClient discovery.CachedDiscoveryInterface) *ApplierBuilder { + b.discoClient = discoClient + return b +} + +func (b *ApplierBuilder) WithRestMapper(mapper meta.RESTMapper) *ApplierBuilder { + b.mapper = mapper + return b +} + +func (b *ApplierBuilder) WithRestConfig(restConfig *rest.Config) *ApplierBuilder { + b.restConfig = restConfig + return b +} + +func (b *ApplierBuilder) WithUnstructuredClientForMapping(unstructuredClientForMapping func(*meta.RESTMapping) (resource.RESTClient, error)) *ApplierBuilder { + b.unstructuredClientForMapping = unstructuredClientForMapping + return b +} + +func (b *ApplierBuilder) WithStatusPoller(statusPoller poller.Poller) *ApplierBuilder { + b.statusPoller = statusPoller + return b +} diff --git a/thirdparty/cli-utils/apply/applier_test.go b/thirdparty/cli-utils/apply/applier_test.go new file mode 100644 index 0000000000..7feaeac10e --- /dev/null +++ b/thirdparty/cli-utils/apply/applier_test.go @@ -0,0 +1,2081 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package apply + +import ( + "context" + "fmt" + "sort" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/kubectl/pkg/scheme" + "sigs.k8s.io/cli-utils/pkg/apply/event" + "sigs.k8s.io/cli-utils/pkg/inventory" + pollevent "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event" + "sigs.k8s.io/cli-utils/pkg/kstatus/status" + "sigs.k8s.io/cli-utils/pkg/multierror" + "sigs.k8s.io/cli-utils/pkg/object" + "sigs.k8s.io/cli-utils/pkg/object/validation" + "sigs.k8s.io/cli-utils/pkg/testutil" +) + +var ( + codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + resources = map[string]string{ + "deployment": ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: foo + namespace: default + uid: dep-uid + generation: 1 +spec: + replicas: 1 +`, + "secret": ` +apiVersion: v1 +kind: Secret +metadata: + name: secret + namespace: default + uid: secret-uid + generation: 1 +type: Opaque +spec: + foo: bar +`, + "inventory": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-inventory-obj + namespace: test-namespace + labels: + cli-utils.sigs.k8s.io/inventory-id: test-app-label +data: {} +`, + "obj1": ` +apiVersion: v1 +kind: Pod +metadata: + name: obj1 + namespace: test-namespace +spec: {} +`, + "obj2": ` +apiVersion: v1 +kind: Pod +metadata: + name: obj2 + namespace: test-namespace +spec: {} +`, + "clusterScopedObj": ` +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: cluster-scoped-1 +`, + } +) + +//nolint:dupl // event lists are very similar +func TestApplier(t *testing.T) { + testCases := map[string]struct { + namespace string + // resources input to applier + resources object.UnstructuredSet + // inventory input to applier + invInfo inventoryInfo + // objects in the cluster + clusterObjs object.UnstructuredSet + // options input to applier.Run + options ApplierOptions + // fake input events from the status poller + statusEvents []pollevent.Event + // expected output status events (async) + expectedStatusEvents []testutil.ExpEvent + // expected output events + expectedEvents []testutil.ExpEvent + // true if runTimeout is expected to have caused cancellation + expectRunTimeout bool + // true if testTimeout is expected to have caused cancellation + expectTestTimeout bool + }{ + "initial apply without status or prune": { + namespace: "default", + resources: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"]), + }, + invInfo: inventoryInfo{ + name: "abc-123", + namespace: "default", + id: "test", + }, + clusterObjs: object.UnstructuredSet{}, + options: ApplierOptions{ + NoPrune: true, + InventoryPolicy: inventory.PolicyMustMatch, + }, + expectedEvents: []testutil.ExpEvent{ + { + EventType: event.InitType, + InitEvent: &testutil.ExpInitEvent{}, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-add-0", + Action: event.InventoryAction, + Type: event.Started, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-add-0", + Action: event.InventoryAction, + Type: event.Finished, + }, + }, + + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "apply-0", + Action: event.ApplyAction, + Type: event.Started, + }, + }, + { + EventType: event.ApplyType, + ApplyEvent: &testutil.ExpApplyEvent{ + GroupName: "apply-0", + Operation: event.Created, // Create new + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "apply-0", + Action: event.ApplyAction, + Type: event.Finished, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "wait-0", + Action: event.WaitAction, + Type: event.Started, + }, + }, + { + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.ReconcilePending, + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + // Timeout waiting for status event saying deployment is current + // TODO: update inventory after timeout + // { + // EventType: event.ActionGroupType, + // ActionGroupEvent: &testutil.ExpActionGroupEvent{ + // GroupName: "wait-0", + // Action: event.WaitAction, + // Type: event.Finished, + // }, + // }, + // { + // EventType: event.ActionGroupType, + // ActionGroupEvent: &testutil.ExpActionGroupEvent{ + // GroupName: "inventory-set-0", + // Action: event.InventoryAction, + // Type: event.Started, + // }, + // }, + // { + // EventType: event.ActionGroupType, + // ActionGroupEvent: &testutil.ExpActionGroupEvent{ + // GroupName: "inventory-set-0", + // Action: event.InventoryAction, + // Type: event.Finished, + // }, + // }, + }, + expectTestTimeout: true, + }, + "first apply multiple resources with status and prune": { + namespace: "default", + resources: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"]), + testutil.Unstructured(t, resources["secret"]), + }, + invInfo: inventoryInfo{ + name: "inv-123", + namespace: "default", + id: "test", + }, + clusterObjs: object.UnstructuredSet{}, + options: ApplierOptions{ + ReconcileTimeout: time.Minute, + InventoryPolicy: inventory.PolicyMustMatch, + EmitStatusEvents: true, + }, + statusEvents: []pollevent.Event{ + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.InProgressStatus, + Resource: testutil.Unstructured(t, resources["deployment"]), + }, + }, + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.CurrentStatus, + Resource: testutil.Unstructured(t, resources["deployment"]), + }, + }, + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["secret"]), + Status: status.CurrentStatus, + Resource: testutil.Unstructured(t, resources["secret"]), + }, + }, + }, + expectedStatusEvents: []testutil.ExpEvent{ + { + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.InProgressStatus, + }, + }, + { + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.CurrentStatus, + }, + }, + { + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: testutil.ToIdentifier(t, resources["secret"]), + Status: status.CurrentStatus, + }, + }, + }, + expectedEvents: []testutil.ExpEvent{ + { + EventType: event.InitType, + InitEvent: &testutil.ExpInitEvent{}, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-add-0", + Action: event.InventoryAction, + Type: event.Started, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-add-0", + Action: event.InventoryAction, + Type: event.Finished, + }, + }, + + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "apply-0", + Action: event.ApplyAction, + Type: event.Started, + }, + }, + // Secrets applied before Deployments (see pkg/ordering) + { + EventType: event.ApplyType, + ApplyEvent: &testutil.ExpApplyEvent{ + GroupName: "apply-0", + Operation: event.Created, // Create new + Identifier: testutil.ToIdentifier(t, resources["secret"]), + }, + }, + { + EventType: event.ApplyType, + ApplyEvent: &testutil.ExpApplyEvent{ + GroupName: "apply-0", + Operation: event.Created, // Create new + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "apply-0", + Action: event.ApplyAction, + Type: event.Finished, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "wait-0", + Action: event.WaitAction, + Type: event.Started, + }, + }, + // Secrets before Deployments (see pkg/ordering) + { + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.ReconcilePending, + Identifier: testutil.ToIdentifier(t, resources["secret"]), + }, + }, + { + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.ReconcilePending, + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + // Deployment before Secret (see statusEvents) + { + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.Reconciled, + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + { + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.Reconciled, + Identifier: testutil.ToIdentifier(t, resources["secret"]), + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "wait-0", + Action: event.WaitAction, + Type: event.Finished, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-set-0", + Action: event.InventoryAction, + Type: event.Started, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-set-0", + Action: event.InventoryAction, + Type: event.Finished, + }, + }, + }, + }, + "apply multiple existing resources with status and prune": { + namespace: "default", + resources: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"]), + testutil.Unstructured(t, resources["secret"]), + }, + invInfo: inventoryInfo{ + name: "inv-123", + namespace: "default", + id: "test", + set: object.ObjMetadataSet{ + object.UnstructuredToObjMetadata( + testutil.Unstructured(t, resources["deployment"]), + ), + }, + }, + clusterObjs: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"]), + }, + options: ApplierOptions{ + ReconcileTimeout: time.Minute, + InventoryPolicy: inventory.PolicyAdoptIfNoInventory, + EmitStatusEvents: true, + }, + statusEvents: []pollevent.Event{ + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.CurrentStatus, + Resource: testutil.Unstructured(t, resources["deployment"]), + }, + }, + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["secret"]), + Status: status.CurrentStatus, + Resource: testutil.Unstructured(t, resources["secret"]), + }, + }, + }, + expectedStatusEvents: []testutil.ExpEvent{ + { + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.CurrentStatus, + }, + }, + { + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: testutil.ToIdentifier(t, resources["secret"]), + Status: status.CurrentStatus, + }, + }, + }, + expectedEvents: []testutil.ExpEvent{ + { + EventType: event.InitType, + InitEvent: &testutil.ExpInitEvent{}, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-add-0", + Action: event.InventoryAction, + Type: event.Started, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-add-0", + Action: event.InventoryAction, + Type: event.Finished, + }, + }, + + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "apply-0", + Action: event.ApplyAction, + Type: event.Started, + }, + }, + // Apply Secrets before Deployments (see ordering.SortableMetas) + { + EventType: event.ApplyType, + ApplyEvent: &testutil.ExpApplyEvent{ + GroupName: "apply-0", + Operation: event.Created, // Create new + Identifier: testutil.ToIdentifier(t, resources["secret"]), + }, + }, + { + EventType: event.ApplyType, + ApplyEvent: &testutil.ExpApplyEvent{ + GroupName: "apply-0", + Operation: event.Configured, // Update existing + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "apply-0", + Action: event.ApplyAction, + Type: event.Finished, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "wait-0", + Action: event.WaitAction, + Type: event.Started, + }, + }, + // Apply Secrets before Deployments (see ordering.SortableMetas) + { + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.ReconcilePending, + Identifier: testutil.ToIdentifier(t, resources["secret"]), + }, + }, + { + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.ReconcilePending, + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + // Wait Deployments before Secrets (see testutil.GroupedEventsByID) + { + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.Reconciled, + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + { + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.Reconciled, + Identifier: testutil.ToIdentifier(t, resources["secret"]), + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "wait-0", + Action: event.WaitAction, + Type: event.Finished, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-set-0", + Action: event.InventoryAction, + Type: event.Started, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-set-0", + Action: event.InventoryAction, + Type: event.Finished, + }, + }, + }, + }, + "apply no resources and prune all existing": { + namespace: "default", + resources: object.UnstructuredSet{}, + invInfo: inventoryInfo{ + name: "inv-123", + namespace: "default", + id: "test", + set: object.ObjMetadataSet{ + object.UnstructuredToObjMetadata( + testutil.Unstructured(t, resources["deployment"]), + ), + object.UnstructuredToObjMetadata( + testutil.Unstructured(t, resources["secret"]), + ), + }, + }, + clusterObjs: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")), + testutil.Unstructured(t, resources["secret"], testutil.AddOwningInv(t, "test")), + }, + options: ApplierOptions{ + InventoryPolicy: inventory.PolicyMustMatch, + EmitStatusEvents: true, + }, + statusEvents: []pollevent.Event{ + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.InProgressStatus, + }, + }, + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["secret"]), + Status: status.InProgressStatus, + }, + }, + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.NotFoundStatus, + }, + }, + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["secret"]), + Status: status.NotFoundStatus, + }, + }, + }, + expectedStatusEvents: []testutil.ExpEvent{ + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-add-0", + Action: event.InventoryAction, + Type: event.Started, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-add-0", + Action: event.InventoryAction, + Type: event.Finished, + }, + }, + { + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.InProgressStatus, + }, + }, + { + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: testutil.ToIdentifier(t, resources["secret"]), + Status: status.InProgressStatus, + }, + }, + { + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.NotFoundStatus, + }, + }, + { + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: testutil.ToIdentifier(t, resources["secret"]), + Status: status.NotFoundStatus, + }, + }, + }, + expectedEvents: []testutil.ExpEvent{ + { + EventType: event.InitType, + InitEvent: &testutil.ExpInitEvent{}, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "prune-0", + Action: event.PruneAction, + Type: event.Started, + }, + }, + // Prune Deployments before Secrets (see ordering.SortableMetas) + { + EventType: event.PruneType, + PruneEvent: &testutil.ExpPruneEvent{ + GroupName: "prune-0", + Operation: event.Pruned, + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + { + EventType: event.PruneType, + PruneEvent: &testutil.ExpPruneEvent{ + GroupName: "prune-0", + Operation: event.Pruned, + Identifier: testutil.ToIdentifier(t, resources["secret"]), + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "prune-0", + Action: event.PruneAction, + Type: event.Finished, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "wait-0", + Action: event.WaitAction, + Type: event.Started, + }, + }, + // Prune Deployments before Secrets (see ordering.SortableMetas) + { + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.ReconcilePending, + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + { + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.ReconcilePending, + Identifier: testutil.ToIdentifier(t, resources["secret"]), + }, + }, + // Wait Deployments before Secrets (see testutil.GroupedEventsByID) + { + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.Reconciled, + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + { + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.Reconciled, + Identifier: testutil.ToIdentifier(t, resources["secret"]), + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "wait-0", + Action: event.WaitAction, + Type: event.Finished, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-set-0", + Action: event.InventoryAction, + Type: event.Started, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-set-0", + Action: event.InventoryAction, + Type: event.Finished, + }, + }, + }, + }, + "apply resource with existing object belonging to different inventory": { + namespace: "default", + resources: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"]), + }, + invInfo: inventoryInfo{ + name: "abc-123", + namespace: "default", + id: "test", + }, + clusterObjs: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "unmatched")), + }, + options: ApplierOptions{ + ReconcileTimeout: time.Minute, + InventoryPolicy: inventory.PolicyMustMatch, + EmitStatusEvents: true, + }, + statusEvents: []pollevent.Event{ + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.InProgressStatus, + }, + }, + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.CurrentStatus, + }, + }, + }, + expectedStatusEvents: []testutil.ExpEvent{ + { + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.InProgressStatus, + }, + }, + { + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.CurrentStatus, + }, + }, + }, + expectedEvents: []testutil.ExpEvent{ + { + EventType: event.InitType, + InitEvent: &testutil.ExpInitEvent{}, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-add-0", + Action: event.InventoryAction, + Type: event.Started, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-add-0", + Action: event.InventoryAction, + Type: event.Finished, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "apply-0", + Action: event.ApplyAction, + Type: event.Started, + }, + }, + { + EventType: event.ApplyType, + ApplyEvent: &testutil.ExpApplyEvent{ + GroupName: "apply-0", + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Error: testutil.EqualErrorType(inventory.NewInventoryOverlapError(fmt.Errorf(""))), + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "apply-0", + Action: event.ApplyAction, + Type: event.Finished, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "wait-0", + Action: event.WaitAction, + Type: event.Started, + }, + }, + { + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.ReconcileSkipped, + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "wait-0", + Action: event.WaitAction, + Type: event.Finished, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-set-0", + Action: event.InventoryAction, + Type: event.Started, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-set-0", + Action: event.InventoryAction, + Type: event.Finished, + }, + }, + }, + }, + "resources belonging to a different inventory should not be pruned": { + namespace: "default", + resources: object.UnstructuredSet{}, + invInfo: inventoryInfo{ + name: "abc-123", + namespace: "default", + id: "test", + set: object.ObjMetadataSet{ + object.UnstructuredToObjMetadata( + testutil.Unstructured(t, resources["deployment"]), + ), + }, + }, + clusterObjs: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "unmatched")), + }, + options: ApplierOptions{ + InventoryPolicy: inventory.PolicyMustMatch, + EmitStatusEvents: true, + }, + expectedEvents: []testutil.ExpEvent{ + { + EventType: event.InitType, + InitEvent: &testutil.ExpInitEvent{}, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-add-0", + Action: event.InventoryAction, + Type: event.Started, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-add-0", + Action: event.InventoryAction, + Type: event.Finished, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "prune-0", + Action: event.PruneAction, + Type: event.Started, + }, + }, + { + EventType: event.PruneType, + PruneEvent: &testutil.ExpPruneEvent{ + GroupName: "prune-0", + Operation: event.PruneSkipped, + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "prune-0", + Action: event.PruneAction, + Type: event.Finished, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "wait-0", + Action: event.WaitAction, + Type: event.Started, + }, + }, + { + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.ReconcileSkipped, + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "wait-0", + Action: event.WaitAction, + Type: event.Finished, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-set-0", + Action: event.InventoryAction, + Type: event.Started, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-set-0", + Action: event.InventoryAction, + Type: event.Finished, + }, + }, + }, + }, + "prune with inventory object annotation matched": { + namespace: "default", + resources: object.UnstructuredSet{}, + invInfo: inventoryInfo{ + name: "abc-123", + namespace: "default", + id: "test", + set: object.ObjMetadataSet{ + object.UnstructuredToObjMetadata( + testutil.Unstructured(t, resources["deployment"]), + ), + }, + }, + clusterObjs: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")), + }, + options: ApplierOptions{ + InventoryPolicy: inventory.PolicyMustMatch, + EmitStatusEvents: true, + }, + statusEvents: []pollevent.Event{ + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.InProgressStatus, + }, + }, + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.NotFoundStatus, + }, + }, + }, + expectedStatusEvents: []testutil.ExpEvent{ + { + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.InProgressStatus, + }, + }, + { + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.NotFoundStatus, + }, + }, + }, + expectedEvents: []testutil.ExpEvent{ + { + EventType: event.InitType, + InitEvent: &testutil.ExpInitEvent{}, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-add-0", + Action: event.InventoryAction, + Type: event.Started, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-add-0", + Action: event.InventoryAction, + Type: event.Finished, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "prune-0", + Action: event.PruneAction, + Type: event.Started, + }, + }, + { + EventType: event.PruneType, + PruneEvent: &testutil.ExpPruneEvent{ + GroupName: "prune-0", + Operation: event.Pruned, + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "prune-0", + Action: event.PruneAction, + Type: event.Finished, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "wait-0", + Action: event.WaitAction, + Type: event.Started, + }, + }, + { + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.ReconcilePending, + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + { + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.Reconciled, + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "wait-0", + Action: event.WaitAction, + Type: event.Finished, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-set-0", + Action: event.InventoryAction, + Type: event.Started, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-set-0", + Action: event.InventoryAction, + Type: event.Finished, + }, + }, + }, + }, + "SkipInvalid - skip invalid objects and apply valid objects": { + namespace: "default", + resources: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"], JSONPathSetter{ + "$.metadata.name", "", + }), + testutil.Unstructured(t, resources["deployment"], JSONPathSetter{ + "$.kind", "", + }), + testutil.Unstructured(t, resources["secret"]), + }, + invInfo: inventoryInfo{ + name: "inv-123", + namespace: "default", + id: "test", + }, + clusterObjs: object.UnstructuredSet{}, + options: ApplierOptions{ + ReconcileTimeout: time.Minute, + InventoryPolicy: inventory.PolicyAdoptIfNoInventory, + EmitStatusEvents: true, + ValidationPolicy: validation.SkipInvalid, + }, + statusEvents: []pollevent.Event{ + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["secret"]), + Status: status.CurrentStatus, + Resource: testutil.Unstructured(t, resources["secret"]), + }, + }, + }, + expectedStatusEvents: []testutil.ExpEvent{ + { + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: testutil.ToIdentifier(t, resources["secret"]), + Status: status.CurrentStatus, + }, + }, + }, + expectedEvents: []testutil.ExpEvent{ + { + EventType: event.ValidationType, + ValidationEvent: &testutil.ExpValidationEvent{ + Identifiers: object.ObjMetadataSet{ + object.UnstructuredToObjMetadata( + testutil.Unstructured(t, resources["deployment"], JSONPathSetter{ + "$.kind", "", + }), + ), + }, + Error: testutil.EqualErrorString(validation.NewError( + field.Required(field.NewPath("kind"), "kind is required"), + object.UnstructuredToObjMetadata( + testutil.Unstructured(t, resources["deployment"], JSONPathSetter{ + "$.kind", "", + }), + ), + ).Error()), + }, + }, + { + EventType: event.ValidationType, + ValidationEvent: &testutil.ExpValidationEvent{ + Identifiers: object.ObjMetadataSet{ + object.UnstructuredToObjMetadata( + testutil.Unstructured(t, resources["deployment"], JSONPathSetter{ + "$.metadata.name", "", + }), + ), + }, + Error: testutil.EqualErrorString(validation.NewError( + field.Required(field.NewPath("metadata", "name"), "name is required"), + object.UnstructuredToObjMetadata( + testutil.Unstructured(t, resources["deployment"], JSONPathSetter{ + "$.metadata.name", "", + }), + ), + ).Error()), + }, + }, + { + EventType: event.InitType, + InitEvent: &testutil.ExpInitEvent{}, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-add-0", + Action: event.InventoryAction, + Type: event.Started, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-add-0", + Action: event.InventoryAction, + Type: event.Finished, + }, + }, + + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "apply-0", + Action: event.ApplyAction, + Type: event.Started, + }, + }, + // Secret applied + { + EventType: event.ApplyType, + ApplyEvent: &testutil.ExpApplyEvent{ + GroupName: "apply-0", + Operation: event.Created, // Create new + Identifier: testutil.ToIdentifier(t, resources["secret"]), + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "apply-0", + Action: event.ApplyAction, + Type: event.Finished, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "wait-0", + Action: event.WaitAction, + Type: event.Started, + }, + }, + // Secret pending + { + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.ReconcilePending, + Identifier: testutil.ToIdentifier(t, resources["secret"]), + }, + }, + // Secret reconciled + { + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.Reconciled, + Identifier: testutil.ToIdentifier(t, resources["secret"]), + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "wait-0", + Action: event.WaitAction, + Type: event.Finished, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-set-0", + Action: event.InventoryAction, + Type: event.Started, + }, + }, + { + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + GroupName: "inventory-set-0", + Action: event.InventoryAction, + Type: event.Finished, + }, + }, + }, + }, + "ExitEarly - exit early on invalid objects and skip valid objects": { + namespace: "default", + resources: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"], JSONPathSetter{ + "$.metadata.name", "", + }), + testutil.Unstructured(t, resources["deployment"], JSONPathSetter{ + "$.kind", "", + }), + testutil.Unstructured(t, resources["secret"]), + }, + invInfo: inventoryInfo{ + name: "inv-123", + namespace: "default", + id: "test", + }, + clusterObjs: object.UnstructuredSet{}, + options: ApplierOptions{ + ReconcileTimeout: time.Minute, + InventoryPolicy: inventory.PolicyAdoptIfNoInventory, + EmitStatusEvents: true, + ValidationPolicy: validation.ExitEarly, + }, + statusEvents: []pollevent.Event{}, + expectedStatusEvents: []testutil.ExpEvent{}, + expectedEvents: []testutil.ExpEvent{ + { + EventType: event.ErrorType, + ErrorEvent: &testutil.ExpErrorEvent{ + Err: testutil.EqualErrorString(multierror.New( + validation.NewError( + field.Required(field.NewPath("metadata", "name"), "name is required"), + object.UnstructuredToObjMetadata( + testutil.Unstructured(t, resources["deployment"], JSONPathSetter{ + "$.metadata.name", "", + }), + ), + ), + validation.NewError( + field.Required(field.NewPath("kind"), "kind is required"), + object.UnstructuredToObjMetadata( + testutil.Unstructured(t, resources["deployment"], JSONPathSetter{ + "$.kind", "", + }), + ), + ), + ).Error()), + }, + }, + }, + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + poller := newFakePoller(tc.statusEvents) + + // Only feed valid objects into the TestApplier. + // Invalid objects should not generate API requests. + validObjs := object.UnstructuredSet{} + for _, obj := range tc.resources { + id := object.UnstructuredToObjMetadata(obj) + if id.GroupKind.Kind == "" || id.Name == "" { + continue + } + validObjs = append(validObjs, obj) + } + + applier := newTestApplier(t, + tc.invInfo, + validObjs, + tc.clusterObjs, + poller, + ) + + // Context for Applier.Run + runCtx, runCancel := context.WithCancel(context.Background()) + defer runCancel() // cleanup + + // Context for this test (in case Applier.Run never closes the event channel) + testTimeout := 10 * time.Second + testCtx, testCancel := context.WithTimeout(context.Background(), testTimeout) + defer testCancel() // cleanup + + eventChannel := applier.Run(runCtx, tc.invInfo.toWrapped(), tc.resources, tc.options) + + // only start sending events once + var once sync.Once + + var events []event.Event + + loop: + for { + select { + case <-testCtx.Done(): + // Test timed out + runCancel() + if tc.expectTestTimeout { + assert.Equal(t, context.DeadlineExceeded, testCtx.Err(), "Applier.Run failed to exit, but not because of expected timeout") + } else { + t.Errorf("Applier.Run failed to exit (timeout: %s)", testTimeout) + } + break loop + + case e, ok := <-eventChannel: + if !ok { + // Event channel closed + testCancel() + break loop + } + if e.Type == event.ActionGroupType && + e.ActionGroupEvent.Type == event.Finished { + // Send events after the first apply/prune task ends + if e.ActionGroupEvent.Action == event.ApplyAction || + e.ActionGroupEvent.Action == event.PruneAction { + once.Do(func() { + // start events + poller.Start() + }) + } + } + events = append(events, e) + } + } + + // Convert events to test events for comparison + receivedEvents := testutil.EventsToExpEvents(events) + + // Validate & remove expected status events + for _, e := range tc.expectedStatusEvents { + var removed int + receivedEvents, removed = testutil.RemoveEqualEvents(receivedEvents, e) + if removed < 1 { + t.Fatalf("Expected status event not received: %#v", e.StatusEvent) + } + } + + // sort to allow comparison of multiple apply/prune tasks in the same task group + sort.Sort(testutil.GroupedEventsByID(receivedEvents)) + + // Validate the rest of the events + testutil.AssertEqual(t, tc.expectedEvents, receivedEvents, + "Actual events (%d) do not match expected events (%d)", + len(receivedEvents), len(tc.expectedEvents)) + + // Validate that the expected timeout was the cause of the run completion. + // just in case something else cancelled the run + switch { + case tc.expectRunTimeout: + assert.Equal(t, context.DeadlineExceeded, runCtx.Err(), "Applier.Run exited, but not by expected context timeout") + case tc.expectTestTimeout: + assert.Equal(t, context.Canceled, runCtx.Err(), "Applier.Run exited, but not because of expected context cancellation") + default: + assert.Nil(t, runCtx.Err(), "Applier.Run exited, but context error is not nil") + } + }) + } +} + +func TestApplierCancel(t *testing.T) { + testCases := map[string]struct { + // resources input to applier + resources object.UnstructuredSet + // inventory input to applier + invInfo inventoryInfo + // objects in the cluster + clusterObjs object.UnstructuredSet + // options input to applier.Run + options ApplierOptions + // timeout for applier.Run + runTimeout time.Duration + // timeout for the test + testTimeout time.Duration + // fake input events from the status poller + statusEvents []pollevent.Event + // expected output status events (async) + expectedStatusEvents []testutil.ExpEvent + // expected output events + expectedEvents []testutil.ExpEvent + // true if runTimeout is expected to have caused cancellation + expectRunTimeout bool + }{ + "cancelled by caller while waiting for reconcile": { + expectRunTimeout: true, + runTimeout: 2 * time.Second, + testTimeout: 30 * time.Second, + resources: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"]), + }, + invInfo: inventoryInfo{ + name: "abc-123", + namespace: "test", + id: "test", + }, + clusterObjs: object.UnstructuredSet{}, + options: ApplierOptions{ + // EmitStatusEvents required to test event output + EmitStatusEvents: true, + NoPrune: true, + InventoryPolicy: inventory.PolicyMustMatch, + // ReconcileTimeout required to enable WaitTasks + ReconcileTimeout: 1 * time.Minute, + }, + statusEvents: []pollevent.Event{ + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.InProgressStatus, + Resource: testutil.Unstructured(t, resources["deployment"]), + }, + }, + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.InProgressStatus, + Resource: testutil.Unstructured(t, resources["deployment"]), + }, + }, + // Resource never becomes Current, blocking applier.Run from exiting + }, + expectedStatusEvents: []testutil.ExpEvent{ + { + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.InProgressStatus, + }, + }, + }, + expectedEvents: []testutil.ExpEvent{ + { + // InitTask + EventType: event.InitType, + InitEvent: &testutil.ExpInitEvent{}, + }, + { + // InvAddTask start + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.InventoryAction, + GroupName: "inventory-add-0", + Type: event.Started, + }, + }, + { + // InvAddTask finished + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.InventoryAction, + GroupName: "inventory-add-0", + Type: event.Finished, + }, + }, + { + // ApplyTask start + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.ApplyAction, + GroupName: "apply-0", + Type: event.Started, + }, + }, + { + // Apply Deployment + EventType: event.ApplyType, + ApplyEvent: &testutil.ExpApplyEvent{ + GroupName: "apply-0", + Operation: event.Created, + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + { + // ApplyTask finished + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.ApplyAction, + GroupName: "apply-0", + Type: event.Finished, + }, + }, + { + // WaitTask start + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.WaitAction, + GroupName: "wait-0", + Type: event.Started, + }, + }, + { + // Deployment reconcile pending. + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Operation: event.ReconcilePending, + }, + }, + // Deployment never becomes Current. + // WaitTask is expected to be cancelled before ReconcileTimeout. + // Cancelled WaitTask do not sent individual timeout WaitEvents + { + // WaitTask finished + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.WaitAction, + GroupName: "wait-0", + Type: event.Finished, // TODO: add Cancelled event type + }, + }, + // TODO: Update the inventory after cancellation + // { + // // InvSetTask start + // EventType: event.ActionGroupType, + // ActionGroupEvent: &testutil.ExpActionGroupEvent{ + // Action: event.InventoryAction, + // GroupName: "inventory-set-0", + // Type: event.Started, + // }, + // }, + // { + // // InvSetTask finished + // EventType: event.ActionGroupType, + // ActionGroupEvent: &testutil.ExpActionGroupEvent{ + // Action: event.InventoryAction, + // GroupName: "inventory-set-0", + // Type: event.Finished, + // }, + // }, + { + // Error + EventType: event.ErrorType, + ErrorEvent: &testutil.ExpErrorEvent{ + Err: context.DeadlineExceeded, + }, + }, + }, + }, + "completed with timeout": { + expectRunTimeout: false, + runTimeout: 10 * time.Second, + testTimeout: 30 * time.Second, + resources: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"]), + }, + invInfo: inventoryInfo{ + name: "abc-123", + namespace: "test", + id: "test", + }, + clusterObjs: object.UnstructuredSet{}, + options: ApplierOptions{ + // EmitStatusEvents required to test event output + EmitStatusEvents: true, + NoPrune: true, + InventoryPolicy: inventory.PolicyMustMatch, + // ReconcileTimeout required to enable WaitTasks + ReconcileTimeout: 1 * time.Minute, + }, + statusEvents: []pollevent.Event{ + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.InProgressStatus, + Resource: testutil.Unstructured(t, resources["deployment"]), + }, + }, + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.CurrentStatus, + Resource: testutil.Unstructured(t, resources["deployment"]), + }, + }, + // Resource becoming Current should unblock applier.Run WaitTask + }, + expectedStatusEvents: []testutil.ExpEvent{ + { + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.InProgressStatus, + }, + }, + { + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.CurrentStatus, + }, + }, + }, + expectedEvents: []testutil.ExpEvent{ + { + // InitTask + EventType: event.InitType, + InitEvent: &testutil.ExpInitEvent{}, + }, + { + // InvAddTask start + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.InventoryAction, + GroupName: "inventory-add-0", + Type: event.Started, + }, + }, + { + // InvAddTask finished + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.InventoryAction, + GroupName: "inventory-add-0", + Type: event.Finished, + }, + }, + { + // ApplyTask start + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.ApplyAction, + GroupName: "apply-0", + Type: event.Started, + }, + }, + { + // Apply Deployment + EventType: event.ApplyType, + ApplyEvent: &testutil.ExpApplyEvent{ + GroupName: "apply-0", + Operation: event.Created, + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + { + // ApplyTask finished + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.ApplyAction, + GroupName: "apply-0", + Type: event.Finished, + }, + }, + { + // WaitTask start + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.WaitAction, + GroupName: "wait-0", + Type: event.Started, + }, + }, + { + // Deployment reconcile pending. + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Operation: event.ReconcilePending, + }, + }, + { + // Deployment becomes Current. + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Operation: event.Reconciled, + }, + }, + { + // WaitTask finished + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.WaitAction, + GroupName: "wait-0", + Type: event.Finished, + }, + }, + { + // InvSetTask start + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.InventoryAction, + GroupName: "inventory-set-0", + Type: event.Started, + }, + }, + { + // InvSetTask finished + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.InventoryAction, + GroupName: "inventory-set-0", + Type: event.Finished, + }, + }, + }, + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + poller := newFakePoller(tc.statusEvents) + + applier := newTestApplier(t, + tc.invInfo, + tc.resources, + tc.clusterObjs, + poller, + ) + + // Context for Applier.Run + runCtx, runCancel := context.WithTimeout(context.Background(), tc.runTimeout) + defer runCancel() // cleanup + + // Context for this test (in case Applier.Run never closes the event channel) + testCtx, testCancel := context.WithTimeout(context.Background(), tc.testTimeout) + defer testCancel() // cleanup + + eventChannel := applier.Run(runCtx, tc.invInfo.toWrapped(), tc.resources, tc.options) + + // only start sending events once + var once sync.Once + + var events []event.Event + + loop: + for { + select { + case <-testCtx.Done(): + // Test timed out + runCancel() + t.Errorf("Applier.Run failed to respond to cancellation (expected: %s, timeout: %s)", tc.runTimeout, tc.testTimeout) + break loop + + case e, ok := <-eventChannel: + if !ok { + // Event channel closed + testCancel() + break loop + } + events = append(events, e) + + if e.Type == event.ActionGroupType && + e.ActionGroupEvent.Type == event.Finished { + // Send events after the first apply/prune task ends + if e.ActionGroupEvent.Action == event.ApplyAction || + e.ActionGroupEvent.Action == event.PruneAction { + once.Do(func() { + // start events + poller.Start() + }) + } + } + } + } + + // Convert events to test events for comparison + receivedEvents := testutil.EventsToExpEvents(events) + + // Validate & remove expected status events + for _, e := range tc.expectedStatusEvents { + var removed int + receivedEvents, removed = testutil.RemoveEqualEvents(receivedEvents, e) + if removed < 1 { + t.Fatalf("Expected status event not received: %#v", e.StatusEvent) + } + } + + // Validate the rest of the events + testutil.AssertEqual(t, tc.expectedEvents, receivedEvents, + "Actual events (%d) do not match expected events (%d)", + len(receivedEvents), len(tc.expectedEvents)) + + // Validate that the expected timeout was the cause of the run completion. + // just in case something else cancelled the run + if tc.expectRunTimeout { + assert.Equal(t, context.DeadlineExceeded, runCtx.Err(), "Applier.Run exited, but not by expected timeout") + } else { + assert.NoError(t, runCtx.Err(), "Applier.Run exited, but not by expected timeout") + } + }) + } +} + +func TestReadAndPrepareObjectsNilInv(t *testing.T) { + applier := Applier{} + _, _, err := applier.prepareObjects(nil, object.UnstructuredSet{}, ApplierOptions{}) + assert.Error(t, err) +} + +func TestReadAndPrepareObjects(t *testing.T) { + inventoryObj := testutil.Unstructured(t, resources["inventory"]) + inventory := inventory.WrapInventoryInfoObj(inventoryObj) + + obj1 := testutil.Unstructured(t, resources["obj1"]) + obj2 := testutil.Unstructured(t, resources["obj2"]) + clusterScopedObj := testutil.Unstructured(t, resources["clusterScopedObj"]) + + testCases := map[string]struct { + // objects in the cluster + clusterObjs object.UnstructuredSet + // inventory input to applier + invInfo inventoryInfo + // resources input to applier + resources object.UnstructuredSet + // expected objects to apply + applyObjs object.UnstructuredSet + // expected objects to prune + pruneObjs object.UnstructuredSet + // expected error + isError bool + }{ + "objects include inventory": { + invInfo: inventoryInfo{ + name: inventory.Name(), + namespace: inventory.Namespace(), + id: inventory.ID(), + }, + resources: object.UnstructuredSet{inventoryObj}, + isError: true, + }, + "empty inventory, empty objects, apply none, prune none": { + invInfo: inventoryInfo{ + name: inventory.Name(), + namespace: inventory.Namespace(), + id: inventory.ID(), + }, + }, + "one in inventory, empty objects, prune one": { + clusterObjs: object.UnstructuredSet{obj1}, + invInfo: inventoryInfo{ + name: inventory.Name(), + namespace: inventory.Namespace(), + id: inventory.ID(), + set: object.ObjMetadataSet{ + object.UnstructuredToObjMetadata(obj1), + }, + }, + pruneObjs: object.UnstructuredSet{obj1}, + }, + "all in inventory, apply all": { + invInfo: inventoryInfo{ + name: inventory.Name(), + namespace: inventory.Namespace(), + id: inventory.ID(), + set: object.ObjMetadataSet{ + object.UnstructuredToObjMetadata(obj1), + object.UnstructuredToObjMetadata(clusterScopedObj), + }, + }, + resources: object.UnstructuredSet{obj1, clusterScopedObj}, + applyObjs: object.UnstructuredSet{obj1, clusterScopedObj}, + }, + "disjoint set, apply new, prune old": { + clusterObjs: object.UnstructuredSet{obj2}, + invInfo: inventoryInfo{ + name: inventory.Name(), + namespace: inventory.Namespace(), + id: inventory.ID(), + set: object.ObjMetadataSet{ + object.UnstructuredToObjMetadata(obj2), + }, + }, + resources: object.UnstructuredSet{obj1, clusterScopedObj}, + applyObjs: object.UnstructuredSet{obj1, clusterScopedObj}, + pruneObjs: object.UnstructuredSet{obj2}, + }, + "most in inventory, apply all": { + clusterObjs: object.UnstructuredSet{obj2}, + invInfo: inventoryInfo{ + name: inventory.Name(), + namespace: inventory.Namespace(), + id: inventory.ID(), + set: object.ObjMetadataSet{ + object.UnstructuredToObjMetadata(obj2), + }, + }, + resources: object.UnstructuredSet{obj1, obj2, clusterScopedObj}, + applyObjs: object.UnstructuredSet{obj1, obj2, clusterScopedObj}, + pruneObjs: object.UnstructuredSet{}, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + applier := newTestApplier(t, + tc.invInfo, + tc.resources, + tc.clusterObjs, + // no events needed for prepareObjects + newFakePoller([]pollevent.Event{}), + ) + + applyObjs, pruneObjs, err := applier.prepareObjects(tc.invInfo.toWrapped(), tc.resources, ApplierOptions{}) + if tc.isError { + assert.Error(t, err) + return + } + require.NoError(t, err) + + testutil.AssertEqual(t, applyObjs, tc.applyObjs, + "Actual applied objects (%d) do not match expected applied objects (%d)", + len(applyObjs), len(tc.applyObjs)) + + testutil.AssertEqual(t, pruneObjs, tc.pruneObjs, + "Actual pruned objects (%d) do not match expected pruned objects (%d)", + len(pruneObjs), len(tc.pruneObjs)) + }) + } +} diff --git a/thirdparty/cli-utils/apply/common_test.go b/thirdparty/cli-utils/apply/common_test.go new file mode 100644 index 0000000000..6e7a94b6c0 --- /dev/null +++ b/thirdparty/cli-utils/apply/common_test.go @@ -0,0 +1,475 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package apply + +import ( + "bytes" + "context" + "fmt" + "io/ioutil" + "net/http" + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/resource" + dynamicfake "k8s.io/client-go/dynamic/fake" + "k8s.io/client-go/rest/fake" + clienttesting "k8s.io/client-go/testing" + "k8s.io/klog/v2" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + "k8s.io/kubectl/pkg/scheme" + "sigs.k8s.io/cli-utils/pkg/apply/poller" + "sigs.k8s.io/cli-utils/pkg/common" + "sigs.k8s.io/cli-utils/pkg/inventory" + "sigs.k8s.io/cli-utils/pkg/jsonpath" + "sigs.k8s.io/cli-utils/pkg/kstatus/polling" + pollevent "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event" + "sigs.k8s.io/cli-utils/pkg/object" +) + +type inventoryInfo struct { + name string + namespace string + id string + set object.ObjMetadataSet +} + +func (i inventoryInfo) toUnstructured() *unstructured.Unstructured { + invMap := make(map[string]interface{}) + for _, objMeta := range i.set { + invMap[objMeta.String()] = "" + } + + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": i.name, + "namespace": i.namespace, + "labels": map[string]interface{}{ + common.InventoryLabel: i.id, + }, + }, + "data": invMap, + }, + } +} + +func (i inventoryInfo) toWrapped() inventory.Info { + return inventory.WrapInventoryInfoObj(i.toUnstructured()) +} + +func newTestApplier( + t *testing.T, + invInfo inventoryInfo, + resources object.UnstructuredSet, + clusterObjs object.UnstructuredSet, + statusPoller poller.Poller, +) *Applier { + tf := newTestFactory(t, invInfo, resources, clusterObjs) + defer tf.Cleanup() + + infoHelper := &fakeInfoHelper{ + factory: tf, + } + + invClient := newTestInventory(t, tf) + + applier, err := NewApplierBuilder(). + WithFactory(tf). + WithInventoryClient(invClient). + WithStatusPoller(statusPoller). + Build() + require.NoError(t, err) + + // Inject the fakeInfoHelper to allow generating Info + // objects that use the FakeRESTClient as the UnstructuredClient. + applier.infoHelper = infoHelper + + return applier +} + +func newTestDestroyer( + t *testing.T, + invInfo inventoryInfo, + clusterObjs object.UnstructuredSet, + statusPoller poller.Poller, +) *Destroyer { + tf := newTestFactory(t, invInfo, object.UnstructuredSet{}, clusterObjs) + defer tf.Cleanup() + + invClient := newTestInventory(t, tf) + + destroyer, err := NewDestroyer(tf, invClient) + require.NoError(t, err) + destroyer.StatusPoller = statusPoller + + return destroyer +} + +func newTestInventory( + t *testing.T, + tf *cmdtesting.TestFactory, +) inventory.Client { + // Use an Client with a fakeInfoHelper to allow generating Info + // objects that use the FakeRESTClient as the UnstructuredClient. + invClient, err := inventory.ClusterClientFactory{}.NewClient(tf) + require.NoError(t, err) + return invClient +} + +func newTestFactory( + t *testing.T, + invInfo inventoryInfo, + resourceSet object.UnstructuredSet, + clusterObjs object.UnstructuredSet, +) *cmdtesting.TestFactory { + tf := cmdtesting.NewTestFactory().WithNamespace(invInfo.namespace) + + mapper, err := tf.ToRESTMapper() + require.NoError(t, err) + + objMap := make(map[object.ObjMetadata]resourceInfo) + for _, r := range resourceSet { + objMeta := object.UnstructuredToObjMetadata(r) + objMap[objMeta] = resourceInfo{ + resource: r, + exists: false, + } + } + for _, r := range clusterObjs { + objMeta := object.UnstructuredToObjMetadata(r) + objMap[objMeta] = resourceInfo{ + resource: r, + exists: true, + } + } + var objs []resourceInfo + for _, obj := range objMap { + objs = append(objs, obj) + } + + handlers := []handler{ + &nsHandler{}, + &genericHandler{ + resources: objs, + mapper: mapper, + }, + } + + tf.UnstructuredClient = newFakeRESTClient(t, handlers) + tf.FakeDynamicClient = fakeDynamicClient(t, mapper, invInfo, objs...) + + return tf +} + +type resourceInfo struct { + resource *unstructured.Unstructured + exists bool +} + +// newFakeRESTClient creates a new client that uses a set of handlers to +// determine how to handle requests. For every request it will iterate through +// the handlers until it can find one that knows how to handle the request. +// This is to keep the main structure of the fake client manageable while still +// allowing different behavior for different testcases. +func newFakeRESTClient(t *testing.T, handlers []handler) *fake.RESTClient { + return &fake.RESTClient{ + NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + klog.V(5).Infof("FakeRESTClient: handling %s request for %q", req.Method, req.URL) + for _, h := range handlers { + resp, handled, err := h.handle(t, req) + if err != nil { + t.Fatalf("unexpected error: %v", err) + return nil, nil + } + if handled { + return resp, nil + } + } + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + }), + } +} + +// The handler interface allows different testcases to provide +// special handling of requests. It also allows a single handler +// to keep state between a set of related requests instead of keeping +// a single large event handler. +type handler interface { + handle(t *testing.T, req *http.Request) (*http.Response, bool, error) +} + +// genericHandler provides a simple handler for resources that can +// be fetched and updated. It will simply return the given resource +// when asked for and accept patch requests. +type genericHandler struct { + resources []resourceInfo + mapper meta.RESTMapper +} + +func (g *genericHandler) handle(t *testing.T, req *http.Request) (*http.Response, bool, error) { + klog.V(5).Infof("genericHandler: handling %s request for %q", req.Method, req.URL) + for _, r := range g.resources { + gvk := r.resource.GroupVersionKind() + mapping, err := g.mapper.RESTMapping(gvk.GroupKind(), gvk.Version) + if err != nil { + return nil, false, err + } + var allPath string + if mapping.Scope == meta.RESTScopeNamespace { + allPath = fmt.Sprintf("/namespaces/%s/%s", r.resource.GetNamespace(), mapping.Resource.Resource) + } else { + allPath = fmt.Sprintf("/%s", mapping.Resource.Resource) + } + singlePath := allPath + "/" + r.resource.GetName() + + if req.URL.Path == singlePath && req.Method == http.MethodGet { + if r.exists { + bodyRC := ioutil.NopCloser(bytes.NewReader(toJSONBytes(t, r.resource))) + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil + } + return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.StringBody("")}, true, nil + } + + if req.URL.Path == singlePath && req.Method == http.MethodPatch { + bodyRC := ioutil.NopCloser(bytes.NewReader(toJSONBytes(t, r.resource))) + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil + } + + if req.URL.Path == singlePath && req.Method == http.MethodDelete { + if r.exists { + bodyRC := ioutil.NopCloser(bytes.NewReader(toJSONBytes(t, r.resource))) + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil + } + + // We're not testing DeletePropagationOrphan, so StatusOK should be + // safe. Otherwise, the status might be StatusAccepted. + // https://github.com/kubernetes/apiserver/blob/v0.22.2/pkg/endpoints/handlers/delete.go#L140 + status := http.StatusOK + + // Return Status object, if resource doesn't exist. + result := &metav1.Status{ + Status: metav1.StatusSuccess, + Code: int32(status), + Details: &metav1.StatusDetails{ + Name: r.resource.GetName(), + Kind: r.resource.GetKind(), + }, + } + bodyRC := ioutil.NopCloser(bytes.NewReader(toJSONBytes(t, result))) + return &http.Response{StatusCode: status, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil + } + + if req.URL.Path == allPath && req.Method == http.MethodPost { + bodyRC := ioutil.NopCloser(bytes.NewReader(toJSONBytes(t, r.resource))) + return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil + } + } + return nil, false, nil +} + +func newInventoryReactor(invInfo inventoryInfo) *inventoryReactor { + return &inventoryReactor{ + inventoryObj: invInfo.toUnstructured(), + } +} + +type inventoryReactor struct { + inventoryObj *unstructured.Unstructured +} + +func (ir *inventoryReactor) updateFakeDynamicClient(fdc *dynamicfake.FakeDynamicClient) { + fdc.PrependReactor("create", "configmaps", func(action clienttesting.Action) (bool, runtime.Object, error) { + obj := *action.(clienttesting.CreateAction).GetObject().(*unstructured.Unstructured) + ir.inventoryObj = &obj + return true, ir.inventoryObj.DeepCopy(), nil + }) + fdc.PrependReactor("list", "configmaps", func(action clienttesting.Action) (bool, runtime.Object, error) { + uList := &unstructured.UnstructuredList{ + Items: []unstructured.Unstructured{}, + } + if ir.inventoryObj != nil { + uList.Items = append(uList.Items, *ir.inventoryObj.DeepCopy()) + } + return true, uList, nil + }) + fdc.PrependReactor("get", "configmaps", func(action clienttesting.Action) (bool, runtime.Object, error) { + return true, ir.inventoryObj.DeepCopy(), nil + }) + fdc.PrependReactor("update", "configmaps", func(action clienttesting.Action) (bool, runtime.Object, error) { + obj := *action.(clienttesting.UpdateAction).GetObject().(*unstructured.Unstructured) + ir.inventoryObj = &obj + return true, ir.inventoryObj.DeepCopy(), nil + }) +} + +// nsHandler can handle requests for a namespace. It will behave as if +// every requested namespace exists. It simply fetches the name of the requested +// namespace from the url and creates a new namespace type with the provided +// name for the response. +type nsHandler struct{} + +var ( + nsPathRegex = regexp.MustCompile(`/api/v1/namespaces/([^/]+)`) +) + +func (n *nsHandler) handle(t *testing.T, req *http.Request) (*http.Response, bool, error) { + match := nsPathRegex.FindStringSubmatch(req.URL.Path) + if req.Method == http.MethodGet && match != nil { + nsName := match[1] + ns := v1.Namespace{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Namespace", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: nsName, + }, + } + bodyRC := ioutil.NopCloser(bytes.NewReader(toJSONBytes(t, &ns))) + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil + } + return nil, false, nil +} + +type fakePoller struct { + start chan struct{} + events []pollevent.Event +} + +func newFakePoller(statusEvents []pollevent.Event) *fakePoller { + return &fakePoller{ + events: statusEvents, + start: make(chan struct{}), + } +} + +// Start events being sent on the status channel +func (f *fakePoller) Start() { + close(f.start) +} + +func (f *fakePoller) Poll(ctx context.Context, _ object.ObjMetadataSet, _ polling.PollOptions) <-chan pollevent.Event { + eventChannel := make(chan pollevent.Event) + go func() { + defer close(eventChannel) + // wait until started to send the events + <-f.start + for _, f := range f.events { + eventChannel <- f + } + // wait until cancelled to close the event channel and exit + <-ctx.Done() + }() + return eventChannel +} + +type fakeInfoHelper struct { + factory *cmdtesting.TestFactory +} + +// TODO(mortent): This has too much code in common with the +// infoHelper implementation. We need to find a better way to structure +// this. +func (f *fakeInfoHelper) UpdateInfo(info *resource.Info) error { + mapper, err := f.factory.ToRESTMapper() + if err != nil { + return err + } + gvk := info.Object.GetObjectKind().GroupVersionKind() + mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) + if err != nil { + return err + } + info.Mapping = mapping + + c, err := f.getClient(gvk.GroupVersion()) + if err != nil { + return err + } + info.Client = c + return nil +} + +func (f *fakeInfoHelper) BuildInfo(obj *unstructured.Unstructured) (*resource.Info, error) { + info := &resource.Info{ + Name: obj.GetName(), + Namespace: obj.GetNamespace(), + Source: "unstructured", + Object: obj, + } + err := f.UpdateInfo(info) + return info, err +} + +func (f *fakeInfoHelper) getClient(gv schema.GroupVersion) (resource.RESTClient, error) { + if f.factory.UnstructuredClientForMappingFunc != nil { + return f.factory.UnstructuredClientForMappingFunc(gv) + } + if f.factory.UnstructuredClient != nil { + return f.factory.UnstructuredClient, nil + } + return f.factory.Client, nil +} + +// fakeDynamicClient returns a fake dynamic client. +func fakeDynamicClient(t *testing.T, mapper meta.RESTMapper, invInfo inventoryInfo, objs ...resourceInfo) *dynamicfake.FakeDynamicClient { + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + + invReactor := newInventoryReactor(invInfo) + invReactor.updateFakeDynamicClient(fakeClient) + + for i := range objs { + obj := objs[i] + gvk := obj.resource.GroupVersionKind() + mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) + if !assert.NoError(t, err) { + t.FailNow() + } + r := mapping.Resource.Resource + fakeClient.PrependReactor("get", r, func(clienttesting.Action) (bool, runtime.Object, error) { + if obj.exists { + return true, obj.resource, nil + } + return false, nil, nil + }) + fakeClient.PrependReactor("delete", r, func(clienttesting.Action) (bool, runtime.Object, error) { + return true, nil, nil + }) + } + + return fakeClient +} + +func toJSONBytes(t *testing.T, obj runtime.Object) []byte { + objBytes, err := runtime.Encode(unstructured.NewJSONFallbackEncoder(codec), obj) + if !assert.NoError(t, err) { + t.Fatal(err) + } + return objBytes +} + +type JSONPathSetter struct { + Path string + Value interface{} +} + +func (jps JSONPathSetter) Mutate(u *unstructured.Unstructured) { + _, err := jsonpath.Set(u.Object, jps.Path, jps.Value) + if err != nil { + panic(fmt.Sprintf("failed to mutate unstructured object: %v", err)) + } +} diff --git a/thirdparty/cli-utils/apply/destroyer.go b/thirdparty/cli-utils/apply/destroyer.go new file mode 100644 index 0000000000..75fd77a8b1 --- /dev/null +++ b/thirdparty/cli-utils/apply/destroyer.go @@ -0,0 +1,228 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package apply + +import ( + "context" + "fmt" + "time" + + "github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/apply/solver" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/klog/v2" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "sigs.k8s.io/cli-utils/pkg/apis/actuation" + "sigs.k8s.io/cli-utils/pkg/apply/cache" + "sigs.k8s.io/cli-utils/pkg/apply/event" + "sigs.k8s.io/cli-utils/pkg/apply/filter" + "sigs.k8s.io/cli-utils/pkg/apply/info" + "sigs.k8s.io/cli-utils/pkg/apply/poller" + "sigs.k8s.io/cli-utils/pkg/apply/prune" + "sigs.k8s.io/cli-utils/pkg/apply/taskrunner" + "sigs.k8s.io/cli-utils/pkg/common" + "sigs.k8s.io/cli-utils/pkg/inventory" + "sigs.k8s.io/cli-utils/pkg/kstatus/polling" + "sigs.k8s.io/cli-utils/pkg/object" + "sigs.k8s.io/cli-utils/pkg/object/validation" +) + +// NewDestroyer returns a new destroyer. It will set up the ApplyOptions and +// PruneOptions 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 NewDestroyer(factory cmdutil.Factory, invClient inventory.Client) (*Destroyer, error) { + pruner, err := prune.NewPruner(factory, invClient) + if err != nil { + return nil, fmt.Errorf("error setting up PruneOptions: %w", err) + } + statusPoller, err := polling.NewStatusPollerFromFactory(factory, polling.Options{}) + if err != nil { + return nil, err + } + return &Destroyer{ + pruner: pruner, + StatusPoller: statusPoller, + factory: factory, + invClient: invClient, + }, nil +} + +// Destroyer performs the step of grabbing all the previous inventory objects and +// prune them. This also deletes all the previous inventory objects +type Destroyer struct { + pruner *prune.Pruner + StatusPoller poller.Poller + factory cmdutil.Factory + invClient inventory.Client +} + +type DestroyerOptions struct { + // InventoryPolicy defines the inventory policy of apply. + InventoryPolicy inventory.Policy + + // DryRunStrategy defines whether changes should actually be performed, + // or if it is just talk and no action. + DryRunStrategy common.DryRunStrategy + + // DeleteTimeout defines how long we should wait for resources + // to be fully deleted. + DeleteTimeout time.Duration + + // DeletePropagationPolicy defines the deletion propagation policy + // that should be used. If this is not provided, the default is to + // use the Background policy. + DeletePropagationPolicy metav1.DeletionPropagation + + // EmitStatusEvents defines whether status events should be + // emitted on the eventChannel to the caller. + EmitStatusEvents bool + + // PollInterval defines how often we should poll for the status + // of resources. + PollInterval time.Duration + + // ValidationPolicy defines how to handle invalid objects. + ValidationPolicy validation.Policy +} + +func setDestroyerDefaults(o *DestroyerOptions) { + if o.PollInterval == time.Duration(0) { + o.PollInterval = defaultPollInterval + } + if o.DeletePropagationPolicy == "" { + o.DeletePropagationPolicy = metav1.DeletePropagationBackground + } +} + +// Run performs the destroy step. Passes the inventory object. This +// happens asynchronously on progress and any errors are reported +// back on the event channel. +func (d *Destroyer) Run(ctx context.Context, invInfo inventory.Info, options DestroyerOptions) <-chan event.Event { + eventChannel := make(chan event.Event) + setDestroyerDefaults(&options) + go func() { + defer close(eventChannel) + // Retrieve the objects to be deleted from the cluster. Second parameter is empty + // because no local objects returns all inventory objects for deletion. + emptyLocalObjs := object.UnstructuredSet{} + deleteObjs, err := d.pruner.GetPruneObjs(invInfo, emptyLocalObjs, prune.Options{ + DryRunStrategy: options.DryRunStrategy, + }) + if err != nil { + handleError(eventChannel, err) + return + } + mapper, err := d.factory.ToRESTMapper() + if err != nil { + handleError(eventChannel, err) + return + } + + // Validate the resources to make sure we catch those problems early + // before anything has been updated in the cluster. + vCollector := &validation.Collector{} + validator := &validation.Validator{ + Collector: vCollector, + Mapper: mapper, + } + validator.Validate(deleteObjs) + + // Build a TaskContext for passing info between tasks + resourceCache := cache.NewResourceCacheMap() + taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache) + + klog.V(4).Infoln("destroyer building task queue...") + dynamicClient, err := d.factory.DynamicClient() + if err != nil { + handleError(eventChannel, err) + return + } + deleteFilters := []filter.ValidationFilter{ + filter.PreventRemoveFilter{}, + filter.InventoryPolicyFilter{ + Inv: invInfo, + InvPolicy: options.InventoryPolicy, + }, + filter.DependencyFilter{ + TaskContext: taskContext, + ActuationStrategy: actuation.ActuationStrategyDelete, + DryRunStrategy: options.DryRunStrategy, + }, + } + taskBuilder := &solver.TaskQueueBuilder{ + Pruner: d.pruner, + DynamicClient: dynamicClient, + OpenAPIGetter: d.factory.OpenAPIGetter(), + InfoHelper: info.NewHelper(mapper, d.factory.UnstructuredClientForMapping), + Mapper: mapper, + InvClient: d.invClient, + Collector: vCollector, + PruneFilters: deleteFilters, + } + opts := solver.Options{ + Destroy: true, + Prune: true, + DryRunStrategy: options.DryRunStrategy, + PrunePropagationPolicy: options.DeletePropagationPolicy, + PruneTimeout: options.DeleteTimeout, + InventoryPolicy: options.InventoryPolicy, + } + + // Build the ordered set of tasks to execute. + taskQueue := taskBuilder. + WithPruneObjects(deleteObjs). + WithInventory(invInfo). + Build(taskContext, opts) + + klog.V(4).Infof("validation errors: %d", len(vCollector.Errors)) + klog.V(4).Infof("invalid objects: %d", len(vCollector.InvalidIds)) + + // Handle validation errors + switch options.ValidationPolicy { + case validation.ExitEarly: + err = vCollector.ToError() + if err != nil { + handleError(eventChannel, err) + return + } + case validation.SkipInvalid: + for _, err := range vCollector.Errors { + handleValidationError(eventChannel, err) + } + default: + handleError(eventChannel, fmt.Errorf("invalid ValidationPolicy: %q", options.ValidationPolicy)) + return + } + + // Register invalid objects to be retained in the inventory, if present. + for _, id := range vCollector.InvalidIds { + taskContext.AddInvalidObject(id) + } + + // Send event to inform the caller about the resources that + // will be pruned. + eventChannel <- event.Event{ + Type: event.InitType, + InitEvent: event.InitEvent{ + ActionGroups: taskQueue.ToActionGroups(), + }, + } + // Create a new TaskStatusRunner to execute the taskQueue. + klog.V(4).Infoln("destroyer building TaskStatusRunner...") + deleteIds := object.UnstructuredSetToObjMetadataSet(deleteObjs) + runner := taskrunner.NewTaskStatusRunner(deleteIds, d.StatusPoller) + klog.V(4).Infoln("destroyer running TaskStatusRunner...") + err = runner.Run(ctx, taskContext, taskQueue.ToChannel(), taskrunner.Options{ + PollInterval: options.PollInterval, + EmitStatusEvents: options.EmitStatusEvents, + }) + if err != nil { + handleError(eventChannel, err) + return + } + }() + return eventChannel +} diff --git a/thirdparty/cli-utils/apply/destroyer_test.go b/thirdparty/cli-utils/apply/destroyer_test.go new file mode 100644 index 0000000000..f294fce97b --- /dev/null +++ b/thirdparty/cli-utils/apply/destroyer_test.go @@ -0,0 +1,391 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package apply + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/cli-utils/pkg/apply/event" + "sigs.k8s.io/cli-utils/pkg/inventory" + pollevent "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event" + "sigs.k8s.io/cli-utils/pkg/kstatus/status" + "sigs.k8s.io/cli-utils/pkg/object" + "sigs.k8s.io/cli-utils/pkg/testutil" +) + +func TestDestroyerCancel(t *testing.T) { + testCases := map[string]struct { + // inventory input to destroyer + invInfo inventoryInfo + // objects in the cluster + clusterObjs object.UnstructuredSet + // options input to destroyer.Run + options DestroyerOptions + // timeout for destroyer.Run + runTimeout time.Duration + // timeout for the test + testTimeout time.Duration + // fake input events from the status poller + statusEvents []pollevent.Event + // expected output status events (async) + expectedStatusEvents []testutil.ExpEvent + // expected output events + expectedEvents []testutil.ExpEvent + // true if runTimeout is expected to have caused cancellation + expectRunTimeout bool + }{ + "cancelled by caller while waiting for deletion": { + expectRunTimeout: true, + runTimeout: 2 * time.Second, + testTimeout: 30 * time.Second, + invInfo: inventoryInfo{ + name: "abc-123", + namespace: "test", + id: "test", + set: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + clusterObjs: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")), + }, + options: DestroyerOptions{ + EmitStatusEvents: true, + // DeleteTimeout needs to block long enough to cancel the run, + // otherwise the WaitTask is skipped. + DeleteTimeout: 1 * time.Minute, + }, + statusEvents: []pollevent.Event{ + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.InProgressStatus, + Resource: testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")), + }, + }, + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.InProgressStatus, + Resource: testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")), + }, + }, + // Resource never becomes NotFound, blocking destroyer.Run from exiting + }, + expectedStatusEvents: []testutil.ExpEvent{ + { + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.InProgressStatus, + }, + }, + }, + expectedEvents: []testutil.ExpEvent{ + { + // InitTask + EventType: event.InitType, + InitEvent: &testutil.ExpInitEvent{}, + }, + { + // PruneTask start + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.DeleteAction, + GroupName: "prune-0", + Type: event.Started, + }, + }, + { + // Delete Deployment + EventType: event.DeleteType, + DeleteEvent: &testutil.ExpDeleteEvent{ + GroupName: "prune-0", + Operation: event.Deleted, + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Error: nil, + }, + }, + { + // PruneTask finished + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.DeleteAction, + GroupName: "prune-0", + Type: event.Finished, + }, + }, + { + // WaitTask start + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.WaitAction, + GroupName: "wait-0", + Type: event.Started, + }, + }, + { + // Deployment reconcile pending. + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.ReconcilePending, + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + // Deployment never becomes NotFound. + // WaitTask is expected to be cancelled before DeleteTimeout. + { + // WaitTask finished + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.WaitAction, + GroupName: "wait-0", + Type: event.Finished, // TODO: add Cancelled event type + }, + }, + // Inventory cannot be deleted, because the objects still exist, + // even tho they've been deleted (ex: blocked by finalizer). + { + // Error + EventType: event.ErrorType, + ErrorEvent: &testutil.ExpErrorEvent{ + Err: context.DeadlineExceeded, + }, + }, + }, + }, + "completed with timeout": { + expectRunTimeout: false, + runTimeout: 10 * time.Second, + testTimeout: 30 * time.Second, + invInfo: inventoryInfo{ + name: "abc-123", + namespace: "test", + id: "test", + set: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + clusterObjs: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")), + }, + options: DestroyerOptions{ + EmitStatusEvents: true, + // DeleteTimeout needs to block long enough for completion + DeleteTimeout: 1 * time.Minute, + }, + statusEvents: []pollevent.Event{ + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.InProgressStatus, + Resource: testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")), + }, + }, + { + Type: pollevent.ResourceUpdateEvent, + Resource: &pollevent.ResourceStatus{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.NotFoundStatus, + }, + }, + // Resource becoming NotFound should unblock destroyer.Run WaitTask + }, + expectedStatusEvents: []testutil.ExpEvent{ + { + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.InProgressStatus, + }, + }, + { + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Status: status.NotFoundStatus, + }, + }, + }, + expectedEvents: []testutil.ExpEvent{ + { + // InitTask + EventType: event.InitType, + InitEvent: &testutil.ExpInitEvent{}, + }, + { + // PruneTask start + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.DeleteAction, + GroupName: "prune-0", + Type: event.Started, + }, + }, + { + // Delete Deployment + EventType: event.DeleteType, + DeleteEvent: &testutil.ExpDeleteEvent{ + GroupName: "prune-0", + Operation: event.Deleted, + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + Error: nil, + }, + }, + { + // PruneTask finished + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.DeleteAction, + GroupName: "prune-0", + Type: event.Finished, + }, + }, + { + // WaitTask start + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.WaitAction, + GroupName: "wait-0", + Type: event.Started, + }, + }, + { + // Deployment reconcile pending. + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.ReconcilePending, + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + { + // Deployment confirmed NotFound. + EventType: event.WaitType, + WaitEvent: &testutil.ExpWaitEvent{ + GroupName: "wait-0", + Operation: event.Reconciled, + Identifier: testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + { + // WaitTask finished + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.WaitAction, + GroupName: "wait-0", + Type: event.Finished, + }, + }, + { + // DeleteInvTask start + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.InventoryAction, + GroupName: "delete-inventory-0", + Type: event.Started, + }, + }, + { + // DeleteInvTask finished + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.InventoryAction, + GroupName: "delete-inventory-0", + Type: event.Finished, + }, + }, + }, + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + poller := newFakePoller(tc.statusEvents) + + invInfo := tc.invInfo.toWrapped() + + destroyer := newTestDestroyer(t, + tc.invInfo, + // Add the inventory to the cluster (to allow deletion) + append(tc.clusterObjs, inventory.InvInfoToConfigMap(invInfo)), + poller, + ) + + // Context for Destroyer.Run + runCtx, runCancel := context.WithTimeout(context.Background(), tc.runTimeout) + defer runCancel() // cleanup + + // Context for this test (in case Destroyer.Run never closes the event channel) + testCtx, testCancel := context.WithTimeout(context.Background(), tc.testTimeout) + defer testCancel() // cleanup + + eventChannel := destroyer.Run(runCtx, invInfo, tc.options) + + // only start poller once per run + var once sync.Once + var events []event.Event + + loop: + for { + select { + case <-testCtx.Done(): + // Test timed out + runCancel() + t.Errorf("Destroyer.Run failed to respond to cancellation (expected: %s, timeout: %s)", tc.runTimeout, tc.testTimeout) + break loop + + case e, ok := <-eventChannel: + if !ok { + // Event channel closed + testCancel() + break loop + } + events = append(events, e) + + if e.Type == event.ActionGroupType && + e.ActionGroupEvent.Action == event.WaitAction { + once.Do(func() { + // Start sending status events after waiting starts + poller.Start() + }) + } + } + } + + // Convert events to test events for comparison + receivedEvents := testutil.EventsToExpEvents(events) + + // Validate & remove expected status events + for _, e := range tc.expectedStatusEvents { + var removed int + receivedEvents, removed = testutil.RemoveEqualEvents(receivedEvents, e) + if removed < 1 { + t.Errorf("Expected status event not received: %#v", e) + } + } + + // Validate the rest of the events + testutil.AssertEqual(t, tc.expectedEvents, receivedEvents, + "Actual events (%d) do not match expected events (%d)", + len(receivedEvents), len(tc.expectedEvents)) + + // Validate that the expected timeout was the cause of the run completion. + // just in case something else cancelled the run + if tc.expectRunTimeout { + assert.Equal(t, context.DeadlineExceeded, runCtx.Err(), "Destroyer.Run exited, but not by expected timeout") + } else { + assert.Nil(t, runCtx.Err(), "Destroyer.Run exited, but not by expected timeout") + } + }) + } +} diff --git a/thirdparty/cli-utils/apply/main_test.go b/thirdparty/cli-utils/apply/main_test.go new file mode 100644 index 0000000000..9cebb4eec3 --- /dev/null +++ b/thirdparty/cli-utils/apply/main_test.go @@ -0,0 +1,19 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package apply + +import ( + "os" + "testing" + + "k8s.io/klog/v2" +) + +// TestMain executes the tests for this package, with optional logging. +// To see all logs, use: +// go test sigs.k8s.io/cli-utils/pkg/apply -v -args -v=5 +func TestMain(m *testing.M) { + klog.InitFlags(nil) + os.Exit(m.Run()) +} diff --git a/thirdparty/cli-utils/apply/solver/main_test.go b/thirdparty/cli-utils/apply/solver/main_test.go new file mode 100644 index 0000000000..3e9b2152bd --- /dev/null +++ b/thirdparty/cli-utils/apply/solver/main_test.go @@ -0,0 +1,19 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package solver + +import ( + "os" + "testing" + + "k8s.io/klog/v2" +) + +// TestMain executes the tests for this package, with optional logging. +// To see all logs, use: +// go test sigs.k8s.io/cli-utils/pkg/apply/solver -v -args -v=5 +func TestMain(m *testing.M) { + klog.InitFlags(nil) + os.Exit(m.Run()) +} diff --git a/thirdparty/cli-utils/apply/solver/solver.go b/thirdparty/cli-utils/apply/solver/solver.go new file mode 100644 index 0000000000..1ed2e7ccb7 --- /dev/null +++ b/thirdparty/cli-utils/apply/solver/solver.go @@ -0,0 +1,294 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +// The solver package is responsible for constructing a +// taskqueue based on the set of resources that should be +// applied. +// This involves setting up the appropriate sequence of +// apply, wait and prune tasks so any dependencies between +// resources doesn't cause a later apply operation to +// fail. +// Currently this package assumes that the resources have +// already been sorted in the appropriate order. We might +// want to consider moving the sorting functionality into +// this package. +package solver + +import ( + "fmt" + "time" + + "github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/apply/task" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + "k8s.io/klog/v2" + "sigs.k8s.io/cli-utils/pkg/apply/event" + "sigs.k8s.io/cli-utils/pkg/apply/filter" + "sigs.k8s.io/cli-utils/pkg/apply/info" + "sigs.k8s.io/cli-utils/pkg/apply/mutator" + "sigs.k8s.io/cli-utils/pkg/apply/prune" + "sigs.k8s.io/cli-utils/pkg/apply/taskrunner" + "sigs.k8s.io/cli-utils/pkg/common" + "sigs.k8s.io/cli-utils/pkg/inventory" + "sigs.k8s.io/cli-utils/pkg/object" + "sigs.k8s.io/cli-utils/pkg/object/graph" + "sigs.k8s.io/cli-utils/pkg/object/validation" +) + +type TaskQueueBuilder struct { + Pruner *prune.Pruner + DynamicClient dynamic.Interface + OpenAPIGetter discovery.OpenAPISchemaInterface + InfoHelper info.Helper + Mapper meta.RESTMapper + InvClient inventory.Client + // Collector is used to collect validation errors and invalid objects. + // Invalid objects will be filtered and not be injected into tasks. + Collector *validation.Collector + ApplyFilters []filter.ValidationFilter + ApplyMutators []mutator.Interface + PruneFilters []filter.ValidationFilter + + // The accumulated tasks and counter variables to name tasks. + applyCounter int + pruneCounter int + waitCounter int + + invInfo inventory.Info + applyObjs object.UnstructuredSet + pruneObjs object.UnstructuredSet +} + +type TaskQueue struct { + tasks []taskrunner.Task +} + +func (tq *TaskQueue) ToChannel() chan taskrunner.Task { + taskQueue := make(chan taskrunner.Task, len(tq.tasks)) + for _, t := range tq.tasks { + taskQueue <- t + } + return taskQueue +} + +func (tq *TaskQueue) ToActionGroups() []event.ActionGroup { + var ags []event.ActionGroup + + for _, t := range tq.tasks { + ags = append(ags, event.ActionGroup{ + Name: t.Name(), + Action: t.Action(), + Identifiers: t.Identifiers(), + }) + } + return ags +} + +type Options struct { + ServerSideOptions common.ServerSideOptions + ReconcileTimeout time.Duration + // True if we are destroying, which deletes the inventory object + // as well (possibly) the inventory namespace. + Destroy bool + // True if we're deleting prune objects + Prune bool + DryRunStrategy common.DryRunStrategy + PrunePropagationPolicy metav1.DeletionPropagation + PruneTimeout time.Duration + InventoryPolicy inventory.Policy +} + +// WithInventory sets the inventory info and returns the builder for chaining. +func (t *TaskQueueBuilder) WithInventory(inv inventory.Info) *TaskQueueBuilder { + t.invInfo = inv + return t +} + +// WithApplyObjects sets the apply objects and returns the builder for chaining. +func (t *TaskQueueBuilder) WithApplyObjects(applyObjs object.UnstructuredSet) *TaskQueueBuilder { + t.applyObjs = applyObjs + return t +} + +// WithPruneObjects sets the prune objects and returns the builder for chaining. +func (t *TaskQueueBuilder) WithPruneObjects(pruneObjs object.UnstructuredSet) *TaskQueueBuilder { + t.pruneObjs = pruneObjs + return t +} + +// Build returns the queue of tasks that have been created +func (t *TaskQueueBuilder) Build(taskContext *taskrunner.TaskContext, o Options) *TaskQueue { + var tasks []taskrunner.Task + + // reset counters + t.applyCounter = 0 + t.pruneCounter = 0 + t.waitCounter = 0 + + // Filter objects that failed earlier validation + applyObjs := t.Collector.FilterInvalidObjects(t.applyObjs) + pruneObjs := t.Collector.FilterInvalidObjects(t.pruneObjs) + + // Merge applyObjs & pruneObjs and graph them together. + // This detects implicit and explicit dependencies. + // Invalid dependency annotations will be treated as validation errors. + allObjs := make(object.UnstructuredSet, 0, len(applyObjs)+len(pruneObjs)) + allObjs = append(allObjs, applyObjs...) + allObjs = append(allObjs, pruneObjs...) + g, err := graph.DependencyGraph(allObjs) + if err != nil { + t.Collector.Collect(err) + } + // Store graph for use by DependencyFilter + taskContext.SetGraph(g) + // Sort objects into phases (apply order). + // Cycles will be treated as validation errors. + idSetList, err := g.Sort() + if err != nil { + t.Collector.Collect(err) + } + + // Filter objects with cycles or invalid dependency annotations + applyObjs = t.Collector.FilterInvalidObjects(applyObjs) + pruneObjs = t.Collector.FilterInvalidObjects(pruneObjs) + + if !o.Destroy { + // InvAddTask creates the inventory and adds any objects being applied + klog.V(2).Infof("adding inventory add task (%d objects)", len(applyObjs)) + tasks = append(tasks, &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: t.InvClient, + InvInfo: t.invInfo, + Objects: applyObjs, + DryRun: o.DryRunStrategy, + }) + } + + if len(applyObjs) > 0 { + // Register actuation plan in the inventory + for _, id := range object.UnstructuredSetToObjMetadataSet(applyObjs) { + taskContext.InventoryManager().AddPendingApply(id) + } + + // Filter idSetList down to just apply objects + applySets := graph.HydrateSetList(idSetList, applyObjs) + + for _, applySet := range applySets { + tasks = append(tasks, + t.newApplyTask(applySet, t.ApplyFilters, t.ApplyMutators, o)) + // dry-run skips wait tasks + if !o.DryRunStrategy.ClientOrServerDryRun() { + applyIds := object.UnstructuredSetToObjMetadataSet(applySet) + tasks = append(tasks, + t.newWaitTask(applyIds, taskrunner.AllCurrent, o.ReconcileTimeout)) + } + } + } + + if o.Prune && len(pruneObjs) > 0 { + // Register actuation plan in the inventory + for _, id := range object.UnstructuredSetToObjMetadataSet(pruneObjs) { + taskContext.InventoryManager().AddPendingDelete(id) + } + + // Filter idSetList down to just prune objects + pruneSets := graph.HydrateSetList(idSetList, pruneObjs) + + // Reverse apply order to get prune order + graph.ReverseSetList(pruneSets) + + for _, pruneSet := range pruneSets { + tasks = append(tasks, + t.newPruneTask(pruneSet, t.PruneFilters, o)) + // dry-run skips wait tasks + if !o.DryRunStrategy.ClientOrServerDryRun() { + pruneIds := object.UnstructuredSetToObjMetadataSet(pruneSet) + tasks = append(tasks, + t.newWaitTask(pruneIds, taskrunner.AllNotFound, o.PruneTimeout)) + } + } + } + + // TODO: add InvSetTask when Destroy=true to retain undeleted objects + if !o.Destroy { + klog.V(2).Infoln("adding inventory set task") + prevInvIds, _ := t.InvClient.GetClusterObjs(t.invInfo) + tasks = append(tasks, &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: t.InvClient, + InvInfo: t.invInfo, + PrevInventory: prevInvIds, + DryRun: o.DryRunStrategy, + }) + } else { + klog.V(2).Infoln("adding delete inventory task") + tasks = append(tasks, &task.DeleteInvTask{ + TaskName: "delete-inventory-0", + InvClient: t.InvClient, + InvInfo: t.invInfo, + DryRun: o.DryRunStrategy, + }) + } + + return &TaskQueue{tasks: tasks} +} + +// AppendApplyTask appends a task to the task queue to apply the passed objects +// to the cluster. Returns a pointer to the Builder to chain function calls. +func (t *TaskQueueBuilder) newApplyTask(applyObjs object.UnstructuredSet, + applyFilters []filter.ValidationFilter, applyMutators []mutator.Interface, o Options) taskrunner.Task { + applyObjs = t.Collector.FilterInvalidObjects(applyObjs) + klog.V(2).Infof("adding apply task (%d objects)", len(applyObjs)) + task := &task.ApplyTask{ + TaskName: fmt.Sprintf("apply-%d", t.applyCounter), + Objects: applyObjs, + Filters: applyFilters, + Mutators: applyMutators, + ServerSideOptions: o.ServerSideOptions, + DryRunStrategy: o.DryRunStrategy, + DynamicClient: t.DynamicClient, + OpenAPIGetter: t.OpenAPIGetter, + InfoHelper: t.InfoHelper, + Mapper: t.Mapper, + } + t.applyCounter++ + return task +} + +// AppendWaitTask appends a task to wait on the passed objects to the task queue. +// Returns a pointer to the Builder to chain function calls. +func (t *TaskQueueBuilder) newWaitTask(waitIds object.ObjMetadataSet, condition taskrunner.Condition, + waitTimeout time.Duration) taskrunner.Task { + waitIds = t.Collector.FilterInvalidIds(waitIds) + klog.V(2).Infoln("adding wait task") + task := taskrunner.NewWaitTask( + fmt.Sprintf("wait-%d", t.waitCounter), + waitIds, + condition, + waitTimeout, + t.Mapper, + ) + t.waitCounter++ + return task +} + +// AppendPruneTask appends a task to delete objects from the cluster to the task queue. +// Returns a pointer to the Builder to chain function calls. +func (t *TaskQueueBuilder) newPruneTask(pruneObjs object.UnstructuredSet, + pruneFilters []filter.ValidationFilter, o Options) taskrunner.Task { + pruneObjs = t.Collector.FilterInvalidObjects(pruneObjs) + klog.V(2).Infof("adding prune task (%d objects)", len(pruneObjs)) + task := &task.PruneTask{ + TaskName: fmt.Sprintf("prune-%d", t.pruneCounter), + Objects: pruneObjs, + Filters: pruneFilters, + Pruner: t.Pruner, + PropagationPolicy: o.PrunePropagationPolicy, + DryRunStrategy: o.DryRunStrategy, + Destroy: o.Destroy, + } + t.pruneCounter++ + return task +} diff --git a/thirdparty/cli-utils/apply/solver/solver_test.go b/thirdparty/cli-utils/apply/solver/solver_test.go new file mode 100644 index 0000000000..4368f3e548 --- /dev/null +++ b/thirdparty/cli-utils/apply/solver/solver_test.go @@ -0,0 +1,1904 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package solver + +import ( + "testing" + "time" + + "github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/apply/task" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/cli-utils/pkg/apis/actuation" + "sigs.k8s.io/cli-utils/pkg/apply/prune" + "sigs.k8s.io/cli-utils/pkg/apply/taskrunner" + "sigs.k8s.io/cli-utils/pkg/common" + "sigs.k8s.io/cli-utils/pkg/inventory" + "sigs.k8s.io/cli-utils/pkg/object" + "sigs.k8s.io/cli-utils/pkg/object/graph" + "sigs.k8s.io/cli-utils/pkg/object/validation" + "sigs.k8s.io/cli-utils/pkg/testutil" +) + +var ( + pruner = &prune.Pruner{} + resources = map[string]string{ + "pod": ` +kind: Pod +apiVersion: v1 +metadata: + name: test-pod + namespace: test-namespace +`, + "default-pod": ` +kind: Pod +apiVersion: v1 +metadata: + name: pod-in-default-namespace + namespace: default +`, + "deployment": ` +kind: Deployment +apiVersion: apps/v1 +metadata: + name: foo + namespace: test-namespace + uid: dep-uid + generation: 1 +spec: + replicas: 1 +`, + "secret": ` +kind: Secret +apiVersion: v1 +metadata: + name: secret + namespace: test-namespace + uid: secret-uid + generation: 1 +type: Opaque +spec: + foo: bar +`, + "namespace": ` +kind: Namespace +apiVersion: v1 +metadata: + name: test-namespace +`, + + "crd": ` +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: crontabs.stable.example.com +spec: + group: stable.example.com + versions: + - name: v1 + served: true + storage: true + scope: Namespaced + names: + plural: crontabs + singular: crontab + kind: CronTab +`, + "crontab1": ` +apiVersion: "stable.example.com/v1" +kind: CronTab +metadata: + name: cron-tab-01 + namespace: test-namespace +`, + "crontab2": ` +apiVersion: "stable.example.com/v1" +kind: CronTab +metadata: + name: cron-tab-02 + namespace: test-namespace +`, + } +) + +func newInvObject(name, namespace, inventoryID string) *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": name, + "namespace": namespace, + "labels": map[string]interface{}{ + common.InventoryLabel: inventoryID, + }, + }, + "data": map[string]string{}, + }, + } +} + +func TestTaskQueueBuilder_ApplyBuild(t *testing.T) { + // Use a custom Asserter to customize the comparison options + asserter := testutil.NewAsserter( + cmpopts.EquateErrors(), + waitTaskComparer(), + fakeClientComparer(), + inventoryInfoComparer(), + ) + + invInfo := inventory.WrapInventoryInfoObj(newInvObject( + "abc-123", "default", "test")) + + testCases := map[string]struct { + applyObjs []*unstructured.Unstructured + options Options + expectedTasks []taskrunner.Task + expectedError error + expectedStatus []actuation.ObjectStatus + }{ + "no resources, no apply or wait tasks": { + applyObjs: []*unstructured.Unstructured{}, + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{}, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{}, + }, + }, + }, + "single resource, one apply task, one wait task": { + applyObjs: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["deployment"]), + }, + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"]), + }, + }, + &task.ApplyTask{ + TaskName: "apply-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["deployment"]), + }, + }, + &taskrunner.WaitTask{ + TaskName: "wait-0", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["deployment"]), + }, + Condition: taskrunner.AllCurrent, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + }, + expectedStatus: []actuation.ObjectStatus{ + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["deployment"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + }, + }, + "multiple resource with no timeout": { + applyObjs: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["deployment"]), + testutil.Unstructured(t, resources["secret"]), + }, + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"]), + testutil.Unstructured(t, resources["secret"]), + }, + }, + &task.ApplyTask{ + TaskName: "apply-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["deployment"]), + testutil.Unstructured(t, resources["secret"]), + }, + DryRunStrategy: common.DryRunNone, + }, + &taskrunner.WaitTask{ + TaskName: "wait-0", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["deployment"]), + testutil.ToIdentifier(t, resources["secret"]), + }, + Condition: taskrunner.AllCurrent, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["deployment"]), + testutil.ToIdentifier(t, resources["secret"]), + }, + }, + }, + expectedStatus: []actuation.ObjectStatus{ + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["deployment"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["secret"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + }, + }, + "multiple resources with reconcile timeout": { + applyObjs: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["deployment"]), + testutil.Unstructured(t, resources["secret"]), + }, + options: Options{ + ReconcileTimeout: 1 * time.Minute, + }, + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{ + testutil.Unstructured(t, resources["secret"]), + testutil.Unstructured(t, resources["deployment"]), + }, + }, + &task.ApplyTask{ + TaskName: "apply-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["secret"]), + testutil.Unstructured(t, resources["deployment"]), + }, + DryRunStrategy: common.DryRunNone, + }, + &taskrunner.WaitTask{ + TaskName: "wait-0", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["secret"]), + testutil.ToIdentifier(t, resources["deployment"]), + }, + Condition: taskrunner.AllCurrent, + Timeout: 1 * time.Minute, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["secret"]), + testutil.ToIdentifier(t, resources["deployment"]), + }, + }, + }, + expectedStatus: []actuation.ObjectStatus{ + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["deployment"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["secret"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + }, + }, + "multiple resources with reconcile timeout and dryrun": { + applyObjs: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["deployment"]), + testutil.Unstructured(t, resources["secret"]), + }, + options: Options{ + ReconcileTimeout: time.Minute, + DryRunStrategy: common.DryRunClient, + }, + // No wait task, since it is dry run + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"]), + testutil.Unstructured(t, resources["secret"]), + }, + DryRun: common.DryRunClient, + }, + &task.ApplyTask{ + TaskName: "apply-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["deployment"]), + testutil.Unstructured(t, resources["secret"]), + }, + DryRunStrategy: common.DryRunClient, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["deployment"]), + testutil.ToIdentifier(t, resources["secret"]), + }, + DryRun: common.DryRunClient, + }, + }, + expectedStatus: []actuation.ObjectStatus{ + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["deployment"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["secret"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + }, + }, + "multiple resources with reconcile timeout and server-dryrun": { + applyObjs: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["pod"]), + testutil.Unstructured(t, resources["default-pod"]), + }, + options: Options{ + ReconcileTimeout: time.Minute, + DryRunStrategy: common.DryRunServer, + }, + // No wait task, since it is dry run + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{ + testutil.Unstructured(t, resources["pod"]), + testutil.Unstructured(t, resources["default-pod"]), + }, + DryRun: common.DryRunServer, + }, + &task.ApplyTask{ + TaskName: "apply-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["pod"]), + testutil.Unstructured(t, resources["default-pod"]), + }, + DryRunStrategy: common.DryRunServer, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["pod"]), + testutil.ToIdentifier(t, resources["default-pod"]), + }, + DryRun: common.DryRunServer, + }, + }, + expectedStatus: []actuation.ObjectStatus{ + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["pod"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["default-pod"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + }, + }, + "multiple resources including CRD": { + applyObjs: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["crontab1"]), + testutil.Unstructured(t, resources["crd"]), + testutil.Unstructured(t, resources["crontab2"]), + }, + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{ + testutil.Unstructured(t, resources["crontab1"]), + testutil.Unstructured(t, resources["crd"]), + testutil.Unstructured(t, resources["crontab2"]), + }, + }, + &task.ApplyTask{ + TaskName: "apply-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["crd"]), + }, + DryRunStrategy: common.DryRunNone, + }, + &taskrunner.WaitTask{ + TaskName: "wait-0", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["crd"]), + }, + Condition: taskrunner.AllCurrent, + }, + &task.ApplyTask{ + TaskName: "apply-1", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["crontab1"]), + testutil.Unstructured(t, resources["crontab2"]), + }, + DryRunStrategy: common.DryRunNone, + }, + &taskrunner.WaitTask{ + TaskName: "wait-1", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["crontab1"]), + testutil.ToIdentifier(t, resources["crontab2"]), + }, + Condition: taskrunner.AllCurrent, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["crontab1"]), + testutil.ToIdentifier(t, resources["crd"]), + testutil.ToIdentifier(t, resources["crontab2"]), + }, + }, + }, + expectedStatus: []actuation.ObjectStatus{ + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["crontab1"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["crd"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["crontab2"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + }, + }, + "no wait with CRDs if it is a dryrun": { + applyObjs: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["crontab1"]), + testutil.Unstructured(t, resources["crd"]), + testutil.Unstructured(t, resources["crontab2"]), + }, + options: Options{ + ReconcileTimeout: time.Minute, + DryRunStrategy: common.DryRunClient, + }, + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{ + testutil.Unstructured(t, resources["crontab1"]), + testutil.Unstructured(t, resources["crd"]), + testutil.Unstructured(t, resources["crontab2"]), + }, + DryRun: common.DryRunClient, + }, + &task.ApplyTask{ + TaskName: "apply-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["crd"]), + }, + DryRunStrategy: common.DryRunClient, + }, + &task.ApplyTask{ + TaskName: "apply-1", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["crontab1"]), + testutil.Unstructured(t, resources["crontab2"]), + }, + DryRunStrategy: common.DryRunClient, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["crontab1"]), + testutil.ToIdentifier(t, resources["crd"]), + testutil.ToIdentifier(t, resources["crontab2"]), + }, + DryRun: common.DryRunClient, + }, + }, + expectedStatus: []actuation.ObjectStatus{ + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["crontab1"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["crd"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["crontab2"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + }, + }, + "resources in namespace creates multiple apply tasks": { + applyObjs: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["namespace"]), + testutil.Unstructured(t, resources["pod"]), + testutil.Unstructured(t, resources["secret"]), + }, + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{ + testutil.Unstructured(t, resources["namespace"]), + testutil.Unstructured(t, resources["pod"]), + testutil.Unstructured(t, resources["secret"]), + }, + }, + &task.ApplyTask{ + TaskName: "apply-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["namespace"]), + }, + DryRunStrategy: common.DryRunNone, + }, + &taskrunner.WaitTask{ + TaskName: "wait-0", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["namespace"]), + }, + Condition: taskrunner.AllCurrent, + }, + &task.ApplyTask{ + TaskName: "apply-1", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["secret"]), + testutil.Unstructured(t, resources["pod"]), + }, + DryRunStrategy: common.DryRunNone, + }, + &taskrunner.WaitTask{ + TaskName: "wait-1", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["secret"]), + testutil.ToIdentifier(t, resources["pod"]), + }, + Condition: taskrunner.AllCurrent, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["namespace"]), + testutil.ToIdentifier(t, resources["pod"]), + testutil.ToIdentifier(t, resources["secret"]), + }, + }, + }, + expectedStatus: []actuation.ObjectStatus{ + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["namespace"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["pod"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["secret"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + }, + }, + "deployment depends on secret creates multiple tasks": { + applyObjs: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["deployment"], + testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), + testutil.Unstructured(t, resources["secret"]), + }, + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"], + testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), + testutil.Unstructured(t, resources["secret"]), + }, + }, + &task.ApplyTask{ + TaskName: "apply-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["secret"]), + }, + DryRunStrategy: common.DryRunNone, + }, + &taskrunner.WaitTask{ + TaskName: "wait-0", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["secret"]), + }, + Condition: taskrunner.AllCurrent, + }, + &task.ApplyTask{ + TaskName: "apply-1", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["deployment"], + testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), + }, + DryRunStrategy: common.DryRunNone, + }, + &taskrunner.WaitTask{ + TaskName: "wait-1", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["deployment"]), + }, + Condition: taskrunner.AllCurrent, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["deployment"]), + testutil.ToIdentifier(t, resources["secret"]), + }, + }, + }, + expectedStatus: []actuation.ObjectStatus{ + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["deployment"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["secret"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + }, + }, + "cyclic dependency returns error": { + applyObjs: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["deployment"], + testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), + testutil.Unstructured(t, resources["secret"], + testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))), + }, + expectedTasks: []taskrunner.Task{}, + expectedError: validation.NewError( + graph.CyclicDependencyError{ + Edges: []graph.Edge{ + { + From: testutil.ToIdentifier(t, resources["secret"]), + To: testutil.ToIdentifier(t, resources["deployment"]), + }, + { + From: testutil.ToIdentifier(t, resources["deployment"]), + To: testutil.ToIdentifier(t, resources["secret"]), + }, + }, + }, + testutil.ToIdentifier(t, resources["secret"]), + testutil.ToIdentifier(t, resources["deployment"]), + ), + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + mapper := testutil.NewFakeRESTMapper() + // inject mapper for equality comparison + for _, t := range tc.expectedTasks { + switch typedTask := t.(type) { + case *task.ApplyTask: + typedTask.Mapper = mapper + case *taskrunner.WaitTask: + typedTask.Mapper = mapper + } + } + + applyIds := object.UnstructuredSetToObjMetadataSet(tc.applyObjs) + fakeInvClient := inventory.NewFakeClient(applyIds) + vCollector := &validation.Collector{} + tqb := TaskQueueBuilder{ + Pruner: pruner, + Mapper: mapper, + InvClient: fakeInvClient, + Collector: vCollector, + } + taskContext := taskrunner.NewTaskContext(nil, nil) + tq := tqb.WithInventory(invInfo). + WithApplyObjects(tc.applyObjs). + Build(taskContext, tc.options) + err := vCollector.ToError() + if tc.expectedError != nil { + assert.EqualError(t, err, tc.expectedError.Error()) + return + } + assert.NoError(t, err) + asserter.Equal(t, tc.expectedTasks, tq.tasks) + + actualStatus := taskContext.InventoryManager().Inventory().Status.Objects + testutil.AssertEqual(t, tc.expectedStatus, actualStatus) + }) + } +} + +func TestTaskQueueBuilder_PruneBuild(t *testing.T) { + // Use a custom Asserter to customize the comparison options + asserter := testutil.NewAsserter( + cmpopts.EquateErrors(), + waitTaskComparer(), + fakeClientComparer(), + inventoryInfoComparer(), + ) + + invInfo := inventory.WrapInventoryInfoObj(newInvObject( + "abc-123", "default", "test")) + + testCases := map[string]struct { + pruneObjs []*unstructured.Unstructured + options Options + expectedTasks []taskrunner.Task + expectedError error + expectedStatus []actuation.ObjectStatus + }{ + "no resources, no apply or prune tasks": { + pruneObjs: []*unstructured.Unstructured{}, + options: Options{Prune: true}, + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{}, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{}, + }, + }, + }, + "single resource, one prune task, one wait task": { + pruneObjs: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["default-pod"]), + }, + options: Options{Prune: true}, + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{}, + }, + &task.PruneTask{ + TaskName: "prune-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["default-pod"]), + }, + }, + &taskrunner.WaitTask{ + TaskName: "wait-0", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["default-pod"]), + }, + Condition: taskrunner.AllNotFound, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["default-pod"]), + }, + }, + }, + expectedStatus: []actuation.ObjectStatus{ + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["default-pod"]), + ), + Strategy: actuation.ActuationStrategyDelete, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + }, + }, + "multiple resources, one prune task, one wait task": { + pruneObjs: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["default-pod"]), + testutil.Unstructured(t, resources["pod"]), + }, + options: Options{Prune: true}, + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{}, + }, + &task.PruneTask{ + TaskName: "prune-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["default-pod"]), + testutil.Unstructured(t, resources["pod"]), + }, + }, + &taskrunner.WaitTask{ + TaskName: "wait-0", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["default-pod"]), + testutil.ToIdentifier(t, resources["pod"]), + }, + Condition: taskrunner.AllNotFound, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["default-pod"]), + testutil.ToIdentifier(t, resources["pod"]), + }, + }, + }, + expectedStatus: []actuation.ObjectStatus{ + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["default-pod"]), + ), + Strategy: actuation.ActuationStrategyDelete, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["pod"]), + ), + Strategy: actuation.ActuationStrategyDelete, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + }, + }, + "dependent resources, two prune tasks, two wait tasks": { + pruneObjs: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["pod"], + testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), + testutil.Unstructured(t, resources["secret"]), + }, + options: Options{Prune: true}, + // Opposite ordering when pruning/deleting + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{}, + }, + &task.PruneTask{ + TaskName: "prune-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["pod"], + testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), + }, + }, + &taskrunner.WaitTask{ + TaskName: "wait-0", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["pod"]), + }, + Condition: taskrunner.AllNotFound, + }, + &task.PruneTask{ + TaskName: "prune-1", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["secret"]), + }, + }, + &taskrunner.WaitTask{ + TaskName: "wait-1", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["secret"]), + }, + Condition: taskrunner.AllNotFound, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["pod"]), + testutil.ToIdentifier(t, resources["secret"]), + }, + }, + }, + expectedStatus: []actuation.ObjectStatus{ + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["pod"]), + ), + Strategy: actuation.ActuationStrategyDelete, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["secret"]), + ), + Strategy: actuation.ActuationStrategyDelete, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + }, + }, + "single resource with prune timeout has wait task": { + pruneObjs: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["pod"]), + }, + options: Options{ + Prune: true, + PruneTimeout: 3 * time.Minute, + }, + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{}, + }, + &task.PruneTask{ + TaskName: "prune-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["pod"]), + }, + }, + &taskrunner.WaitTask{ + TaskName: "wait-0", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["pod"]), + }, + Condition: taskrunner.AllNotFound, + Timeout: 3 * time.Minute, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["pod"]), + }, + }, + }, + expectedStatus: []actuation.ObjectStatus{ + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["pod"]), + ), + Strategy: actuation.ActuationStrategyDelete, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + }, + }, + "multiple resources with prune timeout and server-dryrun": { + pruneObjs: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["pod"]), + testutil.Unstructured(t, resources["default-pod"]), + }, + options: Options{ + PruneTimeout: time.Minute, + DryRunStrategy: common.DryRunServer, + Prune: true, + }, + // No wait task, since it is dry run + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{}, + DryRun: common.DryRunServer, + }, + &task.PruneTask{ + TaskName: "prune-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["pod"]), + testutil.Unstructured(t, resources["default-pod"]), + }, + DryRunStrategy: common.DryRunServer, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["pod"]), + testutil.ToIdentifier(t, resources["default-pod"]), + }, + DryRun: common.DryRunServer, + }, + }, + expectedStatus: []actuation.ObjectStatus{ + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["pod"]), + ), + Strategy: actuation.ActuationStrategyDelete, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["default-pod"]), + ), + Strategy: actuation.ActuationStrategyDelete, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + }, + }, + "multiple resources including CRD": { + pruneObjs: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["crontab1"]), + testutil.Unstructured(t, resources["crd"]), + testutil.Unstructured(t, resources["crontab2"]), + }, + options: Options{Prune: true}, + // Opposite ordering when pruning/deleting. + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{}, + }, + &task.PruneTask{ + TaskName: "prune-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["crontab1"]), + testutil.Unstructured(t, resources["crontab2"]), + }, + }, + &taskrunner.WaitTask{ + TaskName: "wait-0", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["crontab1"]), + testutil.ToIdentifier(t, resources["crontab2"]), + }, + Condition: taskrunner.AllNotFound, + }, + &task.PruneTask{ + TaskName: "prune-1", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["crd"]), + }, + }, + &taskrunner.WaitTask{ + TaskName: "wait-1", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["crd"]), + }, + Condition: taskrunner.AllNotFound, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["crontab1"]), + testutil.ToIdentifier(t, resources["crd"]), + testutil.ToIdentifier(t, resources["crontab2"]), + }, + }, + }, + expectedStatus: []actuation.ObjectStatus{ + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["crontab1"]), + ), + Strategy: actuation.ActuationStrategyDelete, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["crd"]), + ), + Strategy: actuation.ActuationStrategyDelete, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["crontab2"]), + ), + Strategy: actuation.ActuationStrategyDelete, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + }, + }, + "no wait with CRDs if it is a dryrun": { + pruneObjs: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["crontab1"]), + testutil.Unstructured(t, resources["crd"]), + testutil.Unstructured(t, resources["crontab2"]), + }, + options: Options{ + ReconcileTimeout: time.Minute, + DryRunStrategy: common.DryRunClient, + Prune: true, + }, + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{}, + DryRun: common.DryRunClient, + }, + &task.PruneTask{ + TaskName: "prune-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["crontab1"]), + testutil.Unstructured(t, resources["crontab2"]), + }, + DryRunStrategy: common.DryRunClient, + }, + &task.PruneTask{ + TaskName: "prune-1", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["crd"]), + }, + DryRunStrategy: common.DryRunClient, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["crontab1"]), + testutil.ToIdentifier(t, resources["crd"]), + testutil.ToIdentifier(t, resources["crontab2"]), + }, + DryRun: common.DryRunClient, + }, + }, + expectedStatus: []actuation.ObjectStatus{ + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["crontab1"]), + ), + Strategy: actuation.ActuationStrategyDelete, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["crd"]), + ), + Strategy: actuation.ActuationStrategyDelete, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["crontab2"]), + ), + Strategy: actuation.ActuationStrategyDelete, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + }, + }, + "resources in namespace creates multiple apply tasks": { + pruneObjs: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["namespace"]), + testutil.Unstructured(t, resources["pod"]), + testutil.Unstructured(t, resources["secret"]), + }, + options: Options{Prune: true}, + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{}, + }, + &task.PruneTask{ + TaskName: "prune-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["pod"]), + testutil.Unstructured(t, resources["secret"]), + }, + }, + &taskrunner.WaitTask{ + TaskName: "wait-0", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["pod"]), + testutil.ToIdentifier(t, resources["secret"]), + }, + Condition: taskrunner.AllNotFound, + }, + &task.PruneTask{ + TaskName: "prune-1", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["namespace"]), + }, + }, + &taskrunner.WaitTask{ + TaskName: "wait-1", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["namespace"]), + }, + Condition: taskrunner.AllNotFound, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["namespace"]), + testutil.ToIdentifier(t, resources["pod"]), + testutil.ToIdentifier(t, resources["secret"]), + }, + }, + }, + expectedStatus: []actuation.ObjectStatus{ + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["namespace"]), + ), + Strategy: actuation.ActuationStrategyDelete, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["pod"]), + ), + Strategy: actuation.ActuationStrategyDelete, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["secret"]), + ), + Strategy: actuation.ActuationStrategyDelete, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + }, + }, + "cyclic dependency": { + pruneObjs: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["deployment"], + testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), + testutil.Unstructured(t, resources["secret"], + testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))), + }, + options: Options{Prune: true}, + expectedTasks: []taskrunner.Task{}, + expectedError: validation.NewError( + graph.CyclicDependencyError{ + Edges: []graph.Edge{ + { + From: testutil.ToIdentifier(t, resources["secret"]), + To: testutil.ToIdentifier(t, resources["deployment"]), + }, + { + From: testutil.ToIdentifier(t, resources["deployment"]), + To: testutil.ToIdentifier(t, resources["secret"]), + }, + }, + }, + testutil.ToIdentifier(t, resources["secret"]), + testutil.ToIdentifier(t, resources["deployment"]), + ), + }, + "cyclic dependency and valid": { + pruneObjs: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["deployment"], + testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), + testutil.Unstructured(t, resources["secret"], + testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))), + testutil.Unstructured(t, resources["pod"]), + }, + options: Options{Prune: true}, + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{}, + }, + &task.PruneTask{ + TaskName: "prune-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["pod"]), + }, + }, + taskrunner.NewWaitTask( + "wait-0", + object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["pod"]), + }, + taskrunner.AllCurrent, 1*time.Second, + testutil.NewFakeRESTMapper(), + ), + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["pod"]), + }, + }, + }, + expectedError: validation.NewError( + graph.CyclicDependencyError{ + Edges: []graph.Edge{ + { + From: testutil.ToIdentifier(t, resources["secret"]), + To: testutil.ToIdentifier(t, resources["deployment"]), + }, + { + From: testutil.ToIdentifier(t, resources["deployment"]), + To: testutil.ToIdentifier(t, resources["secret"]), + }, + }, + }, + testutil.ToIdentifier(t, resources["secret"]), + testutil.ToIdentifier(t, resources["deployment"]), + ), + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + mapper := testutil.NewFakeRESTMapper() + // inject mapper & pruner for equality comparison + for _, t := range tc.expectedTasks { + switch typedTask := t.(type) { + case *task.PruneTask: + typedTask.Pruner = &prune.Pruner{} + case *taskrunner.WaitTask: + typedTask.Mapper = mapper + } + } + + pruneIds := object.UnstructuredSetToObjMetadataSet(tc.pruneObjs) + fakeInvClient := inventory.NewFakeClient(pruneIds) + vCollector := &validation.Collector{} + tqb := TaskQueueBuilder{ + Pruner: pruner, + Mapper: mapper, + InvClient: fakeInvClient, + Collector: vCollector, + } + taskContext := taskrunner.NewTaskContext(nil, nil) + tq := tqb.WithInventory(invInfo). + WithPruneObjects(tc.pruneObjs). + Build(taskContext, tc.options) + err := vCollector.ToError() + if tc.expectedError != nil { + assert.EqualError(t, err, tc.expectedError.Error()) + return + } + assert.NoError(t, err) + asserter.Equal(t, tc.expectedTasks, tq.tasks) + + actualStatus := taskContext.InventoryManager().Inventory().Status.Objects + testutil.AssertEqual(t, tc.expectedStatus, actualStatus) + }) + } +} + +func TestTaskQueueBuilder_ApplyPruneBuild(t *testing.T) { + // Use a custom Asserter to customize the comparison options + asserter := testutil.NewAsserter( + cmpopts.EquateErrors(), + waitTaskComparer(), + fakeClientComparer(), + inventoryInfoComparer(), + ) + + invInfo := inventory.WrapInventoryInfoObj(newInvObject( + "abc-123", "default", "test")) + + testCases := map[string]struct { + inventoryIDs object.ObjMetadataSet + applyObjs object.UnstructuredSet + pruneObjs object.UnstructuredSet + options Options + expectedTasks []taskrunner.Task + expectedError error + expectedStatus []actuation.ObjectStatus + }{ + "two resources, one apply, one prune": { + inventoryIDs: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["secret"]), + }, + applyObjs: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"]), + }, + pruneObjs: object.UnstructuredSet{ + testutil.Unstructured(t, resources["secret"]), + }, + options: Options{Prune: true}, + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"]), + }, + }, + &task.ApplyTask{ + TaskName: "apply-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["deployment"]), + }, + }, + &taskrunner.WaitTask{ + TaskName: "wait-0", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["deployment"]), + }, + Condition: taskrunner.AllCurrent, + }, + &task.PruneTask{ + TaskName: "prune-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["secret"]), + }, + }, + &taskrunner.WaitTask{ + TaskName: "wait-1", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["secret"]), + }, + Condition: taskrunner.AllNotFound, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["secret"]), + }, + }, + }, + expectedStatus: []actuation.ObjectStatus{ + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["deployment"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["secret"]), + ), + Strategy: actuation.ActuationStrategyDelete, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + }, + }, + "prune disabled": { + inventoryIDs: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["secret"]), + }, + applyObjs: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"]), + }, + pruneObjs: object.UnstructuredSet{ + testutil.Unstructured(t, resources["secret"]), + }, + options: Options{Prune: false}, + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"]), + }, + }, + &task.ApplyTask{ + TaskName: "apply-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["deployment"]), + }, + }, + &taskrunner.WaitTask{ + TaskName: "wait-0", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["deployment"]), + }, + Condition: taskrunner.AllCurrent, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["secret"]), + }, + }, + }, + expectedStatus: []actuation.ObjectStatus{ + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["deployment"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + }, + }, + // This use case returns in a task plan that would cause a dependency + // to be deleted. This is remediated by the DependencyFilter at + // apply-time, by skipping both the apply and prune. + // This test does not verify the DependencyFilter tho, just that the + // dependency was discovered between apply & prune objects. + "dependency: apply -> prune": { + inventoryIDs: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["secret"]), + }, + applyObjs: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"], + testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), + }, + pruneObjs: object.UnstructuredSet{ + testutil.Unstructured(t, resources["secret"]), + }, + options: Options{Prune: true}, + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"], + testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), + }, + }, + &task.ApplyTask{ + TaskName: "apply-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["deployment"], + testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), + }, + }, + &taskrunner.WaitTask{ + TaskName: "wait-0", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["deployment"]), + }, + Condition: taskrunner.AllCurrent, + }, + &task.PruneTask{ + TaskName: "prune-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["secret"]), + }, + }, + &taskrunner.WaitTask{ + TaskName: "wait-1", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["secret"]), + }, + Condition: taskrunner.AllNotFound, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["secret"]), + }, + }, + }, + expectedStatus: []actuation.ObjectStatus{ + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["deployment"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["secret"]), + ), + Strategy: actuation.ActuationStrategyDelete, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + }, + }, + // This use case returns in a task plan that would cause a dependency + // to be applied. This is fine. + // This test just verifies that the dependency was discovered between + // prune & apply objects. + "dependency: prune -> apply": { + inventoryIDs: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["secret"]), + }, + applyObjs: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"]), + }, + pruneObjs: object.UnstructuredSet{ + testutil.Unstructured(t, resources["secret"], + testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))), + }, + options: Options{Prune: true}, + expectedTasks: []taskrunner.Task{ + &task.InvAddTask{ + TaskName: "inventory-add-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + Objects: object.UnstructuredSet{ + testutil.Unstructured(t, resources["deployment"]), + }, + }, + &task.ApplyTask{ + TaskName: "apply-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["deployment"]), + }, + }, + &taskrunner.WaitTask{ + TaskName: "wait-0", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["deployment"]), + }, + Condition: taskrunner.AllCurrent, + }, + &task.PruneTask{ + TaskName: "prune-0", + Objects: []*unstructured.Unstructured{ + testutil.Unstructured(t, resources["secret"], + testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))), + }, + }, + &taskrunner.WaitTask{ + TaskName: "wait-1", + Ids: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["secret"]), + }, + Condition: taskrunner.AllNotFound, + }, + &task.InvSetTask{ + TaskName: "inventory-set-0", + InvClient: &inventory.FakeClient{}, + InvInfo: invInfo, + PrevInventory: object.ObjMetadataSet{ + testutil.ToIdentifier(t, resources["secret"]), + }, + }, + }, + expectedStatus: []actuation.ObjectStatus{ + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["deployment"]), + ), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + { + ObjectReference: inventory.ObjectReferenceFromObjMetadata( + testutil.ToIdentifier(t, resources["secret"]), + ), + Strategy: actuation.ActuationStrategyDelete, + Actuation: actuation.ActuationPending, + Reconcile: actuation.ReconcilePending, + }, + }, + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + mapper := testutil.NewFakeRESTMapper() + // inject mapper & pruner for equality comparison + for _, t := range tc.expectedTasks { + switch typedTask := t.(type) { + case *task.ApplyTask: + typedTask.Mapper = mapper + case *task.PruneTask: + typedTask.Pruner = &prune.Pruner{} + case *taskrunner.WaitTask: + typedTask.Mapper = mapper + } + } + + fakeInvClient := inventory.NewFakeClient(tc.inventoryIDs) + vCollector := &validation.Collector{} + tqb := TaskQueueBuilder{ + Pruner: pruner, + Mapper: mapper, + InvClient: fakeInvClient, + Collector: vCollector, + } + taskContext := taskrunner.NewTaskContext(nil, nil) + tq := tqb.WithInventory(invInfo). + WithApplyObjects(tc.applyObjs). + WithPruneObjects(tc.pruneObjs). + Build(taskContext, tc.options) + + err := vCollector.ToError() + if tc.expectedError != nil { + assert.EqualError(t, err, tc.expectedError.Error()) + return + } + assert.NoError(t, err) + + asserter.Equal(t, tc.expectedTasks, tq.tasks) + + actualStatus := taskContext.InventoryManager().Inventory().Status.Objects + testutil.AssertEqual(t, tc.expectedStatus, actualStatus) + }) + } +} + +// waitTaskComparer allows comparion of WaitTasks, ignoring private fields. +func waitTaskComparer() cmp.Option { + return cmp.Comparer(func(x, y *taskrunner.WaitTask) bool { + if x == nil { + return y == nil + } + if y == nil { + return false + } + return x.TaskName == y.TaskName && + x.Ids.Hash() == y.Ids.Hash() && // exact order match + x.Condition == y.Condition && + x.Timeout == y.Timeout && + cmp.Equal(x.Mapper, y.Mapper) + }) +} + +// fakeClientComparer allows comparion of inventory.FakeClient, ignoring objs. +func fakeClientComparer() cmp.Option { + return cmp.Comparer(func(x, y *inventory.FakeClient) bool { + if x == nil { + return y == nil + } + if y == nil { + return false + } + return true + }) +} + +// inventoryInfoComparer allows comparion of inventory.Info, ignoring impl. +func inventoryInfoComparer() cmp.Option { + return cmp.Comparer(func(x, y inventory.Info) bool { + return x.ID() == y.ID() && + x.Name() == y.Name() && + x.Namespace() == y.Namespace() && + x.Strategy() == y.Strategy() + }) +} diff --git a/thirdparty/cli-utils/apply/task/apply_task.go b/thirdparty/cli-utils/apply/task/apply_task.go new file mode 100644 index 0000000000..4f7aa7ac00 --- /dev/null +++ b/thirdparty/cli-utils/apply/task/apply_task.go @@ -0,0 +1,287 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package task + +import ( + "context" + "fmt" + "io/ioutil" + "strings" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + "k8s.io/klog/v2" + "k8s.io/kubectl/pkg/cmd/apply" + cmddelete "k8s.io/kubectl/pkg/cmd/delete" + applyerror "sigs.k8s.io/cli-utils/pkg/apply/error" + "sigs.k8s.io/cli-utils/pkg/apply/event" + "sigs.k8s.io/cli-utils/pkg/apply/filter" + "sigs.k8s.io/cli-utils/pkg/apply/info" + "sigs.k8s.io/cli-utils/pkg/apply/mutator" + "sigs.k8s.io/cli-utils/pkg/apply/taskrunner" + "sigs.k8s.io/cli-utils/pkg/common" + "sigs.k8s.io/cli-utils/pkg/object" +) + +// applyOptions defines the two key functions on the ApplyOptions +// struct that is used by the ApplyTask. +type applyOptions interface { + + // Run applies the resource set with the SetObjects function + // to the cluster. + Run() error + + // SetObjects sets the slice of resource (in the form form resourceInfo objects) + // that will be applied upon invoking the Run function. + SetObjects([]*resource.Info) +} + +// ApplyTask applies the given Objects to the cluster +// by using the ApplyOptions. +type ApplyTask struct { + TaskName string + + DynamicClient dynamic.Interface + OpenAPIGetter discovery.OpenAPISchemaInterface + InfoHelper info.Helper + Mapper meta.RESTMapper + Objects object.UnstructuredSet + Filters []filter.ValidationFilter + Mutators []mutator.Interface + DryRunStrategy common.DryRunStrategy + ServerSideOptions common.ServerSideOptions +} + +// applyOptionsFactoryFunc is a factory function for creating a new +// applyOptions implementation. Used to allow unit testing. +var applyOptionsFactoryFunc = newApplyOptions + +func (a *ApplyTask) Name() string { + return a.TaskName +} + +func (a *ApplyTask) Action() event.ResourceAction { + return event.ApplyAction +} + +func (a *ApplyTask) Identifiers() object.ObjMetadataSet { + return object.UnstructuredSetToObjMetadataSet(a.Objects) +} + +// Start creates a new goroutine that will invoke +// the Run function on the ApplyOptions to update +// the cluster. It will push a TaskResult on the taskChannel +// to signal to the taskrunner that the task has completed (or failed). +// It will also fetch the Generation from each of the applied resources +// after the Run function has completed. This information is then added +// to the taskContext. The generation is increased every time +// the desired state of a resource is changed. +func (a *ApplyTask) Start(taskContext *taskrunner.TaskContext) { + go func() { + // TODO: pipe Context through TaskContext + ctx := context.TODO() + objects := a.Objects + klog.V(2).Infof("apply task starting (name: %q, objects: %d)", + a.Name(), len(objects)) + for _, obj := range objects { + // Set the client and mapping fields on the provided + // info so they can be applied to the cluster. + info, err := a.InfoHelper.BuildInfo(obj) + // BuildInfo strips path annotations. + // Use modified object for filters, mutations, and events. + obj = info.Object.(*unstructured.Unstructured) + id := object.UnstructuredToObjMetadata(obj) + if err != nil { + if klog.V(4).Enabled() { + klog.Errorf("unable to convert obj to info for %s/%s (%s)--continue", + obj.GetNamespace(), obj.GetName(), err) + } + taskContext.SendEvent(a.createApplyFailedEvent( + id, + applyerror.NewUnknownTypeError(err), + )) + taskContext.InventoryManager().AddFailedApply(id) + continue + } + + // Check filters to see if we're prevented from applying. + var filtered bool + var filterErr error + for _, filter := range a.Filters { + klog.V(6).Infof("apply filter %s: %s", filter.Name(), id) + var reason string + filtered, reason, filterErr = filter.Filter(obj) + if filterErr != nil { + if klog.V(5).Enabled() { + klog.Errorf("error during %s, (%s): %s", filter.Name(), id, filterErr) + } + taskContext.SendEvent(a.createApplyFailedEvent(id, filterErr)) + taskContext.InventoryManager().AddFailedApply(id) + break + } + if filtered { + klog.V(4).Infof("apply filtered (filter: %q, resource: %q, reason: %q)", filter.Name(), id, reason) + taskContext.SendEvent(a.createApplyEvent(id, event.Unchanged, obj)) + taskContext.InventoryManager().AddSkippedApply(id) + break + } + } + if filtered || filterErr != nil { + continue + } + + // Execute mutators, if any apply + err = a.mutate(ctx, obj) + if err != nil { + if klog.V(5).Enabled() { + klog.Errorf("error mutating: %w", err) + } + taskContext.SendEvent(a.createApplyFailedEvent(id, err)) + taskContext.InventoryManager().AddFailedApply(id) + continue + } + + // Create a new instance of the applyOptions interface and use it + // to apply the objects. + ao := applyOptionsFactoryFunc(a.Name(), taskContext.EventChannel(), + a.ServerSideOptions, a.DryRunStrategy, a.DynamicClient, a.OpenAPIGetter) + ao.SetObjects([]*resource.Info{info}) + klog.V(5).Infof("applying %s/%s...", info.Namespace, info.Name) + err = ao.Run() + if err != nil && a.ServerSideOptions.ServerSideApply && isAPIService(obj) && isStreamError(err) { + // Server-side Apply doesn't work with APIService before k8s 1.21 + // https://github.com/kubernetes/kubernetes/issues/89264 + // Thus APIService is handled specially using client-side apply. + err = a.clientSideApply(info, taskContext.EventChannel()) + } + if err != nil { + if klog.V(4).Enabled() { + klog.Errorf("error applying (%s/%s) %s", info.Namespace, info.Name, err) + } + taskContext.SendEvent(a.createApplyFailedEvent( + id, + applyerror.NewApplyRunError(err), + )) + taskContext.InventoryManager().AddFailedApply(id) + } else if info.Object != nil { + acc, err := meta.Accessor(info.Object) + if err == nil { + uid := acc.GetUID() + gen := acc.GetGeneration() + taskContext.InventoryManager().AddSuccessfulApply(id, uid, gen) + } + } + } + a.sendTaskResult(taskContext) + }() +} + +func newApplyOptions(taskName string, eventChannel chan<- event.Event, serverSideOptions common.ServerSideOptions, + strategy common.DryRunStrategy, dynamicClient dynamic.Interface, + openAPIGetter discovery.OpenAPISchemaInterface) applyOptions { + emptyString := "" + return &apply.ApplyOptions{ + VisitedNamespaces: sets.NewString(), + VisitedUids: sets.NewString(), + Overwrite: true, // Normally set in apply.NewApplyOptions + OpenAPIPatch: true, // Normally set in apply.NewApplyOptions + Recorder: genericclioptions.NoopRecorder{}, + IOStreams: genericclioptions.IOStreams{ + Out: ioutil.Discard, + ErrOut: ioutil.Discard, // TODO: Warning for no lastConfigurationAnnotation + // is printed directly to stderr in ApplyOptions. We + // should turn that into a warning on the event channel. + }, + // FilenameOptions are not needed since we don't use the ApplyOptions + // to read manifests. + DeleteOptions: &cmddelete.DeleteOptions{}, + PrintFlags: &genericclioptions.PrintFlags{ + OutputFormat: &emptyString, + }, + // Server-side apply if flag set or server-side dry run. + ServerSideApply: strategy.ServerDryRun() || serverSideOptions.ServerSideApply, + ForceConflicts: serverSideOptions.ForceConflicts, + FieldManager: serverSideOptions.FieldManager, + DryRunStrategy: strategy.Strategy(), + ToPrinter: (&KubectlPrinterAdapter{ + ch: eventChannel, + groupName: taskName, + }).toPrinterFunc(), + DynamicClient: dynamicClient, + DryRunVerifier: resource.NewQueryParamVerifier(dynamicClient, openAPIGetter, resource.QueryParamDryRun), + } +} + +func (a *ApplyTask) sendTaskResult(taskContext *taskrunner.TaskContext) { + klog.V(2).Infof("apply task completing (name: %q)", a.Name()) + taskContext.TaskChannel() <- taskrunner.TaskResult{} +} + +// Cancel is not supported by the ApplyTask. +func (a *ApplyTask) Cancel(_ *taskrunner.TaskContext) {} + +// StatusUpdate is not supported by the ApplyTask. +func (a *ApplyTask) StatusUpdate(_ *taskrunner.TaskContext, _ object.ObjMetadata) {} + +// mutate loops through the mutator list and executes them on the object. +func (a *ApplyTask) mutate(ctx context.Context, obj *unstructured.Unstructured) error { + id := object.UnstructuredToObjMetadata(obj) + for _, mutator := range a.Mutators { + klog.V(6).Infof("apply mutator %s: %s", mutator.Name(), id) + mutated, reason, err := mutator.Mutate(ctx, obj) + if err != nil { + return fmt.Errorf("failed to mutate %q with %q: %w", id, mutator.Name(), err) + } + if mutated { + klog.V(4).Infof("resource mutated (mutator: %q, resource: %q, reason: %q)", mutator.Name(), id, reason) + } + } + return nil +} + +// createApplyEvent is a helper function to package an apply event for a single resource. +func (a *ApplyTask) createApplyEvent(id object.ObjMetadata, operation event.ApplyEventOperation, resource *unstructured.Unstructured) event.Event { + return event.Event{ + Type: event.ApplyType, + ApplyEvent: event.ApplyEvent{ + GroupName: a.Name(), + Identifier: id, + Operation: operation, + Resource: resource, + }, + } +} + +func (a *ApplyTask) createApplyFailedEvent(id object.ObjMetadata, err error) event.Event { + return event.Event{ + Type: event.ApplyType, + ApplyEvent: event.ApplyEvent{ + GroupName: a.Name(), + Identifier: id, + Error: err, + }, + } +} + +func isAPIService(obj *unstructured.Unstructured) bool { + gk := obj.GroupVersionKind().GroupKind() + return gk.Group == "apiregistration.k8s.io" && gk.Kind == "APIService" +} + +// isStreamError checks if the error is a StreamError. Since kubectl wraps the actual StreamError, +// we can't check the error type. +func isStreamError(err error) bool { + return strings.Contains(err.Error(), "stream error: stream ID ") +} + +func (a *ApplyTask) clientSideApply(info *resource.Info, eventChannel chan<- event.Event) error { + ao := applyOptionsFactoryFunc(a.Name(), eventChannel, common.ServerSideOptions{ServerSideApply: false}, a.DryRunStrategy, a.DynamicClient, a.OpenAPIGetter) + ao.SetObjects([]*resource.Info{info}) + return ao.Run() +} diff --git a/thirdparty/cli-utils/apply/task/apply_task_test.go b/thirdparty/cli-utils/apply/task/apply_task_test.go new file mode 100644 index 0000000000..c245c50145 --- /dev/null +++ b/thirdparty/cli-utils/apply/task/apply_task_test.go @@ -0,0 +1,578 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package task + +import ( + "fmt" + "strings" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + "sigs.k8s.io/cli-utils/pkg/apply/cache" + "sigs.k8s.io/cli-utils/pkg/apply/event" + "sigs.k8s.io/cli-utils/pkg/apply/taskrunner" + "sigs.k8s.io/cli-utils/pkg/common" + "sigs.k8s.io/cli-utils/pkg/object" + "sigs.k8s.io/cli-utils/pkg/testutil" +) + +type resourceInfo struct { + group string + apiVersion string + kind string + name string + namespace string + uid types.UID + generation int64 +} + +// Tests that the correct "applied" objects are sent +// to the TaskContext correctly, since these are the +// applied objects added to the final inventory. +func TestApplyTask_BasicAppliedObjects(t *testing.T) { + testCases := map[string]struct { + applied []resourceInfo + }{ + "apply single namespaced resource": { + applied: []resourceInfo{ + { + group: "apps", + apiVersion: "apps/v1", + kind: "Deployment", + name: "foo", + namespace: "default", + uid: types.UID("my-uid"), + generation: int64(42), + }, + }, + }, + "apply multiple clusterscoped resources": { + applied: []resourceInfo{ + { + group: "custom.io", + apiVersion: "custom.io/v1beta1", + kind: "Custom", + name: "bar", + uid: types.UID("uid-1"), + generation: int64(32), + }, + { + group: "custom2.io", + apiVersion: "custom2.io/v1", + kind: "Custom2", + name: "foo", + uid: types.UID("uid-2"), + generation: int64(1), + }, + }, + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + eventChannel := make(chan event.Event) + defer close(eventChannel) + resourceCache := cache.NewResourceCacheMap() + taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache) + + objs := toUnstructureds(tc.applied) + + oldAO := applyOptionsFactoryFunc + applyOptionsFactoryFunc = func(string, chan<- event.Event, common.ServerSideOptions, common.DryRunStrategy, + dynamic.Interface, discovery.OpenAPISchemaInterface) applyOptions { + return &fakeApplyOptions{} + } + defer func() { applyOptionsFactoryFunc = oldAO }() + + restMapper := testutil.NewFakeRESTMapper(schema.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, schema.GroupVersionKind{ + Group: "anothercustom.io", + Version: "v2", + Kind: "AnotherCustom", + }) + + applyTask := &ApplyTask{ + Objects: objs, + Mapper: restMapper, + InfoHelper: &fakeInfoHelper{}, + } + + applyTask.Start(taskContext) + <-taskContext.TaskChannel() + + // The applied resources should be stored in the TaskContext + // for the final inventory. + expectedIDs := object.UnstructuredSetToObjMetadataSet(objs) + actual := taskContext.InventoryManager().SuccessfulApplies() + if !actual.Equal(expectedIDs) { + t.Errorf("expected (%s) inventory resources, got (%s)", expectedIDs, actual) + } + + im := taskContext.InventoryManager() + + for _, id := range expectedIDs { + assert.Falsef(t, im.IsFailedApply(id), "ApplyTask should NOT mark object as failed: %s", id) + assert.Falsef(t, im.IsSkippedApply(id), "ApplyTask should NOT mark object as skipped: %s", id) + } + }) + } +} + +func TestApplyTask_FetchGeneration(t *testing.T) { + testCases := map[string]struct { + rss []resourceInfo + }{ + "single namespaced resource": { + rss: []resourceInfo{ + { + group: "apps", + apiVersion: "apps/v1", + kind: "Deployment", + name: "foo", + namespace: "default", + uid: types.UID("my-uid"), + generation: int64(42), + }, + }, + }, + "multiple clusterscoped resources": { + rss: []resourceInfo{ + { + group: "custom.io", + apiVersion: "custom.io/v1beta1", + kind: "Custom", + name: "bar", + uid: types.UID("uid-1"), + generation: int64(32), + }, + { + group: "custom2.io", + apiVersion: "custom2.io/v1", + kind: "Custom2", + name: "foo", + uid: types.UID("uid-2"), + generation: int64(1), + }, + }, + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + eventChannel := make(chan event.Event) + defer close(eventChannel) + resourceCache := cache.NewResourceCacheMap() + taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache) + + objs := toUnstructureds(tc.rss) + + oldAO := applyOptionsFactoryFunc + applyOptionsFactoryFunc = func(string, chan<- event.Event, common.ServerSideOptions, common.DryRunStrategy, + dynamic.Interface, discovery.OpenAPISchemaInterface) applyOptions { + return &fakeApplyOptions{} + } + defer func() { applyOptionsFactoryFunc = oldAO }() + applyTask := &ApplyTask{ + Objects: objs, + InfoHelper: &fakeInfoHelper{}, + } + applyTask.Start(taskContext) + + <-taskContext.TaskChannel() + + for _, info := range tc.rss { + id := object.ObjMetadata{ + GroupKind: schema.GroupKind{ + Group: info.group, + Kind: info.kind, + }, + Name: info.name, + Namespace: info.namespace, + } + uid, _ := taskContext.InventoryManager().AppliedResourceUID(id) + assert.Equal(t, info.uid, uid) + + gen, _ := taskContext.InventoryManager().AppliedGeneration(id) + assert.Equal(t, info.generation, gen) + } + }) + } +} + +func TestApplyTask_DryRun(t *testing.T) { + testCases := map[string]struct { + objs []*unstructured.Unstructured + expectedObjects []object.ObjMetadata + expectedEvents []event.Event + }{ + "simple dry run": { + objs: []*unstructured.Unstructured{ + toUnstructured(map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "foo", + "namespace": "default", + }, + }), + }, + expectedObjects: []object.ObjMetadata{ + { + GroupKind: schema.GroupKind{ + Group: "apps", + Kind: "Deployment", + }, + Name: "foo", + Namespace: "default", + }, + }, + expectedEvents: []event.Event{}, + }, + "dry run with CRD and CR": { + objs: []*unstructured.Unstructured{ + toUnstructured(map[string]interface{}{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": map[string]interface{}{ + "name": "foo", + }, + "spec": map[string]interface{}{ + "group": "custom.io", + "names": map[string]interface{}{ + "kind": "Custom", + }, + "versions": []interface{}{ + map[string]interface{}{ + "name": "v1alpha1", + }, + }, + }, + }), + toUnstructured(map[string]interface{}{ + "apiVersion": "custom.io/v1alpha1", + "kind": "Custom", + "metadata": map[string]interface{}{ + "name": "bar", + }, + }), + }, + expectedObjects: []object.ObjMetadata{ + { + GroupKind: schema.GroupKind{ + Group: "custom.io", + Kind: "Custom", + }, + Name: "bar", + }, + }, + expectedEvents: []event.Event{}, + }, + } + + for tn, tc := range testCases { + for i := range common.Strategies { + drs := common.Strategies[i] + t.Run(tn, func(t *testing.T) { + eventChannel := make(chan event.Event) + resourceCache := cache.NewResourceCacheMap() + taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache) + + restMapper := testutil.NewFakeRESTMapper(schema.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, schema.GroupVersionKind{ + Group: "anothercustom.io", + Version: "v2", + Kind: "AnotherCustom", + }) + + ao := &fakeApplyOptions{} + oldAO := applyOptionsFactoryFunc + applyOptionsFactoryFunc = func(string, chan<- event.Event, common.ServerSideOptions, common.DryRunStrategy, + dynamic.Interface, discovery.OpenAPISchemaInterface) applyOptions { + return ao + } + defer func() { applyOptionsFactoryFunc = oldAO }() + + applyTask := &ApplyTask{ + Objects: tc.objs, + InfoHelper: &fakeInfoHelper{}, + Mapper: restMapper, + DryRunStrategy: drs, + } + + var events []event.Event + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + for msg := range eventChannel { + events = append(events, msg) + } + }() + + applyTask.Start(taskContext) + <-taskContext.TaskChannel() + close(eventChannel) + wg.Wait() + + assert.Equal(t, len(tc.expectedObjects), len(ao.objects)) + for i, obj := range ao.objects { + actual, err := object.InfoToObjMeta(obj) + if err != nil { + continue + } + assert.Equal(t, tc.expectedObjects[i], actual) + } + + assert.Equal(t, len(tc.expectedEvents), len(events)) + for i, e := range events { + assert.Equal(t, tc.expectedEvents[i].Type, e.Type) + } + }) + } + } +} + +func TestApplyTaskWithError(t *testing.T) { + testCases := map[string]struct { + objs []*unstructured.Unstructured + expectedObjects object.ObjMetadataSet + expectedEvents []event.Event + expectedSkipped object.ObjMetadataSet + expectedFailed object.ObjMetadataSet + }{ + "some resources have apply error": { + objs: []*unstructured.Unstructured{ + toUnstructured(map[string]interface{}{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": map[string]interface{}{ + "name": "foo", + }, + "spec": map[string]interface{}{ + "group": "anothercustom.io", + "names": map[string]interface{}{ + "kind": "AnotherCustom", + }, + "versions": []interface{}{ + map[string]interface{}{ + "name": "v2", + }, + }, + }, + }), + toUnstructured(map[string]interface{}{ + "apiVersion": "anothercustom.io/v2", + "kind": "AnotherCustom", + "metadata": map[string]interface{}{ + "name": "bar", + "namespace": "barbar", + }, + }), + toUnstructured(map[string]interface{}{ + "apiVersion": "anothercustom.io/v2", + "kind": "AnotherCustom", + "metadata": map[string]interface{}{ + "name": "bar-with-failure", + "namespace": "barbar", + }, + }), + }, + expectedObjects: object.ObjMetadataSet{ + { + GroupKind: schema.GroupKind{ + Group: "apiextensions.k8s.io", + Kind: "CustomResourceDefinition", + }, + Name: "foo", + }, + { + GroupKind: schema.GroupKind{ + Group: "anothercustom.io", + Kind: "AnotherCustom", + }, + Name: "bar", + Namespace: "barbar", + }, + }, + expectedEvents: []event.Event{ + { + Type: event.ApplyType, + ApplyEvent: event.ApplyEvent{ + Error: fmt.Errorf("expected apply error"), + }, + }, + }, + expectedFailed: object.ObjMetadataSet{ + { + GroupKind: schema.GroupKind{ + Group: "anothercustom.io", + Kind: "AnotherCustom", + }, + Name: "bar-with-failure", + Namespace: "barbar", + }, + }, + }, + } + + for tn, tc := range testCases { + drs := common.DryRunNone + t.Run(tn, func(t *testing.T) { + eventChannel := make(chan event.Event) + resourceCache := cache.NewResourceCacheMap() + taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache) + + restMapper := testutil.NewFakeRESTMapper(schema.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, schema.GroupVersionKind{ + Group: "anothercustom.io", + Version: "v2", + Kind: "AnotherCustom", + }) + + ao := &fakeApplyOptions{} + oldAO := applyOptionsFactoryFunc + applyOptionsFactoryFunc = func(string, chan<- event.Event, common.ServerSideOptions, common.DryRunStrategy, + dynamic.Interface, discovery.OpenAPISchemaInterface) applyOptions { + return ao + } + defer func() { applyOptionsFactoryFunc = oldAO }() + + applyTask := &ApplyTask{ + Objects: tc.objs, + InfoHelper: &fakeInfoHelper{}, + Mapper: restMapper, + DryRunStrategy: drs, + } + + var events []event.Event + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + for msg := range eventChannel { + events = append(events, msg) + } + }() + + applyTask.Start(taskContext) + <-taskContext.TaskChannel() + close(eventChannel) + wg.Wait() + + assert.Equal(t, len(tc.expectedObjects), len(ao.passedObjects)) + for i, obj := range ao.passedObjects { + actual, err := object.InfoToObjMeta(obj) + if err != nil { + continue + } + assert.Equal(t, tc.expectedObjects[i], actual) + } + + assert.Equal(t, len(tc.expectedEvents), len(events)) + for i, e := range events { + assert.Equal(t, tc.expectedEvents[i].Type, e.Type) + assert.Equal(t, tc.expectedEvents[i].ApplyEvent.Error.Error(), e.ApplyEvent.Error.Error()) + } + + applyIds := object.UnstructuredSetToObjMetadataSet(tc.objs) + + im := taskContext.InventoryManager() + + // validate record of failed prunes + for _, id := range tc.expectedFailed { + assert.Truef(t, im.IsFailedApply(id), "ApplyTask should mark object as failed: %s", id) + } + for _, id := range applyIds.Diff(tc.expectedFailed) { + assert.Falsef(t, im.IsFailedApply(id), "ApplyTask should NOT mark object as failed: %s", id) + } + // validate record of skipped prunes + for _, id := range tc.expectedSkipped { + assert.Truef(t, im.IsSkippedApply(id), "ApplyTask should mark object as skipped: %s", id) + } + for _, id := range applyIds.Diff(tc.expectedSkipped) { + assert.Falsef(t, im.IsSkippedApply(id), "ApplyTask should NOT mark object as skipped: %s", id) + } + }) + } +} + +func toUnstructured(obj map[string]interface{}) *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: obj, + } +} + +func toUnstructureds(rss []resourceInfo) []*unstructured.Unstructured { + var objs []*unstructured.Unstructured + + for _, rs := range rss { + objs = append(objs, &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": rs.apiVersion, + "kind": rs.kind, + "metadata": map[string]interface{}{ + "name": rs.name, + "namespace": rs.namespace, + "uid": string(rs.uid), + "generation": rs.generation, + "annotations": map[string]interface{}{ + "config.k8s.io/owning-inventory": "id", + }, + }, + }, + }) + } + return objs +} + +type fakeApplyOptions struct { + objects []*resource.Info + passedObjects []*resource.Info +} + +func (f *fakeApplyOptions) Run() error { + var err error + for _, obj := range f.objects { + if strings.Contains(obj.Name, "failure") { + err = fmt.Errorf("expected apply error") + } else { + f.passedObjects = append(f.passedObjects, obj) + } + } + return err +} + +func (f *fakeApplyOptions) SetObjects(objects []*resource.Info) { + f.objects = objects +} + +type fakeInfoHelper struct{} + +func (f *fakeInfoHelper) UpdateInfo(*resource.Info) error { + return nil +} + +func (f *fakeInfoHelper) BuildInfos(objs []*unstructured.Unstructured) ([]*resource.Info, error) { + return object.UnstructuredsToInfos(objs) +} + +func (f *fakeInfoHelper) BuildInfo(obj *unstructured.Unstructured) (*resource.Info, error) { + return object.UnstructuredToInfo(obj) +} diff --git a/thirdparty/cli-utils/apply/task/delete_inv_task.go b/thirdparty/cli-utils/apply/task/delete_inv_task.go new file mode 100644 index 0000000000..b1bb75f628 --- /dev/null +++ b/thirdparty/cli-utils/apply/task/delete_inv_task.go @@ -0,0 +1,57 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package task + +import ( + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/klog/v2" + "sigs.k8s.io/cli-utils/pkg/apply/event" + "sigs.k8s.io/cli-utils/pkg/apply/taskrunner" + "sigs.k8s.io/cli-utils/pkg/common" + "sigs.k8s.io/cli-utils/pkg/inventory" + "sigs.k8s.io/cli-utils/pkg/object" +) + +// DeleteInvTask encapsulates structures necessary to delete +// the inventory object from the cluster. Implements +// the Task interface. This task should happen after all +// resources have been deleted. +type DeleteInvTask struct { + TaskName string + InvClient inventory.Client + InvInfo inventory.Info + DryRun common.DryRunStrategy +} + +func (i *DeleteInvTask) Name() string { + return i.TaskName +} + +func (i *DeleteInvTask) Action() event.ResourceAction { + return event.InventoryAction +} + +func (i *DeleteInvTask) Identifiers() object.ObjMetadataSet { + return object.ObjMetadataSet{} +} + +// Start deletes the inventory object from the cluster. +func (i *DeleteInvTask) Start(taskContext *taskrunner.TaskContext) { + go func() { + klog.V(2).Infof("delete inventory task starting (name: %q)", i.Name()) + err := i.InvClient.DeleteInventoryObj(i.InvInfo, i.DryRun) + // Not found is not error, since this means it was already deleted. + if apierrors.IsNotFound(err) { + err = nil + } + klog.V(2).Infof("delete inventory task completing (name: %q)", i.Name()) + taskContext.TaskChannel() <- taskrunner.TaskResult{Err: err} + }() +} + +// Cancel is not supported by the DeleteInvTask. +func (i *DeleteInvTask) Cancel(_ *taskrunner.TaskContext) {} + +// StatusUpdate is not supported by the DeleteInvTask. +func (i *DeleteInvTask) StatusUpdate(_ *taskrunner.TaskContext, _ object.ObjMetadata) {} diff --git a/thirdparty/cli-utils/apply/task/delete_inv_task_test.go b/thirdparty/cli-utils/apply/task/delete_inv_task_test.go new file mode 100644 index 0000000000..aa9ab35935 --- /dev/null +++ b/thirdparty/cli-utils/apply/task/delete_inv_task_test.go @@ -0,0 +1,68 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package task + +import ( + "testing" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/cli-utils/pkg/apply/cache" + "sigs.k8s.io/cli-utils/pkg/apply/event" + "sigs.k8s.io/cli-utils/pkg/apply/taskrunner" + "sigs.k8s.io/cli-utils/pkg/common" + "sigs.k8s.io/cli-utils/pkg/inventory" + "sigs.k8s.io/cli-utils/pkg/object" +) + +func TestDeleteInvTask(t *testing.T) { + testCases := map[string]struct { + err error + isError bool + }{ + "no error case": { + err: nil, + isError: false, + }, + "error is returned in result": { + err: apierrors.NewResourceExpired("unused message"), + isError: true, + }, + "inventory not found is not error and not returned": { + err: apierrors.NewNotFound(schema.GroupResource{Resource: "simples"}, + "unused-resource-name"), + isError: false, + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + client := inventory.NewFakeClient(object.ObjMetadataSet{}) + client.Err = tc.err + eventChannel := make(chan event.Event) + resourceCache := cache.NewResourceCacheMap() + context := taskrunner.NewTaskContext(eventChannel, resourceCache) + + task := DeleteInvTask{ + TaskName: taskName, + InvClient: client, + InvInfo: localInv, + DryRun: common.DryRunNone, + } + if taskName != task.Name() { + t.Errorf("expected task name (%s), got (%s)", taskName, task.Name()) + } + task.Start(context) + result := <-context.TaskChannel() + if tc.isError { + if tc.err != result.Err { + t.Errorf("running DeleteInvTask expected error (%s), got (%s)", tc.err, result.Err) + } + } else { + if result.Err != nil { + t.Errorf("unexpected error running DeleteInvTask: %s", result.Err) + } + } + }) + } +} diff --git a/thirdparty/cli-utils/apply/task/inv_add_task.go b/thirdparty/cli-utils/apply/task/inv_add_task.go new file mode 100644 index 0000000000..ef9087a991 --- /dev/null +++ b/thirdparty/cli-utils/apply/task/inv_add_task.go @@ -0,0 +1,98 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package task + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/klog/v2" + "sigs.k8s.io/cli-utils/pkg/apply/event" + "sigs.k8s.io/cli-utils/pkg/apply/taskrunner" + "sigs.k8s.io/cli-utils/pkg/common" + "sigs.k8s.io/cli-utils/pkg/inventory" + "sigs.k8s.io/cli-utils/pkg/object" +) + +var ( + namespaceGVKv1 = schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"} +) + +// InvAddTask encapsulates structures necessary to add/merge inventory +// into the cluster. The InvAddTask should add/merge inventory references +// before the actual object is applied. +type InvAddTask struct { + TaskName string + InvClient inventory.Client + InvInfo inventory.Info + Objects object.UnstructuredSet + DryRun common.DryRunStrategy +} + +func (i *InvAddTask) Name() string { + return i.TaskName +} + +func (i *InvAddTask) Action() event.ResourceAction { + return event.InventoryAction +} + +func (i *InvAddTask) Identifiers() object.ObjMetadataSet { + return object.UnstructuredSetToObjMetadataSet(i.Objects) +} + +// Start updates the inventory by merging the locally applied objects +// into the current inventory. +func (i *InvAddTask) Start(taskContext *taskrunner.TaskContext) { + go func() { + klog.V(2).Infof("inventory add task starting (name: %q)", i.Name()) + if err := inventory.ValidateNoInventory(i.Objects); err != nil { + i.sendTaskResult(taskContext, err) + return + } + // Ensures the namespace exists before applying the inventory object into it. + if invNamespace := inventoryNamespaceInSet(i.InvInfo, i.Objects); invNamespace != nil { + klog.V(4).Infof("applying inventory namespace %s", invNamespace.GetName()) + if err := i.InvClient.ApplyInventoryNamespace(invNamespace, i.DryRun); err != nil { + i.sendTaskResult(taskContext, err) + return + } + } + klog.V(4).Infof("merging %d local objects into inventory", len(i.Objects)) + currentObjs := object.UnstructuredSetToObjMetadataSet(i.Objects) + _, err := i.InvClient.Merge(i.InvInfo, currentObjs, i.DryRun) + i.sendTaskResult(taskContext, err) + }() +} + +// Cancel is not supported by the InvAddTask. +func (i *InvAddTask) Cancel(_ *taskrunner.TaskContext) {} + +// StatusUpdate is not supported by the InvAddTask. +func (i *InvAddTask) StatusUpdate(_ *taskrunner.TaskContext, _ object.ObjMetadata) {} + +// inventoryNamespaceInSet returns the the namespace the passed inventory +// object will be applied to, or nil if this namespace object does not exist +// in the passed slice "infos" or the inventory object is cluster-scoped. +func inventoryNamespaceInSet(inv inventory.Info, objs object.UnstructuredSet) *unstructured.Unstructured { + if inv == nil { + return nil + } + invNamespace := inv.Namespace() + + for _, obj := range objs { + gvk := obj.GetObjectKind().GroupVersionKind() + if gvk == namespaceGVKv1 && obj.GetName() == invNamespace { + inventory.AddInventoryIDAnnotation(obj, inv) + return obj + } + } + return nil +} + +func (i *InvAddTask) sendTaskResult(taskContext *taskrunner.TaskContext, err error) { + klog.V(2).Infof("inventory add task completing (name: %q)", i.Name()) + taskContext.TaskChannel() <- taskrunner.TaskResult{ + Err: err, + } +} diff --git a/thirdparty/cli-utils/apply/task/inv_add_task_test.go b/thirdparty/cli-utils/apply/task/inv_add_task_test.go new file mode 100644 index 0000000000..51d2804932 --- /dev/null +++ b/thirdparty/cli-utils/apply/task/inv_add_task_test.go @@ -0,0 +1,196 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package task + +import ( + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/cli-utils/pkg/apply/cache" + "sigs.k8s.io/cli-utils/pkg/apply/event" + "sigs.k8s.io/cli-utils/pkg/apply/taskrunner" + "sigs.k8s.io/cli-utils/pkg/common" + "sigs.k8s.io/cli-utils/pkg/inventory" + "sigs.k8s.io/cli-utils/pkg/object" +) + +var namespace = "test-namespace" + +var inventoryObj = &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "test-inventory-obj", + "namespace": namespace, + "labels": map[string]interface{}{ + common.InventoryLabel: "test-app-label", + }, + }, + }, +} + +var localInv = inventory.WrapInventoryInfoObj(inventoryObj) + +var obj1 = &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]interface{}{ + "name": "obj1", + "namespace": namespace, + }, + }, +} + +var obj2 = &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "batch/v1", + "kind": "Job", + "metadata": map[string]interface{}{ + "name": "obj2", + "namespace": namespace, + }, + }, +} + +var obj3 = &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "obj3", + "namespace": "different-namespace", + }, + }, +} + +const taskName = "test-inventory-task" + +func TestInvAddTask(t *testing.T) { + id1 := object.UnstructuredToObjMetadata(obj1) + id2 := object.UnstructuredToObjMetadata(obj2) + id3 := object.UnstructuredToObjMetadata(obj3) + + tests := map[string]struct { + initialObjs object.ObjMetadataSet + applyObjs []*unstructured.Unstructured + expectedObjs object.ObjMetadataSet + }{ + "no initial inventory and no apply objects; no merged inventory": { + initialObjs: object.ObjMetadataSet{}, + applyObjs: []*unstructured.Unstructured{}, + expectedObjs: object.ObjMetadataSet{}, + }, + "no initial inventory, one apply object; one merged inventory": { + initialObjs: object.ObjMetadataSet{}, + applyObjs: []*unstructured.Unstructured{obj1}, + expectedObjs: object.ObjMetadataSet{id1}, + }, + "one initial inventory, no apply object; one merged inventory": { + initialObjs: object.ObjMetadataSet{id2}, + applyObjs: []*unstructured.Unstructured{}, + expectedObjs: object.ObjMetadataSet{id2}, + }, + "one initial inventory, one apply object; one merged inventory": { + initialObjs: object.ObjMetadataSet{id3}, + applyObjs: []*unstructured.Unstructured{obj3}, + expectedObjs: object.ObjMetadataSet{id3}, + }, + "three initial inventory, two same objects; three merged inventory": { + initialObjs: object.ObjMetadataSet{id1, id2, id3}, + applyObjs: []*unstructured.Unstructured{obj2, obj3}, + expectedObjs: object.ObjMetadataSet{id1, id2, id3}, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + client := inventory.NewFakeClient(tc.initialObjs) + eventChannel := make(chan event.Event) + resourceCache := cache.NewResourceCacheMap() + context := taskrunner.NewTaskContext(eventChannel, resourceCache) + + task := InvAddTask{ + TaskName: taskName, + InvClient: client, + InvInfo: nil, + Objects: tc.applyObjs, + } + if taskName != task.Name() { + t.Errorf("expected task name (%s), got (%s)", taskName, task.Name()) + } + applyIds := object.UnstructuredSetToObjMetadataSet(tc.applyObjs) + if !task.Identifiers().Equal(applyIds) { + t.Errorf("expected task ids (%s), got (%s)", applyIds, task.Identifiers()) + } + task.Start(context) + result := <-context.TaskChannel() + if result.Err != nil { + t.Errorf("unexpected error running InvAddTask: %s", result.Err) + } + actual, _ := client.GetClusterObjs(nil) + if !tc.expectedObjs.Equal(actual) { + t.Errorf("expected merged inventory (%s), got (%s)", tc.expectedObjs, actual) + } + }) + } +} + +func TestInventoryNamespaceInSet(t *testing.T) { + inventoryNamespace := createNamespace(namespace) + + tests := map[string]struct { + inv inventory.Info + objects []*unstructured.Unstructured + namespace *unstructured.Unstructured + }{ + "Nil inventory object, no resources returns nil namespace": { + inv: nil, + objects: []*unstructured.Unstructured{}, + namespace: nil, + }, + "Inventory object, but no resources returns nil namespace": { + inv: localInv, + objects: []*unstructured.Unstructured{}, + namespace: nil, + }, + "Inventory object, resources with no namespace returns nil namespace": { + inv: localInv, + objects: []*unstructured.Unstructured{obj1, obj2}, + namespace: nil, + }, + "Inventory object, different namespace returns nil namespace": { + inv: localInv, + objects: []*unstructured.Unstructured{createNamespace("foo")}, + namespace: nil, + }, + "Inventory object, inventory namespace returns inventory namespace": { + inv: localInv, + objects: []*unstructured.Unstructured{obj1, inventoryNamespace, obj3}, + namespace: inventoryNamespace, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + actualNamespace := inventoryNamespaceInSet(tc.inv, tc.objects) + if tc.namespace != actualNamespace { + t.Fatalf("expected namespace (%v), got (%v)", tc.namespace, actualNamespace) + } + }) + } +} + +func createNamespace(ns string) *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": map[string]interface{}{ + "name": ns, + }, + }, + } +} diff --git a/thirdparty/cli-utils/apply/task/inv_set_task.go b/thirdparty/cli-utils/apply/task/inv_set_task.go new file mode 100644 index 0000000000..4bd92da59b --- /dev/null +++ b/thirdparty/cli-utils/apply/task/inv_set_task.go @@ -0,0 +1,132 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package task + +import ( + "k8s.io/klog/v2" + "sigs.k8s.io/cli-utils/pkg/apply/event" + "sigs.k8s.io/cli-utils/pkg/apply/taskrunner" + "sigs.k8s.io/cli-utils/pkg/common" + "sigs.k8s.io/cli-utils/pkg/inventory" + "sigs.k8s.io/cli-utils/pkg/object" +) + +// InvSetTask encapsulates structures necessary to set the +// inventory references at the end of the apply/prune. +type InvSetTask struct { + TaskName string + InvClient inventory.Client + InvInfo inventory.Info + PrevInventory object.ObjMetadataSet + DryRun common.DryRunStrategy +} + +func (i *InvSetTask) Name() string { + return i.TaskName +} + +func (i *InvSetTask) Action() event.ResourceAction { + return event.InventoryAction +} + +func (i *InvSetTask) Identifiers() object.ObjMetadataSet { + return object.ObjMetadataSet{} +} + +// Start sets (creates or replaces) the inventory. +// +// The guiding principal is that anything in the cluster should be in the +// inventory, unless it was explicitly abandoned. +// +// This task must run after all the apply and prune tasks have completed. +// +// Added objects: +// - Applied resources (successful) +// +// Retained objects: +// - Applied resources (filtered/skipped) +// - Applied resources (failed) +// - Deleted resources (filtered/skipped) that were not abandoned +// - Deleted resources (failed) +// - Abandoned resources (failed) +// +// Removed objects: +// - Deleted resources (successful) +// - Abandoned resources (successful) +func (i *InvSetTask) Start(taskContext *taskrunner.TaskContext) { + go func() { + klog.V(2).Infof("inventory set task starting (name: %q)", i.Name()) + invObjs := object.ObjMetadataSet{} + + // TODO: Just use InventoryManager.Store() + im := taskContext.InventoryManager() + + // If an object applied successfully, keep or add it to the inventory. + appliedObjs := im.SuccessfulApplies() + klog.V(4).Infof("set inventory %d successful applies", len(appliedObjs)) + invObjs = invObjs.Union(appliedObjs) + + // If an object failed to apply and was previously stored in the inventory, + // then keep it in the inventory so it can be applied/pruned next time. + // This will remove new resources that failed to apply from the inventory, + // because even tho they were added by InvAddTask, the PrevInventory + // represents the inventory before the pipeline has run. + applyFailures := i.PrevInventory.Intersection(im.FailedApplies()) + klog.V(4).Infof("keep in inventory %d failed applies", len(applyFailures)) + invObjs = invObjs.Union(applyFailures) + + // If an object skipped apply and was previously stored in the inventory, + // then keep it in the inventory so it can be applied/pruned next time. + // It's likely that all the skipped applies are already in the inventory, + // because the apply filters all currently depend on cluster state, + // but we're doing the intersection anyway just to be sure. + applySkips := i.PrevInventory.Intersection(im.SkippedApplies()) + klog.V(4).Infof("keep in inventory %d skipped applies", len(applySkips)) + invObjs = invObjs.Union(applySkips) + + // If an object failed to delete and was previously stored in the inventory, + // then keep it in the inventory so it can be applied/pruned next time. + // It's likely that all the delete failures are already in the inventory, + // because the set of resources to prune comes from the inventory, + // but we're doing the intersection anyway just to be sure. + pruneFailures := i.PrevInventory.Intersection(im.FailedDeletes()) + klog.V(4).Infof("set inventory %d failed prunes", len(pruneFailures)) + invObjs = invObjs.Union(pruneFailures) + + // If an object skipped delete and was previously stored in the inventory, + // then keep it in the inventory so it can be applied/pruned next time. + // It's likely that all the skipped deletes are already in the inventory, + // because the set of resources to prune comes from the inventory, + // but we're doing the intersection anyway just to be sure. + pruneSkips := i.PrevInventory.Intersection(im.SkippedDeletes()) + klog.V(4).Infof("keep in inventory %d skipped prunes", len(pruneSkips)) + invObjs = invObjs.Union(pruneSkips) + + // If an object is abandoned, then remove it from the inventory. + abandonedObjects := taskContext.AbandonedObjects() + klog.V(4).Infof("remove from inventory %d abandoned objects", len(abandonedObjects)) + invObjs = invObjs.Diff(abandonedObjects) + + // If an object is invalid and was previously stored in the inventory, + // then keep it in the inventory so it can be applied/pruned next time. + invalidObjects := i.PrevInventory.Intersection(taskContext.InvalidObjects()) + klog.V(4).Infof("keep in inventory %d invalid objects", len(invalidObjects)) + invObjs = invObjs.Union(invalidObjects) + + klog.V(4).Infof("get the apply status for %d objects", len(invObjs)) + objStatus := taskContext.InventoryManager().Inventory().Status.Objects + + klog.V(4).Infof("set inventory %d total objects", len(invObjs)) + err := i.InvClient.Replace(i.InvInfo, invObjs, objStatus, i.DryRun) + + klog.V(2).Infof("inventory set task completing (name: %q)", i.Name()) + taskContext.TaskChannel() <- taskrunner.TaskResult{Err: err} + }() +} + +// Cancel is not supported by the InvSetTask. +func (i *InvSetTask) Cancel(_ *taskrunner.TaskContext) {} + +// StatusUpdate is not supported by the InvSetTask. +func (i *InvSetTask) StatusUpdate(_ *taskrunner.TaskContext, _ object.ObjMetadata) {} diff --git a/thirdparty/cli-utils/apply/task/inv_set_task_test.go b/thirdparty/cli-utils/apply/task/inv_set_task_test.go new file mode 100644 index 0000000000..766da0bbd1 --- /dev/null +++ b/thirdparty/cli-utils/apply/task/inv_set_task_test.go @@ -0,0 +1,214 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package task + +import ( + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/cli-utils/pkg/apply/cache" + "sigs.k8s.io/cli-utils/pkg/apply/event" + "sigs.k8s.io/cli-utils/pkg/apply/taskrunner" + "sigs.k8s.io/cli-utils/pkg/inventory" + "sigs.k8s.io/cli-utils/pkg/object" + "sigs.k8s.io/cli-utils/pkg/testutil" +) + +var objInvalid = &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + }, +} + +func TestInvSetTask(t *testing.T) { + id1 := object.UnstructuredToObjMetadata(obj1) + id2 := object.UnstructuredToObjMetadata(obj2) + id3 := object.UnstructuredToObjMetadata(obj3) + idInvalid := object.UnstructuredToObjMetadata(objInvalid) + + tests := map[string]struct { + prevInventory object.ObjMetadataSet + appliedObjs object.ObjMetadataSet + failedApplies object.ObjMetadataSet + failedDeletes object.ObjMetadataSet + skippedApplies object.ObjMetadataSet + skippedDeletes object.ObjMetadataSet + abandonedObjs object.ObjMetadataSet + invalidObjs object.ObjMetadataSet + expectedObjs object.ObjMetadataSet + }{ + "no apply objs, no prune failures; no inventory": { + expectedObjs: object.ObjMetadataSet{}, + }, + "one apply objs, no prune failures; one inventory": { + appliedObjs: object.ObjMetadataSet{id1}, + expectedObjs: object.ObjMetadataSet{id1}, + }, + "no apply objs, one prune failure, in prev inventory; one inventory": { + prevInventory: object.ObjMetadataSet{id1}, + failedDeletes: object.ObjMetadataSet{id1}, + expectedObjs: object.ObjMetadataSet{id1}, + }, + "no apply objs, one prune failure, not in prev inventory; no inventory": { + // aritifical use case: prunes come from the inventory + failedDeletes: object.ObjMetadataSet{id1}, + expectedObjs: object.ObjMetadataSet{}, + }, + "one apply objs, one prune failures; one inventory": { + // aritifical use case: applies and prunes are mutually exclusive. + // Delete failure overwrites apply success in object status. + appliedObjs: object.ObjMetadataSet{id3}, + failedDeletes: object.ObjMetadataSet{id3}, + expectedObjs: object.ObjMetadataSet{}, + }, + "two apply objs, two prune failures; three inventory": { + // aritifical use case: applies and prunes are mutually exclusive + prevInventory: object.ObjMetadataSet{id2, id3}, + appliedObjs: object.ObjMetadataSet{id1, id2}, + failedDeletes: object.ObjMetadataSet{id2, id3}, + expectedObjs: object.ObjMetadataSet{id1, id2, id3}, + }, + "no apply objs, no apply failures, no prune failures; no inventory": { + failedApplies: object.ObjMetadataSet{id3}, + expectedObjs: object.ObjMetadataSet{}, + }, + "one apply failure not in prev inventory; no inventory": { + failedApplies: object.ObjMetadataSet{id3}, + expectedObjs: object.ObjMetadataSet{}, + }, + "one apply obj, one apply failure not in prev inventory; one inventory": { + appliedObjs: object.ObjMetadataSet{id2}, + failedApplies: object.ObjMetadataSet{id3}, + expectedObjs: object.ObjMetadataSet{id2}, + }, + "one apply obj, one apply failure in prev inventory; one inventory": { + appliedObjs: object.ObjMetadataSet{id2}, + failedApplies: object.ObjMetadataSet{id3}, + prevInventory: object.ObjMetadataSet{id3}, + expectedObjs: object.ObjMetadataSet{id2, id3}, + }, + "one apply obj, two apply failures with one in prev inventory; two inventory": { + appliedObjs: object.ObjMetadataSet{id2}, + failedApplies: object.ObjMetadataSet{id1, id3}, + prevInventory: object.ObjMetadataSet{id3}, + expectedObjs: object.ObjMetadataSet{id2, id3}, + }, + "three apply failures with two in prev inventory; two inventory": { + failedApplies: object.ObjMetadataSet{id1, id2, id3}, + prevInventory: object.ObjMetadataSet{id2, id3}, + expectedObjs: object.ObjMetadataSet{id2, id3}, + }, + "three apply failures with three in prev inventory; three inventory": { + failedApplies: object.ObjMetadataSet{id1, id2, id3}, + prevInventory: object.ObjMetadataSet{id2, id3, id1}, + expectedObjs: object.ObjMetadataSet{id2, id1, id3}, + }, + "one skipped apply from prev inventory; one inventory": { + prevInventory: object.ObjMetadataSet{id1}, + skippedApplies: object.ObjMetadataSet{id1}, + expectedObjs: object.ObjMetadataSet{id1}, + }, + "one skipped apply, no prev inventory; no inventory": { + skippedApplies: object.ObjMetadataSet{id1}, + expectedObjs: object.ObjMetadataSet{}, + }, + "one apply obj, one skipped apply, two prev inventory; two inventory": { + prevInventory: object.ObjMetadataSet{id1, id2}, + appliedObjs: object.ObjMetadataSet{id2}, + skippedApplies: object.ObjMetadataSet{id1}, + expectedObjs: object.ObjMetadataSet{id1, id2}, + }, + "one skipped delete from prev inventory; one inventory": { + prevInventory: object.ObjMetadataSet{id1}, + skippedDeletes: object.ObjMetadataSet{id1}, + expectedObjs: object.ObjMetadataSet{id1}, + }, + "one apply obj, one skipped delete, two prev inventory; two inventory": { + prevInventory: object.ObjMetadataSet{id1, id2}, + appliedObjs: object.ObjMetadataSet{id2}, + skippedDeletes: object.ObjMetadataSet{id1}, + expectedObjs: object.ObjMetadataSet{id1, id2}, + }, + "two apply obj, one abandoned, three in prev inventory; two inventory": { + prevInventory: object.ObjMetadataSet{id1, id2, id3}, + appliedObjs: object.ObjMetadataSet{id1, id2}, + abandonedObjs: object.ObjMetadataSet{id3}, + expectedObjs: object.ObjMetadataSet{id1, id2}, + }, + "two abandoned, two in prev inventory; no inventory": { + prevInventory: object.ObjMetadataSet{id2, id3}, + abandonedObjs: object.ObjMetadataSet{id2, id3}, + expectedObjs: object.ObjMetadataSet{}, + }, + "same obj skipped delete and abandoned, one in prev inventory; no inventory": { + prevInventory: object.ObjMetadataSet{id3}, + skippedDeletes: object.ObjMetadataSet{id3}, + abandonedObjs: object.ObjMetadataSet{id3}, + expectedObjs: object.ObjMetadataSet{}, + }, + "preserve invalid objects in the inventory": { + prevInventory: object.ObjMetadataSet{id3, idInvalid}, + appliedObjs: object.ObjMetadataSet{id3}, + invalidObjs: object.ObjMetadataSet{idInvalid}, + expectedObjs: object.ObjMetadataSet{id3, idInvalid}, + }, + "ignore invalid objects not in the inventory": { + prevInventory: object.ObjMetadataSet{id3}, + appliedObjs: object.ObjMetadataSet{id3}, + invalidObjs: object.ObjMetadataSet{idInvalid}, + expectedObjs: object.ObjMetadataSet{id3}, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + client := inventory.NewFakeClient(object.ObjMetadataSet{}) + eventChannel := make(chan event.Event) + resourceCache := cache.NewResourceCacheMap() + context := taskrunner.NewTaskContext(eventChannel, resourceCache) + + task := InvSetTask{ + TaskName: taskName, + InvClient: client, + InvInfo: nil, + PrevInventory: tc.prevInventory, + } + im := context.InventoryManager() + for _, applyObj := range tc.appliedObjs { + im.AddSuccessfulApply(applyObj, "unusued-uid", int64(0)) + } + for _, applyFailure := range tc.failedApplies { + im.AddFailedApply(applyFailure) + } + for _, pruneObj := range tc.failedDeletes { + im.AddFailedDelete(pruneObj) + } + for _, skippedApply := range tc.skippedApplies { + im.AddSkippedApply(skippedApply) + } + for _, skippedDelete := range tc.skippedDeletes { + im.AddSkippedDelete(skippedDelete) + } + for _, abandonedObj := range tc.abandonedObjs { + context.AddAbandonedObject(abandonedObj) + } + for _, invalidObj := range tc.invalidObjs { + context.AddInvalidObject(invalidObj) + } + if taskName != task.Name() { + t.Errorf("expected task name (%s), got (%s)", taskName, task.Name()) + } + task.Start(context) + result := <-context.TaskChannel() + if result.Err != nil { + t.Errorf("unexpected error running InvAddTask: %s", result.Err) + } + actual, _ := client.GetClusterObjs(nil) + testutil.AssertEqual(t, tc.expectedObjs, actual, + "Actual cluster objects (%d) do not match expected cluster objects (%d)", + len(actual), len(tc.expectedObjs)) + }) + } +} diff --git a/thirdparty/cli-utils/apply/task/printer_adapter.go b/thirdparty/cli-utils/apply/task/printer_adapter.go new file mode 100644 index 0000000000..084e68d620 --- /dev/null +++ b/thirdparty/cli-utils/apply/task/printer_adapter.go @@ -0,0 +1,82 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package task + +import ( + "fmt" + "io" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/printers" + "sigs.k8s.io/cli-utils/pkg/apply/event" + "sigs.k8s.io/cli-utils/pkg/object" +) + +// 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.Event + groupName string +} + +// resourcePrinterImpl implements the ResourcePrinter interface. But +// instead of printing, it emits information on the provided channel. +type resourcePrinterImpl struct { + applyOperation event.ApplyEventOperation + ch chan<- event.Event + groupName string +} + +// PrintObj takes the provided object and operation and emits +// it on the channel. +func (r *resourcePrinterImpl) PrintObj(obj runtime.Object, _ io.Writer) error { + id, err := object.RuntimeToObjMeta(obj) + if err != nil { + return err + } + r.ch <- event.Event{ + Type: event.ApplyType, + ApplyEvent: event.ApplyEvent{ + GroupName: r.groupName, + Identifier: id, + Operation: r.applyOperation, + Resource: obj.(*unstructured.Unstructured), + }, + } + 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) { + applyOperation, err := operationToApplyOperationConst(operation) + return &resourcePrinterImpl{ + ch: p.ch, + applyOperation: applyOperation, + groupName: p.groupName, + }, err + } +} + +func operationToApplyOperationConst(operation string) (event.ApplyEventOperation, error) { + switch operation { + case "serverside-applied": + return event.ServersideApplied, nil + case "created": + return event.Created, nil + case "unchanged": + return event.Unchanged, nil + case "configured": + return event.Configured, nil + default: + return event.ApplyEventOperation(0), fmt.Errorf("unknown operation %s", operation) + } +} diff --git a/thirdparty/cli-utils/apply/task/printer_adapter_test.go b/thirdparty/cli-utils/apply/task/printer_adapter_test.go new file mode 100644 index 0000000000..68d870b6fb --- /dev/null +++ b/thirdparty/cli-utils/apply/task/printer_adapter_test.go @@ -0,0 +1,55 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package task + +import ( + "bytes" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/cli-utils/pkg/apply/event" +) + +func TestKubectlPrinterAdapter(t *testing.T) { + ch := make(chan event.Event) + buffer := bytes.Buffer{} + operation := "serverside-applied" + + adapter := KubectlPrinterAdapter{ + ch: ch, + groupName: "test-0", + } + + toPrinterFunc := adapter.toPrinterFunc() + resourcePrinter, err := toPrinterFunc(operation) + assert.NoError(t, err) + + deployment := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "name", + "namespace": "namespace", + }, + }, + } + + // Need to run this in a separate gorutine since go channels + // are blocking. + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + err = resourcePrinter.PrintObj(deployment, &buffer) + }() + msg := <-ch + wg.Wait() + + assert.NoError(t, err) + assert.Equal(t, event.ServersideApplied, msg.ApplyEvent.Operation) + assert.Equal(t, deployment, msg.ApplyEvent.Resource) +} diff --git a/thirdparty/cli-utils/apply/task/prune_task.go b/thirdparty/cli-utils/apply/task/prune_task.go new file mode 100644 index 0000000000..11c8e13ff9 --- /dev/null +++ b/thirdparty/cli-utils/apply/task/prune_task.go @@ -0,0 +1,85 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package task + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/klog/v2" + "sigs.k8s.io/cli-utils/pkg/apply/event" + "sigs.k8s.io/cli-utils/pkg/apply/filter" + "sigs.k8s.io/cli-utils/pkg/apply/prune" + "sigs.k8s.io/cli-utils/pkg/apply/taskrunner" + "sigs.k8s.io/cli-utils/pkg/common" + "sigs.k8s.io/cli-utils/pkg/object" +) + +// PruneTask prunes objects from the cluster +// by using the PruneOptions. The provided Objects is the +// set of resources that have just been applied. +type PruneTask struct { + TaskName string + + Pruner *prune.Pruner + Objects object.UnstructuredSet + Filters []filter.ValidationFilter + DryRunStrategy common.DryRunStrategy + PropagationPolicy metav1.DeletionPropagation + // True if we are destroying, which deletes the inventory object + // as well (possibly) the inventory namespace. + Destroy bool +} + +func (p *PruneTask) Name() string { + return p.TaskName +} + +func (p *PruneTask) Action() event.ResourceAction { + action := event.PruneAction + if p.Destroy { + action = event.DeleteAction + } + return action +} + +func (p *PruneTask) Identifiers() object.ObjMetadataSet { + return object.UnstructuredSetToObjMetadataSet(p.Objects) +} + +// Start creates a new goroutine that will invoke +// the Run function on the PruneOptions to update +// the cluster. It will push a TaskResult on the taskChannel +// to signal to the taskrunner that the task has completed (or failed). +func (p *PruneTask) Start(taskContext *taskrunner.TaskContext) { + go func() { + klog.V(2).Infof("prune task starting (name: %q, objects: %d)", + p.Name(), len(p.Objects)) + // Create filter to prevent deletion of currently applied + // objects. Must be done here to wait for applied UIDs. + uidFilter := filter.CurrentUIDFilter{ + CurrentUIDs: taskContext.InventoryManager().AppliedResourceUIDs(), + } + p.Filters = append(p.Filters, uidFilter) + err := p.Pruner.Prune( + p.Objects, + p.Filters, + taskContext, + p.Name(), + prune.Options{ + DryRunStrategy: p.DryRunStrategy, + PropagationPolicy: p.PropagationPolicy, + Destroy: p.Destroy, + }, + ) + klog.V(2).Infof("prune task completing (name: %q)", p.Name()) + taskContext.TaskChannel() <- taskrunner.TaskResult{ + Err: err, + } + }() +} + +// Cancel is not supported by the PruneTask. +func (p *PruneTask) Cancel(_ *taskrunner.TaskContext) {} + +// StatusUpdate is not supported by the PruneTask. +func (p *PruneTask) StatusUpdate(_ *taskrunner.TaskContext, _ object.ObjMetadata) {} diff --git a/thirdparty/cli-utils/apply/task/send_event_task.go b/thirdparty/cli-utils/apply/task/send_event_task.go new file mode 100644 index 0000000000..5140c5add9 --- /dev/null +++ b/thirdparty/cli-utils/apply/task/send_event_task.go @@ -0,0 +1,30 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package task + +import ( + "sigs.k8s.io/cli-utils/pkg/apply/event" + "sigs.k8s.io/cli-utils/pkg/apply/taskrunner" +) + +// SendEventTask is an implementation of the Task interface +// that will send the provided event on the eventChannel when +// executed. +type SendEventTask struct { + Event event.Event +} + +// Start start a separate goroutine that will send the +// event and then push a TaskResult on the taskChannel to +// signal to the taskrunner that the task is completed. +func (s *SendEventTask) Start(taskContext *taskrunner.TaskContext) { + go func() { + taskContext.SendEvent(s.Event) + taskContext.TaskChannel() <- taskrunner.TaskResult{} + }() +} + +// ClearTimeout doesn't do anything as SendEventTask doesn't support +// timeouts. +func (s *SendEventTask) ClearTimeout() {} From fa8dcff3d572b99f451ef582f3e5e21214e2315c Mon Sep 17 00:00:00 2001 From: Mengqi Yu Date: Wed, 20 Apr 2022 18:22:22 -0700 Subject: [PATCH 2/2] update dependencies --- go.mod | 41 ++++++------ go.sum | 99 +++++++++++++++++++--------- internal/cmdapply/cmdapply.go | 2 +- internal/cmddestroy/cmddestroy.go | 2 +- pkg/live/inventoryrg.go | 4 +- porch/go.mod | 54 +++++++-------- porch/go.sum | 106 ++++++++++++++++++++---------- 7 files changed, 191 insertions(+), 117 deletions(-) diff --git a/go.mod b/go.mod index f1edb9f1ca..17c7d29deb 100644 --- a/go.mod +++ b/go.mod @@ -11,24 +11,24 @@ require ( github.com/igorsobreira/titlecase v0.0.0-20140109233139-4156b5b858ac github.com/otiai10/copy v1.7.0 github.com/philopon/go-toposort v0.0.0-20170620085441-9be86dbd762f - github.com/spf13/cobra v1.3.0 + github.com/spf13/cobra v1.4.0 github.com/stretchr/testify v1.7.1 github.com/xlab/treeprint v1.1.0 - golang.org/x/mod v0.5.1 + golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b gotest.tools v2.2.0+incompatible - k8s.io/api v0.23.5 - k8s.io/apiextensions-apiserver v0.23.5 - k8s.io/apimachinery v0.23.5 - k8s.io/cli-runtime v0.23.5 - k8s.io/client-go v0.23.5 - k8s.io/component-base v0.23.5 - k8s.io/klog/v2 v2.40.1 - k8s.io/kubectl v0.23.5 + k8s.io/api v0.24.0-rc.0 + k8s.io/apiextensions-apiserver v0.24.0-rc.0 + k8s.io/apimachinery v0.24.0-rc.0 + k8s.io/cli-runtime v0.24.0-rc.0 + k8s.io/client-go v0.24.0-rc.0 + k8s.io/component-base v0.24.0-rc.0 + k8s.io/klog/v2 v2.60.1 + k8s.io/kubectl v0.24.0-rc.0 sigs.k8s.io/cli-utils v0.29.2 sigs.k8s.io/controller-runtime v0.11.0 - sigs.k8s.io/kustomize/api v0.11.1 - sigs.k8s.io/kustomize/kyaml v0.13.3 + sigs.k8s.io/kustomize/api v0.11.4 + sigs.k8s.io/kustomize/kyaml v0.13.7-0.20220418212550-9d5491c2e20c ) require ( @@ -46,8 +46,10 @@ require ( github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful v2.9.5+incompatible // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect + github.com/fatih/camelcase v1.0.0 // indirect github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect github.com/fvbommel/sortorder v1.0.1 // indirect github.com/go-logr/logr v1.2.0 // indirect @@ -57,9 +59,9 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/btree v1.0.1 // indirect + github.com/google/gnostic v0.5.7-v3refs // indirect github.com/google/gofuzz v1.1.0 // indirect github.com/google/uuid v1.3.0 // indirect - github.com/googleapis/gnostic v0.5.5 // indirect github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect @@ -72,10 +74,11 @@ require ( github.com/mitchellh/go-wordwrap v1.0.0 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/moby/spdystream v0.2.0 // indirect - github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect + github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/onsi/gomega v1.17.0 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect @@ -86,20 +89,20 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/spyzhov/ajson v0.4.2 // indirect go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect - golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect + golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect - golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect + golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.27.1 // indirect + google.golang.org/protobuf v1.28.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect - k8s.io/utils v0.0.0-20211208161948-7d6a63dca704 // indirect + k8s.io/kube-openapi v0.0.0-20220401212409-b28bf2818661 // indirect + k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect diff --git a/go.sum b/go.sum index 50f22a6f2e..2d5675ff52 100644 --- a/go.sum +++ b/go.sum @@ -96,6 +96,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= @@ -108,6 +110,7 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= @@ -160,11 +163,13 @@ github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -183,6 +188,7 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= +github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= @@ -278,7 +284,10 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/cel-go v0.9.0/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w= +github.com/google/cel-go v0.10.1/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w= github.com/google/cel-spec v0.6.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA= +github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -326,7 +335,6 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= -github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= @@ -468,8 +476,9 @@ github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGg github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 h1:yH0SvLzcbZxcJXho2yh7CqdENGMQe73Cw3woZBpPli0= github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -480,6 +489,7 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -535,8 +545,9 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -548,15 +559,17 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.28.0 h1:vGVfV9KrDTvWt5boZO0I19g2E3CsWfpPPKZM9dt3mEw= github.com/prometheus/common v0.28.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= @@ -592,8 +605,9 @@ github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0= github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= +github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= +github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -645,6 +659,7 @@ go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3 go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= +go.etcd.io/etcd/client/v3 v3.5.1/go.mod h1:OnjH4M8OnAotwaB2l9bVgZzRFKru7/ZMoS46OtKyd3Q= go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= @@ -693,8 +708,10 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -731,8 +748,9 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -787,7 +805,9 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -907,6 +927,7 @@ golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -928,8 +949,9 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -994,6 +1016,7 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1111,6 +1134,7 @@ google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1155,8 +1179,9 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1204,53 +1229,62 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.23.0/go.mod h1:8wmDdLBHBNxtOIytwLstXt5E9PddnZb0GaMcqsvDBpg= k8s.io/api v0.23.2/go.mod h1:sYuDb3flCtRPI8ghn6qFrcK5ZBu2mhbElxRE95qpwlI= -k8s.io/api v0.23.5 h1:zno3LUiMubxD/V1Zw3ijyKO3wxrhbUF1Ck+VjBvfaoA= k8s.io/api v0.23.5/go.mod h1:Na4XuKng8PXJ2JsploYYrivXrINeTaycCGcYgF91Xm8= +k8s.io/api v0.24.0-rc.0 h1:0TkqsPskmRuYeIdeliMnWwd5liRAejl0otJRuCLla9Q= +k8s.io/api v0.24.0-rc.0/go.mod h1:PcpENPRRauvrfEa9i+SJ1ai6mHIv2gD5GRvwfDsp+WM= k8s.io/apiextensions-apiserver v0.23.0/go.mod h1:xIFAEEDlAZgpVBl/1VSjGDmLoXAWRG40+GsWhKhAxY4= -k8s.io/apiextensions-apiserver v0.23.5 h1:5SKzdXyvIJKu+zbfPc3kCbWpbxi+O+zdmAJBm26UJqI= -k8s.io/apiextensions-apiserver v0.23.5/go.mod h1:ntcPWNXS8ZPKN+zTXuzYMeg731CP0heCTl6gYBxLcuQ= +k8s.io/apiextensions-apiserver v0.24.0-rc.0 h1:em8IAQisSjp4knkyK0wyZAo6OnKcqs/WzS54By2YKS8= +k8s.io/apiextensions-apiserver v0.24.0-rc.0/go.mod h1:nTAG3+EVJ8+ZJiHhNBEfTU//ZvAFasxNwpCEeZedr94= k8s.io/apimachinery v0.23.0/go.mod h1:fFCTTBKvKcwTPFzjlcxp91uPFZr+JA0FubU4fLzzFYc= k8s.io/apimachinery v0.23.2/go.mod h1:zDqeV0AK62LbCI0CI7KbWCAYdLg+E+8UXJ0rIz5gmS8= -k8s.io/apimachinery v0.23.5 h1:Va7dwhp8wgkUPWsEXk6XglXWU4IKYLKNlv8VkX7SDM0= k8s.io/apimachinery v0.23.5/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= +k8s.io/apimachinery v0.24.0-rc.0 h1:2JM6zl8BsGy9sPsK6yeEnbT3Rv1WNEKOG5wqUu+nrpE= +k8s.io/apimachinery v0.24.0-rc.0/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= k8s.io/apiserver v0.23.0/go.mod h1:Cec35u/9zAepDPPFyT+UMrgqOCjgJ5qtfVJDxjZYmt4= -k8s.io/apiserver v0.23.5/go.mod h1:7wvMtGJ42VRxzgVI7jkbKvMbuCbVbgsWFT7RyXiRNTw= +k8s.io/apiserver v0.24.0-rc.0/go.mod h1:9iUrs4qlMOec7B5vlhUm3IzZQpNxxWPcldDAxkJdP3w= k8s.io/cli-runtime v0.23.2/go.mod h1:Ag70akCDvwux4HxY+nH2J3UqE2e6iwSSdG1HE6p1VTU= -k8s.io/cli-runtime v0.23.5 h1:Z7XUpGoJZYZB2uNjQfJjMbyDKyVkoBGye62Ap0sWQHY= -k8s.io/cli-runtime v0.23.5/go.mod h1:oY6QDF2qo9xndSq32tqcmRp2UyXssdGrLfjAVymgbx4= +k8s.io/cli-runtime v0.24.0-rc.0 h1:1hPCaCibl8DaWUcCNU8iyWHOHJWSUiESHW6uYlGeLDo= +k8s.io/cli-runtime v0.24.0-rc.0/go.mod h1:bAFKWrpO04tcE3El1PXPprLhgk8VUbA7de2jeAoMmR8= k8s.io/client-go v0.23.0/go.mod h1:hrDnpnK1mSr65lHHcUuIZIXDgEbzc7/683c6hyG4jTA= k8s.io/client-go v0.23.2/go.mod h1:k3YbsWg6GWdHF1THHTQP88X9RhB1DWPo3Dq7KfU/D1c= -k8s.io/client-go v0.23.5 h1:zUXHmEuqx0RY4+CsnkOn5l0GU+skkRXKGJrhmE2SLd8= k8s.io/client-go v0.23.5/go.mod h1:flkeinTO1CirYgzMPRWxUCnV0G4Fbu2vLhYCObnt/r4= +k8s.io/client-go v0.24.0-rc.0 h1:PZerqDccRfhZZ19PYCfO6kJIJhuNOZpRQ9tJzPSFeE4= +k8s.io/client-go v0.24.0-rc.0/go.mod h1:zH+SNmPKPUz09GNMCW2Bu5reKghTVx6TpYepwG2mCIM= k8s.io/code-generator v0.23.0/go.mod h1:vQvOhDXhuzqiVfM/YHp+dmg10WDZCchJVObc9MvowsE= k8s.io/code-generator v0.23.2/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= k8s.io/code-generator v0.23.5/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= +k8s.io/code-generator v0.24.0-rc.0/go.mod h1:dpVhs00hTuTdTY6jvVxvTFCk6gSMrtfRydbhZwHI15w= k8s.io/component-base v0.23.0/go.mod h1:DHH5uiFvLC1edCpvcTDV++NKULdYYU6pR9Tt3HIKMKI= k8s.io/component-base v0.23.2/go.mod h1:wS9Z03MO3oJ0RU8bB/dbXTiluGju+SC/F5i660gxB8c= -k8s.io/component-base v0.23.5 h1:8qgP5R6jG1BBSXmRYW+dsmitIrpk8F/fPEvgDenMCCE= -k8s.io/component-base v0.23.5/go.mod h1:c5Nq44KZyt1aLl0IpHX82fhsn84Sb0jjzwjpcA42bY0= +k8s.io/component-base v0.24.0-rc.0 h1:S2xaUcNXdZXHxMjSaVv7vtYgtwYnxqZHB0aHGm9dtVU= +k8s.io/component-base v0.24.0-rc.0/go.mod h1:ACHgb2t5mV1LZHXVYQB8krxcTq2/dpYsjamlsiZJGXg= k8s.io/component-helpers v0.23.2/go.mod h1:J6CMwiaf0izLoNwiLl2OymB4+rGTsTpWp6PL/AqOM4U= -k8s.io/component-helpers v0.23.5/go.mod h1:5riXJgjTIs+ZB8xnf5M2anZ8iQuq37a0B/0BgoPQuSM= +k8s.io/component-helpers v0.24.0-rc.0/go.mod h1:ZCHo6Gwap4GKV5hYkcCysLT8TW8PGtq+4qqGpNEylJE= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/klog/v2 v2.40.1 h1:P4RRucWk/lFOlDdkAr3mc7iWFkgKrZY9qZMAgek06S4= k8s.io/klog/v2 v2.40.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.60.1 h1:VW25q3bZx9uE3vvdL6M8ezOX79vA2Aq1nEWLqNQclHc= +k8s.io/klog/v2 v2.60.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= -k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 h1:E3J9oCLlaobFUqsjG9DfKbP2BmgwBL2p7pn0A3dG9W4= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= +k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42/go.mod h1:Z/45zLw8lUo4wdiUkI+v/ImEGAvu3WatcZl3lPMR4Rk= +k8s.io/kube-openapi v0.0.0-20220401212409-b28bf2818661 h1:nqYOUleKLC/0P1zbU29F5q6aoezM6MOAVz+iyfQbZ5M= +k8s.io/kube-openapi v0.0.0-20220401212409-b28bf2818661/go.mod h1:daOouuuwd9JXpv1L7Y34iV3yf6nxzipkKMWWlqlvK9M= k8s.io/kubectl v0.23.2/go.mod h1:zWm5wt8PdRmHiVhE9a7q7XYW4WFX9StkZGnC18+1v3M= -k8s.io/kubectl v0.23.5 h1:DmDULqCaF4qstj0Im143XmncvqWtJxHzK8IrW2BzlU0= -k8s.io/kubectl v0.23.5/go.mod h1:lLgw7cVY8xbd7o637vOXPca/w6HC205KsPCRDYRCxwE= +k8s.io/kubectl v0.24.0-rc.0 h1:SjAtvBp4oZ+oFMvWVzvrs2EQV2Skn48RjVexaqhBpA4= +k8s.io/kubectl v0.24.0-rc.0/go.mod h1:PSKGtqIwErHxbej7DOZ2YeuEejQL4nUXYdO5XldVhjo= k8s.io/metrics v0.23.2/go.mod h1:idJHc+lLK5teHUC6Z2+d6qTKA12d5FLDxmC/DHiUYKc= -k8s.io/metrics v0.23.5/go.mod h1:WNAtV2a5BYbmDS8+7jSqYYV6E3efuGTpIwJ8PTD1wgs= +k8s.io/metrics v0.24.0-rc.0/go.mod h1:XqqGXg39Dvzmfl+vvAZhIoU1WsvvfqGQ0idWXEhwSSo= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20211208161948-7d6a63dca704 h1:ZKMMxTvduyf5WUtREOqg5LiXaN1KO/+0oOQPRFrClpo= k8s.io/utils v0.0.0-20211208161948-7d6a63dca704/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 h1:HNSDgDCrr/6Ly3WEGKZftiE7IY19Vz2GdbOCyI4qqhc= +k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= @@ -1264,13 +1298,16 @@ sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNza sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 h1:kDi4JBNAsJWfz1aEXhO8Jg87JJaPNLh5tIzYHgStQ9Y= sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= sigs.k8s.io/kustomize/api v0.10.1/go.mod h1:2FigT1QN6xKdcnGS2Ppp1uIWrtWN28Ms8A3OZUZhwr8= -sigs.k8s.io/kustomize/api v0.11.1 h1:/Vutu+gAqVo8skw1xCZrsZD39SN4Adg+z7FrSTw9pds= -sigs.k8s.io/kustomize/api v0.11.1/go.mod h1:GZuhith5YcqxIDe0GnRJNx5xxPTjlwaLTt/e+ChUtJA= +sigs.k8s.io/kustomize/api v0.11.4 h1:/0Mr3kfBBNcNPOW5Qwk/3eb8zkswCwnqQxxKtmrTkRo= +sigs.k8s.io/kustomize/api v0.11.4/go.mod h1:k+8RsqYbgpkIrJ4p9jcdPqe8DprLxFUUO0yNOq8C+xI= sigs.k8s.io/kustomize/cmd/config v0.10.2/go.mod h1:K2aW7nXJ0AaT+VA/eO0/dzFLxmpFcTzudmAgDwPY1HQ= +sigs.k8s.io/kustomize/cmd/config v0.10.6/go.mod h1:/S4A4nUANUa4bZJ/Edt7ZQTyKOY9WCER0uBS1SW2Rco= sigs.k8s.io/kustomize/kustomize/v4 v4.4.1/go.mod h1:qOKJMMz2mBP+vcS7vK+mNz4HBLjaQSWRY22EF6Tb7Io= +sigs.k8s.io/kustomize/kustomize/v4 v4.5.4/go.mod h1:Zo/Xc5FKD6sHl0lilbrieeGeZHVYCA4BzxeAaLI05Bg= sigs.k8s.io/kustomize/kyaml v0.13.0/go.mod h1:FTJxEZ86ScK184NpGSAQcfEqee0nul8oLCK30D47m4E= -sigs.k8s.io/kustomize/kyaml v0.13.3 h1:tNNQIC+8cc+aXFTVg+RtQAOsjwUdYBZRAgYOVI3RBc4= -sigs.k8s.io/kustomize/kyaml v0.13.3/go.mod h1:/ya3Gk4diiQzlE4mBh7wykyLRFZNvqlbh+JnwQ9Vhrc= +sigs.k8s.io/kustomize/kyaml v0.13.6/go.mod h1:yHP031rn1QX1lr/Xd934Ri/xdVNG8BE2ECa78Ht/kEg= +sigs.k8s.io/kustomize/kyaml v0.13.7-0.20220418212550-9d5491c2e20c h1:Y0cW/MVbKH9jRlMbpLe/4gs2m6qteP1pUGP+JkWcGdA= +sigs.k8s.io/kustomize/kyaml v0.13.7-0.20220418212550-9d5491c2e20c/go.mod h1:6K+IUOuir3Y7nucPRAjw9yth04KSWBnP5pqUTGwj/qU= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/structured-merge-diff/v4 v4.2.0/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= diff --git a/internal/cmdapply/cmdapply.go b/internal/cmdapply/cmdapply.go index 488a1e2140..9cea0b440a 100644 --- a/internal/cmdapply/cmdapply.go +++ b/internal/cmdapply/cmdapply.go @@ -26,13 +26,13 @@ import ( "github.com/GoogleContainerTools/kpt/internal/util/strings" "github.com/GoogleContainerTools/kpt/pkg/live" "github.com/GoogleContainerTools/kpt/pkg/status" + "github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/apply" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/kubectl/pkg/cmd/util" "sigs.k8s.io/cli-utils/cmd/flagutils" - "sigs.k8s.io/cli-utils/pkg/apply" "sigs.k8s.io/cli-utils/pkg/common" "sigs.k8s.io/cli-utils/pkg/inventory" "sigs.k8s.io/cli-utils/pkg/printers" diff --git a/internal/cmddestroy/cmddestroy.go b/internal/cmddestroy/cmddestroy.go index 0ff610ed43..20773c31fc 100644 --- a/internal/cmddestroy/cmddestroy.go +++ b/internal/cmddestroy/cmddestroy.go @@ -24,11 +24,11 @@ import ( "github.com/GoogleContainerTools/kpt/internal/util/strings" "github.com/GoogleContainerTools/kpt/pkg/live" "github.com/GoogleContainerTools/kpt/pkg/status" + "github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/apply" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/kubectl/pkg/cmd/util" "sigs.k8s.io/cli-utils/cmd/flagutils" - "sigs.k8s.io/cli-utils/pkg/apply" "sigs.k8s.io/cli-utils/pkg/common" "sigs.k8s.io/cli-utils/pkg/inventory" "sigs.k8s.io/cli-utils/pkg/printers" diff --git a/pkg/live/inventoryrg.go b/pkg/live/inventoryrg.go index 60bb2006bd..08f969ee3f 100644 --- a/pkg/live/inventoryrg.go +++ b/pkg/live/inventoryrg.go @@ -376,9 +376,7 @@ func (rgi *ResourceGroupInstaller) InstallRG(ctx context.Context) error { return e.Error case pollevent.ResourceUpdateEvent: if e.Resource.Status == kstatus.CurrentStatus { - if !meta.MaybeResetRESTMapper(mapper) { - return fmt.Errorf("failed to reset RESTMapper: %T", mapper) - } + meta.MaybeResetRESTMapper(mapper) } } } diff --git a/porch/go.mod b/porch/go.mod index d468633570..67677157c0 100644 --- a/porch/go.mod +++ b/porch/go.mod @@ -23,7 +23,7 @@ require ( github.com/google/go-cmp v0.5.7 github.com/google/go-containerregistry v0.8.0 github.com/hexops/gotextdiff v1.0.3 - github.com/spf13/cobra v1.3.0 + github.com/spf13/cobra v1.4.0 github.com/spf13/pflag v1.0.5 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0 go.opentelemetry.io/otel v0.20.0 @@ -32,22 +32,22 @@ require ( go.opentelemetry.io/otel/sdk v0.20.0 go.opentelemetry.io/otel/sdk/metric v0.20.0 go.opentelemetry.io/otel/trace v0.20.0 - golang.org/x/mod v0.5.1 + golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf google.golang.org/grpc v1.44.0 - google.golang.org/protobuf v1.27.1 + google.golang.org/protobuf v1.28.0 gopkg.in/yaml.v2 v2.4.0 - k8s.io/api v0.23.5 - k8s.io/apimachinery v0.23.5 - k8s.io/apiserver v0.23.5 - k8s.io/client-go v0.23.5 - k8s.io/component-base v0.23.5 + k8s.io/api v0.24.0-rc.0 + k8s.io/apimachinery v0.24.0-rc.0 + k8s.io/apiserver v0.24.0-rc.0 + k8s.io/client-go v0.24.0-rc.0 + k8s.io/component-base v0.24.0-rc.0 k8s.io/klog/v2 v2.60.1 - k8s.io/kube-aggregator v0.23.5 - k8s.io/utils v0.0.0-20211208161948-7d6a63dca704 + k8s.io/kube-aggregator v0.24.0-beta.0 + k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 sigs.k8s.io/controller-runtime v0.11.1 - sigs.k8s.io/kustomize/kyaml v0.13.6 + sigs.k8s.io/kustomize/kyaml v0.13.7-0.20220418212550-9d5491c2e20c sigs.k8s.io/yaml v1.3.0 ) @@ -72,7 +72,7 @@ require ( github.com/andybalholm/cascadia v1.1.0 // indirect github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/blang/semver v3.5.1+incompatible // indirect + github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect github.com/containerd/stargz-snapshotter/estargz v0.10.1 // indirect @@ -81,7 +81,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/cli v20.10.12+incompatible // indirect - github.com/docker/distribution v2.7.1+incompatible // indirect + github.com/docker/distribution v2.8.1+incompatible // indirect github.com/docker/docker v20.10.12+incompatible // indirect github.com/docker/docker-credential-helpers v0.6.4 // indirect github.com/dustmop/soup v1.1.2-0.20190516214245-38228baa104e // indirect @@ -102,11 +102,11 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/btree v1.0.1 // indirect + github.com/google/gnostic v0.5.7-v3refs // indirect github.com/google/gofuzz v1.1.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.3.0 // indirect github.com/googleapis/gax-go/v2 v2.1.1 // indirect - github.com/googleapis/gnostic v0.5.5 // indirect github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect @@ -124,7 +124,7 @@ require ( github.com/mitchellh/go-wordwrap v1.0.0 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/moby/spdystream v0.2.0 // indirect - github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect + github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect @@ -137,10 +137,10 @@ require ( github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.11.0 // indirect + github.com/prometheus/client_golang v1.12.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.28.0 // indirect - github.com/prometheus/procfs v0.6.0 // indirect + github.com/prometheus/common v0.32.1 // indirect + github.com/prometheus/procfs v0.7.3 // indirect github.com/qri-io/starlib v0.5.0 // indirect github.com/russross/blackfriday v1.5.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -152,7 +152,7 @@ require ( github.com/xlab/treeprint v1.1.0 // indirect go.etcd.io/etcd/api/v3 v3.5.1 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.1 // indirect - go.etcd.io/etcd/client/v3 v3.5.0 // indirect + go.etcd.io/etcd/client/v3 v3.5.1 // indirect go.opencensus.io v0.23.0 // indirect go.opentelemetry.io/contrib v0.20.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 // indirect @@ -163,13 +163,13 @@ require ( go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.19.1 // indirect - golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect + golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect - golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect + golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/api v0.70.0 // indirect google.golang.org/appengine v1.6.7 // indirect @@ -177,13 +177,13 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect - k8s.io/apiextensions-apiserver v0.23.5 // indirect - k8s.io/cli-runtime v0.23.5 // indirect - k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect - k8s.io/kubectl v0.23.5 // indirect + k8s.io/apiextensions-apiserver v0.24.0-rc.0 // indirect + k8s.io/cli-runtime v0.24.0-rc.0 // indirect + k8s.io/kube-openapi v0.0.0-20220401212409-b28bf2818661 // indirect + k8s.io/kubectl v0.24.0-rc.0 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30 // indirect - sigs.k8s.io/cli-utils v0.29.2 // indirect + sigs.k8s.io/cli-utils v0.29.4 // indirect sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect - sigs.k8s.io/kustomize/api v0.11.1 // indirect + sigs.k8s.io/kustomize/api v0.11.4 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect ) diff --git a/porch/go.sum b/porch/go.sum index 51562a2a7f..97fdd53201 100644 --- a/porch/go.sum +++ b/porch/go.sum @@ -174,6 +174,8 @@ github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqO github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bluekeyes/go-gitdiff v0.6.1 h1:VPpQZanEVwFid0IoAIyiJLD+SMWM/AXZXYFc+3E9CS4= github.com/bluekeyes/go-gitdiff v0.6.1/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= @@ -352,8 +354,9 @@ github.com/docker/cli v20.10.12+incompatible h1:lZlz0uzG+GH+c0plStMUdF/qk3ppmgns github.com/docker/cli v20.10.12+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= +github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v20.10.12+incompatible h1:CEeNmFM0QZIsJCZKMkZx0ZcahTiewkrgiwfYD+dfl1U= github.com/docker/docker v20.10.12+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o= @@ -521,6 +524,7 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/cel-go v0.9.0/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w= +github.com/google/cel-go v0.10.1/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w= github.com/google/cel-spec v0.6.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA= github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= @@ -577,7 +581,6 @@ github.com/googleapis/gax-go/v2 v2.1.1 h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= -github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= @@ -765,8 +768,9 @@ github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2J github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= -github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 h1:yH0SvLzcbZxcJXho2yh7CqdENGMQe73Cw3woZBpPli0= github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -879,8 +883,9 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -895,8 +900,9 @@ github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.28.0 h1:vGVfV9KrDTvWt5boZO0I19g2E3CsWfpPPKZM9dt3mEw= github.com/prometheus/common v0.28.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= @@ -907,8 +913,9 @@ github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/qri-io/starlib v0.5.0 h1:NlveoBAhO6mNgM7+JpM9QlHh3/3pOtOiH6iXaqSdVK0= github.com/qri-io/starlib v0.5.0/go.mod h1:FpVumyB2CMrKIrjf39fAi4uydYWVvnWEvXEOwfzZRHY= @@ -958,8 +965,9 @@ github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3 github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0= github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= +github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= +github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -1053,8 +1061,9 @@ go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3 go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.etcd.io/etcd/client/v2 v2.305.1 h1:vtxYCKWA9x31w0WJj7DdqsHFNjhkigdAnziDtkZb/l4= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= -go.etcd.io/etcd/client/v3 v3.5.0 h1:62Eh0XOro+rDwkrypAGDfgmNh5Joq+z+W9HZdlXMzek= go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= +go.etcd.io/etcd/client/v3 v3.5.1 h1:oImGuV5LGKjCqXdjkMHCyWa5OO1gYKCnC/1sgdfj1Uk= +go.etcd.io/etcd/client/v3 v3.5.1/go.mod h1:OnjH4M8OnAotwaB2l9bVgZzRFKru7/ZMoS46OtKyd3Q= go.etcd.io/etcd/pkg/v3 v3.5.0 h1:ntrg6vvKRW26JRmHTE0iNlDgYK6JX3hg/4cD62X0ixk= go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= go.etcd.io/etcd/raft/v3 v3.5.0 h1:kw2TmO3yFTgE+F0mdKkG7xMxkit2duBDa2Hu6D/HMlw= @@ -1133,8 +1142,10 @@ golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1171,8 +1182,9 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1235,6 +1247,7 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -1384,6 +1397,7 @@ golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1407,8 +1421,9 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1474,6 +1489,7 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1598,6 +1614,7 @@ google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= @@ -1650,8 +1667,9 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1712,53 +1730,64 @@ k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= k8s.io/api v0.23.0/go.mod h1:8wmDdLBHBNxtOIytwLstXt5E9PddnZb0GaMcqsvDBpg= k8s.io/api v0.23.2/go.mod h1:sYuDb3flCtRPI8ghn6qFrcK5ZBu2mhbElxRE95qpwlI= k8s.io/api v0.23.4/go.mod h1:i77F4JfyNNrhOjZF7OwwNJS5Y1S9dpwvb9iYRYRczfI= -k8s.io/api v0.23.5 h1:zno3LUiMubxD/V1Zw3ijyKO3wxrhbUF1Ck+VjBvfaoA= k8s.io/api v0.23.5/go.mod h1:Na4XuKng8PXJ2JsploYYrivXrINeTaycCGcYgF91Xm8= +k8s.io/api v0.24.0-beta.0/go.mod h1:D7w5dDA57yCeRJnl0vPuRj6KBAwWYxea4Dwo5kgJGIY= +k8s.io/api v0.24.0-rc.0 h1:0TkqsPskmRuYeIdeliMnWwd5liRAejl0otJRuCLla9Q= +k8s.io/api v0.24.0-rc.0/go.mod h1:PcpENPRRauvrfEa9i+SJ1ai6mHIv2gD5GRvwfDsp+WM= k8s.io/apiextensions-apiserver v0.23.0/go.mod h1:xIFAEEDlAZgpVBl/1VSjGDmLoXAWRG40+GsWhKhAxY4= -k8s.io/apiextensions-apiserver v0.23.5 h1:5SKzdXyvIJKu+zbfPc3kCbWpbxi+O+zdmAJBm26UJqI= -k8s.io/apiextensions-apiserver v0.23.5/go.mod h1:ntcPWNXS8ZPKN+zTXuzYMeg731CP0heCTl6gYBxLcuQ= +k8s.io/apiextensions-apiserver v0.24.0-rc.0 h1:em8IAQisSjp4knkyK0wyZAo6OnKcqs/WzS54By2YKS8= +k8s.io/apiextensions-apiserver v0.24.0-rc.0/go.mod h1:nTAG3+EVJ8+ZJiHhNBEfTU//ZvAFasxNwpCEeZedr94= k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= k8s.io/apimachinery v0.23.0/go.mod h1:fFCTTBKvKcwTPFzjlcxp91uPFZr+JA0FubU4fLzzFYc= k8s.io/apimachinery v0.23.2/go.mod h1:zDqeV0AK62LbCI0CI7KbWCAYdLg+E+8UXJ0rIz5gmS8= k8s.io/apimachinery v0.23.4/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= -k8s.io/apimachinery v0.23.5 h1:Va7dwhp8wgkUPWsEXk6XglXWU4IKYLKNlv8VkX7SDM0= k8s.io/apimachinery v0.23.5/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= +k8s.io/apimachinery v0.24.0-beta.0/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= +k8s.io/apimachinery v0.24.0-rc.0 h1:2JM6zl8BsGy9sPsK6yeEnbT3Rv1WNEKOG5wqUu+nrpE= +k8s.io/apimachinery v0.24.0-rc.0/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= k8s.io/apiserver v0.23.0/go.mod h1:Cec35u/9zAepDPPFyT+UMrgqOCjgJ5qtfVJDxjZYmt4= -k8s.io/apiserver v0.23.5 h1:2Ly8oUjz5cnZRn1YwYr+aFgDZzUmEVL9RscXbnIeDSE= -k8s.io/apiserver v0.23.5/go.mod h1:7wvMtGJ42VRxzgVI7jkbKvMbuCbVbgsWFT7RyXiRNTw= +k8s.io/apiserver v0.24.0-beta.0/go.mod h1:pPF5/dEQukQjwauSxjW8IHKn8W1r8u2nERGojwFFlj4= +k8s.io/apiserver v0.24.0-rc.0 h1:WbEHFdU4BtEMlBPxylg9uZ8e8jYvYQ57yfIW6pdnWY4= +k8s.io/apiserver v0.24.0-rc.0/go.mod h1:9iUrs4qlMOec7B5vlhUm3IzZQpNxxWPcldDAxkJdP3w= k8s.io/cli-runtime v0.23.2/go.mod h1:Ag70akCDvwux4HxY+nH2J3UqE2e6iwSSdG1HE6p1VTU= -k8s.io/cli-runtime v0.23.5 h1:Z7XUpGoJZYZB2uNjQfJjMbyDKyVkoBGye62Ap0sWQHY= -k8s.io/cli-runtime v0.23.5/go.mod h1:oY6QDF2qo9xndSq32tqcmRp2UyXssdGrLfjAVymgbx4= +k8s.io/cli-runtime v0.24.0-rc.0 h1:1hPCaCibl8DaWUcCNU8iyWHOHJWSUiESHW6uYlGeLDo= +k8s.io/cli-runtime v0.24.0-rc.0/go.mod h1:bAFKWrpO04tcE3El1PXPprLhgk8VUbA7de2jeAoMmR8= k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= k8s.io/client-go v0.23.0/go.mod h1:hrDnpnK1mSr65lHHcUuIZIXDgEbzc7/683c6hyG4jTA= k8s.io/client-go v0.23.2/go.mod h1:k3YbsWg6GWdHF1THHTQP88X9RhB1DWPo3Dq7KfU/D1c= -k8s.io/client-go v0.23.5 h1:zUXHmEuqx0RY4+CsnkOn5l0GU+skkRXKGJrhmE2SLd8= k8s.io/client-go v0.23.5/go.mod h1:flkeinTO1CirYgzMPRWxUCnV0G4Fbu2vLhYCObnt/r4= +k8s.io/client-go v0.24.0-beta.0/go.mod h1:D4rgRqnNPdFCFMMrcCqCOAouzIwJkPuKXr3zWThEExM= +k8s.io/client-go v0.24.0-rc.0 h1:PZerqDccRfhZZ19PYCfO6kJIJhuNOZpRQ9tJzPSFeE4= +k8s.io/client-go v0.24.0-rc.0/go.mod h1:zH+SNmPKPUz09GNMCW2Bu5reKghTVx6TpYepwG2mCIM= k8s.io/code-generator v0.23.0/go.mod h1:vQvOhDXhuzqiVfM/YHp+dmg10WDZCchJVObc9MvowsE= k8s.io/code-generator v0.23.2/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= k8s.io/code-generator v0.23.5/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= +k8s.io/code-generator v0.24.0-beta.0/go.mod h1:dpVhs00hTuTdTY6jvVxvTFCk6gSMrtfRydbhZwHI15w= +k8s.io/code-generator v0.24.0-rc.0/go.mod h1:dpVhs00hTuTdTY6jvVxvTFCk6gSMrtfRydbhZwHI15w= k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= k8s.io/component-base v0.23.0/go.mod h1:DHH5uiFvLC1edCpvcTDV++NKULdYYU6pR9Tt3HIKMKI= k8s.io/component-base v0.23.2/go.mod h1:wS9Z03MO3oJ0RU8bB/dbXTiluGju+SC/F5i660gxB8c= -k8s.io/component-base v0.23.5 h1:8qgP5R6jG1BBSXmRYW+dsmitIrpk8F/fPEvgDenMCCE= -k8s.io/component-base v0.23.5/go.mod h1:c5Nq44KZyt1aLl0IpHX82fhsn84Sb0jjzwjpcA42bY0= +k8s.io/component-base v0.24.0-beta.0/go.mod h1:UrgHIn7am00FE1rRmr0GHCiNZg7oZO+Oh6iXROJt2oU= +k8s.io/component-base v0.24.0-rc.0 h1:S2xaUcNXdZXHxMjSaVv7vtYgtwYnxqZHB0aHGm9dtVU= +k8s.io/component-base v0.24.0-rc.0/go.mod h1:ACHgb2t5mV1LZHXVYQB8krxcTq2/dpYsjamlsiZJGXg= k8s.io/component-helpers v0.23.2/go.mod h1:J6CMwiaf0izLoNwiLl2OymB4+rGTsTpWp6PL/AqOM4U= -k8s.io/component-helpers v0.23.5/go.mod h1:5riXJgjTIs+ZB8xnf5M2anZ8iQuq37a0B/0BgoPQuSM= +k8s.io/component-helpers v0.24.0-rc.0/go.mod h1:ZCHo6Gwap4GKV5hYkcCysLT8TW8PGtq+4qqGpNEylJE= k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= @@ -1766,24 +1795,27 @@ k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.40.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.60.1 h1:VW25q3bZx9uE3vvdL6M8ezOX79vA2Aq1nEWLqNQclHc= k8s.io/klog/v2 v2.60.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-aggregator v0.23.5 h1:UZ+qE3hGo6DcgKySf27Jg7d3X9/6JQkVLUiHZAoAfCY= -k8s.io/kube-aggregator v0.23.5/go.mod h1:3ynYx07Co6dzjpKPgipM+1/Mt2Jcm7dY++cRlKLr5s8= +k8s.io/kube-aggregator v0.24.0-beta.0 h1:pRl/qwCw2XBuWrMzkTiWOFfONZtClgHWdbKmIZdDY8o= +k8s.io/kube-aggregator v0.24.0-beta.0/go.mod h1:i9SRRGwxhXwuNuArHlkfy/EuWSrrbUEsIah/7OROTZU= k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= -k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 h1:E3J9oCLlaobFUqsjG9DfKbP2BmgwBL2p7pn0A3dG9W4= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= +k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42/go.mod h1:Z/45zLw8lUo4wdiUkI+v/ImEGAvu3WatcZl3lPMR4Rk= +k8s.io/kube-openapi v0.0.0-20220401212409-b28bf2818661 h1:nqYOUleKLC/0P1zbU29F5q6aoezM6MOAVz+iyfQbZ5M= +k8s.io/kube-openapi v0.0.0-20220401212409-b28bf2818661/go.mod h1:daOouuuwd9JXpv1L7Y34iV3yf6nxzipkKMWWlqlvK9M= k8s.io/kubectl v0.23.2/go.mod h1:zWm5wt8PdRmHiVhE9a7q7XYW4WFX9StkZGnC18+1v3M= -k8s.io/kubectl v0.23.5 h1:DmDULqCaF4qstj0Im143XmncvqWtJxHzK8IrW2BzlU0= -k8s.io/kubectl v0.23.5/go.mod h1:lLgw7cVY8xbd7o637vOXPca/w6HC205KsPCRDYRCxwE= +k8s.io/kubectl v0.24.0-rc.0 h1:SjAtvBp4oZ+oFMvWVzvrs2EQV2Skn48RjVexaqhBpA4= +k8s.io/kubectl v0.24.0-rc.0/go.mod h1:PSKGtqIwErHxbej7DOZ2YeuEejQL4nUXYdO5XldVhjo= k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= k8s.io/metrics v0.23.2/go.mod h1:idJHc+lLK5teHUC6Z2+d6qTKA12d5FLDxmC/DHiUYKc= -k8s.io/metrics v0.23.5/go.mod h1:WNAtV2a5BYbmDS8+7jSqYYV6E3efuGTpIwJ8PTD1wgs= +k8s.io/metrics v0.24.0-rc.0/go.mod h1:XqqGXg39Dvzmfl+vvAZhIoU1WsvvfqGQ0idWXEhwSSo= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20211208161948-7d6a63dca704 h1:ZKMMxTvduyf5WUtREOqg5LiXaN1KO/+0oOQPRFrClpo= k8s.io/utils v0.0.0-20211208161948-7d6a63dca704/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 h1:HNSDgDCrr/6Ly3WEGKZftiE7IY19Vz2GdbOCyI4qqhc= +k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= @@ -1792,8 +1824,9 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyz sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.25/go.mod h1:Mlj9PNLmG9bZ6BHFwFKDo5afkpWyUISkb9Me0GnK66I= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30 h1:dUk62HQ3ZFhD48Qr8MIXCiKA8wInBQCtuE4QGfFW7yA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30/go.mod h1:fEO7lRTdivWO2qYVCVG7dEADOMo/MLDCVr8So2g88Uw= -sigs.k8s.io/cli-utils v0.29.2 h1:SaYo2C1xd0MVv65NQXZ6tIqT1W1iWy8CGmC+VnxQGWs= sigs.k8s.io/cli-utils v0.29.2/go.mod h1:WDVRa5/eQBKntG++uyKdyT+xU7MLdCR4XsgseqL5uX4= +sigs.k8s.io/cli-utils v0.29.4 h1:1bsZ5bE90Mbl6Jat4nmZULJCOkLQ9rwJVVrREgqtXT4= +sigs.k8s.io/cli-utils v0.29.4/go.mod h1:WDVRa5/eQBKntG++uyKdyT+xU7MLdCR4XsgseqL5uX4= sigs.k8s.io/controller-runtime v0.11.0/go.mod h1:KKwLiTooNGu+JmLZGn9Sl3Gjmfj66eMbCQznLP5zcqA= sigs.k8s.io/controller-runtime v0.11.1 h1:7YIHT2QnHJArj/dk9aUkYhfqfK5cIxPOX5gPECfdZLU= sigs.k8s.io/controller-runtime v0.11.1/go.mod h1:KKwLiTooNGu+JmLZGn9Sl3Gjmfj66eMbCQznLP5zcqA= @@ -1801,15 +1834,18 @@ sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNza sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 h1:kDi4JBNAsJWfz1aEXhO8Jg87JJaPNLh5tIzYHgStQ9Y= sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= sigs.k8s.io/kustomize/api v0.10.1/go.mod h1:2FigT1QN6xKdcnGS2Ppp1uIWrtWN28Ms8A3OZUZhwr8= -sigs.k8s.io/kustomize/api v0.11.1 h1:/Vutu+gAqVo8skw1xCZrsZD39SN4Adg+z7FrSTw9pds= -sigs.k8s.io/kustomize/api v0.11.1/go.mod h1:GZuhith5YcqxIDe0GnRJNx5xxPTjlwaLTt/e+ChUtJA= +sigs.k8s.io/kustomize/api v0.11.4 h1:/0Mr3kfBBNcNPOW5Qwk/3eb8zkswCwnqQxxKtmrTkRo= +sigs.k8s.io/kustomize/api v0.11.4/go.mod h1:k+8RsqYbgpkIrJ4p9jcdPqe8DprLxFUUO0yNOq8C+xI= sigs.k8s.io/kustomize/cmd/config v0.10.2/go.mod h1:K2aW7nXJ0AaT+VA/eO0/dzFLxmpFcTzudmAgDwPY1HQ= +sigs.k8s.io/kustomize/cmd/config v0.10.6/go.mod h1:/S4A4nUANUa4bZJ/Edt7ZQTyKOY9WCER0uBS1SW2Rco= sigs.k8s.io/kustomize/kustomize/v4 v4.4.1/go.mod h1:qOKJMMz2mBP+vcS7vK+mNz4HBLjaQSWRY22EF6Tb7Io= +sigs.k8s.io/kustomize/kustomize/v4 v4.5.4/go.mod h1:Zo/Xc5FKD6sHl0lilbrieeGeZHVYCA4BzxeAaLI05Bg= sigs.k8s.io/kustomize/kyaml v0.10.21/go.mod h1:TYWhGwW9vjoRh3rWqBwB/ZOXyEGRVWe7Ggc3+KZIO+c= sigs.k8s.io/kustomize/kyaml v0.13.0/go.mod h1:FTJxEZ86ScK184NpGSAQcfEqee0nul8oLCK30D47m4E= sigs.k8s.io/kustomize/kyaml v0.13.3/go.mod h1:/ya3Gk4diiQzlE4mBh7wykyLRFZNvqlbh+JnwQ9Vhrc= -sigs.k8s.io/kustomize/kyaml v0.13.6 h1:eF+wsn4J7GOAXlvajv6OknSunxpcOBQQqsnPxObtkGs= sigs.k8s.io/kustomize/kyaml v0.13.6/go.mod h1:yHP031rn1QX1lr/Xd934Ri/xdVNG8BE2ECa78Ht/kEg= +sigs.k8s.io/kustomize/kyaml v0.13.7-0.20220418212550-9d5491c2e20c h1:Y0cW/MVbKH9jRlMbpLe/4gs2m6qteP1pUGP+JkWcGdA= +sigs.k8s.io/kustomize/kyaml v0.13.7-0.20220418212550-9d5491c2e20c/go.mod h1:6K+IUOuir3Y7nucPRAjw9yth04KSWBnP5pqUTGwj/qU= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4=