Skip to content

Commit

Permalink
feature: add mutatingadmissionpolicy plugin pkg
Browse files Browse the repository at this point in the history
scaf
  • Loading branch information
alexzielenski committed Feb 16, 2024
1 parent bb3ab51 commit 0958983
Show file tree
Hide file tree
Showing 5 changed files with 338 additions and 0 deletions.
@@ -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
}
@@ -0,0 +1,4 @@
package mutating

type mutatingAdmissionPolicyEvaluator struct {
}
@@ -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
}
@@ -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)
}
@@ -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
}

0 comments on commit 0958983

Please sign in to comment.