...

Source file src/k8s.io/cli-runtime/pkg/resource/query_param_verifier.go

Documentation: k8s.io/cli-runtime/pkg/resource

     1  /*
     2  Copyright 2019 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 resource
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  
    23  	openapi_v2 "github.com/google/gnostic-models/openapiv2"
    24  	yaml "gopkg.in/yaml.v2"
    25  
    26  	"k8s.io/apimachinery/pkg/runtime/schema"
    27  	"k8s.io/client-go/discovery"
    28  	"k8s.io/client-go/dynamic"
    29  )
    30  
    31  func NewQueryParamVerifier(dynamicClient dynamic.Interface, openAPIGetter discovery.OpenAPISchemaInterface, queryParam VerifiableQueryParam) *QueryParamVerifier {
    32  	return &QueryParamVerifier{
    33  		finder:        NewCRDFinder(CRDFromDynamic(dynamicClient)),
    34  		openAPIGetter: openAPIGetter,
    35  		queryParam:    queryParam,
    36  	}
    37  }
    38  
    39  // QueryParamVerifier verifies if a given group-version-kind supports a
    40  // given VerifiableQueryParam against the current server.
    41  //
    42  // Currently supported query params are: fieldValidation
    43  //
    44  // Support for each of these query params needs to be verified because
    45  // we determine whether or not to perform server-side or client-side
    46  // schema validation based on whether the fieldValidation query param is
    47  // supported or not.
    48  //
    49  // It reads the OpenAPI to see if the given GVK supports the given query param.
    50  // If the GVK can not be found, we assume that CRDs will have the same level of
    51  // support as "namespaces", and non-CRDs will not be supported. We
    52  // delay the check for CRDs as much as possible though, since it
    53  // requires an extra round-trip to the server.
    54  type QueryParamVerifier struct {
    55  	finder        CRDFinder
    56  	openAPIGetter discovery.OpenAPISchemaInterface
    57  	queryParam    VerifiableQueryParam
    58  }
    59  
    60  // Verifier is the generic verifier interface used for testing QueryParamVerifier
    61  type Verifier interface {
    62  	HasSupport(gvk schema.GroupVersionKind) error
    63  }
    64  
    65  // VerifiableQueryParam is a query parameter who's enablement on the
    66  // apiserver can be determined by evaluating the OpenAPI for a specific
    67  // GVK.
    68  type VerifiableQueryParam string
    69  
    70  const (
    71  	QueryParamFieldValidation VerifiableQueryParam = "fieldValidation"
    72  )
    73  
    74  // HasSupport checks if the given gvk supports the query param configured on v
    75  func (v *QueryParamVerifier) HasSupport(gvk schema.GroupVersionKind) error {
    76  	if (gvk == schema.GroupVersionKind{Version: "v1", Kind: "List"}) {
    77  		return NewParamUnsupportedError(gvk, v.queryParam)
    78  	}
    79  
    80  	oapi, err := v.openAPIGetter.OpenAPISchema()
    81  	if err != nil {
    82  		return fmt.Errorf("failed to download openapi: %v", err)
    83  	}
    84  	supports, err := supportsQueryParam(oapi, gvk, v.queryParam)
    85  	if err != nil {
    86  		// We assume that we couldn't find the type, then check for namespace:
    87  		supports, _ = supportsQueryParam(oapi, schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"}, v.queryParam)
    88  		// If namespace supports the query param, then we will support the query param for CRDs only.
    89  		if supports {
    90  			supports, err = v.finder.HasCRD(gvk.GroupKind())
    91  			if err != nil {
    92  				return fmt.Errorf("failed to check CRD: %v", err)
    93  			}
    94  		}
    95  	}
    96  	if !supports {
    97  		return NewParamUnsupportedError(gvk, v.queryParam)
    98  	}
    99  	return nil
   100  }
   101  
   102  type paramUnsupportedError struct {
   103  	gvk   schema.GroupVersionKind
   104  	param VerifiableQueryParam
   105  }
   106  
   107  func NewParamUnsupportedError(gvk schema.GroupVersionKind, param VerifiableQueryParam) error {
   108  	return &paramUnsupportedError{
   109  		gvk:   gvk,
   110  		param: param,
   111  	}
   112  }
   113  
   114  func (e *paramUnsupportedError) Error() string {
   115  	return fmt.Sprintf("%v doesn't support %s", e.gvk, e.param)
   116  }
   117  
   118  func IsParamUnsupportedError(err error) bool {
   119  	if err == nil {
   120  		return false
   121  	}
   122  	_, ok := err.(*paramUnsupportedError)
   123  	return ok
   124  }
   125  
   126  func hasGVKExtension(extensions []*openapi_v2.NamedAny, gvk schema.GroupVersionKind) bool {
   127  	for _, extension := range extensions {
   128  		if extension.GetValue().GetYaml() == "" ||
   129  			extension.GetName() != "x-kubernetes-group-version-kind" {
   130  			continue
   131  		}
   132  		var value map[string]string
   133  		err := yaml.Unmarshal([]byte(extension.GetValue().GetYaml()), &value)
   134  		if err != nil {
   135  			continue
   136  		}
   137  
   138  		if value["group"] == gvk.Group && value["kind"] == gvk.Kind && value["version"] == gvk.Version {
   139  			return true
   140  		}
   141  		return false
   142  	}
   143  	return false
   144  }
   145  
   146  // supportsQueryParam is a method that let's us look in the OpenAPI if the
   147  // specific group-version-kind supports the specific query parameter for
   148  // the PATCH end-point.
   149  func supportsQueryParam(doc *openapi_v2.Document, gvk schema.GroupVersionKind, queryParam VerifiableQueryParam) (bool, error) {
   150  	globalParams := map[string]*openapi_v2.NamedParameter{}
   151  	for _, p := range doc.GetParameters().GetAdditionalProperties() {
   152  		globalParams["#/parameters/"+p.GetName()] = p
   153  	}
   154  
   155  	for _, path := range doc.GetPaths().GetPath() {
   156  		// Is this describing the gvk we're looking for?
   157  		if !hasGVKExtension(path.GetValue().GetPatch().GetVendorExtension(), gvk) {
   158  			continue
   159  		}
   160  		for _, param := range path.GetValue().GetPatch().GetParameters() {
   161  			if param.GetParameter().GetNonBodyParameter().GetQueryParameterSubSchema().GetName() == string(queryParam) {
   162  				return true, nil
   163  			}
   164  
   165  			// lookup global parameters
   166  			if ref := param.GetJsonReference().GetXRef(); ref != "" {
   167  				if globalParam, ok := globalParams[ref]; ok && globalParam != nil && globalParam.GetValue().GetNonBodyParameter().GetQueryParameterSubSchema().GetName() == string(queryParam) {
   168  					return true, nil
   169  				}
   170  			}
   171  		}
   172  		return false, nil
   173  	}
   174  
   175  	return false, errors.New("couldn't find GVK in openapi")
   176  }
   177  

View as plain text