func PreserveMutuallyExclusiveNonReferenceField()

in pkg/resourceoverrides/utils.go [93:229]


func PreserveMutuallyExclusiveNonReferenceField(crd *apiextensions.CustomResourceDefinition, parentPath []string, referenceFieldName, nonReferenceFieldName string) error {
	if referenceFieldName == "" || nonReferenceFieldName == "" {
		return fmt.Errorf("both 'referenceFieldName' and 'nonReferenceFieldName' must be specified")
	}

	// 1. Get the parent schema of the fields.
	schema := k8s.GetOpenAPIV3SchemaFromCRD(crd)
	var err error
	var parent *apiextensions.JSONSchemaProps
	// Prepend the top-level 'spec' field into path.
	if len(parentPath) == 0 {
		parentPath = []string{"spec"}
	} else {
		parentPath = append([]string{"spec"}, parentPath...)
	}
	parentPathStr := strings.Join(parentPath, ".")
	parent, err = getSchemaForPath(schema, parentPath)
	if err != nil {
		return fmt.Errorf("can't get schema for path '%v' in CRD %s: %w", parentPathStr, crd.Name, err)
	}

	// 2. Check if the reference field is required.
	requiredFields, err := crdutil.GetRequiredRuleForObjectOrArray(parent)
	if err != nil {
		return fmt.Errorf("error getting the required rule under path %s for CRD %s: %w", parentPath, crd.Name, err)
	}
	var required bool
	for _, field := range requiredFields {
		if field == referenceFieldName {
			required = true
			break
		}
	}

	// 3. Set the `oneOf` or `not` rule based on whether the reference field is
	// required.
	if required {
		oneOfRule, err := crdutil.GetOneOfRuleForObjectOrArray(parent)
		if err != nil {
			return fmt.Errorf("error getting the oneOf rule under path %s for CRD %s: %w", parentPath, crd.Name, err)
		}
		// TODO(b/223688758): Handle multiple oneOf rules.
		if oneOfRule != nil {
			return fmt.Errorf("can't handle multiple pairs of required mutually exclusive fields under %s for field %s and %s in CRD %s", parentPath, referenceFieldName, referenceFieldName, crd.Name)
		}

		oneOfRule = []*apiextensions.JSONSchemaProps{
			{
				Required: []string{nonReferenceFieldName},
			},
			{
				Required: []string{referenceFieldName},
			},
		}
		if err := crdutil.SetOneOfRuleForObjectOrArray(parent, oneOfRule); err != nil {
			return fmt.Errorf("error setting the oneOf rule under path %s for CRD %s: %w", parentPath, crd.Name, err)
		}

		var updatedRequiredFields []string
		for _, field := range requiredFields {
			if field != referenceFieldName {
				updatedRequiredFields = append(updatedRequiredFields, field)
			}
		}
		if err := crdutil.SetRequiredRuleForObjectOrArray(parent, updatedRequiredFields); err != nil {
			return fmt.Errorf("error setting the required rule under path %s for CRD %s: %w", parentPath, crd.Name, err)
		}
	} else {
		notRule, err := crdutil.GetNotRuleForObjectOrArray(parent)
		if err != nil {
			return fmt.Errorf("error getting the not rule under path %s for CRD %s: %w", parentPath, crd.Name, err)
		}
		// TODO(b/223688758): Handle multiple not rules.
		if notRule != nil {
			return fmt.Errorf("can't handling multiple pairs of optional mutually exclusive fields for %s in %s", referenceFieldName, crd.Name)
		}

		notRule = &apiextensions.JSONSchemaProps{
			Required: []string{nonReferenceFieldName, referenceFieldName},
		}
		if err := crdutil.SetNotRuleForObjectOrArray(parent, notRule); err != nil {
			return fmt.Errorf("error setting the not rule under path %s for CRD %s: %w", parentPath, crd.Name, err)
		}
	}

	// 4. Add the non-reference field into the schema.
	referenceFieldSchema, ok, err := crdutil.GetSchemaForFieldUnderObjectOrArray(referenceFieldName, parent)
	if err != nil {
		return fmt.Errorf("error getting schema for reference field %s under path %s for CRD %s: %w", referenceFieldName, parentPath, crd.Name, err)
	}
	if !ok {
		return fmt.Errorf("can't find reference field %s under path %s for CRD %s", referenceFieldName, parentPath, crd.Name)
	}
	fieldType := referenceFieldSchema.Type
	if fieldType != "object" && fieldType != "array" {
		return fmt.Errorf("wrong type for reference field %s under path %s for CRD %s: %s", referenceFieldName, parentPath, crd.Name, fieldType)
	}

	var nonReferenceFieldSchema *apiextensions.JSONSchemaProps
	description := fmt.Sprintf("DEPRECATED. Although this field is still available, there is limited support. "+
		"We recommend that you use `%s.%s` instead.", parentPathStr, referenceFieldName)
	if fieldType == "object" {
		nonReferenceFieldSchema = &apiextensions.JSONSchemaProps{
			Description: description,
			// When the type of a reference field is object, it means that the
			// original field type is string.
			Type: "string",
		}
	} else if fieldType == "array" {
		nonReferenceFieldSchema = &apiextensions.JSONSchemaProps{
			Description: description,
			// When the type of a reference field is an array, it means that the
			// original field type is an array of strings.
			Type: "array",
			Items: &apiextensions.JSONSchemaPropsOrArray{
				Schema: &apiextensions.JSONSchemaProps{
					Type: "string",
				},
			},
		}
	}

	if err := crdutil.SetSchemaForFieldUnderObjectOrArray(nonReferenceFieldName, parent, nonReferenceFieldSchema); err != nil {
		return fmt.Errorf("error setting schema for non-reference field %s under path %s for CRD %s: %w", nonReferenceFieldName, parentPath, crd.Name, err)
	}

	// 5. Set the updated schema.
	updatedSchema, err := getSchemaForPath(schema, parentPath[:len(parentPath)-1])
	if err != nil {
		return fmt.Errorf("can't get schema for path '%v' in CRD %s: %w", parentPathStr, crd.Name, err)
	}
	if err := crdutil.SetSchemaForFieldUnderObjectOrArray(parentPath[len(parentPath)-1], updatedSchema, parent); err != nil {
		return fmt.Errorf("error setting updated schema for parent path '%v' for CRD %s: %w", parentPathStr, crd.Name, err)
	}

	return nil
}