...

Source file src/github.com/emissary-ingress/emissary/v3/pkg/kates/validation.go

Documentation: github.com/emissary-ingress/emissary/v3/pkg/kates

     1  package kates
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"path"
     8  	"sync"
     9  
    10  	apiextVInternal "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
    11  	apiextV1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    12  	apiextV1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    13  	"k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
    14  	"k8s.io/kube-openapi/pkg/validation/validate"
    15  
    16  	"github.com/datawire/dlib/derror"
    17  )
    18  
    19  // A Validator may be used in concert with a Client to perform
    20  // validate of freeform jsonish data structures as kubernetes CRDs.
    21  type Validator struct {
    22  	client *Client
    23  	static map[TypeMeta]*apiextVInternal.CustomResourceDefinition
    24  
    25  	mutex      sync.Mutex
    26  	validators map[TypeMeta]*validate.SchemaValidator
    27  }
    28  
    29  // The NewValidator constructor returns a *Validator that uses the
    30  // provided *Client to fetch CustomResourceDefinitions from kubernetes
    31  // on demand as needed to validate data passed to the Validator.Validate()
    32  // method.
    33  func NewValidator(client *Client, staticCRDs []Object) (*Validator, error) {
    34  	if client == nil && len(staticCRDs) == 0 {
    35  		return nil, errors.New("at least 1 client or static CRD must be provided")
    36  	}
    37  
    38  	static := make(map[TypeMeta]*apiextVInternal.CustomResourceDefinition, len(staticCRDs))
    39  	for i, untypedCRD := range staticCRDs {
    40  		var crd apiextVInternal.CustomResourceDefinition
    41  		switch untypedCRD.GetObjectKind().GroupVersionKind() {
    42  		case apiextV1beta1.SchemeGroupVersion.WithKind("CustomResourceDefinition"):
    43  			var crdV1beta1 apiextV1beta1.CustomResourceDefinition
    44  			if err := convert(untypedCRD, &crdV1beta1); err != nil {
    45  				return nil, fmt.Errorf("staticCRDs[%d]: %w", i, err)
    46  			}
    47  			apiextV1beta1.SetDefaults_CustomResourceDefinition(&crdV1beta1)
    48  			if err := apiextV1beta1.Convert_v1beta1_CustomResourceDefinition_To_apiextensions_CustomResourceDefinition(&crdV1beta1, &crd, nil); err != nil {
    49  				return nil, fmt.Errorf("staticCRDs[%d]: %w", i, err)
    50  			}
    51  		case apiextV1.SchemeGroupVersion.WithKind("CustomResourceDefinition"):
    52  			var crdV1 apiextV1.CustomResourceDefinition
    53  			if err := convert(untypedCRD, &crdV1); err != nil {
    54  				return nil, fmt.Errorf("staticCRDs[%d]: %w", i, err)
    55  			}
    56  			apiextV1.SetDefaults_CustomResourceDefinition(&crdV1)
    57  			if err := apiextV1.Convert_v1_CustomResourceDefinition_To_apiextensions_CustomResourceDefinition(&crdV1, &crd, nil); err != nil {
    58  				return nil, fmt.Errorf("staticCRDs[%d]: %w", i, err)
    59  			}
    60  		default:
    61  			err := fmt.Errorf("unrecognized CRD GroupVersionKind: %v", untypedCRD.GetObjectKind().GroupVersionKind())
    62  			return nil, fmt.Errorf("staticCRDs[%d]: %w", i, err)
    63  		}
    64  		for _, version := range crd.Spec.Versions {
    65  			static[TypeMeta{
    66  				APIVersion: crd.Spec.Group + "/" + version.Name,
    67  				Kind:       crd.Spec.Names.Kind,
    68  			}] = &crd
    69  		}
    70  	}
    71  
    72  	return &Validator{
    73  		client: client,
    74  		static: static,
    75  
    76  		validators: make(map[TypeMeta]*validate.SchemaValidator),
    77  	}, nil
    78  }
    79  
    80  func (v *Validator) getCRD(ctx context.Context, tm TypeMeta) (*apiextVInternal.CustomResourceDefinition, error) {
    81  	if crd, ok := v.static[tm]; ok {
    82  		return crd, nil
    83  	}
    84  	if v.client != nil {
    85  		mapping, err := v.client.mappingFor(tm.GroupVersionKind().GroupKind().String())
    86  		if err != nil {
    87  			return nil, err
    88  		}
    89  		crd := mapping.Resource.GroupResource().String()
    90  
    91  		obj := &apiextV1.CustomResourceDefinition{
    92  			TypeMeta: TypeMeta{
    93  				Kind: "CustomResourceDefinition",
    94  			},
    95  			ObjectMeta: ObjectMeta{
    96  				Name: crd,
    97  			},
    98  		}
    99  		err = v.client.Get(ctx, obj, obj)
   100  		if err != nil {
   101  			if IsNotFound(err) {
   102  				return nil, nil
   103  			}
   104  
   105  			return nil, err
   106  		}
   107  
   108  		var ret apiextVInternal.CustomResourceDefinition
   109  		err = apiextV1.Convert_v1_CustomResourceDefinition_To_apiextensions_CustomResourceDefinition(obj, &ret, nil)
   110  		if err != nil {
   111  			return nil, err
   112  		}
   113  		return &ret, nil
   114  	}
   115  	return nil, nil
   116  }
   117  
   118  func (v *Validator) getValidator(ctx context.Context, tm TypeMeta) (*validate.SchemaValidator, error) {
   119  	v.mutex.Lock()
   120  	defer v.mutex.Unlock()
   121  
   122  	validator, ok := v.validators[tm]
   123  	if !ok {
   124  		crd, err := v.getCRD(ctx, tm)
   125  		if err != nil {
   126  			return nil, err
   127  		}
   128  
   129  		if crd != nil {
   130  			if crd.Spec.Validation != nil {
   131  				validator, _, err = validation.NewSchemaValidator(crd.Spec.Validation)
   132  				if err != nil {
   133  					return nil, err
   134  				}
   135  			} else {
   136  				tmVersion := path.Base(tm.APIVersion)
   137  				for _, version := range crd.Spec.Versions {
   138  					if version.Name == tmVersion {
   139  						validator, _, err = validation.NewSchemaValidator(version.Schema)
   140  						if err != nil {
   141  							return nil, err
   142  						}
   143  						break
   144  					}
   145  				}
   146  			}
   147  		}
   148  
   149  		v.validators[tm] = validator // even if validator is nil; cache negative responses
   150  	}
   151  	return validator, nil
   152  }
   153  
   154  // The Validate method validates the supplied jsonish object as a
   155  // kubernetes CRD instance.
   156  //
   157  // If the supplied object is *not* a CRD instance but instead a
   158  // regular kubernetes instance, the Validate method will assume that
   159  // the supplied object is valid.
   160  //
   161  // If the supplied object is not a valid kubernetes resource at all,
   162  // the Validate method will return an error.
   163  //
   164  // Typically the Validate method will perform only local operations,
   165  // however the first time an instance of a given Kind is supplied, the
   166  // Validator needs to query the cluster to figure out if it is a CRD
   167  // and if so to fetch the schema needed to perform validation. All
   168  // subsequent Validate() calls for that Kind will be local.
   169  func (v *Validator) Validate(ctx context.Context, resource interface{}) error {
   170  	var tm TypeMeta
   171  	err := convert(resource, &tm)
   172  	if err != nil {
   173  		return err
   174  	}
   175  
   176  	validator, err := v.getValidator(ctx, tm)
   177  	if err != nil {
   178  		return err
   179  	}
   180  
   181  	result := validator.Validate(resource)
   182  
   183  	var errs derror.MultiError
   184  	for _, e := range result.Errors {
   185  		errs = append(errs, e)
   186  	}
   187  
   188  	for _, w := range result.Warnings {
   189  		errs = append(errs, w)
   190  	}
   191  
   192  	if len(errs) > 0 {
   193  		return errs
   194  	}
   195  
   196  	return nil
   197  }
   198  

View as plain text