From 595e41a3ec8d2754464af13d97b628a9a20a7d4a Mon Sep 17 00:00:00 2001 From: Sean Sullivan Date: Thu, 9 Jan 2020 16:01:07 -0800 Subject: [PATCH] Adds functions to add/retrieve inventory to/from grouping object --- cmd/kubectl/go.mod | 1 - cmd/kubectl/kubectlcobra/grouping.go | 86 ++++++++ cmd/kubectl/kubectlcobra/grouping_test.go | 243 ++++++++++++++++++++-- 3 files changed, 309 insertions(+), 21 deletions(-) diff --git a/cmd/kubectl/go.mod b/cmd/kubectl/go.mod index aee17a388..8bdfa40ff 100644 --- a/cmd/kubectl/go.mod +++ b/cmd/kubectl/go.mod @@ -4,7 +4,6 @@ go 1.13 require ( github.com/spf13/cobra v0.0.5 - k8s.io/api v0.17.0 k8s.io/apimachinery v0.17.0 k8s.io/cli-runtime v0.17.0 k8s.io/client-go v0.17.0 diff --git a/cmd/kubectl/kubectlcobra/grouping.go b/cmd/kubectl/kubectlcobra/grouping.go index 568682093..c7bd92fc1 100644 --- a/cmd/kubectl/kubectlcobra/grouping.go +++ b/cmd/kubectl/kubectlcobra/grouping.go @@ -5,7 +5,10 @@ package kubectlcobra import ( + "fmt" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/resource" ) @@ -57,3 +60,86 @@ func sortGroupingObject(infos []*resource.Info) bool { } return false } + +// Adds the inventory of all objects (passed as infos) to the +// grouping object. Returns an error if a grouping object does not +// exist, or we are unable to successfully add the inventory to +// the grouping object; nil otherwise. Each object is in +// unstructured.Unstructured format. +func addInventoryToGroupingObj(infos []*resource.Info) error { + + // Iterate through the objects (infos), creating an Inventory struct + // as metadata for the object, or if it's the grouping object, store it. + var groupingObj *unstructured.Unstructured + inventoryMap := map[string]string{} + for _, info := range infos { + obj := info.Object + if isGroupingObject(obj) { + // If we have more than one grouping object--error. + if groupingObj != nil { + return fmt.Errorf("Error--applying more than one grouping object.") + } + var ok bool + groupingObj, ok = obj.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Grouping object is not an Unstructured: %#v", groupingObj) + } + } else { + if obj == nil { + return fmt.Errorf("Creating inventory; object is nil") + } + gk := obj.GetObjectKind().GroupVersionKind().GroupKind() + inventory, err := createInventory(info.Namespace, info.Name, gk) + if err != nil { + return err + } + inventoryMap[inventory.String()] = "" + } + } + + // If we've found the grouping object, store the object metadata inventory + // in the grouping config map. + if groupingObj == nil { + return fmt.Errorf("Grouping object not found") + } + err := unstructured.SetNestedStringMap(groupingObj.UnstructuredContent(), inventoryMap, "data") + if err != nil { + return err + } + + return nil +} + +// retrieveInventoryFromGroupingObj returns a slice of pointers to the +// inventory metadata. This function finds the grouping object, then +// parses the stored resource metadata into Inventory structs. Returns +// an error if there is a problem parsing the data into Inventory +// structs, or if the grouping object is not in Unstructured format; nil +// otherwise. If a grouping object does not exist, or it does not have a +// "data" map, then returns an empty slice and no error. +func retrieveInventoryFromGroupingObj(infos []*resource.Info) ([]*Inventory, error) { + inventory := []*Inventory{} + groupingInfo, exists := findGroupingObject(infos) + if exists { + groupingObj, ok := groupingInfo.Object.(*unstructured.Unstructured) + if !ok { + err := fmt.Errorf("Grouping object is not an Unstructured: %#v", groupingObj) + return inventory, err + } + invMap, exists, err := unstructured.NestedStringMap(groupingObj.Object, "data") + if err != nil { + err := fmt.Errorf("Error retrieving inventory from grouping object.") + return inventory, err + } + if exists { + for invStr := range invMap { + inv, err := parseInventory(invStr) + if err != nil { + return inventory, err + } + inventory = append(inventory, inv) + } + } + } + return inventory, nil +} diff --git a/cmd/kubectl/kubectlcobra/grouping_test.go b/cmd/kubectl/kubectlcobra/grouping_test.go index 12911cf52..2aa957072 100644 --- a/cmd/kubectl/kubectlcobra/grouping_test.go +++ b/cmd/kubectl/kubectlcobra/grouping_test.go @@ -7,9 +7,9 @@ package kubectlcobra import ( "testing" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/resource" ) @@ -19,12 +19,16 @@ var pod1Name = "pod-1" var pod2Name = "pod-2" var pod3Name = "pod-3" -var groupingObj = corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: groupingObjName, - Labels: map[string]string{ - GroupingLabel: "true", +var groupingObj = unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": groupingObjName, + "namespace": testNamespace, + "labels": map[string]interface{}{ + GroupingLabel: "true", + }, }, }, } @@ -35,10 +39,14 @@ var groupingInfo = &resource.Info{ Object: &groupingObj, } -var pod1 = corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: pod1Name, +var pod1 = unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]interface{}{ + "name": pod1Name, + "namespace": testNamespace, + }, }, } @@ -48,10 +56,14 @@ var pod1Info = &resource.Info{ Object: &pod1, } -var pod2 = corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: pod2Name, +var pod2 = unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]interface{}{ + "name": pod2Name, + "namespace": testNamespace, + }, }, } @@ -61,10 +73,14 @@ var pod2Info = &resource.Info{ Object: &pod2, } -var pod3 = corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: pod3Name, +var pod3 = unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]interface{}{ + "name": pod3Name, + "namespace": testNamespace, + }, }, } @@ -215,3 +231,190 @@ func TestSortGroupingObject(t *testing.T) { } } } + +func TestAddRetrieveInventoryToFromGroupingObject(t *testing.T) { + tests := []struct { + infos []*resource.Info + expected []*Inventory + isError bool + }{ + // No grouping object is an error. + { + infos: []*resource.Info{}, + isError: true, + }, + // No grouping object is an error. + { + infos: []*resource.Info{pod1Info, pod2Info}, + isError: true, + }, + // Grouping object without other objects is OK. + { + infos: []*resource.Info{groupingInfo}, + expected: []*Inventory{}, + isError: false, + }, + // More than one grouping object is an error. + { + infos: []*resource.Info{groupingInfo, groupingInfo}, + expected: []*Inventory{}, + isError: true, + }, + // More than one grouping object is an error. + { + infos: []*resource.Info{groupingInfo, pod1Info, groupingInfo}, + expected: []*Inventory{}, + isError: true, + }, + { + infos: []*resource.Info{groupingInfo, pod1Info}, + expected: []*Inventory{ + &Inventory{ + Namespace: testNamespace, + Name: pod1Name, + GroupKind: schema.GroupKind{ + Group: "", + Kind: "Pod", + }, + }, + }, + isError: false, + }, + { + infos: []*resource.Info{pod1Info, groupingInfo}, + expected: []*Inventory{ + &Inventory{ + Namespace: testNamespace, + Name: pod1Name, + GroupKind: schema.GroupKind{ + Group: "", + Kind: "Pod", + }, + }, + }, + isError: false, + }, + { + infos: []*resource.Info{pod1Info, pod2Info, groupingInfo, pod3Info}, + expected: []*Inventory{ + &Inventory{ + Namespace: testNamespace, + Name: pod1Name, + GroupKind: schema.GroupKind{ + Group: "", + Kind: "Pod", + }, + }, + &Inventory{ + Namespace: testNamespace, + Name: pod2Name, + GroupKind: schema.GroupKind{ + Group: "", + Kind: "Pod", + }, + }, + &Inventory{ + Namespace: testNamespace, + Name: pod3Name, + GroupKind: schema.GroupKind{ + Group: "", + Kind: "Pod", + }, + }, + }, + isError: false, + }, + { + infos: []*resource.Info{pod1Info, pod2Info, pod3Info, groupingInfo}, + expected: []*Inventory{ + &Inventory{ + Namespace: testNamespace, + Name: pod1Name, + GroupKind: schema.GroupKind{ + Group: "", + Kind: "Pod", + }, + }, + &Inventory{ + Namespace: testNamespace, + Name: pod2Name, + GroupKind: schema.GroupKind{ + Group: "", + Kind: "Pod", + }, + }, + &Inventory{ + Namespace: testNamespace, + Name: pod3Name, + GroupKind: schema.GroupKind{ + Group: "", + Kind: "Pod", + }, + }, + }, + isError: false, + }, + { + infos: []*resource.Info{groupingInfo, pod1Info, pod2Info, pod3Info}, + expected: []*Inventory{ + &Inventory{ + Namespace: testNamespace, + Name: pod1Name, + GroupKind: schema.GroupKind{ + Group: "", + Kind: "Pod", + }, + }, + &Inventory{ + Namespace: testNamespace, + Name: pod2Name, + GroupKind: schema.GroupKind{ + Group: "", + Kind: "Pod", + }, + }, + &Inventory{ + Namespace: testNamespace, + Name: pod3Name, + GroupKind: schema.GroupKind{ + Group: "", + Kind: "Pod", + }, + }, + }, + isError: false, + }, + } + + for _, test := range tests { + err := addInventoryToGroupingObj(test.infos) + if test.isError && err == nil { + t.Errorf("Should have produced an error, but returned none.") + } + if !test.isError { + if err != nil { + t.Errorf("Received error when expecting none (%s)\n", err) + } else { + retrieved, err := retrieveInventoryFromGroupingObj(test.infos) + if err != nil { + t.Errorf("Error retrieving inventory: %s\n", err) + } + if len(test.expected) != len(retrieved) { + t.Errorf("Expected inventory for %d resources, actual %d", + len(test.expected), len(retrieved)) + } + for _, expected := range test.expected { + found := false + for _, actual := range retrieved { + if expected.Equals(actual) { + found = true + } + } + if !found { + t.Errorf("Expected inventory (%s) not found", expected) + } + } + } + } + } +}