...

Source file src/edge-infra.dev/pkg/k8s/object/objects.go

Documentation: edge-infra.dev/pkg/k8s/object

     1  package object
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"strings"
     8  
     9  	"github.com/google/go-cmp/cmp"
    10  	appsv1 "k8s.io/api/apps/v1"
    11  	hpav2beta1 "k8s.io/api/autoscaling/v2beta1"
    12  	hpav2beta2 "k8s.io/api/autoscaling/v2beta2"
    13  	corev1 "k8s.io/api/core/v1"
    14  	apiequality "k8s.io/apimachinery/pkg/api/equality"
    15  	"k8s.io/apimachinery/pkg/api/errors"
    16  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    17  	"k8s.io/apimachinery/pkg/runtime"
    18  	yamlutil "k8s.io/apimachinery/pkg/util/yaml"
    19  	"sigs.k8s.io/cli-utils/pkg/object"
    20  	"sigs.k8s.io/controller-runtime/pkg/client"
    21  	"sigs.k8s.io/yaml"
    22  )
    23  
    24  const (
    25  	fmtSeparator = "/"
    26  	Secret       = "Secret"
    27  	DefaultMask  = "*****"
    28  	DiffMask     = "******"
    29  )
    30  
    31  // FmtMetadata returns the object ID in the format <kind>/<namespace>/<name>.
    32  func FmtMetadata(obj object.ObjMetadata) string {
    33  	var builder strings.Builder
    34  	builder.WriteString(obj.GroupKind.Kind + fmtSeparator)
    35  	if obj.Namespace != "" {
    36  		builder.WriteString(obj.Namespace + fmtSeparator)
    37  	}
    38  	builder.WriteString(obj.Name)
    39  	return builder.String()
    40  }
    41  
    42  // FmtUnstructured returns the object ID in the format <kind>/<namespace>/<name>.
    43  func FmtUnstructured(obj *unstructured.Unstructured) string {
    44  	return FmtMetadata(object.UnstructuredToObjMetadata(obj))
    45  }
    46  
    47  // FmtUnstructuredList returns a line per object in the format <kind>/<namespace>/<name>.
    48  func FmtUnstructuredList(objects []*unstructured.Unstructured) string {
    49  	var b strings.Builder
    50  	for _, obj := range objects {
    51  		b.WriteString(FmtMetadata(object.UnstructuredToObjMetadata(obj)) + "\n")
    52  	}
    53  	return strings.TrimSuffix(b.String(), "\n")
    54  }
    55  
    56  func FmtObject(obj client.Object) string {
    57  	var builder strings.Builder
    58  	builder.WriteString(obj.GetObjectKind().GroupVersionKind().Kind + fmtSeparator)
    59  	if obj.GetNamespace() != "" {
    60  		builder.WriteString(obj.GetNamespace() + fmtSeparator)
    61  	}
    62  	builder.WriteString(obj.GetName())
    63  	return builder.String()
    64  }
    65  
    66  func FmtObjectList(objs []client.Object) string {
    67  	var b strings.Builder
    68  	for _, obj := range objs {
    69  		b.WriteString(FmtObject(obj) + "\n")
    70  	}
    71  	return strings.TrimSuffix(b.String(), "\n")
    72  }
    73  
    74  func GetNestedMap(object *unstructured.Unstructured) (map[string]interface{}, bool, error) {
    75  	dryRunData, foundDryRun, err := unstructured.NestedMap(object.Object, "data")
    76  	if err != nil {
    77  		return nil, foundDryRun, err
    78  	}
    79  
    80  	return dryRunData, foundDryRun, nil
    81  }
    82  
    83  func SetNestedMap(object *unstructured.Unstructured, data map[string]interface{}) error {
    84  	err := unstructured.SetNestedMap(object.Object, data, "data")
    85  	if err != nil {
    86  		return err
    87  	}
    88  
    89  	return nil
    90  }
    91  
    92  func CmpMaskData(currentData, futureData map[string]interface{}) (map[string]interface{}, map[string]interface{}) {
    93  	for k, currentVal := range currentData {
    94  		futureVal, ok := futureData[k]
    95  		if !ok {
    96  			// if the key is not in the existing object, we apply the default masking
    97  			currentData[k] = DefaultMask
    98  			continue
    99  		}
   100  		// if the key is in the existing object, we need to check if the value is the same
   101  		if cmp.Diff(currentVal, futureVal) != "" {
   102  			// if the value is different, we need to apply different masking
   103  			currentData[k] = DefaultMask
   104  			futureData[k] = DiffMask
   105  			continue
   106  		}
   107  		// if the value is the same, we apply the same masking
   108  		currentData[k] = DefaultMask
   109  		futureData[k] = DefaultMask
   110  	}
   111  
   112  	for k := range futureData {
   113  		if _, ok := currentData[k]; !ok {
   114  			// if the key is not in the dry run object, we apply the default masking
   115  			futureData[k] = DefaultMask
   116  		}
   117  	}
   118  
   119  	return currentData, futureData
   120  }
   121  
   122  // MaskSecret replaces the data key values with the given mask.
   123  func MaskSecret(data map[string]interface{}, object *unstructured.Unstructured, mask string) (*unstructured.Unstructured, error) {
   124  	for k := range data {
   125  		data[k] = mask
   126  	}
   127  
   128  	err := SetNestedMap(object, data)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  
   133  	return object, err
   134  }
   135  
   136  // ReadObject decodes a YAML or JSON document from the given reader into an unstructured Kubernetes API object.
   137  func ReadObject(r io.Reader) (*unstructured.Unstructured, error) {
   138  	reader := yamlutil.NewYAMLOrJSONDecoder(r, 2048)
   139  	obj := &unstructured.Unstructured{}
   140  	err := reader.Decode(obj)
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  
   145  	return obj, nil
   146  }
   147  
   148  // ReadObjects decodes the YAML or JSON documents from the given reader into unstructured Kubernetes API objects.
   149  // The documents which do not subscribe to the Kubernetes Object interface, are silently dropped from the result.
   150  func ReadObjects(r io.Reader) ([]*unstructured.Unstructured, error) {
   151  	reader := yamlutil.NewYAMLOrJSONDecoder(r, 2048)
   152  	objects := make([]*unstructured.Unstructured, 0)
   153  
   154  	for {
   155  		obj := &unstructured.Unstructured{}
   156  		err := reader.Decode(obj)
   157  		if err != nil {
   158  			if err == io.EOF {
   159  				break
   160  			}
   161  			return objects, err
   162  		}
   163  
   164  		if obj.IsList() {
   165  			err = obj.EachListItem(func(item runtime.Object) error {
   166  				obj := item.(*unstructured.Unstructured)
   167  				objects = append(objects, obj)
   168  				return nil
   169  			})
   170  			if err != nil {
   171  				return objects, err
   172  			}
   173  			continue
   174  		}
   175  
   176  		if IsKubernetesObject(obj) && !IsKustomization(obj) {
   177  			objects = append(objects, obj)
   178  		}
   179  	}
   180  
   181  	return objects, nil
   182  }
   183  
   184  // ToYAML encodes the given Kubernetes API object to YAML.
   185  func ToYAML(object *unstructured.Unstructured) string {
   186  	var builder strings.Builder
   187  	data, err := yaml.Marshal(object)
   188  	if err != nil {
   189  		return ""
   190  	}
   191  	builder.Write(data)
   192  	builder.WriteString("---\n")
   193  
   194  	return builder.String()
   195  }
   196  
   197  // ObjectsToYAML encodes the given Kubernetes API objects to a YAML multi-doc.
   198  func ObjectsToYAML(objects []*unstructured.Unstructured) (string, error) {
   199  	var builder strings.Builder
   200  	for _, obj := range objects {
   201  		data, err := yaml.Marshal(obj)
   202  		if err != nil {
   203  			return "", err
   204  		}
   205  		builder.Write(data)
   206  		builder.WriteString("---\n")
   207  	}
   208  	return builder.String(), nil
   209  }
   210  
   211  // ToJSON encodes the given Kubernetes API objects to a YAML multi-doc.
   212  func ToJSON(objects []*unstructured.Unstructured) (string, error) {
   213  	list := struct {
   214  		APIVersion string                       `json:"apiVersion,omitempty"`
   215  		Kind       string                       `json:"kind,omitempty"`
   216  		Items      []*unstructured.Unstructured `json:"items,omitempty"`
   217  	}{
   218  		APIVersion: "v1",
   219  		Kind:       "ListMeta",
   220  		Items:      objects,
   221  	}
   222  
   223  	data, err := json.MarshalIndent(list, "", "    ")
   224  	if err != nil {
   225  		return "", err
   226  	}
   227  
   228  	return string(data), nil
   229  }
   230  
   231  // IsClusterDefinition checks if the given object is a Kubernetes namespace or a custom resource definition.
   232  func IsClusterDefinition(object *unstructured.Unstructured) bool {
   233  	kind := object.GetKind()
   234  	switch strings.ToLower(kind) {
   235  	case "customresourcedefinition":
   236  		return true
   237  	case "namespace":
   238  		return true
   239  	}
   240  	return false
   241  }
   242  
   243  // IsKubernetesObject checks if the given object has the minimum required fields to be a Kubernetes object.
   244  func IsKubernetesObject(object *unstructured.Unstructured) bool {
   245  	if object.GetName() == "" || object.GetKind() == "" || object.GetAPIVersion() == "" {
   246  		return false
   247  	}
   248  	return true
   249  }
   250  
   251  // IsKustomization checks if the given object is a Kustomize config.
   252  func IsKustomization(object *unstructured.Unstructured) bool {
   253  	if object.GetKind() == "Kustomization" && object.GroupVersionKind().GroupKind().Group == "kustomize.config.k8s.io" {
   254  		return true
   255  	}
   256  	return false
   257  }
   258  
   259  // AnyInMetadata searches for the specified key-value pairs in labels and annotations,
   260  // returns true if at least one key-value pair matches.
   261  func AnyInMetadata(object *unstructured.Unstructured, metadata map[string]string) bool {
   262  	for key, val := range metadata {
   263  		if object.GetLabels()[key] == val || object.GetAnnotations()[key] == val {
   264  			return true
   265  		}
   266  	}
   267  	return false
   268  }
   269  
   270  // IsDeleted checks object and the error returned from Client.Get() to determine
   271  // if the object has been deleted. An object is considered deleted if any of
   272  // the following are true:
   273  //
   274  // - object's deletion timestamp is non-zero
   275  // - the error is NotFound or Gone
   276  func IsDeleted(object client.Object, err error) bool {
   277  	return !object.GetDeletionTimestamp().IsZero() ||
   278  		errors.IsNotFound(err) ||
   279  		errors.IsGone(err)
   280  }
   281  
   282  // SetNativeKindsDefaults implements workarounds for server-side apply upstream bugs affecting Kubernetes < 1.22
   283  // ContainerPort missing default TCP proto: https://github.com/kubernetes-sigs/structured-merge-diff/issues/130
   284  // ServicePort missing default TCP proto: https://github.com/kubernetes/kubernetes/pull/98576
   285  // PodSpec resources missing int to string conversion for e.g. 'cpu: 2'
   286  // secret.stringData key replacement add an extra key in the resulting data map: https://github.com/kubernetes/kubernetes/issues/108008
   287  func SetNativeKindsDefaults(objects []*unstructured.Unstructured) error { //nolint:gocyclo
   288  	var setProtoDefault = func(spec *corev1.PodSpec) {
   289  		for _, c := range spec.Containers {
   290  			for i, port := range c.Ports {
   291  				if port.Protocol == "" {
   292  					c.Ports[i].Protocol = "TCP"
   293  				}
   294  			}
   295  		}
   296  	}
   297  	for _, u := range objects {
   298  		switch u.GetAPIVersion() {
   299  		case "v1":
   300  			switch u.GetKind() {
   301  			case "Service":
   302  				var d corev1.Service
   303  				err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &d)
   304  				if err != nil {
   305  					return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
   306  				}
   307  
   308  				// set port protocol default
   309  				// workaround for: https://github.com/kubernetes-sigs/structured-merge-diff/issues/130
   310  				for i, port := range d.Spec.Ports {
   311  					if port.Protocol == "" {
   312  						d.Spec.Ports[i].Protocol = "TCP"
   313  					}
   314  				}
   315  
   316  				out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&d)
   317  				if err != nil {
   318  					return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
   319  				}
   320  				u.Object = out
   321  			case "Pod":
   322  				var d corev1.Pod
   323  				err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &d)
   324  				if err != nil {
   325  					return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
   326  				}
   327  
   328  				setProtoDefault(&d.Spec)
   329  
   330  				out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&d)
   331  				if err != nil {
   332  					return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
   333  				}
   334  				u.Object = out
   335  			case "Secret":
   336  				var s corev1.Secret
   337  				err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &s)
   338  				if err != nil {
   339  					return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
   340  				}
   341  				convertStringDataToData(&s)
   342  				out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&s)
   343  				if err != nil {
   344  					return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
   345  				}
   346  				u.Object = out
   347  			}
   348  
   349  		case "apps/v1":
   350  			switch u.GetKind() {
   351  			case "Deployment":
   352  				var d appsv1.Deployment
   353  				err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &d)
   354  				if err != nil {
   355  					return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
   356  				}
   357  
   358  				setProtoDefault(&d.Spec.Template.Spec)
   359  
   360  				out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&d)
   361  				if err != nil {
   362  					return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
   363  				}
   364  				u.Object = out
   365  			case "StatefulSet":
   366  				var d appsv1.StatefulSet
   367  				err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &d)
   368  				if err != nil {
   369  					return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
   370  				}
   371  
   372  				setProtoDefault(&d.Spec.Template.Spec)
   373  
   374  				out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&d)
   375  				if err != nil {
   376  					return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
   377  				}
   378  				u.Object = out
   379  			case "DaemonSet":
   380  				var d appsv1.DaemonSet
   381  				err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &d)
   382  				if err != nil {
   383  					return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
   384  				}
   385  
   386  				setProtoDefault(&d.Spec.Template.Spec)
   387  
   388  				out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&d)
   389  				if err != nil {
   390  					return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
   391  				}
   392  				u.Object = out
   393  			case "ReplicaSet":
   394  				var d appsv1.ReplicaSet
   395  				err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &d)
   396  				if err != nil {
   397  					return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
   398  				}
   399  
   400  				setProtoDefault(&d.Spec.Template.Spec)
   401  
   402  				out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&d)
   403  				if err != nil {
   404  					return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
   405  				}
   406  				u.Object = out
   407  			}
   408  		}
   409  
   410  		switch u.GetKind() {
   411  		case "HorizontalPodAutoscaler":
   412  			switch u.GetAPIVersion() {
   413  			case "autoscaling/v2beta1":
   414  				var d hpav2beta1.HorizontalPodAutoscaler
   415  				err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &d)
   416  				if err != nil {
   417  					return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
   418  				}
   419  				out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&d)
   420  				if err != nil {
   421  					return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
   422  				}
   423  				u.Object = out
   424  			case "autoscaling/v2beta2":
   425  				var d hpav2beta2.HorizontalPodAutoscaler
   426  				err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &d)
   427  				if err != nil {
   428  					return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
   429  				}
   430  				out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&d)
   431  				if err != nil {
   432  					return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
   433  				}
   434  				u.Object = out
   435  			}
   436  		}
   437  
   438  		// remove fields that are not supposed to be present in manifests
   439  		unstructured.RemoveNestedField(u.Object, "metadata", "creationTimestamp")
   440  
   441  		// remove status but for CRDs (kstatus wait doesn't work with empty status fields)
   442  		if u.GetKind() != "CustomResourceDefinition" {
   443  			unstructured.RemoveNestedField(u.Object, "status")
   444  		}
   445  	}
   446  	return nil
   447  }
   448  
   449  // Fix bug in server-side dry-run apply that duplicates the first item in the metrics array
   450  // and inserts an empty metric as the last item in the array.
   451  func FixHorizontalPodAutoscaler(object *unstructured.Unstructured) error {
   452  	if object.GetKind() == "HorizontalPodAutoscaler" {
   453  		switch object.GetAPIVersion() {
   454  		case "autoscaling/v2beta2":
   455  			var d hpav2beta2.HorizontalPodAutoscaler
   456  			err := runtime.DefaultUnstructuredConverter.FromUnstructured(object.Object, &d)
   457  			if err != nil {
   458  				return fmt.Errorf("%s validation error: %w", FmtUnstructured(object), err)
   459  			}
   460  
   461  			var metrics []hpav2beta2.MetricSpec
   462  			for _, metric := range d.Spec.Metrics {
   463  				found := false
   464  				for _, existing := range metrics {
   465  					if apiequality.Semantic.DeepEqual(metric, existing) {
   466  						found = true
   467  						break
   468  					}
   469  				}
   470  				if !found && metric.Type != "" {
   471  					metrics = append(metrics, metric)
   472  				}
   473  			}
   474  
   475  			d.Spec.Metrics = metrics
   476  
   477  			out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&d)
   478  			if err != nil {
   479  				return fmt.Errorf("%s validation error: %w", FmtUnstructured(object), err)
   480  			}
   481  			object.Object = out
   482  		}
   483  	}
   484  	return nil
   485  }
   486  
   487  func ContainsItemString(s []string, e string) bool {
   488  	for _, a := range s {
   489  		if a == e {
   490  			return true
   491  		}
   492  	}
   493  	return false
   494  }
   495  
   496  func convertStringDataToData(secret *corev1.Secret) {
   497  	// StringData overwrites Data
   498  	if len(secret.StringData) > 0 {
   499  		if secret.Data == nil {
   500  			secret.Data = map[string][]byte{}
   501  		}
   502  		for k, v := range secret.StringData {
   503  			secret.Data[k] = []byte(v)
   504  		}
   505  
   506  		secret.StringData = nil
   507  	}
   508  }
   509  

View as plain text