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