392 lines
12 KiB
Go
392 lines
12 KiB
Go
/*
|
|
Copyright 2022 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package validatingadmissionpolicy
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"k8s.io/api/admissionregistration/v1alpha1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
celmetrics "k8s.io/apiserver/pkg/admission/cel"
|
|
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
|
|
"k8s.io/client-go/dynamic"
|
|
"k8s.io/client-go/dynamic/dynamicinformer"
|
|
"k8s.io/client-go/tools/cache"
|
|
)
|
|
|
|
type policyController struct {
|
|
once sync.Once
|
|
context context.Context
|
|
dynamicClient dynamic.Interface
|
|
restMapper meta.RESTMapper
|
|
policyDefinitionsController generic.Controller[*v1alpha1.ValidatingAdmissionPolicy]
|
|
policyBindingController generic.Controller[*v1alpha1.ValidatingAdmissionPolicyBinding]
|
|
|
|
// Provided to the policy's Compile function as an injected dependency to
|
|
// assist with compiling its expressions to CEL
|
|
ValidatorCompiler
|
|
|
|
// Lock which protects:
|
|
// - cachedPolicies
|
|
// - paramCRDControllers
|
|
// - definitionInfo
|
|
// - bindingInfos
|
|
// - definitionsToBindings
|
|
// All other fields should be assumed constant
|
|
mutex sync.RWMutex
|
|
|
|
cachedPolicies []policyData
|
|
|
|
// controller and metadata
|
|
paramsCRDControllers map[v1alpha1.ParamKind]*paramInfo
|
|
|
|
// Index for each definition namespace/name, contains all binding
|
|
// namespace/names known to exist for that definition
|
|
definitionInfo map[namespacedName]*definitionInfo
|
|
|
|
// Index for each bindings namespace/name. Contains compiled templates
|
|
// for the binding depending on the policy/param combination.
|
|
bindingInfos map[namespacedName]*bindingInfo
|
|
|
|
// Map from namespace/name of a definition to a set of namespace/name
|
|
// of bindings which depend on it.
|
|
// All keys must have at least one dependent binding
|
|
// All binding names MUST exist as a key bindingInfos
|
|
definitionsToBindings map[namespacedName]sets.Set[namespacedName]
|
|
}
|
|
|
|
func newPolicyController(
|
|
restMapper meta.RESTMapper,
|
|
dynamicClient dynamic.Interface,
|
|
validatorCompiler ValidatorCompiler,
|
|
policiesInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicy],
|
|
bindingsInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicyBinding],
|
|
) *policyController {
|
|
res := &policyController{}
|
|
*res = policyController{
|
|
ValidatorCompiler: validatorCompiler,
|
|
definitionInfo: make(map[namespacedName]*definitionInfo),
|
|
bindingInfos: make(map[namespacedName]*bindingInfo),
|
|
paramsCRDControllers: make(map[v1alpha1.ParamKind]*paramInfo),
|
|
definitionsToBindings: make(map[namespacedName]sets.Set[namespacedName]),
|
|
policyDefinitionsController: generic.NewController(
|
|
policiesInformer,
|
|
res.reconcilePolicyDefinition,
|
|
generic.ControllerOptions{
|
|
Workers: 1,
|
|
Name: "cel-policy-definitions",
|
|
},
|
|
),
|
|
policyBindingController: generic.NewController(
|
|
bindingsInformer,
|
|
res.reconcilePolicyBinding,
|
|
generic.ControllerOptions{
|
|
Workers: 1,
|
|
Name: "cel-policy-bindings",
|
|
},
|
|
),
|
|
restMapper: restMapper,
|
|
dynamicClient: dynamicClient,
|
|
}
|
|
return res
|
|
}
|
|
|
|
func (c *policyController) Run(ctx context.Context) {
|
|
// Only support being run once
|
|
c.once.Do(func() {
|
|
c.context = ctx
|
|
|
|
wg := sync.WaitGroup{}
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
c.policyDefinitionsController.Run(ctx)
|
|
}()
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
c.policyBindingController.Run(ctx)
|
|
}()
|
|
|
|
<-ctx.Done()
|
|
wg.Wait()
|
|
})
|
|
}
|
|
|
|
func (c *policyController) HasSynced() bool {
|
|
return c.policyDefinitionsController.HasSynced() && c.policyBindingController.HasSynced()
|
|
}
|
|
|
|
func (c *policyController) reconcilePolicyDefinition(namespace, name string, definition *v1alpha1.ValidatingAdmissionPolicy) error {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
|
|
c.cachedPolicies = nil // invalidate cachedPolicies
|
|
|
|
// Namespace for policydefinition is empty.
|
|
nn := getNamespaceName(namespace, name)
|
|
info, ok := c.definitionInfo[nn]
|
|
if !ok {
|
|
info = &definitionInfo{}
|
|
c.definitionInfo[nn] = info
|
|
// TODO(DangerOnTheRanger): add support for "warn" being a valid enforcementAction
|
|
celmetrics.Metrics.ObserveDefinition(context.TODO(), "active", "deny")
|
|
}
|
|
|
|
var paramSource *v1alpha1.ParamKind
|
|
if definition != nil {
|
|
paramSource = definition.Spec.ParamKind
|
|
}
|
|
|
|
// If param source has changed, remove definition as dependent of old params
|
|
// If there are no more dependents of old param, stop and clean up controller
|
|
if info.lastReconciledValue != nil && info.lastReconciledValue.Spec.ParamKind != nil {
|
|
oldParamSource := *info.lastReconciledValue.Spec.ParamKind
|
|
|
|
// If we are:
|
|
// - switching from having a param to not having a param (includes deletion)
|
|
// - or from having a param to a different one
|
|
// we remove dependency on the controller.
|
|
if paramSource == nil || *paramSource != oldParamSource {
|
|
if oldParamInfo, ok := c.paramsCRDControllers[oldParamSource]; ok {
|
|
oldParamInfo.dependentDefinitions.Delete(nn)
|
|
if len(oldParamInfo.dependentDefinitions) == 0 {
|
|
oldParamInfo.stop()
|
|
delete(c.paramsCRDControllers, oldParamSource)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reset all previously compiled evaluators in case something relevant in
|
|
// definition has changed.
|
|
for key := range c.definitionsToBindings[nn] {
|
|
bindingInfo := c.bindingInfos[key]
|
|
bindingInfo.validator = nil
|
|
c.bindingInfos[key] = bindingInfo
|
|
}
|
|
|
|
if definition == nil {
|
|
delete(c.definitionInfo, nn)
|
|
return nil
|
|
}
|
|
|
|
// Update definition info
|
|
info.lastReconciledValue = definition
|
|
info.configurationError = nil
|
|
|
|
if paramSource == nil {
|
|
// Skip setting up controller for empty param type
|
|
return nil
|
|
}
|
|
|
|
// find GVR for params
|
|
// Parse param source into a GVK
|
|
|
|
paramSourceGV, err := schema.ParseGroupVersion(paramSource.APIVersion)
|
|
if err != nil {
|
|
// Failed to resolve. Return error so we retry again (rate limited)
|
|
// Save a record of this definition with an evaluator that unconditionally
|
|
info.configurationError = fmt.Errorf("failed to parse apiVersion of paramKind '%v' with error: %w", paramSource.String(), err)
|
|
|
|
// Return nil, since this error cannot be resolved by waiting more time
|
|
return nil
|
|
}
|
|
|
|
paramsGVR, err := c.restMapper.RESTMapping(schema.GroupKind{
|
|
Group: paramSourceGV.Group,
|
|
Kind: paramSource.Kind,
|
|
}, paramSourceGV.Version)
|
|
|
|
if err != nil {
|
|
// Failed to resolve. Return error so we retry again (rate limited)
|
|
// Save a record of this definition with an evaluator that unconditionally
|
|
//
|
|
info.configurationError = fmt.Errorf("failed to find resource referenced by paramKind: '%v'", paramSourceGV.WithKind(paramSource.Kind))
|
|
return info.configurationError
|
|
}
|
|
|
|
if info, ok := c.paramsCRDControllers[*paramSource]; ok {
|
|
// If a param controller is already active for this paramsource, make
|
|
// sure it is tracking this policy's dependency upon it
|
|
info.dependentDefinitions.Insert(nn)
|
|
|
|
} else {
|
|
instanceContext, instanceCancel := context.WithCancel(c.context)
|
|
|
|
// Watch for new instances of this policy
|
|
informer := dynamicinformer.NewFilteredDynamicInformer(
|
|
c.dynamicClient,
|
|
paramsGVR.Resource,
|
|
corev1.NamespaceAll,
|
|
30*time.Second, // TODO: do we really need to ever resync these?
|
|
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
|
|
nil,
|
|
)
|
|
|
|
controller := generic.NewController(
|
|
generic.NewInformer[*unstructured.Unstructured](informer.Informer()),
|
|
c.reconcileParams,
|
|
generic.ControllerOptions{
|
|
Workers: 1,
|
|
Name: paramSource.String() + "-controller",
|
|
},
|
|
)
|
|
|
|
c.paramsCRDControllers[*paramSource] = ¶mInfo{
|
|
controller: controller,
|
|
stop: instanceCancel,
|
|
dependentDefinitions: sets.New(nn),
|
|
}
|
|
|
|
go informer.Informer().Run(instanceContext.Done())
|
|
go controller.Run(instanceContext)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *policyController) reconcilePolicyBinding(namespace, name string, binding *v1alpha1.ValidatingAdmissionPolicyBinding) error {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
|
|
c.cachedPolicies = nil // invalidate cachedPolicies
|
|
|
|
// Namespace for PolicyBinding is empty. In the future a namespaced binding
|
|
// may be added
|
|
// https://github.com/kubernetes/enhancements/blob/bf5c3c81ea2081d60c1dc7c832faa98479e06209/keps/sig-api-machinery/3488-cel-admission-control/README.md?plain=1#L1042
|
|
nn := getNamespaceName(namespace, name)
|
|
info, ok := c.bindingInfos[nn]
|
|
if !ok {
|
|
info = &bindingInfo{}
|
|
c.bindingInfos[nn] = info
|
|
}
|
|
|
|
var oldNamespacedDefinitionName namespacedName
|
|
if info.lastReconciledValue != nil {
|
|
// All validating policies are cluster-scoped so have empty namespace
|
|
oldNamespacedDefinitionName = getNamespaceName("", info.lastReconciledValue.Spec.PolicyName)
|
|
}
|
|
|
|
var namespacedDefinitionName namespacedName
|
|
if binding != nil {
|
|
// All validating policies are cluster-scoped so have empty namespace
|
|
namespacedDefinitionName = getNamespaceName("", binding.Spec.PolicyName)
|
|
}
|
|
|
|
// Remove record of binding from old definition if the referred policy
|
|
// has changed
|
|
if oldNamespacedDefinitionName != namespacedDefinitionName {
|
|
if dependentBindings, ok := c.definitionsToBindings[oldNamespacedDefinitionName]; ok {
|
|
dependentBindings.Delete(nn)
|
|
|
|
// if there are no more dependent bindings, remove knowledge of the
|
|
// definition altogether
|
|
if len(dependentBindings) == 0 {
|
|
delete(c.definitionsToBindings, oldNamespacedDefinitionName)
|
|
}
|
|
}
|
|
}
|
|
|
|
if binding == nil {
|
|
delete(c.bindingInfos, nn)
|
|
return nil
|
|
}
|
|
|
|
// Add record of binding to new definition
|
|
if dependentBindings, ok := c.definitionsToBindings[namespacedDefinitionName]; ok {
|
|
dependentBindings.Insert(nn)
|
|
} else {
|
|
c.definitionsToBindings[namespacedDefinitionName] = sets.New(nn)
|
|
}
|
|
|
|
// Remove compiled template for old binding
|
|
info.validator = nil
|
|
info.lastReconciledValue = binding
|
|
return nil
|
|
}
|
|
|
|
func (c *policyController) reconcileParams(namespace, name string, params *unstructured.Unstructured) error {
|
|
// Do nothing.
|
|
// When we add informational type checking we will need to compile in the
|
|
// reconcile loops instead of lazily so we can add compiler errors / type
|
|
// checker errors to the status of the resources.
|
|
return nil
|
|
}
|
|
|
|
// Fetches the latest set of policy data or recalculates it if it has changed
|
|
// since it was last fetched
|
|
func (c *policyController) latestPolicyData() []policyData {
|
|
existing := func() []policyData {
|
|
c.mutex.RLock()
|
|
defer c.mutex.RUnlock()
|
|
|
|
return c.cachedPolicies
|
|
}()
|
|
|
|
if existing != nil {
|
|
return existing
|
|
}
|
|
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
|
|
var res []policyData
|
|
for definitionNN, definitionInfo := range c.definitionInfo {
|
|
var bindingInfos []bindingInfo
|
|
for bindingNN := range c.definitionsToBindings[definitionNN] {
|
|
bindingInfo := c.bindingInfos[bindingNN]
|
|
if bindingInfo.validator == nil && definitionInfo.configurationError == nil {
|
|
bindingInfo.validator = c.ValidatorCompiler.Compile(definitionInfo.lastReconciledValue)
|
|
}
|
|
bindingInfos = append(bindingInfos, *bindingInfo)
|
|
}
|
|
|
|
var paramController generic.Controller[*unstructured.Unstructured]
|
|
if paramKind := definitionInfo.lastReconciledValue.Spec.ParamKind; paramKind != nil {
|
|
if info, ok := c.paramsCRDControllers[*paramKind]; ok {
|
|
paramController = info.controller
|
|
}
|
|
}
|
|
|
|
res = append(res, policyData{
|
|
definitionInfo: *definitionInfo,
|
|
paramController: paramController,
|
|
bindings: bindingInfos,
|
|
})
|
|
}
|
|
|
|
c.cachedPolicies = res
|
|
return res
|
|
}
|
|
|
|
func getNamespaceName(namespace, name string) namespacedName {
|
|
return namespacedName{
|
|
namespace: namespace,
|
|
name: name,
|
|
}
|
|
}
|