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  }