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 }