k8s.io/apiserver@v0.31.1/pkg/admission/plugin/policy/generic/policy_source.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 goerrors "errors" 22 "fmt" 23 "sync" 24 "sync/atomic" 25 "time" 26 27 corev1 "k8s.io/api/core/v1" 28 "k8s.io/apimachinery/pkg/api/errors" 29 "k8s.io/apimachinery/pkg/api/meta" 30 "k8s.io/apimachinery/pkg/labels" 31 "k8s.io/apimachinery/pkg/runtime" 32 "k8s.io/apimachinery/pkg/runtime/schema" 33 "k8s.io/apimachinery/pkg/types" 34 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 35 "k8s.io/apimachinery/pkg/util/wait" 36 "k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic" 37 "k8s.io/client-go/dynamic" 38 "k8s.io/client-go/dynamic/dynamicinformer" 39 "k8s.io/client-go/informers" 40 "k8s.io/client-go/tools/cache" 41 "k8s.io/klog/v2" 42 ) 43 44 type policySource[P runtime.Object, B runtime.Object, E Evaluator] struct { 45 ctx context.Context 46 policyInformer generic.Informer[P] 47 bindingInformer generic.Informer[B] 48 restMapper meta.RESTMapper 49 newPolicyAccessor func(P) PolicyAccessor 50 newBindingAccessor func(B) BindingAccessor 51 52 informerFactory informers.SharedInformerFactory 53 dynamicClient dynamic.Interface 54 55 compiler func(P) E 56 57 // Currently compiled list of valid/active policy-binding pairs 58 policies atomic.Pointer[[]PolicyHook[P, B, E]] 59 // Whether the cache of policies is dirty and needs to be recompiled 60 policiesDirty atomic.Bool 61 62 lock sync.Mutex 63 compiledPolicies map[types.NamespacedName]compiledPolicyEntry[E] 64 65 // Temporary until we use the dynamic informer factory 66 paramsCRDControllers map[schema.GroupVersionKind]*paramInfo 67 } 68 69 type paramInfo struct { 70 mapping meta.RESTMapping 71 72 // When the param is changed, or the informer is done being used, the cancel 73 // func should be called to stop/cleanup the original informer 74 cancelFunc func() 75 76 // The lister for this param 77 informer informers.GenericInformer 78 } 79 80 type compiledPolicyEntry[E Evaluator] struct { 81 policyVersion string 82 evaluator E 83 } 84 85 type PolicyHook[P runtime.Object, B runtime.Object, E Evaluator] struct { 86 Policy P 87 Bindings []B 88 89 // ParamInformer is the informer for the param CRD for this policy, or nil if 90 // there is no param or if there was a configuration error 91 ParamInformer informers.GenericInformer 92 ParamScope meta.RESTScope 93 94 Evaluator E 95 ConfigurationError error 96 } 97 98 var _ Source[PolicyHook[runtime.Object, runtime.Object, Evaluator]] = &policySource[runtime.Object, runtime.Object, Evaluator]{} 99 100 func NewPolicySource[P runtime.Object, B runtime.Object, E Evaluator]( 101 policyInformer cache.SharedIndexInformer, 102 bindingInformer cache.SharedIndexInformer, 103 newPolicyAccessor func(P) PolicyAccessor, 104 newBindingAccessor func(B) BindingAccessor, 105 compiler func(P) E, 106 paramInformerFactory informers.SharedInformerFactory, 107 dynamicClient dynamic.Interface, 108 restMapper meta.RESTMapper, 109 ) Source[PolicyHook[P, B, E]] { 110 res := &policySource[P, B, E]{ 111 compiler: compiler, 112 policyInformer: generic.NewInformer[P](policyInformer), 113 bindingInformer: generic.NewInformer[B](bindingInformer), 114 compiledPolicies: map[types.NamespacedName]compiledPolicyEntry[E]{}, 115 newPolicyAccessor: newPolicyAccessor, 116 newBindingAccessor: newBindingAccessor, 117 paramsCRDControllers: map[schema.GroupVersionKind]*paramInfo{}, 118 informerFactory: paramInformerFactory, 119 dynamicClient: dynamicClient, 120 restMapper: restMapper, 121 } 122 return res 123 } 124 125 func (s *policySource[P, B, E]) Run(ctx context.Context) error { 126 if s.ctx != nil { 127 return fmt.Errorf("policy source already running") 128 } 129 130 // Wait for initial cache sync of policies and informers before reconciling 131 // any 132 if !cache.WaitForNamedCacheSync(fmt.Sprintf("%T", s), ctx.Done(), s.UpstreamHasSynced) { 133 err := ctx.Err() 134 if err == nil { 135 err = fmt.Errorf("initial cache sync for %T failed", s) 136 } 137 return err 138 } 139 140 s.ctx = ctx 141 142 // Perform initial policy compilation after initial list has finished 143 s.notify() 144 s.refreshPolicies() 145 146 notifyFuncs := cache.ResourceEventHandlerFuncs{ 147 AddFunc: func(_ interface{}) { 148 s.notify() 149 }, 150 UpdateFunc: func(_, _ interface{}) { 151 s.notify() 152 }, 153 DeleteFunc: func(_ interface{}) { 154 s.notify() 155 }, 156 } 157 handle, err := s.policyInformer.AddEventHandler(notifyFuncs) 158 if err != nil { 159 return err 160 } 161 defer func() { 162 if err := s.policyInformer.RemoveEventHandler(handle); err != nil { 163 utilruntime.HandleError(fmt.Errorf("failed to remove policy event handler: %w", err)) 164 } 165 }() 166 167 bindingHandle, err := s.bindingInformer.AddEventHandler(notifyFuncs) 168 if err != nil { 169 return err 170 } 171 defer func() { 172 if err := s.bindingInformer.RemoveEventHandler(bindingHandle); err != nil { 173 utilruntime.HandleError(fmt.Errorf("failed to remove binding event handler: %w", err)) 174 } 175 }() 176 177 // Start a worker that checks every second to see if policy data is dirty 178 // and needs to be recompiled 179 go func() { 180 // Loop every 1 second until context is cancelled, refreshing policies 181 wait.Until(s.refreshPolicies, 1*time.Second, ctx.Done()) 182 }() 183 184 <-ctx.Done() 185 return nil 186 } 187 188 func (s *policySource[P, B, E]) UpstreamHasSynced() bool { 189 return s.policyInformer.HasSynced() && s.bindingInformer.HasSynced() 190 } 191 192 // HasSynced implements Source. 193 func (s *policySource[P, B, E]) HasSynced() bool { 194 // As an invariant we never store `nil` into the atomic list of processed 195 // policy hooks. If it is nil, then we haven't compiled all the policies 196 // and stored them yet. 197 return s.Hooks() != nil 198 } 199 200 // Hooks implements Source. 201 func (s *policySource[P, B, E]) Hooks() []PolicyHook[P, B, E] { 202 res := s.policies.Load() 203 204 // Error case should not happen since evaluation function never 205 // returns error 206 if res == nil { 207 // Not yet synced 208 return nil 209 } 210 211 return *res 212 } 213 214 func (s *policySource[P, B, E]) refreshPolicies() { 215 if !s.UpstreamHasSynced() { 216 return 217 } else if !s.policiesDirty.Swap(false) { 218 return 219 } 220 221 // It is ok the cache gets marked dirty again between us clearing the 222 // flag and us calculating the policies. The dirty flag would be marked again, 223 // and we'd have a no-op after comparing resource versions on the next sync. 224 klog.Infof("refreshing policies") 225 policies, err := s.calculatePolicyData() 226 227 // Intentionally store policy list regardless of error. There may be 228 // an error returned if there was a configuration error in one of the policies, 229 // but we would still want those policies evaluated 230 // (for instance to return error on failaction). Or if there was an error 231 // listing all policies at all, we would want to wipe the list. 232 s.policies.Store(&policies) 233 234 if err != nil { 235 // An error was generated while syncing policies. Mark it as dirty again 236 // so we can retry later 237 utilruntime.HandleError(fmt.Errorf("encountered error syncing policies: %w. Rescheduling policy sync", err)) 238 s.notify() 239 } 240 } 241 242 func (s *policySource[P, B, E]) notify() { 243 s.policiesDirty.Store(true) 244 } 245 246 // calculatePolicyData calculates the list of policies and bindings for each 247 // policy. If there is an error in generation, it will return the error and 248 // the partial list of policies that were able to be generated. Policies that 249 // have an error will have a non-nil ConfigurationError field, but still be 250 // included in the result. 251 // 252 // This function caches the result of the intermediate compilations 253 func (s *policySource[P, B, E]) calculatePolicyData() ([]PolicyHook[P, B, E], error) { 254 if !s.UpstreamHasSynced() { 255 return nil, fmt.Errorf("cannot calculate policy data until upstream has synced") 256 } 257 258 // Fat-fingered lock that can be made more fine-tuned if required 259 s.lock.Lock() 260 defer s.lock.Unlock() 261 262 // Create a local copy of all policies and bindings 263 policiesToBindings := map[types.NamespacedName][]B{} 264 bindingList, err := s.bindingInformer.List(labels.Everything()) 265 if err != nil { 266 // This should never happen unless types are misconfigured 267 // (can't use meta.accessor on them) 268 return nil, err 269 } 270 271 // Gather a list of all active policy bindings 272 for _, bindingSpec := range bindingList { 273 bindingAccessor := s.newBindingAccessor(bindingSpec) 274 policyKey := bindingAccessor.GetPolicyName() 275 276 // Add this binding to the list of bindings for this policy 277 policiesToBindings[policyKey] = append(policiesToBindings[policyKey], bindingSpec) 278 } 279 280 result := make([]PolicyHook[P, B, E], 0, len(bindingList)) 281 usedParams := map[schema.GroupVersionKind]struct{}{} 282 var errs []error 283 for policyKey, bindingSpecs := range policiesToBindings { 284 var inf generic.NamespacedLister[P] = s.policyInformer 285 if len(policyKey.Namespace) > 0 { 286 inf = s.policyInformer.Namespaced(policyKey.Namespace) 287 } 288 policySpec, err := inf.Get(policyKey.Name) 289 if errors.IsNotFound(err) { 290 // Policy for bindings doesn't exist. This can happen if the policy 291 // was deleted before the binding, or the binding was created first. 292 // 293 // Just skip bindings that refer to non-existent policies 294 // If the policy is recreated, the cache will be marked dirty and 295 // this function will run again. 296 continue 297 } else if err != nil { 298 // This should never happen since fetching from a cache should never 299 // fail and this function checks that the cache was synced before 300 // even getting to this point. 301 errs = append(errs, err) 302 continue 303 } 304 305 var parsedParamKind *schema.GroupVersionKind 306 policyAccessor := s.newPolicyAccessor(policySpec) 307 308 if paramKind := policyAccessor.GetParamKind(); paramKind != nil { 309 groupVersion, err := schema.ParseGroupVersion(paramKind.APIVersion) 310 if err != nil { 311 errs = append(errs, fmt.Errorf("failed to parse paramKind APIVersion: %w", err)) 312 continue 313 } 314 parsedParamKind = &schema.GroupVersionKind{ 315 Group: groupVersion.Group, 316 Version: groupVersion.Version, 317 Kind: paramKind.Kind, 318 } 319 320 // TEMPORARY UNTIL WE HAVE SHARED PARAM INFORMERS 321 usedParams[*parsedParamKind] = struct{}{} 322 } 323 324 paramInformer, paramScope, configurationError := s.ensureParamsForPolicyLocked(parsedParamKind) 325 result = append(result, PolicyHook[P, B, E]{ 326 Policy: policySpec, 327 Bindings: bindingSpecs, 328 Evaluator: s.compilePolicyLocked(policySpec), 329 ParamInformer: paramInformer, 330 ParamScope: paramScope, 331 ConfigurationError: configurationError, 332 }) 333 334 // Should queue a re-sync for policy sync error. If our shared param 335 // informer can notify us when CRD discovery changes we can remove this 336 // and just rely on the informer to notify us when the CRDs change 337 if configurationError != nil { 338 errs = append(errs, configurationError) 339 } 340 } 341 342 // Clean up orphaned policies by replacing the old cache of compiled policies 343 // (the map of used policies is updated by `compilePolicy`) 344 for policyKey := range s.compiledPolicies { 345 if _, wasSeen := policiesToBindings[policyKey]; !wasSeen { 346 delete(s.compiledPolicies, policyKey) 347 } 348 } 349 350 // Clean up orphaned param informers 351 for paramKind, info := range s.paramsCRDControllers { 352 if _, wasSeen := usedParams[paramKind]; !wasSeen { 353 info.cancelFunc() 354 delete(s.paramsCRDControllers, paramKind) 355 } 356 } 357 358 err = nil 359 if len(errs) > 0 { 360 err = goerrors.Join(errs...) 361 } 362 return result, err 363 } 364 365 // ensureParamsForPolicyLocked ensures that the informer for the paramKind is 366 // started and returns the informer and the scope of the paramKind. 367 // 368 // Must be called under write lock 369 func (s *policySource[P, B, E]) ensureParamsForPolicyLocked(paramSource *schema.GroupVersionKind) (informers.GenericInformer, meta.RESTScope, error) { 370 if paramSource == nil { 371 return nil, nil, nil 372 } else if info, ok := s.paramsCRDControllers[*paramSource]; ok { 373 return info.informer, info.mapping.Scope, nil 374 } 375 376 mapping, err := s.restMapper.RESTMapping(schema.GroupKind{ 377 Group: paramSource.Group, 378 Kind: paramSource.Kind, 379 }, paramSource.Version) 380 381 if err != nil { 382 // Failed to resolve. Return error so we retry again (rate limited) 383 // Save a record of this definition with an evaluator that unconditionally 384 return nil, nil, fmt.Errorf("failed to find resource referenced by paramKind: '%v'", *paramSource) 385 } 386 387 // We are not watching this param. Start an informer for it. 388 instanceContext, instanceCancel := context.WithCancel(s.ctx) 389 390 var informer informers.GenericInformer 391 392 // Try to see if our provided informer factory has an informer for this type. 393 // We assume the informer is already started, and starts all types associated 394 // with it. 395 if genericInformer, err := s.informerFactory.ForResource(mapping.Resource); err == nil { 396 informer = genericInformer 397 398 // Start the informer 399 s.informerFactory.Start(instanceContext.Done()) 400 401 } else { 402 // Dynamic JSON informer fallback. 403 // Cannot use shared dynamic informer since it would be impossible 404 // to clean CRD informers properly with multiple dependents 405 // (cannot start ahead of time, and cannot track dependencies via stopCh) 406 informer = dynamicinformer.NewFilteredDynamicInformer( 407 s.dynamicClient, 408 mapping.Resource, 409 corev1.NamespaceAll, 410 // Use same interval as is used for k8s typed sharedInformerFactory 411 // https://github.com/kubernetes/kubernetes/blob/7e0923899fed622efbc8679cca6b000d43633e38/cmd/kube-apiserver/app/server.go#L430 412 10*time.Minute, 413 cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, 414 nil, 415 ) 416 go informer.Informer().Run(instanceContext.Done()) 417 } 418 419 klog.Infof("informer started for %v", *paramSource) 420 ret := ¶mInfo{ 421 mapping: *mapping, 422 cancelFunc: instanceCancel, 423 informer: informer, 424 } 425 s.paramsCRDControllers[*paramSource] = ret 426 return ret.informer, mapping.Scope, nil 427 } 428 429 // For testing 430 func (s *policySource[P, B, E]) getParamInformer(param schema.GroupVersionKind) (informers.GenericInformer, meta.RESTScope) { 431 s.lock.Lock() 432 defer s.lock.Unlock() 433 434 if info, ok := s.paramsCRDControllers[param]; ok { 435 return info.informer, info.mapping.Scope 436 } 437 438 return nil, nil 439 } 440 441 // compilePolicyLocked compiles the policy and returns the evaluator for it. 442 // If the policy has not changed since the last compilation, it will return 443 // the cached evaluator. 444 // 445 // Must be called under write lock 446 func (s *policySource[P, B, E]) compilePolicyLocked(policySpec P) E { 447 policyMeta, err := meta.Accessor(policySpec) 448 if err != nil { 449 // This should not happen if P, and B have ObjectMeta, but 450 // unfortunately there is no way to express "able to call 451 // meta.Accessor" as a type constraint 452 utilruntime.HandleError(err) 453 var emptyEvaluator E 454 return emptyEvaluator 455 } 456 key := types.NamespacedName{ 457 Namespace: policyMeta.GetNamespace(), 458 Name: policyMeta.GetName(), 459 } 460 461 compiledPolicy, wasCompiled := s.compiledPolicies[key] 462 463 // If the policy or binding has changed since it was last compiled, 464 // and if there is no configuration error (like a missing param CRD) 465 // then we recompile 466 if !wasCompiled || 467 compiledPolicy.policyVersion != policyMeta.GetResourceVersion() { 468 469 compiledPolicy = compiledPolicyEntry[E]{ 470 policyVersion: policyMeta.GetResourceVersion(), 471 evaluator: s.compiler(policySpec), 472 } 473 s.compiledPolicies[key] = compiledPolicy 474 } 475 476 return compiledPolicy.evaluator 477 }