istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/config/crd/validator.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package crd
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"io"
    21  	"os"
    22  	"path/filepath"
    23  	"regexp"
    24  	"strings"
    25  	"sync"
    26  
    27  	"github.com/hashicorp/go-multierror"
    28  	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
    29  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    30  	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    31  	structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
    32  	"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
    33  	structuraldefaulting "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting"
    34  	"k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
    35  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    36  	"k8s.io/apimachinery/pkg/runtime"
    37  	"k8s.io/apimachinery/pkg/runtime/schema"
    38  	kubeyaml "k8s.io/apimachinery/pkg/util/yaml"
    39  	celconfig "k8s.io/apiserver/pkg/apis/cel"
    40  	"sigs.k8s.io/yaml"
    41  
    42  	"istio.io/istio/pkg/test"
    43  	"istio.io/istio/pkg/test/env"
    44  	"istio.io/istio/pkg/test/util/yml"
    45  	"istio.io/istio/pkg/util/sets"
    46  )
    47  
    48  // Validator returns a new validator for custom resources
    49  // Warning: this is meant for usage in tests only
    50  type Validator struct {
    51  	byGvk      map[schema.GroupVersionKind]validation.SchemaCreateValidator
    52  	structural map[schema.GroupVersionKind]*structuralschema.Structural
    53  	cel        map[schema.GroupVersionKind]*cel.Validator
    54  	// If enabled, resources without a validator will be ignored. Otherwise, they will fail.
    55  	SkipMissing bool
    56  }
    57  
    58  type ValidationIgnorer struct {
    59  	mu                  sync.RWMutex
    60  	patternsByNamespace map[string]sets.String
    61  }
    62  
    63  // NewValidationIgnorer initializes the ignorer for the validatior, pairs are in namespace/namePattern format.
    64  func NewValidationIgnorer(pairs ...string) *ValidationIgnorer {
    65  	vi := &ValidationIgnorer{
    66  		patternsByNamespace: make(map[string]sets.String),
    67  	}
    68  	for _, pair := range pairs {
    69  		parts := strings.SplitN(pair, "/", 2)
    70  		if len(parts) != 2 {
    71  			continue
    72  		}
    73  		vi.Add(parts[0], parts[1])
    74  	}
    75  	return vi
    76  }
    77  
    78  func (iv *ValidationIgnorer) Add(namespace, pattern string) {
    79  	iv.mu.Lock()
    80  	defer iv.mu.Unlock()
    81  	if iv.patternsByNamespace[namespace] == nil {
    82  		iv.patternsByNamespace[namespace] = sets.String{}
    83  	}
    84  	iv.patternsByNamespace[namespace].Insert(pattern)
    85  }
    86  
    87  // ShouldIgnore checks if a given namespaced name should be ignored based on the patterns.
    88  func (iv *ValidationIgnorer) ShouldIgnore(namespace, name string) bool {
    89  	iv.mu.RLock()
    90  	defer iv.mu.RUnlock()
    91  
    92  	patterns, exists := iv.patternsByNamespace[namespace]
    93  	if !exists {
    94  		return false
    95  	}
    96  
    97  	for _, pattern := range patterns.UnsortedList() {
    98  		match, err := regexp.MatchString(pattern, name)
    99  		if err != nil {
   100  			continue
   101  		}
   102  		if match {
   103  			return true
   104  		}
   105  	}
   106  	return false
   107  }
   108  
   109  func (v *Validator) ValidateCustomResourceYAML(data string, ignorer *ValidationIgnorer) error {
   110  	var errs *multierror.Error
   111  	for _, item := range yml.SplitString(data) {
   112  		obj := &unstructured.Unstructured{}
   113  		if err := yaml.Unmarshal([]byte(item), obj); err != nil {
   114  			return err
   115  		}
   116  		if ignorer != nil && ignorer.ShouldIgnore(obj.GetNamespace(), obj.GetName()) {
   117  			continue
   118  		}
   119  		errs = multierror.Append(errs, v.ValidateCustomResource(obj))
   120  	}
   121  	return errs.ErrorOrNil()
   122  }
   123  
   124  func (v *Validator) ValidateCustomResource(o runtime.Object) error {
   125  	content, err := runtime.DefaultUnstructuredConverter.ToUnstructured(o)
   126  	if err != nil {
   127  		return err
   128  	}
   129  
   130  	un := &unstructured.Unstructured{Object: content}
   131  	vd, f := v.byGvk[un.GroupVersionKind()]
   132  	if !f {
   133  		if v.SkipMissing {
   134  			return nil
   135  		}
   136  		return fmt.Errorf("failed to validate type %v: no validator found", un.GroupVersionKind())
   137  	}
   138  	// Fill in defaults
   139  	structural := v.structural[un.GroupVersionKind()]
   140  	structuraldefaulting.Default(un.Object, structural)
   141  	if err := validation.ValidateCustomResource(nil, un.Object, vd).ToAggregate(); err != nil {
   142  		return fmt.Errorf("%v/%v/%v: %v", un.GroupVersionKind().Kind, un.GetName(), un.GetNamespace(), err)
   143  	}
   144  	errs, _ := v.cel[un.GroupVersionKind()].Validate(context.Background(), nil, structural, un.Object, nil, celconfig.RuntimeCELCostBudget)
   145  	if errs.ToAggregate() != nil {
   146  		return fmt.Errorf("%v/%v/%v: %v", un.GroupVersionKind().Kind, un.GetName(), un.GetNamespace(), errs.ToAggregate().Error())
   147  	}
   148  	return nil
   149  }
   150  
   151  func NewValidatorFromFiles(files ...string) (*Validator, error) {
   152  	crds := []apiextensions.CustomResourceDefinition{}
   153  	closers := make([]io.Closer, 0, len(files))
   154  	defer func() {
   155  		for _, closer := range closers {
   156  			closer.Close()
   157  		}
   158  	}()
   159  	for _, file := range files {
   160  		data, err := os.Open(file)
   161  		if err != nil {
   162  			return nil, fmt.Errorf("failed to read input yaml file: %v", err)
   163  		}
   164  		closers = append(closers, data)
   165  
   166  		yamlDecoder := kubeyaml.NewYAMLOrJSONDecoder(data, 512*1024)
   167  		for {
   168  			un := &unstructured.Unstructured{}
   169  			err = yamlDecoder.Decode(&un)
   170  			if err == io.EOF {
   171  				break
   172  			}
   173  			if err != nil {
   174  				return nil, err
   175  			}
   176  			crd := apiextensions.CustomResourceDefinition{}
   177  			switch un.GroupVersionKind() {
   178  			case schema.GroupVersionKind{
   179  				Group:   "apiextensions.k8s.io",
   180  				Version: "v1",
   181  				Kind:    "CustomResourceDefinition",
   182  			}:
   183  				crdv1 := apiextensionsv1.CustomResourceDefinition{}
   184  				if err := runtime.DefaultUnstructuredConverter.
   185  					FromUnstructured(un.UnstructuredContent(), &crdv1); err != nil {
   186  					return nil, err
   187  				}
   188  				if err := apiextensionsv1.Convert_v1_CustomResourceDefinition_To_apiextensions_CustomResourceDefinition(&crdv1, &crd, nil); err != nil {
   189  					return nil, err
   190  				}
   191  			case schema.GroupVersionKind{
   192  				Group:   "apiextensions.k8s.io",
   193  				Version: "v1beta1",
   194  				Kind:    "CustomResourceDefinition",
   195  			}:
   196  				crdv1beta1 := apiextensionsv1beta1.CustomResourceDefinition{}
   197  				if err := runtime.DefaultUnstructuredConverter.
   198  					FromUnstructured(un.UnstructuredContent(), &crdv1beta1); err != nil {
   199  					return nil, err
   200  				}
   201  				if err := apiextensionsv1beta1.Convert_v1beta1_CustomResourceDefinition_To_apiextensions_CustomResourceDefinition(&crdv1beta1, &crd, nil); err != nil {
   202  					return nil, err
   203  				}
   204  			default:
   205  				return nil, fmt.Errorf("unknown CRD type: %v", un.GroupVersionKind())
   206  			}
   207  			crds = append(crds, crd)
   208  		}
   209  	}
   210  	return NewValidatorFromCRDs(crds...)
   211  }
   212  
   213  func NewValidatorFromCRDs(crds ...apiextensions.CustomResourceDefinition) (*Validator, error) {
   214  	v := &Validator{
   215  		byGvk:      map[schema.GroupVersionKind]validation.SchemaCreateValidator{},
   216  		structural: map[schema.GroupVersionKind]*structuralschema.Structural{},
   217  		cel:        map[schema.GroupVersionKind]*cel.Validator{},
   218  	}
   219  	for _, crd := range crds {
   220  		versions := crd.Spec.Versions
   221  		if len(versions) == 0 {
   222  			versions = []apiextensions.CustomResourceDefinitionVersion{{Name: crd.Spec.Version}} // nolint: staticcheck
   223  		}
   224  		for _, ver := range versions {
   225  			gvk := schema.GroupVersionKind{
   226  				Group:   crd.Spec.Group,
   227  				Version: ver.Name,
   228  				Kind:    crd.Spec.Names.Kind,
   229  			}
   230  			crdSchema := ver.Schema
   231  			if crdSchema == nil {
   232  				crdSchema = crd.Spec.Validation
   233  			}
   234  			if crdSchema == nil {
   235  				return nil, fmt.Errorf("crd did not have validation defined")
   236  			}
   237  
   238  			schemaValidator, _, err := validation.NewSchemaValidator(crdSchema.OpenAPIV3Schema)
   239  			if err != nil {
   240  				return nil, err
   241  			}
   242  			structural, err := structuralschema.NewStructural(crdSchema.OpenAPIV3Schema)
   243  			if err != nil {
   244  				return nil, err
   245  			}
   246  
   247  			v.byGvk[gvk] = schemaValidator
   248  			v.structural[gvk] = structural
   249  			// CEL programs are compiled and cached here
   250  			if celv := cel.NewValidator(structural, true, celconfig.PerCallLimit); celv != nil {
   251  				v.cel[gvk] = celv
   252  			}
   253  
   254  		}
   255  	}
   256  
   257  	return v, nil
   258  }
   259  
   260  func NewIstioValidator(t test.Failer) *Validator {
   261  	v, err := NewValidatorFromFiles(
   262  		filepath.Join(env.IstioSrc, "tests/integration/pilot/testdata/gateway-api-crd.yaml"),
   263  		filepath.Join(env.IstioSrc, "manifests/charts/base/crds/crd-all.gen.yaml"))
   264  	if err != nil {
   265  		t.Fatal(err)
   266  	}
   267  	return v
   268  }