k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/builder/parameters.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 builder 18 19 import ( 20 "encoding/base64" 21 "encoding/json" 22 "fmt" 23 "hash/fnv" 24 "sort" 25 "strconv" 26 "strings" 27 28 "k8s.io/kube-openapi/pkg/validation/spec" 29 ) 30 31 // deduplicateParameters finds parameters that are shared across multiple endpoints and replace them with 32 // references to the shared parameters in order to avoid repetition. 33 // 34 // deduplicateParameters does not mutate the source. 35 func deduplicateParameters(sp *spec.Swagger) (*spec.Swagger, error) { 36 names, parameters, err := collectSharedParameters(sp) 37 if err != nil { 38 return nil, err 39 } 40 41 if sp.Parameters != nil { 42 return nil, fmt.Errorf("shared parameters already exist") // should not happen with the builder, but to be sure 43 } 44 45 clone := *sp 46 clone.Parameters = parameters 47 return replaceSharedParameters(names, &clone) 48 } 49 50 // collectSharedParameters finds parameters that show up for many endpoints. These 51 // are basically all parameters with the exceptions of those where we know they are 52 // endpoint specific, e.g. because they reference the schema of the kind, or have 53 // the kind or resource name in the description. 54 func collectSharedParameters(sp *spec.Swagger) (namesByJSON map[string]string, ret map[string]spec.Parameter, err error) { 55 if sp == nil || sp.Paths == nil { 56 return nil, nil, nil 57 } 58 59 countsByJSON := map[string]int{} 60 shared := map[string]spec.Parameter{} 61 var keys []string 62 63 collect := func(p *spec.Parameter) error { 64 if (p.In == "query" || p.In == "path") && p.Name == "name" { 65 return nil // ignore name parameter as they are never shared with the Kind in the description 66 } 67 if p.In == "query" && p.Name == "fieldValidation" { 68 return nil // keep fieldValidation parameter unshared because kubectl uses it (until 1.27) to detect server-side field validation support 69 } 70 if p.In == "query" && p.Name == "dryRun" { 71 return nil // keep fieldValidation parameter unshared because kubectl uses it (until 1.26) to detect dry-run support 72 } 73 if p.Schema != nil && p.In == "body" && p.Name == "body" && !strings.HasPrefix(p.Schema.Ref.String(), "#/definitions/io.k8s.apimachinery") { 74 return nil // ignore non-generic body parameters as they reference the custom schema of the kind 75 } 76 77 bs, err := json.Marshal(p) 78 if err != nil { 79 return err 80 } 81 82 k := string(bs) 83 countsByJSON[k]++ 84 if count := countsByJSON[k]; count == 1 { 85 shared[k] = *p 86 keys = append(keys, k) 87 } 88 89 return nil 90 } 91 92 for _, path := range sp.Paths.Paths { 93 // per operation parameters 94 for _, op := range operations(&path) { 95 if op == nil { 96 continue // shouldn't happen, but ignore if it does; tested through unit test 97 } 98 for _, p := range op.Parameters { 99 if p.Ref.String() != "" { 100 // shouldn't happen, but ignore if it does 101 continue 102 } 103 if err := collect(&p); err != nil { 104 return nil, nil, err 105 } 106 } 107 } 108 109 // per path parameters 110 for _, p := range path.Parameters { 111 if p.Ref.String() != "" { 112 continue // shouldn't happen, but ignore if it does 113 } 114 if err := collect(&p); err != nil { 115 return nil, nil, err 116 } 117 } 118 } 119 120 // name deterministically 121 sort.Strings(keys) 122 ret = map[string]spec.Parameter{} 123 namesByJSON = map[string]string{} 124 for _, k := range keys { 125 name := shared[k].Name 126 if name == "" { 127 // this should never happen as the name is a required field. But if it does, let's be safe. 128 name = "param" 129 } 130 name += "-" + base64Hash(k) 131 i := 0 132 for { 133 if _, ok := ret[name]; !ok { 134 ret[name] = shared[k] 135 namesByJSON[k] = name 136 break 137 } 138 i++ // only on hash conflict, unlikely with our few variants 139 name = shared[k].Name + "-" + strconv.Itoa(i) 140 } 141 } 142 143 return namesByJSON, ret, nil 144 } 145 146 func operations(path *spec.PathItem) []*spec.Operation { 147 return []*spec.Operation{path.Get, path.Put, path.Post, path.Delete, path.Options, path.Head, path.Patch} 148 } 149 150 func base64Hash(s string) string { 151 hash := fnv.New64() 152 hash.Write([]byte(s)) //nolint:errcheck 153 return base64.URLEncoding.EncodeToString(hash.Sum(make([]byte, 0, 8))[:6]) // 8 characters 154 } 155 156 func replaceSharedParameters(sharedParameterNamesByJSON map[string]string, sp *spec.Swagger) (*spec.Swagger, error) { 157 if sp == nil || sp.Paths == nil { 158 return sp, nil 159 } 160 161 ret := sp 162 163 firstPathChange := true 164 for k, path := range sp.Paths.Paths { 165 pathChanged := false 166 167 // per operation parameters 168 for _, op := range []**spec.Operation{&path.Get, &path.Put, &path.Post, &path.Delete, &path.Options, &path.Head, &path.Patch} { 169 if *op == nil { 170 continue 171 } 172 173 firstParamChange := true 174 for i := range (*op).Parameters { 175 p := (*op).Parameters[i] 176 177 if p.Ref.String() != "" { 178 // shouldn't happen, but be idem-potent if it does 179 continue 180 } 181 182 bs, err := json.Marshal(p) 183 if err != nil { 184 return nil, err 185 } 186 187 if name, ok := sharedParameterNamesByJSON[string(bs)]; ok { 188 if firstParamChange { 189 orig := *op 190 *op = &spec.Operation{} 191 **op = *orig 192 (*op).Parameters = make([]spec.Parameter, len(orig.Parameters)) 193 copy((*op).Parameters, orig.Parameters) 194 firstParamChange = false 195 } 196 197 (*op).Parameters[i] = spec.Parameter{ 198 Refable: spec.Refable{ 199 Ref: spec.MustCreateRef("#/parameters/" + name), 200 }, 201 } 202 pathChanged = true 203 } 204 } 205 } 206 207 // per path parameters 208 firstParamChange := true 209 for i := range path.Parameters { 210 p := path.Parameters[i] 211 212 if p.Ref.String() != "" { 213 // shouldn't happen, but be idem-potent if it does 214 continue 215 } 216 217 bs, err := json.Marshal(p) 218 if err != nil { 219 return nil, err 220 } 221 222 if name, ok := sharedParameterNamesByJSON[string(bs)]; ok { 223 if firstParamChange { 224 orig := path.Parameters 225 path.Parameters = make([]spec.Parameter, len(orig)) 226 copy(path.Parameters, orig) 227 firstParamChange = false 228 } 229 230 path.Parameters[i] = spec.Parameter{ 231 Refable: spec.Refable{ 232 Ref: spec.MustCreateRef("#/parameters/" + name), 233 }, 234 } 235 pathChanged = true 236 } 237 } 238 239 if pathChanged { 240 if firstPathChange { 241 clone := *sp 242 ret = &clone 243 244 pathsClone := *ret.Paths 245 ret.Paths = &pathsClone 246 247 ret.Paths.Paths = make(map[string]spec.PathItem, len(sp.Paths.Paths)) 248 for k, v := range sp.Paths.Paths { 249 ret.Paths.Paths[k] = v 250 } 251 252 firstPathChange = false 253 } 254 ret.Paths.Paths[k] = path 255 } 256 } 257 258 return ret, nil 259 }