Merge pull request #207 from mortent/OrderResourceForPrune

Prune/destroy resources in reverse order from apply
This commit is contained in:
Kubernetes Prow Robot 2020-07-20 10:10:52 -07:00 committed by GitHub
commit e371bd5ca8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 115 additions and 87 deletions

View File

@ -26,6 +26,7 @@ import (
"sigs.k8s.io/cli-utils/pkg/inventory" "sigs.k8s.io/cli-utils/pkg/inventory"
"sigs.k8s.io/cli-utils/pkg/kstatus/polling" "sigs.k8s.io/cli-utils/pkg/kstatus/polling"
"sigs.k8s.io/cli-utils/pkg/object" "sigs.k8s.io/cli-utils/pkg/object"
"sigs.k8s.io/cli-utils/pkg/ordering"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
) )
@ -182,7 +183,7 @@ func (a *Applier) prepareObjects(infos []*resource.Info) (*ResourceObjects, erro
return nil, err return nil, err
} }
sort.Sort(ResourceInfos(resources)) sort.Sort(ordering.SortableInfos(resources))
if !validateNamespace(resources) { if !validateNamespace(resources) {
return nil, fmt.Errorf("objects have differing namespaces") return nil, fmt.Errorf("objects have differing namespaces")

View File

@ -13,6 +13,7 @@ package prune
import ( import (
"fmt" "fmt"
"sort"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
@ -26,6 +27,7 @@ import (
"sigs.k8s.io/cli-utils/pkg/apply/event" "sigs.k8s.io/cli-utils/pkg/apply/event"
"sigs.k8s.io/cli-utils/pkg/common" "sigs.k8s.io/cli-utils/pkg/common"
"sigs.k8s.io/cli-utils/pkg/inventory" "sigs.k8s.io/cli-utils/pkg/inventory"
"sigs.k8s.io/cli-utils/pkg/ordering"
) )
// PruneOptions encapsulates the necessary information to // PruneOptions encapsulates the necessary information to
@ -103,6 +105,11 @@ func (po *PruneOptions) Prune(currentObjects []*resource.Info, eventChannel chan
} }
klog.V(4).Infof("prune %d currently applied objects", len(po.currentUids)) klog.V(4).Infof("prune %d currently applied objects", len(po.currentUids))
klog.V(4).Infof("prune %d previously applied objects", len(pastObjs)) klog.V(4).Infof("prune %d previously applied objects", len(pastObjs))
// Sort the resources in reverse order using the same rules as is
// used for apply.
sort.Sort(sort.Reverse(ordering.SortableMetas(pastObjs)))
// Iterate through set of all previously applied objects. // Iterate through set of all previously applied objects.
for _, past := range pastObjs { for _, past := range pastObjs {
mapping, err := po.mapper.RESTMapping(past.GroupKind) mapping, err := po.mapper.RESTMapping(past.GroupKind)

View File

@ -20,9 +20,9 @@ import (
var testNamespace = "test-inventory-namespace" var testNamespace = "test-inventory-namespace"
var inventoryObjName = "test-inventory-obj" var inventoryObjName = "test-inventory-obj"
var pod1Name = "pod-1" var namespaceName = "namespace"
var pod2Name = "pod-2" var pdbName = "pdb"
var pod3Name = "pod-3" var roleName = "role"
var testInventoryLabel = "test-app-label" var testInventoryLabel = "test-app-label"
@ -40,58 +40,58 @@ var inventoryObj = unstructured.Unstructured{
}, },
} }
var pod1 = unstructured.Unstructured{ var namespace = unstructured.Unstructured{
Object: map[string]interface{}{ Object: map[string]interface{}{
"apiVersion": "v1", "apiVersion": "v1",
"kind": "Pod", "kind": "Namespace",
"metadata": map[string]interface{}{ "metadata": map[string]interface{}{
"name": pod1Name, "name": namespaceName,
"namespace": testNamespace, "namespace": testNamespace,
"uid": "uid1", "uid": "uid1",
}, },
}, },
} }
var pod1Info = &resource.Info{ var namespaceInfo = &resource.Info{
Namespace: testNamespace, Namespace: testNamespace,
Name: pod1Name, Name: namespaceName,
Object: &pod1, Object: &namespace,
} }
var pod2 = unstructured.Unstructured{ var pdb = unstructured.Unstructured{
Object: map[string]interface{}{ Object: map[string]interface{}{
"apiVersion": "v1", "apiVersion": "policy/v1beta1",
"kind": "Pod", "kind": "PodDisruptionBudget",
"metadata": map[string]interface{}{ "metadata": map[string]interface{}{
"name": pod2Name, "name": pdbName,
"namespace": testNamespace, "namespace": testNamespace,
"uid": "uid2", "uid": "uid2",
}, },
}, },
} }
var pod2Info = &resource.Info{ var pdbInfo = &resource.Info{
Namespace: testNamespace, Namespace: testNamespace,
Name: pod2Name, Name: pdbName,
Object: &pod2, Object: &pdb,
} }
var pod3 = unstructured.Unstructured{ var role = unstructured.Unstructured{
Object: map[string]interface{}{ Object: map[string]interface{}{
"apiVersion": "v1", "apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "Pod", "kind": "Role",
"metadata": map[string]interface{}{ "metadata": map[string]interface{}{
"name": pod3Name, "name": roleName,
"namespace": testNamespace, "namespace": testNamespace,
"uid": "uid3", "uid": "uid3",
}, },
}, },
} }
var pod3Info = &resource.Info{ var roleInfo = &resource.Info{
Namespace: testNamespace, Namespace: testNamespace,
Name: pod3Name, Name: roleName,
Object: &pod3, Object: &role,
} }
// Returns a inventory object with the inventory set from // Returns a inventory object with the inventory set from
@ -151,32 +151,32 @@ func TestPrune(t *testing.T) {
isError: false, isError: false,
}, },
"Past and current objects are the same; no pruned objects": { "Past and current objects are the same; no pruned objects": {
pastInfos: []*resource.Info{pod1Info, pod2Info}, pastInfos: []*resource.Info{namespaceInfo, pdbInfo},
currentInfos: []*resource.Info{pod2Info, pod1Info}, currentInfos: []*resource.Info{pdbInfo, namespaceInfo},
prunedInfos: []*resource.Info{}, prunedInfos: []*resource.Info{},
isError: false, isError: false,
}, },
"No past objects; no pruned objects": { "No past objects; no pruned objects": {
pastInfos: []*resource.Info{}, pastInfos: []*resource.Info{},
currentInfos: []*resource.Info{pod2Info, pod1Info}, currentInfos: []*resource.Info{pdbInfo, namespaceInfo},
prunedInfos: []*resource.Info{}, prunedInfos: []*resource.Info{},
isError: false, isError: false,
}, },
"No current objects; all previous objects pruned": { "No current objects; all previous objects pruned in correct order": {
pastInfos: []*resource.Info{pod1Info, pod2Info, pod3Info}, pastInfos: []*resource.Info{namespaceInfo, pdbInfo, roleInfo},
currentInfos: []*resource.Info{}, currentInfos: []*resource.Info{},
prunedInfos: []*resource.Info{pod1Info, pod2Info, pod3Info}, prunedInfos: []*resource.Info{pdbInfo, roleInfo, namespaceInfo},
isError: false, isError: false,
}, },
"Omitted object is pruned": { "Omitted object is pruned": {
pastInfos: []*resource.Info{pod1Info, pod2Info}, pastInfos: []*resource.Info{namespaceInfo, pdbInfo},
currentInfos: []*resource.Info{pod2Info, pod3Info}, currentInfos: []*resource.Info{pdbInfo, roleInfo},
prunedInfos: []*resource.Info{pod1Info}, prunedInfos: []*resource.Info{namespaceInfo},
isError: false, isError: false,
}, },
"Prevent delete lifecycle annotation stops pruning": { "Prevent delete lifecycle annotation stops pruning": {
pastInfos: []*resource.Info{preventDeleteInfo, pod2Info}, pastInfos: []*resource.Info{preventDeleteInfo, pdbInfo},
currentInfos: []*resource.Info{pod2Info, pod3Info}, currentInfos: []*resource.Info{pdbInfo, roleInfo},
prunedInfos: []*resource.Info{}, prunedInfos: []*resource.Info{},
isError: false, isError: false,
}, },
@ -191,29 +191,42 @@ func TestPrune(t *testing.T) {
// Set up the currently applied objects. // Set up the currently applied objects.
currentInventoryInfo := createInventoryInfo("current-group", tc.currentInfos...) currentInventoryInfo := createInventoryInfo("current-group", tc.currentInfos...)
currentInfos := append(tc.currentInfos, currentInventoryInfo) currentInfos := append(tc.currentInfos, currentInventoryInfo)
// Set up the fake dynamic client to recognize all objects, and the RESTMapper.
po.client = fake.NewSimpleDynamicClient(scheme.Scheme,
namespaceInfo.Object, pdbInfo.Object, roleInfo.Object)
po.mapper = testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme,
scheme.Scheme.PrioritizedVersionsAllGroups()...)
// The event channel can not block; make sure its bigger than all // The event channel can not block; make sure its bigger than all
// the events that can be put on it. // the events that can be put on it.
eventChannel := make(chan event.Event, len(tc.pastInfos)+1) // Add one for inventory object eventChannel := make(chan event.Event, len(tc.pastInfos)+1) // Add one for inventory object
defer close(eventChannel) err := func() error {
// Set up the fake dynamic client to recognize all objects, and the RESTMapper. defer close(eventChannel)
po.client = fake.NewSimpleDynamicClient(scheme.Scheme, // Run the prune and validate.
pod1Info.Object, pod2Info.Object, pod3Info.Object) return po.Prune(currentInfos, eventChannel, Options{
po.mapper = testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme, DryRun: true,
scheme.Scheme.PrioritizedVersionsAllGroups()...) })
// Run the prune and validate. }()
err := po.Prune(currentInfos, eventChannel, Options{
DryRun: true,
})
if !tc.isError { if !tc.isError {
if err != nil { if err != nil {
t.Fatalf("Unexpected error during Prune(): %#v", err) t.Fatalf("Unexpected error during Prune(): %#v", err)
} }
// Validate the prune events on the event channel.
expectedPruneEvents := len(tc.prunedInfos) + 1 // One extra for pruning inventory object var actualPruneEvents []event.Event
actualPruneEvents := len(eventChannel) for e := range eventChannel {
if expectedPruneEvents != actualPruneEvents { actualPruneEvents = append(actualPruneEvents, e)
t.Errorf("Expected (%d) prune events, got (%d)", }
expectedPruneEvents, actualPruneEvents) if want, got := len(tc.prunedInfos)+1, len(actualPruneEvents); want != got {
t.Errorf("Expected (%d) prune events, got (%d)", want, got)
}
for i, info := range tc.prunedInfos {
e := actualPruneEvents[i]
expKind := info.Object.GetObjectKind().GroupVersionKind().Kind
actKind := e.PruneEvent.Object.GetObjectKind().GroupVersionKind().Kind
if expKind != actKind {
t.Errorf("Expected kind %s, got %s", expKind, actKind)
}
} }
} else if err == nil { } else if err == nil {
t.Fatalf("Expected error during Prune() but received none") t.Fatalf("Expected error during Prune() but received none")

View File

@ -1,30 +1,43 @@
// Copyright 2020 The Kubernetes Authors. // Copyright 2020 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package apply package ordering
import ( import (
"sort" "sort"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"sigs.k8s.io/cli-utils/pkg/object"
) )
type ResourceInfos []*resource.Info type SortableInfos []*resource.Info
var _ sort.Interface = ResourceInfos{} var _ sort.Interface = SortableInfos{}
func (a ResourceInfos) Len() int { return len(a) } func (a SortableInfos) Len() int { return len(a) }
func (a ResourceInfos) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a SortableInfos) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ResourceInfos) Less(i, j int) bool { func (a SortableInfos) Less(i, j int) bool {
x := a[i].Object.GetObjectKind().GroupVersionKind() return less(object.InfoToObjMeta(a[i]), object.InfoToObjMeta(a[j]))
o := a[j].Object.GetObjectKind().GroupVersionKind() }
if !Equals(x, o) {
return IsLessThan(x, o) type SortableMetas []object.ObjMetadata
var _ sort.Interface = SortableMetas{}
func (a SortableMetas) Len() int { return len(a) }
func (a SortableMetas) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a SortableMetas) Less(i, j int) bool {
return less(a[i], a[j])
}
func less(i, j object.ObjMetadata) bool {
if !Equals(i.GroupKind, j.GroupKind) {
return IsLessThan(i.GroupKind, j.GroupKind)
} }
// In case of tie, compare the namespace and name combination so that the output // In case of tie, compare the namespace and name combination so that the output
// order is consistent irrespective of input order // order is consistent irrespective of input order
return a[i].Namespace+a[i].Name < a[j].Namespace+a[j].Name return i.Namespace+i.Name < j.Namespace+j.Name
} }
// An attempt to order things to help k8s, e.g. // An attempt to order things to help k8s, e.g.
@ -70,13 +83,11 @@ func getIndexByKind(kind string) int {
return m[kind] return m[kind]
} }
// Equals returns true if the GVK's have equal fields. func Equals(x schema.GroupKind, o schema.GroupKind) bool {
func Equals(x schema.GroupVersionKind, o schema.GroupVersionKind) bool { return x.Group == o.Group && x.Kind == o.Kind
return x.Group == o.Group && x.Version == o.Version && x.Kind == o.Kind
} }
// IsLessThan compares two GVK's as per orderFirst and orderLast, returns boolean result. func IsLessThan(x schema.GroupKind, o schema.GroupKind) bool {
func IsLessThan(x schema.GroupVersionKind, o schema.GroupVersionKind) bool {
indexI := getIndexByKind(x.Kind) indexI := getIndexByKind(x.Kind)
indexJ := getIndexByKind(o.Kind) indexJ := getIndexByKind(o.Kind)
if indexI != indexJ { if indexI != indexJ {

View File

@ -1,7 +1,7 @@
// Copyright 2020 The Kubernetes Authors. // Copyright 2020 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package apply package ordering
import ( import (
"sort" "sort"
@ -78,7 +78,7 @@ func TestResourceOrdering(t *testing.T) {
} }
infos := []*resource.Info{&deploymentInfo, &configMapInfo, &namespaceInfo, &deploymentInfo2} infos := []*resource.Info{&deploymentInfo, &configMapInfo, &namespaceInfo, &deploymentInfo2}
sort.Sort(ResourceInfos(infos)) sort.Sort(SortableInfos(infos))
assert.Equal(t, infos[0].Name, "testspace") assert.Equal(t, infos[0].Name, "testspace")
assert.Equal(t, infos[1].Name, "the-map") assert.Equal(t, infos[1].Name, "the-map")
@ -92,33 +92,29 @@ func TestResourceOrdering(t *testing.T) {
} }
func TestGvkLessThan(t *testing.T) { func TestGvkLessThan(t *testing.T) {
gvk1 := schema.GroupVersionKind{ gk1 := schema.GroupKind{
Group: "", Group: "",
Version: "v1", Kind: "Deployment",
Kind: "Deployment",
} }
gvk2 := schema.GroupVersionKind{ gk2 := schema.GroupKind{
Group: "", Group: "",
Version: "v1", Kind: "Namespace",
Kind: "Namespace",
} }
assert.Equal(t, IsLessThan(gvk1, gvk2), false) assert.Equal(t, IsLessThan(gk1, gk2), false)
} }
func TestGvkEquals(t *testing.T) { func TestGvkEquals(t *testing.T) {
gvk1 := schema.GroupVersionKind{ gk1 := schema.GroupKind{
Group: "", Group: "",
Version: "v1", Kind: "Deployment",
Kind: "Deployment",
} }
gvk2 := schema.GroupVersionKind{ gk2 := schema.GroupKind{
Group: "", Group: "",
Version: "v1", Kind: "Deployment",
Kind: "Deployment",
} }
assert.Equal(t, Equals(gvk1, gvk2), true) assert.Equal(t, Equals(gk1, gk2), true)
} }