diff --git a/cmd/kubectl/go.mod b/cmd/kubectl/go.mod index 8bdfa40ff..aee17a388 100644 --- a/cmd/kubectl/go.mod +++ b/cmd/kubectl/go.mod @@ -4,6 +4,7 @@ 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 c7bd92fc1..4b11bee51 100644 --- a/cmd/kubectl/kubectlcobra/grouping.go +++ b/cmd/kubectl/kubectlcobra/grouping.go @@ -6,6 +6,9 @@ package kubectlcobra import ( "fmt" + "hash/fnv" + "sort" + "strconv" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -13,7 +16,10 @@ import ( "k8s.io/cli-runtime/pkg/resource" ) -const GroupingLabel = "kustomize.k8s.io/group-id" +const ( + GroupingLabel = "kustomize.config.k8s.io/inventory-id" + GroupingHash = "kustomize.config.k8s.io/inventory-hash" +) // isGroupingObject returns true if the passed object has the // grouping label. @@ -102,11 +108,30 @@ func addInventoryToGroupingObj(infos []*resource.Info) error { if groupingObj == nil { return fmt.Errorf("Grouping object not found") } - err := unstructured.SetNestedStringMap(groupingObj.UnstructuredContent(), inventoryMap, "data") - if err != nil { - return err - } + if len(inventoryMap) > 0 { + // Adds the inventory map to the ConfigMap "data" section. + err := unstructured.SetNestedStringMap(groupingObj.UnstructuredContent(), + inventoryMap, "data") + if err != nil { + return err + } + // Adds the hash of the inventory strings as an annotation to the + // grouping object. Inventory strings must be sorted to make hash + // deterministic. + inventoryList := mapKeysToSlice(inventoryMap) + sort.Strings(inventoryList) + invHash, err := calcInventoryHash(inventoryList) + if err != nil { + return err + } + annotations := groupingObj.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations[GroupingHash] = strconv.FormatUint(uint64(invHash), 16) + groupingObj.SetAnnotations(annotations) + } return nil } @@ -143,3 +168,46 @@ func retrieveInventoryFromGroupingObj(infos []*resource.Info) ([]*Inventory, err } return inventory, nil } + +// calcInventoryHash returns an unsigned int32 representing the hash +// of the inventory strings. If there is an error writing bytes to +// the hash, then the error is returned; nil is returned otherwise. +// Used to quickly identify the set of resources in the grouping object. +func calcInventoryHash(inv []string) (uint32, error) { + h := fnv.New32a() + for _, is := range inv { + _, err := h.Write([]byte(is)) + if err != nil { + return uint32(0), err + } + } + return h.Sum32(), nil +} + +// retrieveInventoryHash takes a grouping object (encapsulated by +// a resource.Info), and returns the string representing the hash +// of the grouping inventory; returns empty string if the grouping +// object is not in Unstructured format, or if the hash annotation +// does not exist. +func retrieveInventoryHash(groupingInfo *resource.Info) string { + var invHash = "" + groupingObj, ok := groupingInfo.Object.(*unstructured.Unstructured) + if ok { + annotations := groupingObj.GetAnnotations() + if annotations != nil { + invHash = annotations[GroupingHash] + } + } + return invHash +} + +// mapKeysToSlice returns the map keys as a slice of strings. +func mapKeysToSlice(m map[string]string) []string { + s := make([]string, len(m)) + i := 0 + for k := range m { + s[i] = k + i++ + } + return s +} diff --git a/cmd/kubectl/kubectlcobra/grouping_test.go b/cmd/kubectl/kubectlcobra/grouping_test.go index 2aa957072..71dfabaeb 100644 --- a/cmd/kubectl/kubectlcobra/grouping_test.go +++ b/cmd/kubectl/kubectlcobra/grouping_test.go @@ -7,6 +7,8 @@ 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" @@ -90,6 +92,28 @@ var pod3Info = &resource.Info{ Object: &pod3, } +var nonUnstructuredGroupingObj = &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: groupingObjName, + Labels: map[string]string{ + GroupingLabel: "true", + }, + }, +} + +var nonUnstructuredGroupingInfo = &resource.Info{ + Namespace: testNamespace, + Name: groupingObjName, + Object: nonUnstructuredGroupingObj, +} + +var nilInfo = &resource.Info{ + Namespace: testNamespace, + Name: groupingObjName, + Object: nil, +} + func TestIsGroupingObject(t *testing.T) { tests := []struct { obj runtime.Object @@ -249,6 +273,14 @@ func TestAddRetrieveInventoryToFromGroupingObject(t *testing.T) { isError: true, }, // Grouping object without other objects is OK. + { + infos: []*resource.Info{groupingInfo, nilInfo}, + isError: true, + }, + { + infos: []*resource.Info{nonUnstructuredGroupingInfo}, + isError: true, + }, { infos: []*resource.Info{groupingInfo}, expected: []*Inventory{}, @@ -266,6 +298,7 @@ func TestAddRetrieveInventoryToFromGroupingObject(t *testing.T) { expected: []*Inventory{}, isError: true, }, + // Basic test case: one grouping object, one pod. { infos: []*resource.Info{groupingInfo, pod1Info}, expected: []*Inventory{ @@ -414,6 +447,15 @@ func TestAddRetrieveInventoryToFromGroupingObject(t *testing.T) { t.Errorf("Expected inventory (%s) not found", expected) } } + // If the grouping object has an inventory, check the + // grouping object has an inventory hash. + groupingInfo, exists := findGroupingObject(test.infos) + if exists && len(test.expected) > 0 { + invHash := retrieveInventoryHash(groupingInfo) + if len(invHash) == 0 { + t.Errorf("Grouping object missing inventory hash") + } + } } } }