diff --git a/cmd/printers/table/collector.go b/cmd/printers/table/collector.go index a7ac734..de803c6 100644 --- a/cmd/printers/table/collector.go +++ b/cmd/printers/table/collector.go @@ -76,9 +76,13 @@ type ResourceInfo struct { // a resource has been applied to the cluster. ApplyOpResult *event.ApplyEventOperation - // Pruned contains information about whether - // the resources has been pruned. - Pruned bool + // PruneOpResult contains the result after + // a prune operation on a resource + PruneOpResult *event.PruneEventOperation + + // DeleteOpResult contains the result after + // a delete operation on a resource + DeleteOpResult *event.DeleteEventOperation } // Identifier returns the identifier for the given resource. @@ -205,7 +209,7 @@ func (r *ResourceStateCollector) processPruneEvent(e event.PruneEvent) { if !found { return } - previous.Pruned = true + previous.PruneOpResult = &e.Operation } } @@ -290,7 +294,8 @@ func (r *ResourceStateCollector) LatestState() *ResourceState { resourceStatus: ri.resourceStatus, ResourceAction: ri.ResourceAction, ApplyOpResult: ri.ApplyOpResult, - Pruned: ri.Pruned, + PruneOpResult: ri.PruneOpResult, + DeleteOpResult: ri.DeleteOpResult, }) } sort.Sort(resourceInfos) diff --git a/cmd/printers/table/printer.go b/cmd/printers/table/printer.go index 943f7b0..61df677 100644 --- a/cmd/printers/table/printer.go +++ b/cmd/printers/table/printer.go @@ -89,8 +89,8 @@ var ( text = resInfo.ApplyOpResult.String() } case event.PruneAction: - if resInfo.Pruned { - text = "Pruned" + if resInfo.PruneOpResult != nil { + text = resInfo.PruneOpResult.String() } } diff --git a/cmd/printers/table/printer_test.go b/cmd/printers/table/printer_test.go index 795a691..ca6a4f2 100644 --- a/cmd/printers/table/printer_test.go +++ b/cmd/printers/table/printer_test.go @@ -13,6 +13,7 @@ import ( var ( createdOpResult = event.Created + prunedOpResult = event.Pruned ) func TestActionColumnDef(t *testing.T) { @@ -42,7 +43,7 @@ func TestActionColumnDef(t *testing.T) { "pruned": { resource: &ResourceInfo{ ResourceAction: event.PruneAction, - Pruned: true, + PruneOpResult: &prunedOpResult, }, columnWidth: 15, expectedOutput: "Pruned", diff --git a/examples/alphaTestExamples/pruneAndDelete.md b/examples/alphaTestExamples/pruneAndDelete.md new file mode 100644 index 0000000..9aae531 --- /dev/null +++ b/examples/alphaTestExamples/pruneAndDelete.md @@ -0,0 +1,156 @@ +[kind]: https://github.com/kubernetes-sigs/kind + +# Demo: Lifecycle directives + +This demo shows how it is possible to use a lifecycle directive to +change the behavior of prune and delete for specific resources. + +First define a place to work: + + +``` +DEMO_HOME=$(mktemp -d) +``` + +Alternatively, use + +> ``` +> DEMO_HOME=~/hello +> ``` + +## Establish the base + + +``` +BASE=$DEMO_HOME/base +mkdir -p $BASE +OUTPUT=$DEMO_HOME/output +mkdir -p $OUTPUT + +function expectedOutputLine() { + test 1 == \ + $(grep "$@" $OUTPUT/status | wc -l); \ + echo $? +} +``` + +In this example we will just use two ConfigMap resources for simplicity, but +of course any type of resource can be used. On one of our ConfigMaps, we add the +**cli-utils.sigs.k8s.io/on-remove** annotation with the value of **keep**. This +annotation tells the kapply tool that this resource should not be deleted, even +if it would otherwise be pruned or deleted with the destroy command. + + +``` +cat <$BASE/configMap1.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: firstmap +data: + artist: Ornette Coleman + album: The shape of jazz to come +EOF +``` + +This ConfigMap includes the lifecycle directive annotation + + +``` +cat <$BASE/configMap2.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: secondmap + annotations: + cli-utils.sigs.k8s.io/on-remove: keep +data: + artist: Husker Du + album: New Day Rising +EOF +``` + +## Run end-to-end tests + +The following requires installation of [kind]. + +Delete any existing kind cluster and create a new one. By default the name of the cluster is "kind" + +``` +kind delete cluster +kind create cluster +``` + +Use the kapply init command to generate the inventory template. This contains +the namespace and inventory id used by apply to create inventory objects. + +``` +kapply init $BASE +``` + +Apply both resources to the cluster. + +``` +kapply apply $BASE --reconcile-timeout=1m > $OUTPUT/status +``` + +Use the preview command to show what will happen if we run destroy. This should +show that the second ConfigMap will not be deleted even when using the destroy +command. + +``` +kapply preview --destroy $BASE > $OUTPUT/status + +expectedOutputLine "configmap/firstmap deleted (preview)" + +expectedOutputLine "configmap/secondmap delete skipped (preview)" +``` + +We run the destroy command and see that the resource without the annotation +has been deleted, while the resource with the annotation is still in the +cluster. + +``` +kapply destroy $BASE > $OUTPUT/status + +expectedOutputLine "configmap/firstmap deleted" + +expectedOutputLine "configmap/secondmap delete skipped" + +kubectl get cm --no-headers | awk '{print $1}' > $OUTPUT/status +expectedOutputLine "secondmap" +``` + + +Apply the resources back to the cluster so we can demonstrate the lifecycle +directive with pruning. + +``` +kapply apply $BASE --reconcile-timeout=1m > $OUTPUT/status +``` + +Delete the manifest for the second configmap + +``` +rm $BASE/configMap2.yaml +``` + +Run preview to see that while secondmap would normally be pruned, it +will instead be skipped due to the lifecycle directive. + +``` +kapply preview $BASE > $OUTPUT/status + +expectedOutputLine "configmap/secondmap prune skipped (preview)" +``` + +Run apply and verify that secondmap is still in the cluster. + +``` +kapply apply $BASE > $OUTPUT/status + +expectedOutputLine "configmap/secondmap prune skipped" + +kubectl get cm --no-headers | awk '{print $1}' > $OUTPUT/status +expectedOutputLine "secondmap" +``` diff --git a/pkg/apply/basic_printer.go b/pkg/apply/basic_printer.go index cd3fdbc..61bcd5d 100644 --- a/pkg/apply/basic_printer.go +++ b/pkg/apply/basic_printer.go @@ -64,11 +64,16 @@ func (p *pruneStats) incSkipped() { } type deleteStats struct { - count int + deleted int + skipped int } -func (d *deleteStats) inc() { - d.count++ +func (d *deleteStats) incDeleted() { + d.deleted++ +} + +func (d *deleteStats) incSkipped() { + d.skipped++ } type statusCollector struct { @@ -185,31 +190,37 @@ func (b *BasicPrinter) processPruneEvent(pe event.PruneEvent, ps *pruneStats, p switch pe.Type { case event.PruneEventCompleted: p("%d resource(s) pruned, %d skipped", ps.pruned, ps.skipped) - case event.PruneEventSkipped: - obj := pe.Object - gvk := obj.GetObjectKind().GroupVersionKind() - name := getName(obj) - ps.incSkipped() - p("%s %s", resourceIDToString(gvk.GroupKind(), name), "pruned skipped") case event.PruneEventResourceUpdate: obj := pe.Object gvk := obj.GetObjectKind().GroupVersionKind() name := getName(obj) - ps.incPruned() - p("%s %s", resourceIDToString(gvk.GroupKind(), name), "pruned") + switch pe.Operation { + case event.Pruned: + ps.incPruned() + p("%s %s", resourceIDToString(gvk.GroupKind(), name), "pruned") + case event.PruneSkipped: + ps.incSkipped() + p("%s %s", resourceIDToString(gvk.GroupKind(), name), "prune skipped") + } } } func (b *BasicPrinter) processDeleteEvent(de event.DeleteEvent, ds *deleteStats, p printFunc) { switch de.Type { case event.DeleteEventCompleted: - p("%d resource(s) deleted", ds.count) + p("%d resource(s) deleted, %d skipped", ds.deleted, ds.skipped) case event.DeleteEventResourceUpdate: obj := de.Object gvk := obj.GetObjectKind().GroupVersionKind() name := getName(obj) - ds.inc() - p("%s %s", resourceIDToString(gvk.GroupKind(), name), "deleted") + switch de.Operation { + case event.Deleted: + ds.incDeleted() + p("%s %s", resourceIDToString(gvk.GroupKind(), name), "deleted") + case event.DeleteSkipped: + ds.incSkipped() + p("%s %s", resourceIDToString(gvk.GroupKind(), name), "delete skipped") + } } } diff --git a/pkg/apply/destroyer.go b/pkg/apply/destroyer.go index e34eb35..f7e6bf4 100644 --- a/pkg/apply/destroyer.go +++ b/pkg/apply/destroyer.go @@ -4,9 +4,10 @@ package apply import ( + "fmt" + "github.com/go-errors/errors" "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/cli-runtime/pkg/genericclioptions" @@ -165,11 +166,23 @@ func runPruneEventTransformer(eventChannel chan event.Event) (chan event.Event, eventChannel <- event.Event{ Type: event.DeleteType, DeleteEvent: event.DeleteEvent{ - Type: event.DeleteEventResourceUpdate, - Object: msg.PruneEvent.Object, + Type: event.DeleteEventResourceUpdate, + Operation: transformPruneOperation(msg.PruneEvent.Operation), + Object: msg.PruneEvent.Object, }, } } }() return tempEventChannel, completedChannel } + +func transformPruneOperation(pruneOp event.PruneEventOperation) event.DeleteEventOperation { + switch pruneOp { + case event.PruneSkipped: + return event.DeleteSkipped + case event.Pruned: + return event.Deleted + default: + panic(fmt.Errorf("unknown prune operation %s", pruneOp.String())) + } +} diff --git a/pkg/apply/event/deleteeventoperation_string.go b/pkg/apply/event/deleteeventoperation_string.go new file mode 100644 index 0000000..11fdc41 --- /dev/null +++ b/pkg/apply/event/deleteeventoperation_string.go @@ -0,0 +1,27 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by "stringer -type=DeleteEventOperation"; DO NOT EDIT. + +package event + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Deleted-0] + _ = x[DeleteSkipped-1] +} + +const _DeleteEventOperation_name = "DeletedDeleteSkipped" + +var _DeleteEventOperation_index = [...]uint8{0, 7, 20} + +func (i DeleteEventOperation) String() string { + if i < 0 || i >= DeleteEventOperation(len(_DeleteEventOperation_index)-1) { + return "DeleteEventOperation(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _DeleteEventOperation_name[_DeleteEventOperation_index[i]:_DeleteEventOperation_index[i+1]] +} diff --git a/pkg/apply/event/event.go b/pkg/apply/event/event.go index f31dc8c..31c363e 100644 --- a/pkg/apply/event/event.go +++ b/pkg/apply/event/event.go @@ -104,13 +104,21 @@ type PruneEventType int const ( PruneEventResourceUpdate PruneEventType = iota - PruneEventSkipped PruneEventCompleted ) +//go:generate stringer -type=PruneEventOperation +type PruneEventOperation int + +const ( + Pruned PruneEventOperation = iota + PruneSkipped +) + type PruneEvent struct { - Type PruneEventType - Object runtime.Object + Type PruneEventType + Operation PruneEventOperation + Object runtime.Object } //go:generate stringer -type=DeleteEventType @@ -121,7 +129,16 @@ const ( DeleteEventCompleted ) +//go:generate stringer -type=DeleteEventOperation +type DeleteEventOperation int + +const ( + Deleted DeleteEventOperation = iota + DeleteSkipped +) + type DeleteEvent struct { - Type DeleteEventType - Object runtime.Object + Type DeleteEventType + Operation DeleteEventOperation + Object runtime.Object } diff --git a/pkg/apply/event/pruneeventoperation_string.go b/pkg/apply/event/pruneeventoperation_string.go new file mode 100644 index 0000000..992cc64 --- /dev/null +++ b/pkg/apply/event/pruneeventoperation_string.go @@ -0,0 +1,27 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by "stringer -type=PruneEventOperation"; DO NOT EDIT. + +package event + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Pruned-0] + _ = x[PruneSkipped-1] +} + +const _PruneEventOperation_name = "PrunedPruneSkipped" + +var _PruneEventOperation_index = [...]uint8{0, 6, 18} + +func (i PruneEventOperation) String() string { + if i < 0 || i >= PruneEventOperation(len(_PruneEventOperation_index)-1) { + return "PruneEventOperation(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _PruneEventOperation_name[_PruneEventOperation_index[i]:_PruneEventOperation_index[i+1]] +} diff --git a/pkg/apply/event/pruneeventtype_string.go b/pkg/apply/event/pruneeventtype_string.go index 21bf1a1..e896b40 100644 --- a/pkg/apply/event/pruneeventtype_string.go +++ b/pkg/apply/event/pruneeventtype_string.go @@ -12,13 +12,12 @@ func _() { // Re-run the stringer command to generate them again. var x [1]struct{} _ = x[PruneEventResourceUpdate-0] - _ = x[PruneEventSkipped-1] - _ = x[PruneEventCompleted-2] + _ = x[PruneEventCompleted-1] } -const _PruneEventType_name = "PruneEventResourceUpdatePruneEventSkippedPruneEventCompleted" +const _PruneEventType_name = "PruneEventResourceUpdatePruneEventCompleted" -var _PruneEventType_index = [...]uint8{0, 24, 41, 60} +var _PruneEventType_index = [...]uint8{0, 24, 43} func (i PruneEventType) String() string { if i < 0 || i >= PruneEventType(len(_PruneEventType_index)-1) { diff --git a/pkg/apply/prune/prune.go b/pkg/apply/prune/prune.go index 23bcc62..1592efb 100644 --- a/pkg/apply/prune/prune.go +++ b/pkg/apply/prune/prune.go @@ -254,8 +254,9 @@ func (po *PruneOptions) Prune(currentObjects []*resource.Info, eventChannel chan eventChannel <- event.Event{ Type: event.PruneType, PruneEvent: event.PruneEvent{ - Type: event.PruneEventSkipped, - Object: obj, + Type: event.PruneEventResourceUpdate, + Operation: event.PruneSkipped, + Object: obj, }, } continue @@ -270,8 +271,9 @@ func (po *PruneOptions) Prune(currentObjects []*resource.Info, eventChannel chan eventChannel <- event.Event{ Type: event.PruneType, PruneEvent: event.PruneEvent{ - Type: event.PruneEventResourceUpdate, - Object: obj, + Type: event.PruneEventResourceUpdate, + Operation: event.Pruned, + Object: obj, }, } } @@ -292,8 +294,9 @@ func (po *PruneOptions) Prune(currentObjects []*resource.Info, eventChannel chan eventChannel <- event.Event{ Type: event.PruneType, PruneEvent: event.PruneEvent{ - Type: event.PruneEventResourceUpdate, - Object: pastGroupInfo.Object, + Type: event.PruneEventResourceUpdate, + Operation: event.Pruned, + Object: pastGroupInfo.Object, }, } }