k8s.io/apiserver@v0.31.1/pkg/cel/openapi/resolver/refs.go (about)

     1  /*
     2  Copyright 2023 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package resolver
    18  
    19  import (
    20  	"fmt"
    21  
    22  	"k8s.io/apimachinery/pkg/util/sets"
    23  	"k8s.io/kube-openapi/pkg/validation/spec"
    24  )
    25  
    26  // PopulateRefs recursively replaces Refs in the schema with the referred one.
    27  // schemaOf is the callback to find the corresponding schema by the ref.
    28  // This function will not mutate the original schema. If the schema needs to be
    29  // mutated, a copy will be returned, otherwise it returns the original schema.
    30  func PopulateRefs(schemaOf func(ref string) (*spec.Schema, bool), rootRef string) (*spec.Schema, error) {
    31  	visitedRefs := sets.New[string]()
    32  	rootSchema, ok := schemaOf(rootRef)
    33  	visitedRefs.Insert(rootRef)
    34  	if !ok {
    35  		return nil, fmt.Errorf("internal error: cannot resolve Ref for root schema %q: %w", rootRef, ErrSchemaNotFound)
    36  	}
    37  	return populateRefs(schemaOf, visitedRefs, rootSchema)
    38  }
    39  
    40  func populateRefs(schemaOf func(ref string) (*spec.Schema, bool), visited sets.Set[string], schema *spec.Schema) (*spec.Schema, error) {
    41  	result := *schema
    42  	changed := false
    43  
    44  	ref, isRef := refOf(schema)
    45  	if isRef {
    46  		if visited.Has(ref) {
    47  			return &spec.Schema{
    48  				// for circular ref, return an empty object as placeholder
    49  				SchemaProps: spec.SchemaProps{Type: []string{"object"}},
    50  			}, nil
    51  		}
    52  		visited.Insert(ref)
    53  		// restore visited state at the end of the recursion.
    54  		defer func() {
    55  			visited.Delete(ref)
    56  		}()
    57  		// replace the whole schema with the referred one.
    58  		resolved, ok := schemaOf(ref)
    59  		if !ok {
    60  			return nil, fmt.Errorf("internal error: cannot resolve Ref %q: %w", ref, ErrSchemaNotFound)
    61  		}
    62  		result = *resolved
    63  		changed = true
    64  	}
    65  	// schema is an object, populate its properties and additionalProperties
    66  	props := make(map[string]spec.Schema, len(schema.Properties))
    67  	propsChanged := false
    68  	for name, prop := range result.Properties {
    69  		populated, err := populateRefs(schemaOf, visited, &prop)
    70  		if err != nil {
    71  			return nil, err
    72  		}
    73  		if populated != &prop {
    74  			propsChanged = true
    75  		}
    76  		props[name] = *populated
    77  	}
    78  	if propsChanged {
    79  		changed = true
    80  		result.Properties = props
    81  	}
    82  	if result.AdditionalProperties != nil && result.AdditionalProperties.Schema != nil {
    83  		populated, err := populateRefs(schemaOf, visited, result.AdditionalProperties.Schema)
    84  		if err != nil {
    85  			return nil, err
    86  		}
    87  		if populated != result.AdditionalProperties.Schema {
    88  			changed = true
    89  			result.AdditionalProperties.Schema = populated
    90  		}
    91  	}
    92  	// schema is a list, populate its items
    93  	if result.Items != nil && result.Items.Schema != nil {
    94  		populated, err := populateRefs(schemaOf, visited, result.Items.Schema)
    95  		if err != nil {
    96  			return nil, err
    97  		}
    98  		if populated != result.Items.Schema {
    99  			changed = true
   100  			result.Items.Schema = populated
   101  		}
   102  	}
   103  	if changed {
   104  		return &result, nil
   105  	}
   106  	return schema, nil
   107  }
   108  
   109  func refOf(schema *spec.Schema) (string, bool) {
   110  	if schema.Ref.GetURL() != nil {
   111  		return schema.Ref.String(), true
   112  	}
   113  	// A Ref may be wrapped in allOf to preserve its description
   114  	// see https://github.com/kubernetes/kubernetes/issues/106387
   115  	// For kube-openapi, allOf is only used for wrapping a Ref.
   116  	for _, allOf := range schema.AllOf {
   117  		if ref, isRef := refOf(&allOf); isRef {
   118  			return ref, isRef
   119  		}
   120  	}
   121  	return "", false
   122  }