...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/resourceoverrides/utils.go

Documentation: github.com/GoogleCloudPlatform/k8s-config-connector/pkg/resourceoverrides

     1  // Copyright 2022 Google LLC
     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  package resourceoverrides
    16  
    17  import (
    18  	"encoding/json"
    19  	"fmt"
    20  	"reflect"
    21  	"strings"
    22  
    23  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/crd/crdgeneration/crdboilerplate"
    24  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
    25  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/crdutil"
    26  
    27  	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
    30  )
    31  
    32  // KeepTopLevelFieldOptionalWithDefault decorates the input CRD to modify the given top field as optional with the default.
    33  func KeepTopLevelFieldOptionalWithDefault(crd *apiextensions.CustomResourceDefinition, defaultValue interface{}, field string) error {
    34  	schema := k8s.GetOpenAPIV3SchemaFromCRD(crd)
    35  	spec := schema.Properties["spec"]
    36  	fieldSchema := spec.Properties[field]
    37  	bytes, err := json.Marshal(defaultValue)
    38  	if err != nil {
    39  		return err
    40  	}
    41  	fieldSchema.Default = &apiextensions.JSON{
    42  		Raw: bytes,
    43  	}
    44  	spec.Properties[field] = fieldSchema
    45  	// mark the given field optional
    46  	required := make([]string, 0)
    47  	for _, v := range spec.Required {
    48  		if v != field {
    49  			required = append(required, v)
    50  		}
    51  	}
    52  	spec.Required = required
    53  	schema.Properties["spec"] = spec
    54  	if len(spec.Required) == 0 {
    55  		schema.Required = []string{}
    56  	}
    57  	return nil
    58  }
    59  
    60  func getSchemaForPath(schema *apiextensions.JSONSchemaProps, path []string) (*apiextensions.JSONSchemaProps, error) {
    61  	if len(path) == 0 {
    62  		return schema, nil
    63  	}
    64  
    65  	var subSchema apiextensions.JSONSchemaProps
    66  	var ok bool
    67  	if schema.Properties != nil {
    68  		if subSchema, ok = schema.Properties[path[0]]; !ok {
    69  			return nil, fmt.Errorf("can't find field %s under properties", path[0])
    70  		}
    71  	} else if schema.AdditionalProperties != nil {
    72  		// TODO: Handle this edge case once there is a concrete example.
    73  		return nil, fmt.Errorf("field %s is under a map field: this path can't be handled", path[0])
    74  	} else {
    75  		return nil, fmt.Errorf("the schema doesn't support any subfield")
    76  	}
    77  
    78  	if len(path) == 1 {
    79  		return &subSchema, nil
    80  	}
    81  
    82  	switch subSchema.Type {
    83  	case "array":
    84  		return getSchemaForPath(subSchema.Items.Schema, path[1:])
    85  	default:
    86  		return getSchemaForPath(&subSchema, path[1:])
    87  	}
    88  }
    89  
    90  // PreserveMutuallyExclusiveNonReferenceField adds back the non-ref field to keep the
    91  // CRD backwards compatible.
    92  func PreserveMutuallyExclusiveNonReferenceField(crd *apiextensions.CustomResourceDefinition, parentPath []string, referenceFieldName, nonReferenceFieldName string) error {
    93  	if referenceFieldName == "" || nonReferenceFieldName == "" {
    94  		return fmt.Errorf("both 'referenceFieldName' and 'nonReferenceFieldName' must be specified")
    95  	}
    96  
    97  	// 1. Get the parent schema of the fields.
    98  	schema := k8s.GetOpenAPIV3SchemaFromCRD(crd)
    99  	var err error
   100  	var parent *apiextensions.JSONSchemaProps
   101  	// Prepend the top-level 'spec' field into path.
   102  	if len(parentPath) == 0 {
   103  		parentPath = []string{"spec"}
   104  	} else {
   105  		parentPath = append([]string{"spec"}, parentPath...)
   106  	}
   107  	parentPathStr := strings.Join(parentPath, ".")
   108  	parent, err = getSchemaForPath(schema, parentPath)
   109  	if err != nil {
   110  		return fmt.Errorf("can't get schema for path '%v' in CRD %s: %v", parentPathStr, crd.Name, err)
   111  	}
   112  
   113  	// 2. Check if the reference field is required.
   114  	requiredFields, err := crdutil.GetRequiredRuleForObjectOrArray(parent)
   115  	if err != nil {
   116  		return fmt.Errorf("error getting the required rule under path %s for CRD %s: %v", parentPath, crd.Name, err)
   117  	}
   118  	var required bool
   119  	for _, field := range requiredFields {
   120  		if field == referenceFieldName {
   121  			required = true
   122  			break
   123  		}
   124  	}
   125  
   126  	// 3. Set the `oneOf` or `not` rule based on whether the reference field is
   127  	// required.
   128  	if required {
   129  		oneOfRule, err := crdutil.GetOneOfRuleForObjectOrArray(parent)
   130  		if err != nil {
   131  			return fmt.Errorf("error getting the oneOf rule under path %s for CRD %s: %v", parentPath, crd.Name, err)
   132  		}
   133  		// TODO(b/223688758): Handle multiple oneOf rules.
   134  		if oneOfRule != nil {
   135  			return fmt.Errorf("can't handle multiple pairs of required mutually exclustive fields under %s for field %s and %s in CRD %s", parentPath, referenceFieldName, referenceFieldName, crd.Name)
   136  		}
   137  
   138  		oneOfRule = []*apiextensions.JSONSchemaProps{
   139  			{
   140  				Required: []string{nonReferenceFieldName},
   141  			},
   142  			{
   143  				Required: []string{referenceFieldName},
   144  			},
   145  		}
   146  		if err := crdutil.SetOneOfRuleForObjectOrArray(parent, oneOfRule); err != nil {
   147  			return fmt.Errorf("error setting the oneOf rule under path %s for CRD %s: %v", parentPath, crd.Name, err)
   148  		}
   149  
   150  		var updatedRequiredFields []string
   151  		for _, field := range requiredFields {
   152  			if field != referenceFieldName {
   153  				updatedRequiredFields = append(updatedRequiredFields, field)
   154  			}
   155  		}
   156  		if err := crdutil.SetRequiredRuleForObjectOrArray(parent, updatedRequiredFields); err != nil {
   157  			return fmt.Errorf("error setting the required rule under path %s for CRD %s: %v", parentPath, crd.Name, err)
   158  		}
   159  	} else {
   160  		notRule, err := crdutil.GetNotRuleForObjectOrArray(parent)
   161  		if err != nil {
   162  			return fmt.Errorf("error getting the not rule under path %s for CRD %s: %v", parentPath, crd.Name, err)
   163  		}
   164  		// TODO(b/223688758): Handle multiple not rules.
   165  		if notRule != nil {
   166  			return fmt.Errorf("can't handling multiple pairs of optional mutually exclustive fields for %s in %s", referenceFieldName, crd.Name)
   167  		}
   168  
   169  		notRule = &apiextensions.JSONSchemaProps{
   170  			Required: []string{nonReferenceFieldName, referenceFieldName},
   171  		}
   172  		if err := crdutil.SetNotRuleForObjectOrArray(parent, notRule); err != nil {
   173  			return fmt.Errorf("error setting the not rule under path %s for CRD %s: %v", parentPath, crd.Name, err)
   174  		}
   175  	}
   176  
   177  	// 4. Add the non-reference field into the schema.
   178  	referenceFieldSchema, ok, err := crdutil.GetSchemaForFieldUnderObjectOrArray(referenceFieldName, parent)
   179  	if err != nil {
   180  		return fmt.Errorf("error getting schema for reference field %s under path %s for CRD %s: %v", referenceFieldName, parentPath, crd.Name, err)
   181  	}
   182  	if !ok {
   183  		return fmt.Errorf("can't find reference field %s under path %s for CRD %s", referenceFieldName, parentPath, crd.Name)
   184  	}
   185  	fieldType := referenceFieldSchema.Type
   186  	if fieldType != "object" && fieldType != "array" {
   187  		return fmt.Errorf("wrong type for reference field %s under path %s for CRD %s: %s", referenceFieldName, parentPath, crd.Name, fieldType)
   188  	}
   189  
   190  	var nonReferenceFieldSchema *apiextensions.JSONSchemaProps
   191  	description := fmt.Sprintf("DEPRECATED. Although this field is still available, there is limited support. "+
   192  		"We recommend that you use `%s.%s` instead.", parentPathStr, referenceFieldName)
   193  	if fieldType == "object" {
   194  		nonReferenceFieldSchema = &apiextensions.JSONSchemaProps{
   195  			Description: description,
   196  			// When the type of a reference field is object, it means that the
   197  			// original field type is string.
   198  			Type: "string",
   199  		}
   200  	} else if fieldType == "array" {
   201  		nonReferenceFieldSchema = &apiextensions.JSONSchemaProps{
   202  			Description: description,
   203  			// When the type of a reference field is an array, it means that the
   204  			// original field type is an array of strings.
   205  			Type: "array",
   206  			Items: &apiextensions.JSONSchemaPropsOrArray{
   207  				Schema: &apiextensions.JSONSchemaProps{
   208  					Type: "string",
   209  				},
   210  			},
   211  		}
   212  	}
   213  
   214  	if err := crdutil.SetSchemaForFieldUnderObjectOrArray(nonReferenceFieldName, parent, nonReferenceFieldSchema); err != nil {
   215  		return fmt.Errorf("error setting schema for non-reference field %s under path %s for CRD %s: %v", nonReferenceFieldName, parentPath, crd.Name, err)
   216  	}
   217  
   218  	// 5. Set the updated schema.
   219  	updatedSchema, err := getSchemaForPath(schema, parentPath[:len(parentPath)-1])
   220  	if err != nil {
   221  		return fmt.Errorf("can't get schema for path '%v' in CRD %s: %v", parentPathStr, crd.Name, err)
   222  	}
   223  	if err := crdutil.SetSchemaForFieldUnderObjectOrArray(parentPath[len(parentPath)-1], updatedSchema, parent); err != nil {
   224  		return fmt.Errorf("error setting updated schema for parent path '%v' for CRD %s: %v", parentPathStr, crd.Name, err)
   225  	}
   226  
   227  	return nil
   228  }
   229  
   230  // EnsureReferenceFieldIsMultiKind adds the required `kind` field under the
   231  // reference field if the `kind` field doesn't exist.
   232  func EnsureReferenceFieldIsMultiKind(crd *apiextensions.CustomResourceDefinition, parentPath []string, referenceFieldName string, supportedKinds []string) error {
   233  	if referenceFieldName == "" {
   234  		return fmt.Errorf("param 'referenceFieldName' must be specified")
   235  	}
   236  
   237  	// 1. Get the parent schema of the fields.
   238  	schema := k8s.GetOpenAPIV3SchemaFromCRD(crd)
   239  	var err error
   240  	var parent *apiextensions.JSONSchemaProps
   241  	// Prepend the top-level 'spec' field into path.
   242  	if len(parentPath) == 0 {
   243  		parentPath = []string{"spec"}
   244  	} else {
   245  		parentPath = append([]string{"spec"}, parentPath...)
   246  	}
   247  	parentPathStr := strings.Join(parentPath, ".")
   248  	parent, err = getSchemaForPath(schema, parentPath)
   249  	if err != nil {
   250  		return fmt.Errorf("can't get schema for path '%v' in CRD %s: %v", parentPathStr, crd.Name, err)
   251  	}
   252  
   253  	// 2. Ensure the required `kind` subfield is supported in the reference
   254  	// field.
   255  	referenceFieldSchema, ok, err := crdutil.GetSchemaForFieldUnderObjectOrArray(referenceFieldName, parent)
   256  	if err != nil {
   257  		return fmt.Errorf("error getting schema for reference field %s under path %s for CRD %s: %v", referenceFieldName, parentPath, crd.Name, err)
   258  	}
   259  	if !ok {
   260  		return fmt.Errorf("can't find reference field %s under path %s for CRD %s", referenceFieldName, parentPath, crd.Name)
   261  	}
   262  	fieldType := referenceFieldSchema.Type
   263  	if fieldType != "object" && fieldType != "array" {
   264  		return fmt.Errorf("wrong type for reference field %s under path %s for CRD %s: %s", referenceFieldName, parentPath, crd.Name, fieldType)
   265  	}
   266  
   267  	// If the reference field is an array, it needs to be handled differently as
   268  	// the schema for subfields is defined under .Items.Schema.
   269  	referenceFieldSchemaForSubfields := referenceFieldSchema
   270  	if fieldType == "array" {
   271  		referenceFieldSchemaForSubfields = referenceFieldSchema.Items.Schema
   272  	}
   273  	if _, ok := referenceFieldSchemaForSubfields.Properties["kind"]; !ok {
   274  		externalRefSchema, ok := referenceFieldSchemaForSubfields.Properties["external"]
   275  		if !ok {
   276  			return fmt.Errorf("can't find external field under reference %s in CRD %s", referenceFieldName, crd.Name)
   277  		}
   278  
   279  		if len(supportedKinds) == 0 {
   280  			return fmt.Errorf("there must be at least one kind specified in 'supportedKinds' list")
   281  		}
   282  		referenceFieldSchemaWithKind := crdboilerplate.GetMultiKindResourceReferenceSchemaBoilerplate(externalRefSchema.Description, supportedKinds)
   283  
   284  		if fieldType == "array" {
   285  			referenceFieldSchema.Items.Schema = referenceFieldSchemaWithKind
   286  		} else if fieldType == "object" {
   287  			referenceFieldSchema = referenceFieldSchemaWithKind
   288  		}
   289  		if err := crdutil.SetSchemaForFieldUnderObjectOrArray(referenceFieldName, parent, referenceFieldSchema); err != nil {
   290  			return fmt.Errorf("error setting schema for reference field %s under path %s for CRD %s: %v", referenceFieldName, parentPath, crd.Name, err)
   291  		}
   292  	}
   293  
   294  	// 3. Set the updated schema.
   295  	updatedSchema, err := getSchemaForPath(schema, parentPath[:len(parentPath)-1])
   296  	if err != nil {
   297  		return fmt.Errorf("can't get schema for path '%v' in CRD %s: %v", parentPathStr, crd.Name, err)
   298  	}
   299  	if err := crdutil.SetSchemaForFieldUnderObjectOrArray(parentPath[len(parentPath)-1], updatedSchema, parent); err != nil {
   300  		return fmt.Errorf("error setting updated schema for parent path '%v' for CRD %s: %v", parentPathStr, crd.Name, err)
   301  	}
   302  
   303  	return nil
   304  }
   305  
   306  // PruneNoOpsField removes the no-ops field from spec if specified given the field path.
   307  // It doesn't work for sub-fields in the parent field of array type.
   308  func PruneNoOpsField(r *k8s.Resource, path ...string) error {
   309  	unstructured.RemoveNestedField(r.Spec, path...)
   310  	return nil
   311  }
   312  
   313  // PreserveUserSpecifiedLegacyField adds the user specified legacy field back to the reconciled resource.
   314  // The reason to preserve the legacy field is because that users may be confused when the objects they try to create are different from what they get back.
   315  func PreserveUserSpecifiedLegacyField(original, reconciled *k8s.Resource, path ...string) error {
   316  	vo, found, err := unstructured.NestedFieldCopy(original.Spec, path...)
   317  	if err != nil {
   318  		return err
   319  	}
   320  	if !found {
   321  		return nil
   322  	}
   323  	if err := unstructured.SetNestedField(reconciled.Spec, vo, path...); err != nil {
   324  		return err
   325  	}
   326  	return nil
   327  }
   328  
   329  // PreserveUserSpecifiedLegacyFieldUnderSlice iterates through the specified slice/array field in the reconciled and original resource, and adds
   330  // the user-specified non-reference field(s) back into the reconciled resource. The reason to preserve the non-reference field is that users may be
   331  // confused when the objects they try to create are different from what they get back.
   332  // Note: This function assumed that the order of items in the slice are the
   333  // same in the original and reconciled resources.
   334  func PreserveUserSpecifiedLegacyFieldUnderSlice(original, reconciled *k8s.Resource, upToSlicePath []string, path []string) error {
   335  	originalSlice, foundOriginal, err := unstructured.NestedSlice(original.Spec, upToSlicePath...)
   336  	if err != nil {
   337  		return fmt.Errorf("error getting the nested slice under path %s for the original resource: %v", strings.Join(upToSlicePath, "."), err)
   338  	}
   339  	reconciledSlice, foundReconciled, err := unstructured.NestedSlice(reconciled.Spec, upToSlicePath...)
   340  	if err != nil {
   341  		return fmt.Errorf("error getting the nested slice under path %s for the reconciled resource: %v", strings.Join(upToSlicePath, "."), err)
   342  	}
   343  	if !foundOriginal || !foundReconciled {
   344  		return nil
   345  	}
   346  	for i, v := range originalSlice {
   347  		pathFieldValue, foundPathField, err := unstructured.NestedFieldCopy(v.(map[string]interface{}), path...)
   348  		if err != nil {
   349  			return fmt.Errorf("error getting the user-specified value from the path %s within the slice field: %v", strings.Join(path, "."), err)
   350  		}
   351  		if !foundPathField {
   352  			continue
   353  		}
   354  		if err := unstructured.SetNestedField(reconciledSlice[i].(map[string]interface{}), pathFieldValue, path...); err != nil {
   355  			return fmt.Errorf("error setting original value to reconciled slice element: %v", err)
   356  		}
   357  	}
   358  	if err := unstructured.SetNestedSlice(reconciled.Spec, reconciledSlice, upToSlicePath...); err != nil {
   359  		return fmt.Errorf("error setting preserved values back into reconciled object: %v", err)
   360  	}
   361  	return nil
   362  }
   363  
   364  // PreserveUserSpecifiedLegacyArrayField adds the user specified legacy array
   365  // field back to the reconciled resource.
   366  // The reason to preserve the legacy array field is because that users may be
   367  // confused when the objects they try to create are different from what they get
   368  // back.
   369  func PreserveUserSpecifiedLegacyArrayField(original, reconciled *k8s.Resource, path ...string) error {
   370  	vo, found, err := unstructured.NestedSlice(original.Spec, path...)
   371  	if err != nil {
   372  		return err
   373  	}
   374  	if !found {
   375  		return nil
   376  	}
   377  	if err := unstructured.SetNestedSlice(reconciled.Spec, vo, path...); err != nil {
   378  		return err
   379  	}
   380  	return nil
   381  }
   382  
   383  // PruneDefaultedAuthoritativeFieldIfOnlyLegacyFieldSpecified prune the defaulted authoritative field from the reconciled resource (post-actuation) if only
   384  // the legacy field is specified in the original spec.
   385  // Populating the new authoritative field into spec along with the legacy field will cause confusion if users only modify the legacy field in their configuration
   386  // without being aware of the defaulted field in k8s object.
   387  func PruneDefaultedAuthoritativeFieldIfOnlyLegacyFieldSpecified(original, reconciled *k8s.Resource, legacyFieldPath, fieldPath []string) error {
   388  	// If the authoritative field is specified in the original spec, do nothing.
   389  	_, found, err := unstructured.NestedFieldCopy(original.Spec, fieldPath...)
   390  	if err != nil {
   391  		return err
   392  	}
   393  	if found {
   394  		return nil
   395  	}
   396  	// If only the legacy field is specified in the original spec, prune the defaulted authoritative field.
   397  	_, found, err = unstructured.NestedFieldCopy(original.Spec, legacyFieldPath...)
   398  	if err != nil {
   399  		return err
   400  	}
   401  	if found {
   402  		unstructured.RemoveNestedField(reconciled.Spec, fieldPath...)
   403  		return nil
   404  	}
   405  	return nil
   406  }
   407  
   408  // PruneDefaultedAuthoritativeFieldIfOnlyLegacyFieldSpecifiedUnderSlice iterates through the specified slice/array field in the reconciled and original resource,
   409  // and prune the defaulted reference field from the reconciled resource (post-actuation) if only the non-reference field is specified in the original spec.
   410  // Populating the new reference field into spec along with the non-reference field will cause confusion if users only modify the non-reference field in their configuration
   411  // without being aware of the defaulted field in k8s object.
   412  // Note: This function assumed that the order of items in the slice are the
   413  // same in the original and reconciled resources.
   414  func PruneDefaultedAuthoritativeFieldIfOnlyLegacyFieldSpecifiedUnderSlice(original, reconciled *k8s.Resource, pathUpToSlice, nonReferenceFieldPath, referenceFieldPath []string) error {
   415  	originalSlice, foundOriginal, err := unstructured.NestedSlice(original.Spec, pathUpToSlice...)
   416  	if err != nil {
   417  		return fmt.Errorf("error getting the nested slice under path %s for the original resource: %v", strings.Join(pathUpToSlice, "."), err)
   418  	}
   419  	reconciledSlice, foundReconciled, err := unstructured.NestedSlice(reconciled.Spec, pathUpToSlice...)
   420  	if err != nil {
   421  		return fmt.Errorf("error getting the nested slice under path %s for the reconciled resource: %v", strings.Join(pathUpToSlice, "."), err)
   422  	}
   423  	if !foundOriginal || !foundReconciled {
   424  		return nil
   425  	}
   426  	for i, v := range originalSlice {
   427  		_, foundReferenceField, err := unstructured.NestedFieldCopy(v.(map[string]interface{}), referenceFieldPath...)
   428  		if err != nil {
   429  			return fmt.Errorf("error checking the existence of the nested reference field %s within the slice field of the original resource: %v", strings.Join(referenceFieldPath, "."), err)
   430  		}
   431  		_, foundNonReferenceField, err := unstructured.NestedFieldCopy(v.(map[string]interface{}), nonReferenceFieldPath...)
   432  		if err != nil {
   433  			return fmt.Errorf("error checking the existence of the nested non-reference field %s within the slice field of the original resource: %v", strings.Join(nonReferenceFieldPath, "."), err)
   434  		}
   435  		if !foundReferenceField && foundNonReferenceField {
   436  			unstructured.RemoveNestedField(reconciledSlice[i].(map[string]interface{}), referenceFieldPath...)
   437  		}
   438  	}
   439  	if err := unstructured.SetNestedSlice(reconciled.Spec, reconciledSlice, pathUpToSlice...); err != nil {
   440  		return fmt.Errorf("error setting the altered slice field %s back into the reconciled resource: %v", strings.Join(pathUpToSlice, "."), err)
   441  	}
   442  	return nil
   443  }
   444  
   445  // PruneDefaultedAuthoritativeArrayFieldIfOnlyLegacyArrayFieldSpecified prunes
   446  // the defaulted authoritative array field from the reconciled resource
   447  // (post-actuation) if only the legacy array field is specified in the original
   448  // spec.
   449  // Populating the new authoritative array field into spec along with the legacy
   450  // array field will cause confusion if users only modify the legacy array field
   451  // in their configuration without being aware of the defaulted field in k8s
   452  // object.
   453  func PruneDefaultedAuthoritativeArrayFieldIfOnlyLegacyArrayFieldSpecified(original, reconciled *k8s.Resource, legacyFieldPath, fieldPath []string) error {
   454  	// If the authoritative field is specified in the original spec, do nothing.
   455  	_, found, err := unstructured.NestedSlice(original.Spec, fieldPath...)
   456  	if err != nil {
   457  		return err
   458  	}
   459  	if found {
   460  		return nil
   461  	}
   462  
   463  	// If only the legacy field is specified in the original spec, prune the
   464  	// defaulted authoritative field.
   465  	_, found, err = unstructured.NestedSlice(original.Spec, legacyFieldPath...)
   466  	if err != nil {
   467  		return err
   468  	}
   469  	if found {
   470  		unstructured.RemoveNestedField(reconciled.Spec, fieldPath...)
   471  	}
   472  	return nil
   473  }
   474  
   475  // FavorAuthoritativeFieldOverLegacyField favor the value of the authoritative field if it's set;
   476  // otherwise, it takes the value from the legacy field and populate it into the authoritative field and then prune the legacy field.
   477  // If the legacy field is specified, this function will also mark the authoritative field as managed fields.
   478  func FavorAuthoritativeFieldOverLegacyField(r *k8s.Resource, legacyFieldPath, fieldPath []string) error {
   479  	if err := validateIfAuthoritativeFieldAndLegacyFieldTakeDifferentValues(r, legacyFieldPath, fieldPath); err != nil {
   480  		return err
   481  	}
   482  
   483  	// If the authoritative field is set, keep the value.
   484  	_, found, err := unstructured.NestedFieldCopy(r.Spec, fieldPath...)
   485  	if err != nil {
   486  		return err
   487  	}
   488  	if found {
   489  		unstructured.RemoveNestedField(r.Spec, legacyFieldPath...)
   490  		return nil
   491  	}
   492  
   493  	// If only the legacy field is set, populate the value to the authoritative field before actuation.
   494  	v, found, err := unstructured.NestedFieldCopy(r.Spec, legacyFieldPath...)
   495  	if err != nil {
   496  		return err
   497  	}
   498  	if !found {
   499  		return nil
   500  	}
   501  	if isReferenceFieldPath(fieldPath) {
   502  		if err := unstructured.SetNestedField(r.Spec, v, append(fieldPath, "external")...); err != nil {
   503  			return err
   504  		}
   505  		// Mark "external" under the authoritative field as user managed fields because the user has set the legacy field for the same feature.
   506  		if err := markFieldAsManaged(r, append(fieldPath, "external")...); err != nil {
   507  			return err
   508  		}
   509  		// Mark the authoritative field as user managed fields because the user has set the legacy field for the same feature.
   510  		if err := markFieldAsManaged(r, fieldPath...); err != nil {
   511  			return err
   512  		}
   513  		unstructured.RemoveNestedField(r.Spec, legacyFieldPath...)
   514  		return nil
   515  	}
   516  	if err := unstructured.SetNestedField(r.Spec, v, fieldPath...); err != nil {
   517  		return err
   518  	}
   519  	// Mark the authoritative field as user managed fields because the user has set the legacy field for the same feature.
   520  	if err := markFieldAsManaged(r, fieldPath...); err != nil {
   521  		return err
   522  	}
   523  	unstructured.RemoveNestedField(r.Spec, legacyFieldPath...)
   524  	return nil
   525  }
   526  
   527  // isReferenceFieldPath will only identify non-array reference fields (reference array fields end with "Refs")
   528  func isReferenceFieldPath(fieldPath []string) bool {
   529  	field := fieldPath[len(fieldPath)-1]
   530  	return strings.HasSuffix(field, "Ref")
   531  }
   532  
   533  // FavorReferenceFieldOverNonReferenceFieldUnderSlice returns an error if both fields are set; otherwise, take the value
   534  // from the non-reference field and populate it into the "external" field in the reference field, and then prune the non-reference field.
   535  func FavorReferenceFieldOverNonReferenceFieldUnderSlice(r *k8s.Resource, pathUpToSlice, nonReferenceFieldPath, referenceFieldPath []string) error {
   536  	if err := validateAtMostOneFieldIsSetUnderSlice(r, pathUpToSlice, nonReferenceFieldPath, referenceFieldPath); err != nil {
   537  		return err
   538  	}
   539  	sliceVal, found, err := unstructured.NestedSlice(r.Spec, pathUpToSlice...)
   540  	if err != nil {
   541  		return fmt.Errorf("error getting nested slice field %s from resource: %v", strings.Join(pathUpToSlice, "."), err)
   542  	}
   543  	if !found {
   544  		return nil
   545  	}
   546  	for i, v := range sliceVal {
   547  		nonReferenceVal, found, err := unstructured.NestedFieldCopy(v.(map[string]interface{}), nonReferenceFieldPath...)
   548  		if err != nil {
   549  			return fmt.Errorf("error getting non-reference field %s from slice field: %v", strings.Join(nonReferenceFieldPath, "."), err)
   550  		}
   551  		if !found {
   552  			continue
   553  		}
   554  		if err := unstructured.SetNestedField(sliceVal[i].(map[string]interface{}), nonReferenceVal, append(referenceFieldPath, "external")...); err != nil {
   555  			return fmt.Errorf("error setting non-reference value to reference field path %s: %v", strings.Join(append(referenceFieldPath, "external"), "."), err)
   556  		}
   557  		unstructured.RemoveNestedField(sliceVal[i].(map[string]interface{}), nonReferenceFieldPath...)
   558  	}
   559  	if err := unstructured.SetNestedSlice(r.Spec, sliceVal, pathUpToSlice...); err != nil {
   560  		return fmt.Errorf("error setting altered slice %s back into resource: %v", strings.Join(pathUpToSlice, "."), err)
   561  	}
   562  	return nil
   563  }
   564  
   565  // validateIfAuthoritativeFieldAndLegacyFieldTakeDifferentValues returns error if the legacy field and the authoritative field are both present in spec
   566  // but with different values.
   567  func validateIfAuthoritativeFieldAndLegacyFieldTakeDifferentValues(r *k8s.Resource, legacyFieldPath, fieldPath []string) error {
   568  	v1, f1, err := unstructured.NestedFieldCopy(r.Spec, fieldPath...)
   569  	if err != nil {
   570  		return err
   571  	}
   572  
   573  	v2, f2, err := unstructured.NestedFieldCopy(r.Spec, legacyFieldPath...)
   574  	if err != nil {
   575  		return err
   576  	}
   577  
   578  	if f1 && f2 && !reflect.DeepEqual(v1, v2) {
   579  		authoritative := strings.Join(fieldPath, ".")
   580  		legacy := strings.Join(legacyFieldPath, ".")
   581  		return fmt.Errorf("'%v' field and '%v' field are both present in spec, but they take different values. It's recommended to only use '%v' in your configuration because '%v' has been deprecated by the API",
   582  			authoritative, legacy, authoritative, legacy)
   583  	}
   584  	return nil
   585  }
   586  
   587  // validateAtMostOneFieldIsSetUnderSlice returns error if both field paths within the slice field are set
   588  func validateAtMostOneFieldIsSetUnderSlice(r *k8s.Resource, fieldPathUpToSlice, fieldPath1, fieldPath2 []string) error {
   589  	sliceField, found, err := unstructured.NestedSlice(r.Spec, fieldPathUpToSlice...)
   590  	if err != nil {
   591  		return fmt.Errorf("error getting slice field %s from resource: %v", strings.Join(fieldPathUpToSlice, "."), err)
   592  	}
   593  	if !found {
   594  		return nil
   595  	}
   596  	for _, v := range sliceField {
   597  		_, found1, err := unstructured.NestedFieldCopy(v.(map[string]interface{}), fieldPath1...)
   598  		if err != nil {
   599  			return fmt.Errorf("error checking existence of nested field %s within slice field: %v", strings.Join(fieldPath1, "."), err)
   600  		}
   601  		_, found2, err := unstructured.NestedFieldCopy(v.(map[string]interface{}), fieldPath2...)
   602  		if err != nil {
   603  			return fmt.Errorf("error checking existence of nested field %s within slice field: %v", strings.Join(fieldPath2, "."), err)
   604  		}
   605  		if !found1 || !found2 {
   606  			continue
   607  		}
   608  		fullFieldPath1 := strings.Join(append(fieldPathUpToSlice, fieldPath1...), ".")
   609  		fullFieldPath2 := strings.Join(append(fieldPathUpToSlice, fieldPath2...), ".")
   610  		return fmt.Errorf("'%v' field and '%v' field are both set. Please remove one of the two fields", fullFieldPath1, fullFieldPath2)
   611  	}
   612  	return nil
   613  }
   614  
   615  func markFieldAsManaged(r *k8s.Resource, fieldPath ...string) error {
   616  	if r.ManagedFields == nil {
   617  		return nil
   618  	}
   619  	parts := make([]interface{}, 0, len(fieldPath))
   620  	for _, v := range fieldPath {
   621  		parts = append(parts, v)
   622  	}
   623  	p, err := fieldpath.MakePath(parts...)
   624  	if err != nil {
   625  		return err
   626  	}
   627  	r.ManagedFields.Insert(p)
   628  	return nil
   629  }
   630  
   631  // FavorReferenceArrayFieldOverNonReferenceArrayField favor the value of the
   632  // reference array field if it's set; otherwise, it takes the value from the
   633  // non-reference array field, populates it into the 'external' subfield of the
   634  // items in the reference field, and then prune the non-reference field.
   635  func FavorReferenceArrayFieldOverNonReferenceArrayField(r *k8s.Resource, nonReferenceFieldPath, referenceFieldPath []string) error {
   636  	nonRefArray, foundNonRef, err := unstructured.NestedStringSlice(r.Spec, nonReferenceFieldPath...)
   637  	if err != nil {
   638  		return fmt.Errorf("error getting the non-reference field '%v': %v", strings.Join(nonReferenceFieldPath, "."), err)
   639  	}
   640  	_, foundRef, err := unstructured.NestedSlice(r.Spec, referenceFieldPath...)
   641  	if err != nil {
   642  		return fmt.Errorf("error getting the reference field '%v': %v", strings.Join(referenceFieldPath, "."), err)
   643  	}
   644  
   645  	if foundNonRef && foundRef {
   646  		return fmt.Errorf("mutually-exclusive fields '%v' and '%v' are both set", strings.Join(nonReferenceFieldPath, "."), strings.Join(referenceFieldPath, "."))
   647  	}
   648  
   649  	if !foundNonRef || len(nonRefArray) == 0 {
   650  		return nil
   651  	}
   652  
   653  	// If only the non-reference array is set, populate its values to the
   654  	// 'external' subfields of items under the reference array, and remove the
   655  	// non-reference array field.
   656  	refArray := make([]interface{}, len(nonRefArray))
   657  	for i, val := range nonRefArray {
   658  		refItem := make(map[string]interface{})
   659  		if err := unstructured.SetNestedField(refItem, val, "external"); err != nil {
   660  			return fmt.Errorf("error setting the 'external' field under the reference array '%v': %v", strings.Join(referenceFieldPath, "."), err)
   661  		}
   662  		refArray[i] = refItem
   663  	}
   664  	if err := unstructured.SetNestedSlice(r.Spec, refArray, referenceFieldPath...); err != nil {
   665  		return err
   666  	}
   667  	unstructured.RemoveNestedField(r.Spec, nonReferenceFieldPath...)
   668  	return nil
   669  }
   670  
   671  // RemovePrefixFromStringFieldInSpec removes the prefix from the field specified by
   672  // the input path in the resource spec.
   673  // The function returns error if the specified field is not a string.
   674  // The function is no-op if the field is not found in resource spec.
   675  // The function is no-op if the input prefix does not match the prefix of the specified field.
   676  func RemovePrefixFromStringFieldInSpec(r *k8s.Resource, prefix string, path ...string) error {
   677  	vo, found, err := unstructured.NestedString(r.Spec, path...)
   678  	if err != nil {
   679  		return err
   680  	}
   681  	if !found {
   682  		return nil
   683  	}
   684  	if !strings.HasPrefix(vo, prefix) {
   685  		return nil
   686  	}
   687  	v := strings.TrimPrefix(vo, prefix)
   688  	if err := unstructured.SetNestedField(r.Spec, v, path...); err != nil {
   689  		return err
   690  	}
   691  	return nil
   692  }
   693  

View as plain text