Merge pull request #127134 from jpbetz/mutating-admission

KEP-3962: MutatingAdmissionPolicy Alpha
This commit is contained in:
Kubernetes Prow Robot
2024-11-05 17:31:38 +00:00
committed by GitHub
166 changed files with 23284 additions and 1815 deletions

View File

@@ -1441,6 +1441,52 @@
{
"freshness": "Current",
"resources": [
{
"categories": [
"api-extensions"
],
"resource": "mutatingadmissionpolicies",
"responseKind": {
"group": "",
"kind": "MutatingAdmissionPolicy",
"version": ""
},
"scope": "Cluster",
"singularResource": "mutatingadmissionpolicy",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
]
},
{
"categories": [
"api-extensions"
],
"resource": "mutatingadmissionpolicybindings",
"responseKind": {
"group": "",
"kind": "MutatingAdmissionPolicyBinding",
"version": ""
},
"scope": "Cluster",
"singularResource": "mutatingadmissionpolicybinding",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
]
},
{
"categories": [
"api-extensions"

View File

@@ -3,6 +3,46 @@
"groupVersion": "admissionregistration.k8s.io/v1alpha1",
"kind": "APIResourceList",
"resources": [
{
"categories": [
"api-extensions"
],
"kind": "MutatingAdmissionPolicy",
"name": "mutatingadmissionpolicies",
"namespaced": false,
"singularName": "mutatingadmissionpolicy",
"storageVersionHash": "lP2+lF8aHIY=",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
]
},
{
"categories": [
"api-extensions"
],
"kind": "MutatingAdmissionPolicyBinding",
"name": "mutatingadmissionpolicybindings",
"namespaced": false,
"singularName": "mutatingadmissionpolicybinding",
"storageVersionHash": "Q2Qe566oRi8=",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
]
},
{
"categories": [
"api-extensions"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2
go.mod
View File

@@ -29,6 +29,7 @@ require (
github.com/emicklei/go-restful/v3 v3.11.0
github.com/fsnotify/fsnotify v1.7.0
github.com/go-logr/logr v1.4.2
github.com/go-openapi/jsonreference v0.20.2
github.com/godbus/dbus/v5 v5.1.0
github.com/gogo/protobuf v1.3.2
github.com/golang/protobuf v1.5.4
@@ -149,7 +150,6 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect

View File

@@ -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"}: {},

View File

@@ -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
}
},
}
}

View File

@@ -55,6 +55,10 @@ func addKnownTypes(scheme *runtime.Scheme) error {
&ValidatingAdmissionPolicyList{},
&ValidatingAdmissionPolicyBinding{},
&ValidatingAdmissionPolicyBindingList{},
&MutatingAdmissionPolicy{},
&MutatingAdmissionPolicyList{},
&MutatingAdmissionPolicyBinding{},
&MutatingAdmissionPolicyBindingList{},
)
return nil
}

View File

@@ -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.<fieldName>' - CEL type of object field (such as 'Object.spec')
// - 'Object.<fieldName1>.<fieldName2>...<fieldNameN>` - 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.<fieldName>' - CEL type of object field (such as 'Object.spec')
// - 'Object.<fieldName1>.<fieldName2>...<fieldNameN>` - 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
}

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -24,12 +24,13 @@ package v1alpha1
import (
unsafe "unsafe"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
conversion "k8s.io/apimachinery/pkg/conversion"
runtime "k8s.io/apimachinery/pkg/runtime"
admissionregistration "k8s.io/kubernetes/pkg/apis/admissionregistration"
admissionregistrationv1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1"
apisadmissionregistrationv1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1"
)
func init() {
@@ -39,6 +40,16 @@ func init() {
// RegisterConversions adds conversion functions to the given scheme.
// Public to allow building arbitrary schemes.
func RegisterConversions(s *runtime.Scheme) error {
if err := s.AddGeneratedConversionFunc((*admissionregistrationv1alpha1.ApplyConfiguration)(nil), (*admissionregistration.ApplyConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_ApplyConfiguration_To_admissionregistration_ApplyConfiguration(a.(*admissionregistrationv1alpha1.ApplyConfiguration), b.(*admissionregistration.ApplyConfiguration), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*admissionregistration.ApplyConfiguration)(nil), (*admissionregistrationv1alpha1.ApplyConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_admissionregistration_ApplyConfiguration_To_v1alpha1_ApplyConfiguration(a.(*admissionregistration.ApplyConfiguration), b.(*admissionregistrationv1alpha1.ApplyConfiguration), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*admissionregistrationv1alpha1.AuditAnnotation)(nil), (*admissionregistration.AuditAnnotation)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_AuditAnnotation_To_admissionregistration_AuditAnnotation(a.(*admissionregistrationv1alpha1.AuditAnnotation), b.(*admissionregistration.AuditAnnotation), scope)
}); err != nil {
@@ -59,6 +70,16 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*admissionregistrationv1alpha1.JSONPatch)(nil), (*admissionregistration.JSONPatch)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_JSONPatch_To_admissionregistration_JSONPatch(a.(*admissionregistrationv1alpha1.JSONPatch), b.(*admissionregistration.JSONPatch), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*admissionregistration.JSONPatch)(nil), (*admissionregistrationv1alpha1.JSONPatch)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_admissionregistration_JSONPatch_To_v1alpha1_JSONPatch(a.(*admissionregistration.JSONPatch), b.(*admissionregistrationv1alpha1.JSONPatch), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*admissionregistrationv1alpha1.MatchCondition)(nil), (*admissionregistration.MatchCondition)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_MatchCondition_To_admissionregistration_MatchCondition(a.(*admissionregistrationv1alpha1.MatchCondition), b.(*admissionregistration.MatchCondition), scope)
}); err != nil {
@@ -79,6 +100,76 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*admissionregistrationv1alpha1.MutatingAdmissionPolicy)(nil), (*admissionregistration.MutatingAdmissionPolicy)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_MutatingAdmissionPolicy_To_admissionregistration_MutatingAdmissionPolicy(a.(*admissionregistrationv1alpha1.MutatingAdmissionPolicy), b.(*admissionregistration.MutatingAdmissionPolicy), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*admissionregistration.MutatingAdmissionPolicy)(nil), (*admissionregistrationv1alpha1.MutatingAdmissionPolicy)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_admissionregistration_MutatingAdmissionPolicy_To_v1alpha1_MutatingAdmissionPolicy(a.(*admissionregistration.MutatingAdmissionPolicy), b.(*admissionregistrationv1alpha1.MutatingAdmissionPolicy), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding)(nil), (*admissionregistration.MutatingAdmissionPolicyBinding)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_MutatingAdmissionPolicyBinding_To_admissionregistration_MutatingAdmissionPolicyBinding(a.(*admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding), b.(*admissionregistration.MutatingAdmissionPolicyBinding), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*admissionregistration.MutatingAdmissionPolicyBinding)(nil), (*admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_admissionregistration_MutatingAdmissionPolicyBinding_To_v1alpha1_MutatingAdmissionPolicyBinding(a.(*admissionregistration.MutatingAdmissionPolicyBinding), b.(*admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingList)(nil), (*admissionregistration.MutatingAdmissionPolicyBindingList)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_MutatingAdmissionPolicyBindingList_To_admissionregistration_MutatingAdmissionPolicyBindingList(a.(*admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingList), b.(*admissionregistration.MutatingAdmissionPolicyBindingList), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*admissionregistration.MutatingAdmissionPolicyBindingList)(nil), (*admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingList)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_admissionregistration_MutatingAdmissionPolicyBindingList_To_v1alpha1_MutatingAdmissionPolicyBindingList(a.(*admissionregistration.MutatingAdmissionPolicyBindingList), b.(*admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingList), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingSpec)(nil), (*admissionregistration.MutatingAdmissionPolicyBindingSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_MutatingAdmissionPolicyBindingSpec_To_admissionregistration_MutatingAdmissionPolicyBindingSpec(a.(*admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingSpec), b.(*admissionregistration.MutatingAdmissionPolicyBindingSpec), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*admissionregistration.MutatingAdmissionPolicyBindingSpec)(nil), (*admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_admissionregistration_MutatingAdmissionPolicyBindingSpec_To_v1alpha1_MutatingAdmissionPolicyBindingSpec(a.(*admissionregistration.MutatingAdmissionPolicyBindingSpec), b.(*admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingSpec), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*admissionregistrationv1alpha1.MutatingAdmissionPolicyList)(nil), (*admissionregistration.MutatingAdmissionPolicyList)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_MutatingAdmissionPolicyList_To_admissionregistration_MutatingAdmissionPolicyList(a.(*admissionregistrationv1alpha1.MutatingAdmissionPolicyList), b.(*admissionregistration.MutatingAdmissionPolicyList), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*admissionregistration.MutatingAdmissionPolicyList)(nil), (*admissionregistrationv1alpha1.MutatingAdmissionPolicyList)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_admissionregistration_MutatingAdmissionPolicyList_To_v1alpha1_MutatingAdmissionPolicyList(a.(*admissionregistration.MutatingAdmissionPolicyList), b.(*admissionregistrationv1alpha1.MutatingAdmissionPolicyList), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*admissionregistrationv1alpha1.MutatingAdmissionPolicySpec)(nil), (*admissionregistration.MutatingAdmissionPolicySpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_MutatingAdmissionPolicySpec_To_admissionregistration_MutatingAdmissionPolicySpec(a.(*admissionregistrationv1alpha1.MutatingAdmissionPolicySpec), b.(*admissionregistration.MutatingAdmissionPolicySpec), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*admissionregistration.MutatingAdmissionPolicySpec)(nil), (*admissionregistrationv1alpha1.MutatingAdmissionPolicySpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_admissionregistration_MutatingAdmissionPolicySpec_To_v1alpha1_MutatingAdmissionPolicySpec(a.(*admissionregistration.MutatingAdmissionPolicySpec), b.(*admissionregistrationv1alpha1.MutatingAdmissionPolicySpec), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*admissionregistrationv1alpha1.Mutation)(nil), (*admissionregistration.Mutation)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_Mutation_To_admissionregistration_Mutation(a.(*admissionregistrationv1alpha1.Mutation), b.(*admissionregistration.Mutation), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*admissionregistration.Mutation)(nil), (*admissionregistrationv1alpha1.Mutation)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_admissionregistration_Mutation_To_v1alpha1_Mutation(a.(*admissionregistration.Mutation), b.(*admissionregistrationv1alpha1.Mutation), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*admissionregistrationv1alpha1.NamedRuleWithOperations)(nil), (*admissionregistration.NamedRuleWithOperations)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_NamedRuleWithOperations_To_admissionregistration_NamedRuleWithOperations(a.(*admissionregistrationv1alpha1.NamedRuleWithOperations), b.(*admissionregistration.NamedRuleWithOperations), scope)
}); err != nil {
@@ -212,6 +303,26 @@ func RegisterConversions(s *runtime.Scheme) error {
return nil
}
func autoConvert_v1alpha1_ApplyConfiguration_To_admissionregistration_ApplyConfiguration(in *admissionregistrationv1alpha1.ApplyConfiguration, out *admissionregistration.ApplyConfiguration, s conversion.Scope) error {
out.Expression = in.Expression
return nil
}
// Convert_v1alpha1_ApplyConfiguration_To_admissionregistration_ApplyConfiguration is an autogenerated conversion function.
func Convert_v1alpha1_ApplyConfiguration_To_admissionregistration_ApplyConfiguration(in *admissionregistrationv1alpha1.ApplyConfiguration, out *admissionregistration.ApplyConfiguration, s conversion.Scope) error {
return autoConvert_v1alpha1_ApplyConfiguration_To_admissionregistration_ApplyConfiguration(in, out, s)
}
func autoConvert_admissionregistration_ApplyConfiguration_To_v1alpha1_ApplyConfiguration(in *admissionregistration.ApplyConfiguration, out *admissionregistrationv1alpha1.ApplyConfiguration, s conversion.Scope) error {
out.Expression = in.Expression
return nil
}
// Convert_admissionregistration_ApplyConfiguration_To_v1alpha1_ApplyConfiguration is an autogenerated conversion function.
func Convert_admissionregistration_ApplyConfiguration_To_v1alpha1_ApplyConfiguration(in *admissionregistration.ApplyConfiguration, out *admissionregistrationv1alpha1.ApplyConfiguration, s conversion.Scope) error {
return autoConvert_admissionregistration_ApplyConfiguration_To_v1alpha1_ApplyConfiguration(in, out, s)
}
func autoConvert_v1alpha1_AuditAnnotation_To_admissionregistration_AuditAnnotation(in *admissionregistrationv1alpha1.AuditAnnotation, out *admissionregistration.AuditAnnotation, s conversion.Scope) error {
out.Key = in.Key
out.ValueExpression = in.ValueExpression
@@ -256,6 +367,26 @@ func Convert_admissionregistration_ExpressionWarning_To_v1alpha1_ExpressionWarni
return autoConvert_admissionregistration_ExpressionWarning_To_v1alpha1_ExpressionWarning(in, out, s)
}
func autoConvert_v1alpha1_JSONPatch_To_admissionregistration_JSONPatch(in *admissionregistrationv1alpha1.JSONPatch, out *admissionregistration.JSONPatch, s conversion.Scope) error {
out.Expression = in.Expression
return nil
}
// Convert_v1alpha1_JSONPatch_To_admissionregistration_JSONPatch is an autogenerated conversion function.
func Convert_v1alpha1_JSONPatch_To_admissionregistration_JSONPatch(in *admissionregistrationv1alpha1.JSONPatch, out *admissionregistration.JSONPatch, s conversion.Scope) error {
return autoConvert_v1alpha1_JSONPatch_To_admissionregistration_JSONPatch(in, out, s)
}
func autoConvert_admissionregistration_JSONPatch_To_v1alpha1_JSONPatch(in *admissionregistration.JSONPatch, out *admissionregistrationv1alpha1.JSONPatch, s conversion.Scope) error {
out.Expression = in.Expression
return nil
}
// Convert_admissionregistration_JSONPatch_To_v1alpha1_JSONPatch is an autogenerated conversion function.
func Convert_admissionregistration_JSONPatch_To_v1alpha1_JSONPatch(in *admissionregistration.JSONPatch, out *admissionregistrationv1alpha1.JSONPatch, s conversion.Scope) error {
return autoConvert_admissionregistration_JSONPatch_To_v1alpha1_JSONPatch(in, out, s)
}
func autoConvert_v1alpha1_MatchCondition_To_admissionregistration_MatchCondition(in *admissionregistrationv1alpha1.MatchCondition, out *admissionregistration.MatchCondition, s conversion.Scope) error {
out.Name = in.Name
out.Expression = in.Expression
@@ -346,9 +477,257 @@ func Convert_admissionregistration_MatchResources_To_v1alpha1_MatchResources(in
return autoConvert_admissionregistration_MatchResources_To_v1alpha1_MatchResources(in, out, s)
}
func autoConvert_v1alpha1_MutatingAdmissionPolicy_To_admissionregistration_MutatingAdmissionPolicy(in *admissionregistrationv1alpha1.MutatingAdmissionPolicy, out *admissionregistration.MutatingAdmissionPolicy, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
if err := Convert_v1alpha1_MutatingAdmissionPolicySpec_To_admissionregistration_MutatingAdmissionPolicySpec(&in.Spec, &out.Spec, s); err != nil {
return err
}
return nil
}
// Convert_v1alpha1_MutatingAdmissionPolicy_To_admissionregistration_MutatingAdmissionPolicy is an autogenerated conversion function.
func Convert_v1alpha1_MutatingAdmissionPolicy_To_admissionregistration_MutatingAdmissionPolicy(in *admissionregistrationv1alpha1.MutatingAdmissionPolicy, out *admissionregistration.MutatingAdmissionPolicy, s conversion.Scope) error {
return autoConvert_v1alpha1_MutatingAdmissionPolicy_To_admissionregistration_MutatingAdmissionPolicy(in, out, s)
}
func autoConvert_admissionregistration_MutatingAdmissionPolicy_To_v1alpha1_MutatingAdmissionPolicy(in *admissionregistration.MutatingAdmissionPolicy, out *admissionregistrationv1alpha1.MutatingAdmissionPolicy, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
if err := Convert_admissionregistration_MutatingAdmissionPolicySpec_To_v1alpha1_MutatingAdmissionPolicySpec(&in.Spec, &out.Spec, s); err != nil {
return err
}
return nil
}
// Convert_admissionregistration_MutatingAdmissionPolicy_To_v1alpha1_MutatingAdmissionPolicy is an autogenerated conversion function.
func Convert_admissionregistration_MutatingAdmissionPolicy_To_v1alpha1_MutatingAdmissionPolicy(in *admissionregistration.MutatingAdmissionPolicy, out *admissionregistrationv1alpha1.MutatingAdmissionPolicy, s conversion.Scope) error {
return autoConvert_admissionregistration_MutatingAdmissionPolicy_To_v1alpha1_MutatingAdmissionPolicy(in, out, s)
}
func autoConvert_v1alpha1_MutatingAdmissionPolicyBinding_To_admissionregistration_MutatingAdmissionPolicyBinding(in *admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding, out *admissionregistration.MutatingAdmissionPolicyBinding, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
if err := Convert_v1alpha1_MutatingAdmissionPolicyBindingSpec_To_admissionregistration_MutatingAdmissionPolicyBindingSpec(&in.Spec, &out.Spec, s); err != nil {
return err
}
return nil
}
// Convert_v1alpha1_MutatingAdmissionPolicyBinding_To_admissionregistration_MutatingAdmissionPolicyBinding is an autogenerated conversion function.
func Convert_v1alpha1_MutatingAdmissionPolicyBinding_To_admissionregistration_MutatingAdmissionPolicyBinding(in *admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding, out *admissionregistration.MutatingAdmissionPolicyBinding, s conversion.Scope) error {
return autoConvert_v1alpha1_MutatingAdmissionPolicyBinding_To_admissionregistration_MutatingAdmissionPolicyBinding(in, out, s)
}
func autoConvert_admissionregistration_MutatingAdmissionPolicyBinding_To_v1alpha1_MutatingAdmissionPolicyBinding(in *admissionregistration.MutatingAdmissionPolicyBinding, out *admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
if err := Convert_admissionregistration_MutatingAdmissionPolicyBindingSpec_To_v1alpha1_MutatingAdmissionPolicyBindingSpec(&in.Spec, &out.Spec, s); err != nil {
return err
}
return nil
}
// Convert_admissionregistration_MutatingAdmissionPolicyBinding_To_v1alpha1_MutatingAdmissionPolicyBinding is an autogenerated conversion function.
func Convert_admissionregistration_MutatingAdmissionPolicyBinding_To_v1alpha1_MutatingAdmissionPolicyBinding(in *admissionregistration.MutatingAdmissionPolicyBinding, out *admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding, s conversion.Scope) error {
return autoConvert_admissionregistration_MutatingAdmissionPolicyBinding_To_v1alpha1_MutatingAdmissionPolicyBinding(in, out, s)
}
func autoConvert_v1alpha1_MutatingAdmissionPolicyBindingList_To_admissionregistration_MutatingAdmissionPolicyBindingList(in *admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingList, out *admissionregistration.MutatingAdmissionPolicyBindingList, s conversion.Scope) error {
out.ListMeta = in.ListMeta
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]admissionregistration.MutatingAdmissionPolicyBinding, len(*in))
for i := range *in {
if err := Convert_v1alpha1_MutatingAdmissionPolicyBinding_To_admissionregistration_MutatingAdmissionPolicyBinding(&(*in)[i], &(*out)[i], s); err != nil {
return err
}
}
} else {
out.Items = nil
}
return nil
}
// Convert_v1alpha1_MutatingAdmissionPolicyBindingList_To_admissionregistration_MutatingAdmissionPolicyBindingList is an autogenerated conversion function.
func Convert_v1alpha1_MutatingAdmissionPolicyBindingList_To_admissionregistration_MutatingAdmissionPolicyBindingList(in *admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingList, out *admissionregistration.MutatingAdmissionPolicyBindingList, s conversion.Scope) error {
return autoConvert_v1alpha1_MutatingAdmissionPolicyBindingList_To_admissionregistration_MutatingAdmissionPolicyBindingList(in, out, s)
}
func autoConvert_admissionregistration_MutatingAdmissionPolicyBindingList_To_v1alpha1_MutatingAdmissionPolicyBindingList(in *admissionregistration.MutatingAdmissionPolicyBindingList, out *admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingList, s conversion.Scope) error {
out.ListMeta = in.ListMeta
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding, len(*in))
for i := range *in {
if err := Convert_admissionregistration_MutatingAdmissionPolicyBinding_To_v1alpha1_MutatingAdmissionPolicyBinding(&(*in)[i], &(*out)[i], s); err != nil {
return err
}
}
} else {
out.Items = nil
}
return nil
}
// Convert_admissionregistration_MutatingAdmissionPolicyBindingList_To_v1alpha1_MutatingAdmissionPolicyBindingList is an autogenerated conversion function.
func Convert_admissionregistration_MutatingAdmissionPolicyBindingList_To_v1alpha1_MutatingAdmissionPolicyBindingList(in *admissionregistration.MutatingAdmissionPolicyBindingList, out *admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingList, s conversion.Scope) error {
return autoConvert_admissionregistration_MutatingAdmissionPolicyBindingList_To_v1alpha1_MutatingAdmissionPolicyBindingList(in, out, s)
}
func autoConvert_v1alpha1_MutatingAdmissionPolicyBindingSpec_To_admissionregistration_MutatingAdmissionPolicyBindingSpec(in *admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingSpec, out *admissionregistration.MutatingAdmissionPolicyBindingSpec, s conversion.Scope) error {
out.PolicyName = in.PolicyName
out.ParamRef = (*admissionregistration.ParamRef)(unsafe.Pointer(in.ParamRef))
if in.MatchResources != nil {
in, out := &in.MatchResources, &out.MatchResources
*out = new(admissionregistration.MatchResources)
if err := Convert_v1alpha1_MatchResources_To_admissionregistration_MatchResources(*in, *out, s); err != nil {
return err
}
} else {
out.MatchResources = nil
}
return nil
}
// Convert_v1alpha1_MutatingAdmissionPolicyBindingSpec_To_admissionregistration_MutatingAdmissionPolicyBindingSpec is an autogenerated conversion function.
func Convert_v1alpha1_MutatingAdmissionPolicyBindingSpec_To_admissionregistration_MutatingAdmissionPolicyBindingSpec(in *admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingSpec, out *admissionregistration.MutatingAdmissionPolicyBindingSpec, s conversion.Scope) error {
return autoConvert_v1alpha1_MutatingAdmissionPolicyBindingSpec_To_admissionregistration_MutatingAdmissionPolicyBindingSpec(in, out, s)
}
func autoConvert_admissionregistration_MutatingAdmissionPolicyBindingSpec_To_v1alpha1_MutatingAdmissionPolicyBindingSpec(in *admissionregistration.MutatingAdmissionPolicyBindingSpec, out *admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingSpec, s conversion.Scope) error {
out.PolicyName = in.PolicyName
out.ParamRef = (*admissionregistrationv1alpha1.ParamRef)(unsafe.Pointer(in.ParamRef))
if in.MatchResources != nil {
in, out := &in.MatchResources, &out.MatchResources
*out = new(admissionregistrationv1alpha1.MatchResources)
if err := Convert_admissionregistration_MatchResources_To_v1alpha1_MatchResources(*in, *out, s); err != nil {
return err
}
} else {
out.MatchResources = nil
}
return nil
}
// Convert_admissionregistration_MutatingAdmissionPolicyBindingSpec_To_v1alpha1_MutatingAdmissionPolicyBindingSpec is an autogenerated conversion function.
func Convert_admissionregistration_MutatingAdmissionPolicyBindingSpec_To_v1alpha1_MutatingAdmissionPolicyBindingSpec(in *admissionregistration.MutatingAdmissionPolicyBindingSpec, out *admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingSpec, s conversion.Scope) error {
return autoConvert_admissionregistration_MutatingAdmissionPolicyBindingSpec_To_v1alpha1_MutatingAdmissionPolicyBindingSpec(in, out, s)
}
func autoConvert_v1alpha1_MutatingAdmissionPolicyList_To_admissionregistration_MutatingAdmissionPolicyList(in *admissionregistrationv1alpha1.MutatingAdmissionPolicyList, out *admissionregistration.MutatingAdmissionPolicyList, s conversion.Scope) error {
out.ListMeta = in.ListMeta
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]admissionregistration.MutatingAdmissionPolicy, len(*in))
for i := range *in {
if err := Convert_v1alpha1_MutatingAdmissionPolicy_To_admissionregistration_MutatingAdmissionPolicy(&(*in)[i], &(*out)[i], s); err != nil {
return err
}
}
} else {
out.Items = nil
}
return nil
}
// Convert_v1alpha1_MutatingAdmissionPolicyList_To_admissionregistration_MutatingAdmissionPolicyList is an autogenerated conversion function.
func Convert_v1alpha1_MutatingAdmissionPolicyList_To_admissionregistration_MutatingAdmissionPolicyList(in *admissionregistrationv1alpha1.MutatingAdmissionPolicyList, out *admissionregistration.MutatingAdmissionPolicyList, s conversion.Scope) error {
return autoConvert_v1alpha1_MutatingAdmissionPolicyList_To_admissionregistration_MutatingAdmissionPolicyList(in, out, s)
}
func autoConvert_admissionregistration_MutatingAdmissionPolicyList_To_v1alpha1_MutatingAdmissionPolicyList(in *admissionregistration.MutatingAdmissionPolicyList, out *admissionregistrationv1alpha1.MutatingAdmissionPolicyList, s conversion.Scope) error {
out.ListMeta = in.ListMeta
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]admissionregistrationv1alpha1.MutatingAdmissionPolicy, len(*in))
for i := range *in {
if err := Convert_admissionregistration_MutatingAdmissionPolicy_To_v1alpha1_MutatingAdmissionPolicy(&(*in)[i], &(*out)[i], s); err != nil {
return err
}
}
} else {
out.Items = nil
}
return nil
}
// Convert_admissionregistration_MutatingAdmissionPolicyList_To_v1alpha1_MutatingAdmissionPolicyList is an autogenerated conversion function.
func Convert_admissionregistration_MutatingAdmissionPolicyList_To_v1alpha1_MutatingAdmissionPolicyList(in *admissionregistration.MutatingAdmissionPolicyList, out *admissionregistrationv1alpha1.MutatingAdmissionPolicyList, s conversion.Scope) error {
return autoConvert_admissionregistration_MutatingAdmissionPolicyList_To_v1alpha1_MutatingAdmissionPolicyList(in, out, s)
}
func autoConvert_v1alpha1_MutatingAdmissionPolicySpec_To_admissionregistration_MutatingAdmissionPolicySpec(in *admissionregistrationv1alpha1.MutatingAdmissionPolicySpec, out *admissionregistration.MutatingAdmissionPolicySpec, s conversion.Scope) error {
out.ParamKind = (*admissionregistration.ParamKind)(unsafe.Pointer(in.ParamKind))
if in.MatchConstraints != nil {
in, out := &in.MatchConstraints, &out.MatchConstraints
*out = new(admissionregistration.MatchResources)
if err := Convert_v1alpha1_MatchResources_To_admissionregistration_MatchResources(*in, *out, s); err != nil {
return err
}
} else {
out.MatchConstraints = nil
}
out.Variables = *(*[]admissionregistration.Variable)(unsafe.Pointer(&in.Variables))
out.Mutations = *(*[]admissionregistration.Mutation)(unsafe.Pointer(&in.Mutations))
out.FailurePolicy = (*admissionregistration.FailurePolicyType)(unsafe.Pointer(in.FailurePolicy))
out.MatchConditions = *(*[]admissionregistration.MatchCondition)(unsafe.Pointer(&in.MatchConditions))
out.ReinvocationPolicy = admissionregistration.ReinvocationPolicyType(in.ReinvocationPolicy)
return nil
}
// Convert_v1alpha1_MutatingAdmissionPolicySpec_To_admissionregistration_MutatingAdmissionPolicySpec is an autogenerated conversion function.
func Convert_v1alpha1_MutatingAdmissionPolicySpec_To_admissionregistration_MutatingAdmissionPolicySpec(in *admissionregistrationv1alpha1.MutatingAdmissionPolicySpec, out *admissionregistration.MutatingAdmissionPolicySpec, s conversion.Scope) error {
return autoConvert_v1alpha1_MutatingAdmissionPolicySpec_To_admissionregistration_MutatingAdmissionPolicySpec(in, out, s)
}
func autoConvert_admissionregistration_MutatingAdmissionPolicySpec_To_v1alpha1_MutatingAdmissionPolicySpec(in *admissionregistration.MutatingAdmissionPolicySpec, out *admissionregistrationv1alpha1.MutatingAdmissionPolicySpec, s conversion.Scope) error {
out.ParamKind = (*admissionregistrationv1alpha1.ParamKind)(unsafe.Pointer(in.ParamKind))
if in.MatchConstraints != nil {
in, out := &in.MatchConstraints, &out.MatchConstraints
*out = new(admissionregistrationv1alpha1.MatchResources)
if err := Convert_admissionregistration_MatchResources_To_v1alpha1_MatchResources(*in, *out, s); err != nil {
return err
}
} else {
out.MatchConstraints = nil
}
out.Variables = *(*[]admissionregistrationv1alpha1.Variable)(unsafe.Pointer(&in.Variables))
out.Mutations = *(*[]admissionregistrationv1alpha1.Mutation)(unsafe.Pointer(&in.Mutations))
out.FailurePolicy = (*admissionregistrationv1alpha1.FailurePolicyType)(unsafe.Pointer(in.FailurePolicy))
out.MatchConditions = *(*[]admissionregistrationv1alpha1.MatchCondition)(unsafe.Pointer(&in.MatchConditions))
out.ReinvocationPolicy = admissionregistrationv1.ReinvocationPolicyType(in.ReinvocationPolicy)
return nil
}
// Convert_admissionregistration_MutatingAdmissionPolicySpec_To_v1alpha1_MutatingAdmissionPolicySpec is an autogenerated conversion function.
func Convert_admissionregistration_MutatingAdmissionPolicySpec_To_v1alpha1_MutatingAdmissionPolicySpec(in *admissionregistration.MutatingAdmissionPolicySpec, out *admissionregistrationv1alpha1.MutatingAdmissionPolicySpec, s conversion.Scope) error {
return autoConvert_admissionregistration_MutatingAdmissionPolicySpec_To_v1alpha1_MutatingAdmissionPolicySpec(in, out, s)
}
func autoConvert_v1alpha1_Mutation_To_admissionregistration_Mutation(in *admissionregistrationv1alpha1.Mutation, out *admissionregistration.Mutation, s conversion.Scope) error {
out.PatchType = admissionregistration.PatchType(in.PatchType)
out.ApplyConfiguration = (*admissionregistration.ApplyConfiguration)(unsafe.Pointer(in.ApplyConfiguration))
out.JSONPatch = (*admissionregistration.JSONPatch)(unsafe.Pointer(in.JSONPatch))
return nil
}
// Convert_v1alpha1_Mutation_To_admissionregistration_Mutation is an autogenerated conversion function.
func Convert_v1alpha1_Mutation_To_admissionregistration_Mutation(in *admissionregistrationv1alpha1.Mutation, out *admissionregistration.Mutation, s conversion.Scope) error {
return autoConvert_v1alpha1_Mutation_To_admissionregistration_Mutation(in, out, s)
}
func autoConvert_admissionregistration_Mutation_To_v1alpha1_Mutation(in *admissionregistration.Mutation, out *admissionregistrationv1alpha1.Mutation, s conversion.Scope) error {
out.PatchType = admissionregistrationv1alpha1.PatchType(in.PatchType)
out.ApplyConfiguration = (*admissionregistrationv1alpha1.ApplyConfiguration)(unsafe.Pointer(in.ApplyConfiguration))
out.JSONPatch = (*admissionregistrationv1alpha1.JSONPatch)(unsafe.Pointer(in.JSONPatch))
return nil
}
// Convert_admissionregistration_Mutation_To_v1alpha1_Mutation is an autogenerated conversion function.
func Convert_admissionregistration_Mutation_To_v1alpha1_Mutation(in *admissionregistration.Mutation, out *admissionregistrationv1alpha1.Mutation, s conversion.Scope) error {
return autoConvert_admissionregistration_Mutation_To_v1alpha1_Mutation(in, out, s)
}
func autoConvert_v1alpha1_NamedRuleWithOperations_To_admissionregistration_NamedRuleWithOperations(in *admissionregistrationv1alpha1.NamedRuleWithOperations, out *admissionregistration.NamedRuleWithOperations, s conversion.Scope) error {
out.ResourceNames = *(*[]string)(unsafe.Pointer(&in.ResourceNames))
if err := admissionregistrationv1.Convert_v1_RuleWithOperations_To_admissionregistration_RuleWithOperations(&in.RuleWithOperations, &out.RuleWithOperations, s); err != nil {
if err := apisadmissionregistrationv1.Convert_v1_RuleWithOperations_To_admissionregistration_RuleWithOperations(&in.RuleWithOperations, &out.RuleWithOperations, s); err != nil {
return err
}
return nil
@@ -361,7 +740,7 @@ func Convert_v1alpha1_NamedRuleWithOperations_To_admissionregistration_NamedRule
func autoConvert_admissionregistration_NamedRuleWithOperations_To_v1alpha1_NamedRuleWithOperations(in *admissionregistration.NamedRuleWithOperations, out *admissionregistrationv1alpha1.NamedRuleWithOperations, s conversion.Scope) error {
out.ResourceNames = *(*[]string)(unsafe.Pointer(&in.ResourceNames))
if err := admissionregistrationv1.Convert_admissionregistration_RuleWithOperations_To_v1_RuleWithOperations(&in.RuleWithOperations, &out.RuleWithOperations, s); err != nil {
if err := apisadmissionregistrationv1.Convert_admissionregistration_RuleWithOperations_To_v1_RuleWithOperations(&in.RuleWithOperations, &out.RuleWithOperations, s); err != nil {
return err
}
return nil

View File

@@ -31,6 +31,18 @@ import (
// Public to allow building arbitrary schemes.
// All generated defaulters are covering - they call all nested defaulters.
func RegisterDefaults(scheme *runtime.Scheme) error {
scheme.AddTypeDefaultingFunc(&admissionregistrationv1alpha1.MutatingAdmissionPolicy{}, func(obj interface{}) {
SetObjectDefaults_MutatingAdmissionPolicy(obj.(*admissionregistrationv1alpha1.MutatingAdmissionPolicy))
})
scheme.AddTypeDefaultingFunc(&admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding{}, func(obj interface{}) {
SetObjectDefaults_MutatingAdmissionPolicyBinding(obj.(*admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding))
})
scheme.AddTypeDefaultingFunc(&admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingList{}, func(obj interface{}) {
SetObjectDefaults_MutatingAdmissionPolicyBindingList(obj.(*admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingList))
})
scheme.AddTypeDefaultingFunc(&admissionregistrationv1alpha1.MutatingAdmissionPolicyList{}, func(obj interface{}) {
SetObjectDefaults_MutatingAdmissionPolicyList(obj.(*admissionregistrationv1alpha1.MutatingAdmissionPolicyList))
})
scheme.AddTypeDefaultingFunc(&admissionregistrationv1alpha1.ValidatingAdmissionPolicy{}, func(obj interface{}) {
SetObjectDefaults_ValidatingAdmissionPolicy(obj.(*admissionregistrationv1alpha1.ValidatingAdmissionPolicy))
})
@@ -46,6 +58,52 @@ func RegisterDefaults(scheme *runtime.Scheme) error {
return nil
}
func SetObjectDefaults_MutatingAdmissionPolicy(in *admissionregistrationv1alpha1.MutatingAdmissionPolicy) {
SetDefaults_MutatingAdmissionPolicySpec(&in.Spec)
if in.Spec.MatchConstraints != nil {
SetDefaults_MatchResources(in.Spec.MatchConstraints)
for i := range in.Spec.MatchConstraints.ResourceRules {
a := &in.Spec.MatchConstraints.ResourceRules[i]
v1.SetDefaults_Rule(&a.RuleWithOperations.Rule)
}
for i := range in.Spec.MatchConstraints.ExcludeResourceRules {
a := &in.Spec.MatchConstraints.ExcludeResourceRules[i]
v1.SetDefaults_Rule(&a.RuleWithOperations.Rule)
}
}
}
func SetObjectDefaults_MutatingAdmissionPolicyBinding(in *admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding) {
if in.Spec.ParamRef != nil {
SetDefaults_ParamRef(in.Spec.ParamRef)
}
if in.Spec.MatchResources != nil {
SetDefaults_MatchResources(in.Spec.MatchResources)
for i := range in.Spec.MatchResources.ResourceRules {
a := &in.Spec.MatchResources.ResourceRules[i]
v1.SetDefaults_Rule(&a.RuleWithOperations.Rule)
}
for i := range in.Spec.MatchResources.ExcludeResourceRules {
a := &in.Spec.MatchResources.ExcludeResourceRules[i]
v1.SetDefaults_Rule(&a.RuleWithOperations.Rule)
}
}
}
func SetObjectDefaults_MutatingAdmissionPolicyBindingList(in *admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingList) {
for i := range in.Items {
a := &in.Items[i]
SetObjectDefaults_MutatingAdmissionPolicyBinding(a)
}
}
func SetObjectDefaults_MutatingAdmissionPolicyList(in *admissionregistrationv1alpha1.MutatingAdmissionPolicyList) {
for i := range in.Items {
a := &in.Items[i]
SetObjectDefaults_MutatingAdmissionPolicy(a)
}
}
func SetObjectDefaults_ValidatingAdmissionPolicy(in *admissionregistrationv1alpha1.ValidatingAdmissionPolicy) {
SetDefaults_ValidatingAdmissionPolicySpec(&in.Spec)
if in.Spec.MatchConstraints != nil {

View File

@@ -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/patch"
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 := &patch.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 := &patch.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
}

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,22 @@ import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplyConfiguration) DeepCopyInto(out *ApplyConfiguration) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplyConfiguration.
func (in *ApplyConfiguration) DeepCopy() *ApplyConfiguration {
if in == nil {
return nil
}
out := new(ApplyConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AuditAnnotation) DeepCopyInto(out *AuditAnnotation) {
*out = *in
@@ -58,6 +74,22 @@ func (in *ExpressionWarning) DeepCopy() *ExpressionWarning {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *JSONPatch) DeepCopyInto(out *JSONPatch) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JSONPatch.
func (in *JSONPatch) DeepCopy() *JSONPatch {
if in == nil {
return nil
}
out := new(JSONPatch)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MatchCondition) DeepCopyInto(out *MatchCondition) {
*out = *in
@@ -119,6 +151,200 @@ func (in *MatchResources) DeepCopy() *MatchResources {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MutatingAdmissionPolicy) DeepCopyInto(out *MutatingAdmissionPolicy) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MutatingAdmissionPolicy.
func (in *MutatingAdmissionPolicy) DeepCopy() *MutatingAdmissionPolicy {
if in == nil {
return nil
}
out := new(MutatingAdmissionPolicy)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *MutatingAdmissionPolicy) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MutatingAdmissionPolicyBinding) DeepCopyInto(out *MutatingAdmissionPolicyBinding) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MutatingAdmissionPolicyBinding.
func (in *MutatingAdmissionPolicyBinding) DeepCopy() *MutatingAdmissionPolicyBinding {
if in == nil {
return nil
}
out := new(MutatingAdmissionPolicyBinding)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *MutatingAdmissionPolicyBinding) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MutatingAdmissionPolicyBindingList) DeepCopyInto(out *MutatingAdmissionPolicyBindingList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]MutatingAdmissionPolicyBinding, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MutatingAdmissionPolicyBindingList.
func (in *MutatingAdmissionPolicyBindingList) DeepCopy() *MutatingAdmissionPolicyBindingList {
if in == nil {
return nil
}
out := new(MutatingAdmissionPolicyBindingList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *MutatingAdmissionPolicyBindingList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MutatingAdmissionPolicyBindingSpec) DeepCopyInto(out *MutatingAdmissionPolicyBindingSpec) {
*out = *in
if in.ParamRef != nil {
in, out := &in.ParamRef, &out.ParamRef
*out = new(ParamRef)
(*in).DeepCopyInto(*out)
}
if in.MatchResources != nil {
in, out := &in.MatchResources, &out.MatchResources
*out = new(MatchResources)
(*in).DeepCopyInto(*out)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MutatingAdmissionPolicyBindingSpec.
func (in *MutatingAdmissionPolicyBindingSpec) DeepCopy() *MutatingAdmissionPolicyBindingSpec {
if in == nil {
return nil
}
out := new(MutatingAdmissionPolicyBindingSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MutatingAdmissionPolicyList) DeepCopyInto(out *MutatingAdmissionPolicyList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]MutatingAdmissionPolicy, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MutatingAdmissionPolicyList.
func (in *MutatingAdmissionPolicyList) DeepCopy() *MutatingAdmissionPolicyList {
if in == nil {
return nil
}
out := new(MutatingAdmissionPolicyList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *MutatingAdmissionPolicyList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MutatingAdmissionPolicySpec) DeepCopyInto(out *MutatingAdmissionPolicySpec) {
*out = *in
if in.ParamKind != nil {
in, out := &in.ParamKind, &out.ParamKind
*out = new(ParamKind)
**out = **in
}
if in.MatchConstraints != nil {
in, out := &in.MatchConstraints, &out.MatchConstraints
*out = new(MatchResources)
(*in).DeepCopyInto(*out)
}
if in.Variables != nil {
in, out := &in.Variables, &out.Variables
*out = make([]Variable, len(*in))
copy(*out, *in)
}
if in.Mutations != nil {
in, out := &in.Mutations, &out.Mutations
*out = make([]Mutation, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.FailurePolicy != nil {
in, out := &in.FailurePolicy, &out.FailurePolicy
*out = new(FailurePolicyType)
**out = **in
}
if in.MatchConditions != nil {
in, out := &in.MatchConditions, &out.MatchConditions
*out = make([]MatchCondition, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MutatingAdmissionPolicySpec.
func (in *MutatingAdmissionPolicySpec) DeepCopy() *MutatingAdmissionPolicySpec {
if in == nil {
return nil
}
out := new(MutatingAdmissionPolicySpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MutatingWebhook) DeepCopyInto(out *MutatingWebhook) {
*out = *in
@@ -254,6 +480,32 @@ func (in *MutatingWebhookConfigurationList) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Mutation) DeepCopyInto(out *Mutation) {
*out = *in
if in.ApplyConfiguration != nil {
in, out := &in.ApplyConfiguration, &out.ApplyConfiguration
*out = new(ApplyConfiguration)
**out = **in
}
if in.JSONPatch != nil {
in, out := &in.JSONPatch, &out.JSONPatch
*out = new(JSONPatch)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Mutation.
func (in *Mutation) DeepCopy() *Mutation {
if in == nil {
return nil
}
out := new(Mutation)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamedRuleWithOperations) DeepCopyInto(out *NamedRuleWithOperations) {
*out = *in

View File

@@ -19,6 +19,7 @@ package server
import (
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
mutatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/mutating"
validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating"
"k8s.io/apiserver/pkg/admission/plugin/resourcequota"
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
@@ -45,7 +46,8 @@ func DefaultOffAdmissionPlugins() sets.Set[string] {
certsigning.PluginName, // CertificateSigning
ctbattest.PluginName, // ClusterTrustBundleAttest
certsubjectrestriction.PluginName, // CertificateSubjectRestriction
validatingadmissionpolicy.PluginName, // ValidatingAdmissionPolicy, only active when feature gate ValidatingAdmissionPolicy is enabled
validatingadmissionpolicy.PluginName, // ValidatingAdmissionPolicy
mutatingadmissionpolicy.PluginName, // MutatingAdmissionPolicy
)
return sets.New(options.AllOrderedPlugins...).Difference(defaultOnPlugins)

View File

@@ -266,7 +266,7 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
},
genericfeatures.MutatingAdmissionPolicy: {
{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
},
genericfeatures.OpenAPIEnums: {

View File

@@ -18,10 +18,12 @@ package openapi
import (
"encoding/json"
"reflect"
"testing"
"github.com/go-openapi/jsonreference"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/handler"
"k8s.io/kube-openapi/pkg/validation/spec"
@@ -51,8 +53,16 @@ func TestOpenAPIRoundtrip(t *testing.T) {
delete(roundTripped.Extensions, common.ExtensionV2Schema)
delete(value.Schema.Extensions, common.ExtensionV2Schema)
if !reflect.DeepEqual(value.Schema, roundTripped) {
t.Errorf("unexpected diff (a=expected,b=roundtripped):\n%s", cmp.Diff(value.Schema, roundTripped))
opts := []cmp.Option{
cmpopts.EquateEmpty(),
// jsonreference.Ref contains unexported fields. Compare
// by string representation provides a consistent
cmp.Comparer(func(x, y jsonreference.Ref) bool {
return x.String() == y.String()
}),
}
if !cmp.Equal(value.Schema, roundTripped, opts...) {
t.Errorf("unexpected diff (a=expected,b=roundtripped):\n%s", cmp.Diff(value.Schema, roundTripped, opts...))
return
}
})

View File

@@ -61,10 +61,19 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"k8s.io/api/admissionregistration/v1.Validation": schema_k8sio_api_admissionregistration_v1_Validation(ref),
"k8s.io/api/admissionregistration/v1.Variable": schema_k8sio_api_admissionregistration_v1_Variable(ref),
"k8s.io/api/admissionregistration/v1.WebhookClientConfig": schema_k8sio_api_admissionregistration_v1_WebhookClientConfig(ref),
"k8s.io/api/admissionregistration/v1alpha1.ApplyConfiguration": schema_k8sio_api_admissionregistration_v1alpha1_ApplyConfiguration(ref),
"k8s.io/api/admissionregistration/v1alpha1.AuditAnnotation": schema_k8sio_api_admissionregistration_v1alpha1_AuditAnnotation(ref),
"k8s.io/api/admissionregistration/v1alpha1.ExpressionWarning": schema_k8sio_api_admissionregistration_v1alpha1_ExpressionWarning(ref),
"k8s.io/api/admissionregistration/v1alpha1.JSONPatch": schema_k8sio_api_admissionregistration_v1alpha1_JSONPatch(ref),
"k8s.io/api/admissionregistration/v1alpha1.MatchCondition": schema_k8sio_api_admissionregistration_v1alpha1_MatchCondition(ref),
"k8s.io/api/admissionregistration/v1alpha1.MatchResources": schema_k8sio_api_admissionregistration_v1alpha1_MatchResources(ref),
"k8s.io/api/admissionregistration/v1alpha1.MutatingAdmissionPolicy": schema_k8sio_api_admissionregistration_v1alpha1_MutatingAdmissionPolicy(ref),
"k8s.io/api/admissionregistration/v1alpha1.MutatingAdmissionPolicyBinding": schema_k8sio_api_admissionregistration_v1alpha1_MutatingAdmissionPolicyBinding(ref),
"k8s.io/api/admissionregistration/v1alpha1.MutatingAdmissionPolicyBindingList": schema_k8sio_api_admissionregistration_v1alpha1_MutatingAdmissionPolicyBindingList(ref),
"k8s.io/api/admissionregistration/v1alpha1.MutatingAdmissionPolicyBindingSpec": schema_k8sio_api_admissionregistration_v1alpha1_MutatingAdmissionPolicyBindingSpec(ref),
"k8s.io/api/admissionregistration/v1alpha1.MutatingAdmissionPolicyList": schema_k8sio_api_admissionregistration_v1alpha1_MutatingAdmissionPolicyList(ref),
"k8s.io/api/admissionregistration/v1alpha1.MutatingAdmissionPolicySpec": schema_k8sio_api_admissionregistration_v1alpha1_MutatingAdmissionPolicySpec(ref),
"k8s.io/api/admissionregistration/v1alpha1.Mutation": schema_k8sio_api_admissionregistration_v1alpha1_Mutation(ref),
"k8s.io/api/admissionregistration/v1alpha1.NamedRuleWithOperations": schema_k8sio_api_admissionregistration_v1alpha1_NamedRuleWithOperations(ref),
"k8s.io/api/admissionregistration/v1alpha1.ParamKind": schema_k8sio_api_admissionregistration_v1alpha1_ParamKind(ref),
"k8s.io/api/admissionregistration/v1alpha1.ParamRef": schema_k8sio_api_admissionregistration_v1alpha1_ParamRef(ref),
@@ -1506,7 +1515,7 @@ func schema_k8sio_api_admissionregistration_v1_MutatingWebhook(ref common.Refere
},
"reinvocationPolicy": {
SchemaProps: spec.SchemaProps{
Description: "reinvocationPolicy indicates whether this webhook should be called multiple times as part of a single admission evaluation. Allowed values are \"Never\" and \"IfNeeded\".\n\nNever: the webhook will not be called more than once in a single admission evaluation.\n\nIfNeeded: the webhook will 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 webhook call. Webhooks that specify this option *must* be idempotent, able to process objects they previously admitted. Note: * the number of additional invocations is not guaranteed to be exactly one. * if additional invocations result in further modifications to the object, webhooks are not guaranteed to be invoked again. * webhooks that use this option may be reordered to minimize the number of additional invocations. * to validate an object after all mutations are guaranteed complete, use a validating admission webhook instead.\n\nDefaults to \"Never\".\n\nPossible enum values:\n - `\"IfNeeded\"` indicates that the webhook 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 webhook call.\n - `\"Never\"` indicates that the webhook must not be called more than once in a single admission evaluation.",
Description: "reinvocationPolicy indicates whether this webhook should be called multiple times as part of a single admission evaluation. Allowed values are \"Never\" and \"IfNeeded\".\n\nNever: the webhook will not be called more than once in a single admission evaluation.\n\nIfNeeded: the webhook will 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 webhook call. Webhooks that specify this option *must* be idempotent, able to process objects they previously admitted. Note: * the number of additional invocations is not guaranteed to be exactly one. * if additional invocations result in further modifications to the object, webhooks are not guaranteed to be invoked again. * webhooks that use this option may be reordered to minimize the number of additional invocations. * to validate an object after all mutations are guaranteed complete, use a validating admission webhook instead.\n\nDefaults to \"Never\".\n\nPossible enum values:\n - `\"IfNeeded\"` 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.\n - `\"Never\"` indicates that the mutation must not be called more than once in a single admission evaluation.",
Type: []string{"string"},
Format: "",
Enum: []interface{}{"IfNeeded", "Never"},
@@ -2902,6 +2911,26 @@ func schema_k8sio_api_admissionregistration_v1_WebhookClientConfig(ref common.Re
}
}
func schema_k8sio_api_admissionregistration_v1alpha1_ApplyConfiguration(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "ApplyConfiguration defines the desired configuration values of an object.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"expression": {
SchemaProps: spec.SchemaProps{
Description: "expression will be evaluated by CEL to create an apply configuration. ref: https://github.com/google/cel-spec\n\nApply configurations are declared in CEL using object initialization. For example, this CEL expression returns an apply configuration to set a single field:\n\n\tObject{\n\t spec: Object.spec{\n\t serviceAccountName: \"example\"\n\t }\n\t}\n\nApply configurations may not modify atomic structs, maps or arrays due to the risk of accidental deletion of values not included in the apply configuration.\n\nCEL expressions have access to the object types needed to create apply configurations:\n\n- 'Object' - CEL type of the resource object. - 'Object.<fieldName>' - CEL type of object field (such as 'Object.spec') - 'Object.<fieldName1>.<fieldName2>...<fieldNameN>` - CEL type of nested field (such as 'Object.spec.containers')\n\nCEL expressions have access to the contents of the API request, organized into CEL variables as well as some other useful variables:\n\n- '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.\n For example, a variable named 'foo' can be accessed as 'variables.foo'.\n- 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.\n See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz\n- 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the\n request resource.\n\nThe `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the object. No other metadata properties are accessible.\n\nOnly property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. Required.",
Type: []string{"string"},
Format: "",
},
},
},
},
},
}
}
func schema_k8sio_api_admissionregistration_v1alpha1_AuditAnnotation(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -2962,6 +2991,26 @@ func schema_k8sio_api_admissionregistration_v1alpha1_ExpressionWarning(ref commo
}
}
func schema_k8sio_api_admissionregistration_v1alpha1_JSONPatch(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "JSONPatch defines a JSON Patch.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"expression": {
SchemaProps: spec.SchemaProps{
Description: "expression will be evaluated by CEL to create a [JSON patch](https://jsonpatch.com/). ref: https://github.com/google/cel-spec\n\nexpression must return an array of JSONPatch values.\n\nFor example, this CEL expression returns a JSON patch to conditionally modify a value:\n\n\t [\n\t JSONPatch{op: \"test\", path: \"/spec/example\", value: \"Red\"},\n\t JSONPatch{op: \"replace\", path: \"/spec/example\", value: \"Green\"}\n\t ]\n\nTo define an object for the patch value, use Object types. For example:\n\n\t [\n\t JSONPatch{\n\t op: \"add\",\n\t path: \"/spec/selector\",\n\t value: Object.spec.selector{matchLabels: {\"environment\": \"test\"}}\n\t }\n\t ]\n\nTo use strings containing '/' and '~' as JSONPatch path keys, use \"jsonpatch.escapeKey\". For example:\n\n\t [\n\t JSONPatch{\n\t op: \"add\",\n\t path: \"/metadata/labels/\" + jsonpatch.escapeKey(\"example.com/environment\"),\n\t value: \"test\"\n\t },\n\t ]\n\nCEL expressions have access to the types needed to create JSON patches and objects:\n\n- 'JSONPatch' - CEL type of JSON Patch operations. JSONPatch has the fields 'op', 'from', 'path' and 'value'.\n See [JSON patch](https://jsonpatch.com/) for more details. The 'value' field may be set to any of: string,\n integer, array, map or object. If set, the 'path' and 'from' fields must be set to a\n [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901/) string, where the 'jsonpatch.escapeKey()' CEL\n function may be used to escape path keys containing '/' and '~'.\n- 'Object' - CEL type of the resource object. - 'Object.<fieldName>' - CEL type of object field (such as 'Object.spec') - 'Object.<fieldName1>.<fieldName2>...<fieldNameN>` - CEL type of nested field (such as 'Object.spec.containers')\n\nCEL expressions have access to the contents of the API request, organized into CEL variables as well as some other useful variables:\n\n- '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.\n For example, a variable named 'foo' can be accessed as 'variables.foo'.\n- 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.\n See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz\n- 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the\n request resource.\n\nCEL 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:\n\n- 'jsonpatch.escapeKey' - Performs JSONPatch key escaping. '~' and '/' are escaped as '~0' and `~1' respectively).\n\nOnly property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. Required.",
Type: []string{"string"},
Format: "",
},
},
},
},
},
}
}
func schema_k8sio_api_admissionregistration_v1alpha1_MatchCondition(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -3069,6 +3118,370 @@ func schema_k8sio_api_admissionregistration_v1alpha1_MatchResources(ref common.R
}
}
func schema_k8sio_api_admissionregistration_v1alpha1_MutatingAdmissionPolicy(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "MutatingAdmissionPolicy describes the definition of an admission mutation policy that mutates the object coming into admission chain.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Description: "Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata.",
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
"spec": {
SchemaProps: spec.SchemaProps{
Description: "Specification of the desired behavior of the MutatingAdmissionPolicy.",
Default: map[string]interface{}{},
Ref: ref("k8s.io/api/admissionregistration/v1alpha1.MutatingAdmissionPolicySpec"),
},
},
},
},
},
Dependencies: []string{
"k8s.io/api/admissionregistration/v1alpha1.MutatingAdmissionPolicySpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_k8sio_api_admissionregistration_v1alpha1_MutatingAdmissionPolicyBinding(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "MutatingAdmissionPolicyBinding binds the MutatingAdmissionPolicy with parametrized resources. MutatingAdmissionPolicyBinding and the optional parameter resource together define how cluster administrators configure policies for clusters.\n\nFor 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).\n\nAdding/removing policies, bindings, or params can not affect whether a given (policy, binding, param) combination is within its own CEL budget.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Description: "Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata.",
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
"spec": {
SchemaProps: spec.SchemaProps{
Description: "Specification of the desired behavior of the MutatingAdmissionPolicyBinding.",
Default: map[string]interface{}{},
Ref: ref("k8s.io/api/admissionregistration/v1alpha1.MutatingAdmissionPolicyBindingSpec"),
},
},
},
},
},
Dependencies: []string{
"k8s.io/api/admissionregistration/v1alpha1.MutatingAdmissionPolicyBindingSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_k8sio_api_admissionregistration_v1alpha1_MutatingAdmissionPolicyBindingList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "MutatingAdmissionPolicyBindingList is a list of MutatingAdmissionPolicyBinding.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Description: "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
},
},
"items": {
SchemaProps: spec.SchemaProps{
Description: "List of PolicyBinding.",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/api/admissionregistration/v1alpha1.MutatingAdmissionPolicyBinding"),
},
},
},
},
},
},
Required: []string{"items"},
},
},
Dependencies: []string{
"k8s.io/api/admissionregistration/v1alpha1.MutatingAdmissionPolicyBinding", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_k8sio_api_admissionregistration_v1alpha1_MutatingAdmissionPolicyBindingSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "MutatingAdmissionPolicyBindingSpec is the specification of the MutatingAdmissionPolicyBinding.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"policyName": {
SchemaProps: spec.SchemaProps{
Description: "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.",
Type: []string{"string"},
Format: "",
},
},
"paramRef": {
SchemaProps: spec.SchemaProps{
Description: "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.",
Ref: ref("k8s.io/api/admissionregistration/v1alpha1.ParamRef"),
},
},
"matchResources": {
SchemaProps: spec.SchemaProps{
Description: "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.",
Ref: ref("k8s.io/api/admissionregistration/v1alpha1.MatchResources"),
},
},
},
},
},
Dependencies: []string{
"k8s.io/api/admissionregistration/v1alpha1.MatchResources", "k8s.io/api/admissionregistration/v1alpha1.ParamRef"},
}
}
func schema_k8sio_api_admissionregistration_v1alpha1_MutatingAdmissionPolicyList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "MutatingAdmissionPolicyList is a list of MutatingAdmissionPolicy.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Description: "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
},
},
"items": {
SchemaProps: spec.SchemaProps{
Description: "List of ValidatingAdmissionPolicy.",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/api/admissionregistration/v1alpha1.MutatingAdmissionPolicy"),
},
},
},
},
},
},
Required: []string{"items"},
},
},
Dependencies: []string{
"k8s.io/api/admissionregistration/v1alpha1.MutatingAdmissionPolicy", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_k8sio_api_admissionregistration_v1alpha1_MutatingAdmissionPolicySpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "MutatingAdmissionPolicySpec is the specification of the desired behavior of the admission policy.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"paramKind": {
SchemaProps: spec.SchemaProps{
Description: "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.",
Ref: ref("k8s.io/api/admissionregistration/v1alpha1.ParamKind"),
},
},
"matchConstraints": {
SchemaProps: spec.SchemaProps{
Description: "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.",
Ref: ref("k8s.io/api/admissionregistration/v1alpha1.MatchResources"),
},
},
"variables": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Description: "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.\n\nThe 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.",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/api/admissionregistration/v1alpha1.Variable"),
},
},
},
},
},
"mutations": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Description: "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.",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/api/admissionregistration/v1alpha1.Mutation"),
},
},
},
},
},
"failurePolicy": {
SchemaProps: spec.SchemaProps{
Description: "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.\n\nA policy is invalid if paramKind refers to a non-existent Kind. A binding is invalid if paramRef.name refers to a non-existent resource.\n\nfailurePolicy does not define how validations that evaluate to false are handled.\n\nAllowed values are Ignore or Fail. Defaults to Fail.\n\nPossible enum values:\n - `\"Fail\"` means that an error calling the webhook causes the admission to fail.\n - `\"Ignore\"` means that an error calling the webhook is ignored.",
Type: []string{"string"},
Format: "",
Enum: []interface{}{"Fail", "Ignore"},
},
},
"matchConditions": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-map-keys": []interface{}{
"name",
},
"x-kubernetes-list-type": "map",
"x-kubernetes-patch-merge-key": "name",
"x-kubernetes-patch-strategy": "merge",
},
},
SchemaProps: spec.SchemaProps{
Description: "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.\n\nIf a parameter object is provided, it can be accessed via the `params` handle in the same manner as validation expressions.\n\nThe exact matching logic is (in order):\n 1. If ANY matchCondition evaluates to FALSE, the policy is skipped.\n 2. If ALL matchConditions evaluate to TRUE, the policy is evaluated.\n 3. If any matchCondition evaluates to an error (but none are FALSE):\n - If failurePolicy=Fail, reject the request\n - If failurePolicy=Ignore, the policy is skipped",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/api/admissionregistration/v1alpha1.MatchCondition"),
},
},
},
},
},
"reinvocationPolicy": {
SchemaProps: spec.SchemaProps{
Description: "reinvocationPolicy indicates whether mutations may be called multiple times per MutatingAdmissionPolicyBinding as part of a single admission evaluation. Allowed values are \"Never\" and \"IfNeeded\".\n\nNever: These mutations will not be called more than once per binding in a single admission evaluation.\n\nIfNeeded: 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.\n\nPossible enum values:\n - `\"IfNeeded\"` 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.\n - `\"Never\"` indicates that the mutation must not be called more than once in a single admission evaluation.",
Type: []string{"string"},
Format: "",
Enum: []interface{}{"IfNeeded", "Never"},
},
},
},
},
},
Dependencies: []string{
"k8s.io/api/admissionregistration/v1alpha1.MatchCondition", "k8s.io/api/admissionregistration/v1alpha1.MatchResources", "k8s.io/api/admissionregistration/v1alpha1.Mutation", "k8s.io/api/admissionregistration/v1alpha1.ParamKind", "k8s.io/api/admissionregistration/v1alpha1.Variable"},
}
}
func schema_k8sio_api_admissionregistration_v1alpha1_Mutation(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Mutation specifies the CEL expression which is used to apply the Mutation.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"patchType": {
SchemaProps: spec.SchemaProps{
Description: "patchType indicates the patch strategy used. Allowed values are \"ApplyConfiguration\" and \"JSONPatch\". Required.\n\n\nPossible enum values:\n - `\"ApplyConfiguration\"` ApplyConfiguration indicates that the mutation is using apply configuration to mutate the object.\n - `\"JSONPatch\"` JSONPatch indicates that the object is mutated through JSON Patch.",
Default: "",
Type: []string{"string"},
Format: "",
Enum: []interface{}{"ApplyConfiguration", "JSONPatch"},
},
},
"applyConfiguration": {
SchemaProps: spec.SchemaProps{
Description: "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.",
Ref: ref("k8s.io/api/admissionregistration/v1alpha1.ApplyConfiguration"),
},
},
"jsonPatch": {
SchemaProps: spec.SchemaProps{
Description: "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.",
Ref: ref("k8s.io/api/admissionregistration/v1alpha1.JSONPatch"),
},
},
},
Required: []string{"patchType"},
},
},
Dependencies: []string{
"k8s.io/api/admissionregistration/v1alpha1.ApplyConfiguration", "k8s.io/api/admissionregistration/v1alpha1.JSONPatch"},
}
}
func schema_k8sio_api_admissionregistration_v1alpha1_NamedRuleWithOperations(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{

View File

@@ -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"),

View File

@@ -20,7 +20,9 @@ package options
// This should probably be part of some configuration fed into the build for a
// given binary target.
import (
mutatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/mutating"
validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating"
// Admission policies
"k8s.io/kubernetes/plugin/pkg/admission/admit"
"k8s.io/kubernetes/plugin/pkg/admission/alwayspullimages"
@@ -95,6 +97,7 @@ var AllOrderedPlugins = []string{
// new admission plugins should generally be inserted above here
// webhook, resourcequota, and deny plugins must go at the end
mutatingadmissionpolicy.PluginName, // MutatingAdmissionPolicy
mutatingwebhook.PluginName, // MutatingAdmissionWebhook
validatingadmissionpolicy.PluginName, // ValidatingAdmissionPolicy
validatingwebhook.PluginName, // ValidatingAdmissionWebhook
@@ -159,6 +162,7 @@ func DefaultOffAdmissionPlugins() sets.Set[string] {
certsubjectrestriction.PluginName, // CertificateSubjectRestriction
defaultingressclass.PluginName, // DefaultIngressClass
podsecurity.PluginName, // PodSecurity
mutatingadmissionpolicy.PluginName, // Mutatingadmissionpolicy, only active when feature gate MutatingAdmissionpolicy is enabled
validatingadmissionpolicy.PluginName, // ValidatingAdmissionPolicy, only active when feature gate ValidatingAdmissionPolicy is enabled
)

View File

@@ -22,9 +22,9 @@ import (
)
func TestAdmissionPluginOrder(t *testing.T) {
// Ensure the last four admission plugins listed are webhooks, quota, and deny
// Sanity check that the order of admission ends with mutating(policy, webhook), validating(policy, webhook), quota, deny.
allplugins := strings.Join(AllOrderedPlugins, ",")
expectSuffix := ",MutatingAdmissionWebhook,ValidatingAdmissionPolicy,ValidatingAdmissionWebhook,ResourceQuota,AlwaysDeny"
expectSuffix := ",MutatingAdmissionPolicy,MutatingAdmissionWebhook,ValidatingAdmissionPolicy,ValidatingAdmissionWebhook,ResourceQuota,AlwaysDeny"
if !strings.HasSuffix(allplugins, expectSuffix) {
t.Fatalf("AllOrderedPlugins must end with ...%s", expectSuffix)
}

View File

@@ -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 := "<unset>"
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 := "<unset>"
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},

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -1,5 +1,5 @@
/*
Copyright 2024 The Kubernetes Authors.
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.
@@ -14,14 +14,4 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package common
import (
"github.com/google/cel-go/common/types/traits"
)
// RootTypeReferenceName is the root reference that all type names should start with.
const RootTypeReferenceName = "Object"
// ObjectTraits is the bitmask that represents traits that an object should have.
const ObjectTraits = traits.ContainerType
package mutatingadmissionpolicy // import "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingadmissionpolicy"

View File

@@ -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"}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,
},
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,255 @@
/*
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: "deny but relevant fields not updated",
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
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
},
},
{
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)
}

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,
},
},
},
}
}

View File

@@ -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
}

View File

@@ -20,10 +20,12 @@ 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"
)
@@ -31,6 +33,7 @@ func TestAuthorization(t *testing.T) {
for _, tc := range []struct {
name string
userInfo user.Info
obj *admissionregistration.ValidatingAdmissionPolicy
auth AuthFunc
resourceResolver resolver.ResourceResolverFunc
expectErr bool
@@ -39,6 +42,7 @@ func TestAuthorization(t *testing.T) {
name: "superuser",
userInfo: &user.DefaultInfo{Groups: []string{user.SystemPrivilegedGroup}},
expectErr: false, // success despite always-denying authorizer
obj: validValidatingAdmissionPolicy(),
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
return authorizer.DecisionDeny, "", nil
},
@@ -46,6 +50,7 @@ func TestAuthorization(t *testing.T) {
{
name: "authorized",
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
obj: validValidatingAdmissionPolicy(),
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
if a.GetResource() == "replicalimits" {
return authorizer.DecisionAllow, "", nil
@@ -64,6 +69,7 @@ func TestAuthorization(t *testing.T) {
{
name: "denied",
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
obj: validValidatingAdmissionPolicy(),
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
if a.GetResource() == "configmaps" {
return authorizer.DecisionAllow, "", nil
@@ -79,22 +85,36 @@ func TestAuthorization(t *testing.T) {
},
expectErr: true,
},
{
name: "param not found",
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
obj: validValidatingAdmissionPolicy(),
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, validValidatingAdmissionPolicy())
errs := strategy.Validate(ctx, tc.obj)
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 := validValidatingAdmissionPolicy()
objWithUpdatedParamKind := obj.DeepCopy()
objWithUpdatedParamKind := tc.obj.DeepCopy()
objWithUpdatedParamKind.Spec.ParamKind.APIVersion += "1"
errs := strategy.ValidateUpdate(ctx, obj, objWithUpdatedParamKind)
errs := strategy.ValidateUpdate(ctx, tc.obj, objWithUpdatedParamKind)
if len(errs) > 0 != tc.expectErr {
t.Errorf("expected error: %v but got error: %v", tc.expectErr, errs)
}

View File

@@ -23,6 +23,7 @@ import (
"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"
@@ -201,7 +202,7 @@ func newValidatingAdmissionPolicy(name string) *admissionregistration.Validating
}
func newInsecureStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
return newStorage(t, nil, nil)
return newStorage(t, nil, resolver.ResourceResolverFunc(replicaLimitsResolver))
}
func newStorage(t *testing.T, authorizer authorizer.Authorizer, resourceResolver resolver.ResourceResolver) (*REST, *etcd3testing.EtcdTestServer) {
@@ -225,3 +226,11 @@ func TestCategories(t *testing.T) {
expected := []string{"api-extensions"}
registrytest.AssertCategories(t, storage, expected)
}
func replicaLimitsResolver(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
return schema.GroupVersionResource{
Group: "rules.example.com",
Version: "v1",
Resource: "replicalimits",
}, nil
}

View File

@@ -20,12 +20,14 @@ 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 TestValidatingAdmissionPolicyStrategy(t *testing.T) {
strategy := NewStrategy(nil, nil)
strategy := NewStrategy(nil, replicaLimitsResolver)
ctx := genericapirequest.NewDefaultContext()
if strategy.NamespaceScoped() {
t.Error("ValidatingAdmissionPolicy strategy must be cluster scoped")
@@ -49,6 +51,15 @@ func TestValidatingAdmissionPolicyStrategy(t *testing.T) {
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 validValidatingAdmissionPolicy() *admissionregistration.ValidatingAdmissionPolicy {
ignore := admissionregistration.Ignore
return &admissionregistration.ValidatingAdmissionPolicy{

View File

@@ -55,7 +55,10 @@ func (v *validatingAdmissionPolicyBindingStrategy) authorizeUpdate(ctx context.C
}
func (v *validatingAdmissionPolicyBindingStrategy) authorize(ctx context.Context, binding *admissionregistration.ValidatingAdmissionPolicyBinding) error {
if v.authorizer == nil || v.resourceResolver == nil || binding.Spec.ParamRef == nil {
if v.resourceResolver == nil {
return fmt.Errorf(`unexpected internal error: resourceResolver is nil`)
}
if v.authorizer == nil || binding.Spec.ParamRef == nil {
return nil
}
@@ -72,13 +75,21 @@ func (v *validatingAdmissionPolicyBindingStrategy) authorize(ctx context.Context
// default to requiring permissions on all group/version/resources
resource, apiGroup, apiVersion := "*", "*", "*"
if policy, err := v.policyGetter.GetValidatingAdmissionPolicy(ctx, binding.Spec.PolicyName); err == nil && policy.Spec.ParamKind != nil {
var policyErr, gvParseErr, gvrResolveErr error
var policy *admissionregistration.ValidatingAdmissionPolicy
policy, policyErr = v.policyGetter.GetValidatingAdmissionPolicy(ctx, binding.Spec.PolicyName)
if policyErr == nil && policy.Spec.ParamKind != nil {
paramKind := policy.Spec.ParamKind
if gv, err := schema.ParseGroupVersion(paramKind.APIVersion); err == nil {
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
if gvr, err := v.resourceResolver.Resolve(gv.WithKind(paramKind.Kind)); err == nil {
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
}
@@ -107,9 +118,18 @@ func (v *validatingAdmissionPolicyBindingStrategy) authorize(ctx context.Context
d, _, err := v.authorizer.Authorize(ctx, attrs)
if err != nil {
return err
return fmt.Errorf(`failed to authorize request: %w`, 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`, user, verb)
}

View File

@@ -18,8 +18,12 @@ package validatingadmissionpolicybinding
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"
@@ -31,17 +35,16 @@ import (
func TestAuthorization(t *testing.T) {
for _, tc := range []struct {
name string
userInfo user.Info
auth AuthFunc
policyGetter PolicyGetterFunc
resourceResolver resolver.ResourceResolverFunc
expectErr bool
name string
userInfo user.Info
auth AuthFunc
policyGetter PolicyGetterFunc
resourceResolver resolver.ResourceResolverFunc
expectErrContains string
}{
{
name: "superuser",
userInfo: &user.DefaultInfo{Groups: []string{user.SystemPrivilegedGroup}},
expectErr: false, // success despite always-denying authorizer
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
},
@@ -70,7 +73,6 @@ func TestAuthorization(t *testing.T) {
Resource: "configmaps",
}, nil
},
expectErr: false,
},
{
name: "denied",
@@ -96,7 +98,76 @@ func TestAuthorization(t *testing.T) {
Resource: "params",
}, nil
},
expectErr: true,
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.ValidatingAdmissionPolicy, error) {
return &admissionregistration.ValidatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{Name: "replicalimit-policy.example.com"},
Spec: admissionregistration.ValidatingAdmissionPolicySpec{
ParamKind: &admissionregistration.ParamKind{Kind: "ConfigMap", APIVersion: "invalid"},
},
}, 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.ValidatingAdmissionPolicy, error) {
return &admissionregistration.ValidatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{Name: "replicalimit-policy.example.com"},
Spec: admissionregistration.ValidatingAdmissionPolicySpec{
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.ValidatingAdmissionPolicy, 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) {
@@ -105,8 +176,8 @@ func TestAuthorization(t *testing.T) {
ctx := request.WithUser(context.Background(), tc.userInfo)
for _, obj := range validPolicyBindings() {
errs := strategy.Validate(ctx, obj)
if len(errs) > 0 != tc.expectErr {
t.Errorf("expected error: %v but got error: %v", tc.expectErr, errs)
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)
}
}
})
@@ -140,8 +211,8 @@ func TestAuthorization(t *testing.T) {
}
}
errs := strategy.ValidateUpdate(ctx, obj, objWithChangedParamRef)
if len(errs) > 0 != tc.expectErr {
t.Errorf("expected error: %v but got error: %v", tc.expectErr, errs)
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)
}
}
})

View File

@@ -23,6 +23,7 @@ import (
"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"
@@ -230,7 +231,7 @@ func newPolicyBinding(name string) *admissionregistration.ValidatingAdmissionPol
}
func newInsecureStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
return newStorage(t, nil, nil, nil)
return newStorage(t, nil, nil, replicaLimitsResolver)
}
func newStorage(t *testing.T, authorizer authorizer.Authorizer, policyGetter PolicyGetter, resourceResolver resolver.ResourceResolver) (*REST, *etcd3testing.EtcdTestServer) {
@@ -254,3 +255,11 @@ func TestCategories(t *testing.T) {
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
}

View File

@@ -20,13 +20,15 @@ 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, nil)
strategy := NewStrategy(nil, nil, replicaLimitsResolver)
ctx := genericapirequest.NewDefaultContext()
if strategy.NamespaceScoped() {
t.Error("PolicyBinding strategy must be cluster scoped")
@@ -52,6 +54,14 @@ func TestPolicyBindingStrategy(t *testing.T) {
}
}
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.ValidatingAdmissionPolicyBinding {
denyAction := admissionregistration.DenyAction
return []*admissionregistration.ValidatingAdmissionPolicyBinding{

File diff suppressed because it is too large Load Diff

View File

@@ -29,6 +29,51 @@ import "k8s.io/apimachinery/pkg/runtime/schema/generated.proto";
// Package-wide variables from generator "generated".
option go_package = "k8s.io/api/admissionregistration/v1alpha1";
// ApplyConfiguration defines the desired configuration values of an object.
message ApplyConfiguration {
// 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.<fieldName>' - CEL type of object field (such as 'Object.spec')
// - 'Object.<fieldName1>.<fieldName2>...<fieldNameN>` - 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.
optional string expression = 1;
}
// AuditAnnotation describes how to produce an audit annotation for an API request.
message AuditAnnotation {
// key specifies the audit annotation key. The audit annotation keys of
@@ -79,6 +124,75 @@ message ExpressionWarning {
optional string warning = 3;
}
// JSONPatch defines a JSON Patch.
message JSONPatch {
// 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.<fieldName>' - CEL type of object field (such as 'Object.spec')
// - 'Object.<fieldName1>.<fieldName2>...<fieldNameN>` - 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.
optional string expression = 1;
}
message MatchCondition {
// Name is an identifier for this match condition, used for strategic merging of MatchConditions,
// as well as providing an identifier for logging purposes. A good name should be descriptive of
@@ -202,6 +316,193 @@ message MatchResources {
optional string matchPolicy = 7;
}
// MutatingAdmissionPolicy describes the definition of an admission mutation policy that mutates the object coming into admission chain.
message MutatingAdmissionPolicy {
// Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata.
// +optional
optional .k8s.io.apimachinery.pkg.apis.meta.v1.ObjectMeta metadata = 1;
// Specification of the desired behavior of the MutatingAdmissionPolicy.
optional MutatingAdmissionPolicySpec spec = 2;
}
// 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.
message MutatingAdmissionPolicyBinding {
// Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata.
// +optional
optional .k8s.io.apimachinery.pkg.apis.meta.v1.ObjectMeta metadata = 1;
// Specification of the desired behavior of the MutatingAdmissionPolicyBinding.
optional MutatingAdmissionPolicyBindingSpec spec = 2;
}
// MutatingAdmissionPolicyBindingList is a list of MutatingAdmissionPolicyBinding.
message MutatingAdmissionPolicyBindingList {
// Standard list metadata.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
// +optional
optional .k8s.io.apimachinery.pkg.apis.meta.v1.ListMeta metadata = 1;
// List of PolicyBinding.
repeated MutatingAdmissionPolicyBinding items = 2;
}
// MutatingAdmissionPolicyBindingSpec is the specification of the MutatingAdmissionPolicyBinding.
message MutatingAdmissionPolicyBindingSpec {
// 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.
optional string policyName = 1;
// 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
optional ParamRef paramRef = 2;
// 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
optional MatchResources matchResources = 3;
}
// MutatingAdmissionPolicyList is a list of MutatingAdmissionPolicy.
message MutatingAdmissionPolicyList {
// Standard list metadata.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
// +optional
optional .k8s.io.apimachinery.pkg.apis.meta.v1.ListMeta metadata = 1;
// List of ValidatingAdmissionPolicy.
repeated MutatingAdmissionPolicy items = 2;
}
// MutatingAdmissionPolicySpec is the specification of the desired behavior of the admission policy.
message MutatingAdmissionPolicySpec {
// 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
optional ParamKind paramKind = 1;
// 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.
optional MatchResources matchConstraints = 2;
// 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
repeated Variable variables = 3;
// 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
repeated Mutation mutations = 4;
// 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
optional string failurePolicy = 5;
// 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
repeated MatchCondition matchConditions = 6;
// 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.
optional string reinvocationPolicy = 7;
}
// Mutation specifies the CEL expression which is used to apply the Mutation.
message Mutation {
// patchType indicates the patch strategy used.
// Allowed values are "ApplyConfiguration" and "JSONPatch".
// Required.
//
// +unionDiscriminator
optional string patchType = 2;
// 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.
optional ApplyConfiguration applyConfiguration = 3;
// 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.
optional JSONPatch jsonPatch = 4;
}
// NamedRuleWithOperations is a tuple of Operations and Resources with ResourceNames.
// +structType=atomic
message NamedRuleWithOperations {

View File

@@ -50,6 +50,10 @@ func addKnownTypes(scheme *runtime.Scheme) error {
&ValidatingAdmissionPolicyList{},
&ValidatingAdmissionPolicyBinding{},
&ValidatingAdmissionPolicyBindingList{},
&MutatingAdmissionPolicy{},
&MutatingAdmissionPolicyList{},
&MutatingAdmissionPolicyBinding{},
&MutatingAdmissionPolicyBindingList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil

View File

@@ -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.<fieldName>' - CEL type of object field (such as 'Object.spec')
// - 'Object.<fieldName1>.<fieldName2>...<fieldNameN>` - 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.<fieldName>' - CEL type of object field (such as 'Object.spec')
// - 'Object.<fieldName1>.<fieldName2>...<fieldNameN>` - 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"`
}

View File

@@ -27,6 +27,15 @@ package v1alpha1
// Those methods can be generated by using hack/update-codegen.sh
// AUTO-GENERATED FUNCTIONS START HERE. DO NOT EDIT.
var map_ApplyConfiguration = map[string]string{
"": "ApplyConfiguration defines the desired configuration values of an object.",
"expression": "expression will be evaluated by CEL to create an apply configuration. ref: https://github.com/google/cel-spec\n\nApply configurations are declared in CEL using object initialization. For example, this CEL expression returns an apply configuration to set a single field:\n\n\tObject{\n\t spec: Object.spec{\n\t serviceAccountName: \"example\"\n\t }\n\t}\n\nApply configurations may not modify atomic structs, maps or arrays due to the risk of accidental deletion of values not included in the apply configuration.\n\nCEL expressions have access to the object types needed to create apply configurations:\n\n- 'Object' - CEL type of the resource object. - 'Object.<fieldName>' - CEL type of object field (such as 'Object.spec') - 'Object.<fieldName1>.<fieldName2>...<fieldNameN>` - CEL type of nested field (such as 'Object.spec.containers')\n\nCEL expressions have access to the contents of the API request, organized into CEL variables as well as some other useful variables:\n\n- '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.\n For example, a variable named 'foo' can be accessed as 'variables.foo'.\n- 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.\n See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz\n- 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the\n request resource.\n\nThe `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the object. No other metadata properties are accessible.\n\nOnly property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. Required.",
}
func (ApplyConfiguration) SwaggerDoc() map[string]string {
return map_ApplyConfiguration
}
var map_AuditAnnotation = map[string]string{
"": "AuditAnnotation describes how to produce an audit annotation for an API request.",
"key": "key specifies the audit annotation key. The audit annotation keys of a ValidatingAdmissionPolicy must be unique. The key must be a qualified name ([A-Za-z0-9][-A-Za-z0-9_.]*) no more than 63 bytes in length.\n\nThe key is combined with the resource name of the ValidatingAdmissionPolicy to construct an audit annotation key: \"{ValidatingAdmissionPolicy name}/{key}\".\n\nIf an admission webhook uses the same resource name as this ValidatingAdmissionPolicy and the same audit annotation key, the annotation key will be identical. In this case, the first annotation written with the key will be included in the audit event and all subsequent annotations with the same key will be discarded.\n\nRequired.",
@@ -47,6 +56,15 @@ func (ExpressionWarning) SwaggerDoc() map[string]string {
return map_ExpressionWarning
}
var map_JSONPatch = map[string]string{
"": "JSONPatch defines a JSON Patch.",
"expression": "expression will be evaluated by CEL to create a [JSON patch](https://jsonpatch.com/). ref: https://github.com/google/cel-spec\n\nexpression must return an array of JSONPatch values.\n\nFor example, this CEL expression returns a JSON patch to conditionally modify a value:\n\n\t [\n\t JSONPatch{op: \"test\", path: \"/spec/example\", value: \"Red\"},\n\t JSONPatch{op: \"replace\", path: \"/spec/example\", value: \"Green\"}\n\t ]\n\nTo define an object for the patch value, use Object types. For example:\n\n\t [\n\t JSONPatch{\n\t op: \"add\",\n\t path: \"/spec/selector\",\n\t value: Object.spec.selector{matchLabels: {\"environment\": \"test\"}}\n\t }\n\t ]\n\nTo use strings containing '/' and '~' as JSONPatch path keys, use \"jsonpatch.escapeKey\". For example:\n\n\t [\n\t JSONPatch{\n\t op: \"add\",\n\t path: \"/metadata/labels/\" + jsonpatch.escapeKey(\"example.com/environment\"),\n\t value: \"test\"\n\t },\n\t ]\n\nCEL expressions have access to the types needed to create JSON patches and objects:\n\n- 'JSONPatch' - CEL type of JSON Patch operations. JSONPatch has the fields 'op', 'from', 'path' and 'value'.\n See [JSON patch](https://jsonpatch.com/) for more details. The 'value' field may be set to any of: string,\n integer, array, map or object. If set, the 'path' and 'from' fields must be set to a\n [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901/) string, where the 'jsonpatch.escapeKey()' CEL\n function may be used to escape path keys containing '/' and '~'.\n- 'Object' - CEL type of the resource object. - 'Object.<fieldName>' - CEL type of object field (such as 'Object.spec') - 'Object.<fieldName1>.<fieldName2>...<fieldNameN>` - CEL type of nested field (such as 'Object.spec.containers')\n\nCEL expressions have access to the contents of the API request, organized into CEL variables as well as some other useful variables:\n\n- '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.\n For example, a variable named 'foo' can be accessed as 'variables.foo'.\n- 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.\n See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz\n- 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the\n request resource.\n\nCEL 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:\n\n- 'jsonpatch.escapeKey' - Performs JSONPatch key escaping. '~' and '/' are escaped as '~0' and `~1' respectively).\n\nOnly property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. Required.",
}
func (JSONPatch) SwaggerDoc() map[string]string {
return map_JSONPatch
}
var map_MatchResources = map[string]string{
"": "MatchResources decides whether to run the admission control policy on an object based on whether it meets the match criteria. The exclude rules take precedence over include rules (if a resource matches both, it is excluded)",
"namespaceSelector": "NamespaceSelector decides whether to run the admission control policy on an object based on whether the namespace for that object matches the selector. If the object itself is a namespace, the matching is performed on object.metadata.labels. If the object is another cluster scoped resource, it never skips the policy.\n\nFor example, to run the webhook on any objects whose namespace is not associated with \"runlevel\" of \"0\" or \"1\"; you will set the selector as follows: \"namespaceSelector\": {\n \"matchExpressions\": [\n {\n \"key\": \"runlevel\",\n \"operator\": \"NotIn\",\n \"values\": [\n \"0\",\n \"1\"\n ]\n }\n ]\n}\n\nIf instead you want to only run the policy on any objects whose namespace is associated with the \"environment\" of \"prod\" or \"staging\"; you will set the selector as follows: \"namespaceSelector\": {\n \"matchExpressions\": [\n {\n \"key\": \"environment\",\n \"operator\": \"In\",\n \"values\": [\n \"prod\",\n \"staging\"\n ]\n }\n ]\n}\n\nSee https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ for more examples of label selectors.\n\nDefault to the empty LabelSelector, which matches everything.",
@@ -60,6 +78,83 @@ func (MatchResources) SwaggerDoc() map[string]string {
return map_MatchResources
}
var map_MutatingAdmissionPolicy = map[string]string{
"": "MutatingAdmissionPolicy describes the definition of an admission mutation policy that mutates the object coming into admission chain.",
"metadata": "Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata.",
"spec": "Specification of the desired behavior of the MutatingAdmissionPolicy.",
}
func (MutatingAdmissionPolicy) SwaggerDoc() map[string]string {
return map_MutatingAdmissionPolicy
}
var map_MutatingAdmissionPolicyBinding = map[string]string{
"": "MutatingAdmissionPolicyBinding binds the MutatingAdmissionPolicy with parametrized resources. MutatingAdmissionPolicyBinding and the optional parameter resource together define how cluster administrators configure policies for clusters.\n\nFor 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).\n\nAdding/removing policies, bindings, or params can not affect whether a given (policy, binding, param) combination is within its own CEL budget.",
"metadata": "Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata.",
"spec": "Specification of the desired behavior of the MutatingAdmissionPolicyBinding.",
}
func (MutatingAdmissionPolicyBinding) SwaggerDoc() map[string]string {
return map_MutatingAdmissionPolicyBinding
}
var map_MutatingAdmissionPolicyBindingList = map[string]string{
"": "MutatingAdmissionPolicyBindingList is a list of MutatingAdmissionPolicyBinding.",
"metadata": "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
"items": "List of PolicyBinding.",
}
func (MutatingAdmissionPolicyBindingList) SwaggerDoc() map[string]string {
return map_MutatingAdmissionPolicyBindingList
}
var map_MutatingAdmissionPolicyBindingSpec = map[string]string{
"": "MutatingAdmissionPolicyBindingSpec is the specification of the MutatingAdmissionPolicyBinding.",
"policyName": "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.",
"paramRef": "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.",
"matchResources": "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.",
}
func (MutatingAdmissionPolicyBindingSpec) SwaggerDoc() map[string]string {
return map_MutatingAdmissionPolicyBindingSpec
}
var map_MutatingAdmissionPolicyList = map[string]string{
"": "MutatingAdmissionPolicyList is a list of MutatingAdmissionPolicy.",
"metadata": "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
"items": "List of ValidatingAdmissionPolicy.",
}
func (MutatingAdmissionPolicyList) SwaggerDoc() map[string]string {
return map_MutatingAdmissionPolicyList
}
var map_MutatingAdmissionPolicySpec = map[string]string{
"": "MutatingAdmissionPolicySpec is the specification of the desired behavior of the admission policy.",
"paramKind": "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.",
"matchConstraints": "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.",
"variables": "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.\n\nThe 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.",
"mutations": "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.",
"failurePolicy": "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.\n\nA policy is invalid if paramKind refers to a non-existent Kind. A binding is invalid if paramRef.name refers to a non-existent resource.\n\nfailurePolicy does not define how validations that evaluate to false are handled.\n\nAllowed values are Ignore or Fail. Defaults to Fail.",
"matchConditions": "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.\n\nIf a parameter object is provided, it can be accessed via the `params` handle in the same manner as validation expressions.\n\nThe exact matching logic is (in order):\n 1. If ANY matchCondition evaluates to FALSE, the policy is skipped.\n 2. If ALL matchConditions evaluate to TRUE, the policy is evaluated.\n 3. If any matchCondition evaluates to an error (but none are FALSE):\n - If failurePolicy=Fail, reject the request\n - If failurePolicy=Ignore, the policy is skipped",
"reinvocationPolicy": "reinvocationPolicy indicates whether mutations may be called multiple times per MutatingAdmissionPolicyBinding as part of a single admission evaluation. Allowed values are \"Never\" and \"IfNeeded\".\n\nNever: These mutations will not be called more than once per binding in a single admission evaluation.\n\nIfNeeded: 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.",
}
func (MutatingAdmissionPolicySpec) SwaggerDoc() map[string]string {
return map_MutatingAdmissionPolicySpec
}
var map_Mutation = map[string]string{
"": "Mutation specifies the CEL expression which is used to apply the Mutation.",
"patchType": "patchType indicates the patch strategy used. Allowed values are \"ApplyConfiguration\" and \"JSONPatch\". Required.",
"applyConfiguration": "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.",
"jsonPatch": "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.",
}
func (Mutation) SwaggerDoc() map[string]string {
return map_Mutation
}
var map_NamedRuleWithOperations = map[string]string{
"": "NamedRuleWithOperations is a tuple of Operations and Resources with ResourceNames.",
"resourceNames": "ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed.",

View File

@@ -26,6 +26,22 @@ import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplyConfiguration) DeepCopyInto(out *ApplyConfiguration) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplyConfiguration.
func (in *ApplyConfiguration) DeepCopy() *ApplyConfiguration {
if in == nil {
return nil
}
out := new(ApplyConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AuditAnnotation) DeepCopyInto(out *AuditAnnotation) {
*out = *in
@@ -58,6 +74,22 @@ func (in *ExpressionWarning) DeepCopy() *ExpressionWarning {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *JSONPatch) DeepCopyInto(out *JSONPatch) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JSONPatch.
func (in *JSONPatch) DeepCopy() *JSONPatch {
if in == nil {
return nil
}
out := new(JSONPatch)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MatchCondition) DeepCopyInto(out *MatchCondition) {
*out = *in
@@ -119,6 +151,226 @@ func (in *MatchResources) DeepCopy() *MatchResources {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MutatingAdmissionPolicy) DeepCopyInto(out *MutatingAdmissionPolicy) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MutatingAdmissionPolicy.
func (in *MutatingAdmissionPolicy) DeepCopy() *MutatingAdmissionPolicy {
if in == nil {
return nil
}
out := new(MutatingAdmissionPolicy)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *MutatingAdmissionPolicy) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MutatingAdmissionPolicyBinding) DeepCopyInto(out *MutatingAdmissionPolicyBinding) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MutatingAdmissionPolicyBinding.
func (in *MutatingAdmissionPolicyBinding) DeepCopy() *MutatingAdmissionPolicyBinding {
if in == nil {
return nil
}
out := new(MutatingAdmissionPolicyBinding)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *MutatingAdmissionPolicyBinding) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MutatingAdmissionPolicyBindingList) DeepCopyInto(out *MutatingAdmissionPolicyBindingList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]MutatingAdmissionPolicyBinding, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MutatingAdmissionPolicyBindingList.
func (in *MutatingAdmissionPolicyBindingList) DeepCopy() *MutatingAdmissionPolicyBindingList {
if in == nil {
return nil
}
out := new(MutatingAdmissionPolicyBindingList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *MutatingAdmissionPolicyBindingList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MutatingAdmissionPolicyBindingSpec) DeepCopyInto(out *MutatingAdmissionPolicyBindingSpec) {
*out = *in
if in.ParamRef != nil {
in, out := &in.ParamRef, &out.ParamRef
*out = new(ParamRef)
(*in).DeepCopyInto(*out)
}
if in.MatchResources != nil {
in, out := &in.MatchResources, &out.MatchResources
*out = new(MatchResources)
(*in).DeepCopyInto(*out)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MutatingAdmissionPolicyBindingSpec.
func (in *MutatingAdmissionPolicyBindingSpec) DeepCopy() *MutatingAdmissionPolicyBindingSpec {
if in == nil {
return nil
}
out := new(MutatingAdmissionPolicyBindingSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MutatingAdmissionPolicyList) DeepCopyInto(out *MutatingAdmissionPolicyList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]MutatingAdmissionPolicy, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MutatingAdmissionPolicyList.
func (in *MutatingAdmissionPolicyList) DeepCopy() *MutatingAdmissionPolicyList {
if in == nil {
return nil
}
out := new(MutatingAdmissionPolicyList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *MutatingAdmissionPolicyList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MutatingAdmissionPolicySpec) DeepCopyInto(out *MutatingAdmissionPolicySpec) {
*out = *in
if in.ParamKind != nil {
in, out := &in.ParamKind, &out.ParamKind
*out = new(ParamKind)
**out = **in
}
if in.MatchConstraints != nil {
in, out := &in.MatchConstraints, &out.MatchConstraints
*out = new(MatchResources)
(*in).DeepCopyInto(*out)
}
if in.Variables != nil {
in, out := &in.Variables, &out.Variables
*out = make([]Variable, len(*in))
copy(*out, *in)
}
if in.Mutations != nil {
in, out := &in.Mutations, &out.Mutations
*out = make([]Mutation, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.FailurePolicy != nil {
in, out := &in.FailurePolicy, &out.FailurePolicy
*out = new(FailurePolicyType)
**out = **in
}
if in.MatchConditions != nil {
in, out := &in.MatchConditions, &out.MatchConditions
*out = make([]MatchCondition, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MutatingAdmissionPolicySpec.
func (in *MutatingAdmissionPolicySpec) DeepCopy() *MutatingAdmissionPolicySpec {
if in == nil {
return nil
}
out := new(MutatingAdmissionPolicySpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Mutation) DeepCopyInto(out *Mutation) {
*out = *in
if in.ApplyConfiguration != nil {
in, out := &in.ApplyConfiguration, &out.ApplyConfiguration
*out = new(ApplyConfiguration)
**out = **in
}
if in.JSONPatch != nil {
in, out := &in.JSONPatch, &out.JSONPatch
*out = new(JSONPatch)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Mutation.
func (in *Mutation) DeepCopy() *Mutation {
if in == nil {
return nil
}
out := new(Mutation)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamedRuleWithOperations) DeepCopyInto(out *NamedRuleWithOperations) {
*out = *in

View File

@@ -21,6 +21,78 @@ limitations under the License.
package v1alpha1
// APILifecycleIntroduced is an autogenerated function, returning the release in which the API struct was introduced as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:introduced" tags in types.go.
func (in *MutatingAdmissionPolicy) APILifecycleIntroduced() (major, minor int) {
return 1, 32
}
// APILifecycleDeprecated is an autogenerated function, returning the release in which the API struct was or will be deprecated as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:deprecated" tags in types.go or "k8s:prerelease-lifecycle-gen:introduced" plus three minor.
func (in *MutatingAdmissionPolicy) APILifecycleDeprecated() (major, minor int) {
return 1, 35
}
// APILifecycleRemoved is an autogenerated function, returning the release in which the API is no longer served as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:removed" tags in types.go or "k8s:prerelease-lifecycle-gen:deprecated" plus three minor.
func (in *MutatingAdmissionPolicy) APILifecycleRemoved() (major, minor int) {
return 1, 38
}
// APILifecycleIntroduced is an autogenerated function, returning the release in which the API struct was introduced as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:introduced" tags in types.go.
func (in *MutatingAdmissionPolicyBinding) APILifecycleIntroduced() (major, minor int) {
return 1, 32
}
// APILifecycleDeprecated is an autogenerated function, returning the release in which the API struct was or will be deprecated as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:deprecated" tags in types.go or "k8s:prerelease-lifecycle-gen:introduced" plus three minor.
func (in *MutatingAdmissionPolicyBinding) APILifecycleDeprecated() (major, minor int) {
return 1, 35
}
// APILifecycleRemoved is an autogenerated function, returning the release in which the API is no longer served as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:removed" tags in types.go or "k8s:prerelease-lifecycle-gen:deprecated" plus three minor.
func (in *MutatingAdmissionPolicyBinding) APILifecycleRemoved() (major, minor int) {
return 1, 38
}
// APILifecycleIntroduced is an autogenerated function, returning the release in which the API struct was introduced as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:introduced" tags in types.go.
func (in *MutatingAdmissionPolicyBindingList) APILifecycleIntroduced() (major, minor int) {
return 1, 32
}
// APILifecycleDeprecated is an autogenerated function, returning the release in which the API struct was or will be deprecated as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:deprecated" tags in types.go or "k8s:prerelease-lifecycle-gen:introduced" plus three minor.
func (in *MutatingAdmissionPolicyBindingList) APILifecycleDeprecated() (major, minor int) {
return 1, 35
}
// APILifecycleRemoved is an autogenerated function, returning the release in which the API is no longer served as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:removed" tags in types.go or "k8s:prerelease-lifecycle-gen:deprecated" plus three minor.
func (in *MutatingAdmissionPolicyBindingList) APILifecycleRemoved() (major, minor int) {
return 1, 38
}
// APILifecycleIntroduced is an autogenerated function, returning the release in which the API struct was introduced as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:introduced" tags in types.go.
func (in *MutatingAdmissionPolicyList) APILifecycleIntroduced() (major, minor int) {
return 1, 32
}
// APILifecycleDeprecated is an autogenerated function, returning the release in which the API struct was or will be deprecated as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:deprecated" tags in types.go or "k8s:prerelease-lifecycle-gen:introduced" plus three minor.
func (in *MutatingAdmissionPolicyList) APILifecycleDeprecated() (major, minor int) {
return 1, 35
}
// APILifecycleRemoved is an autogenerated function, returning the release in which the API is no longer served as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:removed" tags in types.go or "k8s:prerelease-lifecycle-gen:deprecated" plus three minor.
func (in *MutatingAdmissionPolicyList) APILifecycleRemoved() (major, minor int) {
return 1, 38
}
// APILifecycleIntroduced is an autogenerated function, returning the release in which the API struct was introduced as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:introduced" tags in types.go.
func (in *ValidatingAdmissionPolicy) APILifecycleIntroduced() (major, minor int) {

View File

@@ -0,0 +1,148 @@
{
"kind": "MutatingAdmissionPolicy",
"apiVersion": "admissionregistration.k8s.io/v1alpha1",
"metadata": {
"name": "nameValue",
"generateName": "generateNameValue",
"namespace": "namespaceValue",
"selfLink": "selfLinkValue",
"uid": "uidValue",
"resourceVersion": "resourceVersionValue",
"generation": 7,
"creationTimestamp": "2008-01-01T01:01:01Z",
"deletionTimestamp": "2009-01-01T01:01:01Z",
"deletionGracePeriodSeconds": 10,
"labels": {
"labelsKey": "labelsValue"
},
"annotations": {
"annotationsKey": "annotationsValue"
},
"ownerReferences": [
{
"apiVersion": "apiVersionValue",
"kind": "kindValue",
"name": "nameValue",
"uid": "uidValue",
"controller": true,
"blockOwnerDeletion": true
}
],
"finalizers": [
"finalizersValue"
],
"managedFields": [
{
"manager": "managerValue",
"operation": "operationValue",
"apiVersion": "apiVersionValue",
"time": "2004-01-01T01:01:01Z",
"fieldsType": "fieldsTypeValue",
"fieldsV1": {},
"subresource": "subresourceValue"
}
]
},
"spec": {
"paramKind": {
"apiVersion": "apiVersionValue",
"kind": "kindValue"
},
"matchConstraints": {
"namespaceSelector": {
"matchLabels": {
"matchLabelsKey": "matchLabelsValue"
},
"matchExpressions": [
{
"key": "keyValue",
"operator": "operatorValue",
"values": [
"valuesValue"
]
}
]
},
"objectSelector": {
"matchLabels": {
"matchLabelsKey": "matchLabelsValue"
},
"matchExpressions": [
{
"key": "keyValue",
"operator": "operatorValue",
"values": [
"valuesValue"
]
}
]
},
"resourceRules": [
{
"resourceNames": [
"resourceNamesValue"
],
"operations": [
"operationsValue"
],
"apiGroups": [
"apiGroupsValue"
],
"apiVersions": [
"apiVersionsValue"
],
"resources": [
"resourcesValue"
],
"scope": "scopeValue"
}
],
"excludeResourceRules": [
{
"resourceNames": [
"resourceNamesValue"
],
"operations": [
"operationsValue"
],
"apiGroups": [
"apiGroupsValue"
],
"apiVersions": [
"apiVersionsValue"
],
"resources": [
"resourcesValue"
],
"scope": "scopeValue"
}
],
"matchPolicy": "matchPolicyValue"
},
"variables": [
{
"name": "nameValue",
"expression": "expressionValue"
}
],
"mutations": [
{
"patchType": "patchTypeValue",
"applyConfiguration": {
"expression": "expressionValue"
},
"jsonPatch": {
"expression": "expressionValue"
}
}
],
"failurePolicy": "failurePolicyValue",
"matchConditions": [
{
"name": "nameValue",
"expression": "expressionValue"
}
],
"reinvocationPolicy": "reinvocationPolicyValue"
}
}

View File

@@ -0,0 +1,94 @@
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: MutatingAdmissionPolicy
metadata:
annotations:
annotationsKey: annotationsValue
creationTimestamp: "2008-01-01T01:01:01Z"
deletionGracePeriodSeconds: 10
deletionTimestamp: "2009-01-01T01:01:01Z"
finalizers:
- finalizersValue
generateName: generateNameValue
generation: 7
labels:
labelsKey: labelsValue
managedFields:
- apiVersion: apiVersionValue
fieldsType: fieldsTypeValue
fieldsV1: {}
manager: managerValue
operation: operationValue
subresource: subresourceValue
time: "2004-01-01T01:01:01Z"
name: nameValue
namespace: namespaceValue
ownerReferences:
- apiVersion: apiVersionValue
blockOwnerDeletion: true
controller: true
kind: kindValue
name: nameValue
uid: uidValue
resourceVersion: resourceVersionValue
selfLink: selfLinkValue
uid: uidValue
spec:
failurePolicy: failurePolicyValue
matchConditions:
- expression: expressionValue
name: nameValue
matchConstraints:
excludeResourceRules:
- apiGroups:
- apiGroupsValue
apiVersions:
- apiVersionsValue
operations:
- operationsValue
resourceNames:
- resourceNamesValue
resources:
- resourcesValue
scope: scopeValue
matchPolicy: matchPolicyValue
namespaceSelector:
matchExpressions:
- key: keyValue
operator: operatorValue
values:
- valuesValue
matchLabels:
matchLabelsKey: matchLabelsValue
objectSelector:
matchExpressions:
- key: keyValue
operator: operatorValue
values:
- valuesValue
matchLabels:
matchLabelsKey: matchLabelsValue
resourceRules:
- apiGroups:
- apiGroupsValue
apiVersions:
- apiVersionsValue
operations:
- operationsValue
resourceNames:
- resourceNamesValue
resources:
- resourcesValue
scope: scopeValue
mutations:
- applyConfiguration:
expression: expressionValue
jsonPatch:
expression: expressionValue
patchType: patchTypeValue
paramKind:
apiVersion: apiVersionValue
kind: kindValue
reinvocationPolicy: reinvocationPolicyValue
variables:
- expression: expressionValue
name: nameValue

View File

@@ -0,0 +1,139 @@
{
"kind": "MutatingAdmissionPolicyBinding",
"apiVersion": "admissionregistration.k8s.io/v1alpha1",
"metadata": {
"name": "nameValue",
"generateName": "generateNameValue",
"namespace": "namespaceValue",
"selfLink": "selfLinkValue",
"uid": "uidValue",
"resourceVersion": "resourceVersionValue",
"generation": 7,
"creationTimestamp": "2008-01-01T01:01:01Z",
"deletionTimestamp": "2009-01-01T01:01:01Z",
"deletionGracePeriodSeconds": 10,
"labels": {
"labelsKey": "labelsValue"
},
"annotations": {
"annotationsKey": "annotationsValue"
},
"ownerReferences": [
{
"apiVersion": "apiVersionValue",
"kind": "kindValue",
"name": "nameValue",
"uid": "uidValue",
"controller": true,
"blockOwnerDeletion": true
}
],
"finalizers": [
"finalizersValue"
],
"managedFields": [
{
"manager": "managerValue",
"operation": "operationValue",
"apiVersion": "apiVersionValue",
"time": "2004-01-01T01:01:01Z",
"fieldsType": "fieldsTypeValue",
"fieldsV1": {},
"subresource": "subresourceValue"
}
]
},
"spec": {
"policyName": "policyNameValue",
"paramRef": {
"name": "nameValue",
"namespace": "namespaceValue",
"selector": {
"matchLabels": {
"matchLabelsKey": "matchLabelsValue"
},
"matchExpressions": [
{
"key": "keyValue",
"operator": "operatorValue",
"values": [
"valuesValue"
]
}
]
},
"parameterNotFoundAction": "parameterNotFoundActionValue"
},
"matchResources": {
"namespaceSelector": {
"matchLabels": {
"matchLabelsKey": "matchLabelsValue"
},
"matchExpressions": [
{
"key": "keyValue",
"operator": "operatorValue",
"values": [
"valuesValue"
]
}
]
},
"objectSelector": {
"matchLabels": {
"matchLabelsKey": "matchLabelsValue"
},
"matchExpressions": [
{
"key": "keyValue",
"operator": "operatorValue",
"values": [
"valuesValue"
]
}
]
},
"resourceRules": [
{
"resourceNames": [
"resourceNamesValue"
],
"operations": [
"operationsValue"
],
"apiGroups": [
"apiGroupsValue"
],
"apiVersions": [
"apiVersionsValue"
],
"resources": [
"resourcesValue"
],
"scope": "scopeValue"
}
],
"excludeResourceRules": [
{
"resourceNames": [
"resourceNamesValue"
],
"operations": [
"operationsValue"
],
"apiGroups": [
"apiGroupsValue"
],
"apiVersions": [
"apiVersionsValue"
],
"resources": [
"resourcesValue"
],
"scope": "scopeValue"
}
],
"matchPolicy": "matchPolicyValue"
}
}
}

View File

@@ -0,0 +1,90 @@
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: MutatingAdmissionPolicyBinding
metadata:
annotations:
annotationsKey: annotationsValue
creationTimestamp: "2008-01-01T01:01:01Z"
deletionGracePeriodSeconds: 10
deletionTimestamp: "2009-01-01T01:01:01Z"
finalizers:
- finalizersValue
generateName: generateNameValue
generation: 7
labels:
labelsKey: labelsValue
managedFields:
- apiVersion: apiVersionValue
fieldsType: fieldsTypeValue
fieldsV1: {}
manager: managerValue
operation: operationValue
subresource: subresourceValue
time: "2004-01-01T01:01:01Z"
name: nameValue
namespace: namespaceValue
ownerReferences:
- apiVersion: apiVersionValue
blockOwnerDeletion: true
controller: true
kind: kindValue
name: nameValue
uid: uidValue
resourceVersion: resourceVersionValue
selfLink: selfLinkValue
uid: uidValue
spec:
matchResources:
excludeResourceRules:
- apiGroups:
- apiGroupsValue
apiVersions:
- apiVersionsValue
operations:
- operationsValue
resourceNames:
- resourceNamesValue
resources:
- resourcesValue
scope: scopeValue
matchPolicy: matchPolicyValue
namespaceSelector:
matchExpressions:
- key: keyValue
operator: operatorValue
values:
- valuesValue
matchLabels:
matchLabelsKey: matchLabelsValue
objectSelector:
matchExpressions:
- key: keyValue
operator: operatorValue
values:
- valuesValue
matchLabels:
matchLabelsKey: matchLabelsValue
resourceRules:
- apiGroups:
- apiGroupsValue
apiVersions:
- apiVersionsValue
operations:
- operationsValue
resourceNames:
- resourceNamesValue
resources:
- resourcesValue
scope: scopeValue
paramRef:
name: nameValue
namespace: namespaceValue
parameterNotFoundAction: parameterNotFoundActionValue
selector:
matchExpressions:
- key: keyValue
operator: operatorValue
values:
- valuesValue
matchLabels:
matchLabelsKey: matchLabelsValue
policyName: policyNameValue

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package validating
package authorizer
import (
"context"
@@ -39,7 +39,10 @@ type cachingAuthorizer struct {
decisions map[string]authzResult
}
func newCachingAuthorizer(in authorizer.Authorizer) authorizer.Authorizer {
// NewCachingAuthorizer returns an authorizer that caches decisions for the duration
// of the authorizers use. Intended to be used for short-lived operations such as
// the handling of a request in the admission chain, and then discarded.
func NewCachingAuthorizer(in authorizer.Authorizer) authorizer.Authorizer {
return &cachingAuthorizer{
authorizer: in,
decisions: make(map[string]authzResult),

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package validating
package authorizer
import (
"context"
@@ -491,7 +491,7 @@ func TestCachingAuthorizer(t *testing.T) {
} {
t.Run(tc.name, func(t *testing.T) {
var misses int
frontend := newCachingAuthorizer(func() authorizer.Authorizer {
frontend := NewCachingAuthorizer(func() authorizer.Authorizer {
return authorizer.AuthorizerFunc(func(_ context.Context, attributes authorizer.Attributes) (authorizer.Decision, string, error) {
if misses >= len(tc.backend) {
t.Fatalf("got more than expected %d backend invocations", len(tc.backend))

View File

@@ -0,0 +1,190 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
import (
"context"
"fmt"
"github.com/google/cel-go/interpreter"
"math"
"time"
admissionv1 "k8s.io/api/admission/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/library"
)
// newActivation creates an activation for CEL admission plugins from the given request, admission chain and
// variable binding information.
func newActivation(compositionCtx CompositionContext, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace) (*evaluationActivation, error) {
oldObjectVal, err := objectToResolveVal(versionedAttr.VersionedOldObject)
if err != nil {
return nil, fmt.Errorf("failed to prepare oldObject variable for evaluation: %w", err)
}
objectVal, err := objectToResolveVal(versionedAttr.VersionedObject)
if err != nil {
return nil, fmt.Errorf("failed to prepare object variable for evaluation: %w", err)
}
var paramsVal, authorizerVal, requestResourceAuthorizerVal any
if inputs.VersionedParams != nil {
paramsVal, err = objectToResolveVal(inputs.VersionedParams)
if err != nil {
return nil, fmt.Errorf("failed to prepare params variable for evaluation: %w", err)
}
}
if inputs.Authorizer != nil {
authorizerVal = library.NewAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer)
requestResourceAuthorizerVal = library.NewResourceAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer, versionedAttr)
}
requestVal, err := convertObjectToUnstructured(request)
if err != nil {
return nil, fmt.Errorf("failed to prepare request variable for evaluation: %w", err)
}
namespaceVal, err := objectToResolveVal(namespace)
if err != nil {
return nil, fmt.Errorf("failed to prepare namespace variable for evaluation: %w", err)
}
va := &evaluationActivation{
object: objectVal,
oldObject: oldObjectVal,
params: paramsVal,
request: requestVal.Object,
namespace: namespaceVal,
authorizer: authorizerVal,
requestResourceAuthorizer: requestResourceAuthorizerVal,
}
// composition is an optional feature that only applies for ValidatingAdmissionPolicy and MutatingAdmissionPolicy.
if compositionCtx != nil {
va.variables = compositionCtx.Variables(va)
}
return va, nil
}
type evaluationActivation struct {
object, oldObject, params, request, namespace, authorizer, requestResourceAuthorizer, variables interface{}
}
// ResolveName returns a value from the activation by qualified name, or false if the name
// could not be found.
func (a *evaluationActivation) ResolveName(name string) (interface{}, bool) {
switch name {
case ObjectVarName:
return a.object, true
case OldObjectVarName:
return a.oldObject, true
case ParamsVarName:
return a.params, true // params may be null
case RequestVarName:
return a.request, true
case NamespaceVarName:
return a.namespace, true
case AuthorizerVarName:
return a.authorizer, a.authorizer != nil
case RequestResourceAuthorizerVarName:
return a.requestResourceAuthorizer, a.requestResourceAuthorizer != nil
case VariableVarName: // variables always present
return a.variables, true
default:
return nil, false
}
}
// Parent returns the parent of the current activation, may be nil.
// If non-nil, the parent will be searched during resolve calls.
func (a *evaluationActivation) Parent() interpreter.Activation {
return nil
}
// Evaluate runs a compiled CEL admission plugin expression using the provided activation and CEL
// runtime cost budget.
func (a *evaluationActivation) Evaluate(ctx context.Context, compositionCtx CompositionContext, compilationResult CompilationResult, remainingBudget int64) (EvaluationResult, int64, error) {
var evaluation = EvaluationResult{}
if compilationResult.ExpressionAccessor == nil { // in case of placeholder
return evaluation, remainingBudget, nil
}
evaluation.ExpressionAccessor = compilationResult.ExpressionAccessor
if compilationResult.Error != nil {
evaluation.Error = &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("compilation error: %v", compilationResult.Error),
Cause: compilationResult.Error,
}
return evaluation, remainingBudget, nil
}
if compilationResult.Program == nil {
evaluation.Error = &cel.Error{
Type: cel.ErrorTypeInternal,
Detail: "unexpected internal error compiling expression",
}
return evaluation, remainingBudget, nil
}
t1 := time.Now()
evalResult, evalDetails, err := compilationResult.Program.ContextEval(ctx, a)
// budget may be spent due to lazy evaluation of composited variables
if compositionCtx != nil {
compositionCost := compositionCtx.GetAndResetCost()
if compositionCost > remainingBudget {
return evaluation, -1, &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: "validation failed due to running out of cost budget, no further validation rules will be run",
Cause: cel.ErrOutOfBudget,
}
}
remainingBudget -= compositionCost
}
elapsed := time.Since(t1)
evaluation.Elapsed = elapsed
if evalDetails == nil {
return evaluation, -1, &cel.Error{
Type: cel.ErrorTypeInternal,
Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()),
}
} else {
rtCost := evalDetails.ActualCost()
if rtCost == nil {
return evaluation, -1, &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()),
Cause: cel.ErrOutOfBudget,
}
} else {
if *rtCost > math.MaxInt64 || int64(*rtCost) > remainingBudget {
return evaluation, -1, &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: "validation failed due to running out of cost budget, no further validation rules will be run",
Cause: cel.ErrOutOfBudget,
}
}
remainingBudget -= int64(*rtCost)
}
}
if err != nil {
evaluation.Error = &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("expression '%v' resulted in error: %v", compilationResult.ExpressionAccessor.GetExpression(), err),
}
} else {
evaluation.EvalResult = evalResult
}
return evaluation, remainingBudget, nil
}

View File

@@ -24,8 +24,10 @@ import (
"k8s.io/apimachinery/pkg/util/version"
celconfig "k8s.io/apiserver/pkg/apis/cel"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/common"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/cel/library"
"k8s.io/apiserver/pkg/cel/mutation"
)
const (
@@ -186,7 +188,7 @@ func (c compiler) CompileCELExpression(expressionAccessor ExpressionAccessor, op
found := false
returnTypes := expressionAccessor.ReturnTypes()
for _, returnType := range returnTypes {
if ast.OutputType() == returnType || cel.AnyType == returnType {
if ast.OutputType().IsExactType(returnType) || cel.AnyType.IsExactType(returnType) {
found = true
break
}
@@ -194,9 +196,9 @@ func (c compiler) CompileCELExpression(expressionAccessor ExpressionAccessor, op
if !found {
var reason string
if len(returnTypes) == 1 {
reason = fmt.Sprintf("must evaluate to %v", returnTypes[0].String())
reason = fmt.Sprintf("must evaluate to %v but got %v", returnTypes[0].String(), ast.OutputType().String())
} else {
reason = fmt.Sprintf("must evaluate to one of %v", returnTypes)
reason = fmt.Sprintf("must evaluate to one of %v but got %v", returnTypes, ast.OutputType().String())
}
return resultError(reason, apiservercel.ErrorTypeInvalid, nil)
@@ -226,46 +228,78 @@ func mustBuildEnvs(baseEnv *environment.EnvSet) variableDeclEnvs {
envs := make(variableDeclEnvs, 8) // since the number of variable combinations is small, pre-build a environment for each
for _, hasParams := range []bool{false, true} {
for _, hasAuthorizer := range []bool{false, true} {
var err error
for _, strictCost := range []bool{false, true} {
var envOpts []cel.EnvOption
if hasParams {
envOpts = append(envOpts, cel.Variable(ParamsVarName, cel.DynType))
}
if hasAuthorizer {
envOpts = append(envOpts,
cel.Variable(AuthorizerVarName, library.AuthorizerType),
cel.Variable(RequestResourceAuthorizerVarName, library.ResourceCheckType))
}
envOpts = append(envOpts,
cel.Variable(ObjectVarName, cel.DynType),
cel.Variable(OldObjectVarName, cel.DynType),
cel.Variable(NamespaceVarName, namespaceType.CelType()),
cel.Variable(RequestVarName, requestType.CelType()))
extended, err := baseEnv.Extend(
environment.VersionedOptions{
// Feature epoch was actually 1.26, but we artificially set it to 1.0 because these
// options should always be present.
IntroducedVersion: version.MajorMinor(1, 0),
EnvOptions: envOpts,
DeclTypes: []*apiservercel.DeclType{
namespaceType,
requestType,
},
},
)
decl := OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer, StrictCost: strictCost}
envs[decl], err = createEnvForOpts(baseEnv, namespaceType, requestType, decl)
if err != nil {
panic(fmt.Sprintf("environment misconfigured: %v", err))
panic(err)
}
if strictCost {
extended, err = extended.Extend(environment.StrictCostOpt)
if err != nil {
panic(fmt.Sprintf("environment misconfigured: %v", err))
}
}
envs[OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer, StrictCost: strictCost}] = extended
}
// We only need this ObjectTypes where strict cost is true
decl := OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer, StrictCost: true, HasPatchTypes: true}
envs[decl], err = createEnvForOpts(baseEnv, namespaceType, requestType, decl)
if err != nil {
panic(err)
}
}
}
return envs
}
func createEnvForOpts(baseEnv *environment.EnvSet, namespaceType *apiservercel.DeclType, requestType *apiservercel.DeclType, opts OptionalVariableDeclarations) (*environment.EnvSet, error) {
var envOpts []cel.EnvOption
envOpts = append(envOpts,
cel.Variable(ObjectVarName, cel.DynType),
cel.Variable(OldObjectVarName, cel.DynType),
cel.Variable(NamespaceVarName, namespaceType.CelType()),
cel.Variable(RequestVarName, requestType.CelType()))
if opts.HasParams {
envOpts = append(envOpts, cel.Variable(ParamsVarName, cel.DynType))
}
if opts.HasAuthorizer {
envOpts = append(envOpts,
cel.Variable(AuthorizerVarName, library.AuthorizerType),
cel.Variable(RequestResourceAuthorizerVarName, library.ResourceCheckType))
}
extended, err := baseEnv.Extend(
environment.VersionedOptions{
// Feature epoch was actually 1.26, but we artificially set it to 1.0 because these
// options should always be present.
IntroducedVersion: version.MajorMinor(1, 0),
EnvOptions: envOpts,
DeclTypes: []*apiservercel.DeclType{
namespaceType,
requestType,
},
},
)
if err != nil {
return nil, fmt.Errorf("environment misconfigured: %w", err)
}
if opts.StrictCost {
extended, err = extended.Extend(environment.StrictCostOpt)
if err != nil {
return nil, fmt.Errorf("environment misconfigured: %w", err)
}
}
if opts.HasPatchTypes {
extended, err = extended.Extend(hasPatchTypes)
if err != nil {
return nil, fmt.Errorf("environment misconfigured: %w", err)
}
}
return extended, nil
}
var hasPatchTypes = environment.VersionedOptions{
// Feature epoch was actually 1.32, but we artificially set it to 1.0 because these
// options should always be present.
IntroducedVersion: version.MajorMinor(1, 0),
EnvOptions: []cel.EnvOption{
common.ResolverEnvOption(&mutation.DynamicTypeResolver{}),
library.JSONPatch(), // for jsonPatch.escape() function
},
}

View File

@@ -36,15 +36,27 @@ import (
const VariablesTypeName = "kubernetes.variables"
// CompositedCompiler compiles expressions with variable composition.
type CompositedCompiler struct {
Compiler
FilterCompiler
ConditionCompiler
MutatingCompiler
CompositionEnv *CompositionEnv
}
type CompositedFilter struct {
Filter
// CompositedConditionEvaluator provides evaluation of a condition expression with variable composition.
// The expressions must return a boolean.
type CompositedConditionEvaluator struct {
ConditionEvaluator
compositionEnv *CompositionEnv
}
// CompositedEvaluator provides evaluation of a single expression with variable composition.
// The types that may returned by the expression is determined at compilation time.
type CompositedEvaluator struct {
MutatingEvaluator
compositionEnv *CompositionEnv
}
@@ -64,11 +76,13 @@ func NewCompositedCompilerFromTemplate(context *CompositionEnv) *CompositedCompi
CompiledVariables: map[string]CompilationResult{},
}
compiler := NewCompiler(context.EnvSet)
filterCompiler := NewFilterCompiler(context.EnvSet)
conditionCompiler := &conditionCompiler{compiler}
mutation := &mutatingCompiler{compiler}
return &CompositedCompiler{
Compiler: compiler,
FilterCompiler: filterCompiler,
CompositionEnv: context,
Compiler: compiler,
ConditionCompiler: conditionCompiler,
MutatingCompiler: mutation,
CompositionEnv: context,
}
}
@@ -85,11 +99,20 @@ func (c *CompositedCompiler) CompileAndStoreVariable(variable NamedExpressionAcc
return result
}
func (c *CompositedCompiler) Compile(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) Filter {
filter := c.FilterCompiler.Compile(expressions, optionalDecls, envType)
return &CompositedFilter{
Filter: filter,
compositionEnv: c.CompositionEnv,
func (c *CompositedCompiler) CompileCondition(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) ConditionEvaluator {
condition := c.ConditionCompiler.CompileCondition(expressions, optionalDecls, envType)
return &CompositedConditionEvaluator{
ConditionEvaluator: condition,
compositionEnv: c.CompositionEnv,
}
}
// CompileEvaluator compiles an mutatingEvaluator for the given expression, options and environment.
func (c *CompositedCompiler) CompileMutatingEvaluator(expression ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) MutatingEvaluator {
mutation := c.MutatingCompiler.CompileMutatingEvaluator(expression, optionalDecls, envType)
return &CompositedEvaluator{
MutatingEvaluator: mutation,
compositionEnv: c.CompositionEnv,
}
}
@@ -160,9 +183,9 @@ func (c *compositionContext) Variables(activation any) ref.Val {
return lazyMap
}
func (f *CompositedFilter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
func (f *CompositedConditionEvaluator) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
ctx = f.compositionEnv.CreateContext(ctx)
return f.Filter.ForInput(ctx, versionedAttr, request, optionalVars, namespace, runtimeCELCostBudget)
return f.ConditionEvaluator.ForInput(ctx, versionedAttr, request, optionalVars, namespace, runtimeCELCostBudget)
}
func (c *compositionContext) reportCost(cost int64) {

View File

@@ -223,8 +223,8 @@ func TestCompositedPolicies(t *testing.T) {
t.Fatal(err)
}
compiler.CompileAndStoreVariables(tc.variables, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: tc.strictCostEnforcement}, environment.NewExpressions)
validations := []ExpressionAccessor{&condition{Expression: tc.expression}}
f := compiler.Compile(validations, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: tc.strictCostEnforcement}, environment.NewExpressions)
validations := []ExpressionAccessor{&testCondition{Expression: tc.expression}}
f := compiler.CompileCondition(validations, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: tc.strictCostEnforcement}, environment.NewExpressions)
versionedAttr, err := admission.NewVersionedAttributes(tc.attributes, tc.attributes.GetKind(), newObjectInterfacesForTest())
if err != nil {
t.Fatal(err)

View File

@@ -0,0 +1,216 @@
/*
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 cel
import (
"context"
"reflect"
admissionv1 "k8s.io/api/admission/v1"
authenticationv1 "k8s.io/api/authentication/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/cel/environment"
)
// conditionCompiler implement the interface ConditionCompiler.
type conditionCompiler struct {
compiler Compiler
}
func NewConditionCompiler(env *environment.EnvSet) ConditionCompiler {
return &conditionCompiler{compiler: NewCompiler(env)}
}
// CompileCondition compiles the cel expressions defined in the ExpressionAccessors into a ConditionEvaluator
func (c *conditionCompiler) CompileCondition(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) ConditionEvaluator {
compilationResults := make([]CompilationResult, len(expressionAccessors))
for i, expressionAccessor := range expressionAccessors {
if expressionAccessor == nil {
continue
}
compilationResults[i] = c.compiler.CompileCELExpression(expressionAccessor, options, mode)
}
return NewCondition(compilationResults)
}
// condition implements the ConditionEvaluator interface
type condition struct {
compilationResults []CompilationResult
}
func NewCondition(compilationResults []CompilationResult) ConditionEvaluator {
return &condition{
compilationResults,
}
}
func convertObjectToUnstructured(obj interface{}) (*unstructured.Unstructured, error) {
if obj == nil || reflect.ValueOf(obj).IsNil() {
return &unstructured.Unstructured{Object: nil}, nil
}
ret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return nil, err
}
return &unstructured.Unstructured{Object: ret}, nil
}
func objectToResolveVal(r runtime.Object) (interface{}, error) {
if r == nil || reflect.ValueOf(r).IsNil() {
return nil, nil
}
v, err := convertObjectToUnstructured(r)
if err != nil {
return nil, err
}
return v.Object, nil
}
// ForInput evaluates the compiled CEL expressions converting them into CELEvaluations
// errors per evaluation are returned on the Evaluation object
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
func (c *condition) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
// TODO: replace unstructured with ref.Val for CEL variables when native type support is available
evaluations := make([]EvaluationResult, len(c.compilationResults))
var err error
// if this activation supports composition, we will need the compositionCtx. It may be nil.
compositionCtx, _ := ctx.(CompositionContext)
activation, err := newActivation(compositionCtx, versionedAttr, request, inputs, namespace)
if err != nil {
return nil, -1, err
}
remainingBudget := runtimeCELCostBudget
for i, compilationResult := range c.compilationResults {
evaluations[i], remainingBudget, err = activation.Evaluate(ctx, compositionCtx, compilationResult, remainingBudget)
if err != nil {
return nil, -1, err
}
}
return evaluations, remainingBudget, nil
}
// TODO: to reuse https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/admissionreview.go#L154
func CreateAdmissionRequest(attr admission.Attributes, equivalentGVR metav1.GroupVersionResource, equivalentKind metav1.GroupVersionKind) *admissionv1.AdmissionRequest {
// Attempting to use same logic as webhook for constructing resource
// GVK, GVR, subresource
// Use the GVK, GVR that the matcher decided was equivalent to that of the request
// https://github.com/kubernetes/kubernetes/blob/90c362b3430bcbbf8f245fadbcd521dab39f1d7c/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go#L182-L210
gvk := equivalentKind
gvr := equivalentGVR
subresource := attr.GetSubresource()
requestGVK := attr.GetKind()
requestGVR := attr.GetResource()
requestSubResource := attr.GetSubresource()
aUserInfo := attr.GetUserInfo()
var userInfo authenticationv1.UserInfo
if aUserInfo != nil {
userInfo = authenticationv1.UserInfo{
Extra: make(map[string]authenticationv1.ExtraValue),
Groups: aUserInfo.GetGroups(),
UID: aUserInfo.GetUID(),
Username: aUserInfo.GetName(),
}
// Convert the extra information in the user object
for key, val := range aUserInfo.GetExtra() {
userInfo.Extra[key] = authenticationv1.ExtraValue(val)
}
}
dryRun := attr.IsDryRun()
return &admissionv1.AdmissionRequest{
Kind: metav1.GroupVersionKind{
Group: gvk.Group,
Kind: gvk.Kind,
Version: gvk.Version,
},
Resource: metav1.GroupVersionResource{
Group: gvr.Group,
Resource: gvr.Resource,
Version: gvr.Version,
},
SubResource: subresource,
RequestKind: &metav1.GroupVersionKind{
Group: requestGVK.Group,
Kind: requestGVK.Kind,
Version: requestGVK.Version,
},
RequestResource: &metav1.GroupVersionResource{
Group: requestGVR.Group,
Resource: requestGVR.Resource,
Version: requestGVR.Version,
},
RequestSubResource: requestSubResource,
Name: attr.GetName(),
Namespace: attr.GetNamespace(),
Operation: admissionv1.Operation(attr.GetOperation()),
UserInfo: userInfo,
// Leave Object and OldObject unset since we don't provide access to them via request
DryRun: &dryRun,
Options: runtime.RawExtension{
Object: attr.GetOperationOptions(),
},
}
}
// CreateNamespaceObject creates a Namespace object that is suitable for the CEL evaluation.
// If the namespace is nil, CreateNamespaceObject returns nil
func CreateNamespaceObject(namespace *v1.Namespace) *v1.Namespace {
if namespace == nil {
return nil
}
return &v1.Namespace{
Status: namespace.Status,
Spec: namespace.Spec,
ObjectMeta: metav1.ObjectMeta{
Name: namespace.Name,
GenerateName: namespace.GenerateName,
Namespace: namespace.Namespace,
UID: namespace.UID,
ResourceVersion: namespace.ResourceVersion,
Generation: namespace.Generation,
CreationTimestamp: namespace.CreationTimestamp,
DeletionTimestamp: namespace.DeletionTimestamp,
DeletionGracePeriodSeconds: namespace.DeletionGracePeriodSeconds,
Labels: namespace.Labels,
Annotations: namespace.Annotations,
Finalizers: namespace.Finalizers,
},
}
}
// CompilationErrors returns a list of all the errors from the compilation of the mutatingEvaluator
func (c *condition) CompilationErrors() []error {
compilationErrors := []error{}
for _, result := range c.compilationResults {
if result.Error != nil {
compilationErrors = append(compilationErrors, result.Error)
}
}
return compilationErrors
}

View File

@@ -28,8 +28,6 @@ import (
celtypes "github.com/google/cel-go/common/types"
"github.com/stretchr/testify/require"
pointer "k8s.io/utils/ptr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -48,17 +46,18 @@ import (
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
pointer "k8s.io/utils/ptr"
)
type condition struct {
type testCondition struct {
Expression string
}
func (c *condition) GetExpression() string {
return c.Expression
func (tc *testCondition) GetExpression() string {
return tc.Expression
}
func (v *condition) ReturnTypes() []*celgo.Type {
func (tc *testCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.BoolType}
}
@@ -71,10 +70,10 @@ func TestCompile(t *testing.T) {
{
name: "invalid syntax",
validation: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "1 < 'asdf'",
},
&condition{
&testCondition{
Expression: "1 < 2",
},
},
@@ -85,13 +84,13 @@ func TestCompile(t *testing.T) {
{
name: "valid syntax",
validation: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "1 < 2",
},
&condition{
&testCondition{
Expression: "object.spec.string.matches('[0-9]+')",
},
&condition{
&testCondition{
Expression: "request.kind.group == 'example.com' && request.kind.version == 'v1' && request.kind.kind == 'Fake'",
},
},
@@ -100,13 +99,13 @@ func TestCompile(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := filterCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))}
e := c.Compile(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: true}, environment.NewExpressions)
c := conditionCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))}
e := c.CompileCondition(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: true}, environment.NewExpressions)
if e == nil {
t.Fatalf("unexpected nil validator")
}
validations := tc.validation
CompilationResults := e.(*filter).compilationResults
CompilationResults := e.(*condition).compilationResults
require.Equal(t, len(validations), len(CompilationResults))
meets := make([]bool, len(validations))
@@ -131,7 +130,7 @@ func TestCompile(t *testing.T) {
}
}
func TestFilter(t *testing.T) {
func TestCondition(t *testing.T) {
simpleLabelSelector, err := labels.NewRequirement("apple", selection.Equals, []string{"banana"})
if err != nil {
panic(err)
@@ -205,7 +204,7 @@ func TestFilter(t *testing.T) {
{
name: "valid syntax for object",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "has(object.subsets) && object.subsets.size() < 2",
},
},
@@ -220,7 +219,7 @@ func TestFilter(t *testing.T) {
{
name: "valid syntax for metadata",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "object.metadata.name == 'endpoints1'",
},
},
@@ -235,10 +234,10 @@ func TestFilter(t *testing.T) {
{
name: "valid syntax for oldObject",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "oldObject == null",
},
&condition{
&testCondition{
Expression: "object != null",
},
},
@@ -256,7 +255,7 @@ func TestFilter(t *testing.T) {
{
name: "valid syntax for request",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "request.operation == 'CREATE'",
},
},
@@ -271,7 +270,7 @@ func TestFilter(t *testing.T) {
{
name: "valid syntax for configMap",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "request.namespace != params.data.fakeString",
},
},
@@ -287,7 +286,7 @@ func TestFilter(t *testing.T) {
{
name: "test failure",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "object.subsets.size() > 2",
},
},
@@ -310,10 +309,10 @@ func TestFilter(t *testing.T) {
{
name: "test failure with multiple validations",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "has(object.subsets)",
},
&condition{
&testCondition{
Expression: "object.subsets.size() > 2",
},
},
@@ -332,10 +331,10 @@ func TestFilter(t *testing.T) {
{
name: "test failure policy with multiple failed validations",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "oldObject != null",
},
&condition{
&testCondition{
Expression: "object.subsets.size() > 2",
},
},
@@ -354,10 +353,10 @@ func TestFilter(t *testing.T) {
{
name: "test Object null in delete",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "oldObject != null",
},
&condition{
&testCondition{
Expression: "object == null",
},
},
@@ -376,7 +375,7 @@ func TestFilter(t *testing.T) {
{
name: "test runtime error",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "oldObject.x == 100",
},
},
@@ -392,7 +391,7 @@ func TestFilter(t *testing.T) {
{
name: "test against crd param",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "object.subsets.size() < params.spec.testSize",
},
},
@@ -408,10 +407,10 @@ func TestFilter(t *testing.T) {
{
name: "test compile failure",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "fail to compile test",
},
&condition{
&testCondition{
Expression: "object.subsets.size() > params.spec.testSize",
},
},
@@ -430,7 +429,7 @@ func TestFilter(t *testing.T) {
{
name: "test pod",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "object.spec.nodeName == 'testnode'",
},
},
@@ -446,7 +445,7 @@ func TestFilter(t *testing.T) {
{
name: "test deny paramKind without paramRef",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "params != null",
},
},
@@ -461,7 +460,7 @@ func TestFilter(t *testing.T) {
{
name: "test allow paramKind without paramRef",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "params == null",
},
},
@@ -477,10 +476,10 @@ func TestFilter(t *testing.T) {
{
name: "test authorizer allow resource check",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.group('').resource('endpoints').check('create').allowed()",
},
&condition{
&testCondition{
Expression: "authorizer.group('').resource('endpoints').check('create').errored()",
},
},
@@ -503,7 +502,7 @@ func TestFilter(t *testing.T) {
{
name: "test authorizer error using fieldSelector with 1.30 compatibility",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo=bar').labelSelector('apple=banana').subresource('status').namespace('test').name('backend').check('create').allowed()",
},
},
@@ -535,7 +534,7 @@ func TestFilter(t *testing.T) {
{
name: "test authorizer allow resource check with all fields",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo=bar').labelSelector('apple=banana').subresource('status').namespace('test').name('backend').check('create').allowed()",
},
},
@@ -567,7 +566,7 @@ func TestFilter(t *testing.T) {
{
name: "test authorizer allow resource check with parse failures",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo badoperator bar').labelSelector('apple badoperator banana').subresource('status').namespace('test').name('backend').check('create').allowed()",
},
},
@@ -595,7 +594,7 @@ func TestFilter(t *testing.T) {
{
name: "test authorizer allow resource check with all fields, without gate",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo=bar').labelSelector('apple=banana').subresource('status').namespace('test').name('backend').check('create').allowed()",
},
},
@@ -621,7 +620,7 @@ func TestFilter(t *testing.T) {
{
name: "test authorizer not allowed resource check one incorrect field",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()",
},
@@ -646,7 +645,7 @@ func TestFilter(t *testing.T) {
{
name: "test authorizer reason",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.group('').resource('endpoints').check('create').reason() == 'fake reason'",
},
},
@@ -661,13 +660,13 @@ func TestFilter(t *testing.T) {
{
name: "test authorizer error",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.group('').resource('endpoints').check('create').errored()",
},
&condition{
&testCondition{
Expression: "authorizer.group('').resource('endpoints').check('create').error() == 'fake authz error'",
},
&condition{
&testCondition{
Expression: "authorizer.group('').resource('endpoints').check('create').allowed()",
},
},
@@ -688,7 +687,7 @@ func TestFilter(t *testing.T) {
{
name: "test authorizer allow path check",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.path('/healthz').check('get').allowed()",
},
},
@@ -706,7 +705,7 @@ func TestFilter(t *testing.T) {
{
name: "test authorizer decision is denied path check",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.path('/healthz').check('get').allowed() == false",
},
},
@@ -721,7 +720,7 @@ func TestFilter(t *testing.T) {
{
name: "test request resource authorizer allow check",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.requestResource.check('custom-verb').allowed()",
},
},
@@ -745,7 +744,7 @@ func TestFilter(t *testing.T) {
{
name: "test subresource request resource authorizer allow check",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.requestResource.check('custom-verb').allowed()",
},
},
@@ -769,7 +768,7 @@ func TestFilter(t *testing.T) {
{
name: "test serviceAccount authorizer allow check",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.serviceAccount('default', 'test-serviceaccount').group('').resource('endpoints').namespace('default').name('endpoints1').check('custom-verb').allowed()",
},
},
@@ -796,7 +795,7 @@ func TestFilter(t *testing.T) {
{
name: "test perCallLimit exceed",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "object.subsets.size() < params.spec.testSize",
},
},
@@ -813,28 +812,28 @@ func TestFilter(t *testing.T) {
{
name: "test namespaceObject",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "namespaceObject.metadata.name == 'test'",
},
&condition{
&testCondition{
Expression: "'env' in namespaceObject.metadata.labels && namespaceObject.metadata.labels.env == 'test'",
},
&condition{
&testCondition{
Expression: "('fake' in namespaceObject.metadata.labels) && namespaceObject.metadata.labels.fake == 'test'",
},
&condition{
&testCondition{
Expression: "namespaceObject.spec.finalizers[0] == 'kubernetes'",
},
&condition{
&testCondition{
Expression: "namespaceObject.status.phase == 'Active'",
},
&condition{
&testCondition{
Expression: "size(namespaceObject.metadata.managedFields) == 1",
},
&condition{
&testCondition{
Expression: "size(namespaceObject.metadata.ownerReferences) == 1",
},
&condition{
&testCondition{
Expression: "'env' in namespaceObject.metadata.annotations",
},
},
@@ -891,14 +890,14 @@ func TestFilter(t *testing.T) {
if err != nil {
t.Fatal(err)
}
c := NewFilterCompiler(env)
f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil, StrictCost: tc.strictCost}, environment.NewExpressions)
c := NewConditionCompiler(env)
f := c.CompileCondition(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil, StrictCost: tc.strictCost}, environment.NewExpressions)
if f == nil {
t.Fatalf("unexpected nil validator")
}
validations := tc.validations
CompilationResults := f.(*filter).compilationResults
CompilationResults := f.(*condition).compilationResults
require.Equal(t, len(validations), len(CompilationResults))
versionedAttr, err := admission.NewVersionedAttributes(tc.attributes, tc.attributes.GetKind(), newObjectInterfacesForTest())
@@ -960,10 +959,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "expression exceed RuntimeCELCostBudget at fist expression",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "has(object.subsets) && object.subsets.size() < 2",
},
&condition{
&testCondition{
Expression: "has(object.subsets)",
},
},
@@ -975,10 +974,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "expression exceed RuntimeCELCostBudget at last expression",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "has(object.subsets) && object.subsets.size() < 2",
},
&condition{
&testCondition{
Expression: "object.subsets.size() > 2",
},
},
@@ -991,10 +990,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "test RuntimeCELCostBudge is not exceed",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "oldObject != null",
},
&condition{
&testCondition{
Expression: "object.subsets.size() > 2",
},
},
@@ -1008,10 +1007,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "test RuntimeCELCostBudge exactly covers",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "oldObject != null",
},
&condition{
&testCondition{
Expression: "object.subsets.size() > 2",
},
},
@@ -1025,13 +1024,13 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "test RuntimeCELCostBudge exactly covers then constant",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "oldObject != null",
},
&condition{
&testCondition{
Expression: "object.subsets.size() > 2",
},
&condition{
&testCondition{
Expression: "true", // zero cost
},
},
@@ -1045,7 +1044,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "Extended library cost: authz check",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
},
},
@@ -1059,7 +1058,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "Extended library cost: isSorted()",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "[1,2,3,4].isSorted()",
},
},
@@ -1072,7 +1071,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "Extended library cost: url",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "url('https:://kubernetes.io/').getHostname() == 'kubernetes.io'",
},
},
@@ -1085,7 +1084,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "Extended library cost: split",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "size('abc 123 def 123'.split(' ')) > 0",
},
},
@@ -1098,7 +1097,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "Extended library cost: join",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "size(['aa', 'bb', 'cc', 'd', 'e', 'f', 'g', 'h', 'i', 'j'].join(' ')) > 0",
},
},
@@ -1111,7 +1110,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "Extended library cost: find",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "size('abc 123 def 123'.find('123')) > 0",
},
},
@@ -1124,7 +1123,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "Extended library cost: quantity",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "quantity(\"200M\") == quantity(\"0.2G\") && quantity(\"0.2G\") == quantity(\"200M\")",
},
},
@@ -1137,10 +1136,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: expression exceed RuntimeCELCostBudget at fist expression",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
},
&condition{
&testCondition{
Expression: "has(object.subsets)",
},
},
@@ -1154,10 +1153,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: expression exceed RuntimeCELCostBudget at last expression",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
},
&condition{
&testCondition{
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
},
},
@@ -1171,10 +1170,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: test RuntimeCELCostBudge is not exceed",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
},
&condition{
&testCondition{
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
},
},
@@ -1190,10 +1189,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: test RuntimeCELCostBudge exactly covers",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
},
&condition{
&testCondition{
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
},
},
@@ -1209,7 +1208,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: per call limit exceeds",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed() && !authorizer.group('').resource('endpoints').check('create').allowed() && !authorizer.group('').resource('endpoints').check('create').allowed()",
},
},
@@ -1224,7 +1223,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: Extended library cost: isSorted()",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "[1,2,3,4].isSorted()",
},
},
@@ -1238,7 +1237,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: Extended library cost: url",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "url('https:://kubernetes.io/').getHostname() == 'kubernetes.io'",
},
},
@@ -1252,7 +1251,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: Extended library cost: split",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "size('abc 123 def 123'.split(' ')) > 0",
},
},
@@ -1266,7 +1265,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: Extended library cost: join",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "size(['aa', 'bb', 'cc', 'd', 'e', 'f', 'g', 'h', 'i', 'j'].join(' ')) > 0",
},
},
@@ -1280,7 +1279,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: Extended library cost: find",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "size('abc 123 def 123'.find('123')) > 0",
},
},
@@ -1294,7 +1293,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: Extended library cost: quantity",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "quantity(\"200M\") == quantity(\"0.2G\") && quantity(\"0.2G\") == quantity(\"200M\")",
},
},
@@ -1309,13 +1308,13 @@ func TestRuntimeCELCostBudget(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := filterCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), tc.enableStrictCostEnforcement))}
f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: true, StrictCost: tc.enableStrictCostEnforcement}, environment.NewExpressions)
c := conditionCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), tc.enableStrictCostEnforcement))}
f := c.CompileCondition(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: true, StrictCost: tc.enableStrictCostEnforcement}, environment.NewExpressions)
if f == nil {
t.Fatalf("unexpected nil validator")
}
validations := tc.validations
CompilationResults := f.(*filter).compilationResults
CompilationResults := f.(*condition).compilationResults
require.Equal(t, len(validations), len(CompilationResults))
versionedAttr, err := admission.NewVersionedAttributes(tc.attributes, tc.attributes.GetKind(), newObjectInterfacesForTest())
@@ -1476,7 +1475,7 @@ func TestCompilationErrors(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
e := filter{
e := condition{
compilationResults: tc.results,
}
compilationErrors := e.CompilationErrors()

View File

@@ -1,361 +0,0 @@
/*
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 cel
import (
"context"
"fmt"
"math"
"reflect"
"time"
"github.com/google/cel-go/interpreter"
admissionv1 "k8s.io/api/admission/v1"
authenticationv1 "k8s.io/api/authentication/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/cel/library"
)
// filterCompiler implement the interface FilterCompiler.
type filterCompiler struct {
compiler Compiler
}
func NewFilterCompiler(env *environment.EnvSet) FilterCompiler {
return &filterCompiler{compiler: NewCompiler(env)}
}
type evaluationActivation struct {
object, oldObject, params, request, namespace, authorizer, requestResourceAuthorizer, variables interface{}
}
// ResolveName returns a value from the activation by qualified name, or false if the name
// could not be found.
func (a *evaluationActivation) ResolveName(name string) (interface{}, bool) {
switch name {
case ObjectVarName:
return a.object, true
case OldObjectVarName:
return a.oldObject, true
case ParamsVarName:
return a.params, true // params may be null
case RequestVarName:
return a.request, true
case NamespaceVarName:
return a.namespace, true
case AuthorizerVarName:
return a.authorizer, a.authorizer != nil
case RequestResourceAuthorizerVarName:
return a.requestResourceAuthorizer, a.requestResourceAuthorizer != nil
case VariableVarName: // variables always present
return a.variables, true
default:
return nil, false
}
}
// Parent returns the parent of the current activation, may be nil.
// If non-nil, the parent will be searched during resolve calls.
func (a *evaluationActivation) Parent() interpreter.Activation {
return nil
}
// Compile compiles the cel expressions defined in the ExpressionAccessors into a Filter
func (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) Filter {
compilationResults := make([]CompilationResult, len(expressionAccessors))
for i, expressionAccessor := range expressionAccessors {
if expressionAccessor == nil {
continue
}
compilationResults[i] = c.compiler.CompileCELExpression(expressionAccessor, options, mode)
}
return NewFilter(compilationResults)
}
// filter implements the Filter interface
type filter struct {
compilationResults []CompilationResult
}
func NewFilter(compilationResults []CompilationResult) Filter {
return &filter{
compilationResults,
}
}
func convertObjectToUnstructured(obj interface{}) (*unstructured.Unstructured, error) {
if obj == nil || reflect.ValueOf(obj).IsNil() {
return &unstructured.Unstructured{Object: nil}, nil
}
ret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return nil, err
}
return &unstructured.Unstructured{Object: ret}, nil
}
func objectToResolveVal(r runtime.Object) (interface{}, error) {
if r == nil || reflect.ValueOf(r).IsNil() {
return nil, nil
}
v, err := convertObjectToUnstructured(r)
if err != nil {
return nil, err
}
return v.Object, nil
}
// ForInput evaluates the compiled CEL expressions converting them into CELEvaluations
// errors per evaluation are returned on the Evaluation object
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
func (f *filter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
// TODO: replace unstructured with ref.Val for CEL variables when native type support is available
evaluations := make([]EvaluationResult, len(f.compilationResults))
var err error
oldObjectVal, err := objectToResolveVal(versionedAttr.VersionedOldObject)
if err != nil {
return nil, -1, err
}
objectVal, err := objectToResolveVal(versionedAttr.VersionedObject)
if err != nil {
return nil, -1, err
}
var paramsVal, authorizerVal, requestResourceAuthorizerVal any
if inputs.VersionedParams != nil {
paramsVal, err = objectToResolveVal(inputs.VersionedParams)
if err != nil {
return nil, -1, err
}
}
if inputs.Authorizer != nil {
authorizerVal = library.NewAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer)
requestResourceAuthorizerVal = library.NewResourceAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer, versionedAttr)
}
requestVal, err := convertObjectToUnstructured(request)
if err != nil {
return nil, -1, err
}
namespaceVal, err := objectToResolveVal(namespace)
if err != nil {
return nil, -1, err
}
va := &evaluationActivation{
object: objectVal,
oldObject: oldObjectVal,
params: paramsVal,
request: requestVal.Object,
namespace: namespaceVal,
authorizer: authorizerVal,
requestResourceAuthorizer: requestResourceAuthorizerVal,
}
// composition is an optional feature that only applies for ValidatingAdmissionPolicy.
// check if the context allows composition
var compositionCtx CompositionContext
var ok bool
if compositionCtx, ok = ctx.(CompositionContext); ok {
va.variables = compositionCtx.Variables(va)
}
remainingBudget := runtimeCELCostBudget
for i, compilationResult := range f.compilationResults {
var evaluation = &evaluations[i]
if compilationResult.ExpressionAccessor == nil { // in case of placeholder
continue
}
evaluation.ExpressionAccessor = compilationResult.ExpressionAccessor
if compilationResult.Error != nil {
evaluation.Error = &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("compilation error: %v", compilationResult.Error),
Cause: compilationResult.Error,
}
continue
}
if compilationResult.Program == nil {
evaluation.Error = &cel.Error{
Type: cel.ErrorTypeInternal,
Detail: fmt.Sprintf("unexpected internal error compiling expression"),
}
continue
}
t1 := time.Now()
evalResult, evalDetails, err := compilationResult.Program.ContextEval(ctx, va)
// budget may be spent due to lazy evaluation of composited variables
if compositionCtx != nil {
compositionCost := compositionCtx.GetAndResetCost()
if compositionCost > remainingBudget {
return nil, -1, &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("validation failed due to running out of cost budget, no further validation rules will be run"),
Cause: cel.ErrOutOfBudget,
}
}
remainingBudget -= compositionCost
}
elapsed := time.Since(t1)
evaluation.Elapsed = elapsed
if evalDetails == nil {
return nil, -1, &cel.Error{
Type: cel.ErrorTypeInternal,
Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()),
}
} else {
rtCost := evalDetails.ActualCost()
if rtCost == nil {
return nil, -1, &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()),
Cause: cel.ErrOutOfBudget,
}
} else {
if *rtCost > math.MaxInt64 || int64(*rtCost) > remainingBudget {
return nil, -1, &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("validation failed due to running out of cost budget, no further validation rules will be run"),
Cause: cel.ErrOutOfBudget,
}
}
remainingBudget -= int64(*rtCost)
}
}
if err != nil {
evaluation.Error = &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("expression '%v' resulted in error: %v", compilationResult.ExpressionAccessor.GetExpression(), err),
}
} else {
evaluation.EvalResult = evalResult
}
}
return evaluations, remainingBudget, nil
}
// TODO: to reuse https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/admissionreview.go#L154
func CreateAdmissionRequest(attr admission.Attributes, equivalentGVR metav1.GroupVersionResource, equivalentKind metav1.GroupVersionKind) *admissionv1.AdmissionRequest {
// Attempting to use same logic as webhook for constructing resource
// GVK, GVR, subresource
// Use the GVK, GVR that the matcher decided was equivalent to that of the request
// https://github.com/kubernetes/kubernetes/blob/90c362b3430bcbbf8f245fadbcd521dab39f1d7c/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go#L182-L210
gvk := equivalentKind
gvr := equivalentGVR
subresource := attr.GetSubresource()
requestGVK := attr.GetKind()
requestGVR := attr.GetResource()
requestSubResource := attr.GetSubresource()
aUserInfo := attr.GetUserInfo()
var userInfo authenticationv1.UserInfo
if aUserInfo != nil {
userInfo = authenticationv1.UserInfo{
Extra: make(map[string]authenticationv1.ExtraValue),
Groups: aUserInfo.GetGroups(),
UID: aUserInfo.GetUID(),
Username: aUserInfo.GetName(),
}
// Convert the extra information in the user object
for key, val := range aUserInfo.GetExtra() {
userInfo.Extra[key] = authenticationv1.ExtraValue(val)
}
}
dryRun := attr.IsDryRun()
return &admissionv1.AdmissionRequest{
Kind: metav1.GroupVersionKind{
Group: gvk.Group,
Kind: gvk.Kind,
Version: gvk.Version,
},
Resource: metav1.GroupVersionResource{
Group: gvr.Group,
Resource: gvr.Resource,
Version: gvr.Version,
},
SubResource: subresource,
RequestKind: &metav1.GroupVersionKind{
Group: requestGVK.Group,
Kind: requestGVK.Kind,
Version: requestGVK.Version,
},
RequestResource: &metav1.GroupVersionResource{
Group: requestGVR.Group,
Resource: requestGVR.Resource,
Version: requestGVR.Version,
},
RequestSubResource: requestSubResource,
Name: attr.GetName(),
Namespace: attr.GetNamespace(),
Operation: admissionv1.Operation(attr.GetOperation()),
UserInfo: userInfo,
// Leave Object and OldObject unset since we don't provide access to them via request
DryRun: &dryRun,
Options: runtime.RawExtension{
Object: attr.GetOperationOptions(),
},
}
}
// CreateNamespaceObject creates a Namespace object that is suitable for the CEL evaluation.
// If the namespace is nil, CreateNamespaceObject returns nil
func CreateNamespaceObject(namespace *v1.Namespace) *v1.Namespace {
if namespace == nil {
return nil
}
return &v1.Namespace{
Status: namespace.Status,
Spec: namespace.Spec,
ObjectMeta: metav1.ObjectMeta{
Name: namespace.Name,
GenerateName: namespace.GenerateName,
Namespace: namespace.Namespace,
UID: namespace.UID,
ResourceVersion: namespace.ResourceVersion,
Generation: namespace.Generation,
CreationTimestamp: namespace.CreationTimestamp,
DeletionTimestamp: namespace.DeletionTimestamp,
DeletionGracePeriodSeconds: namespace.DeletionGracePeriodSeconds,
Labels: namespace.Labels,
Annotations: namespace.Annotations,
Finalizers: namespace.Finalizers,
},
}
}
// CompilationErrors returns a list of all the errors from the compilation of the evaluator
func (e *filter) CompilationErrors() []error {
compilationErrors := []error{}
for _, result := range e.compilationResults {
if result.Error != nil {
compilationErrors = append(compilationErrors, result.Error)
}
}
return compilationErrors
}

View File

@@ -63,12 +63,15 @@ type OptionalVariableDeclarations struct {
HasAuthorizer bool
// StrictCost specifies if the CEL cost limitation is strict for extended libraries as well as native libraries.
StrictCost bool
// HasPatchTypes specifies if JSONPatch, Object, Object.metadata and similar types are available in CEL. These can be used
// to initialize the typed objects in CEL required to create patches.
HasPatchTypes bool
}
// FilterCompiler contains a function to assist with converting types and values to/from CEL-typed values.
type FilterCompiler interface {
// Compile is used for the cel expression compilation
Compile(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) Filter
// ConditionCompiler contains a function to assist with converting types and values to/from CEL-typed values.
type ConditionCompiler interface {
// CompileCondition is used for the cel expression compilation
CompileCondition(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) ConditionEvaluator
}
// OptionalVariableBindings provides expression bindings for optional CEL variables.
@@ -82,16 +85,38 @@ type OptionalVariableBindings struct {
Authorizer authorizer.Authorizer
}
// Filter contains a function to evaluate compiled CEL-typed values
// ConditionEvaluator contains the result of compiling a CEL expression
// that evaluates to a condition. This is used both for validation and pre-conditions.
// It expects the inbound object to already have been converted to the version expected
// by the underlying CEL code (which is indicated by the match criteria of a policy definition).
// versionedParams may be nil.
type Filter interface {
type ConditionEvaluator interface {
// ForInput converts compiled CEL-typed values into evaluated CEL-typed value.
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
// If cost budget is calculated, the filter should return the remaining budget.
// If cost budget is calculated, the condition should return the remaining budget.
ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error)
// CompilationErrors returns a list of errors from the compilation of the evaluator
// CompilationErrors returns a list of errors from the compilation of the mutatingEvaluator
CompilationErrors() []error
}
// MutatingCompiler contains a function to assist with converting types and values to/from CEL-typed values.
type MutatingCompiler interface {
// CompileMutatingEvaluator is used for the cel expression compilation
CompileMutatingEvaluator(expression ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) MutatingEvaluator
}
// MutatingEvaluator contains the result of compiling a CEL expression
// that evaluates to a mutation.
// It expects the inbound object to already have been converted to the version expected
// by the underlying CEL code (which is indicated by the match criteria of a policy definition).
// versionedParams may be nil.
type MutatingEvaluator interface {
// ForInput converts compiled CEL-typed values into a CEL-typed value representing a mutation.
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
// If cost budget is calculated, the condition should return the remaining budget.
ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) (EvaluationResult, int64, error)
// CompilationErrors returns a list of errors from the compilation of the mutatingEvaluator
CompilationErrors() []error
}

View File

@@ -0,0 +1,73 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
import (
"context"
admissionv1 "k8s.io/api/admission/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/cel/environment"
)
// mutatingCompiler provides a MutatingCompiler implementation.
type mutatingCompiler struct {
compiler Compiler
}
// CompileMutatingEvaluator compiles a CEL expression for admission plugins and returns an MutatingEvaluator for executing the
// compiled CEL expression.
func (p *mutatingCompiler) CompileMutatingEvaluator(expressionAccessor ExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) MutatingEvaluator {
compilationResult := p.compiler.CompileCELExpression(expressionAccessor, options, mode)
return NewMutatingEvaluator(compilationResult)
}
type mutatingEvaluator struct {
compilationResult CompilationResult
}
func NewMutatingEvaluator(compilationResult CompilationResult) MutatingEvaluator {
return &mutatingEvaluator{compilationResult}
}
// ForInput evaluates the compiled CEL expression and returns an evaluation result
// errors per evaluation are returned in the evaluation result
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
func (p *mutatingEvaluator) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace, runtimeCELCostBudget int64) (EvaluationResult, int64, error) {
// if this activation supports composition, we will need the compositionCtx. It may be nil.
compositionCtx, _ := ctx.(CompositionContext)
activation, err := newActivation(compositionCtx, versionedAttr, request, inputs, namespace)
if err != nil {
return EvaluationResult{}, -1, err
}
evaluation, remainingBudget, err := activation.Evaluate(ctx, compositionCtx, p.compilationResult, runtimeCELCostBudget)
if err != nil {
return evaluation, -1, err
}
return evaluation, remainingBudget, nil
}
// CompilationErrors returns a list of all the errors from the compilation of the mutatingEvaluator
func (p *mutatingEvaluator) CompilationErrors() (compilationErrors []error) {
if p.compilationResult.Error != nil {
return []error{p.compilationResult.Error}
}
return nil
}

View File

@@ -26,6 +26,7 @@ type PolicyAccessor interface {
GetNamespace() string
GetParamKind() *v1.ParamKind
GetMatchConstraints() *v1.MatchResources
GetFailurePolicy() *v1.FailurePolicyType
}
type BindingAccessor interface {

View File

@@ -49,6 +49,9 @@ type Source[H Hook] interface {
// Dispatcher dispatches evaluates an admission request against the currently
// active hooks returned by the source.
type Dispatcher[H Hook] interface {
// Start the dispatcher. This method should be called only once at startup.
Start(ctx context.Context) error
// Dispatch a request to the policies. Dispatcher may choose not to
// call a hook, either because the rules of the hook does not match, or
// the namespaceSelector or the objectSelector of the hook does not

View File

@@ -36,8 +36,9 @@ import (
)
// H is the Hook type generated by the source and consumed by the dispatcher.
// !TODO: Just pass in a Plugin[H] with accessors to all this information
type sourceFactory[H any] func(informers.SharedInformerFactory, kubernetes.Interface, dynamic.Interface, meta.RESTMapper) Source[H]
type dispatcherFactory[H any] func(authorizer.Authorizer, *matching.Matcher) Dispatcher[H]
type dispatcherFactory[H any] func(authorizer.Authorizer, *matching.Matcher, kubernetes.Interface) Dispatcher[H]
// admissionResources is the list of resources related to CEL-based admission
// features.
@@ -170,7 +171,7 @@ func (c *Plugin[H]) ValidateInitialization() error {
}
c.source = c.sourceFactory(c.informerFactory, c.client, c.dynamicClient, c.restMapper)
c.dispatcher = c.dispatcherFactory(c.authorizer, c.matcher)
c.dispatcher = c.dispatcherFactory(c.authorizer, c.matcher, c.client)
pluginContext, pluginContextCancel := context.WithCancel(context.Background())
go func() {
@@ -181,10 +182,15 @@ func (c *Plugin[H]) ValidateInitialization() error {
go func() {
err := c.source.Run(pluginContext)
if err != nil && !errors.Is(err, context.Canceled) {
utilruntime.HandleError(fmt.Errorf("policy source context unexpectedly closed: %v", err))
utilruntime.HandleError(fmt.Errorf("policy source context unexpectedly closed: %w", err))
}
}()
err := c.dispatcher.Start(pluginContext)
if err != nil && !errors.Is(err, context.Canceled) {
utilruntime.HandleError(fmt.Errorf("policy dispatcher context unexpectedly closed: %w", err))
}
c.SetReadyFunc(func() bool {
return namespaceInformer.Informer().HasSynced() && c.source.HasSynced()
})

View File

@@ -36,7 +36,7 @@ import (
"k8s.io/client-go/tools/cache"
)
// A policy invocation is a single policy-binding-param tuple from a Policy Hook
// PolicyInvocation is a single policy-binding-param tuple from a Policy Hook
// in the context of a specific request. The params have already been resolved
// and any error in configuration or setting up the invocation is stored in
// the Error field.
@@ -62,10 +62,6 @@ type PolicyInvocation[P runtime.Object, B runtime.Object, E Evaluator] struct {
// Params fetched by the binding to use to evaluate the policy
Param runtime.Object
// Error is set if there was an error with the policy or binding or its
// params, etc
Error error
}
// dispatcherDelegate is called during a request with a pre-filtered list
@@ -76,7 +72,7 @@ type PolicyInvocation[P runtime.Object, B runtime.Object, E Evaluator] struct {
//
// The delegate provides the "validation" or "mutation" aspect of dispatcher functionality
// (in contrast to generic.PolicyDispatcher which only selects active policies and params)
type dispatcherDelegate[P, B runtime.Object, E Evaluator] func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, versionedAttributes webhookgeneric.VersionedAttributeAccessor, invocations []PolicyInvocation[P, B, E]) error
type dispatcherDelegate[P, B runtime.Object, E Evaluator] func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, versionedAttributes webhookgeneric.VersionedAttributeAccessor, invocations []PolicyInvocation[P, B, E]) ([]PolicyError, *apierrors.StatusError)
type policyDispatcher[P runtime.Object, B runtime.Object, E Evaluator] struct {
newPolicyAccessor func(P) PolicyAccessor
@@ -104,7 +100,10 @@ func NewPolicyDispatcher[P runtime.Object, B runtime.Object, E Evaluator](
// request. It then resolves all params and creates an Invocation for each
// matching policy-binding-param tuple. The delegate is then called with the
// list of tuples.
//
func (d *policyDispatcher[P, B, E]) Start(ctx context.Context) error {
return nil
}
// Note: MatchConditions expressions are not evaluated here. The dispatcher delegate
// is expected to ignore the result of any policies whose match conditions dont pass.
// This may be possible to refactor so matchconditions are checked here instead.
@@ -117,29 +116,33 @@ func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.At
objectInterfaces: o,
}
var policyErrors []PolicyError
addConfigError := func(err error, definition PolicyAccessor, binding BindingAccessor) {
var message error
if binding == nil {
message = fmt.Errorf("failed to configure policy: %w", err)
} else {
message = fmt.Errorf("failed to configure binding: %w", err)
}
policyErrors = append(policyErrors, PolicyError{
Policy: definition,
Binding: binding,
Message: message,
})
}
for _, hook := range hooks {
policyAccessor := d.newPolicyAccessor(hook.Policy)
matches, matchGVR, matchGVK, err := d.matcher.DefinitionMatches(a, o, policyAccessor)
if err != nil {
// There was an error evaluating if this policy matches anything.
utilruntime.HandleError(err)
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
Policy: hook.Policy,
Error: err,
})
addConfigError(err, policyAccessor, nil)
continue
} else if !matches {
continue
} else if hook.ConfigurationError != nil {
// The policy matches but there is a configuration error with the
// policy itself
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
Policy: hook.Policy,
Error: hook.ConfigurationError,
Resource: matchGVR,
Kind: matchGVK,
})
utilruntime.HandleError(hook.ConfigurationError)
addConfigError(hook.ConfigurationError, policyAccessor, nil)
continue
}
@@ -148,19 +151,22 @@ func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.At
matches, err = d.matcher.BindingMatches(a, o, bindingAccessor)
if err != nil {
// There was an error evaluating if this binding matches anything.
utilruntime.HandleError(err)
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
Policy: hook.Policy,
Binding: binding,
Error: err,
Resource: matchGVR,
Kind: matchGVK,
})
addConfigError(err, policyAccessor, bindingAccessor)
continue
} else if !matches {
continue
}
// here the binding matches.
// VersionedAttr result will be cached and reused later during parallel
// hook calls.
if _, err = versionedAttrAccessor.VersionedAttribute(matchGVK); err != nil {
// VersionedAttr result will be cached and reused later during parallel
// hook calls.
addConfigError(err, policyAccessor, nil)
continue
}
// Collect params for this binding
params, err := CollectParams(
policyAccessor.GetParamKind(),
@@ -171,14 +177,7 @@ func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.At
)
if err != nil {
// There was an error collecting params for this binding.
utilruntime.HandleError(err)
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
Policy: hook.Policy,
Binding: binding,
Error: err,
Resource: matchGVR,
Kind: matchGVK,
})
addConfigError(err, policyAccessor, bindingAccessor)
continue
}
@@ -194,23 +193,72 @@ func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.At
Evaluator: hook.Evaluator,
})
}
}
}
// VersionedAttr result will be cached and reused later during parallel
// hook calls
_, err = versionedAttrAccessor.VersionedAttribute(matchGVK)
if err != nil {
return apierrors.NewInternalError(err)
}
if len(relevantHooks) > 0 {
extraPolicyErrors, statusError := d.delegate(ctx, a, o, versionedAttrAccessor, relevantHooks)
if statusError != nil {
return statusError
}
policyErrors = append(policyErrors, extraPolicyErrors...)
}
var filteredErrors []PolicyError
for _, e := range policyErrors {
// we always default the FailurePolicy if it is unset and validate it in API level
var policy v1.FailurePolicyType
if fp := e.Policy.GetFailurePolicy(); fp == nil {
policy = v1.Fail
} else {
policy = *fp
}
switch policy {
case v1.Ignore:
// TODO: add metrics for ignored error here
continue
case v1.Fail:
filteredErrors = append(filteredErrors, e)
default:
filteredErrors = append(filteredErrors, e)
}
}
if len(relevantHooks) == 0 {
// no matching hooks
return nil
if len(filteredErrors) > 0 {
forbiddenErr := admission.NewForbidden(a, fmt.Errorf("admission request denied by policy"))
// The forbiddenErr is always a StatusError.
var err *apierrors.StatusError
if !errors.As(forbiddenErr, &err) {
// Should never happen.
return apierrors.NewInternalError(fmt.Errorf("failed to create status error"))
}
err.ErrStatus.Message = ""
for _, policyError := range filteredErrors {
message := policyError.Error()
// If this is the first denied decision, use its message and reason
// for the status error message.
if err.ErrStatus.Message == "" {
err.ErrStatus.Message = message
if policyError.Reason != "" {
err.ErrStatus.Reason = policyError.Reason
}
}
// Add the denied decision's message to the status error's details
err.ErrStatus.Details.Causes = append(
err.ErrStatus.Details.Causes,
metav1.StatusCause{Message: message})
}
return err
}
return d.delegate(ctx, a, o, versionedAttrAccessor, relevantHooks)
return nil
}
// Returns params to use to evaluate a policy-binding with given param
@@ -352,3 +400,18 @@ func (v *versionedAttributeAccessor) VersionedAttribute(gvk schema.GroupVersionK
v.versionedAttrs[gvk] = versionedAttr
return versionedAttr, nil
}
type PolicyError struct {
Policy PolicyAccessor
Binding BindingAccessor
Message error
Reason metav1.StatusReason
}
func (c PolicyError) Error() string {
if c.Binding != nil {
return fmt.Sprintf("policy '%s' with binding '%s' denied request: %s", c.Policy.GetName(), c.Binding.GetName(), c.Message.Error())
}
return fmt.Sprintf("policy %q denied request: %s", c.Policy.GetName(), c.Message.Error())
}

View File

@@ -17,22 +17,35 @@ limitations under the License.
package generic_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"k8s.io/api/admissionregistration/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
)
func makeTestDispatcher(authorizer.Authorizer, *matching.Matcher) generic.Dispatcher[generic.PolicyHook[*FakePolicy, *FakeBinding, generic.Evaluator]] {
type fakeDispatcher struct{}
func (fd *fakeDispatcher) Dispatch(context.Context, admission.Attributes, admission.ObjectInterfaces, []generic.PolicyHook[*FakePolicy, *FakeBinding, generic.Evaluator]) error {
return nil
}
func (fd *fakeDispatcher) Start(context.Context) error {
return nil
}
func makeTestDispatcher(authorizer.Authorizer, *matching.Matcher, kubernetes.Interface) generic.Dispatcher[generic.PolicyHook[*FakePolicy, *FakeBinding, generic.Evaluator]] {
return &fakeDispatcher{}
}
func TestPolicySourceHasSyncedEmpty(t *testing.T) {
testContext, testCancel, err := generic.NewPolicyTestContext(
@@ -207,6 +220,10 @@ func (fb *FakePolicy) GetMatchConstraints() *v1.MatchResources {
return nil
}
func (fb *FakePolicy) GetFailurePolicy() *v1.FailurePolicyType {
return nil
}
func (fb *FakeBinding) GetName() string {
return fb.Name
}

View File

@@ -0,0 +1,144 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutating
import (
v1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1alpha1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
)
func NewMutatingAdmissionPolicyAccessor(obj *Policy) generic.PolicyAccessor {
return &mutatingAdmissionPolicyAccessor{
Policy: obj,
}
}
func NewMutatingAdmissionPolicyBindingAccessor(obj *PolicyBinding) generic.BindingAccessor {
return &mutatingAdmissionPolicyBindingAccessor{
PolicyBinding: obj,
}
}
type mutatingAdmissionPolicyAccessor struct {
*Policy
}
func (v *mutatingAdmissionPolicyAccessor) GetNamespace() string {
return v.Namespace
}
func (v *mutatingAdmissionPolicyAccessor) GetName() string {
return v.Name
}
func (v *mutatingAdmissionPolicyAccessor) GetParamKind() *v1.ParamKind {
pk := v.Spec.ParamKind
if pk == nil {
return nil
}
return &v1.ParamKind{
APIVersion: pk.APIVersion,
Kind: pk.Kind,
}
}
func (v *mutatingAdmissionPolicyAccessor) GetMatchConstraints() *v1.MatchResources {
return convertV1alpha1ResourceRulesToV1(v.Spec.MatchConstraints)
}
func (v *mutatingAdmissionPolicyAccessor) GetFailurePolicy() *v1.FailurePolicyType {
return toV1FailurePolicy(v.Spec.FailurePolicy)
}
func toV1FailurePolicy(failurePolicy *v1alpha1.FailurePolicyType) *v1.FailurePolicyType {
if failurePolicy == nil {
return nil
}
fp := v1.FailurePolicyType(*failurePolicy)
return &fp
}
type mutatingAdmissionPolicyBindingAccessor struct {
*PolicyBinding
}
func (v *mutatingAdmissionPolicyBindingAccessor) GetNamespace() string {
return v.Namespace
}
func (v *mutatingAdmissionPolicyBindingAccessor) GetName() string {
return v.Name
}
func (v *mutatingAdmissionPolicyBindingAccessor) GetPolicyName() types.NamespacedName {
return types.NamespacedName{
Namespace: "",
Name: v.Spec.PolicyName,
}
}
func (v *mutatingAdmissionPolicyBindingAccessor) GetMatchResources() *v1.MatchResources {
return convertV1alpha1ResourceRulesToV1(v.Spec.MatchResources)
}
func (v *mutatingAdmissionPolicyBindingAccessor) GetParamRef() *v1.ParamRef {
if v.Spec.ParamRef == nil {
return nil
}
var nfa *v1.ParameterNotFoundActionType
if v.Spec.ParamRef.ParameterNotFoundAction != nil {
nfa = new(v1.ParameterNotFoundActionType)
*nfa = v1.ParameterNotFoundActionType(*v.Spec.ParamRef.ParameterNotFoundAction)
}
return &v1.ParamRef{
Name: v.Spec.ParamRef.Name,
Namespace: v.Spec.ParamRef.Namespace,
Selector: v.Spec.ParamRef.Selector,
ParameterNotFoundAction: nfa,
}
}
func convertV1alpha1ResourceRulesToV1(mc *v1alpha1.MatchResources) *v1.MatchResources {
if mc == nil {
return nil
}
var res v1.MatchResources
res.NamespaceSelector = mc.NamespaceSelector
res.ObjectSelector = mc.ObjectSelector
for _, ex := range mc.ExcludeResourceRules {
res.ExcludeResourceRules = append(res.ExcludeResourceRules, v1.NamedRuleWithOperations{
ResourceNames: ex.ResourceNames,
RuleWithOperations: ex.RuleWithOperations,
})
}
for _, ex := range mc.ResourceRules {
res.ResourceRules = append(res.ResourceRules, v1.NamedRuleWithOperations{
ResourceNames: ex.ResourceNames,
RuleWithOperations: ex.RuleWithOperations,
})
}
if mc.MatchPolicy != nil {
mp := v1.MatchPolicyType(*mc.MatchPolicy)
res.MatchPolicy = &mp
}
return &res
}

View File

@@ -0,0 +1,81 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutating
import (
"fmt"
"k8s.io/api/admissionregistration/v1alpha1"
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/environment"
)
// compilePolicy compiles the policy into a PolicyEvaluator
// any error is stored and delayed until invocation.
//
// Each individual mutation is compiled into MutationEvaluationFunc and
// returned is a PolicyEvaluator in the same order as the mutations appeared in the policy.
func compilePolicy(policy *Policy) PolicyEvaluator {
opts := plugincel.OptionalVariableDeclarations{HasParams: policy.Spec.ParamKind != nil, StrictCost: true, HasAuthorizer: true}
compiler, err := plugincel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
if err != nil {
return PolicyEvaluator{Error: &apiservercel.Error{
Type: apiservercel.ErrorTypeInternal,
Detail: fmt.Sprintf("failed to initialize CEL compiler: %v", err),
}}
}
// Compile and store variables
compiler.CompileAndStoreVariables(convertv1alpha1Variables(policy.Spec.Variables), opts, environment.StoredExpressions)
// Compile matchers
var matcher matchconditions.Matcher = nil
matchConditions := policy.Spec.MatchConditions
if len(matchConditions) > 0 {
matchExpressionAccessors := make([]plugincel.ExpressionAccessor, len(matchConditions))
for i := range matchConditions {
matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
}
matcher = matchconditions.NewMatcher(compiler.CompileCondition(matchExpressionAccessors, opts, environment.StoredExpressions), toV1FailurePolicy(policy.Spec.FailurePolicy), "policy", "validate", policy.Name)
}
// Compiler patchers
var patchers []patch.Patcher
patchOptions := opts
patchOptions.HasPatchTypes = true
for _, m := range policy.Spec.Mutations {
switch m.PatchType {
case v1alpha1.PatchTypeJSONPatch:
if m.JSONPatch != nil {
accessor := &patch.JSONPatchCondition{Expression: m.JSONPatch.Expression}
compileResult := compiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions)
patchers = append(patchers, patch.NewJSONPatcher(compileResult))
}
case v1alpha1.PatchTypeApplyConfiguration:
if m.ApplyConfiguration != nil {
accessor := &patch.ApplyConfigurationCondition{Expression: m.ApplyConfiguration.Expression}
compileResult := compiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions)
patchers = append(patchers, patch.NewApplyConfigurationPatcher(compileResult))
}
}
}
return PolicyEvaluator{Matcher: matcher, Mutators: patchers, CompositionEnv: compiler.CompositionEnv}
}

View File

@@ -0,0 +1,457 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutating
import (
"context"
"github.com/google/go-cmp/cmp"
"strings"
"testing"
"time"
"k8s.io/api/admissionregistration/v1alpha1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/client-go/openapi/openapitest"
"k8s.io/utils/ptr"
)
// TestCompilation is an open-box test of mutatingEvaluator.compile
// However, the result is a set of CEL programs, manually invoke them to assert
// on the results.
func TestCompilation(t *testing.T) {
deploymentGVR := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
deploymentGVK := schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}
testCases := []struct {
name string
policy *Policy
gvr schema.GroupVersionResource
object runtime.Object
oldObject runtime.Object
params runtime.Object
namespace *corev1.Namespace
expectedErr string
expectedResult runtime.Object
}{
{
name: "applyConfiguration then jsonPatch",
policy: mutations(policy("d1"), v1alpha1.Mutation{
PatchType: v1alpha1.PatchTypeApplyConfiguration,
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
Expression: `Object{
spec: Object.spec{
replicas: object.spec.replicas + 100
}
}`,
},
},
v1alpha1.Mutation{
PatchType: v1alpha1.PatchTypeJSONPatch,
JSONPatch: &v1alpha1.JSONPatch{
Expression: `[
JSONPatch{op: "replace", path: "/spec/replicas", value: object.spec.replicas + 10}
]`,
},
}),
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](111)}},
},
{
name: "jsonPatch then applyConfiguration",
policy: mutations(policy("d1"),
v1alpha1.Mutation{
PatchType: v1alpha1.PatchTypeJSONPatch,
JSONPatch: &v1alpha1.JSONPatch{
Expression: `[
JSONPatch{op: "replace", path: "/spec/replicas", value: object.spec.replicas + 10}
]`,
},
},
v1alpha1.Mutation{
PatchType: v1alpha1.PatchTypeApplyConfiguration,
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
Expression: `Object{
spec: Object.spec{
replicas: object.spec.replicas + 100
}
}`,
},
}),
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](111)}},
},
{
name: "jsonPatch with variable",
policy: jsonPatches(variables(policy("d1"), v1alpha1.Variable{Name: "desired", Expression: "10"}), v1alpha1.JSONPatch{
Expression: `[
JSONPatch{op: "replace", path: "/spec/replicas", value: variables.desired + 1},
]`,
}),
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](11)}},
},
{
name: "apply configuration with variable",
policy: applyConfigurations(variables(policy("d1"), v1alpha1.Variable{Name: "desired", Expression: "10"}),
`Object{
spec: Object.spec{
replicas: variables.desired + 1
}
}`),
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](11)}},
},
{
name: "apply configuration with params",
policy: paramKind(applyConfigurations(policy("d1"),
`Object{
spec: Object.spec{
replicas: int(params.data['k1'])
}
}`), &v1alpha1.ParamKind{Kind: "ConfigMap", APIVersion: "v1"}),
params: &corev1.ConfigMap{Data: map[string]string{"k1": "100"}},
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](100)}},
},
{
name: "jsonPatch with excessive cost",
policy: jsonPatches(variables(policy("d1"), v1alpha1.Variable{Name: "list", Expression: "[0,1,2,3,4,5,6,7,8,9]"}), v1alpha1.JSONPatch{
Expression: `[
JSONPatch{op: "replace", path: "/spec/replicas",
value: variables.list.all(x1, variables.list.all(x2, variables.list.all(x3, variables.list.all(x4, variables.list.all(x5, variables.list.all(x5, "0123456789" == "0123456789"))))))? 1 : 0
}
]`,
}),
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
expectedErr: "operation cancelled: actual cost limit exceeded",
},
{
name: "applyConfiguration with excessive cost",
policy: variables(applyConfigurations(policy("d1"),
`Object{
spec: Object.spec{
replicas: variables.list.all(x1, variables.list.all(x2, variables.list.all(x3, variables.list.all(x4, variables.list.all(x5, variables.list.all(x5, "0123456789" == "0123456789"))))))? 1 : 0
}
}`), v1alpha1.Variable{Name: "list", Expression: "[0,1,2,3,4,5,6,7,8,9]"}),
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
expectedErr: "operation cancelled: actual cost limit exceeded",
},
{
name: "request variable",
policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{
Expression: `[
JSONPatch{op: "replace", path: "/spec/replicas",
value: request.kind.group == 'apps' && request.kind.version == 'v1' && request.kind.kind == 'Deployment' ? 10 : 0
}
]`}),
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](10)}},
},
{
name: "namespace request variable",
policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{
Expression: `[
JSONPatch{op: "replace", path: "/spec/replicas",
value: namespaceObject.metadata.name == 'ns1' ? 10 : 0
}
]`}),
namespace: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns1"}},
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](10)}},
},
{
name: "authorizer check",
policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{
Expression: `[
JSONPatch{op: "replace", path: "/spec/replicas",
value: authorizer.group('').resource('endpoints').check('create').allowed() ? 10 : 0
}
]`,
}),
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](10)}},
},
{
name: "object type has field access",
policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{
Expression: `[
JSONPatch{
op: "add", path: "/metadata/labels",
value: {
"value": Object{field: "fieldValue"}.field,
}
}
]`,
}),
gvr: deploymentGVR,
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}},
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
"value": "fieldValue",
}}},
},
{
name: "object type has field testing",
policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{
Expression: `[
JSONPatch{
op: "add", path: "/metadata/labels",
value: {
"field": string(has(Object{field: "fieldValue"}.field)),
"field-unset": string(has(Object{}.field)),
}
}
]`,
}),
gvr: deploymentGVR,
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}},
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
"field": "true",
"field-unset": "false",
}}},
},
{
name: "object type equality",
policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{
Expression: `[
JSONPatch{
op: "add", path: "/metadata/labels",
value: {
"empty": string(Object{} == Object{}),
"same": string(Object{field: "x"} == Object{field: "x"}),
"different": string(Object{field: "x"} == Object{field: "y"}),
}
}
]`,
}),
gvr: deploymentGVR,
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}},
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
"empty": "true",
"same": "true",
"different": "false",
}}},
},
{
// TODO: This test documents existing behavior that we should be fixed before
// MutatingAdmissionPolicy graduates to beta.
// It is possible to initialize invalid Object types because we do not yet perform
// a full compilation pass with the types fully bound. Before beta, we should
// recompile all expressions with fully bound types before evaluation and report
// errors if invalid Object types like this are initialized.
name: "object types are not fully type checked",
policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{
Expression: `[
JSONPatch{
op: "add", path: "/spec",
value: Object.invalid{replicas: 1}
}
]`,
}),
gvr: deploymentGVR,
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}},
expectedResult: &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}},
Spec: appsv1.DeploymentSpec{
Replicas: ptr.To[int32](1),
},
},
},
}
scheme := runtime.NewScheme()
err := appsv1.AddToScheme(scheme)
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
tcManager := patch.NewTypeConverterManager(nil, openapitest.NewEmbeddedFileClient())
go tcManager.Run(ctx)
err = wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, time.Second, true, func(context.Context) (done bool, err error) {
converter := tcManager.GetTypeConverter(deploymentGVK)
return converter != nil, nil
})
if err != nil {
t.Fatal(err)
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var gvk schema.GroupVersionKind
gvks, _, err := scheme.ObjectKinds(tc.object)
if err != nil {
t.Fatal(err)
}
if len(gvks) == 1 {
gvk = gvks[0]
} else {
t.Fatalf("Failed to find gvk for type: %T", tc.object)
}
policyEvaluator := compilePolicy(tc.policy)
if policyEvaluator.CompositionEnv != nil {
ctx = policyEvaluator.CompositionEnv.CreateContext(ctx)
}
obj := tc.object
typeAccessor, err := meta.TypeAccessor(obj)
if err != nil {
t.Fatal(err)
}
typeAccessor.SetKind(gvk.Kind)
typeAccessor.SetAPIVersion(gvk.GroupVersion().String())
typeConverter := tcManager.GetTypeConverter(gvk)
metaAccessor, err := meta.Accessor(obj)
if err != nil {
t.Fatal(err)
}
for _, patcher := range policyEvaluator.Mutators {
attrs := admission.NewAttributesRecord(obj, tc.oldObject, gvk,
metaAccessor.GetNamespace(), metaAccessor.GetName(), tc.gvr,
"", admission.Create, &metav1.CreateOptions{}, false, nil)
vAttrs := &admission.VersionedAttributes{
Attributes: attrs,
VersionedKind: gvk,
VersionedObject: obj,
VersionedOldObject: tc.oldObject,
}
r := patch.Request{
MatchedResource: tc.gvr,
VersionedAttributes: vAttrs,
ObjectInterfaces: admission.NewObjectInterfacesFromScheme(scheme),
OptionalVariables: cel.OptionalVariableBindings{VersionedParams: tc.params, Authorizer: fakeAuthorizer{}},
Namespace: tc.namespace,
TypeConverter: typeConverter,
}
obj, err = patcher.Patch(ctx, r, celconfig.RuntimeCELCostBudget)
if len(tc.expectedErr) > 0 {
if err == nil {
t.Fatalf("expected error: %s", tc.expectedErr)
} else {
if !strings.Contains(err.Error(), tc.expectedErr) {
t.Fatalf("expected error: %s, got: %s", tc.expectedErr, err.Error())
}
return
}
}
if err != nil && len(tc.expectedErr) == 0 {
t.Fatalf("unexpected error: %v", err)
}
}
got, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
t.Fatal(err)
}
wantTypeAccessor, err := meta.TypeAccessor(tc.expectedResult)
if err != nil {
t.Fatal(err)
}
wantTypeAccessor.SetKind(gvk.Kind)
wantTypeAccessor.SetAPIVersion(gvk.GroupVersion().String())
want, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.expectedResult)
if err != nil {
t.Fatal(err)
}
if !equality.Semantic.DeepEqual(want, got) {
t.Errorf("unexpected result, got diff:\n%s\n", cmp.Diff(want, got))
}
})
}
}
func policy(name string) *v1alpha1.MutatingAdmissionPolicy {
return &v1alpha1.MutatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: v1alpha1.MutatingAdmissionPolicySpec{},
}
}
func variables(policy *v1alpha1.MutatingAdmissionPolicy, variables ...v1alpha1.Variable) *v1alpha1.MutatingAdmissionPolicy {
policy.Spec.Variables = append(policy.Spec.Variables, variables...)
return policy
}
func jsonPatches(policy *v1alpha1.MutatingAdmissionPolicy, jsonPatches ...v1alpha1.JSONPatch) *v1alpha1.MutatingAdmissionPolicy {
for _, jsonPatch := range jsonPatches {
policy.Spec.Mutations = append(policy.Spec.Mutations, v1alpha1.Mutation{
JSONPatch: &jsonPatch,
PatchType: v1alpha1.PatchTypeJSONPatch,
})
}
return policy
}
func applyConfigurations(policy *v1alpha1.MutatingAdmissionPolicy, expressions ...string) *v1alpha1.MutatingAdmissionPolicy {
for _, expression := range expressions {
policy.Spec.Mutations = append(policy.Spec.Mutations, v1alpha1.Mutation{
ApplyConfiguration: &v1alpha1.ApplyConfiguration{Expression: expression},
PatchType: v1alpha1.PatchTypeApplyConfiguration,
})
}
return policy
}
func paramKind(policy *v1alpha1.MutatingAdmissionPolicy, paramKind *v1alpha1.ParamKind) *v1alpha1.MutatingAdmissionPolicy {
policy.Spec.ParamKind = paramKind
return policy
}
func mutations(policy *v1alpha1.MutatingAdmissionPolicy, mutations ...v1alpha1.Mutation) *v1alpha1.MutatingAdmissionPolicy {
policy.Spec.Mutations = append(policy.Spec.Mutations, mutations...)
return policy
}
func matchConstraints(policy *v1alpha1.MutatingAdmissionPolicy, matchConstraints *v1alpha1.MatchResources) *v1alpha1.MutatingAdmissionPolicy {
policy.Spec.MatchConstraints = matchConstraints
return policy
}
type fakeAuthorizer struct{}
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
return authorizer.DecisionAllow, "", nil
}

View File

@@ -0,0 +1,284 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutating
import (
"context"
"errors"
"fmt"
"k8s.io/api/admissionregistration/v1alpha1"
v1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/admission"
admissionauthorizer "k8s.io/apiserver/pkg/admission/plugin/authorizer"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
webhookgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
func NewDispatcher(a authorizer.Authorizer, m *matching.Matcher, tcm patch.TypeConverterManager) generic.Dispatcher[PolicyHook] {
res := &dispatcher{
matcher: m,
authz: a,
typeConverterManager: tcm,
}
res.Dispatcher = generic.NewPolicyDispatcher[*Policy, *PolicyBinding, PolicyEvaluator](
NewMutatingAdmissionPolicyAccessor,
NewMutatingAdmissionPolicyBindingAccessor,
m,
res.dispatchInvocations,
)
return res
}
type dispatcher struct {
matcher *matching.Matcher
authz authorizer.Authorizer
typeConverterManager patch.TypeConverterManager
generic.Dispatcher[PolicyHook]
}
func (d *dispatcher) Start(ctx context.Context) error {
go d.typeConverterManager.Run(ctx)
return d.Dispatcher.Start(ctx)
}
func (d *dispatcher) dispatchInvocations(
ctx context.Context,
a admission.Attributes,
o admission.ObjectInterfaces,
versionedAttributes webhookgeneric.VersionedAttributeAccessor,
invocations []generic.PolicyInvocation[*Policy, *PolicyBinding, PolicyEvaluator],
) ([]generic.PolicyError, *k8serrors.StatusError) {
var lastVersionedAttr *admission.VersionedAttributes
reinvokeCtx := a.GetReinvocationContext()
var policyReinvokeCtx *policyReinvokeContext
if v := reinvokeCtx.Value(PluginName); v != nil {
policyReinvokeCtx = v.(*policyReinvokeContext)
} else {
policyReinvokeCtx = &policyReinvokeContext{}
reinvokeCtx.SetValue(PluginName, policyReinvokeCtx)
}
if reinvokeCtx.IsReinvoke() && policyReinvokeCtx.IsOutputChangedSinceLastPolicyInvocation(a.GetObject()) {
// If the object has changed, we know the in-tree plugin re-invocations have mutated the object,
// and we need to reinvoke all eligible policies.
policyReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
}
defer func() {
policyReinvokeCtx.SetLastPolicyInvocationOutput(a.GetObject())
}()
var policyErrors []generic.PolicyError
addConfigError := func(err error, invocation generic.PolicyInvocation[*Policy, *PolicyBinding, PolicyEvaluator], reason metav1.StatusReason) {
policyErrors = append(policyErrors, generic.PolicyError{
Message: err,
Policy: NewMutatingAdmissionPolicyAccessor(invocation.Policy),
Binding: NewMutatingAdmissionPolicyBindingAccessor(invocation.Binding),
Reason: reason,
})
}
// There is at least one invocation to invoke. Make sure we have a namespace
// object if the incoming object is not cluster scoped to pass into the evaluator.
var namespace *v1.Namespace
var err error
namespaceName := a.GetNamespace()
// Special case, the namespace object has the namespace of itself (maybe a bug).
// unset it if the incoming object is a namespace
if gvk := a.GetKind(); gvk.Kind == "Namespace" && gvk.Version == "v1" && gvk.Group == "" {
namespaceName = ""
}
// if it is cluster scoped, namespaceName will be empty
// Otherwise, get the Namespace resource.
if namespaceName != "" {
namespace, err = d.matcher.GetNamespace(namespaceName)
if err != nil {
return nil, k8serrors.NewNotFound(schema.GroupResource{Group: "", Resource: "namespaces"}, namespaceName)
}
}
authz := admissionauthorizer.NewCachingAuthorizer(d.authz)
// Should loop through invocations, handling possible error and invoking
// evaluator to apply patch, also should handle re-invocations
for _, invocation := range invocations {
if invocation.Evaluator.CompositionEnv != nil {
ctx = invocation.Evaluator.CompositionEnv.CreateContext(ctx)
}
if len(invocation.Evaluator.Mutators) != len(invocation.Policy.Spec.Mutations) {
// This would be a bug. The compiler should always return exactly as
// many evaluators as there are mutations
return nil, k8serrors.NewInternalError(fmt.Errorf("expected %v compiled evaluators for policy %v, got %v",
len(invocation.Policy.Spec.Mutations), invocation.Policy.Name, len(invocation.Evaluator.Mutators)))
}
versionedAttr, err := versionedAttributes.VersionedAttribute(invocation.Kind)
if err != nil {
// This should never happen, we pre-warm versoined attribute
// accessors before starting the dispatcher
return nil, k8serrors.NewInternalError(err)
}
if invocation.Evaluator.Matcher != nil {
matchResults := invocation.Evaluator.Matcher.Match(ctx, versionedAttr, invocation.Param, authz)
if matchResults.Error != nil {
addConfigError(matchResults.Error, invocation, metav1.StatusReasonInvalid)
continue
}
// if preconditions are not met, then skip mutations
if !matchResults.Matches {
continue
}
}
invocationKey, invocationKeyErr := keyFor(invocation)
if reinvokeCtx.IsReinvoke() && !policyReinvokeCtx.ShouldReinvoke(invocationKey) {
continue
}
objectBeforeMutations := versionedAttr.VersionedObject
// Mutations for a single invocation of a MutatingAdmissionPolicy are evaluated
// in order.
for mutationIndex := range invocation.Policy.Spec.Mutations {
if invocationKeyErr != nil {
// This should never happen. It occurs if there is a programming
// error causing the Param not to be a valid object.
return nil, k8serrors.NewInternalError(invocationKeyErr)
}
lastVersionedAttr = versionedAttr
if versionedAttr.VersionedObject == nil { // Do not call patchers if there is no object to patch.
continue
}
patcher := invocation.Evaluator.Mutators[mutationIndex]
optionalVariables := cel.OptionalVariableBindings{VersionedParams: invocation.Param, Authorizer: authz}
err = d.dispatchOne(ctx, patcher, o, versionedAttr, namespace, invocation.Resource, optionalVariables)
if err != nil {
var statusError *k8serrors.StatusError
if errors.As(err, &statusError) {
return nil, statusError
}
addConfigError(err, invocation, metav1.StatusReasonInvalid)
continue
}
}
if !apiequality.Semantic.DeepEqual(objectBeforeMutations, versionedAttr.VersionedObject) {
// The mutation has changed the object. Prepare to reinvoke all previous mutations that are eligible for re-invocation.
policyReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
reinvokeCtx.SetShouldReinvoke()
}
if invocation.Policy.Spec.ReinvocationPolicy == v1alpha1.IfNeededReinvocationPolicy {
policyReinvokeCtx.AddReinvocablePolicyToPreviouslyInvoked(invocationKey)
}
}
if lastVersionedAttr != nil && lastVersionedAttr.VersionedObject != nil && lastVersionedAttr.Dirty {
policyReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
reinvokeCtx.SetShouldReinvoke()
if err := o.GetObjectConvertor().Convert(lastVersionedAttr.VersionedObject, lastVersionedAttr.Attributes.GetObject(), nil); err != nil {
return nil, k8serrors.NewInternalError(fmt.Errorf("failed to convert object: %w", err))
}
}
return policyErrors, nil
}
func (d *dispatcher) dispatchOne(
ctx context.Context,
patcher patch.Patcher,
o admission.ObjectInterfaces,
versionedAttributes *admission.VersionedAttributes,
namespace *v1.Namespace,
resource schema.GroupVersionResource,
optionalVariables cel.OptionalVariableBindings,
) (err error) {
if patcher == nil {
// internal error. this should not happen
return k8serrors.NewInternalError(fmt.Errorf("policy evaluator is nil"))
}
// Find type converter for the invoked Group-Version.
typeConverter := d.typeConverterManager.GetTypeConverter(versionedAttributes.VersionedKind)
if typeConverter == nil {
// This can happen if the request is for a resource whose schema
// has not been registered with the type converter manager.
return k8serrors.NewServiceUnavailable(fmt.Sprintf("Resource kind %s not found. There can be a delay between when CustomResourceDefinitions are created and when they are available.", versionedAttributes.VersionedKind))
}
patchRequest := patch.Request{
MatchedResource: resource,
VersionedAttributes: versionedAttributes,
ObjectInterfaces: o,
OptionalVariables: optionalVariables,
Namespace: namespace,
TypeConverter: typeConverter,
}
newVersionedObject, err := patcher.Patch(ctx, patchRequest, celconfig.RuntimeCELCostBudget)
if err != nil {
return err
}
versionedAttributes.Dirty = true
versionedAttributes.VersionedObject = newVersionedObject
o.GetObjectDefaulter().Default(newVersionedObject)
return nil
}
func keyFor(invocation generic.PolicyInvocation[*Policy, *PolicyBinding, PolicyEvaluator]) (key, error) {
var paramUID types.NamespacedName
if invocation.Param != nil {
paramAccessor, err := meta.Accessor(invocation.Param)
if err != nil {
// This should never happen, as the param should have been validated
// before being passed to the plugin.
return key{}, err
}
paramUID = types.NamespacedName{
Name: paramAccessor.GetName(),
Namespace: paramAccessor.GetNamespace(),
}
}
return key{
PolicyUID: types.NamespacedName{
Name: invocation.Policy.GetName(),
Namespace: invocation.Policy.GetNamespace(),
},
BindingUID: types.NamespacedName{
Name: invocation.Binding.GetName(),
Namespace: invocation.Binding.GetNamespace(),
},
ParamUID: paramUID,
}, nil
}

View File

@@ -0,0 +1,675 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutating
import (
"context"
"github.com/google/go-cmp/cmp"
"testing"
"time"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1alpha1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/openapi/openapitest"
"k8s.io/utils/ptr"
)
func TestDispatcher(t *testing.T) {
deploymentGVK := schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}
deploymentGVR := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
testCases := []struct {
name string
object, oldObject runtime.Object
gvk schema.GroupVersionKind
gvr schema.GroupVersionResource
params []runtime.Object // All params are expected to be ConfigMap for this test.
policyHooks []PolicyHook
expect runtime.Object
}{
{
name: "simple patch",
gvk: deploymentGVK,
gvr: deploymentGVR,
object: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: "apps/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "d1",
Namespace: "default",
},
Spec: appsv1.DeploymentSpec{
Replicas: ptr.To[int32](1),
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{{Name: "x"}},
},
},
}},
policyHooks: []generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]{
{
Policy: mutations(matchConstraints(policy("policy1"), &v1alpha1.MatchResources{
MatchPolicy: ptr.To(v1alpha1.Equivalent),
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: v1alpha1.RuleWithOperations{
Rule: v1alpha1.Rule{
APIGroups: []string{"apps"},
APIVersions: []string{"v1"},
Resources: []string{"deployments"},
},
Operations: []admissionregistrationv1.OperationType{"*"},
},
},
},
}), v1alpha1.Mutation{
PatchType: v1alpha1.PatchTypeApplyConfiguration,
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
Expression: `Object{
spec: Object.spec{
replicas: object.spec.replicas + 100
}
}`,
}}),
Bindings: []*PolicyBinding{{
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
PolicyName: "policy1",
},
}},
},
},
expect: &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "d1",
Namespace: "default",
},
Spec: appsv1.DeploymentSpec{
Replicas: ptr.To[int32](101),
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{{Name: "x"}},
},
},
}},
},
{
name: "with param",
gvk: deploymentGVK,
gvr: deploymentGVR,
object: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: "apps/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "d1",
Namespace: "default",
},
Spec: appsv1.DeploymentSpec{
Replicas: ptr.To[int32](1),
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{{Name: "x"}},
},
},
}},
params: []runtime.Object{
&corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "ConfigMap",
},
ObjectMeta: metav1.ObjectMeta{
Name: "cm1",
Namespace: "default",
},
Data: map[string]string{
"key": "10",
},
},
},
policyHooks: []generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]{
{
Policy: paramKind(mutations(matchConstraints(policy("policy1"), &v1alpha1.MatchResources{
MatchPolicy: ptr.To(v1alpha1.Equivalent),
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: v1alpha1.RuleWithOperations{
Rule: v1alpha1.Rule{
APIGroups: []string{"apps"},
APIVersions: []string{"v1"},
Resources: []string{"deployments"},
},
Operations: []admissionregistrationv1.OperationType{"*"},
},
},
}}),
v1alpha1.Mutation{
PatchType: v1alpha1.PatchTypeApplyConfiguration,
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
Expression: `Object{
spec: Object.spec{
replicas: object.spec.replicas + int(params.data['key'])
}
}`,
}}),
&v1alpha1.ParamKind{
APIVersion: "v1",
Kind: "ConfigMap",
}),
Bindings: []*PolicyBinding{{
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
PolicyName: "policy1",
ParamRef: &v1alpha1.ParamRef{Name: "cm1", Namespace: "default"},
},
}},
},
},
expect: &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "d1",
Namespace: "default",
},
Spec: appsv1.DeploymentSpec{
Replicas: ptr.To[int32](11),
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{{Name: "x"}},
},
},
}},
},
{
name: "both policies reinvoked",
gvk: deploymentGVK,
gvr: deploymentGVR,
object: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: "apps/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "d1",
Namespace: "default",
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{{Name: "x"}},
},
},
}},
policyHooks: []generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]{
{
Policy: mutations(matchConstraints(policy("policy1"), &v1alpha1.MatchResources{
MatchPolicy: ptr.To(v1alpha1.Equivalent),
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: v1alpha1.RuleWithOperations{
Rule: v1alpha1.Rule{
APIGroups: []string{"apps"},
APIVersions: []string{"v1"},
Resources: []string{"deployments"},
},
Operations: []admissionregistrationv1.OperationType{"*"},
},
},
},
}), v1alpha1.Mutation{
PatchType: v1alpha1.PatchTypeApplyConfiguration,
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
Expression: `Object{
metadata: Object.metadata{
labels: {"policy1": string(int(object.?metadata.labels["count"].orValue("1")) + 1)}
}
}`,
}}),
Bindings: []*PolicyBinding{{
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
PolicyName: "policy1",
},
}},
},
{
Policy: mutations(matchConstraints(policy("policy2"), &v1alpha1.MatchResources{
MatchPolicy: ptr.To(v1alpha1.Equivalent),
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: v1alpha1.RuleWithOperations{
Rule: v1alpha1.Rule{
APIGroups: []string{"apps"},
APIVersions: []string{"v1"},
Resources: []string{"deployments"},
},
Operations: []admissionregistrationv1.OperationType{"*"},
},
},
},
}), v1alpha1.Mutation{
PatchType: v1alpha1.PatchTypeApplyConfiguration,
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
Expression: `Object{
metadata: Object.metadata{
labels: {"policy2": string(int(object.?metadata.labels["count"].orValue("1")) + 1)}
}
}`,
}}),
Bindings: []*PolicyBinding{{
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
PolicyName: "policy2",
},
}},
},
},
expect: &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "d1",
Namespace: "default",
Labels: map[string]string{
"policy1": "2",
"policy2": "2",
},
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{{Name: "x"}},
},
},
}},
},
{
name: "1st policy sets match condition that 2nd policy matches",
gvk: deploymentGVK,
gvr: deploymentGVR,
object: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: "apps/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "d1",
Namespace: "default",
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{{Name: "x"}},
},
},
}},
policyHooks: []generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]{
{
Policy: &v1alpha1.MutatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "policy1",
},
Spec: v1alpha1.MutatingAdmissionPolicySpec{
MatchConstraints: &v1alpha1.MatchResources{
MatchPolicy: ptr.To(v1alpha1.Equivalent),
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: v1alpha1.RuleWithOperations{
Rule: v1alpha1.Rule{
APIGroups: []string{"apps"},
APIVersions: []string{"v1"},
Resources: []string{"deployments"},
},
Operations: []admissionregistrationv1.OperationType{"*"},
},
},
},
},
Mutations: []v1alpha1.Mutation{{
PatchType: v1alpha1.PatchTypeApplyConfiguration,
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
Expression: `Object{
metadata: Object.metadata{
labels: {"environment": "production"}
}
}`}},
},
},
},
Bindings: []*PolicyBinding{{
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
PolicyName: "policy1",
},
}},
},
{
Policy: &v1alpha1.MutatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "policy2",
},
Spec: v1alpha1.MutatingAdmissionPolicySpec{
MatchConstraints: &v1alpha1.MatchResources{
MatchPolicy: ptr.To(v1alpha1.Equivalent),
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: v1alpha1.RuleWithOperations{
Rule: v1alpha1.Rule{
APIGroups: []string{"apps"},
APIVersions: []string{"v1"},
Resources: []string{"deployments"},
},
Operations: []admissionregistrationv1.OperationType{"*"},
},
},
},
},
MatchConditions: []v1alpha1.MatchCondition{
{
Name: "prodonly",
Expression: `object.?metadata.labels["environment"].orValue("") == "production"`,
},
},
Mutations: []v1alpha1.Mutation{{
PatchType: v1alpha1.PatchTypeApplyConfiguration,
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
Expression: `Object{
metadata: Object.metadata{
labels: {"policy1invoked": "true"}
}
}`}},
},
},
},
Bindings: []*PolicyBinding{{
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
PolicyName: "policy2",
},
}},
},
},
expect: &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "d1",
Namespace: "default",
Labels: map[string]string{
"environment": "production",
"policy1invoked": "true",
},
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{{Name: "x"}},
},
},
}},
},
{
// TODO: This behavior pre-exists with webhook match conditions but should be reconsidered
name: "1st policy still does not match after 2nd policy sets match condition",
gvk: deploymentGVK,
gvr: deploymentGVR,
object: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: "apps/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "d1",
Namespace: "default",
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{{Name: "x"}},
},
},
}},
policyHooks: []generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]{
{
Policy: &v1alpha1.MutatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "policy1",
},
Spec: v1alpha1.MutatingAdmissionPolicySpec{
MatchConstraints: &v1alpha1.MatchResources{
MatchPolicy: ptr.To(v1alpha1.Equivalent),
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: v1alpha1.RuleWithOperations{
Rule: v1alpha1.Rule{
APIGroups: []string{"apps"},
APIVersions: []string{"v1"},
Resources: []string{"deployments"},
},
Operations: []admissionregistrationv1.OperationType{"*"},
},
},
},
},
MatchConditions: []v1alpha1.MatchCondition{
{
Name: "prodonly",
Expression: `object.?metadata.labels["environment"].orValue("") == "production"`,
},
},
Mutations: []v1alpha1.Mutation{{
PatchType: v1alpha1.PatchTypeApplyConfiguration,
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
Expression: `Object{
metadata: Object.metadata{
labels: {"policy1invoked": "true"}
}
}`}},
},
},
},
Bindings: []*PolicyBinding{{
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
PolicyName: "policy1",
},
}},
},
{
Policy: &v1alpha1.MutatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "policy2",
},
Spec: v1alpha1.MutatingAdmissionPolicySpec{
MatchConstraints: &v1alpha1.MatchResources{
MatchPolicy: ptr.To(v1alpha1.Equivalent),
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: v1alpha1.RuleWithOperations{
Rule: v1alpha1.Rule{
APIGroups: []string{"apps"},
APIVersions: []string{"v1"},
Resources: []string{"deployments"},
},
Operations: []admissionregistrationv1.OperationType{"*"},
},
},
},
},
Mutations: []v1alpha1.Mutation{{
PatchType: v1alpha1.PatchTypeApplyConfiguration,
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
Expression: `Object{
metadata: Object.metadata{
labels: {"environment": "production"}
}
}`}},
},
},
},
Bindings: []*PolicyBinding{{
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
PolicyName: "policy2",
},
}},
},
},
expect: &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "d1",
Namespace: "default",
Labels: map[string]string{
"environment": "production",
},
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{{Name: "x"}},
},
},
}},
},
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tcManager := patch.NewTypeConverterManager(nil, openapitest.NewEmbeddedFileClient())
go tcManager.Run(ctx)
err := wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, time.Second, true, func(context.Context) (done bool, err error) {
converter := tcManager.GetTypeConverter(deploymentGVK)
return converter != nil, nil
})
if err != nil {
t.Fatal(err)
}
scheme := runtime.NewScheme()
err = appsv1.AddToScheme(scheme)
if err != nil {
t.Fatal(err)
}
err = corev1.AddToScheme(scheme)
if err != nil {
t.Fatal(err)
}
objectInterfaces := admission.NewObjectInterfacesFromScheme(scheme)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
client := fake.NewClientset(tc.params...)
// always include default namespace
err := client.Tracker().Add(&corev1.Namespace{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Namespace",
},
ObjectMeta: metav1.ObjectMeta{
Name: "default",
},
Spec: corev1.NamespaceSpec{},
})
if err != nil {
t.Fatal(err)
}
informerFactory := informers.NewSharedInformerFactory(client, 0)
matcher := matching.NewMatcher(informerFactory.Core().V1().Namespaces().Lister(), client)
paramInformer, err := informerFactory.ForResource(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"})
if err != nil {
t.Fatal(err)
}
informerFactory.WaitForCacheSync(ctx.Done())
informerFactory.Start(ctx.Done())
for i, h := range tc.policyHooks {
tc.policyHooks[i].ParamInformer = paramInformer
tc.policyHooks[i].ParamScope = testParamScope{}
tc.policyHooks[i].Evaluator = compilePolicy(h.Policy)
}
dispatcher := NewDispatcher(fakeAuthorizer{}, matcher, tcManager)
err = dispatcher.Start(ctx)
if err != nil {
t.Fatalf("error starting dispatcher: %v", err)
}
metaAccessor, err := meta.Accessor(tc.object)
if err != nil {
t.Fatal(err)
}
attrs := admission.NewAttributesRecord(tc.object, tc.oldObject, tc.gvk,
metaAccessor.GetNamespace(), metaAccessor.GetName(), tc.gvr,
"", admission.Create, &metav1.CreateOptions{}, false, nil)
vAttrs := &admission.VersionedAttributes{
Attributes: attrs,
VersionedKind: tc.gvk,
VersionedObject: tc.object,
VersionedOldObject: tc.oldObject,
}
err = dispatcher.Dispatch(ctx, vAttrs, objectInterfaces, tc.policyHooks)
if err != nil {
t.Fatalf("error dispatching policy hooks: %v", err)
}
obj := vAttrs.VersionedObject
if !equality.Semantic.DeepEqual(obj, tc.expect) {
t.Errorf("unexpected result, got diff:\n%s\n", cmp.Diff(tc.expect, obj))
}
})
}
}
type testParamScope struct{}
func (t testParamScope) Name() meta.RESTScopeName {
return meta.RESTScopeNameNamespace
}
var _ meta.RESTScope = testParamScope{}

View File

@@ -0,0 +1,45 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package patch
import (
"context"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/managedfields"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
)
// Patcher provides a patch function to perform a mutation to an object in the admission chain.
type Patcher interface {
// Patch returns a copy of the object in the request, modified to change specified by the patch.
// The original object in the request MUST NOT be modified in-place.
Patch(ctx context.Context, request Request, runtimeCELCostBudget int64) (runtime.Object, error)
}
// Request defines the arguments required by a patcher.
type Request struct {
MatchedResource schema.GroupVersionResource
VersionedAttributes *admission.VersionedAttributes
ObjectInterfaces admission.ObjectInterfaces
OptionalVariables cel.OptionalVariableBindings
Namespace *v1.Namespace
TypeConverter managedfields.TypeConverter
}

View File

@@ -0,0 +1,192 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package patch
import (
"context"
gojson "encoding/json"
"errors"
"fmt"
celgo "github.com/google/cel-go/cel"
"reflect"
"strconv"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/traits"
"google.golang.org/protobuf/types/known/structpb"
jsonpatch "gopkg.in/evanphx/json-patch.v4"
admissionv1 "k8s.io/api/admission/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/cel/mutation"
"k8s.io/apiserver/pkg/cel/mutation/dynamic"
pointer "k8s.io/utils/ptr"
)
// JSONPatchCondition contains the inputs needed to compile and evaluate a cel expression
// that returns a JSON patch value.
type JSONPatchCondition struct {
Expression string
}
var _ plugincel.ExpressionAccessor = &JSONPatchCondition{}
func (v *JSONPatchCondition) GetExpression() string {
return v.Expression
}
func (v *JSONPatchCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.ListType(jsonPatchType)}
}
var jsonPatchType = types.NewObjectType("JSONPatch")
// NewJSONPatcher creates a patcher that performs a JSON Patch mutation.
func NewJSONPatcher(patchEvaluator plugincel.MutatingEvaluator) Patcher {
return &jsonPatcher{patchEvaluator}
}
type jsonPatcher struct {
PatchEvaluator plugincel.MutatingEvaluator
}
func (e *jsonPatcher) Patch(ctx context.Context, r Request, runtimeCELCostBudget int64) (runtime.Object, error) {
admissionRequest := plugincel.CreateAdmissionRequest(
r.VersionedAttributes.Attributes,
metav1.GroupVersionResource(r.MatchedResource),
metav1.GroupVersionKind(r.VersionedAttributes.VersionedKind))
compileErrors := e.PatchEvaluator.CompilationErrors()
if len(compileErrors) > 0 {
return nil, errors.Join(compileErrors...)
}
patchObj, _, err := e.evaluatePatchExpression(ctx, e.PatchEvaluator, runtimeCELCostBudget, r, admissionRequest)
if err != nil {
return nil, err
}
o := r.ObjectInterfaces
jsonSerializer := json.NewSerializerWithOptions(json.DefaultMetaFactory, o.GetObjectCreater(), o.GetObjectTyper(), json.SerializerOptions{Pretty: false, Strict: true})
objJS, err := runtime.Encode(jsonSerializer, r.VersionedAttributes.VersionedObject)
if err != nil {
return nil, fmt.Errorf("failed to create JSON patch: %w", err)
}
patchedJS, err := patchObj.Apply(objJS)
if err != nil {
if errors.Is(err, jsonpatch.ErrTestFailed) {
// If a json patch fails a test operation, the patch must not be applied
return r.VersionedAttributes.VersionedObject, nil
}
return nil, fmt.Errorf("JSON Patch: %w", err)
}
var newVersionedObject runtime.Object
if _, ok := r.VersionedAttributes.VersionedObject.(*unstructured.Unstructured); ok {
newVersionedObject = &unstructured.Unstructured{}
} else {
newVersionedObject, err = o.GetObjectCreater().New(r.VersionedAttributes.VersionedKind)
if err != nil {
return nil, apierrors.NewInternalError(err)
}
}
if newVersionedObject, _, err = jsonSerializer.Decode(patchedJS, nil, newVersionedObject); err != nil {
return nil, apierrors.NewInternalError(err)
}
return newVersionedObject, nil
}
func (e *jsonPatcher) evaluatePatchExpression(ctx context.Context, patchEvaluator plugincel.MutatingEvaluator, remainingBudget int64, r Request, admissionRequest *admissionv1.AdmissionRequest) (jsonpatch.Patch, int64, error) {
var err error
var eval plugincel.EvaluationResult
eval, remainingBudget, err = patchEvaluator.ForInput(ctx, r.VersionedAttributes, admissionRequest, r.OptionalVariables, r.Namespace, remainingBudget)
if err != nil {
return nil, -1, err
}
if eval.Error != nil {
return nil, -1, eval.Error
}
refVal := eval.EvalResult
// the return type can be any valid CEL value.
// Scalars, maps and lists are used to set the value when the path points to a field of that type.
// ObjectVal is used when the path points to a struct. A map like "{"field1": 1, "fieldX": bool}" is not
// possible in Kubernetes CEL because maps and lists may not have mixed types.
iter, ok := refVal.(traits.Lister)
if !ok {
// Should never happen since compiler checks return type.
return nil, -1, fmt.Errorf("type mismatch: JSONPatchType.expression should evaluate to array")
}
result := jsonpatch.Patch{}
for it := iter.Iterator(); it.HasNext() == types.True; {
v := it.Next()
patchObj, err := v.ConvertToNative(reflect.TypeOf(&mutation.JSONPatchVal{}))
if err != nil {
// Should never happen since return type is checked by compiler.
return nil, -1, fmt.Errorf("type mismatch: JSONPatchType.expression should evaluate to array of JSONPatch: %w", err)
}
op, ok := patchObj.(*mutation.JSONPatchVal)
if !ok {
// Should never happen since return type is checked by compiler.
return nil, -1, fmt.Errorf("type mismatch: JSONPatchType.expression should evaluate to array of JSONPatch, got element of %T", patchObj)
}
// Construct a JSON Patch from the evaluated CEL expression
resultOp := jsonpatch.Operation{}
resultOp["op"] = pointer.To(gojson.RawMessage(strconv.Quote(op.Op)))
resultOp["path"] = pointer.To(gojson.RawMessage(strconv.Quote(op.Path)))
if len(op.From) > 0 {
resultOp["from"] = pointer.To(gojson.RawMessage(strconv.Quote(op.From)))
}
if op.Val != nil {
if objVal, ok := op.Val.(*dynamic.ObjectVal); ok {
// TODO: Object initializers are insufficiently type checked.
// In the interim, we use this sanity check to detect type mismatches
// between field names and Object initializers. For example,
// "Object.spec{ selector: Object.spec.wrong{}}" is detected as a mismatch.
// Before beta, attaching full type information both to Object initializers and
// the "object" and "oldObject" variables is needed. This will allow CEL to
// perform comprehensive runtime type checking.
err := objVal.CheckTypeNamesMatchFieldPathNames()
if err != nil {
return nil, -1, fmt.Errorf("type mismatch: %w", err)
}
}
// CEL data literals representing arbitrary JSON values can be serialized to JSON for use in
// JSON Patch if first converted to pb.Value.
v, err := op.Val.ConvertToNative(reflect.TypeOf(&structpb.Value{}))
if err != nil {
return nil, -1, fmt.Errorf("JSONPath valueExpression evaluated to a type that could not marshal to JSON: %w", err)
}
b, err := gojson.Marshal(v)
if err != nil {
return nil, -1, fmt.Errorf("JSONPath valueExpression evaluated to a type that could not marshal to JSON: %w", err)
}
resultOp["value"] = pointer.To[gojson.RawMessage](b)
}
result = append(result, resultOp)
}
return result, remainingBudget, nil
}

View File

@@ -0,0 +1,486 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package patch
import (
"context"
"github.com/google/go-cmp/cmp"
"strings"
"testing"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/utils/ptr"
)
func TestJSONPatch(t *testing.T) {
deploymentGVR := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
tests := []struct {
name string
expression string
gvr schema.GroupVersionResource
object, oldObject runtime.Object
expectedResult runtime.Object
expectedErr string
}{
{
name: "jsonPatch with false test operation",
expression: `[
JSONPatch{op: "test", path: "/spec/replicas", value: 100},
JSONPatch{op: "replace", path: "/spec/replicas", value: 3},
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
},
{
name: "jsonPatch with false test operation",
expression: `[
JSONPatch{op: "test", path: "/spec/replicas", value: 100},
JSONPatch{op: "replace", path: "/spec/replicas", value: 3},
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
},
{
name: "jsonPatch with true test operation",
expression: `[
JSONPatch{op: "test", path: "/spec/replicas", value: 1},
JSONPatch{op: "replace", path: "/spec/replicas", value: 3},
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](3)}},
},
{
name: "jsonPatch remove to unset field",
expression: `[
JSONPatch{op: "remove", path: "/spec/replicas"},
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{}},
},
{
name: "jsonPatch remove map entry by key",
expression: `[
JSONPatch{op: "remove", path: "/metadata/labels/y"},
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1", "y": "1"}}, Spec: appsv1.DeploymentSpec{}},
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1"}}, Spec: appsv1.DeploymentSpec{}},
},
{
name: "jsonPatch remove element in list",
expression: `[
JSONPatch{op: "remove", path: "/spec/template/spec/containers/1"},
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "a"}, {Name: "b"}, {Name: "c"}},
}}}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "a"}, {Name: "c"}},
}}}},
},
{
name: "jsonPatch copy map entry by key",
expression: `[
JSONPatch{op: "copy", from: "/metadata/labels/x", path: "/metadata/labels/y"},
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1"}}, Spec: appsv1.DeploymentSpec{}},
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1", "y": "1"}}, Spec: appsv1.DeploymentSpec{}},
},
{
name: "jsonPatch copy first element to end of list",
expression: `[
JSONPatch{op: "copy", from: "/spec/template/spec/containers/0", path: "/spec/template/spec/containers/-"},
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "a"}, {Name: "b"}, {Name: "c"}},
}}}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "a"}, {Name: "b"}, {Name: "c"}, {Name: "a"}},
}}}},
},
{
name: "jsonPatch move map entry by key",
expression: `[
JSONPatch{op: "move", from: "/metadata/labels/x", path: "/metadata/labels/y"},
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1"}}, Spec: appsv1.DeploymentSpec{}},
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "1"}}, Spec: appsv1.DeploymentSpec{}},
},
{
name: "jsonPatch move first element to end of list",
expression: `[
JSONPatch{op: "move", from: "/spec/template/spec/containers/0", path: "/spec/template/spec/containers/-"},
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "a"}, {Name: "b"}, {Name: "c"}},
}}}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "b"}, {Name: "c"}, {Name: "a"}},
}}}},
},
{
name: "jsonPatch add map entry by key and value",
expression: `[
JSONPatch{op: "add", path: "/metadata/labels/x", value: "2"},
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "1"}}, Spec: appsv1.DeploymentSpec{}},
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "1", "x": "2"}}, Spec: appsv1.DeploymentSpec{}},
},
{
name: "jsonPatch add map value to field",
expression: `[
JSONPatch{op: "add", path: "/metadata/labels", value: {"y": "2"}},
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{}},
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "2"}}, Spec: appsv1.DeploymentSpec{}},
},
{
name: "jsonPatch add map to existing map", // performs a replacement
expression: `[
JSONPatch{op: "add", path: "/metadata/labels", value: {"y": "2"}},
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1"}}, Spec: appsv1.DeploymentSpec{}},
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "2"}}, Spec: appsv1.DeploymentSpec{}},
},
{
name: "jsonPatch add to start of list",
expression: `[
JSONPatch{op: "add", path: "/spec/template/spec/containers/0", value: {"name": "x"}},
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "a"}},
}}}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "x"}, {Name: "a"}},
}}}},
},
{
name: "jsonPatch add to end of list",
expression: `[
JSONPatch{op: "add", path: "/spec/template/spec/containers/-", value: {"name": "x"}},
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "a"}},
}}}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "a"}, {Name: "x"}},
}}}},
},
{
name: "jsonPatch replace key in map",
expression: `[
JSONPatch{op: "replace", path: "/metadata/labels/x", value: "2"},
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "1"}}, Spec: appsv1.DeploymentSpec{}},
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "1", "x": "2"}}, Spec: appsv1.DeploymentSpec{}},
},
{
name: "jsonPatch replace map value of unset field", // adds the field value
expression: `[
JSONPatch{op: "replace", path: "/metadata/labels", value: {"y": "2"}},
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{}},
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "2"}}, Spec: appsv1.DeploymentSpec{}},
},
{
name: "jsonPatch replace map value of set field",
expression: `[
JSONPatch{op: "replace", path: "/metadata/labels", value: {"y": "2"}},
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1"}}, Spec: appsv1.DeploymentSpec{}},
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "2"}}, Spec: appsv1.DeploymentSpec{}},
},
{
name: "jsonPatch replace first element in list",
expression: `[
JSONPatch{op: "replace", path: "/spec/template/spec/containers/0", value: {"name": "x"}},
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "a"}},
}}}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "x"}},
}}}},
},
{
name: "jsonPatch add map entry by key and value",
expression: `[
JSONPatch{op: "add", path: "/spec", value: Object.spec{selector: Object.spec.selector{}, replicas: 10}}
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Selector: &metav1.LabelSelector{}, Replicas: ptr.To[int32](10)}},
},
{
name: "JSONPatch patch type has field access",
expression: `[
JSONPatch{
op: "add", path: "/metadata/labels",
value: {
"op": JSONPatch{op: "opValue"}.op,
"path": JSONPatch{path: "pathValue"}.path,
"from": JSONPatch{from: "fromValue"}.from,
"value": string(JSONPatch{value: "valueValue"}.value),
}
}
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}},
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
"op": "opValue",
"path": "pathValue",
"from": "fromValue",
"value": "valueValue",
}}},
},
{
name: "JSONPatch patch type has field testing",
expression: `[
JSONPatch{
op: "add", path: "/metadata/labels",
value: {
"op": string(has(JSONPatch{op: "opValue"}.op)),
"path": string(has(JSONPatch{path: "pathValue"}.path)),
"from": string(has(JSONPatch{from: "fromValue"}.from)),
"value": string(has(JSONPatch{value: "valueValue"}.value)),
"op-unset": string(has(JSONPatch{}.op)),
"path-unset": string(has(JSONPatch{}.path)),
"from-unset": string(has(JSONPatch{}.from)),
"value-unset": string(has(JSONPatch{}.value)),
}
}
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}},
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
"op": "true",
"path": "true",
"from": "true",
"value": "true",
"op-unset": "false",
"path-unset": "false",
"from-unset": "false",
"value-unset": "false",
}}},
},
{
name: "JSONPatch patch type equality",
expression: `[
JSONPatch{
op: "add", path: "/metadata/labels",
value: {
"empty": string(JSONPatch{} == JSONPatch{}),
"partial": string(JSONPatch{op: "add"} == JSONPatch{op: "add"}),
"same-all": string(JSONPatch{op: "add", path: "path", from: "from", value: 1} == JSONPatch{op: "add", path: "path", from: "from", value: 1}),
"different-op": string(JSONPatch{op: "add"} == JSONPatch{op: "remove"}),
"different-path": string(JSONPatch{op: "add", path: "x", from: "from", value: 1} == JSONPatch{op: "add", path: "path", from: "from", value: 1}),
"different-from": string(JSONPatch{op: "add", path: "path", from: "x", value: 1} == JSONPatch{op: "add", path: "path", from: "from", value: 1}),
"different-value": string(JSONPatch{op: "add", path: "path", from: "from", value: "1"} == JSONPatch{op: "add", path: "path", from: "from", value: 1}),
}
}
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}},
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
"empty": "true",
"partial": "true",
"same-all": "true",
"different-op": "false",
"different-path": "false",
"different-from": "false",
"different-value": "false",
}}},
},
{
name: "JSONPatch key escaping",
expression: `[
JSONPatch{
op: "add", path: "/metadata/labels", value: {}
},
JSONPatch{
op: "add", path: "/metadata/labels/" + jsonpatch.escapeKey("k8s.io/x~y"), value: "true"
}
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}},
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
"k8s.io/x~y": "true",
}}},
},
{
name: "jsonPatch with CEL initializer",
expression: `[
JSONPatch{op: "add", path: "/spec/template/spec/containers/-", value: Object.spec.template.spec.containers{
name: "x",
ports: [Object.spec.template.spec.containers.ports{containerPort: 8080}],
}
},
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "a"}},
}}}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "a"}, {Name: "x", Ports: []corev1.ContainerPort{{ContainerPort: 8080}}}},
}}}},
},
{
name: "jsonPatch invalid CEL initializer field",
expression: `[
JSONPatch{
op: "add", path: "/spec/template/spec/containers/-",
value: Object.spec.template.spec.containers{
name: "x",
ports: [Object.spec.template.spec.containers.ports{containerPortZ: 8080}]
}
}
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "a"}},
}}}},
expectedErr: "strict decoding error: unknown field \"spec.template.spec.containers[1].ports[0].containerPortZ\"",
},
{
name: "jsonPatch invalid CEL initializer type",
expression: `[
JSONPatch{
op: "add", path: "/spec/template/spec/containers/-",
value: Object.spec.template.spec.containers{
name: "x",
ports: [Object.spec.template.spec.containers.portsZ{containerPort: 8080}]
}
}
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "a"}},
}}}},
expectedErr: " mismatch: unexpected type name \"Object.spec.template.spec.containers.portsZ\", expected \"Object.spec.template.spec.containers.ports\", which matches field name path from root Object type",
},
{
name: "jsonPatch replace end of list with - not allowed",
expression: `[
JSONPatch{op: "replace", path: "/spec/template/spec/containers/-", value: {"name": "x"}},
]`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "a"}},
}}}},
expectedErr: "JSON Patch: replace operation does not apply: doc is missing key: /spec/template/spec/containers/-: missing value",
},
}
compiler, err := cel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
if err != nil {
t.Fatal(err)
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
accessor := &JSONPatchCondition{Expression: tc.expression}
compileResult := compiler.CompileMutatingEvaluator(accessor, cel.OptionalVariableDeclarations{StrictCost: true, HasPatchTypes: true}, environment.StoredExpressions)
patcher := jsonPatcher{PatchEvaluator: compileResult}
scheme := runtime.NewScheme()
err := appsv1.AddToScheme(scheme)
if err != nil {
t.Fatal(err)
}
var gvk schema.GroupVersionKind
gvks, _, err := scheme.ObjectKinds(tc.object)
if err != nil {
t.Fatal(err)
}
if len(gvks) == 1 {
gvk = gvks[0]
} else {
t.Fatalf("Failed to find gvk for type: %T", tc.object)
}
metaAccessor, err := meta.Accessor(tc.object)
if err != nil {
t.Fatal(err)
}
attrs := admission.NewAttributesRecord(tc.object, tc.oldObject, gvk,
metaAccessor.GetNamespace(), metaAccessor.GetName(), tc.gvr,
"", admission.Create, &metav1.CreateOptions{}, false, nil)
vAttrs := &admission.VersionedAttributes{
Attributes: attrs,
VersionedKind: gvk,
VersionedObject: tc.object,
VersionedOldObject: tc.oldObject,
}
r := Request{
MatchedResource: tc.gvr,
VersionedAttributes: vAttrs,
ObjectInterfaces: admission.NewObjectInterfacesFromScheme(scheme),
OptionalVariables: cel.OptionalVariableBindings{},
}
got, err := patcher.Patch(context.Background(), r, celconfig.RuntimeCELCostBudget)
if len(tc.expectedErr) > 0 {
if err == nil {
t.Fatalf("expected error: %s", tc.expectedErr)
} else {
if !strings.Contains(err.Error(), tc.expectedErr) {
t.Fatalf("expected error: %s, got: %s", tc.expectedErr, err.Error())
}
return
}
}
if err != nil && len(tc.expectedErr) == 0 {
t.Fatalf("unexpected error: %v", err)
}
if !equality.Semantic.DeepEqual(tc.expectedResult, got) {
t.Errorf("unexpected result, got diff:\n%s\n", cmp.Diff(tc.expectedResult, got))
}
})
}
}

View File

@@ -0,0 +1,217 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package patch
import (
"context"
"errors"
"fmt"
celgo "github.com/google/cel-go/cel"
celtypes "github.com/google/cel-go/common/types"
"strings"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/schema"
"sigs.k8s.io/structured-merge-diff/v4/typed"
"sigs.k8s.io/structured-merge-diff/v4/value"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/managedfields"
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/cel/mutation/dynamic"
)
// ApplyConfigurationCondition contains the inputs needed to compile and evaluate a cel expression
// that returns an apply configuration
type ApplyConfigurationCondition struct {
Expression string
}
var _ plugincel.ExpressionAccessor = &ApplyConfigurationCondition{}
func (v *ApplyConfigurationCondition) GetExpression() string {
return v.Expression
}
func (v *ApplyConfigurationCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{applyConfigObjectType}
}
var applyConfigObjectType = celtypes.NewObjectType("Object")
// NewApplyConfigurationPatcher creates a patcher that performs an applyConfiguration mutation.
func NewApplyConfigurationPatcher(expressionEvaluator plugincel.MutatingEvaluator) Patcher {
return &applyConfigPatcher{expressionEvaluator: expressionEvaluator}
}
type applyConfigPatcher struct {
expressionEvaluator plugincel.MutatingEvaluator
}
func (e *applyConfigPatcher) Patch(ctx context.Context, r Request, runtimeCELCostBudget int64) (runtime.Object, error) {
admissionRequest := plugincel.CreateAdmissionRequest(
r.VersionedAttributes.Attributes,
metav1.GroupVersionResource(r.MatchedResource),
metav1.GroupVersionKind(r.VersionedAttributes.VersionedKind))
compileErrors := e.expressionEvaluator.CompilationErrors()
if len(compileErrors) > 0 {
return nil, errors.Join(compileErrors...)
}
eval, _, err := e.expressionEvaluator.ForInput(ctx, r.VersionedAttributes, admissionRequest, r.OptionalVariables, r.Namespace, runtimeCELCostBudget)
if err != nil {
return nil, err
}
if eval.Error != nil {
return nil, eval.Error
}
v := eval.EvalResult
// The compiler ensures that the return type is an ObjectVal with type name of "Object".
objVal, ok := v.(*dynamic.ObjectVal)
if !ok {
// Should not happen since the compiler type checks the return type.
return nil, fmt.Errorf("unsupported return type from ApplyConfiguration expression: %v", v.Type())
}
// TODO: Object initializers are insufficiently type checked.
// In the interim, we use this sanity check to detect type mismatches
// between field names and Object initializers. For example,
// "Object.spec{ selector: Object.spec.wrong{}}" is detected as a mismatch.
// Before beta, attaching full type information both to Object initializers and
// the "object" and "oldObject" variables is needed. This will allow CEL to
// perform comprehensive runtime type checking.
err = objVal.CheckTypeNamesMatchFieldPathNames()
if err != nil {
return nil, fmt.Errorf("type mismatch: %w", err)
}
value, ok := objVal.Value().(map[string]any)
if !ok {
return nil, fmt.Errorf("invalid return type: %T", v)
}
patchObject := unstructured.Unstructured{Object: value}
patchObject.SetGroupVersionKind(r.VersionedAttributes.VersionedObject.GetObjectKind().GroupVersionKind())
patched, err := ApplyStructuredMergeDiff(r.TypeConverter, r.VersionedAttributes.VersionedObject, &patchObject)
if err != nil {
return nil, fmt.Errorf("error applying patch: %w", err)
}
return patched, nil
}
// ApplyStructuredMergeDiff applies a structured merge diff to an object and returns a copy of the object
// with the patch applied.
func ApplyStructuredMergeDiff(
typeConverter managedfields.TypeConverter,
originalObject runtime.Object,
patch *unstructured.Unstructured,
) (runtime.Object, error) {
if patch.GroupVersionKind() != originalObject.GetObjectKind().GroupVersionKind() {
return nil, fmt.Errorf("patch (%v) and original object (%v) are not of the same gvk", patch.GroupVersionKind().String(), originalObject.GetObjectKind().GroupVersionKind().String())
} else if typeConverter == nil {
return nil, fmt.Errorf("type converter must not be nil")
}
patchObjTyped, err := typeConverter.ObjectToTyped(patch)
if err != nil {
return nil, fmt.Errorf("failed to convert patch object to typed object: %w", err)
}
err = validatePatch(patchObjTyped)
if err != nil {
return nil, fmt.Errorf("invalid ApplyConfiguration: %w", err)
}
liveObjTyped, err := typeConverter.ObjectToTyped(originalObject)
if err != nil {
return nil, fmt.Errorf("failed to convert original object to typed object: %w", err)
}
newObjTyped, err := liveObjTyped.Merge(patchObjTyped)
if err != nil {
return nil, fmt.Errorf("failed to merge patch: %w", err)
}
// Our mutating admission policy sets the fields but does not track ownership.
// Newly introduced fields in the patch won't be tracked by a field manager
// (so if the original object is updated again but the mutating policy is
// not active, the fields will be dropped).
//
// This necessarily means that changes to an object by a mutating policy
// are only preserved if the policy was active at the time of the change.
// (If the policy is not active, the changes may be dropped.)
newObj, err := typeConverter.TypedToObject(newObjTyped)
if err != nil {
return nil, fmt.Errorf("failed to convert typed object to object: %w", err)
}
return newObj, nil
}
// validatePatch searches an apply configuration for any arrays, maps or structs elements that are atomic and returns
// an error if any are found.
// This prevents accidental removal of fields that can occur when the user intends to modify some
// fields in an atomic type, not realizing that all fields not explicitly set in the new value
// of the atomic will be removed.
func validatePatch(v *typed.TypedValue) error {
atomics := findAtomics(nil, v.Schema(), v.TypeRef(), v.AsValue())
if len(atomics) > 0 {
return fmt.Errorf("may not mutate atomic arrays, maps or structs: %v", strings.Join(atomics, ", "))
}
return nil
}
// findAtomics returns field paths for any atomic arrays, maps or structs found when traversing the given value.
func findAtomics(path []fieldpath.PathElement, s *schema.Schema, tr schema.TypeRef, v value.Value) (atomics []string) {
if a, ok := s.Resolve(tr); ok { // Validation pass happens before this and checks that all schemas can be resolved
if v.IsMap() && a.Map != nil {
if a.Map.ElementRelationship == schema.Atomic {
atomics = append(atomics, pathString(path))
}
v.AsMap().Iterate(func(key string, val value.Value) bool {
pe := fieldpath.PathElement{FieldName: &key}
if sf, ok := a.Map.FindField(key); ok {
tr = sf.Type
atomics = append(atomics, findAtomics(append(path, pe), s, tr, val)...)
}
return true
})
}
if v.IsList() && a.List != nil {
if a.List.ElementRelationship == schema.Atomic {
atomics = append(atomics, pathString(path))
}
list := v.AsList()
for i := 0; i < list.Length(); i++ {
pe := fieldpath.PathElement{Index: &i}
atomics = append(atomics, findAtomics(append(path, pe), s, a.List.ElementType, list.At(i))...)
}
}
}
return atomics
}
func pathString(path []fieldpath.PathElement) string {
sb := strings.Builder{}
for _, p := range path {
sb.WriteString(p.String())
}
return sb.String()
}

View File

@@ -0,0 +1,375 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package patch
import (
"context"
"github.com/google/go-cmp/cmp"
"strings"
"testing"
"time"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/client-go/openapi/openapitest"
"k8s.io/utils/ptr"
)
func TestApplyConfiguration(t *testing.T) {
deploymentGVR := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
deploymentGVK := schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}
tests := []struct {
name string
expression string
gvr schema.GroupVersionResource
object, oldObject runtime.Object
expectedResult runtime.Object
expectedErr string
}{
{
name: "apply configuration add to listType=map",
expression: `Object{
spec: Object.spec{
template: Object.spec.template{
spec: Object.spec.template.spec{
volumes: [Object.spec.template.spec.volumes{
name: "y"
}]
}
}
}
}`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{{Name: "x"}},
},
},
}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{{Name: "x"}, {Name: "y"}},
},
},
}},
},
{
name: "apply configuration add to listType=map",
expression: `Object{
spec: Object.spec{
template: Object.spec.template{
spec: Object.spec.template.spec{
volumes: [Object.spec.template.spec.volumes{
name: "y"
}]
}
}
}
}`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{{Name: "x"}},
},
},
}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{{Name: "x"}, {Name: "y"}},
},
},
}},
},
{
name: "apply configuration update listType=map entry",
expression: `Object{
spec: Object.spec{
template: Object.spec.template{
spec: Object.spec.template.spec{
volumes: [Object.spec.template.spec.volumes{
name: "y",
hostPath: Object.spec.template.spec.volumes.hostPath{
path: "a"
}
}]
}
}
}
}`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{{Name: "x"}, {Name: "y"}},
},
},
}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{{Name: "x"}, {Name: "y", VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "a"}}}},
},
},
}},
},
{
name: "apply configuration with conditionals",
expression: `Object{
spec: Object.spec{
replicas: object.spec.replicas % 2 == 0?object.spec.replicas + 1:object.spec.replicas
}
}`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](2)}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](3)}},
},
{
name: "apply configuration with old object",
expression: `Object{
spec: Object.spec{
replicas: oldObject.spec.replicas % 2 == 0?oldObject.spec.replicas + 1:oldObject.spec.replicas
}
}`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
oldObject: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](2)}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](3)}},
},
{
name: "complex apply configuration initialization",
expression: `Object{
spec: Object.spec{
replicas: 1,
template: Object.spec.template{
metadata: Object.spec.template.metadata{
labels: {"app": "nginx"}
},
spec: Object.spec.template.spec{
containers: [Object.spec.template.spec.containers{
name: "nginx",
image: "nginx:1.14.2",
ports: [Object.spec.template.spec.containers.ports{
containerPort: 80
}],
resources: Object.spec.template.spec.containers.resources{
limits: {"cpu": "128M"},
}
}]
}
}
}
}`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{}},
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{
Replicas: ptr.To[int32](1),
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app": "nginx"},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "nginx",
Image: "nginx:1.14.2",
Ports: []corev1.ContainerPort{
{ContainerPort: 80},
},
Resources: corev1.ResourceRequirements{
Limits: corev1.ResourceList{corev1.ResourceName("cpu"): resource.MustParse("128M")},
},
}},
},
},
}},
},
{
name: "apply configuration with change to atomic",
expression: `Object{
spec: Object.spec{
selector: Object.spec.selector{
matchLabels: {"l": "v"}
}
}
}`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
expectedErr: "error applying patch: invalid ApplyConfiguration: may not mutate atomic arrays, maps or structs: .spec.selector",
},
{
name: "apply configuration with invalid type name",
expression: `Object{
spec: Object.specx{
replicas: 1
}
}`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
expectedErr: "type mismatch: unexpected type name \"Object.specx\", expected \"Object.spec\", which matches field name path from root Object type",
},
{
name: "apply configuration with invalid field name",
expression: `Object{
spec: Object.spec{
replicasx: 1
}
}`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
expectedErr: "error applying patch: failed to convert patch object to typed object: .spec.replicasx: field not declared in schema",
},
{
name: "apply configuration with invalid return type",
expression: `"I'm a teapot!"`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
expectedErr: "must evaluate to Object but got string",
},
{
name: "apply configuration with invalid initializer return type",
expression: `Object.spec.metadata{}`,
gvr: deploymentGVR,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
expectedErr: "must evaluate to Object but got Object.spec.metadata",
},
}
compiler, err := cel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tcManager := NewTypeConverterManager(nil, openapitest.NewEmbeddedFileClient())
go tcManager.Run(ctx)
err = wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, time.Second, true, func(context.Context) (done bool, err error) {
converter := tcManager.GetTypeConverter(deploymentGVK)
return converter != nil, nil
})
if err != nil {
t.Fatal(err)
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
accessor := &ApplyConfigurationCondition{Expression: tc.expression}
compileResult := compiler.CompileMutatingEvaluator(accessor, cel.OptionalVariableDeclarations{StrictCost: true, HasPatchTypes: true}, environment.StoredExpressions)
patcher := applyConfigPatcher{expressionEvaluator: compileResult}
scheme := runtime.NewScheme()
err := appsv1.AddToScheme(scheme)
if err != nil {
t.Fatal(err)
}
var gvk schema.GroupVersionKind
gvks, _, err := scheme.ObjectKinds(tc.object)
if err != nil {
t.Fatal(err)
}
if len(gvks) == 1 {
gvk = gvks[0]
} else {
t.Fatalf("Failed to find gvk for type: %T", tc.object)
}
metaAccessor, err := meta.Accessor(tc.object)
if err != nil {
t.Fatal(err)
}
typeAccessor, err := meta.TypeAccessor(tc.object)
if err != nil {
t.Fatal(err)
}
typeAccessor.SetKind(gvk.Kind)
typeAccessor.SetAPIVersion(gvk.GroupVersion().String())
attrs := admission.NewAttributesRecord(tc.object, tc.oldObject, gvk,
metaAccessor.GetNamespace(), metaAccessor.GetName(), tc.gvr,
"", admission.Create, &metav1.CreateOptions{}, false, nil)
vAttrs := &admission.VersionedAttributes{
Attributes: attrs,
VersionedKind: gvk,
VersionedObject: tc.object,
VersionedOldObject: tc.oldObject,
}
r := Request{
MatchedResource: tc.gvr,
VersionedAttributes: vAttrs,
ObjectInterfaces: admission.NewObjectInterfacesFromScheme(scheme),
OptionalVariables: cel.OptionalVariableBindings{},
TypeConverter: tcManager.GetTypeConverter(gvk),
}
patched, err := patcher.Patch(ctx, r, celconfig.RuntimeCELCostBudget)
if len(tc.expectedErr) > 0 {
if err == nil {
t.Fatalf("expected error: %s", tc.expectedErr)
} else {
if !strings.Contains(err.Error(), tc.expectedErr) {
t.Fatalf("expected error: %s, got: %s", tc.expectedErr, err.Error())
}
return
}
}
if err != nil && len(tc.expectedErr) == 0 {
t.Fatalf("unexpected error: %v", err)
}
got, err := runtime.DefaultUnstructuredConverter.ToUnstructured(patched)
if err != nil {
t.Fatal(err)
}
wantTypeAccessor, err := meta.TypeAccessor(tc.expectedResult)
if err != nil {
t.Fatal(err)
}
wantTypeAccessor.SetKind(gvk.Kind)
wantTypeAccessor.SetAPIVersion(gvk.GroupVersion().String())
want, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.expectedResult)
if err != nil {
t.Fatal(err)
}
if !equality.Semantic.DeepEqual(want, got) {
t.Errorf("unexpected result, got diff:\n%s\n", cmp.Diff(want, got))
}
})
}
}

View File

@@ -0,0 +1,187 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package patch
import (
"context"
"encoding/json"
"fmt"
"strings"
"sync"
"time"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/managedfields"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/openapi"
"k8s.io/kube-openapi/pkg/spec3"
)
type TypeConverterManager interface {
// GetTypeConverter returns a type converter for the given GVK
GetTypeConverter(gvk schema.GroupVersionKind) managedfields.TypeConverter
Run(ctx context.Context)
}
func NewTypeConverterManager(
staticTypeConverter managedfields.TypeConverter,
openapiClient openapi.Client,
) TypeConverterManager {
return &typeConverterManager{
staticTypeConverter: staticTypeConverter,
openapiClient: openapiClient,
typeConverterMap: make(map[schema.GroupVersion]typeConverterCacheEntry),
lastFetchedPaths: make(map[schema.GroupVersion]openapi.GroupVersion),
}
}
type typeConverterCacheEntry struct {
typeConverter managedfields.TypeConverter
entry openapi.GroupVersion
}
// typeConverterManager helps us make sure we have an up to date schema and
// type converter for our openapi models. It should be connfigured to use a
// static type converter for natively typed schemas, and fetches the schema
// for CRDs/other over the network on demand (trying to reduce network calls where necessary)
type typeConverterManager struct {
// schemaCache is used to cache the schema for a given GVK
staticTypeConverter managedfields.TypeConverter
// discoveryClient is used to fetch the schema for a given GVK
openapiClient openapi.Client
lock sync.RWMutex
typeConverterMap map[schema.GroupVersion]typeConverterCacheEntry
lastFetchedPaths map[schema.GroupVersion]openapi.GroupVersion
}
func (t *typeConverterManager) Run(ctx context.Context) {
// Loop every 5s refershing the OpenAPI schema list to know which
// schemas have been invalidated. This should use e-tags under the hood
_ = wait.PollUntilContextCancel(ctx, 5*time.Second, true, func(_ context.Context) (done bool, err error) {
paths, err := t.openapiClient.Paths()
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to fetch openapi paths: %w", err))
return false, nil
}
// The /openapi/v3 endpoint contains a list of paths whose ServerRelativeURL
// value changes every time the schema is updated. So we poll /openapi/v3
// to get the "version number" for each schema, and invalidate our cache
// if the version number has changed since we pulled it.
parsedPaths := make(map[schema.GroupVersion]openapi.GroupVersion, len(paths))
for path, entry := range paths {
if !strings.HasPrefix(path, "apis/") && !strings.HasPrefix(path, "api/") {
continue
}
path = strings.TrimPrefix(path, "apis/")
path = strings.TrimPrefix(path, "api/")
gv, err := schema.ParseGroupVersion(path)
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to parse group version %q: %w", path, err))
return false, nil
}
parsedPaths[gv] = entry
}
t.lock.Lock()
defer t.lock.Unlock()
t.lastFetchedPaths = parsedPaths
return false, nil
})
}
func (t *typeConverterManager) GetTypeConverter(gvk schema.GroupVersionKind) managedfields.TypeConverter {
// Check to see if the static type converter handles this GVK
if t.staticTypeConverter != nil {
//!TODO: Add ability to check existence to type converter
// working around for now but seeing if getting a typed version of an
// empty object returns error
stub := &unstructured.Unstructured{}
stub.SetGroupVersionKind(gvk)
if _, err := t.staticTypeConverter.ObjectToTyped(stub); err == nil {
return t.staticTypeConverter
}
}
gv := gvk.GroupVersion()
existing, entry, err := func() (managedfields.TypeConverter, openapi.GroupVersion, error) {
t.lock.RLock()
defer t.lock.RUnlock()
// If schema is not supported by static type converter, ask discovery
// for the schema
entry, ok := t.lastFetchedPaths[gv]
if !ok {
// If we can't get the schema, we can't do anything
return nil, nil, fmt.Errorf("no schema for %v", gvk)
}
// If the entry schema has not changed, used the same type converter
if existing, ok := t.typeConverterMap[gv]; ok && existing.entry.ServerRelativeURL() == entry.ServerRelativeURL() {
// If we have a type converter for this GVK, return it
return existing.typeConverter, existing.entry, nil
}
return nil, entry, nil
}()
if err != nil {
utilruntime.HandleError(err)
return nil
} else if existing != nil {
return existing
}
schBytes, err := entry.Schema(runtime.ContentTypeJSON)
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to get schema for %v: %w", gvk, err))
return nil
}
var sch spec3.OpenAPI
if err := json.Unmarshal(schBytes, &sch); err != nil {
utilruntime.HandleError(fmt.Errorf("failed to unmarshal schema for %v: %w", gvk, err))
return nil
}
// The schema has changed, or there is no entry for it, generate
// a new type converter for this GV
tc, err := managedfields.NewTypeConverter(sch.Components.Schemas, false)
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to create type converter for %v: %w", gvk, err))
return nil
}
t.lock.Lock()
defer t.lock.Unlock()
t.typeConverterMap[gv] = typeConverterCacheEntry{
typeConverter: tc,
entry: entry,
}
return tc
}

View File

@@ -0,0 +1,100 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package patch
import (
"context"
"github.com/google/go-cmp/cmp"
"testing"
"time"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/openapi/openapitest"
)
func TestTypeConverter(t *testing.T) {
deploymentGVK := schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}
tests := []struct {
name string
gvk schema.GroupVersionKind
object runtime.Object
}{
{
name: "simple round trip",
gvk: deploymentGVK,
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "a"}, {Name: "x", Ports: []corev1.ContainerPort{{ContainerPort: 8080}}}},
}}}},
},
}
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
tcManager := NewTypeConverterManager(nil, openapitest.NewEmbeddedFileClient())
go tcManager.Run(ctx)
err := wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, time.Second, true, func(context.Context) (done bool, err error) {
converter := tcManager.GetTypeConverter(deploymentGVK)
return converter != nil, nil
})
if err != nil {
t.Fatal(err)
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
typeAccessor, err := meta.TypeAccessor(tc.object)
if err != nil {
t.Fatal(err)
}
typeAccessor.SetKind(tc.gvk.Kind)
typeAccessor.SetAPIVersion(tc.gvk.GroupVersion().String())
converter := tcManager.GetTypeConverter(tc.gvk)
if converter == nil {
t.Errorf("nil TypeConverter")
}
typedObject, err := converter.ObjectToTyped(tc.object)
if err != nil {
t.Fatal(err)
}
roundTripped, err := converter.TypedToObject(typedObject)
if err != nil {
t.Fatal(err)
}
got, err := runtime.DefaultUnstructuredConverter.ToUnstructured(roundTripped)
if err != nil {
t.Fatal(err)
}
want, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.object)
if err != nil {
t.Fatal(err)
}
if !equality.Semantic.DeepEqual(want, got) {
t.Errorf("unexpected result, got diff:\n%s\n", cmp.Diff(want, got))
}
})
}
}

View File

@@ -0,0 +1,151 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutating
import (
"context"
celgo "github.com/google/cel-go/cel"
"io"
"k8s.io/api/admissionregistration/v1alpha1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/managedfields"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/features"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/component-base/featuregate"
)
const (
// PluginName indicates the name of admission plug-in
PluginName = "MutatingAdmissionPolicy"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register(PluginName, func(configFile io.Reader) (admission.Interface, error) {
return NewPlugin(configFile), nil
})
}
type Policy = v1alpha1.MutatingAdmissionPolicy
type PolicyBinding = v1alpha1.MutatingAdmissionPolicyBinding
type PolicyMutation = v1alpha1.Mutation
type PolicyHook = generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]
type Mutator struct {
}
type MutationEvaluationFunc func(
ctx context.Context,
matchedResource schema.GroupVersionResource,
versionedAttr *admission.VersionedAttributes,
o admission.ObjectInterfaces,
versionedParams runtime.Object,
namespace *corev1.Namespace,
typeConverter managedfields.TypeConverter,
runtimeCELCostBudget int64,
authorizer authorizer.Authorizer,
) (runtime.Object, error)
type PolicyEvaluator struct {
Matcher matchconditions.Matcher
Mutators []patch.Patcher
CompositionEnv *cel.CompositionEnv
Error error
}
// Plugin is an implementation of admission.Interface.
type Plugin struct {
*generic.Plugin[PolicyHook]
}
var _ admission.Interface = &Plugin{}
var _ admission.MutationInterface = &Plugin{}
// NewPlugin returns a generic admission webhook plugin.
func NewPlugin(_ io.Reader) *Plugin {
// There is no request body to mutate for DELETE, so this plugin never handles that operation.
handler := admission.NewHandler(admission.Create, admission.Update, admission.Connect)
res := &Plugin{}
res.Plugin = generic.NewPlugin(
handler,
func(f informers.SharedInformerFactory, client kubernetes.Interface, dynamicClient dynamic.Interface, restMapper meta.RESTMapper) generic.Source[PolicyHook] {
return generic.NewPolicySource(
f.Admissionregistration().V1alpha1().MutatingAdmissionPolicies().Informer(),
f.Admissionregistration().V1alpha1().MutatingAdmissionPolicyBindings().Informer(),
NewMutatingAdmissionPolicyAccessor,
NewMutatingAdmissionPolicyBindingAccessor,
compilePolicy,
//!TODO: Create a way to share param informers between
// mutating/validating plugins
f,
dynamicClient,
restMapper,
)
},
func(a authorizer.Authorizer, m *matching.Matcher, client kubernetes.Interface) generic.Dispatcher[PolicyHook] {
return NewDispatcher(a, m, patch.NewTypeConverterManager(nil, client.Discovery().OpenAPIV3()))
},
)
return res
}
// Admit makes an admission decision based on the request attributes.
func (a *Plugin) Admit(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error {
return a.Plugin.Dispatch(ctx, attr, o)
}
func (a *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
a.Plugin.SetEnabled(featureGates.Enabled(features.MutatingAdmissionPolicy))
}
// Variable is a named expression for composition.
type Variable struct {
Name string
Expression string
}
func (v *Variable) GetExpression() string {
return v.Expression
}
func (v *Variable) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.AnyType, celgo.DynType}
}
func (v *Variable) GetName() string {
return v.Name
}
func convertv1alpha1Variables(variables []v1alpha1.Variable) []cel.NamedExpressionAccessor {
namedExpressions := make([]cel.NamedExpressionAccessor, len(variables))
for i, variable := range variables {
namedExpressions[i] = &Variable{Name: variable.Name, Expression: variable.Expression}
}
return namedExpressions
}

View File

@@ -0,0 +1,354 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutating_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"k8s.io/api/admissionregistration/v1alpha1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating"
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/openapi/openapitest"
"k8s.io/utils/ptr"
)
func setupTest(
t *testing.T,
compiler func(*mutating.Policy) mutating.PolicyEvaluator,
) *generic.PolicyTestContext[*mutating.Policy, *mutating.PolicyBinding, mutating.PolicyEvaluator] {
testContext, testCancel, err := generic.NewPolicyTestContext[*mutating.Policy, *mutating.PolicyBinding, mutating.PolicyEvaluator](
mutating.NewMutatingAdmissionPolicyAccessor,
mutating.NewMutatingAdmissionPolicyBindingAccessor,
compiler,
func(a authorizer.Authorizer, m *matching.Matcher, i kubernetes.Interface) generic.Dispatcher[mutating.PolicyHook] {
// Use embedded schemas rather than discovery schemas
return mutating.NewDispatcher(a, m, patch.NewTypeConverterManager(nil, openapitest.NewEmbeddedFileClient()))
},
nil,
[]meta.RESTMapping{
{
Resource: schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "pods",
},
GroupVersionKind: schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "Pod",
},
Scope: meta.RESTScopeNamespace,
},
})
require.NoError(t, err)
t.Cleanup(testCancel)
require.NoError(t, testContext.Start())
return testContext
}
// Show that a compiler that always sets an annotation on the object works
func TestBasicPatch(t *testing.T) {
expectedAnnotations := map[string]string{"foo": "bar"}
// Treat all policies as setting foo annotation to bar
testContext := setupTest(t, func(p *mutating.Policy) mutating.PolicyEvaluator {
return mutating.PolicyEvaluator{Mutators: []patch.Patcher{annotationPatcher{expectedAnnotations}}}
})
// Set up a policy and binding that match, no params
require.NoError(t, testContext.UpdateAndWait(
&mutating.Policy{
ObjectMeta: metav1.ObjectMeta{Name: "policy"},
Spec: v1alpha1.MutatingAdmissionPolicySpec{
MatchConstraints: &v1alpha1.MatchResources{
MatchPolicy: ptr.To(v1alpha1.Equivalent),
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
},
Mutations: []v1alpha1.Mutation{
{
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
Expression: "ignored, but required",
},
PatchType: v1alpha1.PatchTypeApplyConfiguration,
},
},
},
},
&mutating.PolicyBinding{
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
PolicyName: "policy",
},
},
))
// Show that if we run an object through the policy, it gets the annotation
testObject := &corev1.ConfigMap{}
err := testContext.Dispatch(testObject, nil, admission.Create)
require.NoError(t, err)
require.Equal(t, expectedAnnotations, testObject.Annotations)
}
func TestJSONPatch(t *testing.T) {
patchObj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"foo": "bar",
},
},
"data": map[string]interface{}{
"myfield": "myvalue",
},
},
}
testContext := setupTest(t, func(p *mutating.Policy) mutating.PolicyEvaluator {
return mutating.PolicyEvaluator{
Mutators: []patch.Patcher{smdPatcher{patch: patchObj}},
}
})
// Set up a policy and binding that match, no params
require.NoError(t, testContext.UpdateAndWait(
&mutating.Policy{
ObjectMeta: metav1.ObjectMeta{Name: "policy"},
Spec: v1alpha1.MutatingAdmissionPolicySpec{
MatchConstraints: &v1alpha1.MatchResources{
MatchPolicy: ptr.To(v1alpha1.Equivalent),
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
},
Mutations: []v1alpha1.Mutation{
{
JSONPatch: &v1alpha1.JSONPatch{
Expression: "ignored, but required",
},
PatchType: v1alpha1.PatchTypeApplyConfiguration,
},
},
},
},
&mutating.PolicyBinding{
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
PolicyName: "policy",
},
},
))
// Show that if we run an object through the policy, it gets the annotation
testObject := &corev1.ConfigMap{}
err := testContext.Dispatch(testObject, nil, admission.Create)
require.NoError(t, err)
require.Equal(t, &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{"foo": "bar"},
},
Data: map[string]string{"myfield": "myvalue"},
}, testObject)
}
func TestSSAPatch(t *testing.T) {
patchObj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"foo": "bar",
},
},
"data": map[string]interface{}{
"myfield": "myvalue",
},
},
}
testContext := setupTest(t, func(p *mutating.Policy) mutating.PolicyEvaluator {
return mutating.PolicyEvaluator{
Mutators: []patch.Patcher{smdPatcher{patch: patchObj}},
}
})
// Set up a policy and binding that match, no params
require.NoError(t, testContext.UpdateAndWait(
&mutating.Policy{
ObjectMeta: metav1.ObjectMeta{Name: "policy"},
Spec: v1alpha1.MutatingAdmissionPolicySpec{
MatchConstraints: &v1alpha1.MatchResources{
MatchPolicy: ptr.To(v1alpha1.Equivalent),
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
},
Mutations: []v1alpha1.Mutation{
{
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
Expression: "ignored, but required",
},
PatchType: v1alpha1.PatchTypeApplyConfiguration,
},
},
},
},
&mutating.PolicyBinding{
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
PolicyName: "policy",
},
},
))
// Show that if we run an object through the policy, it gets the annotation
testObject := &corev1.ConfigMap{}
err := testContext.Dispatch(testObject, nil, admission.Create)
require.NoError(t, err)
require.Equal(t, &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{"foo": "bar"},
},
Data: map[string]string{"myfield": "myvalue"},
}, testObject)
}
func TestSSAMapList(t *testing.T) {
patchObj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"foo": "bar",
},
},
"spec": map[string]interface{}{
"initContainers": []interface{}{
map[string]interface{}{
"name": "injected-init-container",
"image": "injected-image",
},
},
},
},
}
testContext := setupTest(t, func(p *mutating.Policy) mutating.PolicyEvaluator {
return mutating.PolicyEvaluator{
Mutators: []patch.Patcher{smdPatcher{patch: patchObj}},
}
})
// Set up a policy and binding that match, no params
require.NoError(t, testContext.UpdateAndWait(
&mutating.Policy{
ObjectMeta: metav1.ObjectMeta{Name: "policy"},
Spec: v1alpha1.MutatingAdmissionPolicySpec{
MatchConstraints: &v1alpha1.MatchResources{
MatchPolicy: ptr.To(v1alpha1.Equivalent),
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
},
Mutations: []v1alpha1.Mutation{
{
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
Expression: "ignored, but required",
},
PatchType: v1alpha1.PatchTypeApplyConfiguration,
},
},
},
},
&mutating.PolicyBinding{
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
PolicyName: "policy",
},
},
))
// Show that if we run an object through the policy, it gets the annotation
testObject := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{},
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{
{
Name: "init-container",
Image: "image",
},
},
},
}
err := testContext.Dispatch(testObject, nil, admission.Create)
require.NoError(t, err)
require.Equal(t, &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{"foo": "bar"},
},
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{
{
Name: "init-container",
Image: "image",
},
{
Name: "injected-init-container",
Image: "injected-image",
},
},
},
}, testObject)
}
type annotationPatcher struct {
annotations map[string]string
}
func (ap annotationPatcher) Patch(ctx context.Context, request patch.Request, runtimeCELCostBudget int64) (runtime.Object, error) {
obj := request.VersionedAttributes.VersionedObject.DeepCopyObject()
accessor, err := meta.Accessor(obj)
if err != nil {
return nil, err
}
accessor.SetAnnotations(ap.annotations)
return obj, nil
}
type smdPatcher struct {
patch *unstructured.Unstructured
}
func (sp smdPatcher) Patch(ctx context.Context, request patch.Request, runtimeCELCostBudget int64) (runtime.Object, error) {
return patch.ApplyStructuredMergeDiff(request.TypeConverter, request.VersionedAttributes.VersionedObject, sp.patch)
}

View File

@@ -0,0 +1,76 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutating
import (
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
)
type key struct {
PolicyUID types.NamespacedName
BindingUID types.NamespacedName
ParamUID types.NamespacedName
MutationIndex int
}
type policyReinvokeContext struct {
// lastPolicyOutput holds the result of the last Policy admission plugin call
lastPolicyOutput runtime.Object
// previouslyInvokedReinvocablePolicys holds the set of policies that have been invoked and
// should be reinvoked if a later mutation occurs
previouslyInvokedReinvocablePolicies sets.Set[key]
// reinvokePolicies holds the set of Policies that should be reinvoked
reinvokePolicies sets.Set[key]
}
func (rc *policyReinvokeContext) ShouldReinvoke(policy key) bool {
return rc.reinvokePolicies.Has(policy)
}
func (rc *policyReinvokeContext) IsOutputChangedSinceLastPolicyInvocation(object runtime.Object) bool {
return !apiequality.Semantic.DeepEqual(rc.lastPolicyOutput, object)
}
func (rc *policyReinvokeContext) SetLastPolicyInvocationOutput(object runtime.Object) {
if object == nil {
rc.lastPolicyOutput = nil
return
}
rc.lastPolicyOutput = object.DeepCopyObject()
}
func (rc *policyReinvokeContext) AddReinvocablePolicyToPreviouslyInvoked(policy key) {
if rc.previouslyInvokedReinvocablePolicies == nil {
rc.previouslyInvokedReinvocablePolicies = sets.New[key]()
}
rc.previouslyInvokedReinvocablePolicies.Insert(policy)
}
func (rc *policyReinvokeContext) RequireReinvokingPreviouslyInvokedPlugins() {
if len(rc.previouslyInvokedReinvocablePolicies) > 0 {
if rc.reinvokePolicies == nil {
rc.reinvokePolicies = sets.New[key]()
}
for s := range rc.previouslyInvokedReinvocablePolicies {
rc.reinvokePolicies.Insert(s)
}
rc.previouslyInvokedReinvocablePolicies = sets.New[key]()
}
}

View File

@@ -0,0 +1,147 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutating
import (
"github.com/stretchr/testify/assert"
"testing"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
)
func TestFullReinvocation(t *testing.T) {
key1 := key{PolicyUID: types.NamespacedName{Name: "p1"}, BindingUID: types.NamespacedName{Name: "b1"}}
key2 := key{PolicyUID: types.NamespacedName{Name: "p2"}, BindingUID: types.NamespacedName{Name: "b2"}}
key3 := key{PolicyUID: types.NamespacedName{Name: "p3"}, BindingUID: types.NamespacedName{Name: "b3"}}
cm1v1 := &v1.ConfigMap{Data: map[string]string{"v": "1"}}
cm1v2 := &v1.ConfigMap{Data: map[string]string{"v": "2"}}
rc := policyReinvokeContext{}
// key1 is invoked and it updates the configmap
rc.SetLastPolicyInvocationOutput(cm1v1)
rc.RequireReinvokingPreviouslyInvokedPlugins()
rc.AddReinvocablePolicyToPreviouslyInvoked(key1)
assert.True(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v2))
// key2 is invoked and it updates the configmap
rc.SetLastPolicyInvocationOutput(cm1v2)
rc.RequireReinvokingPreviouslyInvokedPlugins()
rc.AddReinvocablePolicyToPreviouslyInvoked(key2)
assert.True(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v1))
// key3 is invoked but it does not change anything
rc.AddReinvocablePolicyToPreviouslyInvoked(key3)
assert.False(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v2))
// key1 is reinvoked
assert.True(t, rc.ShouldReinvoke(key1))
rc.AddReinvocablePolicyToPreviouslyInvoked(key1)
rc.SetLastPolicyInvocationOutput(cm1v1)
assert.True(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v2))
rc.RequireReinvokingPreviouslyInvokedPlugins()
// key2 is reinvoked
assert.True(t, rc.ShouldReinvoke(key2))
rc.AddReinvocablePolicyToPreviouslyInvoked(key2)
rc.SetLastPolicyInvocationOutput(cm1v2)
assert.True(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v1))
rc.RequireReinvokingPreviouslyInvokedPlugins()
// key3 is reinvoked, because the reinvocations have changed the resource
assert.True(t, rc.ShouldReinvoke(key3))
}
func TestPartialReinvocation(t *testing.T) {
key1 := key{PolicyUID: types.NamespacedName{Name: "p1"}, BindingUID: types.NamespacedName{Name: "b1"}}
key2 := key{PolicyUID: types.NamespacedName{Name: "p2"}, BindingUID: types.NamespacedName{Name: "b2"}}
key3 := key{PolicyUID: types.NamespacedName{Name: "p3"}, BindingUID: types.NamespacedName{Name: "b3"}}
cm1v1 := &v1.ConfigMap{Data: map[string]string{"v": "1"}}
cm1v2 := &v1.ConfigMap{Data: map[string]string{"v": "2"}}
rc := policyReinvokeContext{}
// key1 is invoked and it updates the configmap
rc.SetLastPolicyInvocationOutput(cm1v1)
rc.RequireReinvokingPreviouslyInvokedPlugins()
rc.AddReinvocablePolicyToPreviouslyInvoked(key1)
assert.True(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v2))
// key2 is invoked and it updates the configmap
rc.SetLastPolicyInvocationOutput(cm1v2)
rc.RequireReinvokingPreviouslyInvokedPlugins()
rc.AddReinvocablePolicyToPreviouslyInvoked(key2)
assert.True(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v1))
// key3 is invoked but it does not change anything
rc.AddReinvocablePolicyToPreviouslyInvoked(key3)
assert.False(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v2))
// key1 is reinvoked but does not change anything
assert.True(t, rc.ShouldReinvoke(key1))
// key2 is not reinvoked because nothing changed since last invocation
assert.False(t, rc.ShouldReinvoke(key2))
// key3 is not reinvoked because nothing changed since last invocation
assert.False(t, rc.ShouldReinvoke(key3))
}
func TestNoReinvocation(t *testing.T) {
key1 := key{PolicyUID: types.NamespacedName{Name: "p1"}, BindingUID: types.NamespacedName{Name: "b1"}}
key2 := key{PolicyUID: types.NamespacedName{Name: "p2"}, BindingUID: types.NamespacedName{Name: "b2"}}
key3 := key{PolicyUID: types.NamespacedName{Name: "p3"}, BindingUID: types.NamespacedName{Name: "b3"}}
cm1v1 := &v1.ConfigMap{Data: map[string]string{"v": "1"}}
rc := policyReinvokeContext{}
// key1 is invoked and it updates the configmap
rc.AddReinvocablePolicyToPreviouslyInvoked(key1)
rc.SetLastPolicyInvocationOutput(cm1v1)
assert.False(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v1))
// key2 is invoked but does not change anything
rc.AddReinvocablePolicyToPreviouslyInvoked(key2)
rc.SetLastPolicyInvocationOutput(cm1v1)
assert.False(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v1))
// key3 is invoked but it does not change anything
rc.AddReinvocablePolicyToPreviouslyInvoked(key3)
rc.SetLastPolicyInvocationOutput(cm1v1)
assert.False(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v1))
// no keys are reinvoked
assert.False(t, rc.ShouldReinvoke(key1))
assert.False(t, rc.ShouldReinvoke(key2))
assert.False(t, rc.ShouldReinvoke(key3))
}

View File

@@ -54,6 +54,10 @@ func (v *validatingAdmissionPolicyAccessor) GetMatchConstraints() *v1.MatchResou
return v.Spec.MatchConstraints
}
func (v *validatingAdmissionPolicyAccessor) GetFailurePolicy() *v1.FailurePolicyType {
return v.Spec.FailurePolicy
}
type validatingAdmissionPolicyBindingAccessor struct {
*v1.ValidatingAdmissionPolicyBinding
}

View File

@@ -45,6 +45,7 @@ import (
auditinternal "k8s.io/apiserver/pkg/apis/audit"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/warning"
"k8s.io/client-go/kubernetes"
)
var (
@@ -364,7 +365,7 @@ func setupTestCommon(
func(p *validating.Policy) validating.Validator {
return compiler.CompilePolicy(p)
},
func(a authorizer.Authorizer, m *matching.Matcher) generic.Dispatcher[validating.PolicyHook] {
func(a authorizer.Authorizer, m *matching.Matcher, client kubernetes.Interface) generic.Dispatcher[validating.PolicyHook] {
coolMatcher := matcher
if coolMatcher == nil {
coolMatcher = generic.NewPolicyMatcher(m)

View File

@@ -30,6 +30,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
utiljson "k8s.io/apimachinery/pkg/util/json"
"k8s.io/apiserver/pkg/admission"
admissionauthorizer "k8s.io/apiserver/pkg/admission/plugin/authorizer"
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
celmetrics "k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics"
celconfig "k8s.io/apiserver/pkg/apis/cel"
@@ -63,6 +64,10 @@ type policyDecisionWithMetadata struct {
Binding *admissionregistrationv1.ValidatingAdmissionPolicyBinding
}
func (c *dispatcher) Start(ctx context.Context) error {
return nil
}
// Dispatch implements generic.Dispatcher.
func (c *dispatcher) Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []PolicyHook) error {
@@ -109,7 +114,7 @@ func (c *dispatcher) Dispatch(ctx context.Context, a admission.Attributes, o adm
}
}
authz := newCachingAuthorizer(c.authz)
authz := admissionauthorizer.NewCachingAuthorizer(c.authz)
for _, hook := range hooks {
// versionedAttributes will be set to non-nil inside of the loop, but

View File

@@ -112,7 +112,7 @@ func NewPlugin(_ io.Reader) *Plugin {
restMapper,
)
},
func(a authorizer.Authorizer, m *matching.Matcher) generic.Dispatcher[PolicyHook] {
func(a authorizer.Authorizer, m *matching.Matcher, client kubernetes.Interface) generic.Dispatcher[PolicyHook] {
return NewDispatcher(a, generic.NewPolicyMatcher(m))
},
),
@@ -151,13 +151,13 @@ func compilePolicy(policy *Policy) Validator {
for i := range matchConditions {
matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
}
matcher = matchconditions.NewMatcher(filterCompiler.Compile(matchExpressionAccessors, optionalVars, environment.StoredExpressions), failurePolicy, "policy", "validate", policy.Name)
matcher = matchconditions.NewMatcher(filterCompiler.CompileCondition(matchExpressionAccessors, optionalVars, environment.StoredExpressions), failurePolicy, "policy", "validate", policy.Name)
}
res := NewValidator(
filterCompiler.Compile(convertv1Validations(policy.Spec.Validations), optionalVars, environment.StoredExpressions),
filterCompiler.CompileCondition(convertv1Validations(policy.Spec.Validations), optionalVars, environment.StoredExpressions),
matcher,
filterCompiler.Compile(convertv1AuditAnnotations(policy.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions),
filterCompiler.Compile(convertv1MessageExpressions(policy.Spec.Validations), expressionOptionalVars, environment.StoredExpressions),
filterCompiler.CompileCondition(convertv1AuditAnnotations(policy.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions),
filterCompiler.CompileCondition(convertv1MessageExpressions(policy.Spec.Validations), expressionOptionalVars, environment.StoredExpressions),
failurePolicy,
)

View File

@@ -41,13 +41,13 @@ import (
// validator implements the Validator interface
type validator struct {
celMatcher matchconditions.Matcher
validationFilter cel.Filter
auditAnnotationFilter cel.Filter
messageFilter cel.Filter
validationFilter cel.ConditionEvaluator
auditAnnotationFilter cel.ConditionEvaluator
messageFilter cel.ConditionEvaluator
failPolicy *v1.FailurePolicyType
}
func NewValidator(validationFilter cel.Filter, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.Filter, failPolicy *v1.FailurePolicyType) Validator {
func NewValidator(validationFilter cel.ConditionEvaluator, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.ConditionEvaluator, failPolicy *v1.FailurePolicyType) Validator {
return &validator{
celMatcher: celMatcher,
validationFilter: validationFilter,

View File

@@ -42,7 +42,7 @@ import (
"k8s.io/apiserver/pkg/cel/environment"
)
var _ cel.Filter = &fakeCelFilter{}
var _ cel.ConditionEvaluator = &fakeCelFilter{}
type fakeCelFilter struct {
evaluations []cel.EvaluationResult
@@ -1035,8 +1035,8 @@ func TestContextCanceled(t *testing.T) {
fakeAttr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "default", "foo", schema.GroupVersionResource{}, "", admission.Create, nil, false, nil)
fakeVersionedAttr, _ := admission.NewVersionedAttributes(fakeAttr, schema.GroupVersionKind{}, nil)
fc := cel.NewFilterCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
f := fc.Compile([]cel.ExpressionAccessor{&ValidationCondition{Expression: "[1,2,3,4,5,6,7,8,9,10].map(x, [1,2,3,4,5,6,7,8,9,10].map(y, x*y)) == []"}}, cel.OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, environment.StoredExpressions)
fc := cel.NewConditionCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
f := fc.CompileCondition([]cel.ExpressionAccessor{&ValidationCondition{Expression: "[1,2,3,4,5,6,7,8,9,10].map(x, [1,2,3,4,5,6,7,8,9,10].map(y, x*y)) == []"}}, cel.OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, environment.StoredExpressions)
v := validator{
failPolicy: &fail,
celMatcher: &fakeCELMatcher{matches: true},

View File

@@ -50,7 +50,7 @@ type WebhookAccessor interface {
GetRESTClient(clientManager *webhookutil.ClientManager) (*rest.RESTClient, error)
// GetCompiledMatcher gets the compiled matcher object
GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher
GetCompiledMatcher(compiler cel.ConditionCompiler) matchconditions.Matcher
// GetName gets the webhook Name field. Note that the name is scoped to the webhook
// configuration and does not provide a globally unique identity, if a unique identity is
@@ -132,7 +132,7 @@ func (m *mutatingWebhookAccessor) GetType() string {
return "admit"
}
func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher {
func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.ConditionCompiler) matchconditions.Matcher {
m.compileMatcher.Do(func() {
expressions := make([]cel.ExpressionAccessor, len(m.MutatingWebhook.MatchConditions))
for i, matchCondition := range m.MutatingWebhook.MatchConditions {
@@ -145,7 +145,7 @@ func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler
if utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks) {
strictCost = true
}
m.compiledMatcher = matchconditions.NewMatcher(compiler.Compile(
m.compiledMatcher = matchconditions.NewMatcher(compiler.CompileCondition(
expressions,
cel.OptionalVariableDeclarations{
HasParams: false,
@@ -265,7 +265,7 @@ func (v *validatingWebhookAccessor) GetRESTClient(clientManager *webhookutil.Cli
return v.client, v.clientErr
}
func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher {
func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.ConditionCompiler) matchconditions.Matcher {
v.compileMatcher.Do(func() {
expressions := make([]cel.ExpressionAccessor, len(v.ValidatingWebhook.MatchConditions))
for i, matchCondition := range v.ValidatingWebhook.MatchConditions {
@@ -278,7 +278,7 @@ func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompil
if utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks) {
strictCost = true
}
v.compiledMatcher = matchconditions.NewMatcher(compiler.Compile(
v.compiledMatcher = matchconditions.NewMatcher(compiler.CompileCondition(
expressions,
cel.OptionalVariableDeclarations{
HasParams: false,

View File

@@ -20,6 +20,7 @@ import (
"context"
"fmt"
"io"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
@@ -56,7 +57,7 @@ type Webhook struct {
namespaceMatcher *namespace.Matcher
objectMatcher *object.Matcher
dispatcher Dispatcher
filterCompiler cel.FilterCompiler
filterCompiler cel.ConditionCompiler
authorizer authorizer.Authorizer
}
@@ -101,7 +102,7 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory
namespaceMatcher: &namespace.Matcher{},
objectMatcher: &object.Matcher{},
dispatcher: dispatcherFactory(&cm),
filterCompiler: cel.NewFilterCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks))),
filterCompiler: cel.NewConditionCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks))),
}, nil
}

Some files were not shown because too many files have changed in this diff Show More