Skip to content

Commit

Permalink
Moves inventory policy check for apply into filter
Browse files Browse the repository at this point in the history
  • Loading branch information
seans3 committed Aug 30, 2021
1 parent a4d5c55 commit 45cb7e3
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 332 deletions.
16 changes: 15 additions & 1 deletion pkg/apply/applier.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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{},
Expand All @@ -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()
Expand Down
73 changes: 73 additions & 0 deletions pkg/apply/filter/inventory-policy-apply-filter.go
Original file line number Diff line number Diff line change
@@ -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{})
}
121 changes: 121 additions & 0 deletions pkg/apply/filter/inventory-policy-apply-filter_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
13 changes: 6 additions & 7 deletions pkg/apply/solver/solver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
17 changes: 1 addition & 16 deletions pkg/apply/solver/solver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 45cb7e3

Please sign in to comment.