diff --git a/pkg/apply/applier.go b/pkg/apply/applier.go index 6a437c5a..2d145f2f 100644 --- a/pkg/apply/applier.go +++ b/pkg/apply/applier.go @@ -124,6 +124,11 @@ func (a *Applier) Run(ctx context.Context, invInfo inventory.InventoryInfo, obje go func() { defer close(eventChannel) + client, err := a.factory.DynamicClient() + if err != nil { + handleError(eventChannel, err) + return + } mapper, err := a.factory.ToRESTMapper() if err != nil { handleError(eventChannel, err) @@ -165,6 +170,15 @@ func (a *Applier) Run(ctx context.Context, invInfo inventory.InventoryInfo, obje PruneTimeout: options.PruneTimeout, InventoryPolicy: options.InventoryPolicy, } + // Build list of apply validation filters. + applyFilters := []filter.ValidationFilter{ + filter.InventoryPolicyApplyFilter{ + Client: client, + Mapper: mapper, + Inv: invInfo, + InvPolicy: options.InventoryPolicy, + }, + } // Build list of prune validation filters. pruneFilters := []filter.ValidationFilter{ filter.PreventRemoveFilter{}, @@ -179,7 +193,7 @@ func (a *Applier) Run(ctx context.Context, invInfo inventory.InventoryInfo, obje // Build the task queue by appending tasks in the proper order. taskQueue, err := taskBuilder. AppendInvAddTask(invInfo, applyObjs, options.DryRunStrategy). - AppendApplyWaitTasks(invInfo, applyObjs, opts). + AppendApplyWaitTasks(applyObjs, applyFilters, opts). AppendPruneWaitTasks(pruneObjs, pruneFilters, opts). AppendInvSetTask(invInfo, options.DryRunStrategy). Build() diff --git a/pkg/apply/filter/inventory-policy-apply-filter.go b/pkg/apply/filter/inventory-policy-apply-filter.go new file mode 100644 index 00000000..aa7802ea --- /dev/null +++ b/pkg/apply/filter/inventory-policy-apply-filter.go @@ -0,0 +1,73 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package filter + +import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/dynamic" + "sigs.k8s.io/cli-utils/pkg/inventory" + "sigs.k8s.io/cli-utils/pkg/object" +) + +// InventoryPolicyApplyFilter implements ValidationFilter interface to determine +// if an object should be applied based on the cluster object's inventory id, +// the id for the inventory object, and the inventory policy. +type InventoryPolicyApplyFilter struct { + Client dynamic.Interface + Mapper meta.RESTMapper + Inv inventory.InventoryInfo + InvPolicy inventory.InventoryPolicy +} + +// Name returns a filter identifier for logging. +func (ipaf InventoryPolicyApplyFilter) Name() string { + return "InventoryPolicyApplyFilter" +} + +// Filter returns true if the passed object should be filtered (NOT applied) and +// a filter reason string; false otherwise. Returns an error if one occurred +// during the filter calculation +func (ipaf InventoryPolicyApplyFilter) Filter(obj *unstructured.Unstructured) (bool, string, error) { + if obj == nil { + return true, "missing object", nil + } + // Object must be retrieved from the cluster to get the inventory id. + clusterObj, err := ipaf.getObject(object.UnstructuredToObjMetaOrDie(obj)) + if err != nil { + if apierrors.IsNotFound(err) { + // This simply means the object hasn't been created yet. + return false, "", nil + } + return true, "", err + } + // Check the inventory id "match" and the adopt policy to determine + // if an object should be applied. + canApply, err := inventory.CanApply(ipaf.Inv, clusterObj, ipaf.InvPolicy) + if !canApply { + invMatch := inventory.InventoryIDMatch(ipaf.Inv, clusterObj) + reason := fmt.Sprintf("object removal prevented; inventory match %v : inventory policy: %v", + invMatch, ipaf.InvPolicy) + return true, reason, err + } + return false, "", nil +} + +// getObject retrieves the passed object from the cluster, or an error if one occurred. +func (ipaf InventoryPolicyApplyFilter) getObject(id object.ObjMetadata) (*unstructured.Unstructured, error) { + mapping, err := ipaf.Mapper.RESTMapping(id.GroupKind) + if err != nil { + return nil, err + } + namespacedClient, err := ipaf.Client.Resource(mapping.Resource).Namespace(id.Namespace), nil + if err != nil { + return nil, err + } + return namespacedClient.Get(context.TODO(), id.Name, metav1.GetOptions{}) +} diff --git a/pkg/apply/filter/inventory-policy-apply-filter_test.go b/pkg/apply/filter/inventory-policy-apply-filter_test.go new file mode 100644 index 00000000..e8935c1c --- /dev/null +++ b/pkg/apply/filter/inventory-policy-apply-filter_test.go @@ -0,0 +1,121 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package filter + +import ( + "testing" + + "k8s.io/apimachinery/pkg/api/meta/testrestmapper" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + dynamicfake "k8s.io/client-go/dynamic/fake" + "k8s.io/kubectl/pkg/scheme" + "sigs.k8s.io/cli-utils/pkg/common" + "sigs.k8s.io/cli-utils/pkg/inventory" +) + +var invObjTemplate = &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "inventory-name", + "namespace": "inventory-namespace", + }, + }, +} + +func TestInventoryPolicyApplyFilter(t *testing.T) { + tests := map[string]struct { + inventoryID string + objInventoryID string + policy inventory.InventoryPolicy + filtered bool + isError bool + }{ + "inventory and object ids match, not filtered": { + inventoryID: "foo", + objInventoryID: "foo", + policy: inventory.InventoryPolicyMustMatch, + filtered: false, + isError: false, + }, + "inventory and object ids match and adopt, not filtered": { + inventoryID: "foo", + objInventoryID: "foo", + policy: inventory.AdoptIfNoInventory, + filtered: false, + isError: false, + }, + "inventory and object ids do no match and policy must match, filtered and error": { + inventoryID: "foo", + objInventoryID: "bar", + policy: inventory.InventoryPolicyMustMatch, + filtered: true, + isError: true, + }, + "inventory and object ids do no match and adopt if no inventory, filtered and error": { + inventoryID: "foo", + objInventoryID: "bar", + policy: inventory.AdoptIfNoInventory, + filtered: true, + isError: true, + }, + "inventory and object ids do no match and adopt all, not filtered": { + inventoryID: "foo", + objInventoryID: "bar", + policy: inventory.AdoptAll, + filtered: false, + isError: false, + }, + "object id empty and adopt all, not filtered": { + inventoryID: "foo", + objInventoryID: "", + policy: inventory.AdoptAll, + filtered: false, + isError: false, + }, + "object id empty and policy must match, filtered and error": { + inventoryID: "foo", + objInventoryID: "", + policy: inventory.InventoryPolicyMustMatch, + filtered: true, + isError: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + obj := defaultObj.DeepCopy() + objIDAnnotation := map[string]string{ + "config.k8s.io/owning-inventory": tc.objInventoryID, + } + obj.SetAnnotations(objIDAnnotation) + invIDLabel := map[string]string{ + common.InventoryLabel: tc.inventoryID, + } + invObj := invObjTemplate.DeepCopy() + invObj.SetLabels(invIDLabel) + filter := InventoryPolicyApplyFilter{ + Client: dynamicfake.NewSimpleDynamicClient(scheme.Scheme, obj), + Mapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme, + scheme.Scheme.PrioritizedVersionsAllGroups()...), + Inv: inventory.WrapInventoryInfoObj(invObj), + InvPolicy: tc.policy, + } + actual, reason, err := filter.Filter(obj) + if tc.isError != (err != nil) { + t.Fatalf("Expected InventoryPolicyFilter error (%v), got (%v)", tc.isError, (err != nil)) + } + if tc.filtered != actual { + t.Errorf("InventoryPolicyFilter expected filter (%t), got (%t)", tc.filtered, actual) + } + if tc.filtered && len(reason) == 0 { + t.Errorf("InventoryPolicyFilter filtered; expected but missing Reason") + } + if !tc.filtered && len(reason) > 0 { + t.Errorf("InventoryPolicyFilter not filtered; received unexpected Reason: %s", reason) + } + }) + } +} diff --git a/pkg/apply/solver/solver.go b/pkg/apply/solver/solver.go index c3974dad..8728dc0b 100644 --- a/pkg/apply/solver/solver.go +++ b/pkg/apply/solver/solver.go @@ -156,19 +156,18 @@ func (t *TaskQueueBuilder) AppendDeleteInvTask(inv inventory.InventoryInfo, dryR // AppendInvAddTask 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) AppendApplyTask(inv inventory.InventoryInfo, - applyObjs []*unstructured.Unstructured, o Options) *TaskQueueBuilder { +func (t *TaskQueueBuilder) AppendApplyTask(applyObjs []*unstructured.Unstructured, + applyFilters []filter.ValidationFilter, o Options) *TaskQueueBuilder { klog.V(2).Infof("adding apply task (%d objects)", len(applyObjs)) t.tasks = append(t.tasks, &task.ApplyTask{ TaskName: fmt.Sprintf("apply-%d", t.applyCounter), Objects: applyObjs, + Filters: applyFilters, ServerSideOptions: o.ServerSideOptions, DryRunStrategy: o.DryRunStrategy, InfoHelper: t.InfoHelper, Factory: t.Factory, Mapper: t.Mapper, - InventoryPolicy: o.InventoryPolicy, - InvInfo: inv, }) t.applyCounter += 1 return t @@ -213,8 +212,8 @@ func (t *TaskQueueBuilder) AppendPruneTask(pruneObjs []*unstructured.Unstructure // AppendApplyWaitTasks adds apply and wait tasks to the task queue, // depending on build variables (like dry-run) and resource types // (like CRD's). Returns a pointer to the Builder to chain function calls. -func (t *TaskQueueBuilder) AppendApplyWaitTasks(inv inventory.InventoryInfo, - applyObjs []*unstructured.Unstructured, o Options) *TaskQueueBuilder { +func (t *TaskQueueBuilder) AppendApplyWaitTasks(applyObjs []*unstructured.Unstructured, + applyFilters []filter.ValidationFilter, o Options) *TaskQueueBuilder { // Use the "depends-on" annotation to create a graph, ands sort the // objects to apply into sets using a topological sort. applySets, err := graph.SortObjs(applyObjs) @@ -224,7 +223,7 @@ func (t *TaskQueueBuilder) AppendApplyWaitTasks(inv inventory.InventoryInfo, addWaitTask, waitTimeout := waitTaskTimeout(o.DryRunStrategy.ClientOrServerDryRun(), len(applySets), o.ReconcileTimeout) for _, applySet := range applySets { - t.AppendApplyTask(inv, applySet, o) + t.AppendApplyTask(applySet, applyFilters, o) if addWaitTask { applyIds := object.UnstructuredsToObjMetasOrDie(applySet) t.AppendWaitTask(applyIds, taskrunner.AllCurrent, waitTimeout) diff --git a/pkg/apply/solver/solver_test.go b/pkg/apply/solver/solver_test.go index 7a288f30..32ffa929 100644 --- a/pkg/apply/solver/solver_test.go +++ b/pkg/apply/solver/solver_test.go @@ -101,21 +101,6 @@ metadata: } ) -var inventoryObj = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": "test-inventory-name", - "namespace": "test-inventory-namespace", - "labels": map[string]interface{}{ - common.InventoryLabel: "test-inventory-label", - }, - }, - }, -} -var localInv = inventory.WrapInventoryInfoObj(inventoryObj) - func TestTaskQueueBuilder_AppendApplyWaitTasks(t *testing.T) { testCases := map[string]struct { applyObjs []*unstructured.Unstructured @@ -371,7 +356,7 @@ func TestTaskQueueBuilder_AppendApplyWaitTasks(t *testing.T) { Mapper: testutil.NewFakeRESTMapper(), InvClient: fakeInvClient, } - tq, err := tqb.AppendApplyWaitTasks(localInv, tc.applyObjs, tc.options).Build() + tq, err := tqb.AppendApplyWaitTasks(tc.applyObjs, []filter.ValidationFilter{}, tc.options).Build() if tc.isError { assert.NotNil(t, err, "expected error, but received none") return diff --git a/pkg/apply/task/apply_task.go b/pkg/apply/task/apply_task.go index 53945fe9..597f5114 100644 --- a/pkg/apply/task/apply_task.go +++ b/pkg/apply/task/apply_task.go @@ -4,28 +4,24 @@ package task import ( - "context" "io/ioutil" "strings" - apierrors "k8s.io/apimachinery/pkg/api/errors" "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/util/sets" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/resource" - "k8s.io/client-go/dynamic" "k8s.io/klog/v2" "k8s.io/kubectl/pkg/cmd/apply" cmddelete "k8s.io/kubectl/pkg/cmd/delete" "k8s.io/kubectl/pkg/cmd/util" 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/taskrunner" "sigs.k8s.io/cli-utils/pkg/common" - "sigs.k8s.io/cli-utils/pkg/inventory" "sigs.k8s.io/cli-utils/pkg/object" ) @@ -51,19 +47,15 @@ type ApplyTask struct { InfoHelper info.InfoHelper Mapper meta.RESTMapper Objects []*unstructured.Unstructured + Filters []filter.ValidationFilter DryRunStrategy common.DryRunStrategy ServerSideOptions common.ServerSideOptions - InventoryPolicy inventory.InventoryPolicy - InvInfo inventory.InventoryInfo } // applyOptionsFactoryFunc is a factory function for creating a new // applyOptions implementation. Used to allow unit testing. var applyOptionsFactoryFunc = newApplyOptions -// getClusterObj gets the cluster object. Used for allow unit testing. -var getClusterObj = getClusterObject - func (a *ApplyTask) Name() string { return a.TaskName } @@ -90,7 +82,7 @@ func (a *ApplyTask) Start(taskContext *taskrunner.TaskContext) { klog.V(2).Infof("apply task starting (%d objects)", len(objects)) // Create a new instance of the applyOptions interface and use it // to apply the objects. - ao, dynamic, err := applyOptionsFactoryFunc(taskContext.EventChannel(), + ao, err := applyOptionsFactoryFunc(taskContext.EventChannel(), a.ServerSideOptions, a.DryRunStrategy, a.Factory) if err != nil { if klog.V(4).Enabled() { @@ -115,29 +107,29 @@ func (a *ApplyTask) Start(taskContext *taskrunner.TaskContext) { taskContext.CaptureResourceFailure(id) continue } - clusterObj, err := getClusterObj(dynamic, info) - if err != nil { - if !apierrors.IsNotFound(err) { - if klog.V(4).Enabled() { - klog.Errorf("error (%s) retrieving %s/%s from cluster--continue", - err, info.Namespace, info.Name) + // 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.EventChannel() <- createApplyFailedEvent(id, err) + taskContext.EventChannel() <- createApplyFailedEvent(id, filterErr) taskContext.CaptureResourceFailure(id) - continue + break } - } - canApply, err := inventory.CanApply(a.InvInfo, clusterObj, a.InventoryPolicy) - if !canApply { - klog.V(5).Infof("can not apply %s/%s--continue", - clusterObj.GetNamespace(), clusterObj.GetName()) - if err != nil { - taskContext.EventChannel() <- createApplyFailedEvent(id, err) - } else { - taskContext.EventChannel() <- createApplyEvent(id, - event.Unchanged, clusterObj) + if filtered { + klog.V(4).Infof("apply filtered by %s because (%s): %s", filter.Name(), reason, id) + taskContext.EventChannel() <- createApplyEvent(id, event.Unchanged, obj) + taskContext.CaptureResourceFailure(id) + break } - taskContext.CaptureResourceFailure(id) + } + if filtered || filterErr != nil { continue } ao.SetObjects([]*resource.Info{info}) @@ -170,16 +162,15 @@ func (a *ApplyTask) Start(taskContext *taskrunner.TaskContext) { } func newApplyOptions(eventChannel chan event.Event, serverSideOptions common.ServerSideOptions, - strategy common.DryRunStrategy, factory util.Factory) (applyOptions, dynamic.Interface, error) { + strategy common.DryRunStrategy, factory util.Factory) (applyOptions, error) { discovery, err := factory.ToDiscoveryClient() if err != nil { - return nil, nil, err + return nil, err } dynamic, err := factory.DynamicClient() if err != nil { - return nil, nil, err + return nil, err } - emptyString := "" return &apply.ApplyOptions{ VisitedNamespaces: sets.NewString(), @@ -209,12 +200,7 @@ func newApplyOptions(eventChannel chan event.Event, serverSideOptions common.Ser }).toPrinterFunc(), DynamicClient: dynamic, DryRunVerifier: resource.NewDryRunVerifier(dynamic, discovery), - }, dynamic, nil -} - -func getClusterObject(p dynamic.Interface, info *resource.Info) (*unstructured.Unstructured, error) { - namespacedClient := p.Resource(info.Mapping.Resource).Namespace(info.Namespace) - return namespacedClient.Get(context.TODO(), info.Name, metav1.GetOptions{}) + }, nil } func (a *ApplyTask) sendTaskResult(taskContext *taskrunner.TaskContext) { @@ -269,7 +255,7 @@ func isStreamError(err error) bool { } func clientSideApply(info *resource.Info, eventChannel chan event.Event, strategy common.DryRunStrategy, factory util.Factory) error { - ao, _, err := applyOptionsFactoryFunc(eventChannel, common.ServerSideOptions{ServerSideApply: false}, strategy, factory) + ao, err := applyOptionsFactoryFunc(eventChannel, common.ServerSideOptions{ServerSideApply: false}, strategy, factory) if err != nil { return err } diff --git a/pkg/apply/task/apply_task_test.go b/pkg/apply/task/apply_task_test.go index 0eb3a955..fae754c4 100644 --- a/pkg/apply/task/apply_task_test.go +++ b/pkg/apply/task/apply_task_test.go @@ -15,12 +15,10 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/cli-runtime/pkg/resource" - "k8s.io/client-go/dynamic" "k8s.io/kubectl/pkg/cmd/util" "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" "sigs.k8s.io/cli-utils/pkg/testutil" ) @@ -86,8 +84,8 @@ func TestApplyTask_BasicAppliedObjects(t *testing.T) { objs := toUnstructureds(tc.applied) oldAO := applyOptionsFactoryFunc - applyOptionsFactoryFunc = func(chan event.Event, common.ServerSideOptions, common.DryRunStrategy, util.Factory) (applyOptions, dynamic.Interface, error) { - return &fakeApplyOptions{}, nil, nil + applyOptionsFactoryFunc = func(chan event.Event, common.ServerSideOptions, common.DryRunStrategy, util.Factory) (applyOptions, error) { + return &fakeApplyOptions{}, nil } defer func() { applyOptionsFactoryFunc = oldAO }() @@ -105,12 +103,8 @@ func TestApplyTask_BasicAppliedObjects(t *testing.T) { Objects: objs, Mapper: restMapper, InfoHelper: &fakeInfoHelper{}, - InvInfo: &fakeInventoryInfo{}, } - getClusterObj = func(d dynamic.Interface, info *resource.Info) (*unstructured.Unstructured, error) { - return objs[0], nil - } applyTask.Start(taskContext) <-taskContext.TaskChannel() @@ -175,23 +169,13 @@ func TestApplyTask_FetchGeneration(t *testing.T) { objs := toUnstructureds(tc.rss) oldAO := applyOptionsFactoryFunc - applyOptionsFactoryFunc = func(chan event.Event, common.ServerSideOptions, common.DryRunStrategy, util.Factory) (applyOptions, dynamic.Interface, error) { - return &fakeApplyOptions{}, nil, nil + applyOptionsFactoryFunc = func(chan event.Event, common.ServerSideOptions, common.DryRunStrategy, util.Factory) (applyOptions, error) { + return &fakeApplyOptions{}, nil } defer func() { applyOptionsFactoryFunc = oldAO }() applyTask := &ApplyTask{ Objects: objs, InfoHelper: &fakeInfoHelper{}, - InvInfo: &fakeInventoryInfo{}, - } - - getClusterObj = func(d dynamic.Interface, info *resource.Info) (*unstructured.Unstructured, error) { - for _, obj := range objs { - if info.Name == obj.GetName() && info.Namespace == obj.GetNamespace() { - return obj, nil - } - } - return nil, nil } applyTask.Start(taskContext) @@ -305,20 +289,16 @@ func TestApplyTask_DryRun(t *testing.T) { ao := &fakeApplyOptions{} oldAO := applyOptionsFactoryFunc - applyOptionsFactoryFunc = func(chan event.Event, common.ServerSideOptions, common.DryRunStrategy, util.Factory) (applyOptions, dynamic.Interface, error) { - return ao, nil, nil + applyOptionsFactoryFunc = func(chan event.Event, common.ServerSideOptions, common.DryRunStrategy, util.Factory) (applyOptions, error) { + return ao, nil } defer func() { applyOptionsFactoryFunc = oldAO }() - getClusterObj = func(d dynamic.Interface, info *resource.Info) (*unstructured.Unstructured, error) { - return addOwningInventory(tc.objs[0], "id"), nil - } applyTask := &ApplyTask{ Objects: tc.objs, InfoHelper: &fakeInfoHelper{}, Mapper: restMapper, DryRunStrategy: drs, - InvInfo: &fakeInventoryInfo{}, } var events []event.Event @@ -443,20 +423,16 @@ func TestApplyTaskWithError(t *testing.T) { ao := &fakeApplyOptions{} oldAO := applyOptionsFactoryFunc - applyOptionsFactoryFunc = func(chan event.Event, common.ServerSideOptions, common.DryRunStrategy, util.Factory) (applyOptions, dynamic.Interface, error) { - return ao, nil, nil + applyOptionsFactoryFunc = func(chan event.Event, common.ServerSideOptions, common.DryRunStrategy, util.Factory) (applyOptions, error) { + return ao, nil } defer func() { applyOptionsFactoryFunc = oldAO }() - getClusterObj = func(d dynamic.Interface, info *resource.Info) (*unstructured.Unstructured, error) { - return addOwningInventory(tc.objs[0], "id"), nil - } applyTask := &ApplyTask{ Objects: tc.objs, InfoHelper: &fakeInfoHelper{}, Mapper: restMapper, DryRunStrategy: drs, - InvInfo: &fakeInventoryInfo{}, } var events []event.Event @@ -492,194 +468,6 @@ func TestApplyTaskWithError(t *testing.T) { } } -var deployment = toUnstructured(map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "deploy", - "namespace": "default", - "uid": "uid-deployment", - }, -}) - -var deploymentObjMetadata = []object.ObjMetadata{ - { - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Name: "deploy", - Namespace: "default", - }, -} - -func TestApplyTaskWithDifferentInventoryAnnotation(t *testing.T) { - testCases := map[string]struct { - obj *unstructured.Unstructured - clusterObj *unstructured.Unstructured - policy inventory.InventoryPolicy - expectedObjects []object.ObjMetadata - expectedEvents []event.Event - }{ - "InventoryPolicyMustMatch with object doesn't exist on cluster - Can Apply": { - obj: deployment, - clusterObj: nil, - policy: inventory.InventoryPolicyMustMatch, - expectedObjects: deploymentObjMetadata, - expectedEvents: []event.Event{}, - }, - "InventoryPolicyMustMatch with object annotation is empty - Can't Apply": { - obj: deployment, - clusterObj: removeOwningInventory(deployment), - policy: inventory.InventoryPolicyMustMatch, - expectedObjects: nil, - expectedEvents: []event.Event{ - { - Type: event.ApplyType, - ApplyEvent: event.ApplyEvent{ - Error: inventory.NewNeedAdoptionError( - fmt.Errorf("can't adopt an object without the annotation config.k8s.io/owning-inventory")), - }, - }, - }, - }, - "InventoryPolicyMustMatch with object annotation doesn't match - Can't Apply": { - obj: deployment, - clusterObj: addOwningInventory(deployment, "unmatchd"), - policy: inventory.InventoryPolicyMustMatch, - expectedObjects: nil, - expectedEvents: []event.Event{ - { - Type: event.ApplyType, - ApplyEvent: event.ApplyEvent{ - Error: inventory.NewInventoryOverlapError( - fmt.Errorf("can't apply the resource since its annotation config.k8s.io/owning-inventory is a different inventory object")), - }, - }, - }, - }, - "InventoryPolicyMustMatch with object annotation matches - Can Apply": { - obj: deployment, - clusterObj: addOwningInventory(deployment, "id"), - policy: inventory.InventoryPolicyMustMatch, - expectedObjects: deploymentObjMetadata, - expectedEvents: nil, - }, - "AdoptIfNoInventory with object doesn't exist on cluster - Can Apply": { - obj: deployment, - clusterObj: nil, - policy: inventory.AdoptIfNoInventory, - expectedObjects: deploymentObjMetadata, - expectedEvents: []event.Event{}, - }, - "AdoptIfNoInventory with object annotation is empty - Can Apply": { - obj: deployment, - clusterObj: removeOwningInventory(deployment), - policy: inventory.AdoptIfNoInventory, - expectedObjects: deploymentObjMetadata, - expectedEvents: []event.Event{}, - }, - "AdoptIfNoInventory with object annotation doesn't match - Can't Apply": { - obj: deployment, - clusterObj: addOwningInventory(deployment, "notmatch"), - policy: inventory.AdoptIfNoInventory, - expectedObjects: nil, - expectedEvents: []event.Event{ - { - Type: event.ApplyType, - ApplyEvent: event.ApplyEvent{ - Error: inventory.NewInventoryOverlapError( - fmt.Errorf("can't apply the resource since its annotation config.k8s.io/owning-inventory is a different inventory object")), - }, - }, - }, - }, - "AdoptIfNoInventory with object annotation matches - Can Apply": { - obj: deployment, - clusterObj: addOwningInventory(deployment, "id"), - policy: inventory.AdoptIfNoInventory, - expectedObjects: deploymentObjMetadata, - expectedEvents: []event.Event{}, - }, - "AdoptAll with object doesn't exist on cluster - Can Apply": { - obj: deployment, - clusterObj: nil, - policy: inventory.AdoptAll, - expectedObjects: deploymentObjMetadata, - expectedEvents: []event.Event{}, - }, - } - - for tn, tc := range testCases { - drs := common.DryRunNone - t.Run(tn, func(t *testing.T) { - eventChannel := make(chan event.Event) - taskContext := taskrunner.NewTaskContext(eventChannel) - - restMapper := testutil.NewFakeRESTMapper(schema.GroupVersionKind{ - Group: "apps", - Version: "v1", - Kind: "Deployment", - }) - - ao := &fakeApplyOptions{} - oldAO := applyOptionsFactoryFunc - applyOptionsFactoryFunc = func(chan event.Event, common.ServerSideOptions, common.DryRunStrategy, util.Factory) (applyOptions, dynamic.Interface, error) { - return ao, nil, nil - } - defer func() { applyOptionsFactoryFunc = oldAO }() - - getClusterObj = func(d dynamic.Interface, info *resource.Info) (*unstructured.Unstructured, error) { - return tc.clusterObj, nil - } - applyTask := &ApplyTask{ - Objects: []*unstructured.Unstructured{tc.obj}, - InfoHelper: &fakeInfoHelper{}, - Mapper: restMapper, - DryRunStrategy: drs, - InvInfo: &fakeInventoryInfo{}, - InventoryPolicy: tc.policy, - } - - 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()) - } - actualUids := taskContext.AppliedResourceUIDs() - assert.Equal(t, len(tc.expectedObjects), len(actualUids)) - actualObjs := taskContext.AppliedResources() - if !object.SetEquals(tc.expectedObjects, actualObjs) { - t.Errorf("expected applied objects (%v), got (%v)", tc.expectedObjects, actualObjs) - } - }) - } -} - func toUnstructured(obj map[string]interface{}) *unstructured.Unstructured { return &unstructured.Unstructured{ Object: obj, @@ -743,50 +531,3 @@ func (f *fakeInfoHelper) BuildInfos(objs []*unstructured.Unstructured) ([]*resou func (f *fakeInfoHelper) BuildInfo(obj *unstructured.Unstructured) (*resource.Info, error) { return object.UnstructuredToInfo(obj) } - -type fakeInventoryInfo struct{} - -func (fi *fakeInventoryInfo) Name() string { - return "name" -} - -func (fi *fakeInventoryInfo) Namespace() string { - return "namespace" -} - -func (fi *fakeInventoryInfo) ID() string { - return "id" -} - -func (fi *fakeInventoryInfo) Strategy() inventory.InventoryStrategy { - return inventory.NameStrategy -} - -func addOwningInventory(obj *unstructured.Unstructured, id string) *unstructured.Unstructured { - if obj == nil { - return nil - } - newObj := obj.DeepCopy() - annotations := newObj.GetAnnotations() - if len(annotations) == 0 { - annotations = make(map[string]string) - } - - annotations["config.k8s.io/owning-inventory"] = id - newObj.SetAnnotations(annotations) - return newObj -} - -func removeOwningInventory(obj *unstructured.Unstructured) *unstructured.Unstructured { - if obj == nil { - return nil - } - newObj := obj.DeepCopy() - annotations := newObj.GetAnnotations() - if len(annotations) == 0 { - return newObj - } - delete(annotations, "config.k8s.io/owning-inventory") - newObj.SetAnnotations(annotations) - return newObj -}