k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/generators/extension.go (about) 1 /* 2 Copyright 2018 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 generators 18 19 import ( 20 "fmt" 21 "sort" 22 "strings" 23 24 "k8s.io/gengo/v2" 25 "k8s.io/gengo/v2/types" 26 "k8s.io/kube-openapi/pkg/util/sets" 27 ) 28 29 const extensionPrefix = "x-kubernetes-" 30 31 // extensionAttributes encapsulates common traits for particular extensions. 32 type extensionAttributes struct { 33 xName string 34 kind types.Kind 35 allowedValues sets.String 36 enforceArray bool 37 } 38 39 // Extension tag to openapi extension attributes 40 var tagToExtension = map[string]extensionAttributes{ 41 "patchMergeKey": { 42 xName: "x-kubernetes-patch-merge-key", 43 kind: types.Slice, 44 }, 45 "patchStrategy": { 46 xName: "x-kubernetes-patch-strategy", 47 kind: types.Slice, 48 allowedValues: sets.NewString("merge", "retainKeys"), 49 }, 50 "listMapKey": { 51 xName: "x-kubernetes-list-map-keys", 52 kind: types.Slice, 53 enforceArray: true, 54 }, 55 "listType": { 56 xName: "x-kubernetes-list-type", 57 kind: types.Slice, 58 allowedValues: sets.NewString("atomic", "set", "map"), 59 }, 60 "mapType": { 61 xName: "x-kubernetes-map-type", 62 kind: types.Map, 63 allowedValues: sets.NewString("atomic", "granular"), 64 }, 65 "structType": { 66 xName: "x-kubernetes-map-type", 67 kind: types.Struct, 68 allowedValues: sets.NewString("atomic", "granular"), 69 }, 70 "validations": { 71 xName: "x-kubernetes-validations", 72 kind: types.Slice, 73 }, 74 } 75 76 // Extension encapsulates information necessary to generate an OpenAPI extension. 77 type extension struct { 78 idlTag string // Example: listType 79 xName string // Example: x-kubernetes-list-type 80 values []string // Example: [atomic] 81 } 82 83 func (e extension) hasAllowedValues() bool { 84 return tagToExtension[e.idlTag].allowedValues.Len() > 0 85 } 86 87 func (e extension) allowedValues() sets.String { 88 return tagToExtension[e.idlTag].allowedValues 89 } 90 91 func (e extension) hasKind() bool { 92 return len(tagToExtension[e.idlTag].kind) > 0 93 } 94 95 func (e extension) kind() types.Kind { 96 return tagToExtension[e.idlTag].kind 97 } 98 99 func (e extension) validateAllowedValues() error { 100 // allowedValues not set means no restrictions on values. 101 if !e.hasAllowedValues() { 102 return nil 103 } 104 // Check for missing value. 105 if len(e.values) == 0 { 106 return fmt.Errorf("%s needs a value, none given.", e.idlTag) 107 } 108 // For each extension value, validate that it is allowed. 109 allowedValues := e.allowedValues() 110 if !allowedValues.HasAll(e.values...) { 111 return fmt.Errorf("%v not allowed for %s. Allowed values: %v", 112 e.values, e.idlTag, allowedValues.List()) 113 } 114 return nil 115 } 116 117 func (e extension) validateType(kind types.Kind) error { 118 // If this extension class has no kind, then don't validate the type. 119 if !e.hasKind() { 120 return nil 121 } 122 if kind != e.kind() { 123 return fmt.Errorf("tag %s on type %v; only allowed on type %v", 124 e.idlTag, kind, e.kind()) 125 } 126 return nil 127 } 128 129 func (e extension) hasMultipleValues() bool { 130 return len(e.values) > 1 131 } 132 133 func (e extension) isAlwaysArrayFormat() bool { 134 return tagToExtension[e.idlTag].enforceArray 135 } 136 137 // Returns sorted list of map keys. Needed for deterministic testing. 138 func sortedMapKeys(m map[string][]string) []string { 139 keys := make([]string, len(m)) 140 i := 0 141 for k := range m { 142 keys[i] = k 143 i++ 144 } 145 sort.Strings(keys) 146 return keys 147 } 148 149 // Parses comments to return openapi extensions. Returns a list of 150 // extensions which parsed correctly, as well as a list of the 151 // parse errors. Validating extensions is performed separately. 152 // NOTE: Non-empty errors does not mean extensions is empty. 153 func parseExtensions(comments []string) ([]extension, []error) { 154 extensions := []extension{} 155 errors := []error{} 156 // First, generate extensions from "+k8s:openapi-gen=x-kubernetes-*" annotations. 157 values := getOpenAPITagValue(comments) 158 for _, val := range values { 159 // Example: x-kubernetes-member-tag:member_test 160 if strings.HasPrefix(val, extensionPrefix) { 161 parts := strings.SplitN(val, ":", 2) 162 if len(parts) != 2 { 163 errors = append(errors, fmt.Errorf("invalid extension value: %v", val)) 164 continue 165 } 166 e := extension{ 167 idlTag: tagName, // Example: k8s:openapi-gen 168 xName: parts[0], // Example: x-kubernetes-member-tag 169 values: []string{parts[1]}, // Example: member_test 170 } 171 extensions = append(extensions, e) 172 } 173 } 174 // Next, generate extensions from "idlTags" (e.g. +listType) 175 tagValues := gengo.ExtractCommentTags("+", comments) 176 for _, idlTag := range sortedMapKeys(tagValues) { 177 xAttrs, exists := tagToExtension[idlTag] 178 if !exists { 179 continue 180 } 181 values := tagValues[idlTag] 182 e := extension{ 183 idlTag: idlTag, // listType 184 xName: xAttrs.xName, // x-kubernetes-list-type 185 values: values, // [atomic] 186 } 187 extensions = append(extensions, e) 188 } 189 return extensions, errors 190 } 191 192 func validateMemberExtensions(extensions []extension, m *types.Member) []error { 193 errors := []error{} 194 for _, e := range extensions { 195 if err := e.validateAllowedValues(); err != nil { 196 errors = append(errors, err) 197 } 198 if err := e.validateType(m.Type.Kind); err != nil { 199 errors = append(errors, err) 200 } 201 } 202 return errors 203 }