cli-utils/pkg/object/objmetadata.go

310 lines
9.0 KiB
Go

// Copyright 2020 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
//
// ObjMetadata is the minimal set of information to
// uniquely identify an object. The four fields are:
//
// Group/Kind (NOTE: NOT version)
// Namespace
// Name
//
// We specifically do not use the "version", because
// the APIServer does not recognize a version as a
// different resource. This metadata is used to identify
// resources for pruning and teardown.
package object
import (
"fmt"
"hash/fnv"
"sort"
"strconv"
"strings"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/meta"
"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"
)
const (
// Separates inventory fields. This string is allowable as a
// ConfigMap key, but it is not allowed as a character in
// resource name.
fieldSeparator = "_"
// Transform colons in the RBAC resource names to double
// underscore.
colonTranscoded = "__"
)
// RBACGroupKind is a map of the RBAC resources. Needed since name validation
// is different than other k8s resources.
var RBACGroupKind = map[schema.GroupKind]bool{
{Group: rbacv1.GroupName, Kind: "Role"}: true,
{Group: rbacv1.GroupName, Kind: "ClusterRole"}: true,
{Group: rbacv1.GroupName, Kind: "RoleBinding"}: true,
{Group: rbacv1.GroupName, Kind: "ClusterRoleBinding"}: true,
}
// CoreV1Namespace is Namespace GVK.
var CoreV1Namespace = corev1.SchemeGroupVersion.WithKind("Namespace")
// ObjMetadata organizes and stores the indentifying information
// for an object. This struct (as a string) is stored in a
// inventory object to keep track of sets of applied objects.
type ObjMetadata struct {
Namespace string
Name string
GroupKind schema.GroupKind
}
// ObjMetas is a slice of ObjMetadata.
type ObjMetas []ObjMetadata
// Contains checks if the provided ObjMetadata exists in the ObjMetas slice.
func (oms ObjMetas) Contains(id ObjMetadata) bool {
for _, om := range oms {
if om == id {
return true
}
}
return false
}
// CreateObjMetadata returns a pointer to an ObjMetadata struct filled
// with the passed values. This function normalizes and validates the
// passed fields and returns an error for bad parameters.
func CreateObjMetadata(namespace string, name string, gk schema.GroupKind) (ObjMetadata, error) {
// Namespace can be empty, but name cannot.
name = strings.TrimSpace(name)
if name == "" {
return ObjMetadata{}, fmt.Errorf("empty name for object")
}
if gk.Empty() {
return ObjMetadata{}, fmt.Errorf("empty GroupKind for object")
}
return ObjMetadata{
Namespace: strings.TrimSpace(namespace),
Name: name,
GroupKind: gk,
}, nil
}
// ParseObjMetadata takes a string, splits it into its four fields,
// and returns an ObjMetadata struct storing the four fields.
// Example inventory string:
//
// test-namespace_test-name_apps_ReplicaSet
//
// Returns an error if unable to parse and create the ObjMetadata
// struct.
//
// NOTE: name field can contain double underscore (__), which represents
// a colon. RBAC resources can have this additional character (:) in their name.
func ParseObjMetadata(s string) (ObjMetadata, error) {
// Parse first field namespace
index := strings.Index(s, fieldSeparator)
if index == -1 {
return ObjMetadata{}, fmt.Errorf("unable to parse stored object metadata: %s", s)
}
namespace := s[:index]
s = s[index+1:]
// Next, parse last field kind
index = strings.LastIndex(s, fieldSeparator)
if index == -1 {
return ObjMetadata{}, fmt.Errorf("unable to parse stored object metadata: %s", s)
}
kind := s[index+1:]
s = s[:index]
// Next, parse next to last field group
index = strings.LastIndex(s, fieldSeparator)
if index == -1 {
return ObjMetadata{}, fmt.Errorf("unable to parse stored object metadata: %s", s)
}
group := s[index+1:]
// Finally, second field name. Name may contain colon transcoded as double underscore.
name := s[:index]
name = strings.ReplaceAll(name, colonTranscoded, ":")
// Check that there are no extra fields by search for fieldSeparator.
if strings.Contains(name, fieldSeparator) {
return ObjMetadata{}, fmt.Errorf("too many fields within: %s", s)
}
// Create the ObjMetadata object from the four parsed fields.
gk := schema.GroupKind{
Group: strings.TrimSpace(group),
Kind: strings.TrimSpace(kind),
}
return CreateObjMetadata(namespace, name, gk)
}
// Equals compares two ObjMetadata and returns true if they are equal. This does
// not contain any special treatment for the extensions API group.
func (o *ObjMetadata) Equals(other *ObjMetadata) bool {
if other == nil {
return false
}
return *o == *other
}
// String create a string version of the ObjMetadata struct. For RBAC resources,
// the "name" field transcodes ":" into double underscore for valid storing
// as the label of a ConfigMap.
func (o *ObjMetadata) String() string {
name := o.Name
if _, exists := RBACGroupKind[o.GroupKind]; exists {
name = strings.ReplaceAll(name, ":", colonTranscoded)
}
return fmt.Sprintf("%s%s%s%s%s%s%s",
o.Namespace, fieldSeparator,
name, fieldSeparator,
o.GroupKind.Group, fieldSeparator,
o.GroupKind.Kind)
}
// BuildObjectMetadata returns object metadata (ObjMetadata) for the
// passed objects (infos).
func InfosToObjMetas(infos []*resource.Info) ([]ObjMetadata, error) {
objMetas := []ObjMetadata{}
for _, info := range infos {
objMeta, err := InfoToObjMeta(info)
if err != nil {
return nil, err
}
objMetas = append(objMetas, objMeta)
}
return objMetas, nil
}
// InfoToObjMeta takes information from the provided info and
// returns an ObjMetadata that identifies the resource.
func InfoToObjMeta(info *resource.Info) (ObjMetadata, error) {
if info == nil || info.Object == nil {
return ObjMetadata{}, fmt.Errorf("attempting to transform info, but it is empty")
}
obj := info.Object
gk := obj.GetObjectKind().GroupVersionKind().GroupKind()
return CreateObjMetadata(info.Namespace, info.Name, gk)
}
func UnstructuredsToObjMetas(objs []*unstructured.Unstructured) []ObjMetadata {
var objMetas []ObjMetadata
for _, obj := range objs {
objMetas = append(objMetas, ObjMetadata{
Name: obj.GetName(),
Namespace: obj.GetNamespace(),
GroupKind: obj.GroupVersionKind().GroupKind(),
})
}
return objMetas
}
func UnstructuredToObjMeta(obj *unstructured.Unstructured) ObjMetadata {
return ObjMetadata{
Name: obj.GetName(),
Namespace: obj.GetNamespace(),
GroupKind: obj.GroupVersionKind().GroupKind(),
}
}
func RuntimeToObjMeta(obj runtime.Object) ObjMetadata {
accessor, _ := meta.Accessor(obj)
return ObjMetadata{
Namespace: accessor.GetNamespace(),
Name: accessor.GetName(),
GroupKind: obj.GetObjectKind().GroupVersionKind().GroupKind(),
}
}
// CalcHash returns a hash of the sorted strings from
// the object metadata, or an error if one occurred.
func Hash(objs []ObjMetadata) (string, error) {
objStrs := []string{}
for _, obj := range objs {
objStrs = append(objStrs, obj.String())
}
hashInt, err := calcHash(objStrs)
if err != nil {
return "", err
}
return strconv.FormatUint(uint64(hashInt), 16), nil
}
// calcHash returns an unsigned int32 representing the hash
// of the obj metadata 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 inventory object.
func calcHash(objs []string) (uint32, error) {
sort.Strings(objs)
h := fnv.New32a()
for _, obj := range objs {
_, err := h.Write([]byte(obj))
if err != nil {
return uint32(0), err
}
}
return h.Sum32(), nil
}
// SetDiff returns the slice of objects that exist in "a", but
// do not exist in "b" (A - B).
func SetDiff(setA []ObjMetadata, setB []ObjMetadata) []ObjMetadata {
// Create a map of the elements of A
m := map[string]ObjMetadata{}
for _, a := range setA {
m[a.String()] = a
}
// Remove from A each element of B
for _, b := range setB {
delete(m, b.String()) // OK to delete even if b not in m
}
// Create/return slice from the map of remaining items
diff := []ObjMetadata{}
for _, r := range m {
diff = append(diff, r)
}
return diff
}
// Union returns the slice of objects that is the set of unique
// items of the merging of set A and set B.
func Union(setA []ObjMetadata, setB []ObjMetadata) []ObjMetadata {
m := map[string]ObjMetadata{}
for _, a := range setA {
m[a.String()] = a
}
for _, b := range setB {
m[b.String()] = b
}
union := []ObjMetadata{}
for _, u := range m {
union = append(union, u)
}
return union
}
// SetEquals returns true if the slice of objects in setA equals
// the slice of objects in setB.
func SetEquals(setA []ObjMetadata, setB []ObjMetadata) bool {
mapA := map[string]bool{}
for _, a := range setA {
mapA[a.String()] = true
}
mapB := map[string]bool{}
for _, b := range setB {
mapB[b.String()] = true
}
if len(mapA) != len(mapB) {
return false
}
for b := range mapB {
if _, exists := mapA[b]; !exists {
return false
}
}
return true
}