From 095898324eeadf5b7801f63613d447a2a30be791 Mon Sep 17 00:00:00 2001 From: Alexander Zielenski Date: Mon, 22 Jan 2024 17:33:10 -0800 Subject: [PATCH] feature: add mutatingadmissionpolicy plugin pkg scaf --- .../plugin/policy/mutating/accessor.go | 82 ++++++++++++++ .../plugin/policy/mutating/evaluator.go | 4 + .../plugin/policy/mutating/plugin.go | 106 ++++++++++++++++++ .../plugin/policy/mutating/plugin_test.go | 68 +++++++++++ .../admission/plugin/policy/mutating/types.go | 78 +++++++++++++ 5 files changed, 338 insertions(+) create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/accessor.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/evaluator.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin_test.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/types.go diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/accessor.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/accessor.go new file mode 100644 index 0000000000000..ef3f8ae694da6 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/accessor.go @@ -0,0 +1,82 @@ +/* +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 ( + "k8s.io/api/admissionregistration/v1beta1" + "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() *v1beta1.ParamKind { + return v.Spec.ParamKind +} + +func (v *mutatingAdmissionPolicyAccessor) GetMatchConstraints() *v1beta1.MatchResources { + return v.Spec.MatchConstraints +} + +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() *v1beta1.MatchResources { + return v.Spec.MatchResources +} + +func (v *mutatingAdmissionPolicyBindingAccessor) GetParamRef() *v1beta1.ParamRef { + return v.Spec.ParamRef +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/evaluator.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/evaluator.go new file mode 100644 index 0000000000000..4bdac3b899034 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/evaluator.go @@ -0,0 +1,4 @@ +package mutating + +type mutatingAdmissionPolicyEvaluator struct { +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin.go new file mode 100644 index 0000000000000..65967443365ac --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin.go @@ -0,0 +1,106 @@ +package mutating + +import ( + "context" + "io" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/admission/plugin/policy/generic" + "k8s.io/apiserver/pkg/admission/plugin/policy/matching" + webhookgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + "sigs.k8s.io/structured-merge-diff/v4/fieldpath" +) + +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 = MutatingAdmissionPolicy +type PolicyBinding = MutatingAdmissionPolicyBinding +type PolicyHook = generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator] +type PolicyEvaluator = func() (toSet *unstructured.Unstructured, toRemove fieldpath.Set) + +type Plugin struct { + *generic.Plugin[PolicyHook] +} + +var _ admission.Interface = &Plugin{} +var _ admission.MutationInterface = &Plugin{} + +// NewMutatingWebhook returns a generic admission webhook plugin. +func NewPlugin(_ io.Reader) *Plugin { + handler := admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update) + 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( + nil, //!TODO once we have API types + nil, //!TODO once we have API types + 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) generic.Dispatcher[PolicyHook] { + return NewDispatcher(a, m) + }, + ) + 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 compilePolicy(policy *Policy) PolicyEvaluator { + //!TODO: Implement this + // Should compile the policy into a funciton that takes a param, namespace, + // request info, etc, and returns: + // 1. Unstructured Patch of Fields to Set + // 2. Slice of field paths to delete? Or unstructured map of deleted fields? + return nil +} + +func NewDispatcher(a authorizer.Authorizer, m *matching.Matcher) generic.Dispatcher[PolicyHook] { + return generic.NewPolicyDispatcher[*Policy, *PolicyBinding, PolicyEvaluator]( + NewMutatingAdmissionPolicyAccessor, + NewMutatingAdmissionPolicyBindingAccessor, + m, + dispatch, + ) +} + +func dispatch( + ctx context.Context, + a webhookgeneric.VersionedAttributeAccessor, + o admission.ObjectInterfaces, + invocations []generic.PolicyInvocation[*Policy, *PolicyBinding, PolicyEvaluator], +) error { + + // Should loop through invocations, handling possible error and invoking + // evaluator to apply patch + + return nil +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin_test.go new file mode 100644 index 0000000000000..7b1adb0f50205 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin_test.go @@ -0,0 +1,68 @@ +package mutating_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/api/admissionregistration/v1beta1" + 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/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/admission/plugin/policy/generic" + "k8s.io/apiserver/pkg/admission/plugin/policy/mutating" + "k8s.io/utils/ptr" + "sigs.k8s.io/structured-merge-diff/v4/fieldpath" +) + +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, + mutating.NewDispatcher, + nil, + []meta.RESTMapping{}) + 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) { + // Treat all policies as setting foo annotation to bar + testContext := setupTest(t, func(p *mutating.Policy) mutating.PolicyEvaluator { + return func() (toSet *unstructured.Unstructured, toRemove fieldpath.Set) { + toSet = &unstructured.Unstructured{} + toSet.SetAnnotations(map[string]string{"foo": "bar"}) + return + } + }) + + // Set up a policy and binding that match, no params + testContext.UpdateAndWait( + &mutating.Policy{ + ObjectMeta: metav1.ObjectMeta{Name: "policy"}, + Spec: mutating.MutatingAdmissionPolicySpec{ + FailurePolicy: ptr.To(v1beta1.Fail), + }, + }, + &mutating.PolicyBinding{ + ObjectMeta: metav1.ObjectMeta{Name: "binding"}, + Spec: mutating.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) +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/types.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/types.go new file mode 100644 index 0000000000000..238f987b547d2 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/types.go @@ -0,0 +1,78 @@ +/* +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 ( + "k8s.io/api/admissionregistration/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// Temporary API types to use for unit testing until the real types are available. +type MutatingAdmissionPolicy struct { + metav1.TypeMeta + metav1.ObjectMeta + + Spec MutatingAdmissionPolicySpec + Status MutatingAdmissionPolicyStatus +} + +type MutatingAdmissionPolicyBinding struct { + metav1.TypeMeta + metav1.ObjectMeta + + Spec MutatingAdmissionPolicyBindingSpec +} + +type MutatingAdmissionPolicySpec struct { + ParamKind *v1beta1.ParamKind `json:"paramKind,omitempty" protobuf:"bytes,1,rep,name=paramKind"` + MatchConstraints *v1beta1.MatchResources `json:"matchConstraints,omitempty" protobuf:"bytes,2,rep,name=matchConstraints"` + Validations []v1beta1.Validation `json:"validations,omitempty" protobuf:"bytes,3,rep,name=validations"` + FailurePolicy *v1beta1.FailurePolicyType `json:"failurePolicy,omitempty" protobuf:"bytes,4,opt,name=failurePolicy,casttype=FailurePolicyType"` + AuditAnnotations []v1beta1.AuditAnnotation `json:"auditAnnotations,omitempty" protobuf:"bytes,5,rep,name=auditAnnotations"` + MatchConditions []v1beta1.MatchCondition `json:"matchConditions,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,6,rep,name=matchConditions"` + Variables []v1beta1.Variable `json:"variables,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,7,rep,name=variables"` +} + +type MutatingAdmissionPolicyBindingSpec struct { + PolicyName string `json:"policyName,omitempty" protobuf:"bytes,1,rep,name=policyName"` + ParamRef *v1beta1.ParamRef `json:"paramRef,omitempty" protobuf:"bytes,2,rep,name=paramRef"` + MatchResources *v1beta1.MatchResources `json:"matchResources,omitempty" protobuf:"bytes,3,rep,name=matchResources"` +} + +type MutatingAdmissionPolicyStatus struct { + ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,1,opt,name=observedGeneration"` + TypeChecking *v1beta1.TypeChecking `json:"typeChecking,omitempty" protobuf:"bytes,2,opt,name=typeChecking"` + Conditions []metav1.Condition `json:"conditions,omitempty" protobuf:"bytes,3,rep,name=conditions"` +} + +func (p *MutatingAdmissionPolicy) DeepCopyObject() runtime.Object { + panic("unimplemented") +} + +func (p *MutatingAdmissionPolicy) GetObjectKind() schema.ObjectKind { + return p +} + +func (p *MutatingAdmissionPolicyBinding) DeepCopyObject() runtime.Object { + panic("unimplemented") +} + +func (p *MutatingAdmissionPolicyBinding) GetObjectKind() schema.ObjectKind { + return p +}