istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/object/objects.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  /*
    16  Package manifest provides functions for going between in-memory k8s objects (unstructured.Unstructured) and their JSON
    17  or YAML representations.
    18  */
    19  package object
    20  
    21  import (
    22  	"bytes"
    23  	"fmt"
    24  	"io"
    25  	"sort"
    26  	"strings"
    27  
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	"k8s.io/apimachinery/pkg/runtime/schema"
    30  	"k8s.io/apimachinery/pkg/runtime/serializer/json"
    31  	"k8s.io/apimachinery/pkg/util/intstr"
    32  	k8syaml "k8s.io/apimachinery/pkg/util/yaml"
    33  	"sigs.k8s.io/yaml"
    34  
    35  	"istio.io/istio/operator/pkg/apis/istio/v1alpha1"
    36  	"istio.io/istio/operator/pkg/helm"
    37  	names "istio.io/istio/operator/pkg/name"
    38  	"istio.io/istio/operator/pkg/tpath"
    39  	"istio.io/istio/operator/pkg/util"
    40  	"istio.io/istio/pkg/log"
    41  )
    42  
    43  const (
    44  	// YAMLSeparator is a separator for multi-document YAML files.
    45  	YAMLSeparator = "\n---\n"
    46  )
    47  
    48  // K8sObject is an in-memory representation of a k8s object, used for moving between different representations
    49  // (Unstructured, JSON, YAML) with cached rendering.
    50  type K8sObject struct {
    51  	object *unstructured.Unstructured
    52  
    53  	Group     string
    54  	Kind      string
    55  	Name      string
    56  	Namespace string
    57  
    58  	json []byte
    59  	yaml []byte
    60  }
    61  
    62  // NewK8sObject creates a new K8sObject and returns a ptr to it.
    63  func NewK8sObject(u *unstructured.Unstructured, json, yaml []byte) *K8sObject {
    64  	o := &K8sObject{
    65  		object: u,
    66  		json:   json,
    67  		yaml:   yaml,
    68  	}
    69  
    70  	gvk := u.GetObjectKind().GroupVersionKind()
    71  	o.Group = gvk.Group
    72  	o.Kind = gvk.Kind
    73  	o.Name = u.GetName()
    74  	o.Namespace = u.GetNamespace()
    75  
    76  	return o
    77  }
    78  
    79  // Hash returns a unique, insecure hash based on kind, namespace and name.
    80  func Hash(kind, namespace, name string) string {
    81  	switch kind {
    82  	case names.ClusterRoleStr, names.ClusterRoleBindingStr:
    83  		namespace = ""
    84  	}
    85  	return strings.Join([]string{kind, namespace, name}, ":")
    86  }
    87  
    88  // FromHash parses kind, namespace and name from a hash.
    89  func FromHash(hash string) (kind, namespace, name string) {
    90  	hv := strings.Split(hash, ":")
    91  	if len(hv) != 3 {
    92  		return "Bad hash string: " + hash, "", ""
    93  	}
    94  	kind, namespace, name = hv[0], hv[1], hv[2]
    95  	return
    96  }
    97  
    98  // HashNameKind returns a unique, insecure hash based on kind and name.
    99  func HashNameKind(kind, name string) string {
   100  	return strings.Join([]string{kind, name}, ":")
   101  }
   102  
   103  // ParseJSONToK8sObject parses JSON to an K8sObject.
   104  func ParseJSONToK8sObject(json []byte) (*K8sObject, error) {
   105  	o, _, err := unstructured.UnstructuredJSONScheme.Decode(json, nil, nil)
   106  	if err != nil {
   107  		return nil, fmt.Errorf("error parsing json into unstructured object: %v", err)
   108  	}
   109  
   110  	u, ok := o.(*unstructured.Unstructured)
   111  	if !ok {
   112  		return nil, fmt.Errorf("parsed unexpected type %T", o)
   113  	}
   114  
   115  	return NewK8sObject(u, json, nil), nil
   116  }
   117  
   118  // ParseYAMLToK8sObject parses YAML to an Object.
   119  func ParseYAMLToK8sObject(yaml []byte) (*K8sObject, error) {
   120  	objects, err := ParseK8sObjectsFromYAMLManifest(string(yaml))
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  	if len(objects) > 1 {
   125  		return nil, fmt.Errorf("expect one object, actually: %d", len(objects))
   126  	}
   127  	if len(objects) == 0 || objects[0] == nil {
   128  		return nil, fmt.Errorf("decoding object %v: %v", string(yaml), "no object found")
   129  	}
   130  	return objects[0], nil
   131  }
   132  
   133  // UnstructuredObject exposes the raw object, primarily for testing
   134  func (o *K8sObject) UnstructuredObject() *unstructured.Unstructured {
   135  	return o.object
   136  }
   137  
   138  // ResolveK8sConflict - This method resolves k8s object possible
   139  // conflicting settings. Which K8sObjects may need such method
   140  // depends on the type of the K8sObject.
   141  func (o *K8sObject) ResolveK8sConflict() *K8sObject {
   142  	if o.Kind == names.PDBStr {
   143  		return resolvePDBConflict(o)
   144  	}
   145  	return o
   146  }
   147  
   148  // Unstructured exposes the raw object content, primarily for testing
   149  func (o *K8sObject) Unstructured() map[string]any {
   150  	return o.UnstructuredObject().UnstructuredContent()
   151  }
   152  
   153  // Container returns a container subtree for Deployment objects if one is found, or nil otherwise.
   154  func (o *K8sObject) Container(name string) map[string]any {
   155  	u := o.Unstructured()
   156  	path := fmt.Sprintf("spec.template.spec.containers.[name:%s]", name)
   157  	node, f, err := tpath.GetPathContext(u, util.PathFromString(path), false)
   158  	if err == nil && f {
   159  		// Must be the type from the schema.
   160  		return node.Node.(map[string]any)
   161  	}
   162  	return nil
   163  }
   164  
   165  // GroupVersionKind returns the GroupVersionKind for the K8sObject
   166  func (o *K8sObject) GroupVersionKind() schema.GroupVersionKind {
   167  	return o.object.GroupVersionKind()
   168  }
   169  
   170  // Version returns the APIVersion of the K8sObject
   171  func (o *K8sObject) Version() string {
   172  	return o.object.GetAPIVersion()
   173  }
   174  
   175  // Hash returns a unique hash for the K8sObject
   176  func (o *K8sObject) Hash() string {
   177  	return Hash(o.Kind, o.Namespace, o.Name)
   178  }
   179  
   180  // HashNameKind returns a hash for the K8sObject based on the name and kind only.
   181  func (o *K8sObject) HashNameKind() string {
   182  	return HashNameKind(o.Kind, o.Name)
   183  }
   184  
   185  // JSON returns a JSON representation of the K8sObject, using an internal cache.
   186  func (o *K8sObject) JSON() ([]byte, error) {
   187  	if o.json != nil {
   188  		return o.json, nil
   189  	}
   190  
   191  	b, err := o.object.MarshalJSON()
   192  	if err != nil {
   193  		return nil, err
   194  	}
   195  	return b, nil
   196  }
   197  
   198  // YAML returns a YAML representation of the K8sObject, using an internal cache.
   199  func (o *K8sObject) YAML() ([]byte, error) {
   200  	if o == nil {
   201  		return nil, nil
   202  	}
   203  	if o.yaml != nil {
   204  		return o.yaml, nil
   205  	}
   206  	oj, err := o.JSON()
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  	o.json = oj
   211  	y, err := yaml.JSONToYAML(oj)
   212  	if err != nil {
   213  		return nil, err
   214  	}
   215  	o.yaml = y
   216  	return y, nil
   217  }
   218  
   219  // YAMLDebugString returns a YAML representation of the K8sObject, or an error string if the K8sObject cannot be rendered to YAML.
   220  func (o *K8sObject) YAMLDebugString() string {
   221  	y, err := o.YAML()
   222  	if err != nil {
   223  		return err.Error()
   224  	}
   225  	return string(y)
   226  }
   227  
   228  // K8sObjects holds a collection of k8s objects, so that we can filter / sequence them
   229  type K8sObjects []*K8sObject
   230  
   231  // String implements the Stringer interface.
   232  func (os K8sObjects) String() string {
   233  	var out []string
   234  	for _, oo := range os {
   235  		out = append(out, oo.YAMLDebugString())
   236  	}
   237  	return strings.Join(out, helm.YAMLSeparator)
   238  }
   239  
   240  // Keys returns a slice with the keys of os.
   241  func (os K8sObjects) Keys() []string {
   242  	out := make([]string, 0, len(os))
   243  	for _, oo := range os {
   244  		out = append(out, oo.Hash())
   245  	}
   246  	return out
   247  }
   248  
   249  // UnstructuredItems returns the list of items of unstructured.Unstructured.
   250  func (os K8sObjects) UnstructuredItems() []unstructured.Unstructured {
   251  	usList := make([]unstructured.Unstructured, 0, len(os))
   252  	for _, obj := range os {
   253  		usList = append(usList, *obj.UnstructuredObject())
   254  	}
   255  	return usList
   256  }
   257  
   258  // ParseK8sObjectsFromYAMLManifest returns a K8sObjects representation of manifest.
   259  func ParseK8sObjectsFromYAMLManifest(manifest string) (K8sObjects, error) {
   260  	return ParseK8sObjectsFromYAMLManifestFailOption(manifest, true)
   261  }
   262  
   263  // ParseK8sObjectsFromYAMLManifestFailOption returns a K8sObjects representation of manifest. Continues parsing when a bad object
   264  // is found if failOnError is set to false.
   265  func ParseK8sObjectsFromYAMLManifestFailOption(manifest string, failOnError bool) (K8sObjects, error) {
   266  	jsonDecoder := k8syaml.NewYAMLToJSONDecoder(bytes.NewReader([]byte(manifest)))
   267  	var objects K8sObjects
   268  
   269  	wrapErr := func(err error) error {
   270  		return fmt.Errorf("failed to parse YAML to a k8s object: %v", err)
   271  	}
   272  
   273  	s := json.NewSerializerWithOptions(json.DefaultMetaFactory, nil, nil, json.SerializerOptions{
   274  		Yaml:   true,
   275  		Pretty: true,
   276  		Strict: true,
   277  	})
   278  	for {
   279  		var obj unstructured.Unstructured
   280  		if err := jsonDecoder.Decode(&obj); err != nil {
   281  			if err == io.EOF {
   282  				break
   283  			}
   284  			err = wrapErr(err)
   285  			if failOnError {
   286  				return nil, err
   287  			}
   288  			log.Error(err.Error())
   289  			continue
   290  		}
   291  		if obj.Object == nil {
   292  			continue
   293  		}
   294  
   295  		if !isValidKubernetesObject(obj) {
   296  			if failOnError {
   297  				err := wrapErr(fmt.Errorf("failed to parse YAML to a k8s object: object is an invalid k8s object: %v", obj))
   298  				return nil, err
   299  			}
   300  		}
   301  
   302  		// Convert the unstructured object back into YAML, without comments
   303  		var buf bytes.Buffer
   304  		if err := s.Encode(&obj, &buf); err != nil {
   305  			err = wrapErr(err)
   306  			if failOnError {
   307  				return nil, err
   308  			}
   309  			log.Error(err.Error())
   310  			continue
   311  		}
   312  		cleanedYaml := buf.String()
   313  
   314  		k8sObj := NewK8sObject(&obj, nil, []byte(cleanedYaml))
   315  		if k8sObj.Valid() {
   316  			objects = append(objects, k8sObj)
   317  		}
   318  	}
   319  	return objects, nil
   320  }
   321  
   322  // YAMLManifest returns a YAML representation of K8sObjects os.
   323  func (os K8sObjects) YAMLManifest() (string, error) {
   324  	var b bytes.Buffer
   325  
   326  	for i, item := range os {
   327  		if i != 0 {
   328  			if _, err := b.WriteString("\n\n"); err != nil {
   329  				return "", err
   330  			}
   331  		}
   332  		ym, err := item.YAML()
   333  		if err != nil {
   334  			return "", fmt.Errorf("error building yaml: %v", err)
   335  		}
   336  		if _, err := b.Write(ym); err != nil {
   337  			return "", err
   338  		}
   339  		if _, err := b.WriteString(YAMLSeparator); err != nil {
   340  			return "", err
   341  		}
   342  
   343  	}
   344  
   345  	return b.String(), nil
   346  }
   347  
   348  // Sort will order the items in K8sObjects in order of score, group, kind, name.  The intent is to
   349  // have a deterministic ordering in which K8sObjects are applied.
   350  func (os K8sObjects) Sort(score func(o *K8sObject) int) {
   351  	sort.Slice(os, func(i, j int) bool {
   352  		iScore := score(os[i])
   353  		jScore := score(os[j])
   354  		return iScore < jScore ||
   355  			(iScore == jScore &&
   356  				os[i].Group < os[j].Group) ||
   357  			(iScore == jScore &&
   358  				os[i].Group == os[j].Group &&
   359  				os[i].Kind < os[j].Kind) ||
   360  			(iScore == jScore &&
   361  				os[i].Group == os[j].Group &&
   362  				os[i].Kind == os[j].Kind &&
   363  				os[i].Name < os[j].Name)
   364  	})
   365  }
   366  
   367  // ToMap returns a map of K8sObject hash to K8sObject.
   368  func (os K8sObjects) ToMap() map[string]*K8sObject {
   369  	ret := make(map[string]*K8sObject)
   370  	for _, oo := range os {
   371  		if oo.Valid() {
   372  			ret[oo.Hash()] = oo
   373  		}
   374  	}
   375  	return ret
   376  }
   377  
   378  // ToNameKindMap returns a map of K8sObject name/kind hash to K8sObject.
   379  func (os K8sObjects) ToNameKindMap() map[string]*K8sObject {
   380  	ret := make(map[string]*K8sObject)
   381  	for _, oo := range os {
   382  		if oo.Valid() {
   383  			ret[oo.HashNameKind()] = oo
   384  		}
   385  	}
   386  	return ret
   387  }
   388  
   389  // Valid checks returns true if Kind of K8sObject is not empty.
   390  func (o *K8sObject) Valid() bool {
   391  	return o.Kind != ""
   392  }
   393  
   394  // FullName returns namespace/name of K8s object
   395  func (o *K8sObject) FullName() string {
   396  	return fmt.Sprintf("%s/%s", o.Namespace, o.Name)
   397  }
   398  
   399  // Equal returns true if o and other are both valid and equal to each other.
   400  func (o *K8sObject) Equal(other *K8sObject) bool {
   401  	if o == nil {
   402  		return other == nil
   403  	}
   404  	if other == nil {
   405  		return o == nil
   406  	}
   407  
   408  	ay, err := o.YAML()
   409  	if err != nil {
   410  		return false
   411  	}
   412  	by, err := other.YAML()
   413  	if err != nil {
   414  		return false
   415  	}
   416  
   417  	return util.IsYAMLEqual(string(ay), string(by))
   418  }
   419  
   420  // DefaultObjectOrder is default sorting function used to sort k8s objects.
   421  func DefaultObjectOrder() func(o *K8sObject) int {
   422  	return func(o *K8sObject) int {
   423  		gk := o.Group + "/" + o.Kind
   424  		switch {
   425  		// Create CRDs asap - both because they are slow and because we will likely create instances of them soon
   426  		case gk == "apiextensions.k8s.io/CustomResourceDefinition":
   427  			return -1000
   428  
   429  			// We need to create ServiceAccounts, Roles before we bind them with a RoleBinding
   430  		case gk == "/ServiceAccount" || gk == "rbac.authorization.k8s.io/ClusterRole":
   431  			return 1
   432  		case gk == "rbac.authorization.k8s.io/ClusterRoleBinding":
   433  			return 2
   434  
   435  			// validatingwebhookconfiguration is configured to FAIL-OPEN in the default install. For the
   436  			// re-install case we want to apply the validatingwebhookconfiguration first to reset any
   437  			// orphaned validatingwebhookconfiguration that is FAIL-CLOSE.
   438  		case gk == "admissionregistration.k8s.io/ValidatingWebhookConfiguration":
   439  			return 3
   440  
   441  			// Pods might need configmap or secrets - avoid backoff by creating them first
   442  		case gk == "/ConfigMap" || gk == "/Secrets":
   443  			return 100
   444  
   445  			// Create the pods after we've created other things they might be waiting for
   446  		case gk == "extensions/Deployment" || gk == "apps/Deployment":
   447  			return 1000
   448  
   449  			// Autoscalers typically act on a deployment
   450  		case gk == "autoscaling/HorizontalPodAutoscaler":
   451  			return 1001
   452  
   453  			// Create services late - after pods have been started
   454  		case gk == "/Service":
   455  			return 10000
   456  
   457  		default:
   458  			return 1000
   459  		}
   460  	}
   461  }
   462  
   463  func ObjectsNotInLists(objects K8sObjects, lists ...K8sObjects) K8sObjects {
   464  	var ret K8sObjects
   465  
   466  	filterMap := make(map[*K8sObject]bool)
   467  	for _, list := range lists {
   468  		for _, object := range list {
   469  			filterMap[object] = true
   470  		}
   471  	}
   472  
   473  	for _, o := range objects {
   474  		if !filterMap[o] {
   475  			ret = append(ret, o)
   476  		}
   477  	}
   478  	return ret
   479  }
   480  
   481  // KindObjects returns the subset of objs with the given kind.
   482  func KindObjects(objs K8sObjects, kind string) K8sObjects {
   483  	var ret K8sObjects
   484  	for _, o := range objs {
   485  		if o.Kind == kind {
   486  			ret = append(ret, o)
   487  		}
   488  	}
   489  	return ret
   490  }
   491  
   492  // ParseK8SYAMLToIstioOperator parses a IstioOperator CustomResource YAML string and unmarshals in into
   493  // an IstioOperatorSpec object. It returns the object and an API group/version with it.
   494  func ParseK8SYAMLToIstioOperator(yml string) (*v1alpha1.IstioOperator, *schema.GroupVersionKind, error) {
   495  	o, err := ParseYAMLToK8sObject([]byte(yml))
   496  	if err != nil {
   497  		return nil, nil, err
   498  	}
   499  	iop := &v1alpha1.IstioOperator{}
   500  	if err := yaml.UnmarshalStrict([]byte(yml), iop); err != nil {
   501  		return nil, nil, err
   502  	}
   503  	gvk := o.GroupVersionKind()
   504  	v1alpha1.SetNamespace(iop.Spec, o.Namespace)
   505  	return iop, &gvk, nil
   506  }
   507  
   508  // AllObjectHashes returns a map with object hashes of all the objects contained in cmm as the keys.
   509  func AllObjectHashes(m string) map[string]bool {
   510  	ret := make(map[string]bool)
   511  	objs, err := ParseK8sObjectsFromYAMLManifest(m)
   512  	if err != nil {
   513  		log.Error(err.Error())
   514  	}
   515  	for _, o := range objs {
   516  		ret[o.Hash()] = true
   517  	}
   518  
   519  	return ret
   520  }
   521  
   522  // resolvePDBConflict When user uses both minAvailable and
   523  // maxUnavailable to configure istio instances, these two
   524  // parameters are mutually exclusive, care must be taken
   525  // to resolve the issue
   526  func resolvePDBConflict(o *K8sObject) *K8sObject {
   527  	if o.json == nil {
   528  		return o
   529  	}
   530  	if o.object.Object["spec"] == nil {
   531  		return o
   532  	}
   533  	spec := o.object.Object["spec"].(map[string]any)
   534  	isDefault := func(item any) bool {
   535  		var ii intstr.IntOrString
   536  		switch item := item.(type) {
   537  		case int:
   538  			ii = intstr.FromInt32(int32(item))
   539  		case int64:
   540  			ii = intstr.FromInt32(int32(item))
   541  		case string:
   542  			ii = intstr.FromString(item)
   543  		default:
   544  			ii = intstr.FromInt32(0)
   545  		}
   546  		intVal, err := intstr.GetScaledValueFromIntOrPercent(&ii, 100, false)
   547  		if err != nil || intVal == 0 {
   548  			return true
   549  		}
   550  		return false
   551  	}
   552  	if spec["maxUnavailable"] != nil && spec["minAvailable"] != nil {
   553  		// When both maxUnavailable and minAvailable present and
   554  		// neither has value 0, this is considered a conflict,
   555  		// then maxUnavailable will take precedence.
   556  		if !isDefault(spec["maxUnavailable"]) && !isDefault(spec["minAvailable"]) {
   557  			delete(spec, "minAvailable")
   558  			// Make sure that the json and yaml representation of the object
   559  			// is consistent with the changed object
   560  			o.json = nil
   561  			o.json, _ = o.JSON()
   562  			if o.yaml != nil {
   563  				o.yaml = nil
   564  				o.yaml, _ = o.YAML()
   565  			}
   566  		}
   567  	}
   568  	return o
   569  }
   570  
   571  func isValidKubernetesObject(obj unstructured.Unstructured) bool {
   572  	if _, ok := obj.Object["apiVersion"]; !ok {
   573  		return false
   574  	}
   575  	if _, ok := obj.Object["kind"]; !ok {
   576  		return false
   577  	}
   578  	return true
   579  }