mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-29 22:46:12 +00:00
Add MutatingAdmissionPolicy plugin to admission chain
This expands the generic plugin support to both validating and mutating policies. It also adds the mutating policy admission plugin using the generics plugin support. This also implements both ApplyConfiguration and JSONPatch support. Co-authored-by: Alexander Zielensk <alexzielenski@gmail.com>
This commit is contained in:
parent
081353bf8a
commit
25e11cd1c1
@ -20,7 +20,9 @@ package options
|
||||
// This should probably be part of some configuration fed into the build for a
|
||||
// given binary target.
|
||||
import (
|
||||
mutatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/mutating"
|
||||
validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating"
|
||||
|
||||
// Admission policies
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/admit"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/alwayspullimages"
|
||||
@ -95,6 +97,7 @@ var AllOrderedPlugins = []string{
|
||||
// new admission plugins should generally be inserted above here
|
||||
// webhook, resourcequota, and deny plugins must go at the end
|
||||
|
||||
mutatingadmissionpolicy.PluginName, // MutatingAdmissionPolicy
|
||||
mutatingwebhook.PluginName, // MutatingAdmissionWebhook
|
||||
validatingadmissionpolicy.PluginName, // ValidatingAdmissionPolicy
|
||||
validatingwebhook.PluginName, // ValidatingAdmissionWebhook
|
||||
@ -159,6 +162,7 @@ func DefaultOffAdmissionPlugins() sets.Set[string] {
|
||||
certsubjectrestriction.PluginName, // CertificateSubjectRestriction
|
||||
defaultingressclass.PluginName, // DefaultIngressClass
|
||||
podsecurity.PluginName, // PodSecurity
|
||||
mutatingadmissionpolicy.PluginName, // Mutatingadmissionpolicy, only active when feature gate MutatingAdmissionpolicy is enabled
|
||||
validatingadmissionpolicy.PluginName, // ValidatingAdmissionPolicy, only active when feature gate ValidatingAdmissionPolicy is enabled
|
||||
)
|
||||
|
||||
|
@ -22,9 +22,9 @@ import (
|
||||
)
|
||||
|
||||
func TestAdmissionPluginOrder(t *testing.T) {
|
||||
// Ensure the last four admission plugins listed are webhooks, quota, and deny
|
||||
// Sanity check that the order of admission ends with mutating(policy, webhook), validating(policy, webhook), quota, deny.
|
||||
allplugins := strings.Join(AllOrderedPlugins, ",")
|
||||
expectSuffix := ",MutatingAdmissionWebhook,ValidatingAdmissionPolicy,ValidatingAdmissionWebhook,ResourceQuota,AlwaysDeny"
|
||||
expectSuffix := ",MutatingAdmissionPolicy,MutatingAdmissionWebhook,ValidatingAdmissionPolicy,ValidatingAdmissionWebhook,ResourceQuota,AlwaysDeny"
|
||||
if !strings.HasSuffix(allplugins, expectSuffix) {
|
||||
t.Fatalf("AllOrderedPlugins must end with ...%s", expectSuffix)
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ type PolicyAccessor interface {
|
||||
GetNamespace() string
|
||||
GetParamKind() *v1.ParamKind
|
||||
GetMatchConstraints() *v1.MatchResources
|
||||
GetFailurePolicy() *v1.FailurePolicyType
|
||||
}
|
||||
|
||||
type BindingAccessor interface {
|
||||
|
@ -49,6 +49,9 @@ type Source[H Hook] interface {
|
||||
// Dispatcher dispatches evaluates an admission request against the currently
|
||||
// active hooks returned by the source.
|
||||
type Dispatcher[H Hook] interface {
|
||||
// Run the dispatcher. This method should be called only once at startup.
|
||||
Run(ctx context.Context) error
|
||||
|
||||
// Dispatch a request to the policies. Dispatcher may choose not to
|
||||
// call a hook, either because the rules of the hook does not match, or
|
||||
// the namespaceSelector or the objectSelector of the hook does not
|
||||
|
@ -36,8 +36,9 @@ import (
|
||||
)
|
||||
|
||||
// H is the Hook type generated by the source and consumed by the dispatcher.
|
||||
// !TODO: Just pass in a Plugin[H] with accessors to all this information
|
||||
type sourceFactory[H any] func(informers.SharedInformerFactory, kubernetes.Interface, dynamic.Interface, meta.RESTMapper) Source[H]
|
||||
type dispatcherFactory[H any] func(authorizer.Authorizer, *matching.Matcher) Dispatcher[H]
|
||||
type dispatcherFactory[H any] func(authorizer.Authorizer, *matching.Matcher, kubernetes.Interface) Dispatcher[H]
|
||||
|
||||
// admissionResources is the list of resources related to CEL-based admission
|
||||
// features.
|
||||
@ -170,7 +171,7 @@ func (c *Plugin[H]) ValidateInitialization() error {
|
||||
}
|
||||
|
||||
c.source = c.sourceFactory(c.informerFactory, c.client, c.dynamicClient, c.restMapper)
|
||||
c.dispatcher = c.dispatcherFactory(c.authorizer, c.matcher)
|
||||
c.dispatcher = c.dispatcherFactory(c.authorizer, c.matcher, c.client)
|
||||
|
||||
pluginContext, pluginContextCancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
@ -181,10 +182,15 @@ func (c *Plugin[H]) ValidateInitialization() error {
|
||||
go func() {
|
||||
err := c.source.Run(pluginContext)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
utilruntime.HandleError(fmt.Errorf("policy source context unexpectedly closed: %v", err))
|
||||
utilruntime.HandleError(fmt.Errorf("policy source context unexpectedly closed: %w", err))
|
||||
}
|
||||
}()
|
||||
|
||||
err := c.dispatcher.Run(pluginContext)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
utilruntime.HandleError(fmt.Errorf("policy dispatcher context unexpectedly closed: %w", err))
|
||||
}
|
||||
|
||||
c.SetReadyFunc(func() bool {
|
||||
return namespaceInformer.Informer().HasSynced() && c.source.HasSynced()
|
||||
})
|
||||
|
@ -36,7 +36,7 @@ import (
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
// A policy invocation is a single policy-binding-param tuple from a Policy Hook
|
||||
// PolicyInvocation is a single policy-binding-param tuple from a Policy Hook
|
||||
// in the context of a specific request. The params have already been resolved
|
||||
// and any error in configuration or setting up the invocation is stored in
|
||||
// the Error field.
|
||||
@ -62,10 +62,6 @@ type PolicyInvocation[P runtime.Object, B runtime.Object, E Evaluator] struct {
|
||||
|
||||
// Params fetched by the binding to use to evaluate the policy
|
||||
Param runtime.Object
|
||||
|
||||
// Error is set if there was an error with the policy or binding or its
|
||||
// params, etc
|
||||
Error error
|
||||
}
|
||||
|
||||
// dispatcherDelegate is called during a request with a pre-filtered list
|
||||
@ -76,7 +72,7 @@ type PolicyInvocation[P runtime.Object, B runtime.Object, E Evaluator] struct {
|
||||
//
|
||||
// The delegate provides the "validation" or "mutation" aspect of dispatcher functionality
|
||||
// (in contrast to generic.PolicyDispatcher which only selects active policies and params)
|
||||
type dispatcherDelegate[P, B runtime.Object, E Evaluator] func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, versionedAttributes webhookgeneric.VersionedAttributeAccessor, invocations []PolicyInvocation[P, B, E]) error
|
||||
type dispatcherDelegate[P, B runtime.Object, E Evaluator] func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, versionedAttributes webhookgeneric.VersionedAttributeAccessor, invocations []PolicyInvocation[P, B, E]) ([]PolicyError, *apierrors.StatusError)
|
||||
|
||||
type policyDispatcher[P runtime.Object, B runtime.Object, E Evaluator] struct {
|
||||
newPolicyAccessor func(P) PolicyAccessor
|
||||
@ -104,7 +100,10 @@ func NewPolicyDispatcher[P runtime.Object, B runtime.Object, E Evaluator](
|
||||
// request. It then resolves all params and creates an Invocation for each
|
||||
// matching policy-binding-param tuple. The delegate is then called with the
|
||||
// list of tuples.
|
||||
//
|
||||
func (d *policyDispatcher[P, B, E]) Run(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Note: MatchConditions expressions are not evaluated here. The dispatcher delegate
|
||||
// is expected to ignore the result of any policies whose match conditions dont pass.
|
||||
// This may be possible to refactor so matchconditions are checked here instead.
|
||||
@ -117,29 +116,33 @@ func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.At
|
||||
objectInterfaces: o,
|
||||
}
|
||||
|
||||
var policyErrors []PolicyError
|
||||
addConfigError := func(err error, definition PolicyAccessor, binding BindingAccessor) {
|
||||
var message error
|
||||
if binding == nil {
|
||||
message = fmt.Errorf("failed to configure policy: %w", err)
|
||||
} else {
|
||||
message = fmt.Errorf("failed to configure binding: %w", err)
|
||||
}
|
||||
|
||||
policyErrors = append(policyErrors, PolicyError{
|
||||
Policy: definition,
|
||||
Binding: binding,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
for _, hook := range hooks {
|
||||
policyAccessor := d.newPolicyAccessor(hook.Policy)
|
||||
matches, matchGVR, matchGVK, err := d.matcher.DefinitionMatches(a, o, policyAccessor)
|
||||
if err != nil {
|
||||
// There was an error evaluating if this policy matches anything.
|
||||
utilruntime.HandleError(err)
|
||||
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
|
||||
Policy: hook.Policy,
|
||||
Error: err,
|
||||
})
|
||||
addConfigError(err, policyAccessor, nil)
|
||||
continue
|
||||
} else if !matches {
|
||||
continue
|
||||
} else if hook.ConfigurationError != nil {
|
||||
// The policy matches but there is a configuration error with the
|
||||
// policy itself
|
||||
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
|
||||
Policy: hook.Policy,
|
||||
Error: hook.ConfigurationError,
|
||||
Resource: matchGVR,
|
||||
Kind: matchGVK,
|
||||
})
|
||||
utilruntime.HandleError(hook.ConfigurationError)
|
||||
addConfigError(hook.ConfigurationError, policyAccessor, nil)
|
||||
continue
|
||||
}
|
||||
|
||||
@ -148,19 +151,22 @@ func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.At
|
||||
matches, err = d.matcher.BindingMatches(a, o, bindingAccessor)
|
||||
if err != nil {
|
||||
// There was an error evaluating if this binding matches anything.
|
||||
utilruntime.HandleError(err)
|
||||
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
|
||||
Policy: hook.Policy,
|
||||
Binding: binding,
|
||||
Error: err,
|
||||
Resource: matchGVR,
|
||||
Kind: matchGVK,
|
||||
})
|
||||
addConfigError(err, policyAccessor, bindingAccessor)
|
||||
continue
|
||||
} else if !matches {
|
||||
continue
|
||||
}
|
||||
|
||||
// here the binding matches.
|
||||
// VersionedAttr result will be cached and reused later during parallel
|
||||
// hook calls.
|
||||
if _, err = versionedAttrAccessor.VersionedAttribute(matchGVK); err != nil {
|
||||
// VersionedAttr result will be cached and reused later during parallel
|
||||
// hook calls.
|
||||
addConfigError(err, policyAccessor, nil)
|
||||
continue
|
||||
}
|
||||
|
||||
// Collect params for this binding
|
||||
params, err := CollectParams(
|
||||
policyAccessor.GetParamKind(),
|
||||
@ -171,14 +177,7 @@ func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.At
|
||||
)
|
||||
if err != nil {
|
||||
// There was an error collecting params for this binding.
|
||||
utilruntime.HandleError(err)
|
||||
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
|
||||
Policy: hook.Policy,
|
||||
Binding: binding,
|
||||
Error: err,
|
||||
Resource: matchGVR,
|
||||
Kind: matchGVK,
|
||||
})
|
||||
addConfigError(err, policyAccessor, bindingAccessor)
|
||||
continue
|
||||
}
|
||||
|
||||
@ -194,23 +193,72 @@ func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.At
|
||||
Evaluator: hook.Evaluator,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VersionedAttr result will be cached and reused later during parallel
|
||||
// hook calls
|
||||
_, err = versionedAttrAccessor.VersionedAttribute(matchGVK)
|
||||
if err != nil {
|
||||
return apierrors.NewInternalError(err)
|
||||
}
|
||||
if len(relevantHooks) > 0 {
|
||||
extraPolicyErrors, statusError := d.delegate(ctx, a, o, versionedAttrAccessor, relevantHooks)
|
||||
if statusError != nil {
|
||||
return statusError
|
||||
}
|
||||
policyErrors = append(policyErrors, extraPolicyErrors...)
|
||||
}
|
||||
|
||||
var filteredErrors []PolicyError
|
||||
for _, e := range policyErrors {
|
||||
// we always default the FailurePolicy if it is unset and validate it in API level
|
||||
var policy v1.FailurePolicyType
|
||||
if fp := e.Policy.GetFailurePolicy(); fp == nil {
|
||||
policy = v1.Fail
|
||||
} else {
|
||||
policy = *fp
|
||||
}
|
||||
|
||||
switch policy {
|
||||
case v1.Ignore:
|
||||
// TODO: add metrics for ignored error here
|
||||
continue
|
||||
case v1.Fail:
|
||||
filteredErrors = append(filteredErrors, e)
|
||||
default:
|
||||
filteredErrors = append(filteredErrors, e)
|
||||
}
|
||||
}
|
||||
|
||||
if len(relevantHooks) == 0 {
|
||||
// no matching hooks
|
||||
return nil
|
||||
if len(filteredErrors) > 0 {
|
||||
|
||||
forbiddenErr := admission.NewForbidden(a, fmt.Errorf("admission request denied by policy"))
|
||||
|
||||
// The forbiddenErr is always a StatusError.
|
||||
var err *apierrors.StatusError
|
||||
if !errors.As(forbiddenErr, &err) {
|
||||
// Should never happen.
|
||||
return apierrors.NewInternalError(fmt.Errorf("failed to create status error"))
|
||||
}
|
||||
err.ErrStatus.Message = ""
|
||||
|
||||
for _, policyError := range filteredErrors {
|
||||
message := policyError.Error()
|
||||
|
||||
// If this is the first denied decision, use its message and reason
|
||||
// for the status error message.
|
||||
if err.ErrStatus.Message == "" {
|
||||
err.ErrStatus.Message = message
|
||||
if policyError.Reason != "" {
|
||||
err.ErrStatus.Reason = policyError.Reason
|
||||
}
|
||||
}
|
||||
|
||||
// Add the denied decision's message to the status error's details
|
||||
err.ErrStatus.Details.Causes = append(
|
||||
err.ErrStatus.Details.Causes,
|
||||
metav1.StatusCause{Message: message})
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return d.delegate(ctx, a, o, versionedAttrAccessor, relevantHooks)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns params to use to evaluate a policy-binding with given param
|
||||
@ -352,3 +400,18 @@ func (v *versionedAttributeAccessor) VersionedAttribute(gvk schema.GroupVersionK
|
||||
v.versionedAttrs[gvk] = versionedAttr
|
||||
return versionedAttr, nil
|
||||
}
|
||||
|
||||
type PolicyError struct {
|
||||
Policy PolicyAccessor
|
||||
Binding BindingAccessor
|
||||
Message error
|
||||
Reason metav1.StatusReason
|
||||
}
|
||||
|
||||
func (c PolicyError) Error() string {
|
||||
if c.Binding != nil {
|
||||
return fmt.Sprintf("policy '%s' with binding '%s' denied request: %s", c.Policy.GetName(), c.Binding.GetName(), c.Message.Error())
|
||||
}
|
||||
|
||||
return fmt.Sprintf("policy '%s' denied request: %s", c.Policy.GetName(), c.Message.Error())
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
package generic_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -24,15 +25,26 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
func makeTestDispatcher(authorizer.Authorizer, *matching.Matcher) generic.Dispatcher[generic.PolicyHook[*FakePolicy, *FakeBinding, generic.Evaluator]] {
|
||||
type fakeDispatcher struct{}
|
||||
|
||||
func (fd *fakeDispatcher) Dispatch(context.Context, admission.Attributes, admission.ObjectInterfaces, []generic.PolicyHook[*FakePolicy, *FakeBinding, generic.Evaluator]) error {
|
||||
return nil
|
||||
}
|
||||
func (fd *fakeDispatcher) Run(context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeTestDispatcher(authorizer.Authorizer, *matching.Matcher, kubernetes.Interface) generic.Dispatcher[generic.PolicyHook[*FakePolicy, *FakeBinding, generic.Evaluator]] {
|
||||
return &fakeDispatcher{}
|
||||
}
|
||||
|
||||
func TestPolicySourceHasSyncedEmpty(t *testing.T) {
|
||||
testContext, testCancel, err := generic.NewPolicyTestContext(
|
||||
@ -207,6 +219,10 @@ func (fb *FakePolicy) GetMatchConstraints() *v1.MatchResources {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fb *FakePolicy) GetFailurePolicy() *v1.FailurePolicyType {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fb *FakeBinding) GetName() string {
|
||||
return fb.Name
|
||||
}
|
||||
|
@ -0,0 +1,144 @@
|
||||
/*
|
||||
Copyright 2024 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 mutating
|
||||
|
||||
import (
|
||||
v1 "k8s.io/api/admissionregistration/v1"
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
|
||||
)
|
||||
|
||||
func NewMutatingAdmissionPolicyAccessor(obj *Policy) generic.PolicyAccessor {
|
||||
return &mutatingAdmissionPolicyAccessor{
|
||||
Policy: obj,
|
||||
}
|
||||
}
|
||||
|
||||
func NewMutatingAdmissionPolicyBindingAccessor(obj *PolicyBinding) generic.BindingAccessor {
|
||||
return &mutatingAdmissionPolicyBindingAccessor{
|
||||
PolicyBinding: obj,
|
||||
}
|
||||
}
|
||||
|
||||
type mutatingAdmissionPolicyAccessor struct {
|
||||
*Policy
|
||||
}
|
||||
|
||||
func (v *mutatingAdmissionPolicyAccessor) GetNamespace() string {
|
||||
return v.Namespace
|
||||
}
|
||||
|
||||
func (v *mutatingAdmissionPolicyAccessor) GetName() string {
|
||||
return v.Name
|
||||
}
|
||||
|
||||
func (v *mutatingAdmissionPolicyAccessor) GetParamKind() *v1.ParamKind {
|
||||
pk := v.Spec.ParamKind
|
||||
if pk == nil {
|
||||
return nil
|
||||
}
|
||||
return &v1.ParamKind{
|
||||
APIVersion: pk.APIVersion,
|
||||
Kind: pk.Kind,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *mutatingAdmissionPolicyAccessor) GetMatchConstraints() *v1.MatchResources {
|
||||
return convertV1alpha1ResourceRulesToV1(v.Spec.MatchConstraints)
|
||||
}
|
||||
|
||||
func (v *mutatingAdmissionPolicyAccessor) GetFailurePolicy() *v1.FailurePolicyType {
|
||||
return toV1FailurePolicy(v.Spec.FailurePolicy)
|
||||
}
|
||||
|
||||
func toV1FailurePolicy(failurePolicy *v1alpha1.FailurePolicyType) *v1.FailurePolicyType {
|
||||
if failurePolicy == nil {
|
||||
return nil
|
||||
}
|
||||
fp := v1.FailurePolicyType(*failurePolicy)
|
||||
return &fp
|
||||
}
|
||||
|
||||
type mutatingAdmissionPolicyBindingAccessor struct {
|
||||
*PolicyBinding
|
||||
}
|
||||
|
||||
func (v *mutatingAdmissionPolicyBindingAccessor) GetNamespace() string {
|
||||
return v.Namespace
|
||||
}
|
||||
|
||||
func (v *mutatingAdmissionPolicyBindingAccessor) GetName() string {
|
||||
return v.Name
|
||||
}
|
||||
|
||||
func (v *mutatingAdmissionPolicyBindingAccessor) GetPolicyName() types.NamespacedName {
|
||||
return types.NamespacedName{
|
||||
Namespace: "",
|
||||
Name: v.Spec.PolicyName,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *mutatingAdmissionPolicyBindingAccessor) GetMatchResources() *v1.MatchResources {
|
||||
return convertV1alpha1ResourceRulesToV1(v.Spec.MatchResources)
|
||||
}
|
||||
|
||||
func (v *mutatingAdmissionPolicyBindingAccessor) GetParamRef() *v1.ParamRef {
|
||||
if v.Spec.ParamRef == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var nfa *v1.ParameterNotFoundActionType
|
||||
if v.Spec.ParamRef.ParameterNotFoundAction != nil {
|
||||
nfa = new(v1.ParameterNotFoundActionType)
|
||||
*nfa = v1.ParameterNotFoundActionType(*v.Spec.ParamRef.ParameterNotFoundAction)
|
||||
}
|
||||
|
||||
return &v1.ParamRef{
|
||||
Name: v.Spec.ParamRef.Name,
|
||||
Namespace: v.Spec.ParamRef.Namespace,
|
||||
Selector: v.Spec.ParamRef.Selector,
|
||||
ParameterNotFoundAction: nfa,
|
||||
}
|
||||
}
|
||||
|
||||
func convertV1alpha1ResourceRulesToV1(mc *v1alpha1.MatchResources) *v1.MatchResources {
|
||||
if mc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var res v1.MatchResources
|
||||
res.NamespaceSelector = mc.NamespaceSelector
|
||||
res.ObjectSelector = mc.ObjectSelector
|
||||
for _, ex := range mc.ExcludeResourceRules {
|
||||
res.ExcludeResourceRules = append(res.ExcludeResourceRules, v1.NamedRuleWithOperations{
|
||||
ResourceNames: ex.ResourceNames,
|
||||
RuleWithOperations: ex.RuleWithOperations,
|
||||
})
|
||||
}
|
||||
for _, ex := range mc.ResourceRules {
|
||||
res.ResourceRules = append(res.ResourceRules, v1.NamedRuleWithOperations{
|
||||
ResourceNames: ex.ResourceNames,
|
||||
RuleWithOperations: ex.RuleWithOperations,
|
||||
})
|
||||
}
|
||||
if mc.MatchPolicy != nil {
|
||||
mp := v1.MatchPolicyType(*mc.MatchPolicy)
|
||||
res.MatchPolicy = &mp
|
||||
}
|
||||
return &res
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
/*
|
||||
Copyright 2024 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 mutating
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
)
|
||||
|
||||
// compilePolicy compiles the policy into a PolicyEvaluator
|
||||
// any error is stored and delayed until invocation.
|
||||
//
|
||||
// Each individual mutation is compiled into MutationEvaluationFunc and
|
||||
// returned is a PolicyEvaluator in the same order as the mutations appeared in the policy.
|
||||
func compilePolicy(policy *Policy) PolicyEvaluator {
|
||||
opts := plugincel.OptionalVariableDeclarations{HasParams: policy.Spec.ParamKind != nil, StrictCost: true, HasAuthorizer: true}
|
||||
compiler, err := plugincel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
|
||||
if err != nil {
|
||||
return PolicyEvaluator{Error: &apiservercel.Error{
|
||||
Type: apiservercel.ErrorTypeInternal,
|
||||
Detail: fmt.Sprintf("failed to initialize CEL compiler: %v", err),
|
||||
}}
|
||||
}
|
||||
|
||||
// Compile and store variables
|
||||
compiler.CompileAndStoreVariables(convertv1alpha1Variables(policy.Spec.Variables), opts, environment.StoredExpressions)
|
||||
|
||||
// Compile matchers
|
||||
var matcher matchconditions.Matcher = nil
|
||||
matchConditions := policy.Spec.MatchConditions
|
||||
if len(matchConditions) > 0 {
|
||||
matchExpressionAccessors := make([]plugincel.ExpressionAccessor, len(matchConditions))
|
||||
for i := range matchConditions {
|
||||
matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
|
||||
}
|
||||
matcher = matchconditions.NewMatcher(compiler.CompileCondition(matchExpressionAccessors, opts, environment.StoredExpressions), toV1FailurePolicy(policy.Spec.FailurePolicy), "policy", "validate", policy.Name)
|
||||
}
|
||||
|
||||
// Compiler patchers
|
||||
var patchers []patch.Patcher
|
||||
patchOptions := opts
|
||||
patchOptions.HasPatchTypes = true
|
||||
for _, m := range policy.Spec.Mutations {
|
||||
switch m.PatchType {
|
||||
case v1alpha1.PatchTypeJSONPatch:
|
||||
if m.JSONPatch != nil {
|
||||
accessor := &JSONPatchCondition{Expression: m.JSONPatch.Expression}
|
||||
compileResult := compiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions)
|
||||
patchers = append(patchers, patch.NewJSONPatcher(compileResult))
|
||||
}
|
||||
case v1alpha1.PatchTypeApplyConfiguration:
|
||||
if m.ApplyConfiguration != nil {
|
||||
accessor := &ApplyConfigurationCondition{Expression: m.ApplyConfiguration.Expression}
|
||||
compileResult := compiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions)
|
||||
patchers = append(patchers, patch.NewApplyConfigurationPatcher(compileResult))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return PolicyEvaluator{Matcher: matcher, Mutators: patchers, CompositionEnv: compiler.CompositionEnv}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,284 @@
|
||||
/*
|
||||
Copyright 2024 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 mutating
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
admissionauthorizer "k8s.io/apiserver/pkg/admission/plugin/authorizer"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
|
||||
webhookgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
)
|
||||
|
||||
func NewDispatcher(a authorizer.Authorizer, m *matching.Matcher, tcm patch.TypeConverterManager) generic.Dispatcher[PolicyHook] {
|
||||
res := &dispatcher{
|
||||
matcher: m,
|
||||
authz: a,
|
||||
//!TODO: pass in static type converter to reduce network calls
|
||||
typeConverterManager: tcm,
|
||||
}
|
||||
res.Dispatcher = generic.NewPolicyDispatcher[*Policy, *PolicyBinding, PolicyEvaluator](
|
||||
NewMutatingAdmissionPolicyAccessor,
|
||||
NewMutatingAdmissionPolicyBindingAccessor,
|
||||
m,
|
||||
res.dispatchInvocations,
|
||||
)
|
||||
return res
|
||||
}
|
||||
|
||||
type dispatcher struct {
|
||||
matcher *matching.Matcher
|
||||
authz authorizer.Authorizer
|
||||
typeConverterManager patch.TypeConverterManager
|
||||
generic.Dispatcher[PolicyHook]
|
||||
}
|
||||
|
||||
func (d *dispatcher) Run(ctx context.Context) error {
|
||||
go d.typeConverterManager.Run(ctx)
|
||||
return d.Dispatcher.Run(ctx)
|
||||
}
|
||||
|
||||
func (d *dispatcher) dispatchInvocations(
|
||||
ctx context.Context,
|
||||
a admission.Attributes,
|
||||
o admission.ObjectInterfaces,
|
||||
versionedAttributes webhookgeneric.VersionedAttributeAccessor,
|
||||
invocations []generic.PolicyInvocation[*Policy, *PolicyBinding, PolicyEvaluator],
|
||||
) ([]generic.PolicyError, *k8serrors.StatusError) {
|
||||
var lastVersionedAttr *admission.VersionedAttributes
|
||||
|
||||
reinvokeCtx := a.GetReinvocationContext()
|
||||
var policyReinvokeCtx *policyReinvokeContext
|
||||
if v := reinvokeCtx.Value(PluginName); v != nil {
|
||||
policyReinvokeCtx = v.(*policyReinvokeContext)
|
||||
} else {
|
||||
policyReinvokeCtx = &policyReinvokeContext{}
|
||||
reinvokeCtx.SetValue(PluginName, policyReinvokeCtx)
|
||||
}
|
||||
|
||||
if reinvokeCtx.IsReinvoke() && policyReinvokeCtx.IsOutputChangedSinceLastPolicyInvocation(a.GetObject()) {
|
||||
// If the object has changed, we know the in-tree plugin re-invocations have mutated the object,
|
||||
// and we need to reinvoke all eligible policies.
|
||||
policyReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
|
||||
}
|
||||
defer func() {
|
||||
policyReinvokeCtx.SetLastPolicyInvocationOutput(a.GetObject())
|
||||
}()
|
||||
|
||||
var policyErrors []generic.PolicyError
|
||||
addConfigError := func(err error, invocation generic.PolicyInvocation[*Policy, *PolicyBinding, PolicyEvaluator], reason metav1.StatusReason) {
|
||||
policyErrors = append(policyErrors, generic.PolicyError{
|
||||
Message: err,
|
||||
Policy: NewMutatingAdmissionPolicyAccessor(invocation.Policy),
|
||||
Binding: NewMutatingAdmissionPolicyBindingAccessor(invocation.Binding),
|
||||
Reason: reason,
|
||||
})
|
||||
}
|
||||
|
||||
// There is at least one invocation to invoke. Make sure we have a namespace
|
||||
// object if the incoming object is not cluster scoped to pass into the evaluator.
|
||||
var namespace *v1.Namespace
|
||||
var err error
|
||||
namespaceName := a.GetNamespace()
|
||||
|
||||
// Special case, the namespace object has the namespace of itself (maybe a bug).
|
||||
// unset it if the incoming object is a namespace
|
||||
if gvk := a.GetKind(); gvk.Kind == "Namespace" && gvk.Version == "v1" && gvk.Group == "" {
|
||||
namespaceName = ""
|
||||
}
|
||||
|
||||
// if it is cluster scoped, namespaceName will be empty
|
||||
// Otherwise, get the Namespace resource.
|
||||
if namespaceName != "" {
|
||||
namespace, err = d.matcher.GetNamespace(namespaceName)
|
||||
if err != nil {
|
||||
return nil, k8serrors.NewNotFound(schema.GroupResource{Group: "", Resource: "namespaces"}, namespaceName)
|
||||
}
|
||||
}
|
||||
|
||||
authz := admissionauthorizer.NewCachingAuthorizer(d.authz)
|
||||
|
||||
// Should loop through invocations, handling possible error and invoking
|
||||
// evaluator to apply patch, also should handle re-invocations
|
||||
for _, invocation := range invocations {
|
||||
if invocation.Evaluator.CompositionEnv != nil {
|
||||
ctx = invocation.Evaluator.CompositionEnv.CreateContext(ctx)
|
||||
}
|
||||
if len(invocation.Evaluator.Mutators) != len(invocation.Policy.Spec.Mutations) {
|
||||
// This would be a bug. The compiler should always return exactly as
|
||||
// many evaluators as there are mutations
|
||||
return nil, k8serrors.NewInternalError(fmt.Errorf("expected %v compiled evaluators for policy %v, got %v",
|
||||
invocation.Policy.Name, len(invocation.Policy.Spec.Mutations), len(invocation.Evaluator.Mutators)))
|
||||
}
|
||||
|
||||
versionedAttr, err := versionedAttributes.VersionedAttribute(invocation.Kind)
|
||||
if err != nil {
|
||||
// This should never happen, we pre-warm versoined attribute
|
||||
// accessors before starting the dispatcher
|
||||
return nil, k8serrors.NewInternalError(err)
|
||||
}
|
||||
|
||||
if invocation.Evaluator.Matcher != nil {
|
||||
matchResults := invocation.Evaluator.Matcher.Match(ctx, versionedAttr, invocation.Param, authz)
|
||||
if matchResults.Error != nil {
|
||||
addConfigError(matchResults.Error, invocation, metav1.StatusReasonInvalid)
|
||||
}
|
||||
|
||||
// if preconditions are not met, then skip mutations
|
||||
if !matchResults.Matches {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
invocationKey, invocationKeyErr := keyFor(invocation)
|
||||
if reinvokeCtx.IsReinvoke() && !policyReinvokeCtx.ShouldReinvoke(invocationKey) {
|
||||
continue
|
||||
}
|
||||
|
||||
objectBeforeMutations := versionedAttr.VersionedObject
|
||||
// Mutations for a single invocation of a MutatingAdmissionPolicy are evaluated
|
||||
// in order.
|
||||
for mutationIndex := range invocation.Policy.Spec.Mutations {
|
||||
if invocationKeyErr != nil {
|
||||
// This should never happen. It occurs if there is a programming
|
||||
// error causing the Param not to be a valid object.
|
||||
return nil, k8serrors.NewInternalError(invocationKeyErr)
|
||||
}
|
||||
|
||||
lastVersionedAttr = versionedAttr
|
||||
if versionedAttr.VersionedObject == nil { // Do not call patchers if there is no object to patch.
|
||||
continue
|
||||
}
|
||||
|
||||
patcher := invocation.Evaluator.Mutators[mutationIndex]
|
||||
optionalVariables := cel.OptionalVariableBindings{VersionedParams: invocation.Param, Authorizer: authz}
|
||||
err = d.dispatchOne(ctx, patcher, o, versionedAttr, namespace, invocation.Resource, optionalVariables)
|
||||
if err != nil {
|
||||
var statusError *k8serrors.StatusError
|
||||
if errors.As(err, &statusError) {
|
||||
return nil, statusError
|
||||
}
|
||||
|
||||
addConfigError(err, invocation, metav1.StatusReasonInvalid)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !apiequality.Semantic.DeepEqual(objectBeforeMutations, versionedAttr.VersionedObject) {
|
||||
// The mutation has changed the object. Prepare to reinvoke all previous mutations that are eligible for re-invocation.
|
||||
policyReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
|
||||
reinvokeCtx.SetShouldReinvoke()
|
||||
}
|
||||
if invocation.Policy.Spec.ReinvocationPolicy == v1alpha1.IfNeededReinvocationPolicy {
|
||||
policyReinvokeCtx.AddReinvocablePolicyToPreviouslyInvoked(invocationKey)
|
||||
}
|
||||
}
|
||||
|
||||
if lastVersionedAttr != nil && lastVersionedAttr.VersionedObject != nil && lastVersionedAttr.Dirty {
|
||||
policyReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
|
||||
reinvokeCtx.SetShouldReinvoke()
|
||||
if err := o.GetObjectConvertor().Convert(lastVersionedAttr.VersionedObject, lastVersionedAttr.Attributes.GetObject(), nil); err != nil {
|
||||
return nil, k8serrors.NewInternalError(fmt.Errorf("failed to convert object: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
return policyErrors, nil
|
||||
}
|
||||
|
||||
func (d *dispatcher) dispatchOne(
|
||||
ctx context.Context,
|
||||
patcher patch.Patcher,
|
||||
o admission.ObjectInterfaces,
|
||||
versionedAttributes *admission.VersionedAttributes,
|
||||
namespace *v1.Namespace,
|
||||
resource schema.GroupVersionResource,
|
||||
optionalVariables cel.OptionalVariableBindings,
|
||||
) (err error) {
|
||||
if patcher == nil {
|
||||
// internal error. this should not happen
|
||||
return k8serrors.NewInternalError(fmt.Errorf("policy evaluator is nil"))
|
||||
}
|
||||
|
||||
// Find type converter for the invoked Group-Version.
|
||||
typeConverter := d.typeConverterManager.GetTypeConverter(versionedAttributes.VersionedKind)
|
||||
if typeConverter == nil {
|
||||
// This can happen if the request is for a resource whose schema
|
||||
// has not been registered with the type converter manager.
|
||||
return k8serrors.NewServiceUnavailable(fmt.Sprintf("Resource kind %s not found. There can be a delay between when CustomResourceDefinitions are created and when they are available.", versionedAttributes.VersionedKind))
|
||||
}
|
||||
|
||||
patchRequest := patch.Request{
|
||||
MatchedResource: resource,
|
||||
VersionedAttributes: versionedAttributes,
|
||||
ObjectInterfaces: o,
|
||||
OptionalVariables: optionalVariables,
|
||||
Namespace: namespace,
|
||||
TypeConverter: typeConverter,
|
||||
}
|
||||
newVersionedObject, err := patcher.Patch(ctx, patchRequest, celconfig.RuntimeCELCostBudget)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
versionedAttributes.Dirty = true
|
||||
versionedAttributes.VersionedObject = newVersionedObject
|
||||
o.GetObjectDefaulter().Default(newVersionedObject)
|
||||
return nil
|
||||
}
|
||||
|
||||
func keyFor(invocation generic.PolicyInvocation[*Policy, *PolicyBinding, PolicyEvaluator]) (key, error) {
|
||||
var paramUID types.NamespacedName
|
||||
if invocation.Param != nil {
|
||||
paramAccessor, err := meta.Accessor(invocation.Param)
|
||||
if err != nil {
|
||||
// This should never happen, as the param should have been validated
|
||||
// before being passed to the plugin.
|
||||
return key{}, err
|
||||
}
|
||||
paramUID = types.NamespacedName{
|
||||
Name: paramAccessor.GetName(),
|
||||
Namespace: paramAccessor.GetNamespace(),
|
||||
}
|
||||
}
|
||||
|
||||
return key{
|
||||
PolicyUID: types.NamespacedName{
|
||||
Name: invocation.Policy.GetName(),
|
||||
Namespace: invocation.Policy.GetNamespace(),
|
||||
},
|
||||
BindingUID: types.NamespacedName{
|
||||
Name: invocation.Binding.GetName(),
|
||||
Namespace: invocation.Binding.GetNamespace(),
|
||||
},
|
||||
ParamUID: paramUID,
|
||||
}, nil
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
Copyright 2024 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 mutating
|
||||
|
||||
import (
|
||||
celgo "github.com/google/cel-go/cel"
|
||||
celtypes "github.com/google/cel-go/common/types"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
)
|
||||
|
||||
var _ cel.ExpressionAccessor = &ApplyConfigurationCondition{}
|
||||
|
||||
// ApplyConfigurationCondition contains the inputs needed to compile and evaluate a cel expression
|
||||
// that returns an apply configuration
|
||||
type ApplyConfigurationCondition struct {
|
||||
Expression string
|
||||
}
|
||||
|
||||
func (v *ApplyConfigurationCondition) GetExpression() string {
|
||||
return v.Expression
|
||||
}
|
||||
|
||||
func (v *ApplyConfigurationCondition) ReturnTypes() []*celgo.Type {
|
||||
return []*celgo.Type{applyConfigObjectType}
|
||||
}
|
||||
|
||||
var applyConfigObjectType = celtypes.NewObjectType("Object")
|
||||
|
||||
var _ cel.ExpressionAccessor = &JSONPatchCondition{}
|
||||
|
||||
// JSONPatchCondition contains the inputs needed to compile and evaluate a cel expression
|
||||
// that returns a JSON patch value.
|
||||
type JSONPatchCondition struct {
|
||||
Expression string
|
||||
}
|
||||
|
||||
func (v *JSONPatchCondition) GetExpression() string {
|
||||
return v.Expression
|
||||
}
|
||||
|
||||
func (v *JSONPatchCondition) ReturnTypes() []*celgo.Type {
|
||||
return []*celgo.Type{celgo.ListType(jsonPatchType)}
|
||||
}
|
||||
|
||||
var jsonPatchType = celtypes.NewObjectType("JSONPatch")
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright 2024 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 patch
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
)
|
||||
|
||||
// Patcher provides a patch function to perform a mutation to an object in the admission chain.
|
||||
type Patcher interface {
|
||||
Patch(ctx context.Context, request Request, runtimeCELCostBudget int64) (runtime.Object, error)
|
||||
}
|
||||
|
||||
// Request defines the arguments required by a patcher.
|
||||
type Request struct {
|
||||
MatchedResource schema.GroupVersionResource
|
||||
VersionedAttributes *admission.VersionedAttributes
|
||||
ObjectInterfaces admission.ObjectInterfaces
|
||||
OptionalVariables cel.OptionalVariableBindings
|
||||
Namespace *v1.Namespace
|
||||
TypeConverter managedfields.TypeConverter
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
/*
|
||||
Copyright 2024 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 patch
|
||||
|
||||
import (
|
||||
"context"
|
||||
gojson "encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/traits"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
jsonpatch "gopkg.in/evanphx/json-patch.v4"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
||||
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/cel/mutation"
|
||||
"k8s.io/apiserver/pkg/cel/mutation/dynamic"
|
||||
pointer "k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
// NewJSONPatcher creates a patcher that performs a JSON Patch mutation.
|
||||
func NewJSONPatcher(patchEvaluator plugincel.MutatingEvaluator) Patcher {
|
||||
return &jsonPatcher{patchEvaluator}
|
||||
}
|
||||
|
||||
type jsonPatcher struct {
|
||||
PatchEvaluator plugincel.MutatingEvaluator
|
||||
}
|
||||
|
||||
func (e *jsonPatcher) Patch(ctx context.Context, r Request, runtimeCELCostBudget int64) (runtime.Object, error) {
|
||||
admissionRequest := plugincel.CreateAdmissionRequest(
|
||||
r.VersionedAttributes.Attributes,
|
||||
metav1.GroupVersionResource(r.MatchedResource),
|
||||
metav1.GroupVersionKind(r.VersionedAttributes.VersionedKind))
|
||||
|
||||
compileErrors := e.PatchEvaluator.CompilationErrors()
|
||||
if len(compileErrors) > 0 {
|
||||
return nil, errors.Join(compileErrors...)
|
||||
}
|
||||
patchObj, _, err := e.evaluatePatchExpression(ctx, e.PatchEvaluator, runtimeCELCostBudget, r, admissionRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o := r.ObjectInterfaces
|
||||
jsonSerializer := json.NewSerializerWithOptions(json.DefaultMetaFactory, o.GetObjectCreater(), o.GetObjectTyper(), json.SerializerOptions{Pretty: false, Strict: true})
|
||||
objJS, err := runtime.Encode(jsonSerializer, r.VersionedAttributes.VersionedObject)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create JSON patch: %w", err)
|
||||
}
|
||||
patchedJS, err := patchObj.Apply(objJS)
|
||||
if err != nil {
|
||||
if errors.Is(err, jsonpatch.ErrTestFailed) {
|
||||
// If a json patch fails a test operation, the patch must not be applied
|
||||
return r.VersionedAttributes.VersionedObject, nil
|
||||
}
|
||||
return nil, fmt.Errorf("JSON Patch: %w", err)
|
||||
}
|
||||
|
||||
var newVersionedObject runtime.Object
|
||||
if _, ok := r.VersionedAttributes.VersionedObject.(*unstructured.Unstructured); ok {
|
||||
newVersionedObject = &unstructured.Unstructured{}
|
||||
} else {
|
||||
newVersionedObject, err = o.GetObjectCreater().New(r.VersionedAttributes.VersionedKind)
|
||||
if err != nil {
|
||||
return nil, apierrors.NewInternalError(err)
|
||||
}
|
||||
}
|
||||
|
||||
if newVersionedObject, _, err = jsonSerializer.Decode(patchedJS, nil, newVersionedObject); err != nil {
|
||||
return nil, apierrors.NewInternalError(err)
|
||||
}
|
||||
|
||||
return newVersionedObject, nil
|
||||
}
|
||||
|
||||
func (e *jsonPatcher) evaluatePatchExpression(ctx context.Context, patchEvaluator plugincel.MutatingEvaluator, remainingBudget int64, r Request, admissionRequest *admissionv1.AdmissionRequest) (jsonpatch.Patch, int64, error) {
|
||||
var err error
|
||||
var eval plugincel.EvaluationResult
|
||||
eval, remainingBudget, err = patchEvaluator.ForInput(ctx, r.VersionedAttributes, admissionRequest, r.OptionalVariables, r.Namespace, remainingBudget)
|
||||
if err != nil {
|
||||
return nil, -1, err
|
||||
}
|
||||
if eval.Error != nil {
|
||||
return nil, -1, eval.Error
|
||||
}
|
||||
refVal := eval.EvalResult
|
||||
|
||||
// the return type can be any valid CEL value.
|
||||
// Scalars, maps and lists are used to set the value when the path points to a field of that type.
|
||||
// ObjectVal is used when the path points to a struct. A map like "{"field1": 1, "fieldX": bool}" is not
|
||||
// possible in Kubernetes CEL because maps and lists may not have mixed types.
|
||||
|
||||
iter, ok := refVal.(traits.Lister)
|
||||
if !ok {
|
||||
// Should never happen since compiler checks return type.
|
||||
return nil, -1, fmt.Errorf("type mismatch: JSONPatchType.expression should evaluate to array")
|
||||
}
|
||||
result := jsonpatch.Patch{}
|
||||
for it := iter.Iterator(); it.HasNext() == types.True; {
|
||||
v := it.Next()
|
||||
patchObj, err := v.ConvertToNative(reflect.TypeOf(&mutation.JSONPatchVal{}))
|
||||
if err != nil {
|
||||
// Should never happen since return type is checked by compiler.
|
||||
return nil, -1, fmt.Errorf("type mismatch: JSONPatchType.expression should evaluate to array of JSONPatch: %w", err)
|
||||
}
|
||||
op, ok := patchObj.(*mutation.JSONPatchVal)
|
||||
if !ok {
|
||||
// Should never happen since return type is checked by compiler.
|
||||
return nil, -1, fmt.Errorf("type mismatch: JSONPatchType.expression should evaluate to array of JSONPatch, got element of %T", patchObj)
|
||||
}
|
||||
|
||||
// Construct a JSON Patch from the evaluated CEL expression
|
||||
resultOp := jsonpatch.Operation{}
|
||||
resultOp["op"] = pointer.To(gojson.RawMessage(strconv.Quote(op.Op)))
|
||||
resultOp["path"] = pointer.To(gojson.RawMessage(strconv.Quote(op.Path)))
|
||||
if len(op.From) > 0 {
|
||||
resultOp["from"] = pointer.To(gojson.RawMessage(strconv.Quote(op.From)))
|
||||
}
|
||||
if op.Val != nil {
|
||||
if objVal, ok := op.Val.(*dynamic.ObjectVal); ok {
|
||||
// TODO: Object initializers are insufficiently type checked.
|
||||
// In the interim, we use this sanity check to detect type mismatches
|
||||
// between field names and Object initializers. For example,
|
||||
// "Object.spec{ selector: Object.spec.wrong{}}" is detected as a mismatch.
|
||||
// Before beta, attaching full type information both to Object initializers and
|
||||
// the "object" and "oldObject" variables is needed. This will allow CEL to
|
||||
// perform comprehensive runtime type checking.
|
||||
err := objVal.CheckTypeNamesMatchFieldPathNames()
|
||||
if err != nil {
|
||||
return nil, -1, fmt.Errorf("type mismatch: %w", err)
|
||||
}
|
||||
}
|
||||
// CEL data literals representing arbitrary JSON values can be serialized to JSON for use in
|
||||
// JSON Patch if first converted to pb.Value.
|
||||
v, err := op.Val.ConvertToNative(reflect.TypeOf(&structpb.Value{}))
|
||||
if err != nil {
|
||||
return nil, -1, fmt.Errorf("JSONPath valueExpression evaluated to a type that could not marshal to JSON: %w", err)
|
||||
}
|
||||
b, err := gojson.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, -1, fmt.Errorf("JSONPath valueExpression evaluated to a type that could not marshal to JSON: %w", err)
|
||||
}
|
||||
resultOp["value"] = pointer.To[gojson.RawMessage](b)
|
||||
}
|
||||
|
||||
result = append(result, resultOp)
|
||||
}
|
||||
|
||||
return result, remainingBudget, nil
|
||||
}
|
@ -0,0 +1,194 @@
|
||||
/*
|
||||
Copyright 2024 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 patch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/schema"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/typed"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/value"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/cel/mutation/dynamic"
|
||||
)
|
||||
|
||||
// NewApplyConfigurationPatcher creates a patcher that performs an applyConfiguration mutation.
|
||||
func NewApplyConfigurationPatcher(expressionEvaluator plugincel.MutatingEvaluator) Patcher {
|
||||
return &applyConfigPatcher{expressionEvaluator: expressionEvaluator}
|
||||
}
|
||||
|
||||
type applyConfigPatcher struct {
|
||||
expressionEvaluator plugincel.MutatingEvaluator
|
||||
}
|
||||
|
||||
func (e *applyConfigPatcher) Patch(ctx context.Context, r Request, runtimeCELCostBudget int64) (runtime.Object, error) {
|
||||
admissionRequest := plugincel.CreateAdmissionRequest(
|
||||
r.VersionedAttributes.Attributes,
|
||||
metav1.GroupVersionResource(r.MatchedResource),
|
||||
metav1.GroupVersionKind(r.VersionedAttributes.VersionedKind))
|
||||
|
||||
compileErrors := e.expressionEvaluator.CompilationErrors()
|
||||
if len(compileErrors) > 0 {
|
||||
return nil, errors.Join(compileErrors...)
|
||||
}
|
||||
eval, _, err := e.expressionEvaluator.ForInput(ctx, r.VersionedAttributes, admissionRequest, r.OptionalVariables, r.Namespace, runtimeCELCostBudget)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if eval.Error != nil {
|
||||
return nil, eval.Error
|
||||
}
|
||||
v := eval.EvalResult
|
||||
|
||||
// The compiler ensures that the return type is an ObjectVal with type name of "Object".
|
||||
objVal, ok := v.(*dynamic.ObjectVal)
|
||||
if !ok {
|
||||
// Should not happen since the compiler type checks the return type.
|
||||
return nil, fmt.Errorf("unsupported return type from ApplyConfiguration expression: %v", v.Type())
|
||||
}
|
||||
// TODO: Object initializers are insufficiently type checked.
|
||||
// In the interim, we use this sanity check to detect type mismatches
|
||||
// between field names and Object initializers. For example,
|
||||
// "Object.spec{ selector: Object.spec.wrong{}}" is detected as a mismatch.
|
||||
// Before beta, attaching full type information both to Object initializers and
|
||||
// the "object" and "oldObject" variables is needed. This will allow CEL to
|
||||
// perform comprehensive runtime type checking.
|
||||
err = objVal.CheckTypeNamesMatchFieldPathNames()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("type mismatch: %w", err)
|
||||
}
|
||||
|
||||
value, ok := objVal.Value().(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid return type: %T", v)
|
||||
}
|
||||
|
||||
patchObject := unstructured.Unstructured{Object: value}
|
||||
patchObject.SetGroupVersionKind(r.VersionedAttributes.VersionedObject.GetObjectKind().GroupVersionKind())
|
||||
patched, err := ApplyStructuredMergeDiff(r.TypeConverter, r.VersionedAttributes.VersionedObject, &patchObject)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error applying patch: %w", err)
|
||||
}
|
||||
return patched, nil
|
||||
}
|
||||
|
||||
// ApplyStructuredMergeDiff applies a structured merge diff to an object and returns a copy of the object
|
||||
// with the patch applied.
|
||||
func ApplyStructuredMergeDiff(
|
||||
typeConverter managedfields.TypeConverter,
|
||||
originalObject runtime.Object,
|
||||
patch *unstructured.Unstructured,
|
||||
) (runtime.Object, error) {
|
||||
if patch.GroupVersionKind() != originalObject.GetObjectKind().GroupVersionKind() {
|
||||
return nil, fmt.Errorf("patch (%v) and original object (%v) are not of the same gvk", patch.GroupVersionKind().String(), originalObject.GetObjectKind().GroupVersionKind().String())
|
||||
} else if typeConverter == nil {
|
||||
return nil, fmt.Errorf("type converter must not be nil")
|
||||
}
|
||||
|
||||
patchObjTyped, err := typeConverter.ObjectToTyped(patch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert patch object to typed object: %w", err)
|
||||
}
|
||||
|
||||
err = validatePatch(patchObjTyped)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid ApplyConfiguration: %w", err)
|
||||
}
|
||||
|
||||
liveObjTyped, err := typeConverter.ObjectToTyped(originalObject)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert original object to typed object: %w", err)
|
||||
}
|
||||
|
||||
newObjTyped, err := liveObjTyped.Merge(patchObjTyped)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to merge patch: %w", err)
|
||||
}
|
||||
|
||||
// Our mutating admission policy sets the fields but does not track ownership.
|
||||
// Newly introduced fields in the patch won't be tracked by a field manager
|
||||
// (so if the original object is updated again but the mutating policy is
|
||||
// not active, the fields will be dropped).
|
||||
//
|
||||
// This necessarily means that changes to an object by a mutating policy
|
||||
// are only preserved if the policy was active at the time of the change.
|
||||
// (If the policy is not active, the changes may be dropped.)
|
||||
|
||||
newObj, err := typeConverter.TypedToObject(newObjTyped)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert typed object to object: %w", err)
|
||||
}
|
||||
|
||||
return newObj, nil
|
||||
}
|
||||
|
||||
// validatePatch searches an apply configuration for any arrays, maps or structs elements that are atomic and returns
|
||||
// an error if any are found.
|
||||
func validatePatch(v *typed.TypedValue) error {
|
||||
atomics := findAtomics(nil, v.Schema(), v.TypeRef(), v.AsValue())
|
||||
if len(atomics) > 0 {
|
||||
return fmt.Errorf("may not mutate atomic arrays, maps or structs: %v", strings.Join(atomics, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findAtomics returns field paths for any atomic arrays, maps or structs found when traversing the given value.
|
||||
func findAtomics(path []fieldpath.PathElement, s *schema.Schema, tr schema.TypeRef, v value.Value) (atomics []string) {
|
||||
if a, ok := s.Resolve(tr); ok { // Validation pass happens before this and checks that all schemas can be resolved
|
||||
if v.IsMap() && a.Map != nil {
|
||||
if a.Map.ElementRelationship == schema.Atomic {
|
||||
atomics = append(atomics, pathString(path))
|
||||
}
|
||||
v.AsMap().Iterate(func(key string, val value.Value) bool {
|
||||
pe := fieldpath.PathElement{FieldName: &key}
|
||||
if sf, ok := a.Map.FindField(key); ok {
|
||||
tr = sf.Type
|
||||
atomics = append(atomics, findAtomics(append(path, pe), s, tr, val)...)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
if v.IsList() && a.List != nil {
|
||||
if a.List.ElementRelationship == schema.Atomic {
|
||||
atomics = append(atomics, pathString(path))
|
||||
}
|
||||
list := v.AsList()
|
||||
for i := 0; i < list.Length(); i++ {
|
||||
pe := fieldpath.PathElement{Index: &i}
|
||||
atomics = append(atomics, findAtomics(append(path, pe), s, a.List.ElementType, list.At(i))...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return atomics
|
||||
}
|
||||
|
||||
func pathString(path []fieldpath.PathElement) string {
|
||||
sb := strings.Builder{}
|
||||
for _, p := range path {
|
||||
sb.WriteString(p.String())
|
||||
}
|
||||
return sb.String()
|
||||
}
|
@ -0,0 +1,187 @@
|
||||
/*
|
||||
Copyright 2024 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 patch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/openapi"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
)
|
||||
|
||||
type TypeConverterManager interface {
|
||||
// GetTypeConverter returns a type converter for the given GVK
|
||||
GetTypeConverter(gvk schema.GroupVersionKind) managedfields.TypeConverter
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
func NewTypeConverterManager(
|
||||
staticTypeConverter managedfields.TypeConverter,
|
||||
openapiClient openapi.Client,
|
||||
) TypeConverterManager {
|
||||
return &typeConverterManager{
|
||||
staticTypeConverter: staticTypeConverter,
|
||||
openapiClient: openapiClient,
|
||||
typeConverterMap: make(map[schema.GroupVersion]typeConverterCacheEntry),
|
||||
lastFetchedPaths: make(map[schema.GroupVersion]openapi.GroupVersion),
|
||||
}
|
||||
}
|
||||
|
||||
type typeConverterCacheEntry struct {
|
||||
typeConverter managedfields.TypeConverter
|
||||
entry openapi.GroupVersion
|
||||
}
|
||||
|
||||
// typeConverterManager helps us make sure we have an up to date schema and
|
||||
// type converter for our openapi models. It should be connfigured to use a
|
||||
// static type converter for natively typed schemas, and fetches the schema
|
||||
// for CRDs/other over the network on demand (trying to reduce network calls where necessary)
|
||||
type typeConverterManager struct {
|
||||
// schemaCache is used to cache the schema for a given GVK
|
||||
staticTypeConverter managedfields.TypeConverter
|
||||
|
||||
// discoveryClient is used to fetch the schema for a given GVK
|
||||
openapiClient openapi.Client
|
||||
|
||||
lock sync.RWMutex
|
||||
|
||||
typeConverterMap map[schema.GroupVersion]typeConverterCacheEntry
|
||||
lastFetchedPaths map[schema.GroupVersion]openapi.GroupVersion
|
||||
}
|
||||
|
||||
func (t *typeConverterManager) Run(ctx context.Context) {
|
||||
// Loop every 5s refershing the OpenAPI schema list to know which
|
||||
// schemas have been invalidated. This should use e-tags under the hood
|
||||
_ = wait.PollUntilContextCancel(ctx, 5*time.Second, true, func(_ context.Context) (done bool, err error) {
|
||||
paths, err := t.openapiClient.Paths()
|
||||
if err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("failed to fetch openapi paths: %w", err))
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// The /openapi/v3 endpoint contains a list of paths whose ServerRelativeURL
|
||||
// value changes every time the schema is updated. So we poll /openapi/v3
|
||||
// to get the "version number" for each schema, and invalidate our cache
|
||||
// if the version number has changed since we pulled it.
|
||||
parsedPaths := make(map[schema.GroupVersion]openapi.GroupVersion, len(paths))
|
||||
for path, entry := range paths {
|
||||
if !strings.HasPrefix(path, "apis/") && !strings.HasPrefix(path, "api/") {
|
||||
continue
|
||||
}
|
||||
path = strings.TrimPrefix(path, "apis/")
|
||||
path = strings.TrimPrefix(path, "api/")
|
||||
|
||||
gv, err := schema.ParseGroupVersion(path)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("failed to parse group version %q: %w", path, err))
|
||||
return false, nil
|
||||
}
|
||||
|
||||
parsedPaths[gv] = entry
|
||||
}
|
||||
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
t.lastFetchedPaths = parsedPaths
|
||||
return false, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (t *typeConverterManager) GetTypeConverter(gvk schema.GroupVersionKind) managedfields.TypeConverter {
|
||||
// Check to see if the static type converter handles this GVK
|
||||
if t.staticTypeConverter != nil {
|
||||
//!TODO: Add ability to check existence to type converter
|
||||
// working around for now but seeing if getting a typed version of an
|
||||
// empty object returns error
|
||||
stub := &unstructured.Unstructured{}
|
||||
stub.SetGroupVersionKind(gvk)
|
||||
|
||||
if _, err := t.staticTypeConverter.ObjectToTyped(stub); err == nil {
|
||||
return t.staticTypeConverter
|
||||
}
|
||||
}
|
||||
|
||||
gv := gvk.GroupVersion()
|
||||
|
||||
existing, entry, err := func() (managedfields.TypeConverter, openapi.GroupVersion, error) {
|
||||
t.lock.RLock()
|
||||
defer t.lock.RUnlock()
|
||||
|
||||
// If schema is not supported by static type converter, ask discovery
|
||||
// for the schema
|
||||
entry, ok := t.lastFetchedPaths[gv]
|
||||
if !ok {
|
||||
// If we can't get the schema, we can't do anything
|
||||
return nil, nil, fmt.Errorf("no schema for %v", gvk)
|
||||
}
|
||||
|
||||
// If the entry schema has not changed, used the same type converter
|
||||
if existing, ok := t.typeConverterMap[gv]; ok && existing.entry.ServerRelativeURL() == entry.ServerRelativeURL() {
|
||||
// If we have a type converter for this GVK, return it
|
||||
return existing.typeConverter, existing.entry, nil
|
||||
}
|
||||
|
||||
return nil, entry, nil
|
||||
}()
|
||||
if err != nil {
|
||||
utilruntime.HandleError(err)
|
||||
return nil
|
||||
} else if existing != nil {
|
||||
return existing
|
||||
}
|
||||
|
||||
schBytes, err := entry.Schema(runtime.ContentTypeJSON)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("failed to get schema for %v: %w", gvk, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
var sch spec3.OpenAPI
|
||||
if err := json.Unmarshal(schBytes, &sch); err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("failed to unmarshal schema for %v: %w", gvk, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
// The schema has changed, or there is no entry for it, generate
|
||||
// a new type converter for this GV
|
||||
tc, err := managedfields.NewTypeConverter(sch.Components.Schemas, false)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("failed to create type converter for %v: %w", gvk, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
|
||||
t.typeConverterMap[gv] = typeConverterCacheEntry{
|
||||
typeConverter: tc,
|
||||
entry: entry,
|
||||
}
|
||||
|
||||
return tc
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
/*
|
||||
Copyright 2024 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 mutating
|
||||
|
||||
import (
|
||||
"context"
|
||||
celgo "github.com/google/cel-go/cel"
|
||||
"io"
|
||||
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/component-base/featuregate"
|
||||
)
|
||||
|
||||
const (
|
||||
// PluginName indicates the name of admission plug-in
|
||||
PluginName = "MutatingAdmissionPolicy"
|
||||
)
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(configFile io.Reader) (admission.Interface, error) {
|
||||
return NewPlugin(configFile), nil
|
||||
})
|
||||
}
|
||||
|
||||
// Plugin is an implementation of admission.Interface.
|
||||
type Policy = v1alpha1.MutatingAdmissionPolicy
|
||||
type PolicyBinding = v1alpha1.MutatingAdmissionPolicyBinding
|
||||
type PolicyMutation = v1alpha1.Mutation
|
||||
type PolicyHook = generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]
|
||||
|
||||
type Mutator struct {
|
||||
}
|
||||
type MutationEvaluationFunc func(
|
||||
ctx context.Context,
|
||||
matchedResource schema.GroupVersionResource,
|
||||
versionedAttr *admission.VersionedAttributes,
|
||||
o admission.ObjectInterfaces,
|
||||
versionedParams runtime.Object,
|
||||
namespace *corev1.Namespace,
|
||||
typeConverter managedfields.TypeConverter,
|
||||
runtimeCELCostBudget int64,
|
||||
authorizer authorizer.Authorizer,
|
||||
) (runtime.Object, error)
|
||||
|
||||
type PolicyEvaluator struct {
|
||||
Matcher matchconditions.Matcher
|
||||
Mutators []patch.Patcher
|
||||
CompositionEnv *cel.CompositionEnv
|
||||
Error error
|
||||
}
|
||||
|
||||
type Plugin struct {
|
||||
*generic.Plugin[PolicyHook]
|
||||
}
|
||||
|
||||
var _ admission.Interface = &Plugin{}
|
||||
var _ admission.MutationInterface = &Plugin{}
|
||||
|
||||
// NewPlugin returns a generic admission webhook plugin.
|
||||
func NewPlugin(_ io.Reader) *Plugin {
|
||||
// There is no request body to mutate for DELETE, so this plugin never handles that operation.
|
||||
handler := admission.NewHandler(admission.Create, admission.Update, admission.Connect)
|
||||
res := &Plugin{}
|
||||
res.Plugin = generic.NewPlugin(
|
||||
handler,
|
||||
func(f informers.SharedInformerFactory, client kubernetes.Interface, dynamicClient dynamic.Interface, restMapper meta.RESTMapper) generic.Source[PolicyHook] {
|
||||
return generic.NewPolicySource(
|
||||
f.Admissionregistration().V1alpha1().MutatingAdmissionPolicies().Informer(),
|
||||
f.Admissionregistration().V1alpha1().MutatingAdmissionPolicyBindings().Informer(),
|
||||
NewMutatingAdmissionPolicyAccessor,
|
||||
NewMutatingAdmissionPolicyBindingAccessor,
|
||||
compilePolicy,
|
||||
//!TODO: Create a way to share param informers between
|
||||
// mutating/validating plugins
|
||||
f,
|
||||
dynamicClient,
|
||||
restMapper,
|
||||
)
|
||||
},
|
||||
func(a authorizer.Authorizer, m *matching.Matcher, client kubernetes.Interface) generic.Dispatcher[PolicyHook] {
|
||||
return NewDispatcher(a, m, patch.NewTypeConverterManager(nil, client.Discovery().OpenAPIV3()))
|
||||
},
|
||||
)
|
||||
return res
|
||||
}
|
||||
|
||||
// Admit makes an admission decision based on the request attributes.
|
||||
func (a *Plugin) Admit(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error {
|
||||
return a.Plugin.Dispatch(ctx, attr, o)
|
||||
}
|
||||
|
||||
func (a *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
|
||||
a.Plugin.SetEnabled(featureGates.Enabled(features.MutatingAdmissionPolicy))
|
||||
}
|
||||
|
||||
// Variable is a named expression for composition.
|
||||
type Variable struct {
|
||||
Name string
|
||||
Expression string
|
||||
}
|
||||
|
||||
func (v *Variable) GetExpression() string {
|
||||
return v.Expression
|
||||
}
|
||||
|
||||
func (v *Variable) ReturnTypes() []*celgo.Type {
|
||||
return []*celgo.Type{celgo.AnyType, celgo.DynType}
|
||||
}
|
||||
|
||||
func (v *Variable) GetName() string {
|
||||
return v.Name
|
||||
}
|
||||
|
||||
func convertv1alpha1Variables(variables []v1alpha1.Variable) []cel.NamedExpressionAccessor {
|
||||
namedExpressions := make([]cel.NamedExpressionAccessor, len(variables))
|
||||
for i, variable := range variables {
|
||||
namedExpressions[i] = &Variable{Name: variable.Name, Expression: variable.Expression}
|
||||
}
|
||||
return namedExpressions
|
||||
}
|
@ -0,0 +1,292 @@
|
||||
/*
|
||||
Copyright 2024 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 mutating_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/openapi/openapitest"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func setupTest(
|
||||
t *testing.T,
|
||||
compiler func(*mutating.Policy) mutating.PolicyEvaluator,
|
||||
) *generic.PolicyTestContext[*mutating.Policy, *mutating.PolicyBinding, mutating.PolicyEvaluator] {
|
||||
|
||||
testContext, testCancel, err := generic.NewPolicyTestContext[*mutating.Policy, *mutating.PolicyBinding, mutating.PolicyEvaluator](
|
||||
mutating.NewMutatingAdmissionPolicyAccessor,
|
||||
mutating.NewMutatingAdmissionPolicyBindingAccessor,
|
||||
compiler,
|
||||
func(a authorizer.Authorizer, m *matching.Matcher, i kubernetes.Interface) generic.Dispatcher[mutating.PolicyHook] {
|
||||
// Use embedded schemas rather than discovery schemas
|
||||
return mutating.NewDispatcher(a, m, patch.NewTypeConverterManager(nil, openapitest.NewEmbeddedFileClient()))
|
||||
},
|
||||
nil,
|
||||
[]meta.RESTMapping{
|
||||
{
|
||||
Resource: schema.GroupVersionResource{
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Resource: "pods",
|
||||
},
|
||||
GroupVersionKind: schema.GroupVersionKind{
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Kind: "Pod",
|
||||
},
|
||||
Scope: meta.RESTScopeNamespace,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(testCancel)
|
||||
require.NoError(t, testContext.Start())
|
||||
return testContext
|
||||
}
|
||||
|
||||
// Show that a compiler that always sets an annotation on the object works
|
||||
func TestBasicPatch(t *testing.T) {
|
||||
expectedAnnotations := map[string]string{"foo": "bar"}
|
||||
|
||||
// Treat all policies as setting foo annotation to bar
|
||||
testContext := setupTest(t, func(p *mutating.Policy) mutating.PolicyEvaluator {
|
||||
return mutating.PolicyEvaluator{Mutators: []patch.Patcher{annotationPatcher{expectedAnnotations}}}
|
||||
})
|
||||
|
||||
// Set up a policy and binding that match, no params
|
||||
require.NoError(t, testContext.UpdateAndWait(
|
||||
&mutating.Policy{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "policy"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicySpec{
|
||||
MatchConstraints: &v1alpha1.MatchResources{
|
||||
MatchPolicy: ptr.To(v1alpha1.Equivalent),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
},
|
||||
Mutations: []v1alpha1.Mutation{
|
||||
{
|
||||
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
|
||||
Expression: "ignored, but required",
|
||||
},
|
||||
PatchType: v1alpha1.PatchTypeApplyConfiguration,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&mutating.PolicyBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
|
||||
PolicyName: "policy",
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
// Show that if we run an object through the policy, it gets the annotation
|
||||
testObject := &corev1.ConfigMap{}
|
||||
err := testContext.Dispatch(testObject, nil, admission.Create)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedAnnotations, testObject.Annotations)
|
||||
}
|
||||
|
||||
func TestSSAPatch(t *testing.T) {
|
||||
patchObj := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{
|
||||
"annotations": map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"myfield": "myvalue",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testContext := setupTest(t, func(p *mutating.Policy) mutating.PolicyEvaluator {
|
||||
return mutating.PolicyEvaluator{
|
||||
Mutators: []patch.Patcher{smdPatcher{patch: patchObj}},
|
||||
}
|
||||
})
|
||||
|
||||
// Set up a policy and binding that match, no params
|
||||
require.NoError(t, testContext.UpdateAndWait(
|
||||
&mutating.Policy{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "policy"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicySpec{
|
||||
MatchConstraints: &v1alpha1.MatchResources{
|
||||
MatchPolicy: ptr.To(v1alpha1.Equivalent),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
},
|
||||
Mutations: []v1alpha1.Mutation{
|
||||
{
|
||||
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
|
||||
Expression: "ignored, but required",
|
||||
},
|
||||
PatchType: v1alpha1.PatchTypeApplyConfiguration,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&mutating.PolicyBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
|
||||
PolicyName: "policy",
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
// Show that if we run an object through the policy, it gets the annotation
|
||||
testObject := &corev1.ConfigMap{}
|
||||
err := testContext.Dispatch(testObject, nil, admission.Create)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"foo": "bar"},
|
||||
},
|
||||
Data: map[string]string{"myfield": "myvalue"},
|
||||
}, testObject)
|
||||
}
|
||||
|
||||
func TestSSAMapList(t *testing.T) {
|
||||
patchObj := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": map[string]interface{}{
|
||||
"annotations": map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"initContainers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "injected-init-container",
|
||||
"image": "injected-image",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testContext := setupTest(t, func(p *mutating.Policy) mutating.PolicyEvaluator {
|
||||
return mutating.PolicyEvaluator{
|
||||
Mutators: []patch.Patcher{smdPatcher{patch: patchObj}},
|
||||
}
|
||||
})
|
||||
|
||||
// Set up a policy and binding that match, no params
|
||||
require.NoError(t, testContext.UpdateAndWait(
|
||||
&mutating.Policy{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "policy"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicySpec{
|
||||
MatchConstraints: &v1alpha1.MatchResources{
|
||||
MatchPolicy: ptr.To(v1alpha1.Equivalent),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
},
|
||||
Mutations: []v1alpha1.Mutation{
|
||||
{
|
||||
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
|
||||
Expression: "ignored, but required",
|
||||
},
|
||||
PatchType: v1alpha1.PatchTypeApplyConfiguration,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&mutating.PolicyBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
|
||||
PolicyName: "policy",
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
// Show that if we run an object through the policy, it gets the annotation
|
||||
testObject := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{},
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "init-container",
|
||||
Image: "image",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err := testContext.Dispatch(testObject, nil, admission.Create)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"foo": "bar"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "init-container",
|
||||
Image: "image",
|
||||
},
|
||||
{
|
||||
Name: "injected-init-container",
|
||||
Image: "injected-image",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, testObject)
|
||||
}
|
||||
|
||||
type annotationPatcher struct {
|
||||
annotations map[string]string
|
||||
}
|
||||
|
||||
func (ap annotationPatcher) Patch(ctx context.Context, request patch.Request, runtimeCELCostBudget int64) (runtime.Object, error) {
|
||||
obj := request.VersionedAttributes.VersionedObject.DeepCopyObject()
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accessor.SetAnnotations(ap.annotations)
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
type smdPatcher struct {
|
||||
patch *unstructured.Unstructured
|
||||
}
|
||||
|
||||
func (sp smdPatcher) Patch(ctx context.Context, request patch.Request, runtimeCELCostBudget int64) (runtime.Object, error) {
|
||||
return patch.ApplyStructuredMergeDiff(request.TypeConverter, request.VersionedAttributes.VersionedObject, sp.patch)
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
Copyright 2019 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 mutating
|
||||
|
||||
import (
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
)
|
||||
|
||||
type key struct {
|
||||
PolicyUID types.NamespacedName
|
||||
BindingUID types.NamespacedName
|
||||
ParamUID types.NamespacedName
|
||||
MutationIndex int
|
||||
}
|
||||
|
||||
type policyReinvokeContext struct {
|
||||
// lastPolicyOutput holds the result of the last Policy admission plugin call
|
||||
lastPolicyOutput runtime.Object
|
||||
// previouslyInvokedReinvocablePolicys holds the set of policies that have been invoked and
|
||||
// should be reinvoked if a later mutation occurs
|
||||
previouslyInvokedReinvocablePolicies sets.Set[key]
|
||||
// reinvokePolicies holds the set of Policies that should be reinvoked
|
||||
reinvokePolicies sets.Set[key]
|
||||
}
|
||||
|
||||
func (rc *policyReinvokeContext) ShouldReinvoke(policy key) bool {
|
||||
return rc.reinvokePolicies.Has(policy)
|
||||
}
|
||||
|
||||
func (rc *policyReinvokeContext) IsOutputChangedSinceLastPolicyInvocation(object runtime.Object) bool {
|
||||
return !apiequality.Semantic.DeepEqual(rc.lastPolicyOutput, object)
|
||||
}
|
||||
|
||||
func (rc *policyReinvokeContext) SetLastPolicyInvocationOutput(object runtime.Object) {
|
||||
if object == nil {
|
||||
rc.lastPolicyOutput = nil
|
||||
return
|
||||
}
|
||||
rc.lastPolicyOutput = object.DeepCopyObject()
|
||||
}
|
||||
|
||||
func (rc *policyReinvokeContext) AddReinvocablePolicyToPreviouslyInvoked(policy key) {
|
||||
if rc.previouslyInvokedReinvocablePolicies == nil {
|
||||
rc.previouslyInvokedReinvocablePolicies = sets.New[key]()
|
||||
}
|
||||
rc.previouslyInvokedReinvocablePolicies.Insert(policy)
|
||||
}
|
||||
|
||||
func (rc *policyReinvokeContext) RequireReinvokingPreviouslyInvokedPlugins() {
|
||||
if len(rc.previouslyInvokedReinvocablePolicies) > 0 {
|
||||
if rc.reinvokePolicies == nil {
|
||||
rc.reinvokePolicies = sets.New[key]()
|
||||
}
|
||||
for s := range rc.previouslyInvokedReinvocablePolicies {
|
||||
rc.reinvokePolicies.Insert(s)
|
||||
}
|
||||
rc.previouslyInvokedReinvocablePolicies = sets.New[key]()
|
||||
}
|
||||
}
|
@ -54,6 +54,10 @@ func (v *validatingAdmissionPolicyAccessor) GetMatchConstraints() *v1.MatchResou
|
||||
return v.Spec.MatchConstraints
|
||||
}
|
||||
|
||||
func (v *validatingAdmissionPolicyAccessor) GetFailurePolicy() *v1.FailurePolicyType {
|
||||
return v.Spec.FailurePolicy
|
||||
}
|
||||
|
||||
type validatingAdmissionPolicyBindingAccessor struct {
|
||||
*v1.ValidatingAdmissionPolicyBinding
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ import (
|
||||
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/warning"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -364,7 +365,7 @@ func setupTestCommon(
|
||||
func(p *validating.Policy) validating.Validator {
|
||||
return compiler.CompilePolicy(p)
|
||||
},
|
||||
func(a authorizer.Authorizer, m *matching.Matcher) generic.Dispatcher[validating.PolicyHook] {
|
||||
func(a authorizer.Authorizer, m *matching.Matcher, client kubernetes.Interface) generic.Dispatcher[validating.PolicyHook] {
|
||||
coolMatcher := matcher
|
||||
if coolMatcher == nil {
|
||||
coolMatcher = generic.NewPolicyMatcher(m)
|
||||
|
@ -30,6 +30,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utiljson "k8s.io/apimachinery/pkg/util/json"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
admissionauthorizer "k8s.io/apiserver/pkg/admission/plugin/authorizer"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
|
||||
celmetrics "k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
@ -63,6 +64,10 @@ type policyDecisionWithMetadata struct {
|
||||
Binding *admissionregistrationv1.ValidatingAdmissionPolicyBinding
|
||||
}
|
||||
|
||||
func (c *dispatcher) Run(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Dispatch implements generic.Dispatcher.
|
||||
func (c *dispatcher) Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []PolicyHook) error {
|
||||
|
||||
@ -109,7 +114,7 @@ func (c *dispatcher) Dispatch(ctx context.Context, a admission.Attributes, o adm
|
||||
}
|
||||
}
|
||||
|
||||
authz := newCachingAuthorizer(c.authz)
|
||||
authz := admissionauthorizer.NewCachingAuthorizer(c.authz)
|
||||
|
||||
for _, hook := range hooks {
|
||||
// versionedAttributes will be set to non-nil inside of the loop, but
|
||||
|
@ -112,7 +112,7 @@ func NewPlugin(_ io.Reader) *Plugin {
|
||||
restMapper,
|
||||
)
|
||||
},
|
||||
func(a authorizer.Authorizer, m *matching.Matcher) generic.Dispatcher[PolicyHook] {
|
||||
func(a authorizer.Authorizer, m *matching.Matcher, client kubernetes.Interface) generic.Dispatcher[PolicyHook] {
|
||||
return NewDispatcher(a, generic.NewPolicyMatcher(m))
|
||||
},
|
||||
),
|
||||
@ -151,13 +151,13 @@ func compilePolicy(policy *Policy) Validator {
|
||||
for i := range matchConditions {
|
||||
matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
|
||||
}
|
||||
matcher = matchconditions.NewMatcher(filterCompiler.Compile(matchExpressionAccessors, optionalVars, environment.StoredExpressions), failurePolicy, "policy", "validate", policy.Name)
|
||||
matcher = matchconditions.NewMatcher(filterCompiler.CompileCondition(matchExpressionAccessors, optionalVars, environment.StoredExpressions), failurePolicy, "policy", "validate", policy.Name)
|
||||
}
|
||||
res := NewValidator(
|
||||
filterCompiler.Compile(convertv1Validations(policy.Spec.Validations), optionalVars, environment.StoredExpressions),
|
||||
filterCompiler.CompileCondition(convertv1Validations(policy.Spec.Validations), optionalVars, environment.StoredExpressions),
|
||||
matcher,
|
||||
filterCompiler.Compile(convertv1AuditAnnotations(policy.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions),
|
||||
filterCompiler.Compile(convertv1MessageExpressions(policy.Spec.Validations), expressionOptionalVars, environment.StoredExpressions),
|
||||
filterCompiler.CompileCondition(convertv1AuditAnnotations(policy.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions),
|
||||
filterCompiler.CompileCondition(convertv1MessageExpressions(policy.Spec.Validations), expressionOptionalVars, environment.StoredExpressions),
|
||||
failurePolicy,
|
||||
)
|
||||
|
||||
|
@ -436,7 +436,7 @@ func buildEnvSet(hasParams bool, hasAuthorizer bool, types typeOverwrite) (*envi
|
||||
)
|
||||
}
|
||||
|
||||
// createVariableOpts creates a slice of EnvOption
|
||||
// createVariableOpts creates a slice of ResolverEnvOption
|
||||
// that can be used for creating a CEL env containing variables of declType.
|
||||
// declType can be nil, in which case the variables will be of DynType.
|
||||
func createVariableOpts(declType *apiservercel.DeclType, variables ...string) []cel.EnvOption {
|
||||
|
@ -31,6 +31,7 @@ import (
|
||||
"k8s.io/apiserver/pkg/admission/initializer"
|
||||
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
|
||||
mutatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/mutating"
|
||||
validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating"
|
||||
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
|
||||
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
|
||||
@ -90,7 +91,7 @@ func NewAdmissionOptions() *AdmissionOptions {
|
||||
// admission plugins. The apiserver always runs the validating ones
|
||||
// after all the mutating ones, so their relative order in this list
|
||||
// doesn't matter.
|
||||
RecommendedPluginOrder: []string{lifecycle.PluginName, mutatingwebhook.PluginName, validatingadmissionpolicy.PluginName, validatingwebhook.PluginName},
|
||||
RecommendedPluginOrder: []string{lifecycle.PluginName, mutatingadmissionpolicy.PluginName, mutatingwebhook.PluginName, validatingadmissionpolicy.PluginName, validatingwebhook.PluginName},
|
||||
DefaultOffPlugins: sets.Set[string]{},
|
||||
}
|
||||
server.RegisterAllAdmissionPlugins(options.Plugins)
|
||||
|
@ -36,7 +36,7 @@ func TestEnabledPluginNames(t *testing.T) {
|
||||
}{
|
||||
// scenario 0: check if a call to enabledPluginNames sets expected values.
|
||||
{
|
||||
expectedPluginNames: []string{"NamespaceLifecycle", "MutatingAdmissionWebhook", "ValidatingAdmissionPolicy", "ValidatingAdmissionWebhook"},
|
||||
expectedPluginNames: []string{"NamespaceLifecycle", "MutatingAdmissionPolicy", "MutatingAdmissionWebhook", "ValidatingAdmissionPolicy", "ValidatingAdmissionWebhook"},
|
||||
},
|
||||
|
||||
// scenario 1: use default off plugins if no specified
|
||||
|
@ -20,6 +20,7 @@ package server
|
||||
import (
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
|
||||
mutatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/mutating"
|
||||
validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating"
|
||||
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
|
||||
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
|
||||
@ -31,4 +32,5 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
|
||||
validatingwebhook.Register(plugins)
|
||||
mutatingwebhook.Register(plugins)
|
||||
validatingadmissionpolicy.Register(plugins)
|
||||
mutatingadmissionpolicy.Register(plugins)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user