From 3a1733f302d0fe9994bcc8e91fa2191c94606c2b Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Fri, 25 Oct 2024 13:25:46 -0400 Subject: [PATCH] Add MutatingAdmissionPolicy API This is closely aligned with ValidatingAdmissionPolicy except that instead of validations that can fail with messages, there are mutations, which can be defined either with as an ApplyConfiguration or JSONPatch. Co-authored-by: cici37 --- pkg/api/testing/defaulting_test.go | 4 + .../admissionregistration/fuzzer/fuzzer.go | 23 + pkg/apis/admissionregistration/register.go | 4 + pkg/apis/admissionregistration/types.go | 330 ++- .../v1alpha1/defaults.go | 8 + .../v1alpha1/defaults_test.go | 37 + .../validation/validation.go | 244 +- .../validation/validation_test.go | 1985 ++++++++++++++++- .../default_storage_factory_builder.go | 3 + pkg/printers/internalversion/printers.go | 76 + pkg/printers/internalversion/printers_test.go | 12 + .../mutatingadmissionpolicy/authz.go | 105 + .../mutatingadmissionpolicy/authz_test.go | 131 ++ .../mutatingadmissionpolicy/doc.go | 17 + .../storage/storage.go | 73 + .../storage/storage_test.go | 252 +++ .../mutatingadmissionpolicy/strategy.go | 122 + .../mutatingadmissionpolicy/strategy_test.go | 114 + .../mutatingadmissionpolicybinding/authz.go | 137 ++ .../authz_test.go | 233 ++ .../mutatingadmissionpolicybinding/doc.go | 17 + .../storage/storage.go | 94 + .../storage/storage_test.go | 260 +++ .../strategy.go | 130 ++ .../strategy_test.go | 127 ++ .../rest/storage_apiserver.go | 21 + .../v1alpha1/register.go | 4 + .../admissionregistration/v1alpha1/types.go | 343 +++ .../policy/mutating/patch/json_patch_test.go | 1 + .../plugin/webhook/predicates/rules/rules.go | 2 +- .../admissionwebhook/admission_test.go | 2 + .../apiserver/cel/admission_test_util.go | 2 + test/integration/controlplane/generic_test.go | 2 + test/integration/etcd/data.go | 8 + 34 files changed, 4911 insertions(+), 12 deletions(-) create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicy/authz.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicy/authz_test.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicy/doc.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicy/storage/storage.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicy/storage/storage_test.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicy/strategy.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicy/strategy_test.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicybinding/authz.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicybinding/authz_test.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicybinding/doc.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicybinding/storage/storage.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicybinding/storage/storage_test.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicybinding/strategy.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicybinding/strategy_test.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch_test.go diff --git a/pkg/api/testing/defaulting_test.go b/pkg/api/testing/defaulting_test.go index 75a5e0b1cfd..fa6ca54226a 100644 --- a/pkg/api/testing/defaulting_test.go +++ b/pkg/api/testing/defaulting_test.go @@ -143,6 +143,10 @@ func TestDefaulting(t *testing.T) { {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicyList"}: {}, {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicyBinding"}: {}, {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicyBindingList"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "MutatingAdmissionPolicy"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "MutatingAdmissionPolicyList"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "MutatingAdmissionPolicyBinding"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "MutatingAdmissionPolicyBindingList"}: {}, {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingWebhookConfiguration"}: {}, {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingWebhookConfigurationList"}: {}, {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "MutatingWebhookConfiguration"}: {}, diff --git a/pkg/apis/admissionregistration/fuzzer/fuzzer.go b/pkg/apis/admissionregistration/fuzzer/fuzzer.go index ca675e9b19b..45fddb02ecf 100644 --- a/pkg/apis/admissionregistration/fuzzer/fuzzer.go +++ b/pkg/apis/admissionregistration/fuzzer/fuzzer.go @@ -107,5 +107,28 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} { obj.ParameterNotFoundAction = &v } }, + func(obj *admissionregistration.MutatingAdmissionPolicySpec, c fuzz.Continue) { + c.FuzzNoCustom(obj) // fuzz self without calling this function again + if obj.FailurePolicy == nil { + p := admissionregistration.FailurePolicyType("Fail") + obj.FailurePolicy = &p + } + obj.ReinvocationPolicy = admissionregistration.NeverReinvocationPolicy + }, + func(obj *admissionregistration.Mutation, c fuzz.Continue) { + c.FuzzNoCustom(obj) // fuzz self without calling this function again + patchTypes := []admissionregistration.PatchType{admissionregistration.PatchTypeJSONPatch, admissionregistration.PatchTypeApplyConfiguration} + obj.PatchType = patchTypes[c.Rand.Intn(len(patchTypes))] + if obj.PatchType == admissionregistration.PatchTypeJSONPatch { + obj.JSONPatch = &admissionregistration.JSONPatch{} + c.Fuzz(&obj.JSONPatch) + obj.ApplyConfiguration = nil + } + if obj.PatchType == admissionregistration.PatchTypeApplyConfiguration { + obj.ApplyConfiguration = &admissionregistration.ApplyConfiguration{} + c.Fuzz(obj.ApplyConfiguration) + obj.JSONPatch = nil + } + }, } } diff --git a/pkg/apis/admissionregistration/register.go b/pkg/apis/admissionregistration/register.go index a69343e20b4..c3cf074e4e9 100644 --- a/pkg/apis/admissionregistration/register.go +++ b/pkg/apis/admissionregistration/register.go @@ -55,6 +55,10 @@ func addKnownTypes(scheme *runtime.Scheme) error { &ValidatingAdmissionPolicyList{}, &ValidatingAdmissionPolicyBinding{}, &ValidatingAdmissionPolicyBindingList{}, + &MutatingAdmissionPolicy{}, + &MutatingAdmissionPolicyList{}, + &MutatingAdmissionPolicyBinding{}, + &MutatingAdmissionPolicyBindingList{}, ) return nil } diff --git a/pkg/apis/admissionregistration/types.go b/pkg/apis/admissionregistration/types.go index 50d000484f7..8c4d80221d9 100644 --- a/pkg/apis/admissionregistration/types.go +++ b/pkg/apis/admissionregistration/types.go @@ -206,7 +206,7 @@ type ValidatingAdmissionPolicySpec struct { ParamKind *ParamKind // MatchConstraints specifies what resources this policy is designed to validate. - // The AdmissionPolicy cares about a request if it matches _all_ Constraint. + // The MutatingAdmissionPolicy cares about a request if it matches _all_ Constraint. // However, in order to prevent clusters from being put into an unstable state that cannot be recovered from via the API // ValidatingAdmissionPolicy cannot match ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding. // Required. @@ -267,6 +267,7 @@ type ValidatingAdmissionPolicySpec struct { // // The expression of a variable can refer to other variables defined earlier in the list but not those after. // Thus, Variables must be sorted by the order of first appearance and acyclic. + // +listType=atomic // +optional Variables []Variable } @@ -1163,3 +1164,330 @@ type MatchCondition struct { // Required. Expression string } + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.32 + +// MutatingAdmissionPolicy describes an admission policy that may mutate an object. +type MutatingAdmissionPolicy struct { + metav1.TypeMeta + // Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata. + // +optional + metav1.ObjectMeta + // Specification of the desired behavior of the MutatingAdmissionPolicy. + Spec MutatingAdmissionPolicySpec +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.32 + +// MutatingAdmissionPolicyList is a list of MutatingAdmissionPolicy. +type MutatingAdmissionPolicyList struct { + metav1.TypeMeta + // Standard list metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + // +optional + metav1.ListMeta + // List of ValidatingAdmissionPolicy. + Items []MutatingAdmissionPolicy +} + +// MutatingAdmissionPolicySpec is the specification of the desired behavior of the admission policy. +type MutatingAdmissionPolicySpec struct { + // paramKind specifies the kind of resources used to parameterize this policy. + // If absent, there are no parameters for this policy and the param CEL variable will not be provided to validation expressions. + // If paramKind refers to a non-existent kind, this policy definition is mis-configured and the FailurePolicy is applied. + // If paramKind is specified but paramRef is unset in MutatingAdmissionPolicyBinding, the params variable will be null. + // +optional + ParamKind *ParamKind + + // matchConstraints specifies what resources this policy is designed to validate. + // The AdmissionPolicy cares about a request if it matches _all_ Constraints. + // However, in order to prevent clusters from being put into an unstable state that cannot be recovered from via the API + // MutatingAdmissionPolicy cannot match MutatingAdmissionPolicy and MutatingAdmissionPolicyBinding. + // Only the CREATE, UPDATE, and CONNECT operations are allowed. + // '*' matches only CREATE, UPDATE, and CONNECT. + // Required. + MatchConstraints *MatchResources + + // variables contain definitions of variables that can be used in composition of other expressions. + // Each variable is defined as a named CEL expression. + // The variables defined here will be available under `variables` in other expressions of the policy + // except matchConditions because matchConditions are evaluated before the rest of the policy. + // + // The expression of a variable can refer to other variables defined earlier in the list but not those after. + // Thus, variables must be sorted by the order of first appearance and acyclic. + // +listType=atomic + // +optional + Variables []Variable + + // mutations contain operations to perform on matching objects. + // mutations may not be empty; a minimum of one mutation is required. + // mutations are evaluated in order, and are reinvoked according to + // the reinvocationPolicy. + // The mutations of a policy are invoked for each binding of this policy + // and reinvocation of mutations occurs on a per binding basis. + // + // +listType=atomic + // +optional + Mutations []Mutation + + // failurePolicy defines how to handle failures for the admission policy. Failures can + // occur from CEL expression parse errors, type check errors, runtime errors and invalid + // or mis-configured policy definitions or bindings. + // + // A policy is invalid if paramKind refers to a non-existent Kind. + // A binding is invalid if paramRef.name refers to a non-existent resource. + // + // failurePolicy does not define how validations that evaluate to false are handled. + // + // Allowed values are Ignore or Fail. Defaults to Fail. + // +optional + FailurePolicy *FailurePolicyType + + // matchConditions is a list of conditions that must be met for a request to be validated. + // Match conditions filter requests that have already been matched by the matchConstraints, + // An empty list of matchConditions matches all requests. + // There are a maximum of 64 match conditions allowed. + // + // If a parameter object is provided, it can be accessed via the `params` handle in the same + // manner as validation expressions. + // + // The exact matching logic is (in order): + // 1. If ANY matchCondition evaluates to FALSE, the policy is skipped. + // 2. If ALL matchConditions evaluate to TRUE, the policy is evaluated. + // 3. If any matchCondition evaluates to an error (but none are FALSE): + // - If failurePolicy=Fail, reject the request + // - If failurePolicy=Ignore, the policy is skipped + // + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +optional + MatchConditions []MatchCondition + + // reinvocationPolicy indicates whether mutations may be called multiple times per MutatingAdmissionPolicyBinding + // as part of a single admission evaluation. + // Allowed values are "Never" and "IfNeeded". + // + // Never: These mutations will not be called more than once per binding in a single admission evaluation. + // + // IfNeeded: These mutations may be invoked more than once per binding for a single admission request and there is no guarantee of + // order with respect to other admission plugins, admission webhooks, bindings of this policy and admission policies. Mutations are only + // reinvoked when mutations change the object after this mutation is invoked. + // Required. + ReinvocationPolicy ReinvocationPolicyType +} + +// Mutation specifies the operation that performs a Mutation. +type Mutation struct { + // patchType indicates the patch strategy used. + // Allowed values are "ApplyConfiguration" and "JSONPatch". + // Required. + // + // +unionDiscriminator + PatchType PatchType + + // applyConfiguration defines the desired configuration values of an object. + // The configuration is applied to the admission object using + // [structured merge diff](https://github.com/kubernetes-sigs/structured-merge-diff). + // A CEL expression is used to create apply configuration. + ApplyConfiguration *ApplyConfiguration + + // jsonPatch defines a [JSON patch](https://jsonpatch.com/) to perform a mutation to the object. + // A CEL expression is used to create the JSON patch. + JSONPatch *JSONPatch +} + +// PatchType specifies the type of patch operation for a mutation. +// +enum +type PatchType string + +const ( + // ApplyConfiguration indicates that the mutation is using apply configuration to mutate the object. + PatchTypeApplyConfiguration PatchType = "ApplyConfiguration" + // JSONPatch indicates that the object is mutated through JSON Patch. + PatchTypeJSONPatch PatchType = "JSONPatch" +) + +// ApplyConfiguration defines the desired configuration values of an object. +type ApplyConfiguration struct { + // expression will be evaluated by CEL to create an apply configuration. + // ref: https://github.com/google/cel-spec + // + // Apply configurations are declared in CEL using object initialization. For example, this CEL expression + // returns an apply configuration to set a single field: + // + // Object{ + // spec: Object.spec{ + // serviceAccountName: "example" + // } + // } + // + // Apply configurations may not modify atomic structs, maps or arrays due to the risk of accidental deletion of + // values not included in the apply configuration. + // + // CEL expressions have access to the object types needed to create apply configurations: + // - 'Object' - CEL type of the resource object. + // - 'Object.' - CEL type of object field (such as 'Object.spec') + // - 'Object.....` - CEL type of nested field (such as 'Object.spec.containers') + // + // CEL expressions have access to the contents of the API request, organized into CEL variables as well as some other useful variables: + // + // - 'object' - The object from the incoming request. The value is null for DELETE requests. + // - 'oldObject' - The existing object. The value is null for CREATE requests. + // - 'request' - Attributes of the API request([ref](/pkg/apis/admission/types.go#AdmissionRequest)). + // - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind. + // - 'namespaceObject' - The namespace object that the incoming object belongs to. The value is null for cluster-scoped resources. + // - 'variables' - Map of composited variables, from its name to its lazily evaluated value. + // For example, a variable named 'foo' can be accessed as 'variables.foo'. + // - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request. + // See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz + // - 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the + // request resource. + // + // The `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the + // object. No other metadata properties are accessible. + // + // Only property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. + // Required. + Expression string +} + +// JSONPatch defines a JSON Patch. +type JSONPatch struct { + // expression will be evaluated by CEL to create a [JSON patch](https://jsonpatch.com/). + // ref: https://github.com/google/cel-spec + // + // expression must return an array of JSONPatch values. + // + // For example, this CEL expression returns a JSON patch to conditionally modify a value: + // + // [ + // JSONPatch{op: "test", path: "/spec/example", value: "Red"}, + // JSONPatch{op: "replace", path: "/spec/example", value: "Green"} + // ] + // + // To define an object for the patch value, use Object types. For example: + // + // [ + // JSONPatch{ + // op: "add", + // path: "/spec/selector", + // value: Object.spec.selector{matchLabels: {"environment": "test"}} + // } + // ] + // + // To use strings containing '/' and '~' as JSONPatch path keys, use "jsonpatch.escapeKey". For example: + // + // [ + // JSONPatch{ + // op: "add", + // path: "/metadata/labels/" + jsonpatch.escapeKey("example.com/environment"), + // value: "test" + // }, + // ] + // + // CEL expressions have access to the types needed to create JSON patches and objects: + // + // - 'JSONPatch' - CEL type of JSON Patch operations. JSONPatch has the fields 'op', 'from', 'path' and 'value'. + // See [JSON patch](https://jsonpatch.com/) for more details. The 'value' field may be set to any of: string, + // integer, array, map or object. If set, the 'path' and 'from' fields must be set to a + // [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901/) string, where the 'jsonpatch.escapeKey()' CEL + // function may be used to escape path keys containing '/' and '~'. + // - 'Object' - CEL type of the resource object. + // - 'Object.' - CEL type of object field (such as 'Object.spec') + // - 'Object.....` - CEL type of nested field (such as 'Object.spec.containers') + // + // CEL expressions have access to the contents of the API request, organized into CEL variables as well as some other useful variables: + // + // - 'object' - The object from the incoming request. The value is null for DELETE requests. + // - 'oldObject' - The existing object. The value is null for CREATE requests. + // - 'request' - Attributes of the API request([ref](/pkg/apis/admission/types.go#AdmissionRequest)). + // - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind. + // - 'namespaceObject' - The namespace object that the incoming object belongs to. The value is null for cluster-scoped resources. + // - 'variables' - Map of composited variables, from its name to its lazily evaluated value. + // For example, a variable named 'foo' can be accessed as 'variables.foo'. + // - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request. + // See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz + // - 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the + // request resource. + // + // CEL expressions have access to [Kubernetes CEL function libraries](https://kubernetes.io/docs/reference/using-api/cel/#cel-options-language-features-and-libraries) + // as well as: + // + // - 'jsonpatch.escapeKey' - Performs JSONPatch key escaping. '~' and '/' are escaped as '~0' and `~1' respectively). + // + // + // Only property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. + // Required. + Expression string +} + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.32 + +// MutatingAdmissionPolicyBinding binds the MutatingAdmissionPolicy with parametrized resources. +// MutatingAdmissionPolicyBinding and the optional parameter resource together define how cluster administrators +// configure policies for clusters. +// +// For a given admission request, each binding will cause its policy to be +// evaluated N times, where N is 1 for policies/bindings that don't use +// params, otherwise N is the number of parameters selected by the binding. +// Each evaluation is constrained by a [runtime cost budget](https://kubernetes.io/docs/reference/using-api/cel/#runtime-cost-budget). +// +// Adding/removing policies, bindings, or params can not affect whether a +// given (policy, binding, param) combination is within its own CEL budget. +type MutatingAdmissionPolicyBinding struct { + metav1.TypeMeta + // Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata. + // +optional + metav1.ObjectMeta + // Specification of the desired behavior of the MutatingAdmissionPolicyBinding. + Spec MutatingAdmissionPolicyBindingSpec +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.32 + +// MutatingAdmissionPolicyBindingList is a list of MutatingAdmissionPolicyBinding. +type MutatingAdmissionPolicyBindingList struct { + metav1.TypeMeta + // Standard list metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + // +optional + metav1.ListMeta + // List of PolicyBinding. + Items []MutatingAdmissionPolicyBinding +} + +// MutatingAdmissionPolicyBindingSpec is the specification of the MutatingAdmissionPolicyBinding. +type MutatingAdmissionPolicyBindingSpec struct { + // policyName references a MutatingAdmissionPolicy name which the MutatingAdmissionPolicyBinding binds to. + // If the referenced resource does not exist, this binding is considered invalid and will be ignored + // Required. + PolicyName string + // paramRef specifies the parameter resource used to configure the admission control policy. + // It should point to a resource of the type specified in spec.ParamKind of the bound MutatingAdmissionPolicy. + // If the policy specifies a ParamKind and the resource referred to by ParamRef does not exist, this binding is considered mis-configured and the FailurePolicy of the MutatingAdmissionPolicy applied. + // If the policy does not specify a ParamKind then this field is ignored, and the rules are evaluated without a param. + // +optional + ParamRef *ParamRef + + // matchResources limits what resources match this binding and may be mutated by it. + // Note that if matchResources matches a resource, the resource must also match a policy's matchConstraints and + // matchConditions before the resource may be mutated. + // When matchResources is unset, it does not constrain resource matching, and only the policy's matchConstraints + // and matchConditions must match for the resource to be mutated. + // Additionally, matchResources.resourceRules are optional and do not constraint matching when unset. + // Note that this is differs from MutatingAdmissionPolicy matchConstraints, where resourceRules are required. + // The CREATE, UPDATE and CONNECT operations are allowed. The DELETE operation may not be matched. + // '*' matches CREATE, UPDATE and CONNECT. + // +optional + MatchResources *MatchResources +} diff --git a/pkg/apis/admissionregistration/v1alpha1/defaults.go b/pkg/apis/admissionregistration/v1alpha1/defaults.go index 1abb61b2c0c..4461d33f440 100644 --- a/pkg/apis/admissionregistration/v1alpha1/defaults.go +++ b/pkg/apis/admissionregistration/v1alpha1/defaults.go @@ -57,3 +57,11 @@ func SetDefaults_ParamRef(obj *admissionregistrationv1alpha1.ParamRef) { obj.ParameterNotFoundAction = &v } } + +// SetDefaults_MutatingAdmissionPolicySpec sets defaults for MutatingAdmissionPolicySpec +func SetDefaults_MutatingAdmissionPolicySpec(obj *admissionregistrationv1alpha1.MutatingAdmissionPolicySpec) { + if obj.FailurePolicy == nil { + policy := admissionregistrationv1alpha1.Fail + obj.FailurePolicy = &policy + } +} diff --git a/pkg/apis/admissionregistration/v1alpha1/defaults_test.go b/pkg/apis/admissionregistration/v1alpha1/defaults_test.go index 3c09d708d5f..7fbbb23201e 100644 --- a/pkg/apis/admissionregistration/v1alpha1/defaults_test.go +++ b/pkg/apis/admissionregistration/v1alpha1/defaults_test.go @@ -31,6 +31,7 @@ import ( func TestDefaultAdmissionPolicy(t *testing.T) { fail := v1alpha1.Fail + never := v1alpha1.NeverReinvocationPolicy equivalent := v1alpha1.Equivalent allScopes := v1alpha1.AllScopes @@ -103,6 +104,42 @@ func TestDefaultAdmissionPolicy(t *testing.T) { }, }, }, + { + name: "MutatingAdmissionPolicy", + original: &v1alpha1.MutatingAdmissionPolicy{ + Spec: v1alpha1.MutatingAdmissionPolicySpec{ + MatchConstraints: &v1alpha1.MatchResources{}, + ReinvocationPolicy: never, + Mutations: []v1alpha1.Mutation{ + { + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: "fake string", + }, + }, + }, + }, + }, + expected: &v1alpha1.MutatingAdmissionPolicy{ + Spec: v1alpha1.MutatingAdmissionPolicySpec{ + MatchConstraints: &v1alpha1.MatchResources{ + MatchPolicy: &equivalent, + NamespaceSelector: &metav1.LabelSelector{}, + ObjectSelector: &metav1.LabelSelector{}, + }, + FailurePolicy: &fail, + ReinvocationPolicy: never, + Mutations: []v1alpha1.Mutation{ + { + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: "fake string", + }, + }, + }, + }, + }, + }, } for _, test := range tests { diff --git a/pkg/apis/admissionregistration/validation/validation.go b/pkg/apis/admissionregistration/validation/validation.go index 53e2256f411..0fbf8833252 100644 --- a/pkg/apis/admissionregistration/validation/validation.go +++ b/pkg/apis/admissionregistration/validation/validation.go @@ -23,6 +23,7 @@ import ( "strings" "sync" + "k8s.io/apimachinery/pkg/api/equality" genericvalidation "k8s.io/apimachinery/pkg/api/validation" "k8s.io/apimachinery/pkg/api/validation/path" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -32,6 +33,7 @@ import ( utilvalidation "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" plugincel "k8s.io/apiserver/pkg/admission/plugin/cel" + "k8s.io/apiserver/pkg/admission/plugin/policy/mutating" validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating" "k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions" "k8s.io/apiserver/pkg/cel" @@ -274,6 +276,8 @@ type preexistingExpressions struct { validationExpressions sets.Set[string] validationMessageExpressions sets.Set[string] auditAnnotationValuesExpressions sets.Set[string] + applyConfigurationExpressions sets.Set[string] + jsonPatchExpressions sets.Set[string] } func newPreexistingExpressions() preexistingExpressions { @@ -282,6 +286,8 @@ func newPreexistingExpressions() preexistingExpressions { validationExpressions: sets.New[string](), validationMessageExpressions: sets.New[string](), auditAnnotationValuesExpressions: sets.New[string](), + applyConfigurationExpressions: sets.New[string](), + jsonPatchExpressions: sets.New[string](), } } @@ -322,6 +328,22 @@ func findValidatingPolicyPreexistingExpressions(validatingPolicy *admissionregis return preexisting } +func findMutatingPolicyPreexistingExpressions(mutatingPolicy *admissionregistration.MutatingAdmissionPolicy) preexistingExpressions { + preexisting := newPreexistingExpressions() + for _, mc := range mutatingPolicy.Spec.MatchConditions { + preexisting.matchConditionExpressions.Insert(mc.Expression) + } + for _, v := range mutatingPolicy.Spec.Mutations { + if v.ApplyConfiguration != nil { + preexisting.applyConfigurationExpressions.Insert(v.ApplyConfiguration.Expression) + } + if v.JSONPatch != nil { + preexisting.jsonPatchExpressions.Insert(v.JSONPatch.Expression) + } + } + return preexisting +} + func validateMutatingWebhookConfiguration(e *admissionregistration.MutatingWebhookConfiguration, opts validationOptions) field.ErrorList { allErrors := genericvalidation.ValidateObjectMeta(&e.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata")) @@ -495,6 +517,19 @@ var supportedValidationPolicyReason = sets.NewString( string(metav1.StatusReasonRequestEntityTooLarge), ) +var supportedPatchType = sets.NewString( + string(admissionregistration.PatchTypeApplyConfiguration), + string(admissionregistration.PatchTypeJSONPatch), +) + +// MutatatingAdmissionPolicy does not support DELETE +var supportedMutatingOperations = sets.NewString( + string(admissionregistration.OperationAll), + string(admissionregistration.Create), + string(admissionregistration.Update), + string(admissionregistration.Connect), +) + func hasWildcardOperation(operations []admissionregistration.OperationType) bool { for _, o := range operations { if o == admissionregistration.OperationAll { @@ -588,10 +623,21 @@ func ignoreValidatingWebhookMatchConditions(new, old []admissionregistration.Val // ignoreValidatingAdmissionPolicyMatchConditions returns true if there have been no updates that could invalidate previously-valid match conditions func ignoreValidatingAdmissionPolicyMatchConditions(new, old *admissionregistration.ValidatingAdmissionPolicy) bool { - if !reflect.DeepEqual(new.Spec.ParamKind, old.Spec.ParamKind) { + if !equality.Semantic.DeepEqual(new.Spec.ParamKind, old.Spec.ParamKind) { return false } - if !reflect.DeepEqual(new.Spec.MatchConditions, old.Spec.MatchConditions) { + if !equality.Semantic.DeepEqual(new.Spec.MatchConditions, old.Spec.MatchConditions) { + return false + } + return true +} + +// ignoreMutatingAdmissionPolicyMatchConditions returns true if there have been no updates that could invalidate previously-valid match conditions +func ignoreMutatingAdmissionPolicyMatchConditions(new, old *admissionregistration.MutatingAdmissionPolicy) bool { + if !equality.Semantic.DeepEqual(new.Spec.ParamKind, old.Spec.ParamKind) { + return false + } + if !equality.Semantic.DeepEqual(new.Spec.MatchConditions, old.Spec.MatchConditions) { return false } return true @@ -1158,7 +1204,7 @@ func validateValidatingAdmissionPolicyBindingSpec(spec *admissionregistration.Va } } allErrors = append(allErrors, validateParamRef(spec.ParamRef, fldPath.Child("paramRef"))...) - allErrors = append(allErrors, validateMatchResources(spec.MatchResources, fldPath.Child("matchResouces"))...) + allErrors = append(allErrors, validateMatchResources(spec.MatchResources, fldPath.Child("matchResources"))...) allErrors = append(allErrors, validateValidationActions(spec.ValidationActions, fldPath.Child("validationActions"))...) return allErrors @@ -1328,3 +1374,195 @@ func isCELIdentifier(name string) bool { // | "var" | "void" | "while" return celIdentRegex.MatchString(name) && !celReserved.Has(name) } + +// ValidateMutatingAdmissionPolicyUpdate validates update of mutating admission policy +func ValidateMutatingAdmissionPolicyUpdate(newC, oldC *admissionregistration.MutatingAdmissionPolicy) field.ErrorList { + return validateMutatingAdmissionPolicy(newC, validationOptions{ + ignoreMatchConditions: ignoreMutatingAdmissionPolicyMatchConditions(newC, oldC), + preexistingExpressions: findMutatingPolicyPreexistingExpressions(oldC), + strictCostEnforcement: true, + }) +} + +// ValidateMutatingAdmissionPolicyBindingUpdate validates update of mutating admission policy +func ValidateMutatingAdmissionPolicyBindingUpdate(newC, oldC *admissionregistration.MutatingAdmissionPolicyBinding) field.ErrorList { + return validateMutatingAdmissionPolicyBinding(newC) +} + +// ValidateMutatingAdmissionPolicy validates a MutatingAdmissionPolicy before creation. +func ValidateMutatingAdmissionPolicy(p *admissionregistration.MutatingAdmissionPolicy) field.ErrorList { + return validateMutatingAdmissionPolicy(p, validationOptions{ignoreMatchConditions: false, strictCostEnforcement: true}) +} + +func validateMutatingAdmissionPolicy(p *admissionregistration.MutatingAdmissionPolicy, opts validationOptions) field.ErrorList { + allErrors := genericvalidation.ValidateObjectMeta(&p.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata")) + allErrors = append(allErrors, validateMutatingAdmissionPolicySpec(p.ObjectMeta, &p.Spec, opts, field.NewPath("spec"))...) + return allErrors +} + +func validateMutatingAdmissionPolicySpec(meta metav1.ObjectMeta, spec *admissionregistration.MutatingAdmissionPolicySpec, opts validationOptions, fldPath *field.Path) field.ErrorList { + var allErrors field.ErrorList + + compiler := createCompiler(true, true) + + if spec.FailurePolicy == nil { + allErrors = append(allErrors, field.Required(fldPath.Child("failurePolicy"), "")) + } else if !supportedFailurePolicies.Has(string(*spec.FailurePolicy)) { + allErrors = append(allErrors, field.NotSupported(fldPath.Child("failurePolicy"), *spec.FailurePolicy, supportedFailurePolicies.List())) + } + if spec.ParamKind != nil { + opts.allowParamsInMatchConditions = true + allErrors = append(allErrors, validateParamKind(*spec.ParamKind, fldPath.Child("paramKind"))...) + } + if spec.MatchConstraints == nil { + allErrors = append(allErrors, field.Required(fldPath.Child("matchConstraints"), "")) + } else { + allErrors = append(allErrors, validateMatchResources(spec.MatchConstraints, fldPath.Child("matchConstraints"))...) + // at least one resourceRule must be defined to provide type information + if len(spec.MatchConstraints.ResourceRules) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("matchConstraints", "resourceRules"), "")) + } + + // It is only possible to mutate create and update requests + for _, rule := range spec.MatchConstraints.ResourceRules { + for _, op := range rule.RuleWithOperations.Operations { + if !supportedMutatingOperations.Has(string(op)) { + allErrors = append(allErrors, field.NotSupported(fldPath.Child("matchConstraints", "resourceRules", "operations"), op, supportedMutatingOperations.List())) + } + } + } + } + if !opts.ignoreMatchConditions { + allErrors = append(allErrors, validateMatchConditions(spec.MatchConditions, opts, fldPath.Child("matchConditions"))...) + } + for i, variable := range spec.Variables { + allErrors = append(allErrors, validateVariable(compiler, &variable, spec.ParamKind, opts, fldPath.Child("variables").Index(i))...) + } + if len(spec.Mutations) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("mutations"), "mutations must contain at least one item")) + } else { + for i, mutation := range spec.Mutations { + allErrors = append(allErrors, validateMutation(compiler, &mutation, spec.ParamKind, opts, fldPath.Child("mutations").Index(i))...) + } + } + if len(spec.ReinvocationPolicy) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("reinvocationPolicy"), "")) + } else if !supportedReinvocationPolicies.Has(string(spec.ReinvocationPolicy)) { + allErrors = append(allErrors, field.NotSupported(fldPath.Child("reinvocationPolicy"), spec.ReinvocationPolicy, supportedReinvocationPolicies.List())) + } + return allErrors +} + +func validateMutation(compiler plugincel.Compiler, m *admissionregistration.Mutation, paramKind *admissionregistration.ParamKind, opts validationOptions, fldPath *field.Path) (allErrors field.ErrorList) { + if len(m.PatchType) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("patchType"), "")) + } else { + switch m.PatchType { + case admissionregistration.PatchTypeJSONPatch: + if m.JSONPatch == nil { + allErrors = append(allErrors, field.Required(fldPath.Child("jsonPatch"), "must be specified when patchType is JSONPatch")) + } else { + allErrors = append(allErrors, validateJSONPatch(compiler, m.JSONPatch, paramKind, opts, fldPath.Child("jsonPatch"))...) + } + if m.ApplyConfiguration != nil { + allErrors = append(allErrors, field.Invalid(fldPath.Child("applyConfiguration"), "{applyConfiguration}", "must not be specified when patchType is JSONPatch")) + } + case admissionregistration.PatchTypeApplyConfiguration: + if m.ApplyConfiguration == nil { + allErrors = append(allErrors, field.Required(fldPath.Child("applyConfiguration"), "must be specified when patchType is ApplyConfiguration")) + } else { + allErrors = append(allErrors, validateApplyConfiguration(compiler, m.ApplyConfiguration, paramKind, opts, fldPath.Child("applyConfiguration"))...) + } + if m.JSONPatch != nil { + allErrors = append(allErrors, field.Invalid(fldPath.Child("jsonPatch"), "{jsonPatch}", "must not be specified when patchType is ApplyConfiguration")) + } + default: + allErrors = append(allErrors, field.NotSupported(fldPath.Child("patchType"), m.PatchType, supportedPatchType.List())) + } + } + return allErrors +} + +func validateApplyConfiguration(compiler plugincel.Compiler, applyConfig *admissionregistration.ApplyConfiguration, paramKind *admissionregistration.ParamKind, opts validationOptions, fldPath *field.Path) (allErrors field.ErrorList) { + trimmedExpression := strings.TrimSpace(applyConfig.Expression) + if len(trimmedExpression) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("expression"), "")) + } else { + + envType := environment.NewExpressions + if opts.preexistingExpressions.applyConfigurationExpressions.Has(applyConfig.Expression) { + envType = environment.StoredExpressions + } + accessor := &mutating.ApplyConfigurationCondition{ + Expression: trimmedExpression, + } + opts := plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true, StrictCost: true, HasPatchTypes: true} + result := compiler.CompileCELExpression(accessor, opts, envType) + + if result.Error != nil { + allErrors = append(allErrors, convertCELErrorToValidationError(fldPath.Child("expression"), accessor, result.Error)) + } + } + return allErrors +} + +func validateJSONPatch(compiler plugincel.Compiler, jsonPatch *admissionregistration.JSONPatch, paramKind *admissionregistration.ParamKind, opts validationOptions, fldPath *field.Path) (allErrors field.ErrorList) { + trimmedExpression := strings.TrimSpace(jsonPatch.Expression) + if len(trimmedExpression) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("expression"), "")) + } else { + + envType := environment.NewExpressions + if opts.preexistingExpressions.applyConfigurationExpressions.Has(jsonPatch.Expression) { + envType = environment.StoredExpressions + } + accessor := &mutating.JSONPatchCondition{ + Expression: trimmedExpression, + } + opts := plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true, StrictCost: true, HasPatchTypes: true} + result := compiler.CompileCELExpression(accessor, opts, envType) + + if result.Error != nil { + allErrors = append(allErrors, convertCELErrorToValidationError(fldPath.Child("expression"), accessor, result.Error)) + } + } + return allErrors +} + +// ValidateMutatingAdmissionPolicyBinding validates a MutatingAdmissionPolicyBinding before create. +func ValidateMutatingAdmissionPolicyBinding(pb *admissionregistration.MutatingAdmissionPolicyBinding) field.ErrorList { + return validateMutatingAdmissionPolicyBinding(pb) +} + +func validateMutatingAdmissionPolicyBinding(pb *admissionregistration.MutatingAdmissionPolicyBinding) field.ErrorList { + allErrors := genericvalidation.ValidateObjectMeta(&pb.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata")) + allErrors = append(allErrors, validateMutatingAdmissionPolicyBindingSpec(&pb.Spec, field.NewPath("spec"))...) + + return allErrors +} + +func validateMutatingAdmissionPolicyBindingSpec(spec *admissionregistration.MutatingAdmissionPolicyBindingSpec, fldPath *field.Path) field.ErrorList { + var allErrors field.ErrorList + + if len(spec.PolicyName) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("policyName"), "")) + } else { + for _, msg := range genericvalidation.NameIsDNSSubdomain(spec.PolicyName, false) { + allErrors = append(allErrors, field.Invalid(fldPath.Child("policyName"), spec.PolicyName, msg)) + } + } + allErrors = append(allErrors, validateParamRef(spec.ParamRef, fldPath.Child("paramRef"))...) + allErrors = append(allErrors, validateMatchResources(spec.MatchResources, fldPath.Child("matchResources"))...) + if spec.MatchResources != nil { + // It is only possible to mutate create and update requests + for _, rule := range spec.MatchResources.ResourceRules { + for _, op := range rule.RuleWithOperations.Operations { + if !supportedMutatingOperations.Has(string(op)) { + allErrors = append(allErrors, field.NotSupported(fldPath.Child("matchResources", "resourceRules", "operations"), op, supportedMutatingOperations.List())) + } + } + } + } + + return allErrors +} diff --git a/pkg/apis/admissionregistration/validation/validation_test.go b/pkg/apis/admissionregistration/validation/validation_test.go index bc6c7331306..85dc825583a 100644 --- a/pkg/apis/admissionregistration/validation/validation_test.go +++ b/pkg/apis/admissionregistration/validation/validation_test.go @@ -2368,7 +2368,7 @@ func TestValidateValidatingAdmissionPolicy(t *testing.T) { }, expectedError: `Unsupported value: ""`, }, { - name: "operation must be either create/update/delete/connect", + name: "operation must be either create/update", config: &admissionregistration.ValidatingAdmissionPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "config", @@ -3209,7 +3209,7 @@ func TestValidateValidatingAdmissionPolicyUpdate(t *testing.T) { MatchConstraints: &admissionregistration.MatchResources{ ResourceRules: []admissionregistration.NamedRuleWithOperations{{ RuleWithOperations: admissionregistration.RuleWithOperations{ - Operations: []admissionregistration.OperationType{"*"}, + Operations: []admissionregistration.OperationType{"CREATE", "UPDATE"}, Rule: admissionregistration.Rule{ APIGroups: []string{"a"}, APIVersions: []string{"a"}, @@ -3495,6 +3495,48 @@ func validatingAdmissionPolicyWithExpressions( } } +func mutatingAdmissionPolicyWithExpressions( + matchConditions []admissionregistration.MatchCondition, + mutations []admissionregistration.Mutation) *admissionregistration.MutatingAdmissionPolicy { + return &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"*"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + }, + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Ignore") + return &r + }(), + MatchConditions: matchConditions, + Mutations: mutations, + }, + } +} + func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) { tests := []struct { name string @@ -3537,7 +3579,7 @@ func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) { }, }, }, - expectedError: `spec.matchResouces.matchPolicy: Unsupported value: "other": supported values: "Equivalent", "Exact"`, + expectedError: `spec.matchResources.matchPolicy: Unsupported value: "other": supported values: "Equivalent", "Exact"`, }, { name: "Operations must not be empty or nil", config: &admissionregistration.ValidatingAdmissionPolicyBinding{ @@ -3592,7 +3634,7 @@ func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) { }, }, }, - expectedError: `spec.matchResouces.resourceRules[0].operations: Required value, spec.matchResouces.resourceRules[1].operations: Required value, spec.matchResouces.excludeResourceRules[0].operations: Required value, spec.matchResouces.excludeResourceRules[1].operations: Required value`, + expectedError: `spec.matchResources.resourceRules[0].operations: Required value, spec.matchResources.resourceRules[1].operations: Required value, spec.matchResources.excludeResourceRules[0].operations: Required value, spec.matchResources.excludeResourceRules[1].operations: Required value`, }, { name: "\"\" is NOT a valid operation", config: &admissionregistration.ValidatingAdmissionPolicyBinding{ @@ -3765,7 +3807,7 @@ func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) { }, }, }, - expectedError: `spec.matchResouces.resourceRules[0].resources[1]: Invalid value: "a/x": if 'a/*' is present, must not specify a/x`, + expectedError: `spec.matchResources.resourceRules[0].resources[1]: Invalid value: "a/x": if 'a/*' is present, must not specify a/x`, }, { name: "resource a/* can mix with a", config: &admissionregistration.ValidatingAdmissionPolicyBinding{ @@ -3830,7 +3872,7 @@ func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) { }, }, }, - expectedError: `spec.matchResouces.resourceRules[0].resources[1]: Invalid value: "x/a": if '*/a' is present, must not specify x/a`, + expectedError: `spec.matchResources.resourceRules[0].resources[1]: Invalid value: "x/a": if '*/a' is present, must not specify x/a`, }, { name: "resource */* cannot mix with other resources", config: &admissionregistration.ValidatingAdmissionPolicyBinding{ @@ -3858,7 +3900,7 @@ func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) { }, }, }, - expectedError: `spec.matchResouces.resourceRules[0].resources: Invalid value: []string{"*/*", "a"}: if '*/*' is present, must not specify other resources`, + expectedError: `spec.matchResources.resourceRules[0].resources: Invalid value: []string{"*/*", "a"}: if '*/*' is present, must not specify other resources`, }, { name: "validationActions must be unique", config: &admissionregistration.ValidatingAdmissionPolicyBinding{ @@ -4194,3 +4236,1932 @@ func get65MatchConditions() []admissionregistration.MatchCondition { } return result } + +func TestValidateMutatingAdmissionPolicy(t *testing.T) { + applyConfigurationPatchType := admissionregistration.PatchTypeApplyConfiguration + tests := []struct { + name string + config *admissionregistration.MutatingAdmissionPolicy + expectedError string + }{{ + name: "metadata.name validation", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "!!!!", + }, + }, + expectedError: `metadata.name: Invalid value: "!!!!":`, + }, { + name: "failure policy validation", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("other") + return &r + }(), + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: `Object{ + spec: Object.spec{ + replicas: object.spec.replicas % 2 == 0?object.spec.replicas + 1:object.spec.replicas + } + }`, + }, + PatchType: applyConfigurationPatchType, + }}, + }, + }, + expectedError: `spec.failurePolicy: Unsupported value: "other": supported values: "Fail", "Ignore"`, + }, { + name: "unsupported expression type validation", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "1 < 2", + }, + PatchType: applyConfigurationPatchType, + }, + }, + }, + }, + expectedError: `spec.mutations[0].applyConfiguration.expression: Invalid value: "1 < 2": must evaluate to Object`, + }, { + name: "patchType validation", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: `Object{ + spec: Object.spec{ + replicas: object.spec.replicas % 2 == 0?object.spec.replicas + 1:object.spec.replicas + } + }`, + }, + PatchType: "other", + }, + }, + }, + }, + expectedError: `spec.mutations[0].patchType: Unsupported value: "other": supported values: "ApplyConfiguration", "JSONPatch"`, + }, { + name: "PatchType is JSONPatch but union member is ApplyConfiguration", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: `Object{ + spec: Object.spec{ + replicas: object.spec.replicas % 2 == 0?object.spec.replicas + 1:object.spec.replicas + } + }`, + }, + PatchType: "JSONPatch", + }, + }, + }, + }, + expectedError: `spec.mutations[0].applyConfiguration: Invalid value: "{applyConfiguration}": must not be specified when patchType is JSONPatch`, + }, { + name: "PatchType is ApplyConfiguration but union member is JSONPatch", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + JSONPatch: &admissionregistration.JSONPatch{ + Expression: `[ + JSONPatch{op: "replace", path: "/spec/repliacs", value: 1} + ]`, + }, + PatchType: "ApplyConfiguration", + }, + }, + }, + }, + expectedError: `spec.mutations[0].jsonPatch: Invalid value: "{jsonPatch}": must not be specified when patchType is ApplyConfiguration`, + }, { + name: "JSONPatch is empty", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + PatchType: "JSONPatch", + }, + }, + }, + }, + expectedError: `spec.mutations[0].jsonPatch: Required value: must be specified when patchType is JSONPatch`, + }, { + name: "JSONPatch has an empty value expression", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + JSONPatch: &admissionregistration.JSONPatch{ + Expression: ` `, + }, + PatchType: "JSONPatch", + }, + }, + }, + }, + expectedError: `spec.mutations[0].jsonPatch.expression: Required value`, + }, { + name: "invalid variable", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Variables: []admissionregistration.Variable{ + { + Name: "x", + Expression: "///", + }, + }, + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + JSONPatch: &admissionregistration.JSONPatch{ + Expression: `[ + JSONPatch{op: "add", path: "/spec/repliacs", value: variables.x} + ]`, + }, + PatchType: "JSONPatch", + }, + }, + }, + }, + expectedError: `spec.variables[0].expression: Invalid value: "///": compilation failed`, + }, { + name: "Reference to missing variable", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Variables: []admissionregistration.Variable{ + { + Name: "x", + Expression: "10 + 10", + }, + }, + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + JSONPatch: &admissionregistration.JSONPatch{ + Expression: `[ + JSONPatch{op: "add", path: "/spec/repliacs", value: variables.x + variables.y} + ]`, + }, + PatchType: "JSONPatch", + }, + }, + }, + }, + expectedError: `undefined field 'y'`, + }, { + name: "API version is required in ParamKind", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + ParamKind: &admissionregistration.ParamKind{ + Kind: "Example", + APIVersion: "test.example.com", + }, + }, + }, + expectedError: `spec.paramKind.apiVersion: Invalid value: "test.example.com"`, + }, { + name: "API kind is required in ParamKind", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + ParamKind: &admissionregistration.ParamKind{ + APIVersion: "test.example.com/v1", + }, + }, + }, + expectedError: `spec.paramKind.kind: Required value`, + }, { + name: "API version format in ParamKind", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + ParamKind: &admissionregistration.ParamKind{ + Kind: "Example", + APIVersion: "test.example.com/!!!", + }, + }, + }, + expectedError: `pec.paramKind.apiVersion: Invalid value: "!!!":`, + }, { + name: "API group format in ParamKind", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + ParamKind: &admissionregistration.ParamKind{ + APIVersion: "!!!/v1", + Kind: "ReplicaLimit", + }, + }, + }, + expectedError: `pec.paramKind.apiVersion: Invalid value: "!!!":`, + }, { + name: "Validations is required", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{}, + }, + + expectedError: `spec.failurePolicy: Required value, spec.matchConstraints: Required value, spec.mutations: Required value: mutations must contain at least one item`, + }, { + name: "MatchConstraints is required", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + }, + }, + expectedError: `spec.matchConstraints: Required value`, + }, { + name: "matchConstraints.resourceRules is required", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{}, + }, + }, + expectedError: `spec.matchConstraints.resourceRules: Required value`, + }, { + name: "matchConstraints.resourceRules has at least one explicit rule", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Rule: admissionregistration.Rule{}, + }, + ResourceNames: []string{"/./."}, + }}, + }, + }, + }, + expectedError: `spec.matchConstraints.resourceRules[0].apiVersions: Required value`, + }, { + name: "expression is required", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{ + { + PatchType: admissionregistration.PatchTypeApplyConfiguration, + ApplyConfiguration: &admissionregistration.ApplyConfiguration{}, + }, + }, + }, + }, + + expectedError: `spec.mutations[0].applyConfiguration.expression: Required value`, + }, { + name: "matchResources resourceNames check", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + ResourceNames: []string{"/./."}, + }}, + }, + }, + }, + expectedError: `spec.matchConstraints.resourceRules[0].resourceNames[0]: Invalid value: "/./."`, + }, { + name: "matchResources resourceNames cannot duplicate", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + ResourceNames: []string{"test", "test"}, + }}, + }, + }, + }, + expectedError: `spec.matchConstraints.resourceRules[0].resourceNames[1]: Duplicate value: "test"`, + }, { + name: "matchResources validation: matchPolicy", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("other") + return &r + }(), + }, + }, + }, + expectedError: `spec.matchConstraints.matchPolicy: Unsupported value: "other": supported values: "Equivalent", "Exact"`, + }, { + name: "Operations must not be empty or nil", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + MatchConstraints: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: nil, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + ExcludeResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: nil, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `spec.matchConstraints.resourceRules[0].operations: Required value, spec.matchConstraints.resourceRules[1].operations: Required value, spec.matchConstraints.excludeResourceRules[0].operations: Required value, spec.matchConstraints.excludeResourceRules[1].operations: Required value`, + }, { + name: "\"\" is NOT a valid operation", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE", ""}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `Unsupported value: ""`, + }, { + name: "operation must be either create/update", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"PATCH"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `Unsupported value: "PATCH"`, + }, { + name: "operation must not be delete", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"DELETE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `Unsupported value: "DELETE"`, + }, { + name: "wildcard operation cannot be mixed with other strings", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE", "*"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `if '*' is present, must not specify other operations`, + }, { + name: `resource "*" can co-exist with resources that have subresources`, + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*", "a/b", "a/*", "*/b"}, + }, + }, + }}, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + }, + }, + }, + }, { + name: `resource "*" cannot mix with resources that don't have subresources`, + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*", "a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `if '*' is present, must not specify other resources without subresources`, + }, { + name: "resource a/* cannot mix with a/x", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a/*", "a/x"}, + }, + }, + }}, + }, + }, + }, + expectedError: `spec.matchConstraints.resourceRules[0].resources[1]: Invalid value: "a/x": if 'a/*' is present, must not specify a/x`, + }, { + name: "resource a/* can mix with a", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a/*", "a"}, + }, + }, + }}, + }, + }, + }, + }, { + name: "resource */a cannot mix with x/a", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*/a", "x/a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `spec.matchConstraints.resourceRules[0].resources[1]: Invalid value: "x/a": if '*/a' is present, must not specify x/a`, + }, { + name: "resource */* cannot mix with other resources", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*/*", "a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `spec.matchConstraints.resourceRules[0].resources: Invalid value: []string{"*/*", "a"}: if '*/*' is present, must not specify other resources`, + }, { + name: "patchType required", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*/*"}, + }, + }, + }}, + }, + }, + }, + expectedError: `spec.mutations[0].patchType: Required value`, + }, { + name: "single match condition must have a name", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + MatchConditions: []admissionregistration.MatchCondition{{ + Expression: "true", + }}, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + }, + }, + expectedError: `spec.matchConditions[0].name: Required value`, + }, { + name: "match condition with parameters allowed", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ParamKind: &admissionregistration.ParamKind{ + Kind: "Foo", + APIVersion: "foobar/v1alpha1", + }, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE", "UPDATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + }, + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + MatchConditions: []admissionregistration.MatchCondition{{ + Name: "hasParams", + Expression: `params.foo == "okay"`, + }}, + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + }, + }, + expectedError: "", + }, { + name: "match condition with parameters not allowed if no param kind", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"*"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + }, + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + MatchConditions: []admissionregistration.MatchCondition{{ + Name: "hasParams", + Expression: `params.foo == "okay"`, + }}, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + }, + }, + expectedError: `undeclared reference to 'params'`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + errs := ValidateMutatingAdmissionPolicy(test.config) + err := errs.ToAggregate() + if err != nil { + if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { + t.Errorf("expected to contain %s, got %s", e, a) + } + } else { + if test.expectedError != "" { + t.Errorf("unexpected no error, expected to contain %s", test.expectedError) + } + } + }) + } +} + +func TestValidateMutatingAdmissionPolicyUpdate(t *testing.T) { + applyConfigurationPatchType := admissionregistration.PatchTypeApplyConfiguration + tests := []struct { + name string + oldconfig *admissionregistration.MutatingAdmissionPolicy + config *admissionregistration.MutatingAdmissionPolicy + expectedError string + }{{ + name: "should pass on valid new MutatingAdmissionPolicy", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + oldconfig: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + }, { + name: "should pass on valid new MutatingAdmissionPolicy with invalid old MutatingAdmissionPolicy", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + oldconfig: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "!!!", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{}, + }, + }, { + name: "match conditions re-checked if paramKind changes", + oldconfig: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ParamKind: &admissionregistration.ParamKind{ + Kind: "Foo", + APIVersion: "foobar/v1alpha1", + }, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"*"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + }, + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + MatchConditions: []admissionregistration.MatchCondition{{ + Name: "hasParams", + Expression: `params.foo == "okay"`, + }}, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + }, + }, + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"*"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + }, + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + MatchConditions: []admissionregistration.MatchCondition{{ + Name: "hasParams", + Expression: `params.foo == "okay"`, + }}, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + }, + }, + expectedError: `undeclared reference to 'params'`, + }, { + name: "match conditions not re-checked if no change to paramKind or matchConditions", + oldconfig: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE", "UPDATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + }, + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + MatchConditions: []admissionregistration.MatchCondition{{ + Name: "hasParams", + Expression: `params.foo == "okay"`, + }}, + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + }, + }, + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE", "UPDATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + }, + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Ignore") + return &r + }(), + MatchConditions: []admissionregistration.MatchCondition{{ + Name: "hasParams", + Expression: `params.foo == "okay"`, + }}, + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + }, + }, + expectedError: "", + }, + { + name: "matchCondition expressions that are changed must be compiled using the NewExpression environment", + oldconfig: mutatingAdmissionPolicyWithExpressions( + []admissionregistration.MatchCondition{ + { + Name: "checkEnvironmentMode", + Expression: `true`, + }, + }, + nil), + config: mutatingAdmissionPolicyWithExpressions( + []admissionregistration.MatchCondition{ + { + Name: "checkEnvironmentMode", + Expression: `test() == true`, + }, + }, + nil), + expectedError: `undeclared reference to 'test'`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + errs := ValidateMutatingAdmissionPolicyUpdate(test.config, test.oldconfig) + err := errs.ToAggregate() + if err != nil { + if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { + t.Errorf("expected to contain %s, got %s", e, a) + } + } else { + if test.expectedError != "" { + t.Errorf("unexpected no error, expected to contain %s", test.expectedError) + } + } + }) + + } +} + +func TestValidateMutatingAdmissionPolicyBinding(t *testing.T) { + tests := []struct { + name string + config *admissionregistration.MutatingAdmissionPolicyBinding + expectedError string + }{{ + name: "metadata.name validation", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "!!!!", + }, + }, + expectedError: `metadata.name: Invalid value: "!!!!":`, + }, { + name: "PolicyName is required", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{}, + }, + expectedError: `spec.policyName: Required value`, + }, { + name: "matchResources validation: matchPolicy", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("other") + return &r + }(), + }, + }, + }, + expectedError: `spec.matchResources.matchPolicy: Unsupported value: "other": supported values: "Equivalent", "Exact"`, + }, { + name: "Operations must not be empty or nil", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: nil, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + ExcludeResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: nil, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `spec.matchResources.resourceRules[0].operations: Required value, spec.matchResources.resourceRules[1].operations: Required value, spec.matchResources.excludeResourceRules[0].operations: Required value, spec.matchResources.excludeResourceRules[1].operations: Required value`, + }, { + name: "\"\" is NOT a valid operation", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE", ""}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `Unsupported value: ""`, + }, { + name: "operation must be either create/update", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"PATCH"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `Unsupported value: "PATCH"`, + }, { + name: "operation must not be DELETE", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"DELETE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `Unsupported value: "DELETE"`, + }, { + name: "wildcard operation cannot be mixed with other strings", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE", "*"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `if '*' is present, must not specify other operations`, + }, { + name: `resource "*" can co-exist with resources that have subresources`, + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*", "a/b", "a/*", "*/b"}, + }, + }, + }}, + }, + }, + }, + }, { + name: `resource "*" cannot mix with resources that don't have subresources`, + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*", "a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `if '*' is present, must not specify other resources without subresources`, + }, { + name: "resource a/* cannot mix with a/x", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a/*", "a/x"}, + }, + }, + }}, + }, + }, + }, + expectedError: `spec.matchResources.resourceRules[0].resources[1]: Invalid value: "a/x": if 'a/*' is present, must not specify a/x`, + }, { + name: "resource a/* can mix with a", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a/*", "a"}, + }, + }, + }}, + }, + }, + }, + }, { + name: "resource */a cannot mix with x/a", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*/a", "x/a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `spec.matchResources.resourceRules[0].resources[1]: Invalid value: "x/a": if '*/a' is present, must not specify x/a`, + }, { + name: "resource */* cannot mix with other resources", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*/*", "a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `spec.matchResources.resourceRules[0].resources: Invalid value: []string{"*/*", "a"}: if '*/*' is present, must not specify other resources`, + }, { + name: "paramRef selector must not be set when name is set", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "label": "value", + }, + }, + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + }, + }, + expectedError: `spec.paramRef.name: Forbidden: name and selector are mutually exclusive, spec.paramRef.selector: Forbidden: name and selector are mutually exclusive`, + }, { + name: "paramRef parameterNotFoundAction must be set", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + }, + }, + }, + expectedError: "spec.paramRef.parameterNotFoundAction: Required value", + }, { + name: "paramRef parameterNotFoundAction must be an valid value", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.ParameterNotFoundActionType("invalid")), + }, + }, + }, + expectedError: `spec.paramRef.parameterNotFoundAction: Unsupported value: "invalid": supported values: "Deny", "Allow"`, + }, { + name: "paramRef one of name or selector", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + }, + }, + expectedError: `one of name or selector must be specified`, + }} + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + errs := ValidateMutatingAdmissionPolicyBinding(test.config) + err := errs.ToAggregate() + if err != nil { + if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { + t.Errorf("expected to contain %s, got %s", e, a) + } + } else { + if test.expectedError != "" { + t.Errorf("unexpected no error, expected to contain %s", test.expectedError) + } + } + }) + + } +} + +func TestValidateMutatingAdmissionPolicyBindingUpdate(t *testing.T) { + tests := []struct { + name string + oldconfig *admissionregistration.MutatingAdmissionPolicyBinding + config *admissionregistration.MutatingAdmissionPolicyBinding + expectedError string + }{{ + name: "should pass on valid new MutatingAdmissionPolicyBinding", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + oldconfig: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + }, { + name: "should pass on valid new MutatingAdmissionPolicyBinding with invalid old ValidatingAdmissionPolicyBinding", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + oldconfig: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "!!!", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{}, + }, + }} + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + errs := ValidateMutatingAdmissionPolicyBindingUpdate(test.config, test.oldconfig) + err := errs.ToAggregate() + if err != nil { + if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { + t.Errorf("expected to contain %s, got %s", e, a) + } + } else { + if test.expectedError != "" { + t.Errorf("unexpected no error, expected to contain %s", test.expectedError) + } + } + }) + + } +} diff --git a/pkg/kubeapiserver/default_storage_factory_builder.go b/pkg/kubeapiserver/default_storage_factory_builder.go index 125f5d8a8bc..ba679bfdaa7 100644 --- a/pkg/kubeapiserver/default_storage_factory_builder.go +++ b/pkg/kubeapiserver/default_storage_factory_builder.go @@ -27,6 +27,7 @@ import ( "k8s.io/apiserver/pkg/storage/storagebackend" version "k8s.io/component-base/version" "k8s.io/kubernetes/pkg/api/legacyscheme" + "k8s.io/kubernetes/pkg/apis/admissionregistration" "k8s.io/kubernetes/pkg/apis/apps" "k8s.io/kubernetes/pkg/apis/certificates" "k8s.io/kubernetes/pkg/apis/coordination" @@ -75,6 +76,8 @@ func NewStorageFactoryConfig() *StorageFactoryConfig { coordination.Resource("leasecandidates").WithVersion("v1alpha1"), networking.Resource("ipaddresses").WithVersion("v1beta1"), networking.Resource("servicecidrs").WithVersion("v1beta1"), + admissionregistration.Resource("mutatingadmissionpolicies").WithVersion("v1alpha1"), + admissionregistration.Resource("mutatingadmissionpolicybindings").WithVersion("v1alpha1"), certificates.Resource("clustertrustbundles").WithVersion("v1alpha1"), storage.Resource("volumeattributesclasses").WithVersion("v1beta1"), storagemigration.Resource("storagemigrations").WithVersion("v1alpha1"), diff --git a/pkg/printers/internalversion/printers.go b/pkg/printers/internalversion/printers.go index 00f23e34356..468c41ec5bf 100644 --- a/pkg/printers/internalversion/printers.go +++ b/pkg/printers/internalversion/printers.go @@ -596,6 +596,24 @@ func AddHandlers(h printers.PrintHandler) { _ = h.TableHandler(validatingAdmissionPolicyBinding, printValidatingAdmissionPolicyBinding) _ = h.TableHandler(validatingAdmissionPolicyBinding, printValidatingAdmissionPolicyBindingList) + mutatingAdmissionPolicy := []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, + {Name: "Mutations", Type: "integer", Description: "Mutation indicates the number of mutations rules defined in this configuration"}, + {Name: "ParamKind", Type: "string", Description: "ParamKind specifies the kind of resources used to parameterize this policy"}, + {Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]}, + } + _ = h.TableHandler(mutatingAdmissionPolicy, printMutatingAdmissionPolicy) + _ = h.TableHandler(mutatingAdmissionPolicy, printMutatingAdmissionPolicyList) + + mutatingAdmissionPolicyBinding := []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, + {Name: "PolicyName", Type: "string", Description: "PolicyName indicates the policy definition which the policy binding binded to"}, + {Name: "ParamRef", Type: "string", Description: "ParamRef indicates the param resource which sets the configration param"}, + {Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]}, + } + _ = h.TableHandler(mutatingAdmissionPolicyBinding, printMutatingAdmissionPolicyBinding) + _ = h.TableHandler(mutatingAdmissionPolicyBinding, printMutatingAdmissionPolicyBindingList) + flowSchemaColumnDefinitions := []metav1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, {Name: "PriorityLevel", Type: "string", Description: flowcontrolv1.PriorityLevelConfigurationReference{}.SwaggerDoc()["name"]}, @@ -1765,6 +1783,64 @@ func printValidatingAdmissionPolicyBindingList(list *admissionregistration.Valid return rows, nil } +func printMutatingAdmissionPolicy(obj *admissionregistration.MutatingAdmissionPolicy, options printers.GenerateOptions) ([]metav1.TableRow, error) { + row := metav1.TableRow{ + Object: runtime.RawExtension{Object: obj}, + } + paramKind := "" + if obj.Spec.ParamKind != nil { + paramKind = obj.Spec.ParamKind.APIVersion + "/" + obj.Spec.ParamKind.Kind + } + row.Cells = append(row.Cells, obj.Name, int64(len(obj.Spec.Mutations)), paramKind, translateTimestampSince(obj.CreationTimestamp)) + return []metav1.TableRow{row}, nil +} + +func printMutatingAdmissionPolicyList(list *admissionregistration.MutatingAdmissionPolicyList, options printers.GenerateOptions) ([]metav1.TableRow, error) { + rows := make([]metav1.TableRow, 0, len(list.Items)) + for i := range list.Items { + r, err := printMutatingAdmissionPolicy(&list.Items[i], options) + if err != nil { + return nil, err + } + rows = append(rows, r...) + } + return rows, nil +} + +func printMutatingAdmissionPolicyBinding(obj *admissionregistration.MutatingAdmissionPolicyBinding, options printers.GenerateOptions) ([]metav1.TableRow, error) { + row := metav1.TableRow{ + Object: runtime.RawExtension{Object: obj}, + } + paramName := "" + if pr := obj.Spec.ParamRef; pr != nil { + if len(pr.Name) > 0 { + if pr.Namespace != "" { + paramName = pr.Namespace + "/" + pr.Name + } else { + // Can't tell from here if param is cluster-scoped, so all + // params without names get * namespace + paramName = "*/" + pr.Name + } + } else if pr.Selector != nil { + paramName = pr.Selector.String() + } + } + row.Cells = append(row.Cells, obj.Name, obj.Spec.PolicyName, paramName, translateTimestampSince(obj.CreationTimestamp)) + return []metav1.TableRow{row}, nil +} + +func printMutatingAdmissionPolicyBindingList(list *admissionregistration.MutatingAdmissionPolicyBindingList, options printers.GenerateOptions) ([]metav1.TableRow, error) { + rows := make([]metav1.TableRow, 0, len(list.Items)) + for i := range list.Items { + r, err := printMutatingAdmissionPolicyBinding(&list.Items[i], options) + if err != nil { + return nil, err + } + rows = append(rows, r...) + } + return rows, nil +} + func printNamespace(obj *api.Namespace, options printers.GenerateOptions) ([]metav1.TableRow, error) { row := metav1.TableRow{ Object: runtime.RawExtension{Object: obj}, diff --git a/pkg/printers/internalversion/printers_test.go b/pkg/printers/internalversion/printers_test.go index 1df69067c5b..57d78f80ebc 100644 --- a/pkg/printers/internalversion/printers_test.go +++ b/pkg/printers/internalversion/printers_test.go @@ -7025,6 +7025,18 @@ func TestTableRowDeepCopyShouldNotPanic(t *testing.T) { return printValidatingAdmissionPolicyBinding(&admissionregistration.ValidatingAdmissionPolicyBinding{}, printers.GenerateOptions{}) }, }, + { + name: "MutatingAdmissionPolicy", + printer: func() ([]metav1.TableRow, error) { + return printMutatingAdmissionPolicy(&admissionregistration.MutatingAdmissionPolicy{}, printers.GenerateOptions{}) + }, + }, + { + name: "MutatingAdmissionPolicyBinding", + printer: func() ([]metav1.TableRow, error) { + return printMutatingAdmissionPolicyBinding(&admissionregistration.MutatingAdmissionPolicyBinding{}, printers.GenerateOptions{}) + }, + }, { name: "Namespace", printer: func() ([]metav1.TableRow, error) { diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicy/authz.go b/pkg/registry/admissionregistration/mutatingadmissionpolicy/authz.go new file mode 100644 index 00000000000..93f6f2ca4f2 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicy/authz.go @@ -0,0 +1,105 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mutatingadmissionpolicy + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authorization/authorizer" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + rbacregistry "k8s.io/kubernetes/pkg/registry/rbac" +) + +func (v *mutatingAdmissionPolicyStrategy) authorizeCreate(ctx context.Context, obj runtime.Object) error { + policy := obj.(*admissionregistration.MutatingAdmissionPolicy) + if policy.Spec.ParamKind == nil { + // no paramRef in new object + return nil + } + + return v.authorize(ctx, policy) +} + +func (v *mutatingAdmissionPolicyStrategy) authorizeUpdate(ctx context.Context, obj, old runtime.Object) error { + policy := obj.(*admissionregistration.MutatingAdmissionPolicy) + if policy.Spec.ParamKind == nil { + // no paramRef in new object + return nil + } + + oldPolicy := old.(*admissionregistration.MutatingAdmissionPolicy) + if oldPolicy.Spec.ParamKind != nil && *oldPolicy.Spec.ParamKind == *policy.Spec.ParamKind { + // identical paramKind to old object + return nil + } + + return v.authorize(ctx, policy) +} + +func (v *mutatingAdmissionPolicyStrategy) authorize(ctx context.Context, policy *admissionregistration.MutatingAdmissionPolicy) error { + if v.authorizer == nil || policy.Spec.ParamKind == nil { + return nil + } + + // for superuser, skip all checks + if rbacregistry.EscalationAllowed(ctx) { + return nil + } + + user, ok := genericapirequest.UserFrom(ctx) + if !ok { + return fmt.Errorf("cannot identify user to authorize read access to paramKind resources") + } + + paramKind := policy.Spec.ParamKind + // default to requiring permissions on all group/version/resources + resource, apiGroup, apiVersion := "*", "*", "*" + if gv, err := schema.ParseGroupVersion(paramKind.APIVersion); err == nil { + // we only need to authorize the parsed group/version + apiGroup = gv.Group + apiVersion = gv.Version + if gvr, err := v.resourceResolver.Resolve(gv.WithKind(paramKind.Kind)); err == nil { + // we only need to authorize the resolved resource + resource = gvr.Resource + } + } + + // require that the user can read (verb "get") the referred kind. + attrs := authorizer.AttributesRecord{ + User: user, + Verb: "get", + ResourceRequest: true, + Name: "*", + Namespace: "*", + APIGroup: apiGroup, + APIVersion: apiVersion, + Resource: resource, + } + + d, _, err := v.authorizer.Authorize(ctx, attrs) + if err != nil { + return err + } + if d != authorizer.DecisionAllow { + return fmt.Errorf(`user %v must have "get" permission on all objects of the referenced paramKind (kind=%s, apiVersion=%s)`, user, paramKind.Kind, paramKind.APIVersion) + } + return nil +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicy/authz_test.go b/pkg/registry/admissionregistration/mutatingadmissionpolicy/authz_test.go new file mode 100644 index 00000000000..cecadc792fe --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicy/authz_test.go @@ -0,0 +1,131 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mutatingadmissionpolicy + +import ( + "context" + "testing" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" +) + +func TestAuthorization(t *testing.T) { + for _, tc := range []struct { + name string + userInfo user.Info + obj *admissionregistration.MutatingAdmissionPolicy + auth AuthFunc + resourceResolver resolver.ResourceResolverFunc + expectErr bool + }{ + { + name: "superuser", + userInfo: &user.DefaultInfo{Groups: []string{user.SystemPrivilegedGroup}}, + expectErr: false, // success despite always-denying authorizer + obj: validMutatingAdmissionPolicy(), + auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + return authorizer.DecisionDeny, "", nil + }, + }, + { + name: "authorized", + userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}}, + obj: validMutatingAdmissionPolicy(), + auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + if a.GetResource() == "replicalimits" { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "", nil + }, + resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: "rules.example.com", + Version: "v1", + Resource: "replicalimits", + }, nil + }, + expectErr: false, + }, + { + name: "denied", + userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}}, + obj: validMutatingAdmissionPolicy(), + auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + if a.GetResource() == "configmaps" { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "", nil + }, + resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: "rules.example.com", + Version: "v1", + Resource: "replicalimits", + }, nil + }, + expectErr: true, + }, + { + name: "param not found", + userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}}, + obj: validMutatingAdmissionPolicy(), + auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + if a.GetResource() == "replicalimits" { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "", nil + }, + resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{}, &meta.NoKindMatchError{GroupKind: gvk.GroupKind(), SearchedVersions: []string{gvk.Version}} + }, + expectErr: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + strategy := NewStrategy(tc.auth, tc.resourceResolver) + t.Run("create", func(t *testing.T) { + ctx := request.WithUser(context.Background(), tc.userInfo) + errs := strategy.Validate(ctx, validMutatingAdmissionPolicy()) + if len(errs) > 0 != tc.expectErr { + t.Errorf("expected error: %v but got error: %v", tc.expectErr, errs) + } + }) + t.Run("update", func(t *testing.T) { + ctx := request.WithUser(context.Background(), tc.userInfo) + obj := validMutatingAdmissionPolicy() + objWithUpdatedParamKind := obj.DeepCopy() + objWithUpdatedParamKind.Spec.ParamKind.APIVersion += "1" + errs := strategy.ValidateUpdate(ctx, obj, objWithUpdatedParamKind) + if len(errs) > 0 != tc.expectErr { + t.Errorf("expected error: %v but got error: %v", tc.expectErr, errs) + } + }) + }) + } +} + +type AuthFunc func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) + +func (f AuthFunc) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + return f(ctx, a) +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicy/doc.go b/pkg/registry/admissionregistration/mutatingadmissionpolicy/doc.go new file mode 100644 index 00000000000..5a842f7af37 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicy/doc.go @@ -0,0 +1,17 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mutatingadmissionpolicy // import "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingadmissionpolicy" diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicy/storage/storage.go b/pkg/registry/admissionregistration/mutatingadmissionpolicy/storage/storage.go new file mode 100644 index 00000000000..e5ba64ba4ab --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicy/storage/storage.go @@ -0,0 +1,73 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package storage + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/registry/generic" + genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/printers" + printersinternal "k8s.io/kubernetes/pkg/printers/internalversion" + printerstorage "k8s.io/kubernetes/pkg/printers/storage" + "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingadmissionpolicy" + "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" +) + +// REST implements a RESTStorage for MutatingAdmissionPolicy against etcd +type REST struct { + *genericregistry.Store +} + +var groupResource = admissionregistration.Resource("mutatingadmissionpolicies") + +// NewREST returns the RESTStorage objects that will work against MutatingAdmissionPolicy. +func NewREST(optsGetter generic.RESTOptionsGetter, authorizer authorizer.Authorizer, resourceResolver resolver.ResourceResolver) (*REST, error) { + r := &REST{} + strategy := mutatingadmissionpolicy.NewStrategy(authorizer, resourceResolver) + store := &genericregistry.Store{ + NewFunc: func() runtime.Object { return &admissionregistration.MutatingAdmissionPolicy{} }, + NewListFunc: func() runtime.Object { return &admissionregistration.MutatingAdmissionPolicyList{} }, + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*admissionregistration.MutatingAdmissionPolicy).Name, nil + }, + DefaultQualifiedResource: groupResource, + SingularQualifiedResource: admissionregistration.Resource("mutatingadmissionpolicy"), + + CreateStrategy: strategy, + UpdateStrategy: strategy, + DeleteStrategy: strategy, + + TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)}, + } + options := &generic.StoreOptions{RESTOptions: optsGetter} + if err := store.CompleteWithOptions(options); err != nil { + return nil, err + } + r.Store = store + return r, nil +} + +// Implement CategoriesProvider +var _ rest.CategoriesProvider = &REST{} + +// Categories implements the CategoriesProvider interface. Returns a list of categories a resource is part of. +func (r *REST) Categories() []string { + return []string{"api-extensions"} +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicy/storage/storage_test.go b/pkg/registry/admissionregistration/mutatingadmissionpolicy/storage/storage_test.go new file mode 100644 index 00000000000..32b5c460a40 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicy/storage/storage_test.go @@ -0,0 +1,252 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package storage + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/registry/generic" + genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing" + etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" + "k8s.io/kubernetes/pkg/registry/registrytest" + + // Ensure that admissionregistration package is initialized. + _ "k8s.io/kubernetes/pkg/apis/admissionregistration/install" +) + +func TestCreate(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + configuration := validMutatingAdmissionPolicy() + test.TestCreate( + // valid + configuration, + // invalid + newMutatingAdmissionPolicy(""), + ) +} + +func TestUpdate(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + + test.TestUpdate( + // valid + validMutatingAdmissionPolicy(), + // updateFunc + func(obj runtime.Object) runtime.Object { + object := obj.(*admissionregistration.MutatingAdmissionPolicy) + object.Labels = map[string]string{"c": "d"} + return object + }, + // invalid updateFunc + func(obj runtime.Object) runtime.Object { + object := obj.(*admissionregistration.MutatingAdmissionPolicy) + object.Name = "" + return object + }, + ) +} + +func TestGet(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestGet(validMutatingAdmissionPolicy()) +} + +func TestList(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestList(validMutatingAdmissionPolicy()) +} + +func TestDelete(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestDelete(validMutatingAdmissionPolicy()) +} + +func TestWatch(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestWatch( + validMutatingAdmissionPolicy(), + []labels.Set{}, + []labels.Set{ + {"hoo": "bar"}, + }, + []fields.Set{ + {"metadata.name": "foo"}, + }, + []fields.Set{ + {"metadata.name": "nomatch"}, + }, + ) +} + +func validMutatingAdmissionPolicy() *admissionregistration.MutatingAdmissionPolicy { + return &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + ParamKind: &admissionregistration.ParamKind{ + APIVersion: "rules.example.com/v1", + Kind: "ReplicaLimit", + }, + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + PatchType: admissionregistration.PatchTypeApplyConfiguration, + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: `Object{ + spec: Object.spec{ + replicas: object.spec.replicas % 2 == 0?object.spec.replicas + 1:object.spec.replicas + } + }`, + }, + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + }, + }, + } +} + +func newMutatingAdmissionPolicy(name string) *admissionregistration.MutatingAdmissionPolicy { + ignore := admissionregistration.Ignore + return &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{"foo": "bar"}, + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ParamKind: &admissionregistration.ParamKind{ + APIVersion: "rules.example.com/v1", + Kind: "ReplicaLimit", + }, + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + PatchType: admissionregistration.PatchTypeApplyConfiguration, + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: `Object{ + spec: Object.spec{ + replicas: object.spec.replicas % 2 == 0?object.spec.replicas + 1:object.spec.replicas + } + }`, + }, + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, + FailurePolicy: &ignore, + }, + } +} + +func newInsecureStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) { + return newStorage(t, nil, replicaLimitsResolver) +} + +func newStorage(t *testing.T, authorizer authorizer.Authorizer, resourceResolver resolver.ResourceResolver) (*REST, *etcd3testing.EtcdTestServer) { + etcdStorage, server := registrytest.NewEtcdStorageForResource(t, admissionregistration.Resource("mutatingadmissionpolicies")) + restOptions := generic.RESTOptions{ + StorageConfig: etcdStorage, + Decorator: generic.UndecoratedStorage, + DeleteCollectionWorkers: 1, + ResourcePrefix: "mutatingadmissionpolicies"} + storage, err := NewREST(restOptions, authorizer, resourceResolver) + if err != nil { + t.Fatalf("unexpected error from REST storage: %v", err) + } + return storage, server +} + +func TestCategories(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + expected := []string{"api-extensions"} + registrytest.AssertCategories(t, storage, expected) +} + +var replicaLimitsResolver resolver.ResourceResolverFunc = func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: "rules.example.com", + Version: "v1", + Resource: "replicalimits", + }, nil +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicy/strategy.go b/pkg/registry/admissionregistration/mutatingadmissionpolicy/strategy.go new file mode 100644 index 00000000000..698eb8adf69 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicy/strategy.go @@ -0,0 +1,122 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mutatingadmissionpolicy + +import ( + "context" + + apiequality "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/storage/names" + "k8s.io/kubernetes/pkg/api/legacyscheme" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/apis/admissionregistration/validation" + "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" +) + +// mutatingAdmissionPolicyStrategy implements verification logic for MutatingAdmissionPolicy. +type mutatingAdmissionPolicyStrategy struct { + runtime.ObjectTyper + names.NameGenerator + authorizer authorizer.Authorizer + resourceResolver resolver.ResourceResolver +} + +// NewStrategy is the default logic that applies when creating and updating MutatingAdmissionPolicy objects. +func NewStrategy(authorizer authorizer.Authorizer, resourceResolver resolver.ResourceResolver) *mutatingAdmissionPolicyStrategy { + return &mutatingAdmissionPolicyStrategy{ + ObjectTyper: legacyscheme.Scheme, + NameGenerator: names.SimpleNameGenerator, + authorizer: authorizer, + resourceResolver: resourceResolver, + } +} + +// NamespaceScoped returns false because MutatingAdmissionPolicy is cluster-scoped resource. +func (v *mutatingAdmissionPolicyStrategy) NamespaceScoped() bool { + return false +} + +// PrepareForCreate clears the status of an MutatingAdmissionPolicy before creation. +func (v *mutatingAdmissionPolicyStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { + ic := obj.(*admissionregistration.MutatingAdmissionPolicy) + ic.Generation = 1 +} + +// PrepareForUpdate clears fields that are not allowed to be set by end users on update. +func (v *mutatingAdmissionPolicyStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { + newIC := obj.(*admissionregistration.MutatingAdmissionPolicy) + oldIC := old.(*admissionregistration.MutatingAdmissionPolicy) + + // Any changes to the spec increment the generation number, any changes to the + // status should reflect the generation number of the corresponding object. + // See metav1.ObjectMeta description for more information on Generation. + if !apiequality.Semantic.DeepEqual(oldIC.Spec, newIC.Spec) { + newIC.Generation = oldIC.Generation + 1 + } +} + +// Validate validates a new MutatingAdmissionPolicy. +func (v *mutatingAdmissionPolicyStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { + errs := validation.ValidateMutatingAdmissionPolicy(obj.(*admissionregistration.MutatingAdmissionPolicy)) + if len(errs) == 0 { + // if the object is well-formed, also authorize the paramKind + if err := v.authorizeCreate(ctx, obj); err != nil { + errs = append(errs, field.Forbidden(field.NewPath("spec", "paramKind"), err.Error())) + } + } + return errs +} + +// WarningsOnCreate returns warnings for the creation of the given object. +func (v *mutatingAdmissionPolicyStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { + return nil +} + +// Canonicalize normalizes the object after validation. +func (v *mutatingAdmissionPolicyStrategy) Canonicalize(obj runtime.Object) { +} + +// AllowCreateOnUpdate is false for MutatingAdmissionPolicy; this means you may not create one with a PUT request. +func (v *mutatingAdmissionPolicyStrategy) AllowCreateOnUpdate() bool { + return false +} + +// ValidateUpdate is the default update validation for an end user. +func (v *mutatingAdmissionPolicyStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { + errs := validation.ValidateMutatingAdmissionPolicyUpdate(obj.(*admissionregistration.MutatingAdmissionPolicy), old.(*admissionregistration.MutatingAdmissionPolicy)) + if len(errs) == 0 { + // if the object is well-formed, also authorize the paramKind + if err := v.authorizeUpdate(ctx, obj, old); err != nil { + errs = append(errs, field.Forbidden(field.NewPath("spec", "paramKind"), err.Error())) + } + } + return errs +} + +// WarningsOnUpdate returns warnings for the given update. +func (v *mutatingAdmissionPolicyStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { + return nil +} + +// AllowUnconditionalUpdate is the default update policy for MutatingAdmissionPolicy objects. Status update should +// only be allowed if version match. +func (v *mutatingAdmissionPolicyStrategy) AllowUnconditionalUpdate() bool { + return false +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicy/strategy_test.go b/pkg/registry/admissionregistration/mutatingadmissionpolicy/strategy_test.go new file mode 100644 index 00000000000..2b181cf74a5 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicy/strategy_test.go @@ -0,0 +1,114 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mutatingadmissionpolicy + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" +) + +func TestMutatingAdmissionPolicyStrategy(t *testing.T) { + strategy := NewStrategy(nil, replicaLimitsResolver) + ctx := genericapirequest.NewDefaultContext() + if strategy.NamespaceScoped() { + t.Error("MutatingAdmissionPolicy strategy must be cluster scoped") + } + if strategy.AllowCreateOnUpdate() { + t.Errorf("MutatingAdmissionPolicy should not allow create on update") + } + + configuration := validMutatingAdmissionPolicy() + strategy.PrepareForCreate(ctx, configuration) + errs := strategy.Validate(ctx, configuration) + if len(errs) != 0 { + t.Errorf("Unexpected error mutating %v", errs) + } + invalidConfiguration := &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: ""}, + } + strategy.PrepareForUpdate(ctx, invalidConfiguration, configuration) + errs = strategy.ValidateUpdate(ctx, invalidConfiguration, configuration) + if len(errs) == 0 { + t.Errorf("Expected a validation error") + } +} + +var replicaLimitsResolver resolver.ResourceResolverFunc = func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: "rules.example.com", + Version: "v1", + Resource: "replicalimits", + }, nil +} + +func validMutatingAdmissionPolicy() *admissionregistration.MutatingAdmissionPolicy { + ignore := admissionregistration.Ignore + return &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ParamKind: &admissionregistration.ParamKind{ + Kind: "ReplicaLimit", + APIVersion: "rules.example.com/v1", + }, + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + PatchType: admissionregistration.PatchTypeApplyConfiguration, + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: `Object{ + spec: Object.spec{ + replicas: object.spec.replicas % 2 == 0?object.spec.replicas + 1:object.spec.replicas + } + }`, + }, + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, + FailurePolicy: &ignore, + }, + } +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/authz.go b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/authz.go new file mode 100644 index 00000000000..474b7656721 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/authz.go @@ -0,0 +1,137 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mutatingadmissionpolicybinding + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authorization/authorizer" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + rbacregistry "k8s.io/kubernetes/pkg/registry/rbac" +) + +func (v *mutatingAdmissionPolicyBindingStrategy) authorizeCreate(ctx context.Context, obj runtime.Object) error { + binding := obj.(*admissionregistration.MutatingAdmissionPolicyBinding) + if binding.Spec.ParamRef == nil { + // no paramRef in new object + return nil + } + + return v.authorize(ctx, binding) +} + +func (v *mutatingAdmissionPolicyBindingStrategy) authorizeUpdate(ctx context.Context, obj, old runtime.Object) error { + binding := obj.(*admissionregistration.MutatingAdmissionPolicyBinding) + if binding.Spec.ParamRef == nil { + // no paramRef in new object + return nil + } + + oldBinding := old.(*admissionregistration.MutatingAdmissionPolicyBinding) + if oldBinding.Spec.ParamRef != nil && *oldBinding.Spec.ParamRef == *binding.Spec.ParamRef && oldBinding.Spec.PolicyName == binding.Spec.PolicyName { + // identical paramRef and policy to old object + return nil + } + + return v.authorize(ctx, binding) +} + +func (v *mutatingAdmissionPolicyBindingStrategy) authorize(ctx context.Context, binding *admissionregistration.MutatingAdmissionPolicyBinding) error { + if v.resourceResolver == nil { + return fmt.Errorf(`unexpected internal error: resourceResolver is nil`) + } + if v.authorizer == nil || binding.Spec.ParamRef == nil { + return nil + } + + // for superuser, skip all checks + if rbacregistry.EscalationAllowed(ctx) { + return nil + } + + user, ok := genericapirequest.UserFrom(ctx) + if !ok { + return fmt.Errorf("cannot identify user to authorize read access to paramRef object") + } + + // default to requiring permissions on all group/version/resources + resource, apiGroup, apiVersion := "*", "*", "*" + + var policyErr, gvParseErr, gvrResolveErr error + + var policy *admissionregistration.MutatingAdmissionPolicy + policy, policyErr = v.policyGetter.GetMutatingAdmissionPolicy(ctx, binding.Spec.PolicyName) + if policyErr == nil && policy.Spec.ParamKind != nil { + paramKind := policy.Spec.ParamKind + var gv schema.GroupVersion + gv, gvParseErr = schema.ParseGroupVersion(paramKind.APIVersion) + if gvParseErr == nil { + // we only need to authorize the parsed group/version + apiGroup = gv.Group + apiVersion = gv.Version + var gvr schema.GroupVersionResource + gvr, gvrResolveErr = v.resourceResolver.Resolve(gv.WithKind(paramKind.Kind)) + if gvrResolveErr == nil { + // we only need to authorize the resolved resource + resource = gvr.Resource + } + } + } + + var attrs authorizer.AttributesRecord + + paramRef := binding.Spec.ParamRef + verb := "get" + + if len(paramRef.Name) == 0 { + verb = "list" + } + + attrs = authorizer.AttributesRecord{ + User: user, + Verb: verb, + ResourceRequest: true, + Name: paramRef.Name, + Namespace: paramRef.Namespace, // if empty, no namespace indicates get across all namespaces + APIGroup: apiGroup, + APIVersion: apiVersion, + Resource: resource, + } + + d, _, err := v.authorizer.Authorize(ctx, attrs) + if err != nil { + return err + } + if d != authorizer.DecisionAllow { + if policyErr != nil { + return fmt.Errorf(`unable to get policy %s to determine minimum required permissions and user %v does not have "%v" permission for all groups, versions and resources`, binding.Spec.PolicyName, user, verb) + } + if gvParseErr != nil { + return fmt.Errorf(`unable to parse paramKind %v to determine minimum required permissions and user %v does not have "%v" permission for all groups, versions and resources`, policy.Spec.ParamKind, user, verb) + } + if gvrResolveErr != nil { + return fmt.Errorf(`unable to resolve paramKind %v to determine minimum required permissions and user %v does not have "%v" permission for all groups, versions and resources`, policy.Spec.ParamKind, user, verb) + } + return fmt.Errorf(`user %v does not have "%v" permission on the object referenced by paramRef`, verb, user) + } + + return nil +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/authz_test.go b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/authz_test.go new file mode 100644 index 00000000000..b8229782d13 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/authz_test.go @@ -0,0 +1,233 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mutatingadmissionpolicybinding + +import ( + "context" + "errors" + "fmt" + "strings" + "testing" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" +) + +func TestAuthorization(t *testing.T) { + for _, tc := range []struct { + name string + userInfo user.Info + auth AuthFunc + policyGetter PolicyGetterFunc + resourceResolver resolver.ResourceResolverFunc + expectErrContains string + }{ + { + name: "superuser", // success despite always-denying authorizer + userInfo: &user.DefaultInfo{Groups: []string{user.SystemPrivilegedGroup}}, + auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + return authorizer.DecisionDeny, "", nil + }, + }, + { + name: "authorized", + userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}}, + auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + if a.GetResource() == "configmaps" { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "", nil + }, + policyGetter: func(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) { + return &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "replicalimit-policy.example.com"}, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ParamKind: &admissionregistration.ParamKind{Kind: "ConfigMap", APIVersion: "v1"}, + }, + }, nil + }, + resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "configmaps", + }, nil + }, + }, + { + name: "denied", + userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}}, + auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + if a.GetResource() == "configmaps" { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "", nil + }, + policyGetter: func(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) { + return &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "replicalimit-policy.example.com"}, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ParamKind: &admissionregistration.ParamKind{Kind: "Params", APIVersion: "foo.example.com/v1"}, + }, + }, nil + }, + resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: "foo.example.com", + Version: "v1", + Resource: "params", + }, nil + }, + expectErrContains: "permission on the object referenced by paramRef", + }, + { + name: "unable to parse paramRef", + userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}}, + auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + if a.GetResource() == "configmaps" { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "", nil + }, + policyGetter: func(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) { + return &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "replicalimit-policy.example.com"}, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ParamKind: &admissionregistration.ParamKind{Kind: "Params", APIVersion: "foo.example.com/v1"}, + }, + }, nil + }, + resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "configmaps", + }, nil + }, + expectErrContains: "unable to parse paramKind &{foo.example.com/v1 Params} to determine minimum required permissions", + }, + { + name: "unable to resolve param", + userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}}, + auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + if a.GetResource() == "configmaps" { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "", nil + }, + policyGetter: func(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) { + return &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "replicalimit-policy.example.com"}, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ParamKind: &admissionregistration.ParamKind{Kind: "Params", APIVersion: "foo.example.com/v1"}, + }, + }, nil + }, + resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{}, &meta.NoKindMatchError{GroupKind: gvk.GroupKind(), SearchedVersions: []string{gvk.Version}} + }, + expectErrContains: "unable to resolve paramKind &{foo.example.com/v1 Params} to determine minimum required permissions", + }, + { + name: "unable to get policy", + userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}}, + auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + if a.GetResource() == "configmaps" { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "", nil + }, + policyGetter: func(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) { + return nil, fmt.Errorf("no such policy") + }, + resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "configmaps", + }, nil + }, + expectErrContains: "unable to get policy replicalimit-policy.example.com to determine minimum required permissions", + }, + } { + t.Run(tc.name, func(t *testing.T) { + strategy := NewStrategy(tc.auth, tc.policyGetter, tc.resourceResolver) + t.Run("create", func(t *testing.T) { + ctx := request.WithUser(context.Background(), tc.userInfo) + for _, obj := range validPolicyBindings() { + errs := strategy.Validate(ctx, obj) + if len(errs) > 0 && !strings.Contains(errors.Join(errs.ToAggregate().Errors()...).Error(), tc.expectErrContains) { + t.Errorf("expected error to contain: %v but got error: %v", tc.expectErrContains, errs) + } + } + }) + t.Run("update", func(t *testing.T) { + ctx := request.WithUser(context.Background(), tc.userInfo) + for _, obj := range validPolicyBindings() { + objWithChangedParamRef := obj.DeepCopy() + if pr := objWithChangedParamRef.Spec.ParamRef; pr != nil { + if len(pr.Name) > 0 { + pr.Name = "changed" + } + + if pr.Selector != nil { + pr.Selector = &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "changed": "value", + }, + } + } + + if len(pr.Namespace) > 0 { + pr.Namespace = "othernamespace" + } + + if pr.ParameterNotFoundAction == nil || *pr.ParameterNotFoundAction == admissionregistration.AllowAction { + v := admissionregistration.DenyAction + pr.ParameterNotFoundAction = &v + } else { + v := admissionregistration.AllowAction + pr.ParameterNotFoundAction = &v + } + } + errs := strategy.ValidateUpdate(ctx, obj, objWithChangedParamRef) + if len(errs) > 0 && !strings.Contains(errors.Join(errs.ToAggregate().Errors()...).Error(), tc.expectErrContains) { + t.Errorf("expected error to contain: %v but got error: %v", tc.expectErrContains, errs) + } + } + }) + }) + } +} + +type AuthFunc func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) + +func (f AuthFunc) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + return f(ctx, a) +} + +type PolicyGetterFunc func(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) + +func (f PolicyGetterFunc) GetMutatingAdmissionPolicy(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) { + return f(ctx, name) +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/doc.go b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/doc.go new file mode 100644 index 00000000000..b19a2185a08 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/doc.go @@ -0,0 +1,17 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mutatingadmissionpolicybinding // import "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingadmissionpolicybinding" diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/storage/storage.go b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/storage/storage.go new file mode 100644 index 00000000000..146ae24b146 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/storage/storage.go @@ -0,0 +1,94 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package storage + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/registry/generic" + genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/printers" + printersinternal "k8s.io/kubernetes/pkg/printers/internalversion" + printerstorage "k8s.io/kubernetes/pkg/printers/storage" + mutatingadmissionpolicybinding "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingadmissionpolicybinding" + "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" +) + +// REST implements a RESTStorage for policyBinding against etcd +type REST struct { + *genericregistry.Store +} + +var groupResource = admissionregistration.Resource("mutatingadmissionpolicybindings") + +// NewREST returns a RESTStorage object that will work against policyBinding. +func NewREST(optsGetter generic.RESTOptionsGetter, authorizer authorizer.Authorizer, policyGetter PolicyGetter, resourceResolver resolver.ResourceResolver) (*REST, error) { + r := &REST{} + strategy := mutatingadmissionpolicybinding.NewStrategy(authorizer, policyGetter, resourceResolver) + store := &genericregistry.Store{ + NewFunc: func() runtime.Object { return &admissionregistration.MutatingAdmissionPolicyBinding{} }, + NewListFunc: func() runtime.Object { return &admissionregistration.MutatingAdmissionPolicyBindingList{} }, + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*admissionregistration.MutatingAdmissionPolicyBinding).Name, nil + }, + DefaultQualifiedResource: groupResource, + SingularQualifiedResource: admissionregistration.Resource("mutatingadmissionpolicybinding"), + + CreateStrategy: strategy, + UpdateStrategy: strategy, + DeleteStrategy: strategy, + + TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)}, + } + options := &generic.StoreOptions{RESTOptions: optsGetter} + if err := store.CompleteWithOptions(options); err != nil { + return nil, err + } + r.Store = store + return r, nil +} + +// Implement CategoriesProvider +var _ rest.CategoriesProvider = &REST{} + +// Categories implements the CategoriesProvider interface. Returns a list of categories a resource is part of. +func (r *REST) Categories() []string { + return []string{"api-extensions"} +} + +type PolicyGetter interface { + // GetMutatingAdmissionPolicy returns a GetMutatingAdmissionPolicy + // by its name. There is no namespace because it is cluster-scoped. + GetMutatingAdmissionPolicy(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) +} + +type DefaultPolicyGetter struct { + Getter rest.Getter +} + +func (g *DefaultPolicyGetter) GetMutatingAdmissionPolicy(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) { + p, err := g.Getter.Get(ctx, name, &metav1.GetOptions{}) + if err != nil { + return nil, err + } + return p.(*admissionregistration.MutatingAdmissionPolicy), err +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/storage/storage_test.go b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/storage/storage_test.go new file mode 100644 index 00000000000..1d406059884 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/storage/storage_test.go @@ -0,0 +1,260 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package storage + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/registry/generic" + genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing" + etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing" + + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" + "k8s.io/kubernetes/pkg/registry/registrytest" + + // Ensure that admissionregistration package is initialized. + _ "k8s.io/kubernetes/pkg/apis/admissionregistration/install" +) + +func TestCreate(t *testing.T) { + for _, configuration := range validPolicyBindings() { + t.Run(configuration.Name, func(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + + test.TestCreate( + // valid + configuration, + // invalid + newPolicyBinding(""), + ) + }) + } +} + +func TestUpdate(t *testing.T) { + for _, b := range validPolicyBindings() { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + t.Run(b.Name, func(t *testing.T) { + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestUpdate( + // valid + b, + // updateFunc + func(obj runtime.Object) runtime.Object { + object := obj.(*admissionregistration.MutatingAdmissionPolicyBinding) + object.Labels = map[string]string{"c": "d"} + return object + }, + // invalid updateFunc + func(obj runtime.Object) runtime.Object { + object := obj.(*admissionregistration.MutatingAdmissionPolicyBinding) + object.Name = "" + return object + }, + ) + }) + } +} + +func TestGet(t *testing.T) { + for _, b := range validPolicyBindings() { + t.Run(b.Name, func(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestGet(b) + }) + } +} + +func TestList(t *testing.T) { + for _, b := range validPolicyBindings() { + t.Run(b.Name, func(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestList(b) + }) + } +} + +func TestDelete(t *testing.T) { + for _, b := range validPolicyBindings() { + t.Run(b.Name, func(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestDelete(b) + }) + } +} + +func TestWatch(t *testing.T) { + for _, b := range validPolicyBindings() { + t.Run(b.Name, func(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestWatch( + b, + []labels.Set{}, + []labels.Set{ + {"hoo": "bar"}, + }, + []fields.Set{ + {"metadata.name": b.Name}, + }, + []fields.Set{ + {"metadata.name": "nomatch"}, + }, + ) + }) + } +} + +func validPolicyBindings() []*admissionregistration.MutatingAdmissionPolicyBinding { + denyAction := admissionregistration.DenyAction + return []*admissionregistration.MutatingAdmissionPolicyBinding{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "replicalimit-policy.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "replica-limit-test.example.com", + ParameterNotFoundAction: &denyAction, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-clusterwide", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "replicalimit-policy.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "replica-limit-test.example.com", + Namespace: "default", + ParameterNotFoundAction: &denyAction, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-selector", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "replicalimit-policy.example.com", + ParamRef: &admissionregistration.ParamRef{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "label": "value", + }, + }, + ParameterNotFoundAction: &denyAction, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-selector-clusterwide", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "replicalimit-policy.example.com", + ParamRef: &admissionregistration.ParamRef{ + Namespace: "mynamespace", + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "label": "value", + }, + }, + ParameterNotFoundAction: &denyAction, + }, + }, + }, + } +} + +func newPolicyBinding(name string) *admissionregistration.MutatingAdmissionPolicyBinding { + return &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{"foo": "bar"}, + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "replicalimit-policy.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "param-test", + Namespace: "default", + }, + MatchResources: &admissionregistration.MatchResources{}, + }, + } +} + +func newInsecureStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) { + return newStorage(t, nil, nil, replicaLimitsResolver) +} + +func newStorage(t *testing.T, authorizer authorizer.Authorizer, policyGetter PolicyGetter, resourceResolver resolver.ResourceResolver) (*REST, *etcd3testing.EtcdTestServer) { + etcdStorage, server := registrytest.NewEtcdStorageForResource(t, admissionregistration.Resource("mutatingadmissionpolicybindings")) + restOptions := generic.RESTOptions{ + StorageConfig: etcdStorage, + Decorator: generic.UndecoratedStorage, + DeleteCollectionWorkers: 1, + ResourcePrefix: "mutatingadmissionpolicybindings"} + storage, err := NewREST(restOptions, authorizer, policyGetter, resourceResolver) + if err != nil { + t.Fatalf("unexpected error from REST storage: %v", err) + } + return storage, server +} + +func TestCategories(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + expected := []string{"api-extensions"} + registrytest.AssertCategories(t, storage, expected) +} + +var replicaLimitsResolver resolver.ResourceResolverFunc = func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: "rules.example.com", + Version: "v1", + Resource: "replicalimits", + }, nil +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/strategy.go b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/strategy.go new file mode 100644 index 00000000000..16be56cbc8a --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/strategy.go @@ -0,0 +1,130 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mutatingadmissionpolicybinding + +import ( + "context" + + apiequality "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/storage/names" + "k8s.io/kubernetes/pkg/api/legacyscheme" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/apis/admissionregistration/validation" + "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" +) + +// MutatingAdmissionPolicyBindingStrategy implements verification logic for MutatingAdmissionPolicyBinding. +type mutatingAdmissionPolicyBindingStrategy struct { + runtime.ObjectTyper + names.NameGenerator + authorizer authorizer.Authorizer + policyGetter PolicyGetter + resourceResolver resolver.ResourceResolver +} + +type PolicyGetter interface { + // GetMutatingAdmissionPolicy returns a GetMutatingAdmissionPolicy + // by its name. There is no namespace because it is cluster-scoped. + GetMutatingAdmissionPolicy(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) +} + +// NewStrategy is the default logic that applies when creating and updating MutatingAdmissionPolicyBinding objects. +func NewStrategy(authorizer authorizer.Authorizer, policyGetter PolicyGetter, resourceResolver resolver.ResourceResolver) *mutatingAdmissionPolicyBindingStrategy { + return &mutatingAdmissionPolicyBindingStrategy{ + ObjectTyper: legacyscheme.Scheme, + NameGenerator: names.SimpleNameGenerator, + authorizer: authorizer, + policyGetter: policyGetter, + resourceResolver: resourceResolver, + } +} + +// NamespaceScoped returns false because MutatingAdmissionPolicyBinding is cluster-scoped resource. +func (v *mutatingAdmissionPolicyBindingStrategy) NamespaceScoped() bool { + return false +} + +// PrepareForCreate clears the status of an MutatingAdmissionPolicyBinding before creation. +func (v *mutatingAdmissionPolicyBindingStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { + ic := obj.(*admissionregistration.MutatingAdmissionPolicyBinding) + ic.Generation = 1 +} + +// PrepareForUpdate clears fields that are not allowed to be set by end users on update. +func (v *mutatingAdmissionPolicyBindingStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { + newIC := obj.(*admissionregistration.MutatingAdmissionPolicyBinding) + oldIC := old.(*admissionregistration.MutatingAdmissionPolicyBinding) + + // Any changes to the spec increment the generation number, any changes to the + // status should reflect the generation number of the corresponding object. + // See metav1.ObjectMeta description for more information on Generation. + if !apiequality.Semantic.DeepEqual(oldIC.Spec, newIC.Spec) { + newIC.Generation = oldIC.Generation + 1 + } +} + +// Validate validates a new MutatingAdmissionPolicyBinding. +func (v *mutatingAdmissionPolicyBindingStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { + errs := validation.ValidateMutatingAdmissionPolicyBinding(obj.(*admissionregistration.MutatingAdmissionPolicyBinding)) + if len(errs) == 0 { + // if the object is well-formed, also authorize the paramRef + if err := v.authorizeCreate(ctx, obj); err != nil { + errs = append(errs, field.Forbidden(field.NewPath("spec", "paramRef"), err.Error())) + } + } + return errs +} + +// WarningsOnCreate returns warnings for the creation of the given object. +func (v *mutatingAdmissionPolicyBindingStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { + return nil +} + +// Canonicalize normalizes the object after validation. +func (v *mutatingAdmissionPolicyBindingStrategy) Canonicalize(obj runtime.Object) { +} + +// AllowCreateOnUpdate is false for MutatingAdmissionPolicyBinding; this means you may not create one with a PUT request. +func (v *mutatingAdmissionPolicyBindingStrategy) AllowCreateOnUpdate() bool { + return false +} + +// ValidateUpdate is the default update validation for an end user. +func (v *mutatingAdmissionPolicyBindingStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { + errs := validation.ValidateMutatingAdmissionPolicyBindingUpdate(obj.(*admissionregistration.MutatingAdmissionPolicyBinding), old.(*admissionregistration.MutatingAdmissionPolicyBinding)) + if len(errs) == 0 { + // if the object is well-formed, also authorize the paramRef + if err := v.authorizeUpdate(ctx, obj, old); err != nil { + errs = append(errs, field.Forbidden(field.NewPath("spec", "paramRef"), err.Error())) + } + } + return errs +} + +// WarningsOnUpdate returns warnings for the given update. +func (v *mutatingAdmissionPolicyBindingStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { + return nil +} + +// AllowUnconditionalUpdate is the default update policy for MutatingAdmissionPolicyBinding objects. Status update should +// only be allowed if version match. +func (v *mutatingAdmissionPolicyBindingStrategy) AllowUnconditionalUpdate() bool { + return false +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/strategy_test.go b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/strategy_test.go new file mode 100644 index 00000000000..ddc2f010c20 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/strategy_test.go @@ -0,0 +1,127 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mutatingadmissionpolicybinding + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" + + "k8s.io/kubernetes/pkg/apis/admissionregistration" +) + +func TestPolicyBindingStrategy(t *testing.T) { + strategy := NewStrategy(nil, nil, replicaLimitsResolver) + ctx := genericapirequest.NewDefaultContext() + if strategy.NamespaceScoped() { + t.Error("PolicyBinding strategy must be cluster scoped") + } + if strategy.AllowCreateOnUpdate() { + t.Errorf("PolicyBinding should not allow create on update") + } + + for _, configuration := range validPolicyBindings() { + strategy.PrepareForCreate(ctx, configuration) + errs := strategy.Validate(ctx, configuration) + if len(errs) != 0 { + t.Errorf("Unexpected error validating %v", errs) + } + invalidConfiguration := &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{Name: ""}, + } + strategy.PrepareForUpdate(ctx, invalidConfiguration, configuration) + errs = strategy.ValidateUpdate(ctx, invalidConfiguration, configuration) + if len(errs) == 0 { + t.Errorf("Expected a validation error") + } + } +} + +var replicaLimitsResolver resolver.ResourceResolverFunc = func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: "rules.example.com", + Version: "v1", + Resource: "replicalimits", + }, nil +} + +func validPolicyBindings() []*admissionregistration.MutatingAdmissionPolicyBinding { + denyAction := admissionregistration.DenyAction + return []*admissionregistration.MutatingAdmissionPolicyBinding{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "replicalimit-policy.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "replica-limit-test.example.com", + ParameterNotFoundAction: &denyAction, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-clusterwide", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "replicalimit-policy.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "replica-limit-test.example.com", + Namespace: "default", + ParameterNotFoundAction: &denyAction, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-selector", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "replicalimit-policy.example.com", + ParamRef: &admissionregistration.ParamRef{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "label": "value", + }, + }, + ParameterNotFoundAction: &denyAction, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-selector-clusterwide", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "replicalimit-policy.example.com", + ParamRef: &admissionregistration.ParamRef{ + Namespace: "mynamespace", + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "label": "value", + }, + }, + ParameterNotFoundAction: &denyAction, + }, + }, + }, + } +} diff --git a/pkg/registry/admissionregistration/rest/storage_apiserver.go b/pkg/registry/admissionregistration/rest/storage_apiserver.go index 57922cda656..991dc507586 100644 --- a/pkg/registry/admissionregistration/rest/storage_apiserver.go +++ b/pkg/registry/admissionregistration/rest/storage_apiserver.go @@ -28,6 +28,8 @@ import ( "k8s.io/client-go/discovery" "k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/apis/admissionregistration" + mutatingadmissionpolicystorage "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingadmissionpolicy/storage" + mutationpolicybindingstorage "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/storage" mutatingwebhookconfigurationstorage "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingwebhookconfiguration/storage" "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" validatingadmissionpolicystorage "k8s.io/kubernetes/pkg/registry/admissionregistration/validatingadmissionpolicy/storage" @@ -148,6 +150,25 @@ func (p RESTStorageProvider) v1alpha1Storage(apiResourceConfigSource serverstora storage[resource] = policyBindingStorage } + // mutatingadmissionpolicies + if resource := "mutatingadmissionpolicies"; apiResourceConfigSource.ResourceEnabled(admissionregistrationv1alpha1.SchemeGroupVersion.WithResource(resource)) { + policyStorage, err := mutatingadmissionpolicystorage.NewREST(restOptionsGetter, p.Authorizer, r) + if err != nil { + return storage, err + } + policyGetter = policyStorage + storage[resource] = policyStorage + } + + // mutatingadmissionpolicybindings + if resource := "mutatingadmissionpolicybindings"; apiResourceConfigSource.ResourceEnabled(admissionregistrationv1alpha1.SchemeGroupVersion.WithResource(resource)) { + mutationpolicybindingstorage, err := mutationpolicybindingstorage.NewREST(restOptionsGetter, p.Authorizer, &mutationpolicybindingstorage.DefaultPolicyGetter{Getter: policyGetter}, r) + if err != nil { + return storage, err + } + storage[resource] = mutationpolicybindingstorage + } + return storage, nil } diff --git a/staging/src/k8s.io/api/admissionregistration/v1alpha1/register.go b/staging/src/k8s.io/api/admissionregistration/v1alpha1/register.go index d4c2fbe807f..eead376cc7d 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1alpha1/register.go +++ b/staging/src/k8s.io/api/admissionregistration/v1alpha1/register.go @@ -50,6 +50,10 @@ func addKnownTypes(scheme *runtime.Scheme) error { &ValidatingAdmissionPolicyList{}, &ValidatingAdmissionPolicyBinding{}, &ValidatingAdmissionPolicyBindingList{}, + &MutatingAdmissionPolicy{}, + &MutatingAdmissionPolicyList{}, + &MutatingAdmissionPolicyBinding{}, + &MutatingAdmissionPolicyBindingList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go b/staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go index 78d918bc72f..ee50fbe2d4c 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go +++ b/staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go @@ -663,3 +663,346 @@ const ( Delete OperationType = v1.Delete Connect OperationType = v1.Connect ) + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.32 + +// MutatingAdmissionPolicy describes the definition of an admission mutation policy that mutates the object coming into admission chain. +type MutatingAdmissionPolicy struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Specification of the desired behavior of the MutatingAdmissionPolicy. + Spec MutatingAdmissionPolicySpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.32 + +// MutatingAdmissionPolicyList is a list of MutatingAdmissionPolicy. +type MutatingAdmissionPolicyList struct { + metav1.TypeMeta `json:",inline"` + // Standard list metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // List of ValidatingAdmissionPolicy. + Items []MutatingAdmissionPolicy `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// MutatingAdmissionPolicySpec is the specification of the desired behavior of the admission policy. +type MutatingAdmissionPolicySpec struct { + // paramKind specifies the kind of resources used to parameterize this policy. + // If absent, there are no parameters for this policy and the param CEL variable will not be provided to validation expressions. + // If paramKind refers to a non-existent kind, this policy definition is mis-configured and the FailurePolicy is applied. + // If paramKind is specified but paramRef is unset in MutatingAdmissionPolicyBinding, the params variable will be null. + // +optional + ParamKind *ParamKind `json:"paramKind,omitempty" protobuf:"bytes,1,rep,name=paramKind"` + + // matchConstraints specifies what resources this policy is designed to validate. + // The MutatingAdmissionPolicy cares about a request if it matches _all_ Constraints. + // However, in order to prevent clusters from being put into an unstable state that cannot be recovered from via the API + // MutatingAdmissionPolicy cannot match MutatingAdmissionPolicy and MutatingAdmissionPolicyBinding. + // The CREATE, UPDATE and CONNECT operations are allowed. The DELETE operation may not be matched. + // '*' matches CREATE, UPDATE and CONNECT. + // Required. + MatchConstraints *MatchResources `json:"matchConstraints,omitempty" protobuf:"bytes,2,rep,name=matchConstraints"` + + // variables contain definitions of variables that can be used in composition of other expressions. + // Each variable is defined as a named CEL expression. + // The variables defined here will be available under `variables` in other expressions of the policy + // except matchConditions because matchConditions are evaluated before the rest of the policy. + // + // The expression of a variable can refer to other variables defined earlier in the list but not those after. + // Thus, variables must be sorted by the order of first appearance and acyclic. + // +listType=atomic + // +optional + Variables []Variable `json:"variables,omitempty" protobuf:"bytes,3,rep,name=variables"` + + // mutations contain operations to perform on matching objects. + // mutations may not be empty; a minimum of one mutation is required. + // mutations are evaluated in order, and are reinvoked according to + // the reinvocationPolicy. + // The mutations of a policy are invoked for each binding of this policy + // and reinvocation of mutations occurs on a per binding basis. + // + // +listType=atomic + // +optional + Mutations []Mutation `json:"mutations,omitempty" protobuf:"bytes,4,rep,name=mutations"` + + // failurePolicy defines how to handle failures for the admission policy. Failures can + // occur from CEL expression parse errors, type check errors, runtime errors and invalid + // or mis-configured policy definitions or bindings. + // + // A policy is invalid if paramKind refers to a non-existent Kind. + // A binding is invalid if paramRef.name refers to a non-existent resource. + // + // failurePolicy does not define how validations that evaluate to false are handled. + // + // Allowed values are Ignore or Fail. Defaults to Fail. + // +optional + FailurePolicy *FailurePolicyType `json:"failurePolicy,omitempty" protobuf:"bytes,5,opt,name=failurePolicy,casttype=FailurePolicyType"` + + // matchConditions is a list of conditions that must be met for a request to be validated. + // Match conditions filter requests that have already been matched by the matchConstraints. + // An empty list of matchConditions matches all requests. + // There are a maximum of 64 match conditions allowed. + // + // If a parameter object is provided, it can be accessed via the `params` handle in the same + // manner as validation expressions. + // + // The exact matching logic is (in order): + // 1. If ANY matchCondition evaluates to FALSE, the policy is skipped. + // 2. If ALL matchConditions evaluate to TRUE, the policy is evaluated. + // 3. If any matchCondition evaluates to an error (but none are FALSE): + // - If failurePolicy=Fail, reject the request + // - If failurePolicy=Ignore, the policy is skipped + // + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +optional + MatchConditions []MatchCondition `json:"matchConditions,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,6,rep,name=matchConditions"` + + // reinvocationPolicy indicates whether mutations may be called multiple times per MutatingAdmissionPolicyBinding + // as part of a single admission evaluation. + // Allowed values are "Never" and "IfNeeded". + // + // Never: These mutations will not be called more than once per binding in a single admission evaluation. + // + // IfNeeded: These mutations may be invoked more than once per binding for a single admission request and there is no guarantee of + // order with respect to other admission plugins, admission webhooks, bindings of this policy and admission policies. Mutations are only + // reinvoked when mutations change the object after this mutation is invoked. + // Required. + ReinvocationPolicy ReinvocationPolicyType `json:"reinvocationPolicy,omitempty" protobuf:"bytes,7,opt,name=reinvocationPolicy,casttype=ReinvocationPolicyType"` +} + +// Mutation specifies the CEL expression which is used to apply the Mutation. +type Mutation struct { + // patchType indicates the patch strategy used. + // Allowed values are "ApplyConfiguration" and "JSONPatch". + // Required. + // + // +unionDiscriminator + PatchType PatchType `json:"patchType" protobuf:"bytes,2,opt,name=patchType,casttype=PatchType"` + + // applyConfiguration defines the desired configuration values of an object. + // The configuration is applied to the admission object using + // [structured merge diff](https://github.com/kubernetes-sigs/structured-merge-diff). + // A CEL expression is used to create apply configuration. + ApplyConfiguration *ApplyConfiguration `json:"applyConfiguration,omitempty" protobuf:"bytes,3,opt,name=applyConfiguration"` + + // jsonPatch defines a [JSON patch](https://jsonpatch.com/) operation to perform a mutation to the object. + // A CEL expression is used to create the JSON patch. + JSONPatch *JSONPatch `json:"jsonPatch,omitempty" protobuf:"bytes,4,opt,name=jsonPatch"` +} + +// PatchType specifies the type of patch operation for a mutation. +// +enum +type PatchType string + +const ( + // ApplyConfiguration indicates that the mutation is using apply configuration to mutate the object. + PatchTypeApplyConfiguration PatchType = "ApplyConfiguration" + // JSONPatch indicates that the object is mutated through JSON Patch. + PatchTypeJSONPatch PatchType = "JSONPatch" +) + +// ApplyConfiguration defines the desired configuration values of an object. +type ApplyConfiguration struct { + // expression will be evaluated by CEL to create an apply configuration. + // ref: https://github.com/google/cel-spec + // + // Apply configurations are declared in CEL using object initialization. For example, this CEL expression + // returns an apply configuration to set a single field: + // + // Object{ + // spec: Object.spec{ + // serviceAccountName: "example" + // } + // } + // + // Apply configurations may not modify atomic structs, maps or arrays due to the risk of accidental deletion of + // values not included in the apply configuration. + // + // CEL expressions have access to the object types needed to create apply configurations: + // + // - 'Object' - CEL type of the resource object. + // - 'Object.' - CEL type of object field (such as 'Object.spec') + // - 'Object.....` - CEL type of nested field (such as 'Object.spec.containers') + // + // CEL expressions have access to the contents of the API request, organized into CEL variables as well as some other useful variables: + // + // - 'object' - The object from the incoming request. The value is null for DELETE requests. + // - 'oldObject' - The existing object. The value is null for CREATE requests. + // - 'request' - Attributes of the API request([ref](/pkg/apis/admission/types.go#AdmissionRequest)). + // - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind. + // - 'namespaceObject' - The namespace object that the incoming object belongs to. The value is null for cluster-scoped resources. + // - 'variables' - Map of composited variables, from its name to its lazily evaluated value. + // For example, a variable named 'foo' can be accessed as 'variables.foo'. + // - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request. + // See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz + // - 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the + // request resource. + // + // The `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the + // object. No other metadata properties are accessible. + // + // Only property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. + // Required. + Expression string `json:"expression,omitempty" protobuf:"bytes,1,opt,name=expression"` +} + +// JSONPatch defines a JSON Patch. +type JSONPatch struct { + // expression will be evaluated by CEL to create a [JSON patch](https://jsonpatch.com/). + // ref: https://github.com/google/cel-spec + // + // expression must return an array of JSONPatch values. + // + // For example, this CEL expression returns a JSON patch to conditionally modify a value: + // + // [ + // JSONPatch{op: "test", path: "/spec/example", value: "Red"}, + // JSONPatch{op: "replace", path: "/spec/example", value: "Green"} + // ] + // + // To define an object for the patch value, use Object types. For example: + // + // [ + // JSONPatch{ + // op: "add", + // path: "/spec/selector", + // value: Object.spec.selector{matchLabels: {"environment": "test"}} + // } + // ] + // + // To use strings containing '/' and '~' as JSONPatch path keys, use "jsonpatch.escapeKey". For example: + // + // [ + // JSONPatch{ + // op: "add", + // path: "/metadata/labels/" + jsonpatch.escapeKey("example.com/environment"), + // value: "test" + // }, + // ] + // + // CEL expressions have access to the types needed to create JSON patches and objects: + // + // - 'JSONPatch' - CEL type of JSON Patch operations. JSONPatch has the fields 'op', 'from', 'path' and 'value'. + // See [JSON patch](https://jsonpatch.com/) for more details. The 'value' field may be set to any of: string, + // integer, array, map or object. If set, the 'path' and 'from' fields must be set to a + // [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901/) string, where the 'jsonpatch.escapeKey()' CEL + // function may be used to escape path keys containing '/' and '~'. + // - 'Object' - CEL type of the resource object. + // - 'Object.' - CEL type of object field (such as 'Object.spec') + // - 'Object.....` - CEL type of nested field (such as 'Object.spec.containers') + // + // CEL expressions have access to the contents of the API request, organized into CEL variables as well as some other useful variables: + // + // - 'object' - The object from the incoming request. The value is null for DELETE requests. + // - 'oldObject' - The existing object. The value is null for CREATE requests. + // - 'request' - Attributes of the API request([ref](/pkg/apis/admission/types.go#AdmissionRequest)). + // - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind. + // - 'namespaceObject' - The namespace object that the incoming object belongs to. The value is null for cluster-scoped resources. + // - 'variables' - Map of composited variables, from its name to its lazily evaluated value. + // For example, a variable named 'foo' can be accessed as 'variables.foo'. + // - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request. + // See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz + // - 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the + // request resource. + // + // CEL expressions have access to [Kubernetes CEL function libraries](https://kubernetes.io/docs/reference/using-api/cel/#cel-options-language-features-and-libraries) + // as well as: + // + // - 'jsonpatch.escapeKey' - Performs JSONPatch key escaping. '~' and '/' are escaped as '~0' and `~1' respectively). + // + // + // Only property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. + // Required. + Expression string `json:"expression,omitempty" protobuf:"bytes,1,opt,name=expression"` +} + +// ReinvocationPolicyType specifies what type of policy the admission mutation uses. +// +enum +type ReinvocationPolicyType = v1.ReinvocationPolicyType + +const ( + // NeverReinvocationPolicy indicates that the mutation must not be called more than once in a + // single admission evaluation. + NeverReinvocationPolicy ReinvocationPolicyType = v1.NeverReinvocationPolicy + // IfNeededReinvocationPolicy indicates that the mutation may be called at least one + // additional time as part of the admission evaluation if the object being admitted is + // modified by other admission plugins after the initial mutation call. + IfNeededReinvocationPolicy ReinvocationPolicyType = v1.IfNeededReinvocationPolicy +) + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.32 + +// MutatingAdmissionPolicyBinding binds the MutatingAdmissionPolicy with parametrized resources. +// MutatingAdmissionPolicyBinding and the optional parameter resource together define how cluster administrators +// configure policies for clusters. +// +// For a given admission request, each binding will cause its policy to be +// evaluated N times, where N is 1 for policies/bindings that don't use +// params, otherwise N is the number of parameters selected by the binding. +// Each evaluation is constrained by a [runtime cost budget](https://kubernetes.io/docs/reference/using-api/cel/#runtime-cost-budget). +// +// Adding/removing policies, bindings, or params can not affect whether a +// given (policy, binding, param) combination is within its own CEL budget. +type MutatingAdmissionPolicyBinding struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Specification of the desired behavior of the MutatingAdmissionPolicyBinding. + Spec MutatingAdmissionPolicyBindingSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.32 + +// MutatingAdmissionPolicyBindingList is a list of MutatingAdmissionPolicyBinding. +type MutatingAdmissionPolicyBindingList struct { + metav1.TypeMeta `json:",inline"` + // Standard list metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // List of PolicyBinding. + Items []MutatingAdmissionPolicyBinding `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// MutatingAdmissionPolicyBindingSpec is the specification of the MutatingAdmissionPolicyBinding. +type MutatingAdmissionPolicyBindingSpec struct { + // policyName references a MutatingAdmissionPolicy name which the MutatingAdmissionPolicyBinding binds to. + // If the referenced resource does not exist, this binding is considered invalid and will be ignored + // Required. + PolicyName string `json:"policyName,omitempty" protobuf:"bytes,1,rep,name=policyName"` + + // paramRef specifies the parameter resource used to configure the admission control policy. + // It should point to a resource of the type specified in spec.ParamKind of the bound MutatingAdmissionPolicy. + // If the policy specifies a ParamKind and the resource referred to by ParamRef does not exist, this binding is considered mis-configured and the FailurePolicy of the MutatingAdmissionPolicy applied. + // If the policy does not specify a ParamKind then this field is ignored, and the rules are evaluated without a param. + // +optional + ParamRef *ParamRef `json:"paramRef,omitempty" protobuf:"bytes,2,rep,name=paramRef"` + + // matchResources limits what resources match this binding and may be mutated by it. + // Note that if matchResources matches a resource, the resource must also match a policy's matchConstraints and + // matchConditions before the resource may be mutated. + // When matchResources is unset, it does not constrain resource matching, and only the policy's matchConstraints + // and matchConditions must match for the resource to be mutated. + // Additionally, matchResources.resourceRules are optional and do not constraint matching when unset. + // Note that this is differs from MutatingAdmissionPolicy matchConstraints, where resourceRules are required. + // The CREATE, UPDATE and CONNECT operations are allowed. The DELETE operation may not be matched. + // '*' matches CREATE, UPDATE and CONNECT. + // +optional + MatchResources *MatchResources `json:"matchResources,omitempty" protobuf:"bytes,3,rep,name=matchResources"` +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch_test.go new file mode 100644 index 00000000000..58c4a55b838 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch_test.go @@ -0,0 +1 @@ +package patch diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules/rules.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules/rules.go index b926f65dc10..10bef0a8f69 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules/rules.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules/rules.go @@ -121,7 +121,7 @@ func (r *Matcher) resource() bool { func IsExemptAdmissionConfigurationResource(attr admission.Attributes) bool { gvk := attr.GetKind() if gvk.Group == "admissionregistration.k8s.io" { - if gvk.Kind == "ValidatingWebhookConfiguration" || gvk.Kind == "MutatingWebhookConfiguration" || gvk.Kind == "ValidatingAdmissionPolicy" || gvk.Kind == "ValidatingAdmissionPolicyBinding" { + if gvk.Kind == "ValidatingWebhookConfiguration" || gvk.Kind == "MutatingWebhookConfiguration" || gvk.Kind == "ValidatingAdmissionPolicy" || gvk.Kind == "ValidatingAdmissionPolicyBinding" || gvk.Kind == "MutatingAdmissionPolicy" || gvk.Kind == "MutatingAdmissionPolicyBinding" { return true } } diff --git a/test/integration/apiserver/admissionwebhook/admission_test.go b/test/integration/apiserver/admissionwebhook/admission_test.go index a29fc0b5e1f..a211bc6adca 100644 --- a/test/integration/apiserver/admissionwebhook/admission_test.go +++ b/test/integration/apiserver/admissionwebhook/admission_test.go @@ -148,6 +148,8 @@ var ( gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicies"): true, gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicies/status"): true, gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicybindings"): true, + gvr("admissionregistration.k8s.io", "v1alpha1", "mutatingadmissionpolicies"): true, + gvr("admissionregistration.k8s.io", "v1alpha1", "mutatingadmissionpolicybindings"): true, } parentResources = map[schema.GroupVersionResource]schema.GroupVersionResource{ diff --git a/test/integration/apiserver/cel/admission_test_util.go b/test/integration/apiserver/cel/admission_test_util.go index 9917c61b0bb..9ac02c7ec9c 100644 --- a/test/integration/apiserver/cel/admission_test_util.go +++ b/test/integration/apiserver/cel/admission_test_util.go @@ -146,6 +146,8 @@ var ( gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicies"): true, gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicies/status"): true, gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicybindings"): true, + gvr("admissionregistration.k8s.io", "v1alpha1", "mutatingadmissionpolicies"): true, + gvr("admissionregistration.k8s.io", "v1alpha1", "mutatingadmissionpolicybindings"): true, // transient resource exemption gvr("authentication.k8s.io", "v1", "selfsubjectreviews"): true, gvr("authentication.k8s.io", "v1beta1", "selfsubjectreviews"): true, diff --git a/test/integration/controlplane/generic_test.go b/test/integration/controlplane/generic_test.go index 259f279c330..85f6e7bbb5b 100644 --- a/test/integration/controlplane/generic_test.go +++ b/test/integration/controlplane/generic_test.go @@ -103,6 +103,8 @@ func TestGenericControlplaneStartUp(t *testing.T) { "validatingadmissionpolicies.admissionregistration.k8s.io", "validatingadmissionpolicybindings.admissionregistration.k8s.io", "validatingwebhookconfigurations.admissionregistration.k8s.io", + "mutatingadmissionpolicies.admissionregistration.k8s.io", + "mutatingadmissionpolicybindings.admissionregistration.k8s.io", ) if diff := cmp.Diff(sets.List(expected), sets.List(grs)); diff != "" { t.Fatalf("unexpected API groups: +want, -got\n%s", diff) diff --git a/test/integration/etcd/data.go b/test/integration/etcd/data.go index ad1f71aabdb..01dad0df1ec 100644 --- a/test/integration/etcd/data.go +++ b/test/integration/etcd/data.go @@ -362,6 +362,14 @@ func GetEtcdStorageDataForNamespace(namespace string) map[schema.GroupVersionRes ExpectedEtcdPath: "/registry/validatingadmissionpolicybindings/pb1a1", ExpectedGVK: gvkP("admissionregistration.k8s.io", "v1", "ValidatingAdmissionPolicyBinding"), }, + gvr("admissionregistration.k8s.io", "v1alpha1", "mutatingadmissionpolicies"): { + Stub: `{"metadata":{"name":"map1","creationTimestamp":null},"spec":{"paramKind":{"apiVersion":"test.example.com/v1","kind":"Example"},"matchConstraints":{"resourceRules": [{"resourceNames": ["fakeName"], "apiGroups":["apps"],"apiVersions":["v1"],"operations":["CREATE", "UPDATE"], "resources":["deployments"]}]},"reinvocationPolicy": "IfNeeded","mutations":[{"applyConfiguration": {"expression":"Object{metadata: Object.metadata{labels: {'example':'true'}}}"}, "patchType":"ApplyConfiguration"}]}}`, + ExpectedEtcdPath: "/registry/mutatingadmissionpolicies/map1", + }, + gvr("admissionregistration.k8s.io", "v1alpha1", "mutatingadmissionpolicybindings"): { + Stub: `{"metadata":{"name":"mpb1","creationTimestamp":null},"spec":{"policyName":"replicalimit-policy.example.com","paramRef":{"name":"replica-limit-test.example.com"}}}`, + ExpectedEtcdPath: "/registry/mutatingadmissionpolicybindings/mpb1", + }, // -- // k8s.io/kubernetes/pkg/apis/scheduling/v1