k8s.io/apiserver@v0.31.1/pkg/admission/plugin/policy/generic/policy_test_context.go (about) 1 /* 2 Copyright 2024 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 generic 18 19 import ( 20 "context" 21 "fmt" 22 "time" 23 24 corev1 "k8s.io/api/core/v1" 25 "k8s.io/apimachinery/pkg/api/errors" 26 "k8s.io/apimachinery/pkg/api/meta" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 29 "k8s.io/apimachinery/pkg/runtime" 30 "k8s.io/apimachinery/pkg/runtime/schema" 31 "k8s.io/apimachinery/pkg/runtime/serializer" 32 "k8s.io/apimachinery/pkg/types" 33 "k8s.io/apimachinery/pkg/util/uuid" 34 "k8s.io/apimachinery/pkg/util/wait" 35 "k8s.io/apimachinery/pkg/watch" 36 "k8s.io/client-go/dynamic" 37 dynamicfake "k8s.io/client-go/dynamic/fake" 38 "k8s.io/client-go/informers" 39 "k8s.io/client-go/kubernetes" 40 "k8s.io/client-go/kubernetes/fake" 41 clienttesting "k8s.io/client-go/testing" 42 "k8s.io/client-go/tools/cache" 43 "k8s.io/component-base/featuregate" 44 45 "k8s.io/apiserver/pkg/admission" 46 "k8s.io/apiserver/pkg/admission/initializer" 47 "k8s.io/apiserver/pkg/authorization/authorizer" 48 "k8s.io/apiserver/pkg/features" 49 ) 50 51 // PolicyTestContext is everything you need to unit test a policy plugin 52 type PolicyTestContext[P runtime.Object, B runtime.Object, E Evaluator] struct { 53 context.Context 54 Plugin *Plugin[PolicyHook[P, B, E]] 55 Source Source[PolicyHook[P, B, E]] 56 Start func() error 57 58 scheme *runtime.Scheme 59 restMapper *meta.DefaultRESTMapper 60 policyGVR schema.GroupVersionResource 61 bindingGVR schema.GroupVersionResource 62 63 policyGVK schema.GroupVersionKind 64 bindingGVK schema.GroupVersionKind 65 66 nativeTracker clienttesting.ObjectTracker 67 policyAndBindingTracker clienttesting.ObjectTracker 68 unstructuredTracker clienttesting.ObjectTracker 69 } 70 71 func NewPolicyTestContext[P, B runtime.Object, E Evaluator]( 72 newPolicyAccessor func(P) PolicyAccessor, 73 newBindingAccessor func(B) BindingAccessor, 74 compileFunc func(P) E, 75 dispatcher dispatcherFactory[PolicyHook[P, B, E]], 76 initialObjects []runtime.Object, 77 paramMappings []meta.RESTMapping, 78 ) (*PolicyTestContext[P, B, E], func(), error) { 79 var Pexample P 80 var Bexample B 81 82 // Create a fake resource and kind for the provided policy and binding types 83 fakePolicyGVR := schema.GroupVersionResource{ 84 Group: "policy.example.com", 85 Version: "v1", 86 Resource: "fakepolicies", 87 } 88 fakeBindingGVR := schema.GroupVersionResource{ 89 Group: "policy.example.com", 90 Version: "v1", 91 Resource: "fakebindings", 92 } 93 fakePolicyGVK := fakePolicyGVR.GroupVersion().WithKind("FakePolicy") 94 fakeBindingGVK := fakeBindingGVR.GroupVersion().WithKind("FakeBinding") 95 96 policySourceTestScheme, err := func() (*runtime.Scheme, error) { 97 scheme := runtime.NewScheme() 98 99 if err := fake.AddToScheme(scheme); err != nil { 100 return nil, err 101 } 102 103 scheme.AddKnownTypeWithName(fakePolicyGVK, Pexample) 104 scheme.AddKnownTypeWithName(fakeBindingGVK, Bexample) 105 scheme.AddKnownTypeWithName(fakePolicyGVK.GroupVersion().WithKind(fakePolicyGVK.Kind+"List"), &FakeList[P]{}) 106 scheme.AddKnownTypeWithName(fakeBindingGVK.GroupVersion().WithKind(fakeBindingGVK.Kind+"List"), &FakeList[B]{}) 107 108 for _, mapping := range paramMappings { 109 // Skip if it is in the scheme already 110 if scheme.Recognizes(mapping.GroupVersionKind) { 111 continue 112 } 113 scheme.AddKnownTypeWithName(mapping.GroupVersionKind, &unstructured.Unstructured{}) 114 scheme.AddKnownTypeWithName(mapping.GroupVersionKind.GroupVersion().WithKind(mapping.GroupVersionKind.Kind+"List"), &unstructured.UnstructuredList{}) 115 } 116 117 return scheme, nil 118 }() 119 if err != nil { 120 return nil, nil, err 121 } 122 123 fakeRestMapper := func() *meta.DefaultRESTMapper { 124 res := meta.NewDefaultRESTMapper([]schema.GroupVersion{ 125 { 126 Group: "", 127 Version: "v1", 128 }, 129 }) 130 131 res.Add(fakePolicyGVK, meta.RESTScopeRoot) 132 res.Add(fakeBindingGVK, meta.RESTScopeRoot) 133 res.Add(corev1.SchemeGroupVersion.WithKind("ConfigMap"), meta.RESTScopeNamespace) 134 135 for _, mapping := range paramMappings { 136 res.AddSpecific(mapping.GroupVersionKind, mapping.Resource, mapping.Resource, mapping.Scope) 137 } 138 139 return res 140 }() 141 142 nativeClient := fake.NewSimpleClientset() 143 dynamicClient := dynamicfake.NewSimpleDynamicClient(policySourceTestScheme) 144 fakeInformerFactory := informers.NewSharedInformerFactory(nativeClient, 30*time.Second) 145 146 // Make an object tracker specifically for our policies and bindings 147 policiesAndBindingsTracker := clienttesting.NewObjectTracker( 148 policySourceTestScheme, 149 serializer.NewCodecFactory(policySourceTestScheme).UniversalDecoder()) 150 151 // Make an informer for our policies and bindings 152 153 policyInformer := cache.NewSharedIndexInformer( 154 &cache.ListWatch{ 155 ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 156 return policiesAndBindingsTracker.List(fakePolicyGVR, fakePolicyGVK, "") 157 }, 158 WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 159 return policiesAndBindingsTracker.Watch(fakePolicyGVR, "") 160 }, 161 }, 162 Pexample, 163 30*time.Second, 164 cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, 165 ) 166 bindingInformer := cache.NewSharedIndexInformer( 167 &cache.ListWatch{ 168 ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 169 return policiesAndBindingsTracker.List(fakeBindingGVR, fakeBindingGVK, "") 170 }, 171 WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 172 return policiesAndBindingsTracker.Watch(fakeBindingGVR, "") 173 }, 174 }, 175 Bexample, 176 30*time.Second, 177 cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, 178 ) 179 180 var source Source[PolicyHook[P, B, E]] 181 plugin := NewPlugin[PolicyHook[P, B, E]]( 182 admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update), 183 func(sif informers.SharedInformerFactory, i1 kubernetes.Interface, i2 dynamic.Interface, r meta.RESTMapper) Source[PolicyHook[P, B, E]] { 184 source = NewPolicySource[P, B, E]( 185 policyInformer, 186 bindingInformer, 187 newPolicyAccessor, 188 newBindingAccessor, 189 compileFunc, 190 sif, 191 i2, 192 r, 193 ) 194 return source 195 }, dispatcher) 196 plugin.SetEnabled(true) 197 198 featureGate := featuregate.NewFeatureGate() 199 err = featureGate.Add(map[featuregate.Feature]featuregate.FeatureSpec{ 200 //!TODO: move this to validating specific tests 201 features.ValidatingAdmissionPolicy: { 202 Default: true, PreRelease: featuregate.Beta}}) 203 if err != nil { 204 return nil, nil, err 205 } 206 err = featureGate.SetFromMap(map[string]bool{string(features.ValidatingAdmissionPolicy): true}) 207 if err != nil { 208 return nil, nil, err 209 } 210 211 testContext, testCancel := context.WithCancel(context.Background()) 212 genericInitializer := initializer.New( 213 nativeClient, 214 dynamicClient, 215 fakeInformerFactory, 216 fakeAuthorizer{}, 217 featureGate, 218 testContext.Done(), 219 fakeRestMapper, 220 ) 221 genericInitializer.Initialize(plugin) 222 plugin.SetRESTMapper(fakeRestMapper) 223 224 if err := plugin.ValidateInitialization(); err != nil { 225 testCancel() 226 return nil, nil, err 227 } 228 229 res := &PolicyTestContext[P, B, E]{ 230 Context: testContext, 231 Plugin: plugin, 232 Source: source, 233 234 restMapper: fakeRestMapper, 235 scheme: policySourceTestScheme, 236 policyGVK: fakePolicyGVK, 237 bindingGVK: fakeBindingGVK, 238 policyGVR: fakePolicyGVR, 239 bindingGVR: fakeBindingGVR, 240 nativeTracker: nativeClient.Tracker(), 241 policyAndBindingTracker: policiesAndBindingsTracker, 242 unstructuredTracker: dynamicClient.Tracker(), 243 } 244 245 for _, obj := range initialObjects { 246 err := res.updateOne(obj) 247 if err != nil { 248 testCancel() 249 return nil, nil, err 250 } 251 } 252 253 res.Start = func() error { 254 fakeInformerFactory.Start(res.Done()) 255 go policyInformer.Run(res.Done()) 256 go bindingInformer.Run(res.Done()) 257 258 if !cache.WaitForCacheSync(res.Done(), res.Source.HasSynced) { 259 return fmt.Errorf("timed out waiting for initial cache sync") 260 } 261 return nil 262 } 263 return res, testCancel, nil 264 } 265 266 // UpdateAndWait updates the given object in the test, or creates it if it doesn't exist 267 // Depending upon object type, waits afterward until the object is synced 268 // by the policy source 269 // 270 // Be aware the UpdateAndWait will modify the ResourceVersion of the 271 // provided objects. 272 func (p *PolicyTestContext[P, B, E]) UpdateAndWait(objects ...runtime.Object) error { 273 return p.update(true, objects...) 274 } 275 276 // Update updates the given object in the test, or creates it if it doesn't exist 277 // 278 // Be aware the Update will modify the ResourceVersion of the 279 // provided objects. 280 func (p *PolicyTestContext[P, B, E]) Update(objects ...runtime.Object) error { 281 return p.update(false, objects...) 282 } 283 284 // Objects the given object in the test, or creates it if it doesn't exist 285 // Depending upon object type, waits afterward until the object is synced 286 // by the policy source 287 func (p *PolicyTestContext[P, B, E]) update(wait bool, objects ...runtime.Object) error { 288 for _, object := range objects { 289 if err := p.updateOne(object); err != nil { 290 return err 291 } 292 } 293 294 if wait { 295 timeoutCtx, timeoutCancel := context.WithTimeout(p, 3*time.Second) 296 defer timeoutCancel() 297 298 for _, object := range objects { 299 if err := p.WaitForReconcile(timeoutCtx, object); err != nil { 300 return fmt.Errorf("error waiting for reconcile of %v: %v", object, err) 301 } 302 } 303 } 304 return nil 305 } 306 307 // Depending upon object type, waits afterward until the object is synced 308 // by the policy source. Note that policies that are not bound are skipped, 309 // so you should not try to wait for an unbound policy. Create both the binding 310 // and policy, then wait. 311 func (p *PolicyTestContext[P, B, E]) WaitForReconcile(timeoutCtx context.Context, object runtime.Object) error { 312 if !p.Source.HasSynced() { 313 return nil 314 } 315 316 objectMeta, err := meta.Accessor(object) 317 if err != nil { 318 return err 319 } 320 321 objectGVK, _, err := p.inferGVK(object) 322 if err != nil { 323 return err 324 } 325 326 switch objectGVK { 327 case p.policyGVK: 328 return wait.PollUntilContextCancel(timeoutCtx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) { 329 policies := p.Source.Hooks() 330 for _, policy := range policies { 331 policyMeta, err := meta.Accessor(policy.Policy) 332 if err != nil { 333 return true, err 334 } else if policyMeta.GetName() == objectMeta.GetName() && policyMeta.GetResourceVersion() == objectMeta.GetResourceVersion() { 335 return true, nil 336 } 337 } 338 return false, nil 339 }) 340 case p.bindingGVK: 341 return wait.PollUntilContextCancel(timeoutCtx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) { 342 policies := p.Source.Hooks() 343 for _, policy := range policies { 344 for _, binding := range policy.Bindings { 345 bindingMeta, err := meta.Accessor(binding) 346 if err != nil { 347 return true, err 348 } else if bindingMeta.GetName() == objectMeta.GetName() && bindingMeta.GetResourceVersion() == objectMeta.GetResourceVersion() { 349 return true, nil 350 } 351 } 352 } 353 return false, nil 354 }) 355 356 default: 357 // Do nothing, params are visible immediately 358 // Loop until one of the params is visible via get of the param informer 359 return wait.PollUntilContextCancel(timeoutCtx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) { 360 informer, scope := p.Source.(*policySource[P, B, E]).getParamInformer(objectGVK) 361 if informer == nil { 362 // Informer does not exist yet, keep waiting for sync 363 return false, nil 364 } 365 366 if !cache.WaitForCacheSync(timeoutCtx.Done(), informer.Informer().HasSynced) { 367 return false, fmt.Errorf("timed out waiting for cache sync of param informer") 368 } 369 370 var lister cache.GenericNamespaceLister = informer.Lister() 371 if scope == meta.RESTScopeNamespace { 372 lister = informer.Lister().ByNamespace(objectMeta.GetNamespace()) 373 } 374 375 fetched, err := lister.Get(objectMeta.GetName()) 376 if err != nil { 377 if errors.IsNotFound(err) { 378 return false, nil 379 } 380 return true, err 381 } 382 383 // Ensure RV matches 384 fetchedMeta, err := meta.Accessor(fetched) 385 if err != nil { 386 return true, err 387 } else if fetchedMeta.GetResourceVersion() != objectMeta.GetResourceVersion() { 388 return false, nil 389 } 390 391 return true, nil 392 }) 393 } 394 } 395 396 func (p *PolicyTestContext[P, B, E]) waitForDelete(ctx context.Context, objectGVK schema.GroupVersionKind, name types.NamespacedName) error { 397 srce := p.Source.(*policySource[P, B, E]) 398 399 return wait.PollUntilContextCancel(ctx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) { 400 switch objectGVK { 401 case p.policyGVK: 402 for _, hook := range p.Source.Hooks() { 403 accessor := srce.newPolicyAccessor(hook.Policy) 404 if accessor.GetName() == name.Name && accessor.GetNamespace() == name.Namespace { 405 return false, nil 406 } 407 } 408 409 return true, nil 410 case p.bindingGVK: 411 for _, hook := range p.Source.Hooks() { 412 for _, binding := range hook.Bindings { 413 accessor := srce.newBindingAccessor(binding) 414 if accessor.GetName() == name.Name && accessor.GetNamespace() == name.Namespace { 415 return false, nil 416 } 417 } 418 } 419 return true, nil 420 default: 421 // Do nothing, params are visible immediately 422 // Loop until one of the params is visible via get of the param informer 423 informer, scope := p.Source.(*policySource[P, B, E]).getParamInformer(objectGVK) 424 if informer == nil { 425 return true, nil 426 } 427 428 var lister cache.GenericNamespaceLister = informer.Lister() 429 if scope == meta.RESTScopeNamespace { 430 lister = informer.Lister().ByNamespace(name.Namespace) 431 } 432 433 _, err = lister.Get(name.Name) 434 if err != nil { 435 if errors.IsNotFound(err) { 436 return true, nil 437 } 438 return false, err 439 } 440 return false, nil 441 } 442 }) 443 } 444 445 func (p *PolicyTestContext[P, B, E]) updateOne(object runtime.Object) error { 446 objectMeta, err := meta.Accessor(object) 447 if err != nil { 448 return err 449 } 450 objectMeta.SetResourceVersion(string(uuid.NewUUID())) 451 objectGVK, gvr, err := p.inferGVK(object) 452 if err != nil { 453 return err 454 } 455 456 switch objectGVK { 457 case p.policyGVK: 458 err := p.policyAndBindingTracker.Update(p.policyGVR, object, objectMeta.GetNamespace()) 459 if errors.IsNotFound(err) { 460 err = p.policyAndBindingTracker.Create(p.policyGVR, object, objectMeta.GetNamespace()) 461 } 462 463 return err 464 case p.bindingGVK: 465 err := p.policyAndBindingTracker.Update(p.bindingGVR, object, objectMeta.GetNamespace()) 466 if errors.IsNotFound(err) { 467 err = p.policyAndBindingTracker.Create(p.bindingGVR, object, objectMeta.GetNamespace()) 468 } 469 470 return err 471 default: 472 if _, ok := object.(*unstructured.Unstructured); ok { 473 if err := p.unstructuredTracker.Create(gvr, object, objectMeta.GetNamespace()); err != nil { 474 if errors.IsAlreadyExists(err) { 475 return p.unstructuredTracker.Update(gvr, object, objectMeta.GetNamespace()) 476 } 477 return err 478 } 479 return nil 480 } else if err := p.nativeTracker.Create(gvr, object, objectMeta.GetNamespace()); err != nil { 481 if errors.IsAlreadyExists(err) { 482 return p.nativeTracker.Update(gvr, object, objectMeta.GetNamespace()) 483 } 484 } 485 return nil 486 } 487 } 488 489 // Depending upon object type, waits afterward until the object is synced 490 // by the policy source 491 func (p *PolicyTestContext[P, B, E]) DeleteAndWait(object ...runtime.Object) error { 492 for _, object := range object { 493 if err := p.deleteOne(object); err != nil && !errors.IsNotFound(err) { 494 return err 495 } 496 } 497 498 timeoutCtx, timeoutCancel := context.WithTimeout(p, 3*time.Second) 499 defer timeoutCancel() 500 501 for _, object := range object { 502 accessor, err := meta.Accessor(object) 503 if err != nil { 504 return err 505 } 506 507 objectGVK, _, err := p.inferGVK(object) 508 if err != nil { 509 return err 510 } 511 512 if err := p.waitForDelete( 513 timeoutCtx, 514 objectGVK, 515 types.NamespacedName{Name: accessor.GetName(), Namespace: accessor.GetNamespace()}); err != nil { 516 return err 517 } 518 } 519 return nil 520 } 521 522 func (p *PolicyTestContext[P, B, E]) deleteOne(object runtime.Object) error { 523 objectMeta, err := meta.Accessor(object) 524 if err != nil { 525 return err 526 } 527 objectMeta.SetResourceVersion(string(uuid.NewUUID())) 528 objectGVK, gvr, err := p.inferGVK(object) 529 if err != nil { 530 return err 531 } 532 533 switch objectGVK { 534 case p.policyGVK: 535 return p.policyAndBindingTracker.Delete(p.policyGVR, objectMeta.GetNamespace(), objectMeta.GetName()) 536 case p.bindingGVK: 537 return p.policyAndBindingTracker.Delete(p.bindingGVR, objectMeta.GetNamespace(), objectMeta.GetName()) 538 default: 539 if _, ok := object.(*unstructured.Unstructured); ok { 540 return p.unstructuredTracker.Delete(gvr, objectMeta.GetNamespace(), objectMeta.GetName()) 541 } 542 return p.nativeTracker.Delete(gvr, objectMeta.GetNamespace(), objectMeta.GetName()) 543 } 544 } 545 546 func (p *PolicyTestContext[P, B, E]) Dispatch( 547 new, old runtime.Object, 548 operation admission.Operation, 549 ) error { 550 if old == nil && new == nil { 551 return fmt.Errorf("both old and new objects cannot be nil") 552 } 553 554 nonNilObject := new 555 if nonNilObject == nil { 556 nonNilObject = old 557 } 558 559 gvk, gvr, err := p.inferGVK(nonNilObject) 560 if err != nil { 561 return err 562 } 563 564 nonNilMeta, err := meta.Accessor(nonNilObject) 565 if err != nil { 566 return err 567 } 568 569 return p.Plugin.Dispatch( 570 p, 571 admission.NewAttributesRecord( 572 new, 573 old, 574 gvk, 575 nonNilMeta.GetName(), 576 nonNilMeta.GetNamespace(), 577 gvr, 578 "", 579 operation, 580 nil, 581 false, 582 nil, 583 ), admission.NewObjectInterfacesFromScheme(p.scheme)) 584 } 585 586 func (p *PolicyTestContext[P, B, E]) inferGVK(object runtime.Object) (schema.GroupVersionKind, schema.GroupVersionResource, error) { 587 objectGVK := object.GetObjectKind().GroupVersionKind() 588 if objectGVK.Empty() { 589 // If the object doesn't have a GVK, ask the schema for preferred GVK 590 knownKinds, _, err := p.scheme.ObjectKinds(object) 591 if err != nil { 592 return schema.GroupVersionKind{}, schema.GroupVersionResource{}, err 593 } else if len(knownKinds) == 0 { 594 return schema.GroupVersionKind{}, schema.GroupVersionResource{}, fmt.Errorf("no known GVKs for object in schema: %T", object) 595 } 596 toTake := 0 597 598 // Prefer GVK if it is our fake policy or binding 599 for i, knownKind := range knownKinds { 600 if knownKind == p.policyGVK || knownKind == p.bindingGVK { 601 toTake = i 602 break 603 } 604 } 605 606 objectGVK = knownKinds[toTake] 607 } 608 609 // Make sure GVK is known to the fake rest mapper. To prevent cryptic error 610 mapping, err := p.restMapper.RESTMapping(objectGVK.GroupKind(), objectGVK.Version) 611 if err != nil { 612 return schema.GroupVersionKind{}, schema.GroupVersionResource{}, err 613 } 614 return objectGVK, mapping.Resource, nil 615 } 616 617 type FakeList[T runtime.Object] struct { 618 metav1.TypeMeta 619 metav1.ListMeta 620 Items []T 621 } 622 623 func (fl *FakeList[P]) DeepCopyObject() runtime.Object { 624 copiedItems := make([]P, len(fl.Items)) 625 for i, item := range fl.Items { 626 copiedItems[i] = item.DeepCopyObject().(P) 627 } 628 return &FakeList[P]{ 629 TypeMeta: fl.TypeMeta, 630 ListMeta: fl.ListMeta, 631 Items: copiedItems, 632 } 633 } 634 635 type fakeAuthorizer struct{} 636 637 func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { 638 return authorizer.DecisionAllow, "", nil 639 }