mirror of https://github.com/fluxcd/cli-utils.git
Merge pull request #207 from mortent/OrderResourceForPrune
Prune/destroy resources in reverse order from apply
This commit is contained in:
commit
e371bd5ca8
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
err := func() error {
|
||||||
defer close(eventChannel)
|
defer close(eventChannel)
|
||||||
// Set up the fake dynamic client to recognize all objects, and the RESTMapper.
|
|
||||||
po.client = fake.NewSimpleDynamicClient(scheme.Scheme,
|
|
||||||
pod1Info.Object, pod2Info.Object, pod3Info.Object)
|
|
||||||
po.mapper = testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme,
|
|
||||||
scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
|
||||||
// Run the prune and validate.
|
// Run the prune and validate.
|
||||||
err := po.Prune(currentInfos, eventChannel, Options{
|
return po.Prune(currentInfos, eventChannel, Options{
|
||||||
DryRun: true,
|
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")
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue