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 }