From 910c2e2dad58b08d7a5ee6c59a2970454e241e2a Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Fri, 25 Oct 2024 13:20:14 -0400 Subject: [PATCH 01/16] Move caching authorizer to shared location to be used by mutating and validating policy --- .../plugin/authorizer/caching_authorizer.go | 155 ++++++ .../authorizer/caching_authorizer_test.go | 523 ++++++++++++++++++ 2 files changed, 678 insertions(+) create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/authorizer/caching_authorizer.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/authorizer/caching_authorizer_test.go diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/authorizer/caching_authorizer.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/authorizer/caching_authorizer.go new file mode 100644 index 00000000000..5dffd972366 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/authorizer/caching_authorizer.go @@ -0,0 +1,155 @@ +/* +Copyright 2023 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 authorizer + +import ( + "context" + "encoding/json" + "sort" + "strings" + + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +type authzResult struct { + authorized authorizer.Decision + reason string + err error +} + +type cachingAuthorizer struct { + authorizer authorizer.Authorizer + decisions map[string]authzResult +} + +// 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), + } +} + +// The attribute accessors known to cache key construction. If this fails to compile, the cache +// implementation may need to be updated. +var _ authorizer.Attributes = (interface { + GetUser() user.Info + GetVerb() string + IsReadOnly() bool + GetNamespace() string + GetResource() string + GetSubresource() string + GetName() string + GetAPIGroup() string + GetAPIVersion() string + IsResourceRequest() bool + GetPath() string + GetFieldSelector() (fields.Requirements, error) + GetLabelSelector() (labels.Requirements, error) +})(nil) + +// The user info accessors known to cache key construction. If this fails to compile, the cache +// implementation may need to be updated. +var _ user.Info = (interface { + GetName() string + GetUID() string + GetGroups() []string + GetExtra() map[string][]string +})(nil) + +// Authorize returns an authorization decision by delegating to another Authorizer. If an equivalent +// check has already been performed, a cached result is returned. Not safe for concurrent use. +func (ca *cachingAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { + type SerializableAttributes struct { + authorizer.AttributesRecord + LabelSelector string + } + + serializableAttributes := SerializableAttributes{ + AttributesRecord: authorizer.AttributesRecord{ + Verb: a.GetVerb(), + Namespace: a.GetNamespace(), + APIGroup: a.GetAPIGroup(), + APIVersion: a.GetAPIVersion(), + Resource: a.GetResource(), + Subresource: a.GetSubresource(), + Name: a.GetName(), + ResourceRequest: a.IsResourceRequest(), + Path: a.GetPath(), + }, + } + // in the error case, we won't honor this field selector, so the cache doesn't need it. + if fieldSelector, err := a.GetFieldSelector(); len(fieldSelector) > 0 { + serializableAttributes.FieldSelectorRequirements, serializableAttributes.FieldSelectorParsingErr = fieldSelector, err + } + if labelSelector, _ := a.GetLabelSelector(); len(labelSelector) > 0 { + // the labels requirements have private elements so those don't help us serialize to a unique key + serializableAttributes.LabelSelector = labelSelector.String() + } + + if u := a.GetUser(); u != nil { + di := &user.DefaultInfo{ + Name: u.GetName(), + UID: u.GetUID(), + } + + // Differently-ordered groups or extras could cause otherwise-equivalent checks to + // have distinct cache keys. + if groups := u.GetGroups(); len(groups) > 0 { + di.Groups = make([]string, len(groups)) + copy(di.Groups, groups) + sort.Strings(di.Groups) + } + + if extra := u.GetExtra(); len(extra) > 0 { + di.Extra = make(map[string][]string, len(extra)) + for k, vs := range extra { + vdupe := make([]string, len(vs)) + copy(vdupe, vs) + sort.Strings(vdupe) + di.Extra[k] = vdupe + } + } + + serializableAttributes.User = di + } + + var b strings.Builder + if err := json.NewEncoder(&b).Encode(serializableAttributes); err != nil { + return authorizer.DecisionNoOpinion, "", err + } + key := b.String() + + if cached, ok := ca.decisions[key]; ok { + return cached.authorized, cached.reason, cached.err + } + + authorized, reason, err := ca.authorizer.Authorize(ctx, a) + + ca.decisions[key] = authzResult{ + authorized: authorized, + reason: reason, + err: err, + } + + return authorized, reason, err +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/authorizer/caching_authorizer_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/authorizer/caching_authorizer_test.go new file mode 100644 index 00000000000..78779c7aa69 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/authorizer/caching_authorizer_test.go @@ -0,0 +1,523 @@ +/* +Copyright 2023 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 authorizer + +import ( + "context" + "errors" + "fmt" + "testing" + + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + genericfeatures "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + featuregatetesting "k8s.io/component-base/featuregate/testing" +) + +func mustParseLabelSelector(str string) labels.Requirements { + ret, err := labels.Parse(str) + if err != nil { + panic(err) + } + retRequirements, _ /*selectable*/ := ret.Requirements() + return retRequirements +} + +func TestCachingAuthorizer(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true) + + type result struct { + decision authorizer.Decision + reason string + error error + } + + type invocation struct { + attributes authorizer.Attributes + expected result + } + + for _, tc := range []struct { + name string + calls []invocation + backend []result + }{ + { + name: "hit", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{Name: "test name"}, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + { + attributes: authorizer.AttributesRecord{Name: "test name"}, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + { + name: "hit with differently-ordered groups", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{ + User: &user.DefaultInfo{ + Groups: []string{"a", "b", "c"}, + }, + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + { + attributes: authorizer.AttributesRecord{ + User: &user.DefaultInfo{ + Groups: []string{"c", "b", "a"}, + }, + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + { + name: "hit with differently-ordered extra", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{ + User: &user.DefaultInfo{ + Extra: map[string][]string{ + "k": {"a", "b", "c"}, + }, + }, + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + { + attributes: authorizer.AttributesRecord{ + User: &user.DefaultInfo{ + Extra: map[string][]string{ + "k": {"c", "b", "a"}, + }, + }, + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + { + name: "miss due to different name", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{Name: "alpha"}, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason alpha", + error: fmt.Errorf("test error alpha"), + }, + }, + { + attributes: authorizer.AttributesRecord{Name: "beta"}, + expected: result{ + decision: authorizer.DecisionDeny, + reason: "test reason beta", + error: fmt.Errorf("test error beta"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason alpha", + error: fmt.Errorf("test error alpha"), + }, + { + decision: authorizer.DecisionDeny, + reason: "test reason beta", + error: fmt.Errorf("test error beta"), + }, + }, + }, + { + name: "miss due to different user", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{ + User: &user.DefaultInfo{Name: "alpha"}, + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason alpha", + error: fmt.Errorf("test error alpha"), + }, + }, + { + attributes: authorizer.AttributesRecord{ + User: &user.DefaultInfo{Name: "beta"}, + }, + expected: result{ + decision: authorizer.DecisionDeny, + reason: "test reason beta", + error: fmt.Errorf("test error beta"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason alpha", + error: fmt.Errorf("test error alpha"), + }, + { + decision: authorizer.DecisionDeny, + reason: "test reason beta", + error: fmt.Errorf("test error beta"), + }, + }, + }, + { + name: "honor good field selector", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{ + Name: "test name", + FieldSelectorRequirements: fields.ParseSelectorOrDie("foo=bar").Requirements(), + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + { + attributes: authorizer.AttributesRecord{ + Name: "test name", + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason 2", + error: fmt.Errorf("test error 2"), + }, + }, + { + // now this should be cached + attributes: authorizer.AttributesRecord{ + Name: "test name", + FieldSelectorRequirements: fields.ParseSelectorOrDie("foo=bar").Requirements(), + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + { + decision: authorizer.DecisionAllow, + reason: "test reason 2", + error: fmt.Errorf("test error 2"), + }, + }, + }, + { + name: "ignore malformed field selector first", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{ + Name: "test name", + FieldSelectorParsingErr: errors.New("malformed"), + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + { + // notice that this does not have the malformed field selector. + // it should use the cached result + attributes: authorizer.AttributesRecord{ + Name: "test name", + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + { + name: "ignore malformed field selector second", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{ + Name: "test name", + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + { + // this should use the broader cached value because the selector will be ignored + attributes: authorizer.AttributesRecord{ + Name: "test name", + FieldSelectorParsingErr: errors.New("malformed"), + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + + { + name: "honor good label selector", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{ + Name: "test name", + LabelSelectorRequirements: mustParseLabelSelector("foo=bar"), + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + { + attributes: authorizer.AttributesRecord{ + Name: "test name", + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason 2", + error: fmt.Errorf("test error 2"), + }, + }, + { + // now this should be cached + attributes: authorizer.AttributesRecord{ + Name: "test name", + LabelSelectorRequirements: mustParseLabelSelector("foo=bar"), + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + { + attributes: authorizer.AttributesRecord{ + Name: "test name", + LabelSelectorRequirements: mustParseLabelSelector("diff=zero"), + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason 3", + error: fmt.Errorf("test error 3"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + { + decision: authorizer.DecisionAllow, + reason: "test reason 2", + error: fmt.Errorf("test error 2"), + }, + { + decision: authorizer.DecisionAllow, + reason: "test reason 3", + error: fmt.Errorf("test error 3"), + }, + }, + }, + { + name: "ignore malformed label selector first", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{ + Name: "test name", + LabelSelectorParsingErr: errors.New("malformed mess"), + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + { + // notice that this does not have the malformed field selector. + // it should use the cached result + attributes: authorizer.AttributesRecord{ + Name: "test name", + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + { + name: "ignore malformed label selector second", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{ + Name: "test name", + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + { + // this should use the broader cached value because the selector will be ignored + attributes: authorizer.AttributesRecord{ + Name: "test name", + LabelSelectorParsingErr: errors.New("malformed mess"), + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var misses int + 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)) + } + result := tc.backend[misses] + misses++ + return result.decision, result.reason, result.error + }) + }()) + + for i, invocation := range tc.calls { + decision, reason, err := frontend.Authorize(context.TODO(), invocation.attributes) + if decision != invocation.expected.decision { + t.Errorf("(call %d of %d) expected decision %v, got %v", i+1, len(tc.calls), invocation.expected.decision, decision) + } + if reason != invocation.expected.reason { + t.Errorf("(call %d of %d) expected reason %q, got %q", i+1, len(tc.calls), invocation.expected.reason, reason) + } + if err.Error() != invocation.expected.error.Error() { + t.Errorf("(call %d of %d) expected error %q, got %q", i+1, len(tc.calls), invocation.expected.error.Error(), err.Error()) + } + } + + if len(tc.backend) > misses { + t.Errorf("expected %d backend invocations, got %d", len(tc.backend), misses) + } + }) + } +} From 9ee1ea9d37c25d8151aad18d5a2a959836dcbe12 Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Fri, 25 Oct 2024 13:22:06 -0400 Subject: [PATCH 02/16] Clean up Object initialization support The initial work of this had been merged before this PR but was not yet in use. This simplifies the implementation and adds some basic type sanity checking. Co-authored-by: Jiahui Feng --- .../apiserver/pkg/cel/common/typeprovider.go | 127 ++++++++ .../pkg/cel/common/typeprovider_test.go | 170 +++++++++++ .../pkg/cel/mutation/common/constants.go | 27 -- .../pkg/cel/mutation/common/interface.go | 45 --- .../apiserver/pkg/cel/mutation/common/val.go | 140 --------- .../pkg/cel/mutation/common/val_test.go | 65 ---- .../pkg/cel/mutation/dynamic/objects.go | 249 ++++++++++++++++ .../pkg/cel/mutation/dynamic/objects_test.go | 166 +++++++++++ .../apiserver/pkg/cel/mutation/env_test.go | 51 ---- .../apiserver/pkg/cel/mutation/jsonpatch.go | 178 +++++++++++ .../apiserver/pkg/cel/mutation/mock_test.go | 110 ------- .../pkg/cel/mutation/optional_test.go | 147 --------- .../pkg/cel/mutation/typeprovider.go | 100 ------- .../pkg/cel/mutation/typeprovider_test.go | 66 ----- .../pkg/cel/mutation/typeresolver.go | 47 +++ .../pkg/cel/mutation/typeresolver_test.go | 280 ++++++++++++++++++ .../cel/mutation/unstructured/fieldtype.go | 43 --- .../pkg/cel/mutation/unstructured/typeref.go | 66 ----- .../cel/mutation/unstructured/typeresolver.go | 39 --- .../unstructured/typeresolver_test.go | 156 ---------- 20 files changed, 1217 insertions(+), 1055 deletions(-) create mode 100644 staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider.go create mode 100644 staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider_test.go delete mode 100644 staging/src/k8s.io/apiserver/pkg/cel/mutation/common/constants.go delete mode 100644 staging/src/k8s.io/apiserver/pkg/cel/mutation/common/interface.go delete mode 100644 staging/src/k8s.io/apiserver/pkg/cel/mutation/common/val.go delete mode 100644 staging/src/k8s.io/apiserver/pkg/cel/mutation/common/val_test.go create mode 100644 staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects.go create mode 100644 staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects_test.go delete mode 100644 staging/src/k8s.io/apiserver/pkg/cel/mutation/env_test.go create mode 100644 staging/src/k8s.io/apiserver/pkg/cel/mutation/jsonpatch.go delete mode 100644 staging/src/k8s.io/apiserver/pkg/cel/mutation/mock_test.go delete mode 100644 staging/src/k8s.io/apiserver/pkg/cel/mutation/optional_test.go delete mode 100644 staging/src/k8s.io/apiserver/pkg/cel/mutation/typeprovider.go delete mode 100644 staging/src/k8s.io/apiserver/pkg/cel/mutation/typeprovider_test.go create mode 100644 staging/src/k8s.io/apiserver/pkg/cel/mutation/typeresolver.go create mode 100644 staging/src/k8s.io/apiserver/pkg/cel/mutation/typeresolver_test.go delete mode 100644 staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/fieldtype.go delete mode 100644 staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/typeref.go delete mode 100644 staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/typeresolver.go delete mode 100644 staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/typeresolver_test.go diff --git a/staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider.go b/staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider.go new file mode 100644 index 00000000000..685a585c70e --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider.go @@ -0,0 +1,127 @@ +/* +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 common + +import ( + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" +) + +// TypeResolver resolves a type by a given name. +type TypeResolver interface { + // Resolve resolves the type by its name. + // This function returns false if the name does not refer to a known object type. + Resolve(name string) (ResolvedType, bool) +} + +// ResolvedType refers an object type that can be looked up for its fields. +type ResolvedType interface { + ref.Type + + Type() *types.Type + + // Field finds the field by the field name, or false if the field is not known. + // This function directly return a FieldType that is known to CEL to be more customizable. + Field(name string) (*types.FieldType, bool) + + // FieldNames returns the field names associated with the type, if the type + // is found. + FieldNames() ([]string, bool) + + // Val creates an instance for the ResolvedType, given its fields and their values. + Val(fields map[string]ref.Val) ref.Val +} + +// ResolverTypeProvider delegates type resolution first to the TypeResolver and then +// to the underlying types.Provider for types not resolved by the TypeResolver. +type ResolverTypeProvider struct { + typeResolver TypeResolver + underlyingTypeProvider types.Provider +} + +var _ types.Provider = (*ResolverTypeProvider)(nil) + +// FindStructType returns the Type give a qualified type name, by looking it up with +// the DynamicTypeResolver and translating it to CEL Type. +// If the type is not known to the DynamicTypeResolver, the lookup falls back to the underlying +// ResolverTypeProvider instead. +func (p *ResolverTypeProvider) FindStructType(structType string) (*types.Type, bool) { + t, ok := p.typeResolver.Resolve(structType) + if ok { + return types.NewTypeTypeWithParam(t.Type()), true + } + return p.underlyingTypeProvider.FindStructType(structType) +} + +// FindStructFieldNames returns the field names associated with the type, if the type +// is found. +func (p *ResolverTypeProvider) FindStructFieldNames(structType string) ([]string, bool) { + t, ok := p.typeResolver.Resolve(structType) + if ok { + return t.FieldNames() + } + return p.underlyingTypeProvider.FindStructFieldNames(structType) +} + +// FindStructFieldType returns the field type for a checked type value. +// Returns false if the field could not be found. +func (p *ResolverTypeProvider) FindStructFieldType(structType, fieldName string) (*types.FieldType, bool) { + t, ok := p.typeResolver.Resolve(structType) + if ok { + return t.Field(fieldName) + } + return p.underlyingTypeProvider.FindStructFieldType(structType, fieldName) +} + +// NewValue creates a new type value from a qualified name and map of fields. +func (p *ResolverTypeProvider) NewValue(structType string, fields map[string]ref.Val) ref.Val { + t, ok := p.typeResolver.Resolve(structType) + if ok { + return t.Val(fields) + } + return p.underlyingTypeProvider.NewValue(structType, fields) +} + +func (p *ResolverTypeProvider) EnumValue(enumName string) ref.Val { + return p.underlyingTypeProvider.EnumValue(enumName) +} + +func (p *ResolverTypeProvider) FindIdent(identName string) (ref.Val, bool) { + return p.underlyingTypeProvider.FindIdent(identName) +} + +// ResolverEnvOption creates the ResolverTypeProvider with a given DynamicTypeResolver, +// and also returns the CEL ResolverEnvOption to apply it to the env. +func ResolverEnvOption(resolver TypeResolver) cel.EnvOption { + _, envOpt := NewResolverTypeProviderAndEnvOption(resolver) + return envOpt +} + +// NewResolverTypeProviderAndEnvOption creates the ResolverTypeProvider with a given DynamicTypeResolver, +// and also returns the CEL ResolverEnvOption to apply it to the env. +func NewResolverTypeProviderAndEnvOption(resolver TypeResolver) (*ResolverTypeProvider, cel.EnvOption) { + tp := &ResolverTypeProvider{typeResolver: resolver} + var envOption cel.EnvOption = func(e *cel.Env) (*cel.Env, error) { + // wrap the existing type provider (acquired from the env) + // and set new type provider for the env. + tp.underlyingTypeProvider = e.CELTypeProvider() + typeProviderOption := cel.CustomTypeProvider(tp) + return typeProviderOption(e) + } + return tp, envOption +} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider_test.go b/staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider_test.go new file mode 100644 index 00000000000..ae21fe07300 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider_test.go @@ -0,0 +1,170 @@ +/* +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 common + +import ( + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "reflect" + "strings" + "testing" + + "k8s.io/apimachinery/pkg/util/version" + "k8s.io/apiserver/pkg/cel/environment" + "k8s.io/apiserver/pkg/cel/mutation/dynamic" +) + +func TestTypeProvider(t *testing.T) { + for _, tc := range []struct { + name string + expression string + expectedValue any + expectCompileError string + }{ + { + name: "not an object", + expression: `2 * 31 * 1847`, + expectedValue: int64(114514), // type resolver should not interfere. + }, + { + name: "empty", + expression: "Test{}", + expectedValue: map[string]any{}, + }, + { + name: "simple", + expression: "Test{x: 1}", + expectedValue: map[string]any{"x": int64(1)}, + }, + { + name: "invalid type", + expression: "Objectfoo{}", + expectCompileError: "undeclared reference to 'Objectfoo'", + }, + } { + t.Run(tc.name, func(t *testing.T) { + _, option := NewResolverTypeProviderAndEnvOption(&mockTypeResolver{}) + env := mustCreateEnv(t, option) + ast, issues := env.Compile(tc.expression) + if len(tc.expectCompileError) > 0 { + if issues == nil { + t.Fatalf("expected error %v but got no error", tc.expectCompileError) + } + if !strings.Contains(issues.String(), tc.expectCompileError) { + t.Fatalf("expected error %v but got %v", tc.expectCompileError, issues.String()) + } + return + } + if issues != nil { + t.Fatalf("unexpected issues during compilation: %v", issues) + } + program, err := env.Program(ast) + if err != nil { + t.Fatalf("unexpected error while creating program: %v", err) + } + r, _, err := program.Eval(map[string]any{}) + if err != nil { + t.Fatalf("unexpected error during evaluation: %v", err) + } + if v := r.Value(); !reflect.DeepEqual(tc.expectedValue, v) { + t.Errorf("expected %v but got %v", tc.expectedValue, v) + } + }) + } +} + +// mockTypeResolver is a mock implementation of DynamicTypeResolver that +// allows the object to contain any field. +type mockTypeResolver struct { +} + +func (m *mockTypeResolver) Resolve(name string) (ResolvedType, bool) { + if name == "Test" { + return newMockResolvedType(m, name), true + } + return nil, false +} + +// mockResolvedType is a mock implementation of ResolvedType that +// contains any field. +type mockResolvedType struct { + objectType *types.Type + resolver TypeResolver +} + +func newMockResolvedType(resolver TypeResolver, name string) *mockResolvedType { + objectType := types.NewObjectType(name) + return &mockResolvedType{ + objectType: objectType, + resolver: resolver, + } +} + +func (m *mockResolvedType) HasTrait(trait int) bool { + return m.objectType.HasTrait(trait) +} + +func (m *mockResolvedType) TypeName() string { + return m.objectType.TypeName() +} + +func (m *mockResolvedType) Type() *types.Type { + return m.objectType +} + +func (m *mockResolvedType) TypeType() *types.Type { + return types.NewTypeTypeWithParam(m.objectType) +} + +func (m *mockResolvedType) Field(name string) (*types.FieldType, bool) { + return &types.FieldType{ + Type: types.DynType, + IsSet: func(target any) bool { + return true + }, + GetFrom: func(target any) (any, error) { + return nil, nil + }, + }, true +} + +func (m *mockResolvedType) FieldNames() ([]string, bool) { + return nil, true +} + +func (m *mockResolvedType) Val(fields map[string]ref.Val) ref.Val { + return dynamic.NewObjectVal(m.objectType, fields) +} + +// mustCreateEnv creates the default env for testing, with given option. +// it fatally fails the test if the env fails to set up. +func mustCreateEnv(t testing.TB, envOptions ...cel.EnvOption) *cel.Env { + envSet, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true). + Extend(environment.VersionedOptions{ + IntroducedVersion: version.MajorMinor(1, 30), + EnvOptions: envOptions, + }) + if err != nil { + t.Fatalf("fail to create env set: %v", err) + } + env, err := envSet.Env(environment.StoredExpressions) + if err != nil { + t.Fatalf("fail to setup env: %v", env) + } + return env +} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/constants.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/constants.go deleted file mode 100644 index 5eaa3a1b1b6..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/constants.go +++ /dev/null @@ -1,27 +0,0 @@ -/* -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 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 diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/interface.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/interface.go deleted file mode 100644 index b2708a70719..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/interface.go +++ /dev/null @@ -1,45 +0,0 @@ -/* -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 common - -import ( - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" -) - -// TypeResolver resolves a type by a given name. -type TypeResolver interface { - // Resolve resolves the type by its name, starting with "Object" as its root. - // The type that the name refers to must be an object. - // This function returns false if the name does not refer to a known object type. - Resolve(name string) (TypeRef, bool) -} - -// TypeRef refers an object type that can be looked up for its fields. -type TypeRef interface { - ref.Type - - // CELType wraps the TypeRef to be a type that is understood by CEL. - CELType() *types.Type - - // Field finds the field by the field name, or false if the field is not known. - // This function directly return a FieldType that is known to CEL to be more customizable. - Field(name string) (*types.FieldType, bool) - - // Val creates an instance for the TypeRef, given its fields and their values. - Val(fields map[string]ref.Val) ref.Val -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/val.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/val.go deleted file mode 100644 index aefd497159d..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/val.go +++ /dev/null @@ -1,140 +0,0 @@ -/* -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 common - -import ( - "fmt" - "reflect" - - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/common/types/traits" -) - -// ObjectVal is the CEL Val for an object that is constructed via the object -// construction syntax. -type ObjectVal struct { - typeRef TypeRef - fields map[string]ref.Val -} - -// NewObjectVal creates an ObjectVal by its TypeRef and its fields. -func NewObjectVal(typeRef TypeRef, fields map[string]ref.Val) *ObjectVal { - return &ObjectVal{ - typeRef: typeRef, - fields: fields, - } -} - -var _ ref.Val = (*ObjectVal)(nil) -var _ traits.Zeroer = (*ObjectVal)(nil) - -// ConvertToNative converts the object to map[string]any. -// All nested lists are converted into []any native type. -// -// It returns an error if the target type is not map[string]any, -// or any recursive conversion fails. -func (v *ObjectVal) ConvertToNative(typeDesc reflect.Type) (any, error) { - var result map[string]any - if typeDesc != reflect.TypeOf(result) { - return nil, fmt.Errorf("unable to convert to %v", typeDesc) - } - result = make(map[string]any, len(v.fields)) - for k, v := range v.fields { - converted, err := convertField(v) - if err != nil { - return nil, fmt.Errorf("fail to convert field %q: %w", k, err) - } - result[k] = converted - } - return result, nil -} - -// ConvertToType supports type conversions between CEL value types supported by the expression language. -func (v *ObjectVal) ConvertToType(typeValue ref.Type) ref.Val { - switch typeValue { - case v.typeRef: - return v - case types.TypeType: - return v.typeRef.CELType() - } - return types.NewErr("unsupported conversion into %v", typeValue) -} - -// Equal returns true if the `other` value has the same type and content as the implementing struct. -func (v *ObjectVal) Equal(other ref.Val) ref.Val { - if rhs, ok := other.(*ObjectVal); ok { - return types.Bool(reflect.DeepEqual(v.fields, rhs.fields)) - } - return types.Bool(false) -} - -// Type returns the TypeValue of the value. -func (v *ObjectVal) Type() ref.Type { - return v.typeRef.CELType() -} - -// Value returns its value as a map[string]any. -func (v *ObjectVal) Value() any { - var result any - var object map[string]any - result, err := v.ConvertToNative(reflect.TypeOf(object)) - if err != nil { - return types.WrapErr(err) - } - return result -} - -// IsZeroValue indicates whether the object is the zero value for the type. -// For the ObjectVal, it is zero value if and only if the fields map is empty. -func (v *ObjectVal) IsZeroValue() bool { - return len(v.fields) == 0 -} - -// convertField converts a referred ref.Val to its expected type. -// For objects, the expected type is map[string]any -// For lists, the expected type is []any -// For maps, the expected type is map[string]any -// For anything else, it is converted via value.Value() -// -// It will return an error if the request type is a map but the key -// is not a string. -func convertField(value ref.Val) (any, error) { - // special handling for lists, where the elements are converted with Value() instead of ConvertToNative - // to allow them to become native value of any type. - if listOfVal, ok := value.Value().([]ref.Val); ok { - var result []any - for _, v := range listOfVal { - result = append(result, v.Value()) - } - return result, nil - } - // unstructured maps, as seen in annotations - // map keys must be strings - if mapOfVal, ok := value.Value().(map[ref.Val]ref.Val); ok { - result := make(map[string]any) - for k, v := range mapOfVal { - stringKey, ok := k.Value().(string) - if !ok { - return nil, fmt.Errorf("map key %q is of type %t, not string", k, k) - } - result[stringKey] = v.Value() - } - return result, nil - } - return value.Value(), nil -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/val_test.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/val_test.go deleted file mode 100644 index b175906d008..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/val_test.go +++ /dev/null @@ -1,65 +0,0 @@ -/* -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 common - -import ( - "reflect" - "testing" - - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" -) - -func TestOptional(t *testing.T) { - for _, tc := range []struct { - name string - fields map[string]ref.Val - expected map[string]any - }{ - { - name: "present", - fields: map[string]ref.Val{ - "zero": types.OptionalOf(types.IntZero), - }, - expected: map[string]any{ - "zero": int64(0), - }, - }, - { - name: "none", - fields: map[string]ref.Val{ - "absent": types.OptionalNone, - }, - expected: map[string]any{ - // right now no way to differ from a plain null. - // we will need to filter out optional.none() before this conversion. - "absent": nil, - }, - }, - } { - t.Run(tc.name, func(t *testing.T) { - v := &ObjectVal{ - typeRef: nil, // safe in this test, otherwise put a mock - fields: tc.fields, - } - converted := v.Value() - if !reflect.DeepEqual(tc.expected, converted) { - t.Errorf("wrong result, expected %v but got %v", tc.expected, converted) - } - }) - } -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects.go new file mode 100644 index 00000000000..b00db23de4e --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects.go @@ -0,0 +1,249 @@ +/* +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 dynamic + +import ( + "errors" + "fmt" + "reflect" + "strings" + + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/common/types/traits" + "google.golang.org/protobuf/types/known/structpb" +) + +// ObjectType is the implementation of the Object type for use when compiling +// CEL expressions without schema information about the object. +// This is to provide CEL expressions with access to Object{} types constructors. +type ObjectType struct { + objectType *types.Type +} + +func (o *ObjectType) HasTrait(trait int) bool { + return o.objectType.HasTrait(trait) +} + +// TypeName returns the name of this ObjectType. +func (o *ObjectType) TypeName() string { + return o.objectType.TypeName() +} + +// Val returns an instance given the fields. +func (o *ObjectType) Val(fields map[string]ref.Val) ref.Val { + return NewObjectVal(o.objectType, fields) +} + +func (o *ObjectType) Type() *types.Type { + return o.objectType +} + +// Field looks up the field by name. +// This is the unstructured version that allows any name as the field name. +// The returned field is of DynType type. +func (o *ObjectType) Field(name string) (*types.FieldType, bool) { + return &types.FieldType{ + // for unstructured, we do not check for its type, + // use DynType for all fields. + Type: types.DynType, + IsSet: func(target any) bool { + if m, ok := target.(map[string]any); ok { + _, isSet := m[name] + return isSet + } + return false + }, + GetFrom: func(target any) (any, error) { + if m, ok := target.(map[string]any); ok { + return m[name], nil + } + return nil, fmt.Errorf("cannot get field %q", name) + }, + }, true +} + +func (o *ObjectType) FieldNames() ([]string, bool) { + return nil, true // Field names are not known for dynamic types. All field names are allowed. +} + +// NewObjectType creates a ObjectType by the given field name. +func NewObjectType(name string) *ObjectType { + return &ObjectType{ + objectType: types.NewObjectType(name), + } +} + +// ObjectVal is the CEL Val for an object that is constructed via the Object{} in +// CEL expressions without schema information about the object. +type ObjectVal struct { + objectType *types.Type + fields map[string]ref.Val +} + +// NewObjectVal creates an ObjectVal by its ResolvedType and its fields. +func NewObjectVal(objectType *types.Type, fields map[string]ref.Val) *ObjectVal { + return &ObjectVal{ + objectType: objectType, + fields: fields, + } +} + +var _ ref.Val = (*ObjectVal)(nil) +var _ traits.Zeroer = (*ObjectVal)(nil) + +// ConvertToNative converts the object to map[string]any. +// All nested lists are converted into []any native type. +// +// It returns an error if the target type is not map[string]any, +// or any recursive conversion fails. +func (v *ObjectVal) ConvertToNative(typeDesc reflect.Type) (any, error) { + result := make(map[string]any, len(v.fields)) + for k, v := range v.fields { + converted, err := convertField(v) + if err != nil { + return nil, fmt.Errorf("fail to convert field %q: %w", k, err) + } + result[k] = converted + } + if typeDesc == reflect.TypeOf(result) { + return result, nil + } + // CEL's builtin data literal values all support conversion to structpb.Value, which + // can then be serialized to JSON. This is convenient for CEL expressions that return + // an arbitrary JSON value, such as our MutatingAdmissionPolicy JSON Patch valueExpression + // field, so we support the conversion here, for Object data literals, as well. + if typeDesc == reflect.TypeOf(&structpb.Value{}) { + return structpb.NewStruct(result) + } + return nil, fmt.Errorf("unable to convert to %v", typeDesc) +} + +// ConvertToType supports type conversions between CEL value types supported by the expression language. +func (v *ObjectVal) ConvertToType(typeValue ref.Type) ref.Val { + if v.objectType.TypeName() == typeValue.TypeName() { + return v + } + if typeValue == types.TypeType { + return types.NewTypeTypeWithParam(v.objectType) + } + return types.NewErr("unsupported conversion into %v", typeValue) +} + +// Equal returns true if the `other` value has the same type and content as the implementing struct. +func (v *ObjectVal) Equal(other ref.Val) ref.Val { + if rhs, ok := other.(*ObjectVal); ok { + if v.objectType.Equal(rhs.objectType) != types.True { + return types.False + } + return types.Bool(reflect.DeepEqual(v.fields, rhs.fields)) + } + return types.False +} + +// Type returns the TypeValue of the value. +func (v *ObjectVal) Type() ref.Type { + return types.NewObjectType(v.objectType.TypeName()) +} + +// Value returns its value as a map[string]any. +func (v *ObjectVal) Value() any { + var result any + var object map[string]any + result, err := v.ConvertToNative(reflect.TypeOf(object)) + if err != nil { + return types.WrapErr(err) + } + return result +} + +// CheckTypeNamesMatchFieldPathNames transitively checks the CEL object type names of this ObjectVal. Returns all +// found type name mismatch errors. +// Children ObjectVal types under or this ObjectVal +// must have type names of the form ".", children of that type must have type names of the +// form ".." and so on. +// Intermediate maps and lists are unnamed and ignored. +func (v *ObjectVal) CheckTypeNamesMatchFieldPathNames() error { + return errors.Join(typeCheck(v, []string{v.Type().TypeName()})...) + +} + +func typeCheck(v ref.Val, typeNamePath []string) []error { + var errs []error + if ov, ok := v.(*ObjectVal); ok { + tn := ov.objectType.TypeName() + if strings.Join(typeNamePath, ".") != tn { + errs = append(errs, fmt.Errorf("unexpected type name %q, expected %q, which matches field name path from root Object type", tn, strings.Join(typeNamePath, "."))) + } + for k, f := range ov.fields { + errs = append(errs, typeCheck(f, append(typeNamePath, k))...) + } + } + value := v.Value() + if listOfVal, ok := value.([]ref.Val); ok { + for _, v := range listOfVal { + errs = append(errs, typeCheck(v, typeNamePath)...) + } + } + + if mapOfVal, ok := value.(map[ref.Val]ref.Val); ok { + for _, v := range mapOfVal { + errs = append(errs, typeCheck(v, typeNamePath)...) + } + } + return errs +} + +// IsZeroValue indicates whether the object is the zero value for the type. +// For the ObjectVal, it is zero value if and only if the fields map is empty. +func (v *ObjectVal) IsZeroValue() bool { + return len(v.fields) == 0 +} + +// convertField converts a referred ref.Val to its expected type. +// For objects, the expected type is map[string]any +// For lists, the expected type is []any +// For maps, the expected type is map[string]any +// For anything else, it is converted via value.Value() +// +// It will return an error if the request type is a map but the key +// is not a string. +func convertField(value ref.Val) (any, error) { + // special handling for lists, where the elements are converted with Value() instead of ConvertToNative + // to allow them to become native value of any type. + if listOfVal, ok := value.Value().([]ref.Val); ok { + var result []any + for _, v := range listOfVal { + result = append(result, v.Value()) + } + return result, nil + } + // unstructured maps, as seen in annotations + // map keys must be strings + if mapOfVal, ok := value.Value().(map[ref.Val]ref.Val); ok { + result := make(map[string]any) + for k, v := range mapOfVal { + stringKey, ok := k.Value().(string) + if !ok { + return nil, fmt.Errorf("map key %q is of type %t, not string", k, k) + } + result[stringKey] = v.Value() + } + return result, nil + } + return value.Value(), nil +} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects_test.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects_test.go new file mode 100644 index 00000000000..8426986a080 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects_test.go @@ -0,0 +1,166 @@ +/* +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 dynamic + +import ( + "reflect" + "strings" + "testing" + + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" +) + +func TestOptional(t *testing.T) { + for _, tc := range []struct { + name string + fields map[string]ref.Val + expected map[string]any + }{ + { + name: "present", + fields: map[string]ref.Val{ + "zero": types.OptionalOf(types.IntZero), + }, + expected: map[string]any{ + "zero": int64(0), + }, + }, + { + name: "none", + fields: map[string]ref.Val{ + "absent": types.OptionalNone, + }, + expected: map[string]any{ + // right now no way to differ from a plain null. + // we will need to filter out optional.none() before this conversion. + "absent": nil, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + v := &ObjectVal{ + objectType: nil, // safe in this test, otherwise put a mock + fields: tc.fields, + } + converted := v.Value() + if !reflect.DeepEqual(tc.expected, converted) { + t.Errorf("wrong result, expected %v but got %v", tc.expected, converted) + } + }) + } +} + +func TestCheckTypeNamesMatchFieldPathNames(t *testing.T) { + for _, tc := range []struct { + name string + obj *ObjectVal + expectError string + }{ + { + name: "valid", + obj: &ObjectVal{ + objectType: types.NewObjectType("Object"), + fields: map[string]ref.Val{ + "spec": &ObjectVal{ + objectType: types.NewObjectType("Object.spec"), + fields: map[string]ref.Val{ + "replicas": types.Int(100), + "m": types.NewRefValMap(nil, map[ref.Val]ref.Val{ + types.String("k1"): &ObjectVal{ + objectType: types.NewObjectType("Object.spec.m"), + }, + }), + "l": types.NewRefValList(nil, []ref.Val{ + &ObjectVal{ + objectType: types.NewObjectType("Object.spec.l"), + }, + }), + }, + }, + }, + }, + }, + { + name: "invalid struct field", + obj: &ObjectVal{ + objectType: types.NewObjectType("Object"), + fields: map[string]ref.Val{"invalid": &ObjectVal{ + objectType: types.NewObjectType("Object.spec"), + fields: map[string]ref.Val{"replicas": types.Int(100)}, + }}, + }, + expectError: "unexpected type name \"Object.spec\", expected \"Object.invalid\"", + }, + { + name: "invalid map field", + obj: &ObjectVal{ + objectType: types.NewObjectType("Object"), + fields: map[string]ref.Val{ + "spec": &ObjectVal{ + objectType: types.NewObjectType("Object.spec"), + fields: map[string]ref.Val{ + "replicas": types.Int(100), + "m": types.NewRefValMap(nil, map[ref.Val]ref.Val{ + types.String("k1"): &ObjectVal{ + objectType: types.NewObjectType("Object.spec.invalid"), + }, + }), + }, + }, + }, + }, + expectError: "unexpected type name \"Object.spec.invalid\", expected \"Object.spec.m\"", + }, + { + name: "invalid list field", + obj: &ObjectVal{ + objectType: types.NewObjectType("Object"), + fields: map[string]ref.Val{ + "spec": &ObjectVal{ + objectType: types.NewObjectType("Object.spec"), + fields: map[string]ref.Val{ + "replicas": types.Int(100), + "l": types.NewRefValList(nil, []ref.Val{ + &ObjectVal{ + objectType: types.NewObjectType("Object.spec.invalid"), + }, + }), + }, + }, + }, + }, + expectError: "unexpected type name \"Object.spec.invalid\", expected \"Object.spec.l\"", + }, + } { + t.Run(tc.name, func(t *testing.T) { + err := tc.obj.CheckTypeNamesMatchFieldPathNames() + if tc.expectError == "" { + if err != nil { + t.Errorf("unexpected error: %v", err) + } + } else { + if err == nil { + t.Errorf("expected error") + } + if !strings.Contains(err.Error(), tc.expectError) { + t.Errorf("expected error to contain %v, got %v", tc.expectError, err) + } + } + }) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/env_test.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/env_test.go deleted file mode 100644 index 911ae5db2b4..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/env_test.go +++ /dev/null @@ -1,51 +0,0 @@ -/* -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 mutation - -import ( - "testing" - - "github.com/google/cel-go/cel" - - "k8s.io/apimachinery/pkg/util/version" - "k8s.io/apiserver/pkg/cel/environment" -) - -// mustCreateEnv creates the default env for testing, with given option. -// it fatally fails the test if the env fails to set up. -func mustCreateEnv(t testing.TB, envOptions ...cel.EnvOption) *cel.Env { - envSet, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true). - Extend(environment.VersionedOptions{ - IntroducedVersion: version.MajorMinor(1, 30), - EnvOptions: envOptions, - }) - if err != nil { - t.Fatalf("fail to create env set: %v", err) - } - env, err := envSet.Env(environment.StoredExpressions) - if err != nil { - t.Fatalf("fail to setup env: %v", env) - } - return env -} - -// mustCreateEnvWithOptional creates the default env for testing, with given option, -// and set up the optional library with default configuration. -// it fatally fails the test if the env fails to set up. -func mustCreateEnvWithOptional(t testing.TB, envOptions ...cel.EnvOption) *cel.Env { - return mustCreateEnv(t, append(envOptions, cel.OptionalTypes())...) -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/jsonpatch.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/jsonpatch.go new file mode 100644 index 00000000000..1cb018db957 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/mutation/jsonpatch.go @@ -0,0 +1,178 @@ +/* +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 mutation + +import ( + "fmt" + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "reflect" +) + +var jsonPatchType = types.NewObjectType(JSONPatchTypeName) + +var ( + jsonPatchOp = "op" + jsonPatchPath = "path" + jsonPatchFrom = "from" + jsonPatchValue = "value" +) + +// JSONPatchType and JSONPatchVal are defined entirely from scratch here because JSONPatchVal +// has a dynamic 'value' field which can not be defined with an OpenAPI schema, +// preventing us from using DeclType and UnstructuredToVal. + +// JSONPatchType provides a CEL type for "JSONPatch" operations. +type JSONPatchType struct{} + +func (r *JSONPatchType) HasTrait(trait int) bool { + return jsonPatchType.HasTrait(trait) +} + +// TypeName returns the name of this ObjectType. +func (r *JSONPatchType) TypeName() string { + return jsonPatchType.TypeName() +} + +// Val returns an instance given the fields. +func (r *JSONPatchType) Val(fields map[string]ref.Val) ref.Val { + result := &JSONPatchVal{} + for name, value := range fields { + switch name { + case jsonPatchOp: + if s, ok := value.Value().(string); ok { + result.Op = s + } else { + return types.NewErr("unexpected type %T for JSONPatchType 'op' field", value.Value()) + } + case jsonPatchPath: + if s, ok := value.Value().(string); ok { + result.Path = s + } else { + return types.NewErr("unexpected type %T for JSONPatchType 'path' field", value.Value()) + } + case jsonPatchFrom: + if s, ok := value.Value().(string); ok { + result.From = s + } else { + return types.NewErr("unexpected type %T for JSONPatchType 'from' field", value.Value()) + } + case jsonPatchValue: + result.Val = value + default: + return types.NewErr("unexpected JSONPatchType field: %s", name) + } + } + return result +} + +func (r *JSONPatchType) Type() *types.Type { + return jsonPatchType +} + +func (r *JSONPatchType) Field(name string) (*types.FieldType, bool) { + var fieldType *types.Type + switch name { + case jsonPatchOp, jsonPatchFrom, jsonPatchPath: + fieldType = cel.StringType + case jsonPatchValue: + fieldType = types.DynType + } + return &types.FieldType{ + Type: fieldType, + }, true +} + +func (r *JSONPatchType) FieldNames() ([]string, bool) { + return []string{jsonPatchOp, jsonPatchFrom, jsonPatchPath, jsonPatchValue}, true +} + +// JSONPatchVal is the ref.Val for a JSONPatch. +type JSONPatchVal struct { + Op, From, Path string + Val ref.Val +} + +func (p *JSONPatchVal) ConvertToNative(typeDesc reflect.Type) (any, error) { + if typeDesc == reflect.TypeOf(&JSONPatchVal{}) { + return p, nil + } + return nil, fmt.Errorf("cannot convert to native type: %v", typeDesc) +} + +func (p *JSONPatchVal) ConvertToType(typeValue ref.Type) ref.Val { + if typeValue == jsonPatchType { + return p + } else if typeValue == types.TypeType { + return types.NewTypeTypeWithParam(jsonPatchType) + } + return types.NewErr("Unsupported type: %s", typeValue.TypeName()) +} + +func (p *JSONPatchVal) Equal(other ref.Val) ref.Val { + if o, ok := other.(*JSONPatchVal); ok && p != nil && o != nil { + if *p == *o { + return types.True + } + } + return types.False +} + +func (p *JSONPatchVal) Get(index ref.Val) ref.Val { + if name, ok := index.Value().(string); ok { + switch name { + case jsonPatchOp: + return types.String(p.Op) + case jsonPatchPath: + return types.String(p.Path) + case jsonPatchFrom: + return types.String(p.From) + case jsonPatchValue: + return p.Val + default: + + } + } + return types.NewErr("unsupported indexer: %s", index) +} + +func (p *JSONPatchVal) IsSet(field ref.Val) ref.Val { + if name, ok := field.Value().(string); ok { + switch name { + case jsonPatchOp: + return types.Bool(len(p.Op) > 0) + case jsonPatchPath: + return types.Bool(len(p.Path) > 0) + case jsonPatchFrom: + return types.Bool(len(p.From) > 0) + case jsonPatchValue: + return types.Bool(p.Val != nil) + } + } + return types.NewErr("unsupported field: %s", field) +} + +func (p *JSONPatchVal) Type() ref.Type { + return jsonPatchType +} + +func (p *JSONPatchVal) Value() any { + return p +} + +var _ ref.Val = &JSONPatchVal{} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/mock_test.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/mock_test.go deleted file mode 100644 index f8d30bd299e..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/mock_test.go +++ /dev/null @@ -1,110 +0,0 @@ -/* -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 mutation - -import ( - "strings" - - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - - "k8s.io/apiserver/pkg/cel/mutation/common" -) - -// mockTypeResolver is a mock implementation of TypeResolver that -// allows the object to contain any field. -type mockTypeResolver struct { -} - -// mockTypeRef is a mock implementation of TypeRef that -// contains any field. -type mockTypeRef struct { - objectType *types.Type - resolver common.TypeResolver -} - -func newMockTypeRef(resolver common.TypeResolver, name string) *mockTypeRef { - objectType := types.NewObjectType(name, common.ObjectTraits) - return &mockTypeRef{ - objectType: objectType, - resolver: resolver, - } -} - -func (m *mockTypeRef) HasTrait(trait int) bool { - return common.ObjectTraits|trait != 0 -} - -func (m *mockTypeRef) TypeName() string { - return m.objectType.TypeName() -} - -func (m *mockTypeRef) CELType() *types.Type { - return types.NewTypeTypeWithParam(m.objectType) -} - -func (m *mockTypeRef) Field(name string) (*types.FieldType, bool) { - return &types.FieldType{ - Type: types.DynType, - IsSet: func(target any) bool { - return true - }, - GetFrom: func(target any) (any, error) { - return nil, nil - }, - }, true -} - -func (m *mockTypeRef) Val(fields map[string]ref.Val) ref.Val { - return common.NewObjectVal(m, fields) -} - -func (m *mockTypeResolver) Resolve(name string) (common.TypeRef, bool) { - if strings.HasPrefix(name, common.RootTypeReferenceName) { - return newMockTypeRef(m, name), true - } - return nil, false -} - -// mockTypeResolverForOptional behaves the same as mockTypeResolver -// except returning a mockTypeRefForOptional instead of mockTypeRef -type mockTypeResolverForOptional struct { - *mockTypeResolver -} - -// mockTypeRefForOptional behaves the same as the underlying TypeRef -// except treating "nonExisting" field as non-existing. -// This is used for optional tests. -type mockTypeRefForOptional struct { - common.TypeRef -} - -// Field returns a mock FieldType, or false if the field should not exist. -func (m *mockTypeRefForOptional) Field(name string) (*types.FieldType, bool) { - if name == "nonExisting" { - return nil, false - } - return m.TypeRef.Field(name) -} - -func (m *mockTypeResolverForOptional) Resolve(name string) (common.TypeRef, bool) { - r, ok := m.mockTypeResolver.Resolve(name) - if ok { - return &mockTypeRefForOptional{TypeRef: r}, ok - } - return nil, false -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/optional_test.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/optional_test.go deleted file mode 100644 index 6d47f0100e5..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/optional_test.go +++ /dev/null @@ -1,147 +0,0 @@ -/* -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 mutation - -import ( - "strings" - "testing" - - celtypes "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - - "k8s.io/apiserver/pkg/cel/mutation/common" -) - -// TestCELOptional is an exploration test to demonstrate how CEL optional library -// behave for the use cases that the mutation library requires. -func TestCELOptional(t *testing.T) { - for _, tc := range []struct { - name string - expression string - expectedVal ref.Val - expectedCompileError string - }{ - { - // question mark syntax still requires the field to exist in object construction - name: "construct non-existing field, compile error", - expression: `Object{ - ?nonExisting: optional.none() - }`, - expectedCompileError: `undefined field 'nonExisting'`, - }, - { - // The root cause of the behavior above is that, has on an object (or Message in the Language Def), - // still require the field to be declared in the schema. - // - // Quoting from - // https://github.com/google/cel-spec/blob/master/doc/langdef.md#field-selection - // - // To test for the presence of a field, the boolean-valued macro has(e.f) can be used. - // - // 2. If e evaluates to a message and f is not a declared field for the message, - // has(e.f) raises a no_such_field error. - name: "has(Object{}), de-sugared, compile error", - expression: "has(Object{}.nonExisting)", - expectedCompileError: `undefined field 'nonExisting'`, - }, - { - name: "construct existing field with none, empty object", - expression: `Object{ - ?existing: optional.none() - }`, - expectedVal: common.NewObjectVal(nil, map[string]ref.Val{ - // "existing" field was not set. - }), - }, - { - name: "object of zero value, ofNonZeroValue", - expression: `Object{?spec: optional.ofNonZeroValue(Object.spec{?replicas: Object{}.?replicas})}`, - expectedVal: common.NewObjectVal(nil, map[string]ref.Val{ - // "existing" field was not set. - }), - }, - { - name: "access non-existing field, return none", - expression: `Object{}.?nonExisting`, - expectedCompileError: `undefined field 'nonExisting'`, - }, - { - name: "access existing field, return none", - expression: `Object{}.?existing`, - expectedVal: celtypes.OptionalNone, - }, - { - name: "map non-existing field, return none", - expression: `{"foo": 1}[?"bar"]`, - expectedVal: celtypes.OptionalNone, - }, - { - name: "map existing field, return actual value", - expression: `{"foo": 1}[?"foo"]`, - expectedVal: celtypes.OptionalOf(celtypes.Int(1)), - }, - { - // Map has a different behavior than Object - // - // Quoting from - // https://github.com/google/cel-spec/blob/master/doc/langdef.md#field-selection - // - // To test for the presence of a field, the boolean-valued macro has(e.f) can be used. - // - // 1. If e evaluates to a map, then has(e.f) indicates whether the string f is - // a key in the map (note that f must syntactically be an identifier). - // - name: "has on a map, de-sugared, non-existing field, returns false", - // has marco supports only the dot access syntax. - expression: `has({"foo": 1}.bar)`, - expectedVal: celtypes.False, - }, - { - name: "has on a map, de-sugared, existing field, returns true", - // has marco supports only the dot access syntax. - expression: `has({"foo": 1}.foo)`, - expectedVal: celtypes.True, - }, - } { - t.Run(tc.name, func(t *testing.T) { - _, option := NewTypeProviderAndEnvOption(&mockTypeResolverForOptional{ - mockTypeResolver: &mockTypeResolver{}, - }) - env := mustCreateEnvWithOptional(t, option) - ast, issues := env.Compile(tc.expression) - if issues != nil { - if tc.expectedCompileError == "" { - t.Fatalf("unexpected issues during compilation: %v", issues) - } else if !strings.Contains(issues.String(), tc.expectedCompileError) { - t.Fatalf("unexpected compile error, want to contain %q but got %v", tc.expectedCompileError, issues) - } - return - } - program, err := env.Program(ast) - if err != nil { - t.Fatalf("unexpected error while creating program: %v", err) - } - r, _, err := program.Eval(map[string]any{}) - if err != nil { - t.Fatalf("unexpected error during evaluation: %v", err) - } - if equals := tc.expectedVal.Equal(r); equals.Value() != true { - t.Errorf("expected %v but got %v", tc.expectedVal, r) - } - }) - } -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeprovider.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeprovider.go deleted file mode 100644 index 694ed3c08f2..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeprovider.go +++ /dev/null @@ -1,100 +0,0 @@ -/* -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 mutation - -import ( - "k8s.io/apiserver/pkg/cel/mutation/common" - - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" -) - -// TypeProvider is a specialized CEL type provider that understands -// the Object type alias that is used to construct an Apply configuration for -// a mutation operation. -type TypeProvider struct { - typeResolver common.TypeResolver - underlyingTypeProvider types.Provider -} - -var _ types.Provider = (*TypeProvider)(nil) - -// EnumValue returns the numeric value of the given enum value name. -// This TypeProvider does not have special handling for EnumValue and thus directly delegate -// to its underlying type provider. -func (p *TypeProvider) EnumValue(enumName string) ref.Val { - return p.underlyingTypeProvider.EnumValue(enumName) -} - -// FindIdent takes a qualified identifier name and returns a ref.ObjectVal if one exists. -// This TypeProvider does not have special handling for FindIdent and thus directly delegate -// to its underlying type provider. -func (p *TypeProvider) FindIdent(identName string) (ref.Val, bool) { - return p.underlyingTypeProvider.FindIdent(identName) -} - -// FindStructType returns the Type give a qualified type name, by looking it up with -// the TypeResolver and translating it to CEL Type. -// If the type is not known to the TypeResolver, the lookup falls back to the underlying -// TypeProvider instead. -func (p *TypeProvider) FindStructType(structType string) (*types.Type, bool) { - t, ok := p.typeResolver.Resolve(structType) - if ok { - return t.CELType(), true - } - return p.underlyingTypeProvider.FindStructType(structType) -} - -// FindStructFieldNames returns the field names associated with the type, if the type -// is found. -func (p *TypeProvider) FindStructFieldNames(structType string) ([]string, bool) { - return nil, true -} - -// FindStructFieldType returns the field type for a checked type value. -// Returns false if the field could not be found. -func (p *TypeProvider) FindStructFieldType(structType, fieldName string) (*types.FieldType, bool) { - t, ok := p.typeResolver.Resolve(structType) - if ok { - return t.Field(fieldName) - } - return p.underlyingTypeProvider.FindStructFieldType(structType, fieldName) -} - -// NewValue creates a new type value from a qualified name and map of fields. -func (p *TypeProvider) NewValue(structType string, fields map[string]ref.Val) ref.Val { - t, ok := p.typeResolver.Resolve(structType) - if ok { - return t.Val(fields) - } - return p.underlyingTypeProvider.NewValue(structType, fields) -} - -// NewTypeProviderAndEnvOption creates the TypeProvider with a given TypeResolver, -// and also returns the CEL EnvOption to apply it to the env. -func NewTypeProviderAndEnvOption(resolver common.TypeResolver) (*TypeProvider, cel.EnvOption) { - tp := &TypeProvider{typeResolver: resolver} - var envOption cel.EnvOption = func(e *cel.Env) (*cel.Env, error) { - // wrap the existing type provider (acquired from the env) - // and set new type provider for the env. - tp.underlyingTypeProvider = e.CELTypeProvider() - typeProviderOption := cel.CustomTypeProvider(tp) - return typeProviderOption(e) - } - return tp, envOption -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeprovider_test.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeprovider_test.go deleted file mode 100644 index 8e95d087372..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeprovider_test.go +++ /dev/null @@ -1,66 +0,0 @@ -/* -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 mutation - -import ( - "reflect" - "testing" -) - -func TestTypeProvider(t *testing.T) { - for _, tc := range []struct { - name string - expression string - expectedValue any - }{ - { - name: "not an object", - expression: `2 * 31 * 1847`, - expectedValue: int64(114514), // type resolver should not interfere. - }, - { - name: "empty", - expression: "Object{}", - expectedValue: map[string]any{}, - }, - { - name: "Object.spec", - expression: "Object{spec: Object.spec{replicas: 3}}", - expectedValue: map[string]any{"spec": map[string]any{"replicas": int64(3)}}, - }, - } { - t.Run(tc.name, func(t *testing.T) { - _, option := NewTypeProviderAndEnvOption(&mockTypeResolver{}) - env := mustCreateEnv(t, option) - ast, issues := env.Compile(tc.expression) - if issues != nil { - t.Fatalf("unexpected issues during compilation: %v", issues) - } - program, err := env.Program(ast) - if err != nil { - t.Fatalf("unexpected error while creating program: %v", err) - } - r, _, err := program.Eval(map[string]any{}) - if err != nil { - t.Fatalf("unexpected error during evaluation: %v", err) - } - if v := r.Value(); !reflect.DeepEqual(tc.expectedValue, v) { - t.Errorf("expected %v but got %v", tc.expectedValue, v) - } - }) - } -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeresolver.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeresolver.go new file mode 100644 index 00000000000..aceed5ae55e --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeresolver.go @@ -0,0 +1,47 @@ +/* +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 mutation + +import ( + "strings" + + "k8s.io/apiserver/pkg/cel/common" + "k8s.io/apiserver/pkg/cel/mutation/dynamic" +) + +// ObjectTypeName is the name of Object types that are used to declare the types of +// Kubernetes objects in CEL dynamically using the naming scheme "Object....". +// For example "Object.spec.containers" is the type of the spec.containers field of the object in scope. +const ObjectTypeName = "Object" + +// JSONPatchTypeName is the name of the JSONPatch type. This type is typically used to create JSON patches +// in CEL expressions. +const JSONPatchTypeName = "JSONPatch" + +// DynamicTypeResolver resolves the Object and JSONPatch types when compiling +// CEL expressions without schema information about the object. +type DynamicTypeResolver struct{} + +func (r *DynamicTypeResolver) Resolve(name string) (common.ResolvedType, bool) { + if name == JSONPatchTypeName { + return &JSONPatchType{}, true + } + if name == ObjectTypeName || strings.HasPrefix(name, ObjectTypeName+".") { + return dynamic.NewObjectType(name), true + } + return nil, false +} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeresolver_test.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeresolver_test.go new file mode 100644 index 00000000000..092c01770f1 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeresolver_test.go @@ -0,0 +1,280 @@ +/* +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 mutation + +import ( + "reflect" + "strings" + "testing" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + + "k8s.io/apimachinery/pkg/util/version" + "k8s.io/apiserver/pkg/cel/common" + "k8s.io/apiserver/pkg/cel/environment" + "k8s.io/apiserver/pkg/cel/mutation/dynamic" +) + +func TestTypeResolver(t *testing.T) { + for _, tc := range []struct { + name string + expression string + expectedValue any + expectCompileError string + }{ + { + name: "not an object", + expression: `string(114514)`, + expectedValue: "114514", + }, + { + name: "empty", + expression: "Object{}", + expectedValue: map[string]any{}, + }, + { + name: "Object.spec", + expression: "Object{spec: Object.spec{replicas: 3}}", + expectedValue: map[string]any{ + "spec": map[string]any{ + // an integer maps to int64 + "replicas": int64(3), + }, + }, + }, + { + // list literal does not require new Path code of the type provider + // comparing to the object literal. + // This test case serves as a note of "supported syntax" + name: "Object.spec.template.containers", + expression: `Object{ + spec: Object.spec{ + template: Object.spec.template{ + containers: [ + Object.spec.template.containers.item{ + name: "nginx", + image: "nginx", + args: ["-g"] + } + ] + } + } + }`, + expectedValue: map[string]any{ + "spec": map[string]any{ + "template": map[string]any{ + "containers": []any{ + map[string]any{ + "name": "nginx", + "image": "nginx", + "args": []any{"-g"}, + }, + }, + }, + }, + }, + }, + { + name: "list of ints", + expression: `Object{ + intList: [1, 2, 3] + }`, + expectedValue: map[string]any{ + "intList": []any{int64(1), int64(2), int64(3)}, + }, + }, + { + name: "map string-to-string", + expression: `Object{ + annotations: {"foo": "bar"} + }`, + expectedValue: map[string]any{ + "annotations": map[string]any{ + "foo": "bar", + }, + }, + }, + { + name: "field access", + expression: `Object{ + intList: [1, 2, 3] + }.intList.sum()`, + expectedValue: int64(6), + }, + { + name: "equality check", + expression: "Object{spec: Object.spec{replicas: 3}} == Object{spec: Object.spec{replicas: 1 + 2}}", + expectedValue: true, + }, + { + name: "invalid type", + expression: "Invalid{}", + expectCompileError: "undeclared reference to 'Invalid'", + }, + } { + t.Run(tc.name, func(t *testing.T) { + _, option := common.NewResolverTypeProviderAndEnvOption(&DynamicTypeResolver{}) + env := mustCreateEnv(t, option) + ast, issues := env.Compile(tc.expression) + if len(tc.expectCompileError) > 0 { + if issues == nil { + t.Fatalf("expected error %v but got no error", tc.expectCompileError) + } + if !strings.Contains(issues.String(), tc.expectCompileError) { + t.Fatalf("expected error %v but got %v", tc.expectCompileError, issues.String()) + } + return + } + + if issues != nil { + t.Fatalf("unexpected issues during compilation: %v", issues) + } + program, err := env.Program(ast) + if err != nil { + t.Fatalf("unexpected error while creating program: %v", err) + } + r, _, err := program.Eval(map[string]any{}) + if err != nil { + t.Fatalf("unexpected error during evaluation: %v", err) + } + if v := r.Value(); !reflect.DeepEqual(v, tc.expectedValue) { + t.Errorf("expected %v but got %v", tc.expectedValue, v) + } + }) + } +} + +// TestCELOptional is an exploration test to demonstrate how CEL optional library +// behave for the use cases that the mutation library requires. +func TestCELOptional(t *testing.T) { + for _, tc := range []struct { + name string + expression string + expectedVal ref.Val + expectedCompileError string + }{ + { + name: "construct existing field with none, empty object", + expression: `Object{ + ?existing: optional.none() + }`, + expectedVal: dynamic.NewObjectVal(types.NewObjectType("Object"), map[string]ref.Val{ + // "existing" field was not set. + }), + }, + { + name: "object of zero value, ofNonZeroValue", + expression: `Object{?spec: optional.ofNonZeroValue(Object.spec{?replicas: Object{}.?replicas})}`, + expectedVal: dynamic.NewObjectVal(types.NewObjectType("Object"), map[string]ref.Val{ + // "existing" field was not set. + }), + }, + { + name: "access existing field, return none", + expression: `Object{}.?existing`, + expectedVal: types.OptionalNone, + }, + { + name: "map non-existing field, return none", + expression: `{"foo": 1}[?"bar"]`, + expectedVal: types.OptionalNone, + }, + { + name: "map existing field, return actual value", + expression: `{"foo": 1}[?"foo"]`, + expectedVal: types.OptionalOf(types.Int(1)), + }, + { + // Map has a different behavior than Object + // + // Quoting from + // https://github.com/google/cel-spec/blob/master/doc/langdef.md#field-selection + // + // To test for the presence of a field, the boolean-valued macro has(e.f) can be used. + // + // 1. If e evaluates to a map, then has(e.f) indicates whether the string f is + // a key in the map (note that f must syntactically be an identifier). + // + name: "has on a map, de-sugared, non-existing field, returns false", + // has marco supports only the dot access syntax. + expression: `has({"foo": 1}.bar)`, + expectedVal: types.False, + }, + { + name: "has on a map, de-sugared, existing field, returns true", + // has marco supports only the dot access syntax. + expression: `has({"foo": 1}.foo)`, + expectedVal: types.True, + }, + } { + t.Run(tc.name, func(t *testing.T) { + _, option := common.NewResolverTypeProviderAndEnvOption(&DynamicTypeResolver{}) + env := mustCreateEnvWithOptional(t, option) + ast, issues := env.Compile(tc.expression) + if len(tc.expectedCompileError) > 0 { + if issues == nil { + t.Fatalf("expected error %v but got no error", tc.expectedCompileError) + } + if !strings.Contains(issues.String(), tc.expectedCompileError) { + t.Fatalf("expected error %v but got %v", tc.expectedCompileError, issues.String()) + } + return + } + if issues != nil { + t.Fatalf("unexpected issues during compilation: %v", issues) + } + program, err := env.Program(ast) + if err != nil { + t.Fatalf("unexpected error while creating program: %v", err) + } + r, _, err := program.Eval(map[string]any{}) + if err != nil { + t.Fatalf("unexpected error during evaluation: %v", err) + } + if equals := tc.expectedVal.Equal(r); equals.Value() != true { + t.Errorf("expected %#+v but got %#+v", tc.expectedVal, r) + } + }) + } +} + +// mustCreateEnv creates the default env for testing, with given option. +// it fatally fails the test if the env fails to set up. +func mustCreateEnv(t testing.TB, envOptions ...cel.EnvOption) *cel.Env { + envSet, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true). + Extend(environment.VersionedOptions{ + IntroducedVersion: version.MajorMinor(1, 0), // Always enabled. This is just for test. + EnvOptions: envOptions, + }) + if err != nil { + t.Fatalf("fail to create env set: %v", err) + } + env, err := envSet.Env(environment.StoredExpressions) + if err != nil { + t.Fatalf("fail to setup env: %v", env) + } + return env +} + +// mustCreateEnvWithOptional creates the default env for testing, with given option, +// and set up the optional library with default configuration. +// it fatally fails the test if the env fails to set up. +func mustCreateEnvWithOptional(t testing.TB, envOptions ...cel.EnvOption) *cel.Env { + return mustCreateEnv(t, append(envOptions, cel.OptionalTypes())...) +} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/fieldtype.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/fieldtype.go deleted file mode 100644 index f99ac516d03..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/fieldtype.go +++ /dev/null @@ -1,43 +0,0 @@ -/* -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 unstructured - -import ( - "fmt" - - "github.com/google/cel-go/common/types" -) - -// NewFieldType creates a field by its field name. -// This version of FieldType is unstructured and has DynType as its type. -func NewFieldType(name string) *types.FieldType { - return &types.FieldType{ - // for unstructured, we do not check for its type, - // use DynType for all fields. - Type: types.DynType, - IsSet: func(target any) bool { - // for an unstructured object, we allow any field to be considered set. - return true - }, - GetFrom: func(target any) (any, error) { - if m, ok := target.(map[string]any); ok { - return m[name], nil - } - return nil, fmt.Errorf("cannot get field %q", name) - }, - } -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/typeref.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/typeref.go deleted file mode 100644 index 32ab8d28274..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/typeref.go +++ /dev/null @@ -1,66 +0,0 @@ -/* -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 unstructured - -import ( - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - - "k8s.io/apiserver/pkg/cel/mutation/common" -) - -// TypeRef is the implementation of TypeRef for an unstructured object. -// This is especially usefully when the schema is not known or available. -type TypeRef struct { - celObjectType *types.Type - celTypeType *types.Type -} - -func (r *TypeRef) HasTrait(trait int) bool { - return common.ObjectTraits|trait != 0 -} - -// TypeName returns the name of this TypeRef. -func (r *TypeRef) TypeName() string { - return r.celObjectType.TypeName() -} - -// Val returns an instance given the fields. -func (r *TypeRef) Val(fields map[string]ref.Val) ref.Val { - return common.NewObjectVal(r, fields) -} - -// CELType returns the type. The returned type is of TypeType type. -func (r *TypeRef) CELType() *types.Type { - return r.celTypeType -} - -// Field looks up the field by name. -// This is the unstructured version that allows any name as the field name. -// The returned field is of DynType type. -func (r *TypeRef) Field(name string) (*types.FieldType, bool) { - return NewFieldType(name), true -} - -// NewTypeRef creates a TypeRef by the given field name. -func NewTypeRef(name string) *TypeRef { - objectType := types.NewObjectType(name, common.ObjectTraits) - return &TypeRef{ - celObjectType: objectType, - celTypeType: types.NewTypeTypeWithParam(objectType), - } -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/typeresolver.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/typeresolver.go deleted file mode 100644 index 2182b931676..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/typeresolver.go +++ /dev/null @@ -1,39 +0,0 @@ -/* -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 unstructured - -import ( - "strings" - - "k8s.io/apiserver/pkg/cel/mutation/common" -) - -const object = common.RootTypeReferenceName - -type TypeResolver struct { -} - -// Resolve resolves the TypeRef for the given type name -// that starts with "Object". -// This is the unstructured version, which means the -// returned TypeRef does not refer to the schema. -func (r *TypeResolver) Resolve(name string) (common.TypeRef, bool) { - if !strings.HasPrefix(name, object) { - return nil, false - } - return NewTypeRef(name), true -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/typeresolver_test.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/typeresolver_test.go deleted file mode 100644 index 7ca743b27c3..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/typeresolver_test.go +++ /dev/null @@ -1,156 +0,0 @@ -/* -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 unstructured - -import ( - "reflect" - "testing" - - "github.com/google/cel-go/cel" - - "k8s.io/apimachinery/pkg/util/version" - "k8s.io/apiserver/pkg/cel/environment" - "k8s.io/apiserver/pkg/cel/mutation" -) - -func TestTypeProvider(t *testing.T) { - for _, tc := range []struct { - name string - expression string - expectedValue any - }{ - { - name: "not an object", - expression: `string(114514)`, - expectedValue: "114514", - }, - { - name: "empty", - expression: "Object{}", - expectedValue: map[string]any{}, - }, - { - name: "Object.spec", - expression: "Object{spec: Object.spec{replicas: 3}}", - expectedValue: map[string]any{ - "spec": map[string]any{ - // an integer maps to int64 - "replicas": int64(3), - }, - }, - }, - { - // list literal does not require new path code of the type provider - // comparing to the object literal. - // This test case serves as a note of "supported syntax" - name: "Object.spec.template.containers", - expression: `Object{ - spec: Object.spec{ - template: Object.spec.template{ - containers: [ - Object.spec.template.containers.item{ - name: "nginx", - image: "nginx", - args: ["-g"] - } - ] - } - } - }`, - expectedValue: map[string]any{ - "spec": map[string]any{ - "template": map[string]any{ - "containers": []any{ - map[string]any{ - "name": "nginx", - "image": "nginx", - "args": []any{"-g"}, - }, - }, - }, - }, - }, - }, - { - name: "list of ints", - expression: `Object{ - intList: [1, 2, 3] - }`, - expectedValue: map[string]any{ - "intList": []any{int64(1), int64(2), int64(3)}, - }, - }, - { - name: "map string-to-string", - expression: `Object{ - annotations: {"foo": "bar"} - }`, - expectedValue: map[string]any{ - "annotations": map[string]any{ - "foo": "bar", - }, - }, - }, - { - name: "field access", - expression: `Object{ - intList: [1, 2, 3] - }.intList.sum()`, - expectedValue: int64(6), - }, - { - name: "equality check", - expression: "Object{spec: Object.spec{replicas: 3}} == Object{spec: Object.spec{replicas: 1 + 2}}", - expectedValue: true, - }, - } { - t.Run(tc.name, func(t *testing.T) { - _, option := mutation.NewTypeProviderAndEnvOption(&TypeResolver{}) - env := mustCreateEnv(t, option) - ast, issues := env.Compile(tc.expression) - if issues != nil { - t.Fatalf("unexpected issues during compilation: %v", issues) - } - program, err := env.Program(ast) - if err != nil { - t.Fatalf("unexpected error while creating program: %v", err) - } - r, _, err := program.Eval(map[string]any{}) - if err != nil { - t.Fatalf("unexpected error during evaluation: %v", err) - } - if v := r.Value(); !reflect.DeepEqual(v, tc.expectedValue) { - t.Errorf("expected %v but got %v", tc.expectedValue, v) - } - }) - } -} -func mustCreateEnv(t testing.TB, envOptions ...cel.EnvOption) *cel.Env { - envSet, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true). - Extend(environment.VersionedOptions{ - IntroducedVersion: version.MajorMinor(1, 30), - EnvOptions: envOptions, - }) - if err != nil { - t.Fatalf("fail to create env set: %v", err) - } - env, err := envSet.Env(environment.StoredExpressions) - if err != nil { - t.Fatalf("fail to setup env: %v", env) - } - return env -} From 4b13362dda020f80cf961dbeefae750f63f803f9 Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Fri, 25 Oct 2024 13:22:58 -0400 Subject: [PATCH 03/16] Add feature gate Co-authored-by: cici37 --- pkg/features/versioned_kube_features.go | 2 +- staging/src/k8s.io/apiserver/pkg/features/kube_features.go | 4 ++-- .../featuregates_linter/test_data/versioned_feature_list.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/features/versioned_kube_features.go b/pkg/features/versioned_kube_features.go index 96e1a426417..978478d22b2 100644 --- a/pkg/features/versioned_kube_features.go +++ b/pkg/features/versioned_kube_features.go @@ -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: { diff --git a/staging/src/k8s.io/apiserver/pkg/features/kube_features.go b/staging/src/k8s.io/apiserver/pkg/features/kube_features.go index 1c5d1cc6d49..a61c9655a11 100644 --- a/staging/src/k8s.io/apiserver/pkg/features/kube_features.go +++ b/staging/src/k8s.io/apiserver/pkg/features/kube_features.go @@ -117,7 +117,7 @@ const ( // Enables KMS v1 API for encryption at rest. KMSv1 featuregate.Feature = "KMSv1" - // owner: @alexzielenski, @cici37, @jiahuif + // owner: @alexzielenski, @cici37, @jiahuif, @jpbetz // kep: https://kep.k8s.io/3962 // // Enables the MutatingAdmissionPolicy in Admission Chain @@ -329,7 +329,7 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate }, MutatingAdmissionPolicy: { - {Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha}, + {Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha}, }, OpenAPIEnums: { diff --git a/test/featuregates_linter/test_data/versioned_feature_list.yaml b/test/featuregates_linter/test_data/versioned_feature_list.yaml index b542b85b85c..acd35d59749 100644 --- a/test/featuregates_linter/test_data/versioned_feature_list.yaml +++ b/test/featuregates_linter/test_data/versioned_feature_list.yaml @@ -747,7 +747,7 @@ - default: false lockToDefault: false preRelease: Alpha - version: "1.30" + version: "1.32" - name: NFTablesProxyMode versionedSpecs: - default: false From 3a1733f302d0fe9994bcc8e91fa2191c94606c2b Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Fri, 25 Oct 2024 13:25:46 -0400 Subject: [PATCH 04/16] Add MutatingAdmissionPolicy API This is closely aligned with ValidatingAdmissionPolicy except that instead of validations that can fail with messages, there are mutations, which can be defined either with as an ApplyConfiguration or JSONPatch. Co-authored-by: cici37 --- pkg/api/testing/defaulting_test.go | 4 + .../admissionregistration/fuzzer/fuzzer.go | 23 + pkg/apis/admissionregistration/register.go | 4 + pkg/apis/admissionregistration/types.go | 330 ++- .../v1alpha1/defaults.go | 8 + .../v1alpha1/defaults_test.go | 37 + .../validation/validation.go | 244 +- .../validation/validation_test.go | 1985 ++++++++++++++++- .../default_storage_factory_builder.go | 3 + pkg/printers/internalversion/printers.go | 76 + pkg/printers/internalversion/printers_test.go | 12 + .../mutatingadmissionpolicy/authz.go | 105 + .../mutatingadmissionpolicy/authz_test.go | 131 ++ .../mutatingadmissionpolicy/doc.go | 17 + .../storage/storage.go | 73 + .../storage/storage_test.go | 252 +++ .../mutatingadmissionpolicy/strategy.go | 122 + .../mutatingadmissionpolicy/strategy_test.go | 114 + .../mutatingadmissionpolicybinding/authz.go | 137 ++ .../authz_test.go | 233 ++ .../mutatingadmissionpolicybinding/doc.go | 17 + .../storage/storage.go | 94 + .../storage/storage_test.go | 260 +++ .../strategy.go | 130 ++ .../strategy_test.go | 127 ++ .../rest/storage_apiserver.go | 21 + .../v1alpha1/register.go | 4 + .../admissionregistration/v1alpha1/types.go | 343 +++ .../policy/mutating/patch/json_patch_test.go | 1 + .../plugin/webhook/predicates/rules/rules.go | 2 +- .../admissionwebhook/admission_test.go | 2 + .../apiserver/cel/admission_test_util.go | 2 + test/integration/controlplane/generic_test.go | 2 + test/integration/etcd/data.go | 8 + 34 files changed, 4911 insertions(+), 12 deletions(-) create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicy/authz.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicy/authz_test.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicy/doc.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicy/storage/storage.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicy/storage/storage_test.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicy/strategy.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicy/strategy_test.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicybinding/authz.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicybinding/authz_test.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicybinding/doc.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicybinding/storage/storage.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicybinding/storage/storage_test.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicybinding/strategy.go create mode 100644 pkg/registry/admissionregistration/mutatingadmissionpolicybinding/strategy_test.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch_test.go diff --git a/pkg/api/testing/defaulting_test.go b/pkg/api/testing/defaulting_test.go index 75a5e0b1cfd..fa6ca54226a 100644 --- a/pkg/api/testing/defaulting_test.go +++ b/pkg/api/testing/defaulting_test.go @@ -143,6 +143,10 @@ func TestDefaulting(t *testing.T) { {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicyList"}: {}, {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicyBinding"}: {}, {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicyBindingList"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "MutatingAdmissionPolicy"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "MutatingAdmissionPolicyList"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "MutatingAdmissionPolicyBinding"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "MutatingAdmissionPolicyBindingList"}: {}, {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingWebhookConfiguration"}: {}, {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingWebhookConfigurationList"}: {}, {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "MutatingWebhookConfiguration"}: {}, diff --git a/pkg/apis/admissionregistration/fuzzer/fuzzer.go b/pkg/apis/admissionregistration/fuzzer/fuzzer.go index ca675e9b19b..45fddb02ecf 100644 --- a/pkg/apis/admissionregistration/fuzzer/fuzzer.go +++ b/pkg/apis/admissionregistration/fuzzer/fuzzer.go @@ -107,5 +107,28 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} { obj.ParameterNotFoundAction = &v } }, + func(obj *admissionregistration.MutatingAdmissionPolicySpec, c fuzz.Continue) { + c.FuzzNoCustom(obj) // fuzz self without calling this function again + if obj.FailurePolicy == nil { + p := admissionregistration.FailurePolicyType("Fail") + obj.FailurePolicy = &p + } + obj.ReinvocationPolicy = admissionregistration.NeverReinvocationPolicy + }, + func(obj *admissionregistration.Mutation, c fuzz.Continue) { + c.FuzzNoCustom(obj) // fuzz self without calling this function again + patchTypes := []admissionregistration.PatchType{admissionregistration.PatchTypeJSONPatch, admissionregistration.PatchTypeApplyConfiguration} + obj.PatchType = patchTypes[c.Rand.Intn(len(patchTypes))] + if obj.PatchType == admissionregistration.PatchTypeJSONPatch { + obj.JSONPatch = &admissionregistration.JSONPatch{} + c.Fuzz(&obj.JSONPatch) + obj.ApplyConfiguration = nil + } + if obj.PatchType == admissionregistration.PatchTypeApplyConfiguration { + obj.ApplyConfiguration = &admissionregistration.ApplyConfiguration{} + c.Fuzz(obj.ApplyConfiguration) + obj.JSONPatch = nil + } + }, } } diff --git a/pkg/apis/admissionregistration/register.go b/pkg/apis/admissionregistration/register.go index a69343e20b4..c3cf074e4e9 100644 --- a/pkg/apis/admissionregistration/register.go +++ b/pkg/apis/admissionregistration/register.go @@ -55,6 +55,10 @@ func addKnownTypes(scheme *runtime.Scheme) error { &ValidatingAdmissionPolicyList{}, &ValidatingAdmissionPolicyBinding{}, &ValidatingAdmissionPolicyBindingList{}, + &MutatingAdmissionPolicy{}, + &MutatingAdmissionPolicyList{}, + &MutatingAdmissionPolicyBinding{}, + &MutatingAdmissionPolicyBindingList{}, ) return nil } diff --git a/pkg/apis/admissionregistration/types.go b/pkg/apis/admissionregistration/types.go index 50d000484f7..8c4d80221d9 100644 --- a/pkg/apis/admissionregistration/types.go +++ b/pkg/apis/admissionregistration/types.go @@ -206,7 +206,7 @@ type ValidatingAdmissionPolicySpec struct { ParamKind *ParamKind // MatchConstraints specifies what resources this policy is designed to validate. - // The AdmissionPolicy cares about a request if it matches _all_ Constraint. + // The MutatingAdmissionPolicy cares about a request if it matches _all_ Constraint. // However, in order to prevent clusters from being put into an unstable state that cannot be recovered from via the API // ValidatingAdmissionPolicy cannot match ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding. // Required. @@ -267,6 +267,7 @@ type ValidatingAdmissionPolicySpec struct { // // The expression of a variable can refer to other variables defined earlier in the list but not those after. // Thus, Variables must be sorted by the order of first appearance and acyclic. + // +listType=atomic // +optional Variables []Variable } @@ -1163,3 +1164,330 @@ type MatchCondition struct { // Required. Expression string } + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.32 + +// MutatingAdmissionPolicy describes an admission policy that may mutate an object. +type MutatingAdmissionPolicy struct { + metav1.TypeMeta + // Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata. + // +optional + metav1.ObjectMeta + // Specification of the desired behavior of the MutatingAdmissionPolicy. + Spec MutatingAdmissionPolicySpec +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.32 + +// MutatingAdmissionPolicyList is a list of MutatingAdmissionPolicy. +type MutatingAdmissionPolicyList struct { + metav1.TypeMeta + // Standard list metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + // +optional + metav1.ListMeta + // List of ValidatingAdmissionPolicy. + Items []MutatingAdmissionPolicy +} + +// MutatingAdmissionPolicySpec is the specification of the desired behavior of the admission policy. +type MutatingAdmissionPolicySpec struct { + // paramKind specifies the kind of resources used to parameterize this policy. + // If absent, there are no parameters for this policy and the param CEL variable will not be provided to validation expressions. + // If paramKind refers to a non-existent kind, this policy definition is mis-configured and the FailurePolicy is applied. + // If paramKind is specified but paramRef is unset in MutatingAdmissionPolicyBinding, the params variable will be null. + // +optional + ParamKind *ParamKind + + // matchConstraints specifies what resources this policy is designed to validate. + // The AdmissionPolicy cares about a request if it matches _all_ Constraints. + // However, in order to prevent clusters from being put into an unstable state that cannot be recovered from via the API + // MutatingAdmissionPolicy cannot match MutatingAdmissionPolicy and MutatingAdmissionPolicyBinding. + // Only the CREATE, UPDATE, and CONNECT operations are allowed. + // '*' matches only CREATE, UPDATE, and CONNECT. + // Required. + MatchConstraints *MatchResources + + // variables contain definitions of variables that can be used in composition of other expressions. + // Each variable is defined as a named CEL expression. + // The variables defined here will be available under `variables` in other expressions of the policy + // except matchConditions because matchConditions are evaluated before the rest of the policy. + // + // The expression of a variable can refer to other variables defined earlier in the list but not those after. + // Thus, variables must be sorted by the order of first appearance and acyclic. + // +listType=atomic + // +optional + Variables []Variable + + // mutations contain operations to perform on matching objects. + // mutations may not be empty; a minimum of one mutation is required. + // mutations are evaluated in order, and are reinvoked according to + // the reinvocationPolicy. + // The mutations of a policy are invoked for each binding of this policy + // and reinvocation of mutations occurs on a per binding basis. + // + // +listType=atomic + // +optional + Mutations []Mutation + + // failurePolicy defines how to handle failures for the admission policy. Failures can + // occur from CEL expression parse errors, type check errors, runtime errors and invalid + // or mis-configured policy definitions or bindings. + // + // A policy is invalid if paramKind refers to a non-existent Kind. + // A binding is invalid if paramRef.name refers to a non-existent resource. + // + // failurePolicy does not define how validations that evaluate to false are handled. + // + // Allowed values are Ignore or Fail. Defaults to Fail. + // +optional + FailurePolicy *FailurePolicyType + + // matchConditions is a list of conditions that must be met for a request to be validated. + // Match conditions filter requests that have already been matched by the matchConstraints, + // An empty list of matchConditions matches all requests. + // There are a maximum of 64 match conditions allowed. + // + // If a parameter object is provided, it can be accessed via the `params` handle in the same + // manner as validation expressions. + // + // The exact matching logic is (in order): + // 1. If ANY matchCondition evaluates to FALSE, the policy is skipped. + // 2. If ALL matchConditions evaluate to TRUE, the policy is evaluated. + // 3. If any matchCondition evaluates to an error (but none are FALSE): + // - If failurePolicy=Fail, reject the request + // - If failurePolicy=Ignore, the policy is skipped + // + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +optional + MatchConditions []MatchCondition + + // reinvocationPolicy indicates whether mutations may be called multiple times per MutatingAdmissionPolicyBinding + // as part of a single admission evaluation. + // Allowed values are "Never" and "IfNeeded". + // + // Never: These mutations will not be called more than once per binding in a single admission evaluation. + // + // IfNeeded: These mutations may be invoked more than once per binding for a single admission request and there is no guarantee of + // order with respect to other admission plugins, admission webhooks, bindings of this policy and admission policies. Mutations are only + // reinvoked when mutations change the object after this mutation is invoked. + // Required. + ReinvocationPolicy ReinvocationPolicyType +} + +// Mutation specifies the operation that performs a Mutation. +type Mutation struct { + // patchType indicates the patch strategy used. + // Allowed values are "ApplyConfiguration" and "JSONPatch". + // Required. + // + // +unionDiscriminator + PatchType PatchType + + // applyConfiguration defines the desired configuration values of an object. + // The configuration is applied to the admission object using + // [structured merge diff](https://github.com/kubernetes-sigs/structured-merge-diff). + // A CEL expression is used to create apply configuration. + ApplyConfiguration *ApplyConfiguration + + // jsonPatch defines a [JSON patch](https://jsonpatch.com/) to perform a mutation to the object. + // A CEL expression is used to create the JSON patch. + JSONPatch *JSONPatch +} + +// PatchType specifies the type of patch operation for a mutation. +// +enum +type PatchType string + +const ( + // ApplyConfiguration indicates that the mutation is using apply configuration to mutate the object. + PatchTypeApplyConfiguration PatchType = "ApplyConfiguration" + // JSONPatch indicates that the object is mutated through JSON Patch. + PatchTypeJSONPatch PatchType = "JSONPatch" +) + +// ApplyConfiguration defines the desired configuration values of an object. +type ApplyConfiguration struct { + // expression will be evaluated by CEL to create an apply configuration. + // ref: https://github.com/google/cel-spec + // + // Apply configurations are declared in CEL using object initialization. For example, this CEL expression + // returns an apply configuration to set a single field: + // + // Object{ + // spec: Object.spec{ + // serviceAccountName: "example" + // } + // } + // + // Apply configurations may not modify atomic structs, maps or arrays due to the risk of accidental deletion of + // values not included in the apply configuration. + // + // CEL expressions have access to the object types needed to create apply configurations: + // - 'Object' - CEL type of the resource object. + // - 'Object.' - CEL type of object field (such as 'Object.spec') + // - 'Object.....` - CEL type of nested field (such as 'Object.spec.containers') + // + // CEL expressions have access to the contents of the API request, organized into CEL variables as well as some other useful variables: + // + // - 'object' - The object from the incoming request. The value is null for DELETE requests. + // - 'oldObject' - The existing object. The value is null for CREATE requests. + // - 'request' - Attributes of the API request([ref](/pkg/apis/admission/types.go#AdmissionRequest)). + // - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind. + // - 'namespaceObject' - The namespace object that the incoming object belongs to. The value is null for cluster-scoped resources. + // - 'variables' - Map of composited variables, from its name to its lazily evaluated value. + // For example, a variable named 'foo' can be accessed as 'variables.foo'. + // - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request. + // See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz + // - 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the + // request resource. + // + // The `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the + // object. No other metadata properties are accessible. + // + // Only property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. + // Required. + Expression string +} + +// JSONPatch defines a JSON Patch. +type JSONPatch struct { + // expression will be evaluated by CEL to create a [JSON patch](https://jsonpatch.com/). + // ref: https://github.com/google/cel-spec + // + // expression must return an array of JSONPatch values. + // + // For example, this CEL expression returns a JSON patch to conditionally modify a value: + // + // [ + // JSONPatch{op: "test", path: "/spec/example", value: "Red"}, + // JSONPatch{op: "replace", path: "/spec/example", value: "Green"} + // ] + // + // To define an object for the patch value, use Object types. For example: + // + // [ + // JSONPatch{ + // op: "add", + // path: "/spec/selector", + // value: Object.spec.selector{matchLabels: {"environment": "test"}} + // } + // ] + // + // To use strings containing '/' and '~' as JSONPatch path keys, use "jsonpatch.escapeKey". For example: + // + // [ + // JSONPatch{ + // op: "add", + // path: "/metadata/labels/" + jsonpatch.escapeKey("example.com/environment"), + // value: "test" + // }, + // ] + // + // CEL expressions have access to the types needed to create JSON patches and objects: + // + // - 'JSONPatch' - CEL type of JSON Patch operations. JSONPatch has the fields 'op', 'from', 'path' and 'value'. + // See [JSON patch](https://jsonpatch.com/) for more details. The 'value' field may be set to any of: string, + // integer, array, map or object. If set, the 'path' and 'from' fields must be set to a + // [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901/) string, where the 'jsonpatch.escapeKey()' CEL + // function may be used to escape path keys containing '/' and '~'. + // - 'Object' - CEL type of the resource object. + // - 'Object.' - CEL type of object field (such as 'Object.spec') + // - 'Object.....` - CEL type of nested field (such as 'Object.spec.containers') + // + // CEL expressions have access to the contents of the API request, organized into CEL variables as well as some other useful variables: + // + // - 'object' - The object from the incoming request. The value is null for DELETE requests. + // - 'oldObject' - The existing object. The value is null for CREATE requests. + // - 'request' - Attributes of the API request([ref](/pkg/apis/admission/types.go#AdmissionRequest)). + // - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind. + // - 'namespaceObject' - The namespace object that the incoming object belongs to. The value is null for cluster-scoped resources. + // - 'variables' - Map of composited variables, from its name to its lazily evaluated value. + // For example, a variable named 'foo' can be accessed as 'variables.foo'. + // - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request. + // See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz + // - 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the + // request resource. + // + // CEL expressions have access to [Kubernetes CEL function libraries](https://kubernetes.io/docs/reference/using-api/cel/#cel-options-language-features-and-libraries) + // as well as: + // + // - 'jsonpatch.escapeKey' - Performs JSONPatch key escaping. '~' and '/' are escaped as '~0' and `~1' respectively). + // + // + // Only property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. + // Required. + Expression string +} + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.32 + +// MutatingAdmissionPolicyBinding binds the MutatingAdmissionPolicy with parametrized resources. +// MutatingAdmissionPolicyBinding and the optional parameter resource together define how cluster administrators +// configure policies for clusters. +// +// For a given admission request, each binding will cause its policy to be +// evaluated N times, where N is 1 for policies/bindings that don't use +// params, otherwise N is the number of parameters selected by the binding. +// Each evaluation is constrained by a [runtime cost budget](https://kubernetes.io/docs/reference/using-api/cel/#runtime-cost-budget). +// +// Adding/removing policies, bindings, or params can not affect whether a +// given (policy, binding, param) combination is within its own CEL budget. +type MutatingAdmissionPolicyBinding struct { + metav1.TypeMeta + // Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata. + // +optional + metav1.ObjectMeta + // Specification of the desired behavior of the MutatingAdmissionPolicyBinding. + Spec MutatingAdmissionPolicyBindingSpec +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.32 + +// MutatingAdmissionPolicyBindingList is a list of MutatingAdmissionPolicyBinding. +type MutatingAdmissionPolicyBindingList struct { + metav1.TypeMeta + // Standard list metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + // +optional + metav1.ListMeta + // List of PolicyBinding. + Items []MutatingAdmissionPolicyBinding +} + +// MutatingAdmissionPolicyBindingSpec is the specification of the MutatingAdmissionPolicyBinding. +type MutatingAdmissionPolicyBindingSpec struct { + // policyName references a MutatingAdmissionPolicy name which the MutatingAdmissionPolicyBinding binds to. + // If the referenced resource does not exist, this binding is considered invalid and will be ignored + // Required. + PolicyName string + // paramRef specifies the parameter resource used to configure the admission control policy. + // It should point to a resource of the type specified in spec.ParamKind of the bound MutatingAdmissionPolicy. + // If the policy specifies a ParamKind and the resource referred to by ParamRef does not exist, this binding is considered mis-configured and the FailurePolicy of the MutatingAdmissionPolicy applied. + // If the policy does not specify a ParamKind then this field is ignored, and the rules are evaluated without a param. + // +optional + ParamRef *ParamRef + + // matchResources limits what resources match this binding and may be mutated by it. + // Note that if matchResources matches a resource, the resource must also match a policy's matchConstraints and + // matchConditions before the resource may be mutated. + // When matchResources is unset, it does not constrain resource matching, and only the policy's matchConstraints + // and matchConditions must match for the resource to be mutated. + // Additionally, matchResources.resourceRules are optional and do not constraint matching when unset. + // Note that this is differs from MutatingAdmissionPolicy matchConstraints, where resourceRules are required. + // The CREATE, UPDATE and CONNECT operations are allowed. The DELETE operation may not be matched. + // '*' matches CREATE, UPDATE and CONNECT. + // +optional + MatchResources *MatchResources +} diff --git a/pkg/apis/admissionregistration/v1alpha1/defaults.go b/pkg/apis/admissionregistration/v1alpha1/defaults.go index 1abb61b2c0c..4461d33f440 100644 --- a/pkg/apis/admissionregistration/v1alpha1/defaults.go +++ b/pkg/apis/admissionregistration/v1alpha1/defaults.go @@ -57,3 +57,11 @@ func SetDefaults_ParamRef(obj *admissionregistrationv1alpha1.ParamRef) { obj.ParameterNotFoundAction = &v } } + +// SetDefaults_MutatingAdmissionPolicySpec sets defaults for MutatingAdmissionPolicySpec +func SetDefaults_MutatingAdmissionPolicySpec(obj *admissionregistrationv1alpha1.MutatingAdmissionPolicySpec) { + if obj.FailurePolicy == nil { + policy := admissionregistrationv1alpha1.Fail + obj.FailurePolicy = &policy + } +} diff --git a/pkg/apis/admissionregistration/v1alpha1/defaults_test.go b/pkg/apis/admissionregistration/v1alpha1/defaults_test.go index 3c09d708d5f..7fbbb23201e 100644 --- a/pkg/apis/admissionregistration/v1alpha1/defaults_test.go +++ b/pkg/apis/admissionregistration/v1alpha1/defaults_test.go @@ -31,6 +31,7 @@ import ( func TestDefaultAdmissionPolicy(t *testing.T) { fail := v1alpha1.Fail + never := v1alpha1.NeverReinvocationPolicy equivalent := v1alpha1.Equivalent allScopes := v1alpha1.AllScopes @@ -103,6 +104,42 @@ func TestDefaultAdmissionPolicy(t *testing.T) { }, }, }, + { + name: "MutatingAdmissionPolicy", + original: &v1alpha1.MutatingAdmissionPolicy{ + Spec: v1alpha1.MutatingAdmissionPolicySpec{ + MatchConstraints: &v1alpha1.MatchResources{}, + ReinvocationPolicy: never, + Mutations: []v1alpha1.Mutation{ + { + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: "fake string", + }, + }, + }, + }, + }, + expected: &v1alpha1.MutatingAdmissionPolicy{ + Spec: v1alpha1.MutatingAdmissionPolicySpec{ + MatchConstraints: &v1alpha1.MatchResources{ + MatchPolicy: &equivalent, + NamespaceSelector: &metav1.LabelSelector{}, + ObjectSelector: &metav1.LabelSelector{}, + }, + FailurePolicy: &fail, + ReinvocationPolicy: never, + Mutations: []v1alpha1.Mutation{ + { + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: "fake string", + }, + }, + }, + }, + }, + }, } for _, test := range tests { diff --git a/pkg/apis/admissionregistration/validation/validation.go b/pkg/apis/admissionregistration/validation/validation.go index 53e2256f411..0fbf8833252 100644 --- a/pkg/apis/admissionregistration/validation/validation.go +++ b/pkg/apis/admissionregistration/validation/validation.go @@ -23,6 +23,7 @@ import ( "strings" "sync" + "k8s.io/apimachinery/pkg/api/equality" genericvalidation "k8s.io/apimachinery/pkg/api/validation" "k8s.io/apimachinery/pkg/api/validation/path" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -32,6 +33,7 @@ import ( utilvalidation "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" plugincel "k8s.io/apiserver/pkg/admission/plugin/cel" + "k8s.io/apiserver/pkg/admission/plugin/policy/mutating" validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating" "k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions" "k8s.io/apiserver/pkg/cel" @@ -274,6 +276,8 @@ type preexistingExpressions struct { validationExpressions sets.Set[string] validationMessageExpressions sets.Set[string] auditAnnotationValuesExpressions sets.Set[string] + applyConfigurationExpressions sets.Set[string] + jsonPatchExpressions sets.Set[string] } func newPreexistingExpressions() preexistingExpressions { @@ -282,6 +286,8 @@ func newPreexistingExpressions() preexistingExpressions { validationExpressions: sets.New[string](), validationMessageExpressions: sets.New[string](), auditAnnotationValuesExpressions: sets.New[string](), + applyConfigurationExpressions: sets.New[string](), + jsonPatchExpressions: sets.New[string](), } } @@ -322,6 +328,22 @@ func findValidatingPolicyPreexistingExpressions(validatingPolicy *admissionregis return preexisting } +func findMutatingPolicyPreexistingExpressions(mutatingPolicy *admissionregistration.MutatingAdmissionPolicy) preexistingExpressions { + preexisting := newPreexistingExpressions() + for _, mc := range mutatingPolicy.Spec.MatchConditions { + preexisting.matchConditionExpressions.Insert(mc.Expression) + } + for _, v := range mutatingPolicy.Spec.Mutations { + if v.ApplyConfiguration != nil { + preexisting.applyConfigurationExpressions.Insert(v.ApplyConfiguration.Expression) + } + if v.JSONPatch != nil { + preexisting.jsonPatchExpressions.Insert(v.JSONPatch.Expression) + } + } + return preexisting +} + func validateMutatingWebhookConfiguration(e *admissionregistration.MutatingWebhookConfiguration, opts validationOptions) field.ErrorList { allErrors := genericvalidation.ValidateObjectMeta(&e.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata")) @@ -495,6 +517,19 @@ var supportedValidationPolicyReason = sets.NewString( string(metav1.StatusReasonRequestEntityTooLarge), ) +var supportedPatchType = sets.NewString( + string(admissionregistration.PatchTypeApplyConfiguration), + string(admissionregistration.PatchTypeJSONPatch), +) + +// MutatatingAdmissionPolicy does not support DELETE +var supportedMutatingOperations = sets.NewString( + string(admissionregistration.OperationAll), + string(admissionregistration.Create), + string(admissionregistration.Update), + string(admissionregistration.Connect), +) + func hasWildcardOperation(operations []admissionregistration.OperationType) bool { for _, o := range operations { if o == admissionregistration.OperationAll { @@ -588,10 +623,21 @@ func ignoreValidatingWebhookMatchConditions(new, old []admissionregistration.Val // ignoreValidatingAdmissionPolicyMatchConditions returns true if there have been no updates that could invalidate previously-valid match conditions func ignoreValidatingAdmissionPolicyMatchConditions(new, old *admissionregistration.ValidatingAdmissionPolicy) bool { - if !reflect.DeepEqual(new.Spec.ParamKind, old.Spec.ParamKind) { + if !equality.Semantic.DeepEqual(new.Spec.ParamKind, old.Spec.ParamKind) { return false } - if !reflect.DeepEqual(new.Spec.MatchConditions, old.Spec.MatchConditions) { + if !equality.Semantic.DeepEqual(new.Spec.MatchConditions, old.Spec.MatchConditions) { + return false + } + return true +} + +// ignoreMutatingAdmissionPolicyMatchConditions returns true if there have been no updates that could invalidate previously-valid match conditions +func ignoreMutatingAdmissionPolicyMatchConditions(new, old *admissionregistration.MutatingAdmissionPolicy) bool { + if !equality.Semantic.DeepEqual(new.Spec.ParamKind, old.Spec.ParamKind) { + return false + } + if !equality.Semantic.DeepEqual(new.Spec.MatchConditions, old.Spec.MatchConditions) { return false } return true @@ -1158,7 +1204,7 @@ func validateValidatingAdmissionPolicyBindingSpec(spec *admissionregistration.Va } } allErrors = append(allErrors, validateParamRef(spec.ParamRef, fldPath.Child("paramRef"))...) - allErrors = append(allErrors, validateMatchResources(spec.MatchResources, fldPath.Child("matchResouces"))...) + allErrors = append(allErrors, validateMatchResources(spec.MatchResources, fldPath.Child("matchResources"))...) allErrors = append(allErrors, validateValidationActions(spec.ValidationActions, fldPath.Child("validationActions"))...) return allErrors @@ -1328,3 +1374,195 @@ func isCELIdentifier(name string) bool { // | "var" | "void" | "while" return celIdentRegex.MatchString(name) && !celReserved.Has(name) } + +// ValidateMutatingAdmissionPolicyUpdate validates update of mutating admission policy +func ValidateMutatingAdmissionPolicyUpdate(newC, oldC *admissionregistration.MutatingAdmissionPolicy) field.ErrorList { + return validateMutatingAdmissionPolicy(newC, validationOptions{ + ignoreMatchConditions: ignoreMutatingAdmissionPolicyMatchConditions(newC, oldC), + preexistingExpressions: findMutatingPolicyPreexistingExpressions(oldC), + strictCostEnforcement: true, + }) +} + +// ValidateMutatingAdmissionPolicyBindingUpdate validates update of mutating admission policy +func ValidateMutatingAdmissionPolicyBindingUpdate(newC, oldC *admissionregistration.MutatingAdmissionPolicyBinding) field.ErrorList { + return validateMutatingAdmissionPolicyBinding(newC) +} + +// ValidateMutatingAdmissionPolicy validates a MutatingAdmissionPolicy before creation. +func ValidateMutatingAdmissionPolicy(p *admissionregistration.MutatingAdmissionPolicy) field.ErrorList { + return validateMutatingAdmissionPolicy(p, validationOptions{ignoreMatchConditions: false, strictCostEnforcement: true}) +} + +func validateMutatingAdmissionPolicy(p *admissionregistration.MutatingAdmissionPolicy, opts validationOptions) field.ErrorList { + allErrors := genericvalidation.ValidateObjectMeta(&p.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata")) + allErrors = append(allErrors, validateMutatingAdmissionPolicySpec(p.ObjectMeta, &p.Spec, opts, field.NewPath("spec"))...) + return allErrors +} + +func validateMutatingAdmissionPolicySpec(meta metav1.ObjectMeta, spec *admissionregistration.MutatingAdmissionPolicySpec, opts validationOptions, fldPath *field.Path) field.ErrorList { + var allErrors field.ErrorList + + compiler := createCompiler(true, true) + + if spec.FailurePolicy == nil { + allErrors = append(allErrors, field.Required(fldPath.Child("failurePolicy"), "")) + } else if !supportedFailurePolicies.Has(string(*spec.FailurePolicy)) { + allErrors = append(allErrors, field.NotSupported(fldPath.Child("failurePolicy"), *spec.FailurePolicy, supportedFailurePolicies.List())) + } + if spec.ParamKind != nil { + opts.allowParamsInMatchConditions = true + allErrors = append(allErrors, validateParamKind(*spec.ParamKind, fldPath.Child("paramKind"))...) + } + if spec.MatchConstraints == nil { + allErrors = append(allErrors, field.Required(fldPath.Child("matchConstraints"), "")) + } else { + allErrors = append(allErrors, validateMatchResources(spec.MatchConstraints, fldPath.Child("matchConstraints"))...) + // at least one resourceRule must be defined to provide type information + if len(spec.MatchConstraints.ResourceRules) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("matchConstraints", "resourceRules"), "")) + } + + // It is only possible to mutate create and update requests + for _, rule := range spec.MatchConstraints.ResourceRules { + for _, op := range rule.RuleWithOperations.Operations { + if !supportedMutatingOperations.Has(string(op)) { + allErrors = append(allErrors, field.NotSupported(fldPath.Child("matchConstraints", "resourceRules", "operations"), op, supportedMutatingOperations.List())) + } + } + } + } + if !opts.ignoreMatchConditions { + allErrors = append(allErrors, validateMatchConditions(spec.MatchConditions, opts, fldPath.Child("matchConditions"))...) + } + for i, variable := range spec.Variables { + allErrors = append(allErrors, validateVariable(compiler, &variable, spec.ParamKind, opts, fldPath.Child("variables").Index(i))...) + } + if len(spec.Mutations) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("mutations"), "mutations must contain at least one item")) + } else { + for i, mutation := range spec.Mutations { + allErrors = append(allErrors, validateMutation(compiler, &mutation, spec.ParamKind, opts, fldPath.Child("mutations").Index(i))...) + } + } + if len(spec.ReinvocationPolicy) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("reinvocationPolicy"), "")) + } else if !supportedReinvocationPolicies.Has(string(spec.ReinvocationPolicy)) { + allErrors = append(allErrors, field.NotSupported(fldPath.Child("reinvocationPolicy"), spec.ReinvocationPolicy, supportedReinvocationPolicies.List())) + } + return allErrors +} + +func validateMutation(compiler plugincel.Compiler, m *admissionregistration.Mutation, paramKind *admissionregistration.ParamKind, opts validationOptions, fldPath *field.Path) (allErrors field.ErrorList) { + if len(m.PatchType) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("patchType"), "")) + } else { + switch m.PatchType { + case admissionregistration.PatchTypeJSONPatch: + if m.JSONPatch == nil { + allErrors = append(allErrors, field.Required(fldPath.Child("jsonPatch"), "must be specified when patchType is JSONPatch")) + } else { + allErrors = append(allErrors, validateJSONPatch(compiler, m.JSONPatch, paramKind, opts, fldPath.Child("jsonPatch"))...) + } + if m.ApplyConfiguration != nil { + allErrors = append(allErrors, field.Invalid(fldPath.Child("applyConfiguration"), "{applyConfiguration}", "must not be specified when patchType is JSONPatch")) + } + case admissionregistration.PatchTypeApplyConfiguration: + if m.ApplyConfiguration == nil { + allErrors = append(allErrors, field.Required(fldPath.Child("applyConfiguration"), "must be specified when patchType is ApplyConfiguration")) + } else { + allErrors = append(allErrors, validateApplyConfiguration(compiler, m.ApplyConfiguration, paramKind, opts, fldPath.Child("applyConfiguration"))...) + } + if m.JSONPatch != nil { + allErrors = append(allErrors, field.Invalid(fldPath.Child("jsonPatch"), "{jsonPatch}", "must not be specified when patchType is ApplyConfiguration")) + } + default: + allErrors = append(allErrors, field.NotSupported(fldPath.Child("patchType"), m.PatchType, supportedPatchType.List())) + } + } + return allErrors +} + +func validateApplyConfiguration(compiler plugincel.Compiler, applyConfig *admissionregistration.ApplyConfiguration, paramKind *admissionregistration.ParamKind, opts validationOptions, fldPath *field.Path) (allErrors field.ErrorList) { + trimmedExpression := strings.TrimSpace(applyConfig.Expression) + if len(trimmedExpression) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("expression"), "")) + } else { + + envType := environment.NewExpressions + if opts.preexistingExpressions.applyConfigurationExpressions.Has(applyConfig.Expression) { + envType = environment.StoredExpressions + } + accessor := &mutating.ApplyConfigurationCondition{ + Expression: trimmedExpression, + } + opts := plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true, StrictCost: true, HasPatchTypes: true} + result := compiler.CompileCELExpression(accessor, opts, envType) + + if result.Error != nil { + allErrors = append(allErrors, convertCELErrorToValidationError(fldPath.Child("expression"), accessor, result.Error)) + } + } + return allErrors +} + +func validateJSONPatch(compiler plugincel.Compiler, jsonPatch *admissionregistration.JSONPatch, paramKind *admissionregistration.ParamKind, opts validationOptions, fldPath *field.Path) (allErrors field.ErrorList) { + trimmedExpression := strings.TrimSpace(jsonPatch.Expression) + if len(trimmedExpression) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("expression"), "")) + } else { + + envType := environment.NewExpressions + if opts.preexistingExpressions.applyConfigurationExpressions.Has(jsonPatch.Expression) { + envType = environment.StoredExpressions + } + accessor := &mutating.JSONPatchCondition{ + Expression: trimmedExpression, + } + opts := plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true, StrictCost: true, HasPatchTypes: true} + result := compiler.CompileCELExpression(accessor, opts, envType) + + if result.Error != nil { + allErrors = append(allErrors, convertCELErrorToValidationError(fldPath.Child("expression"), accessor, result.Error)) + } + } + return allErrors +} + +// ValidateMutatingAdmissionPolicyBinding validates a MutatingAdmissionPolicyBinding before create. +func ValidateMutatingAdmissionPolicyBinding(pb *admissionregistration.MutatingAdmissionPolicyBinding) field.ErrorList { + return validateMutatingAdmissionPolicyBinding(pb) +} + +func validateMutatingAdmissionPolicyBinding(pb *admissionregistration.MutatingAdmissionPolicyBinding) field.ErrorList { + allErrors := genericvalidation.ValidateObjectMeta(&pb.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata")) + allErrors = append(allErrors, validateMutatingAdmissionPolicyBindingSpec(&pb.Spec, field.NewPath("spec"))...) + + return allErrors +} + +func validateMutatingAdmissionPolicyBindingSpec(spec *admissionregistration.MutatingAdmissionPolicyBindingSpec, fldPath *field.Path) field.ErrorList { + var allErrors field.ErrorList + + if len(spec.PolicyName) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("policyName"), "")) + } else { + for _, msg := range genericvalidation.NameIsDNSSubdomain(spec.PolicyName, false) { + allErrors = append(allErrors, field.Invalid(fldPath.Child("policyName"), spec.PolicyName, msg)) + } + } + allErrors = append(allErrors, validateParamRef(spec.ParamRef, fldPath.Child("paramRef"))...) + allErrors = append(allErrors, validateMatchResources(spec.MatchResources, fldPath.Child("matchResources"))...) + if spec.MatchResources != nil { + // It is only possible to mutate create and update requests + for _, rule := range spec.MatchResources.ResourceRules { + for _, op := range rule.RuleWithOperations.Operations { + if !supportedMutatingOperations.Has(string(op)) { + allErrors = append(allErrors, field.NotSupported(fldPath.Child("matchResources", "resourceRules", "operations"), op, supportedMutatingOperations.List())) + } + } + } + } + + return allErrors +} diff --git a/pkg/apis/admissionregistration/validation/validation_test.go b/pkg/apis/admissionregistration/validation/validation_test.go index bc6c7331306..85dc825583a 100644 --- a/pkg/apis/admissionregistration/validation/validation_test.go +++ b/pkg/apis/admissionregistration/validation/validation_test.go @@ -2368,7 +2368,7 @@ func TestValidateValidatingAdmissionPolicy(t *testing.T) { }, expectedError: `Unsupported value: ""`, }, { - name: "operation must be either create/update/delete/connect", + name: "operation must be either create/update", config: &admissionregistration.ValidatingAdmissionPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "config", @@ -3209,7 +3209,7 @@ func TestValidateValidatingAdmissionPolicyUpdate(t *testing.T) { MatchConstraints: &admissionregistration.MatchResources{ ResourceRules: []admissionregistration.NamedRuleWithOperations{{ RuleWithOperations: admissionregistration.RuleWithOperations{ - Operations: []admissionregistration.OperationType{"*"}, + Operations: []admissionregistration.OperationType{"CREATE", "UPDATE"}, Rule: admissionregistration.Rule{ APIGroups: []string{"a"}, APIVersions: []string{"a"}, @@ -3495,6 +3495,48 @@ func validatingAdmissionPolicyWithExpressions( } } +func mutatingAdmissionPolicyWithExpressions( + matchConditions []admissionregistration.MatchCondition, + mutations []admissionregistration.Mutation) *admissionregistration.MutatingAdmissionPolicy { + return &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"*"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + }, + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Ignore") + return &r + }(), + MatchConditions: matchConditions, + Mutations: mutations, + }, + } +} + func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) { tests := []struct { name string @@ -3537,7 +3579,7 @@ func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) { }, }, }, - expectedError: `spec.matchResouces.matchPolicy: Unsupported value: "other": supported values: "Equivalent", "Exact"`, + expectedError: `spec.matchResources.matchPolicy: Unsupported value: "other": supported values: "Equivalent", "Exact"`, }, { name: "Operations must not be empty or nil", config: &admissionregistration.ValidatingAdmissionPolicyBinding{ @@ -3592,7 +3634,7 @@ func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) { }, }, }, - expectedError: `spec.matchResouces.resourceRules[0].operations: Required value, spec.matchResouces.resourceRules[1].operations: Required value, spec.matchResouces.excludeResourceRules[0].operations: Required value, spec.matchResouces.excludeResourceRules[1].operations: Required value`, + expectedError: `spec.matchResources.resourceRules[0].operations: Required value, spec.matchResources.resourceRules[1].operations: Required value, spec.matchResources.excludeResourceRules[0].operations: Required value, spec.matchResources.excludeResourceRules[1].operations: Required value`, }, { name: "\"\" is NOT a valid operation", config: &admissionregistration.ValidatingAdmissionPolicyBinding{ @@ -3765,7 +3807,7 @@ func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) { }, }, }, - expectedError: `spec.matchResouces.resourceRules[0].resources[1]: Invalid value: "a/x": if 'a/*' is present, must not specify a/x`, + expectedError: `spec.matchResources.resourceRules[0].resources[1]: Invalid value: "a/x": if 'a/*' is present, must not specify a/x`, }, { name: "resource a/* can mix with a", config: &admissionregistration.ValidatingAdmissionPolicyBinding{ @@ -3830,7 +3872,7 @@ func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) { }, }, }, - expectedError: `spec.matchResouces.resourceRules[0].resources[1]: Invalid value: "x/a": if '*/a' is present, must not specify x/a`, + expectedError: `spec.matchResources.resourceRules[0].resources[1]: Invalid value: "x/a": if '*/a' is present, must not specify x/a`, }, { name: "resource */* cannot mix with other resources", config: &admissionregistration.ValidatingAdmissionPolicyBinding{ @@ -3858,7 +3900,7 @@ func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) { }, }, }, - expectedError: `spec.matchResouces.resourceRules[0].resources: Invalid value: []string{"*/*", "a"}: if '*/*' is present, must not specify other resources`, + expectedError: `spec.matchResources.resourceRules[0].resources: Invalid value: []string{"*/*", "a"}: if '*/*' is present, must not specify other resources`, }, { name: "validationActions must be unique", config: &admissionregistration.ValidatingAdmissionPolicyBinding{ @@ -4194,3 +4236,1932 @@ func get65MatchConditions() []admissionregistration.MatchCondition { } return result } + +func TestValidateMutatingAdmissionPolicy(t *testing.T) { + applyConfigurationPatchType := admissionregistration.PatchTypeApplyConfiguration + tests := []struct { + name string + config *admissionregistration.MutatingAdmissionPolicy + expectedError string + }{{ + name: "metadata.name validation", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "!!!!", + }, + }, + expectedError: `metadata.name: Invalid value: "!!!!":`, + }, { + name: "failure policy validation", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("other") + return &r + }(), + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: `Object{ + spec: Object.spec{ + replicas: object.spec.replicas % 2 == 0?object.spec.replicas + 1:object.spec.replicas + } + }`, + }, + PatchType: applyConfigurationPatchType, + }}, + }, + }, + expectedError: `spec.failurePolicy: Unsupported value: "other": supported values: "Fail", "Ignore"`, + }, { + name: "unsupported expression type validation", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "1 < 2", + }, + PatchType: applyConfigurationPatchType, + }, + }, + }, + }, + expectedError: `spec.mutations[0].applyConfiguration.expression: Invalid value: "1 < 2": must evaluate to Object`, + }, { + name: "patchType validation", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: `Object{ + spec: Object.spec{ + replicas: object.spec.replicas % 2 == 0?object.spec.replicas + 1:object.spec.replicas + } + }`, + }, + PatchType: "other", + }, + }, + }, + }, + expectedError: `spec.mutations[0].patchType: Unsupported value: "other": supported values: "ApplyConfiguration", "JSONPatch"`, + }, { + name: "PatchType is JSONPatch but union member is ApplyConfiguration", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: `Object{ + spec: Object.spec{ + replicas: object.spec.replicas % 2 == 0?object.spec.replicas + 1:object.spec.replicas + } + }`, + }, + PatchType: "JSONPatch", + }, + }, + }, + }, + expectedError: `spec.mutations[0].applyConfiguration: Invalid value: "{applyConfiguration}": must not be specified when patchType is JSONPatch`, + }, { + name: "PatchType is ApplyConfiguration but union member is JSONPatch", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + JSONPatch: &admissionregistration.JSONPatch{ + Expression: `[ + JSONPatch{op: "replace", path: "/spec/repliacs", value: 1} + ]`, + }, + PatchType: "ApplyConfiguration", + }, + }, + }, + }, + expectedError: `spec.mutations[0].jsonPatch: Invalid value: "{jsonPatch}": must not be specified when patchType is ApplyConfiguration`, + }, { + name: "JSONPatch is empty", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + PatchType: "JSONPatch", + }, + }, + }, + }, + expectedError: `spec.mutations[0].jsonPatch: Required value: must be specified when patchType is JSONPatch`, + }, { + name: "JSONPatch has an empty value expression", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + JSONPatch: &admissionregistration.JSONPatch{ + Expression: ` `, + }, + PatchType: "JSONPatch", + }, + }, + }, + }, + expectedError: `spec.mutations[0].jsonPatch.expression: Required value`, + }, { + name: "invalid variable", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Variables: []admissionregistration.Variable{ + { + Name: "x", + Expression: "///", + }, + }, + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + JSONPatch: &admissionregistration.JSONPatch{ + Expression: `[ + JSONPatch{op: "add", path: "/spec/repliacs", value: variables.x} + ]`, + }, + PatchType: "JSONPatch", + }, + }, + }, + }, + expectedError: `spec.variables[0].expression: Invalid value: "///": compilation failed`, + }, { + name: "Reference to missing variable", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Variables: []admissionregistration.Variable{ + { + Name: "x", + Expression: "10 + 10", + }, + }, + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + JSONPatch: &admissionregistration.JSONPatch{ + Expression: `[ + JSONPatch{op: "add", path: "/spec/repliacs", value: variables.x + variables.y} + ]`, + }, + PatchType: "JSONPatch", + }, + }, + }, + }, + expectedError: `undefined field 'y'`, + }, { + name: "API version is required in ParamKind", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + ParamKind: &admissionregistration.ParamKind{ + Kind: "Example", + APIVersion: "test.example.com", + }, + }, + }, + expectedError: `spec.paramKind.apiVersion: Invalid value: "test.example.com"`, + }, { + name: "API kind is required in ParamKind", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + ParamKind: &admissionregistration.ParamKind{ + APIVersion: "test.example.com/v1", + }, + }, + }, + expectedError: `spec.paramKind.kind: Required value`, + }, { + name: "API version format in ParamKind", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + ParamKind: &admissionregistration.ParamKind{ + Kind: "Example", + APIVersion: "test.example.com/!!!", + }, + }, + }, + expectedError: `pec.paramKind.apiVersion: Invalid value: "!!!":`, + }, { + name: "API group format in ParamKind", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + ParamKind: &admissionregistration.ParamKind{ + APIVersion: "!!!/v1", + Kind: "ReplicaLimit", + }, + }, + }, + expectedError: `pec.paramKind.apiVersion: Invalid value: "!!!":`, + }, { + name: "Validations is required", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{}, + }, + + expectedError: `spec.failurePolicy: Required value, spec.matchConstraints: Required value, spec.mutations: Required value: mutations must contain at least one item`, + }, { + name: "MatchConstraints is required", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + }, + }, + expectedError: `spec.matchConstraints: Required value`, + }, { + name: "matchConstraints.resourceRules is required", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{}, + }, + }, + expectedError: `spec.matchConstraints.resourceRules: Required value`, + }, { + name: "matchConstraints.resourceRules has at least one explicit rule", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Rule: admissionregistration.Rule{}, + }, + ResourceNames: []string{"/./."}, + }}, + }, + }, + }, + expectedError: `spec.matchConstraints.resourceRules[0].apiVersions: Required value`, + }, { + name: "expression is required", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{ + { + PatchType: admissionregistration.PatchTypeApplyConfiguration, + ApplyConfiguration: &admissionregistration.ApplyConfiguration{}, + }, + }, + }, + }, + + expectedError: `spec.mutations[0].applyConfiguration.expression: Required value`, + }, { + name: "matchResources resourceNames check", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + ResourceNames: []string{"/./."}, + }}, + }, + }, + }, + expectedError: `spec.matchConstraints.resourceRules[0].resourceNames[0]: Invalid value: "/./."`, + }, { + name: "matchResources resourceNames cannot duplicate", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + ResourceNames: []string{"test", "test"}, + }}, + }, + }, + }, + expectedError: `spec.matchConstraints.resourceRules[0].resourceNames[1]: Duplicate value: "test"`, + }, { + name: "matchResources validation: matchPolicy", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("other") + return &r + }(), + }, + }, + }, + expectedError: `spec.matchConstraints.matchPolicy: Unsupported value: "other": supported values: "Equivalent", "Exact"`, + }, { + name: "Operations must not be empty or nil", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + MatchConstraints: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: nil, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + ExcludeResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: nil, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `spec.matchConstraints.resourceRules[0].operations: Required value, spec.matchConstraints.resourceRules[1].operations: Required value, spec.matchConstraints.excludeResourceRules[0].operations: Required value, spec.matchConstraints.excludeResourceRules[1].operations: Required value`, + }, { + name: "\"\" is NOT a valid operation", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE", ""}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `Unsupported value: ""`, + }, { + name: "operation must be either create/update", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"PATCH"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `Unsupported value: "PATCH"`, + }, { + name: "operation must not be delete", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"DELETE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `Unsupported value: "DELETE"`, + }, { + name: "wildcard operation cannot be mixed with other strings", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE", "*"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `if '*' is present, must not specify other operations`, + }, { + name: `resource "*" can co-exist with resources that have subresources`, + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*", "a/b", "a/*", "*/b"}, + }, + }, + }}, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + }, + }, + }, + }, { + name: `resource "*" cannot mix with resources that don't have subresources`, + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*", "a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `if '*' is present, must not specify other resources without subresources`, + }, { + name: "resource a/* cannot mix with a/x", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a/*", "a/x"}, + }, + }, + }}, + }, + }, + }, + expectedError: `spec.matchConstraints.resourceRules[0].resources[1]: Invalid value: "a/x": if 'a/*' is present, must not specify a/x`, + }, { + name: "resource a/* can mix with a", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a/*", "a"}, + }, + }, + }}, + }, + }, + }, + }, { + name: "resource */a cannot mix with x/a", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*/a", "x/a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `spec.matchConstraints.resourceRules[0].resources[1]: Invalid value: "x/a": if '*/a' is present, must not specify x/a`, + }, { + name: "resource */* cannot mix with other resources", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*/*", "a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `spec.matchConstraints.resourceRules[0].resources: Invalid value: []string{"*/*", "a"}: if '*/*' is present, must not specify other resources`, + }, { + name: "patchType required", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*/*"}, + }, + }, + }}, + }, + }, + }, + expectedError: `spec.mutations[0].patchType: Required value`, + }, { + name: "single match condition must have a name", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + MatchConditions: []admissionregistration.MatchCondition{{ + Expression: "true", + }}, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + }, + }, + expectedError: `spec.matchConditions[0].name: Required value`, + }, { + name: "match condition with parameters allowed", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ParamKind: &admissionregistration.ParamKind{ + Kind: "Foo", + APIVersion: "foobar/v1alpha1", + }, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE", "UPDATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + }, + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + MatchConditions: []admissionregistration.MatchCondition{{ + Name: "hasParams", + Expression: `params.foo == "okay"`, + }}, + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + }, + }, + expectedError: "", + }, { + name: "match condition with parameters not allowed if no param kind", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"*"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + }, + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + MatchConditions: []admissionregistration.MatchCondition{{ + Name: "hasParams", + Expression: `params.foo == "okay"`, + }}, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + }, + }, + expectedError: `undeclared reference to 'params'`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + errs := ValidateMutatingAdmissionPolicy(test.config) + err := errs.ToAggregate() + if err != nil { + if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { + t.Errorf("expected to contain %s, got %s", e, a) + } + } else { + if test.expectedError != "" { + t.Errorf("unexpected no error, expected to contain %s", test.expectedError) + } + } + }) + } +} + +func TestValidateMutatingAdmissionPolicyUpdate(t *testing.T) { + applyConfigurationPatchType := admissionregistration.PatchTypeApplyConfiguration + tests := []struct { + name string + oldconfig *admissionregistration.MutatingAdmissionPolicy + config *admissionregistration.MutatingAdmissionPolicy + expectedError string + }{{ + name: "should pass on valid new MutatingAdmissionPolicy", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + oldconfig: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + }, { + name: "should pass on valid new MutatingAdmissionPolicy with invalid old MutatingAdmissionPolicy", + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + MatchConstraints: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + oldconfig: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "!!!", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{}, + }, + }, { + name: "match conditions re-checked if paramKind changes", + oldconfig: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ParamKind: &admissionregistration.ParamKind{ + Kind: "Foo", + APIVersion: "foobar/v1alpha1", + }, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"*"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + }, + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + MatchConditions: []admissionregistration.MatchCondition{{ + Name: "hasParams", + Expression: `params.foo == "okay"`, + }}, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + }, + }, + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"*"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + }, + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + MatchConditions: []admissionregistration.MatchCondition{{ + Name: "hasParams", + Expression: `params.foo == "okay"`, + }}, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + }, + }, + expectedError: `undeclared reference to 'params'`, + }, { + name: "match conditions not re-checked if no change to paramKind or matchConditions", + oldconfig: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE", "UPDATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + }, + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + MatchConditions: []admissionregistration.MatchCondition{{ + Name: "hasParams", + Expression: `params.foo == "okay"`, + }}, + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + }, + }, + config: &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE", "UPDATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + }, + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Ignore") + return &r + }(), + MatchConditions: []admissionregistration.MatchCondition{{ + Name: "hasParams", + Expression: `params.foo == "okay"`, + }}, + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{{ + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: "Object{ spec: Object.spec{ replicas: 1 } }", + }, + PatchType: applyConfigurationPatchType, + }}, + }, + }, + expectedError: "", + }, + { + name: "matchCondition expressions that are changed must be compiled using the NewExpression environment", + oldconfig: mutatingAdmissionPolicyWithExpressions( + []admissionregistration.MatchCondition{ + { + Name: "checkEnvironmentMode", + Expression: `true`, + }, + }, + nil), + config: mutatingAdmissionPolicyWithExpressions( + []admissionregistration.MatchCondition{ + { + Name: "checkEnvironmentMode", + Expression: `test() == true`, + }, + }, + nil), + expectedError: `undeclared reference to 'test'`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + errs := ValidateMutatingAdmissionPolicyUpdate(test.config, test.oldconfig) + err := errs.ToAggregate() + if err != nil { + if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { + t.Errorf("expected to contain %s, got %s", e, a) + } + } else { + if test.expectedError != "" { + t.Errorf("unexpected no error, expected to contain %s", test.expectedError) + } + } + }) + + } +} + +func TestValidateMutatingAdmissionPolicyBinding(t *testing.T) { + tests := []struct { + name string + config *admissionregistration.MutatingAdmissionPolicyBinding + expectedError string + }{{ + name: "metadata.name validation", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "!!!!", + }, + }, + expectedError: `metadata.name: Invalid value: "!!!!":`, + }, { + name: "PolicyName is required", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{}, + }, + expectedError: `spec.policyName: Required value`, + }, { + name: "matchResources validation: matchPolicy", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("other") + return &r + }(), + }, + }, + }, + expectedError: `spec.matchResources.matchPolicy: Unsupported value: "other": supported values: "Equivalent", "Exact"`, + }, { + name: "Operations must not be empty or nil", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: nil, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + ExcludeResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: nil, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `spec.matchResources.resourceRules[0].operations: Required value, spec.matchResources.resourceRules[1].operations: Required value, spec.matchResources.excludeResourceRules[0].operations: Required value, spec.matchResources.excludeResourceRules[1].operations: Required value`, + }, { + name: "\"\" is NOT a valid operation", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE", ""}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `Unsupported value: ""`, + }, { + name: "operation must be either create/update", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"PATCH"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `Unsupported value: "PATCH"`, + }, { + name: "operation must not be DELETE", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"DELETE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `Unsupported value: "DELETE"`, + }, { + name: "wildcard operation cannot be mixed with other strings", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE", "*"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `if '*' is present, must not specify other operations`, + }, { + name: `resource "*" can co-exist with resources that have subresources`, + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*", "a/b", "a/*", "*/b"}, + }, + }, + }}, + }, + }, + }, + }, { + name: `resource "*" cannot mix with resources that don't have subresources`, + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*", "a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `if '*' is present, must not specify other resources without subresources`, + }, { + name: "resource a/* cannot mix with a/x", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a/*", "a/x"}, + }, + }, + }}, + }, + }, + }, + expectedError: `spec.matchResources.resourceRules[0].resources[1]: Invalid value: "a/x": if 'a/*' is present, must not specify a/x`, + }, { + name: "resource a/* can mix with a", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a/*", "a"}, + }, + }, + }}, + }, + }, + }, + }, { + name: "resource */a cannot mix with x/a", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*/a", "x/a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `spec.matchResources.resourceRules[0].resources[1]: Invalid value: "x/a": if '*/a' is present, must not specify x/a`, + }, { + name: "resource */* cannot mix with other resources", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*/*", "a"}, + }, + }, + }}, + }, + }, + }, + expectedError: `spec.matchResources.resourceRules[0].resources: Invalid value: []string{"*/*", "a"}: if '*/*' is present, must not specify other resources`, + }, { + name: "paramRef selector must not be set when name is set", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "label": "value", + }, + }, + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + }, + }, + expectedError: `spec.paramRef.name: Forbidden: name and selector are mutually exclusive, spec.paramRef.selector: Forbidden: name and selector are mutually exclusive`, + }, { + name: "paramRef parameterNotFoundAction must be set", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + }, + }, + }, + expectedError: "spec.paramRef.parameterNotFoundAction: Required value", + }, { + name: "paramRef parameterNotFoundAction must be an valid value", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.ParameterNotFoundActionType("invalid")), + }, + }, + }, + expectedError: `spec.paramRef.parameterNotFoundAction: Unsupported value: "invalid": supported values: "Deny", "Allow"`, + }, { + name: "paramRef one of name or selector", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + }, + }, + expectedError: `one of name or selector must be specified`, + }} + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + errs := ValidateMutatingAdmissionPolicyBinding(test.config) + err := errs.ToAggregate() + if err != nil { + if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { + t.Errorf("expected to contain %s, got %s", e, a) + } + } else { + if test.expectedError != "" { + t.Errorf("unexpected no error, expected to contain %s", test.expectedError) + } + } + }) + + } +} + +func TestValidateMutatingAdmissionPolicyBindingUpdate(t *testing.T) { + tests := []struct { + name string + oldconfig *admissionregistration.MutatingAdmissionPolicyBinding + config *admissionregistration.MutatingAdmissionPolicyBinding + expectedError string + }{{ + name: "should pass on valid new MutatingAdmissionPolicyBinding", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + oldconfig: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + }, { + name: "should pass on valid new MutatingAdmissionPolicyBinding with invalid old ValidatingAdmissionPolicyBinding", + config: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "xyzlimit-scale.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "xyzlimit-scale-setting.example.com", + ParameterNotFoundAction: ptr.To(admissionregistration.DenyAction), + }, + MatchResources: &admissionregistration.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{{ + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }}, + }, + }, + }, + oldconfig: &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "!!!", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{}, + }, + }} + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + errs := ValidateMutatingAdmissionPolicyBindingUpdate(test.config, test.oldconfig) + err := errs.ToAggregate() + if err != nil { + if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { + t.Errorf("expected to contain %s, got %s", e, a) + } + } else { + if test.expectedError != "" { + t.Errorf("unexpected no error, expected to contain %s", test.expectedError) + } + } + }) + + } +} diff --git a/pkg/kubeapiserver/default_storage_factory_builder.go b/pkg/kubeapiserver/default_storage_factory_builder.go index 125f5d8a8bc..ba679bfdaa7 100644 --- a/pkg/kubeapiserver/default_storage_factory_builder.go +++ b/pkg/kubeapiserver/default_storage_factory_builder.go @@ -27,6 +27,7 @@ import ( "k8s.io/apiserver/pkg/storage/storagebackend" version "k8s.io/component-base/version" "k8s.io/kubernetes/pkg/api/legacyscheme" + "k8s.io/kubernetes/pkg/apis/admissionregistration" "k8s.io/kubernetes/pkg/apis/apps" "k8s.io/kubernetes/pkg/apis/certificates" "k8s.io/kubernetes/pkg/apis/coordination" @@ -75,6 +76,8 @@ func NewStorageFactoryConfig() *StorageFactoryConfig { coordination.Resource("leasecandidates").WithVersion("v1alpha1"), networking.Resource("ipaddresses").WithVersion("v1beta1"), networking.Resource("servicecidrs").WithVersion("v1beta1"), + admissionregistration.Resource("mutatingadmissionpolicies").WithVersion("v1alpha1"), + admissionregistration.Resource("mutatingadmissionpolicybindings").WithVersion("v1alpha1"), certificates.Resource("clustertrustbundles").WithVersion("v1alpha1"), storage.Resource("volumeattributesclasses").WithVersion("v1beta1"), storagemigration.Resource("storagemigrations").WithVersion("v1alpha1"), diff --git a/pkg/printers/internalversion/printers.go b/pkg/printers/internalversion/printers.go index 00f23e34356..468c41ec5bf 100644 --- a/pkg/printers/internalversion/printers.go +++ b/pkg/printers/internalversion/printers.go @@ -596,6 +596,24 @@ func AddHandlers(h printers.PrintHandler) { _ = h.TableHandler(validatingAdmissionPolicyBinding, printValidatingAdmissionPolicyBinding) _ = h.TableHandler(validatingAdmissionPolicyBinding, printValidatingAdmissionPolicyBindingList) + mutatingAdmissionPolicy := []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, + {Name: "Mutations", Type: "integer", Description: "Mutation indicates the number of mutations rules defined in this configuration"}, + {Name: "ParamKind", Type: "string", Description: "ParamKind specifies the kind of resources used to parameterize this policy"}, + {Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]}, + } + _ = h.TableHandler(mutatingAdmissionPolicy, printMutatingAdmissionPolicy) + _ = h.TableHandler(mutatingAdmissionPolicy, printMutatingAdmissionPolicyList) + + mutatingAdmissionPolicyBinding := []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, + {Name: "PolicyName", Type: "string", Description: "PolicyName indicates the policy definition which the policy binding binded to"}, + {Name: "ParamRef", Type: "string", Description: "ParamRef indicates the param resource which sets the configration param"}, + {Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]}, + } + _ = h.TableHandler(mutatingAdmissionPolicyBinding, printMutatingAdmissionPolicyBinding) + _ = h.TableHandler(mutatingAdmissionPolicyBinding, printMutatingAdmissionPolicyBindingList) + flowSchemaColumnDefinitions := []metav1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, {Name: "PriorityLevel", Type: "string", Description: flowcontrolv1.PriorityLevelConfigurationReference{}.SwaggerDoc()["name"]}, @@ -1765,6 +1783,64 @@ func printValidatingAdmissionPolicyBindingList(list *admissionregistration.Valid return rows, nil } +func printMutatingAdmissionPolicy(obj *admissionregistration.MutatingAdmissionPolicy, options printers.GenerateOptions) ([]metav1.TableRow, error) { + row := metav1.TableRow{ + Object: runtime.RawExtension{Object: obj}, + } + paramKind := "" + if obj.Spec.ParamKind != nil { + paramKind = obj.Spec.ParamKind.APIVersion + "/" + obj.Spec.ParamKind.Kind + } + row.Cells = append(row.Cells, obj.Name, int64(len(obj.Spec.Mutations)), paramKind, translateTimestampSince(obj.CreationTimestamp)) + return []metav1.TableRow{row}, nil +} + +func printMutatingAdmissionPolicyList(list *admissionregistration.MutatingAdmissionPolicyList, options printers.GenerateOptions) ([]metav1.TableRow, error) { + rows := make([]metav1.TableRow, 0, len(list.Items)) + for i := range list.Items { + r, err := printMutatingAdmissionPolicy(&list.Items[i], options) + if err != nil { + return nil, err + } + rows = append(rows, r...) + } + return rows, nil +} + +func printMutatingAdmissionPolicyBinding(obj *admissionregistration.MutatingAdmissionPolicyBinding, options printers.GenerateOptions) ([]metav1.TableRow, error) { + row := metav1.TableRow{ + Object: runtime.RawExtension{Object: obj}, + } + paramName := "" + if pr := obj.Spec.ParamRef; pr != nil { + if len(pr.Name) > 0 { + if pr.Namespace != "" { + paramName = pr.Namespace + "/" + pr.Name + } else { + // Can't tell from here if param is cluster-scoped, so all + // params without names get * namespace + paramName = "*/" + pr.Name + } + } else if pr.Selector != nil { + paramName = pr.Selector.String() + } + } + row.Cells = append(row.Cells, obj.Name, obj.Spec.PolicyName, paramName, translateTimestampSince(obj.CreationTimestamp)) + return []metav1.TableRow{row}, nil +} + +func printMutatingAdmissionPolicyBindingList(list *admissionregistration.MutatingAdmissionPolicyBindingList, options printers.GenerateOptions) ([]metav1.TableRow, error) { + rows := make([]metav1.TableRow, 0, len(list.Items)) + for i := range list.Items { + r, err := printMutatingAdmissionPolicyBinding(&list.Items[i], options) + if err != nil { + return nil, err + } + rows = append(rows, r...) + } + return rows, nil +} + func printNamespace(obj *api.Namespace, options printers.GenerateOptions) ([]metav1.TableRow, error) { row := metav1.TableRow{ Object: runtime.RawExtension{Object: obj}, diff --git a/pkg/printers/internalversion/printers_test.go b/pkg/printers/internalversion/printers_test.go index 1df69067c5b..57d78f80ebc 100644 --- a/pkg/printers/internalversion/printers_test.go +++ b/pkg/printers/internalversion/printers_test.go @@ -7025,6 +7025,18 @@ func TestTableRowDeepCopyShouldNotPanic(t *testing.T) { return printValidatingAdmissionPolicyBinding(&admissionregistration.ValidatingAdmissionPolicyBinding{}, printers.GenerateOptions{}) }, }, + { + name: "MutatingAdmissionPolicy", + printer: func() ([]metav1.TableRow, error) { + return printMutatingAdmissionPolicy(&admissionregistration.MutatingAdmissionPolicy{}, printers.GenerateOptions{}) + }, + }, + { + name: "MutatingAdmissionPolicyBinding", + printer: func() ([]metav1.TableRow, error) { + return printMutatingAdmissionPolicyBinding(&admissionregistration.MutatingAdmissionPolicyBinding{}, printers.GenerateOptions{}) + }, + }, { name: "Namespace", printer: func() ([]metav1.TableRow, error) { diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicy/authz.go b/pkg/registry/admissionregistration/mutatingadmissionpolicy/authz.go new file mode 100644 index 00000000000..93f6f2ca4f2 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicy/authz.go @@ -0,0 +1,105 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mutatingadmissionpolicy + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authorization/authorizer" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + rbacregistry "k8s.io/kubernetes/pkg/registry/rbac" +) + +func (v *mutatingAdmissionPolicyStrategy) authorizeCreate(ctx context.Context, obj runtime.Object) error { + policy := obj.(*admissionregistration.MutatingAdmissionPolicy) + if policy.Spec.ParamKind == nil { + // no paramRef in new object + return nil + } + + return v.authorize(ctx, policy) +} + +func (v *mutatingAdmissionPolicyStrategy) authorizeUpdate(ctx context.Context, obj, old runtime.Object) error { + policy := obj.(*admissionregistration.MutatingAdmissionPolicy) + if policy.Spec.ParamKind == nil { + // no paramRef in new object + return nil + } + + oldPolicy := old.(*admissionregistration.MutatingAdmissionPolicy) + if oldPolicy.Spec.ParamKind != nil && *oldPolicy.Spec.ParamKind == *policy.Spec.ParamKind { + // identical paramKind to old object + return nil + } + + return v.authorize(ctx, policy) +} + +func (v *mutatingAdmissionPolicyStrategy) authorize(ctx context.Context, policy *admissionregistration.MutatingAdmissionPolicy) error { + if v.authorizer == nil || policy.Spec.ParamKind == nil { + return nil + } + + // for superuser, skip all checks + if rbacregistry.EscalationAllowed(ctx) { + return nil + } + + user, ok := genericapirequest.UserFrom(ctx) + if !ok { + return fmt.Errorf("cannot identify user to authorize read access to paramKind resources") + } + + paramKind := policy.Spec.ParamKind + // default to requiring permissions on all group/version/resources + resource, apiGroup, apiVersion := "*", "*", "*" + if gv, err := schema.ParseGroupVersion(paramKind.APIVersion); err == nil { + // we only need to authorize the parsed group/version + apiGroup = gv.Group + apiVersion = gv.Version + if gvr, err := v.resourceResolver.Resolve(gv.WithKind(paramKind.Kind)); err == nil { + // we only need to authorize the resolved resource + resource = gvr.Resource + } + } + + // require that the user can read (verb "get") the referred kind. + attrs := authorizer.AttributesRecord{ + User: user, + Verb: "get", + ResourceRequest: true, + Name: "*", + Namespace: "*", + APIGroup: apiGroup, + APIVersion: apiVersion, + Resource: resource, + } + + d, _, err := v.authorizer.Authorize(ctx, attrs) + if err != nil { + return err + } + if d != authorizer.DecisionAllow { + return fmt.Errorf(`user %v must have "get" permission on all objects of the referenced paramKind (kind=%s, apiVersion=%s)`, user, paramKind.Kind, paramKind.APIVersion) + } + return nil +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicy/authz_test.go b/pkg/registry/admissionregistration/mutatingadmissionpolicy/authz_test.go new file mode 100644 index 00000000000..cecadc792fe --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicy/authz_test.go @@ -0,0 +1,131 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mutatingadmissionpolicy + +import ( + "context" + "testing" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" +) + +func TestAuthorization(t *testing.T) { + for _, tc := range []struct { + name string + userInfo user.Info + obj *admissionregistration.MutatingAdmissionPolicy + auth AuthFunc + resourceResolver resolver.ResourceResolverFunc + expectErr bool + }{ + { + name: "superuser", + userInfo: &user.DefaultInfo{Groups: []string{user.SystemPrivilegedGroup}}, + expectErr: false, // success despite always-denying authorizer + obj: validMutatingAdmissionPolicy(), + auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + return authorizer.DecisionDeny, "", nil + }, + }, + { + name: "authorized", + userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}}, + obj: validMutatingAdmissionPolicy(), + auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + if a.GetResource() == "replicalimits" { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "", nil + }, + resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: "rules.example.com", + Version: "v1", + Resource: "replicalimits", + }, nil + }, + expectErr: false, + }, + { + name: "denied", + userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}}, + obj: validMutatingAdmissionPolicy(), + auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + if a.GetResource() == "configmaps" { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "", nil + }, + resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: "rules.example.com", + Version: "v1", + Resource: "replicalimits", + }, nil + }, + expectErr: true, + }, + { + name: "param not found", + userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}}, + obj: validMutatingAdmissionPolicy(), + auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + if a.GetResource() == "replicalimits" { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "", nil + }, + resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{}, &meta.NoKindMatchError{GroupKind: gvk.GroupKind(), SearchedVersions: []string{gvk.Version}} + }, + expectErr: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + strategy := NewStrategy(tc.auth, tc.resourceResolver) + t.Run("create", func(t *testing.T) { + ctx := request.WithUser(context.Background(), tc.userInfo) + errs := strategy.Validate(ctx, validMutatingAdmissionPolicy()) + if len(errs) > 0 != tc.expectErr { + t.Errorf("expected error: %v but got error: %v", tc.expectErr, errs) + } + }) + t.Run("update", func(t *testing.T) { + ctx := request.WithUser(context.Background(), tc.userInfo) + obj := validMutatingAdmissionPolicy() + objWithUpdatedParamKind := obj.DeepCopy() + objWithUpdatedParamKind.Spec.ParamKind.APIVersion += "1" + errs := strategy.ValidateUpdate(ctx, obj, objWithUpdatedParamKind) + if len(errs) > 0 != tc.expectErr { + t.Errorf("expected error: %v but got error: %v", tc.expectErr, errs) + } + }) + }) + } +} + +type AuthFunc func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) + +func (f AuthFunc) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + return f(ctx, a) +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicy/doc.go b/pkg/registry/admissionregistration/mutatingadmissionpolicy/doc.go new file mode 100644 index 00000000000..5a842f7af37 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicy/doc.go @@ -0,0 +1,17 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mutatingadmissionpolicy // import "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingadmissionpolicy" diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicy/storage/storage.go b/pkg/registry/admissionregistration/mutatingadmissionpolicy/storage/storage.go new file mode 100644 index 00000000000..e5ba64ba4ab --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicy/storage/storage.go @@ -0,0 +1,73 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package storage + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/registry/generic" + genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/printers" + printersinternal "k8s.io/kubernetes/pkg/printers/internalversion" + printerstorage "k8s.io/kubernetes/pkg/printers/storage" + "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingadmissionpolicy" + "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" +) + +// REST implements a RESTStorage for MutatingAdmissionPolicy against etcd +type REST struct { + *genericregistry.Store +} + +var groupResource = admissionregistration.Resource("mutatingadmissionpolicies") + +// NewREST returns the RESTStorage objects that will work against MutatingAdmissionPolicy. +func NewREST(optsGetter generic.RESTOptionsGetter, authorizer authorizer.Authorizer, resourceResolver resolver.ResourceResolver) (*REST, error) { + r := &REST{} + strategy := mutatingadmissionpolicy.NewStrategy(authorizer, resourceResolver) + store := &genericregistry.Store{ + NewFunc: func() runtime.Object { return &admissionregistration.MutatingAdmissionPolicy{} }, + NewListFunc: func() runtime.Object { return &admissionregistration.MutatingAdmissionPolicyList{} }, + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*admissionregistration.MutatingAdmissionPolicy).Name, nil + }, + DefaultQualifiedResource: groupResource, + SingularQualifiedResource: admissionregistration.Resource("mutatingadmissionpolicy"), + + CreateStrategy: strategy, + UpdateStrategy: strategy, + DeleteStrategy: strategy, + + TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)}, + } + options := &generic.StoreOptions{RESTOptions: optsGetter} + if err := store.CompleteWithOptions(options); err != nil { + return nil, err + } + r.Store = store + return r, nil +} + +// Implement CategoriesProvider +var _ rest.CategoriesProvider = &REST{} + +// Categories implements the CategoriesProvider interface. Returns a list of categories a resource is part of. +func (r *REST) Categories() []string { + return []string{"api-extensions"} +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicy/storage/storage_test.go b/pkg/registry/admissionregistration/mutatingadmissionpolicy/storage/storage_test.go new file mode 100644 index 00000000000..32b5c460a40 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicy/storage/storage_test.go @@ -0,0 +1,252 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package storage + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/registry/generic" + genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing" + etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" + "k8s.io/kubernetes/pkg/registry/registrytest" + + // Ensure that admissionregistration package is initialized. + _ "k8s.io/kubernetes/pkg/apis/admissionregistration/install" +) + +func TestCreate(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + configuration := validMutatingAdmissionPolicy() + test.TestCreate( + // valid + configuration, + // invalid + newMutatingAdmissionPolicy(""), + ) +} + +func TestUpdate(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + + test.TestUpdate( + // valid + validMutatingAdmissionPolicy(), + // updateFunc + func(obj runtime.Object) runtime.Object { + object := obj.(*admissionregistration.MutatingAdmissionPolicy) + object.Labels = map[string]string{"c": "d"} + return object + }, + // invalid updateFunc + func(obj runtime.Object) runtime.Object { + object := obj.(*admissionregistration.MutatingAdmissionPolicy) + object.Name = "" + return object + }, + ) +} + +func TestGet(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestGet(validMutatingAdmissionPolicy()) +} + +func TestList(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestList(validMutatingAdmissionPolicy()) +} + +func TestDelete(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestDelete(validMutatingAdmissionPolicy()) +} + +func TestWatch(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestWatch( + validMutatingAdmissionPolicy(), + []labels.Set{}, + []labels.Set{ + {"hoo": "bar"}, + }, + []fields.Set{ + {"metadata.name": "foo"}, + }, + []fields.Set{ + {"metadata.name": "nomatch"}, + }, + ) +} + +func validMutatingAdmissionPolicy() *admissionregistration.MutatingAdmissionPolicy { + return &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("Fail") + return &r + }(), + ParamKind: &admissionregistration.ParamKind{ + APIVersion: "rules.example.com/v1", + Kind: "ReplicaLimit", + }, + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + PatchType: admissionregistration.PatchTypeApplyConfiguration, + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: `Object{ + spec: Object.spec{ + replicas: object.spec.replicas % 2 == 0?object.spec.replicas + 1:object.spec.replicas + } + }`, + }, + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + }, + }, + } +} + +func newMutatingAdmissionPolicy(name string) *admissionregistration.MutatingAdmissionPolicy { + ignore := admissionregistration.Ignore + return &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{"foo": "bar"}, + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ParamKind: &admissionregistration.ParamKind{ + APIVersion: "rules.example.com/v1", + Kind: "ReplicaLimit", + }, + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + PatchType: admissionregistration.PatchTypeApplyConfiguration, + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: `Object{ + spec: Object.spec{ + replicas: object.spec.replicas % 2 == 0?object.spec.replicas + 1:object.spec.replicas + } + }`, + }, + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, + FailurePolicy: &ignore, + }, + } +} + +func newInsecureStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) { + return newStorage(t, nil, replicaLimitsResolver) +} + +func newStorage(t *testing.T, authorizer authorizer.Authorizer, resourceResolver resolver.ResourceResolver) (*REST, *etcd3testing.EtcdTestServer) { + etcdStorage, server := registrytest.NewEtcdStorageForResource(t, admissionregistration.Resource("mutatingadmissionpolicies")) + restOptions := generic.RESTOptions{ + StorageConfig: etcdStorage, + Decorator: generic.UndecoratedStorage, + DeleteCollectionWorkers: 1, + ResourcePrefix: "mutatingadmissionpolicies"} + storage, err := NewREST(restOptions, authorizer, resourceResolver) + if err != nil { + t.Fatalf("unexpected error from REST storage: %v", err) + } + return storage, server +} + +func TestCategories(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + expected := []string{"api-extensions"} + registrytest.AssertCategories(t, storage, expected) +} + +var replicaLimitsResolver resolver.ResourceResolverFunc = func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: "rules.example.com", + Version: "v1", + Resource: "replicalimits", + }, nil +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicy/strategy.go b/pkg/registry/admissionregistration/mutatingadmissionpolicy/strategy.go new file mode 100644 index 00000000000..698eb8adf69 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicy/strategy.go @@ -0,0 +1,122 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mutatingadmissionpolicy + +import ( + "context" + + apiequality "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/storage/names" + "k8s.io/kubernetes/pkg/api/legacyscheme" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/apis/admissionregistration/validation" + "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" +) + +// mutatingAdmissionPolicyStrategy implements verification logic for MutatingAdmissionPolicy. +type mutatingAdmissionPolicyStrategy struct { + runtime.ObjectTyper + names.NameGenerator + authorizer authorizer.Authorizer + resourceResolver resolver.ResourceResolver +} + +// NewStrategy is the default logic that applies when creating and updating MutatingAdmissionPolicy objects. +func NewStrategy(authorizer authorizer.Authorizer, resourceResolver resolver.ResourceResolver) *mutatingAdmissionPolicyStrategy { + return &mutatingAdmissionPolicyStrategy{ + ObjectTyper: legacyscheme.Scheme, + NameGenerator: names.SimpleNameGenerator, + authorizer: authorizer, + resourceResolver: resourceResolver, + } +} + +// NamespaceScoped returns false because MutatingAdmissionPolicy is cluster-scoped resource. +func (v *mutatingAdmissionPolicyStrategy) NamespaceScoped() bool { + return false +} + +// PrepareForCreate clears the status of an MutatingAdmissionPolicy before creation. +func (v *mutatingAdmissionPolicyStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { + ic := obj.(*admissionregistration.MutatingAdmissionPolicy) + ic.Generation = 1 +} + +// PrepareForUpdate clears fields that are not allowed to be set by end users on update. +func (v *mutatingAdmissionPolicyStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { + newIC := obj.(*admissionregistration.MutatingAdmissionPolicy) + oldIC := old.(*admissionregistration.MutatingAdmissionPolicy) + + // Any changes to the spec increment the generation number, any changes to the + // status should reflect the generation number of the corresponding object. + // See metav1.ObjectMeta description for more information on Generation. + if !apiequality.Semantic.DeepEqual(oldIC.Spec, newIC.Spec) { + newIC.Generation = oldIC.Generation + 1 + } +} + +// Validate validates a new MutatingAdmissionPolicy. +func (v *mutatingAdmissionPolicyStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { + errs := validation.ValidateMutatingAdmissionPolicy(obj.(*admissionregistration.MutatingAdmissionPolicy)) + if len(errs) == 0 { + // if the object is well-formed, also authorize the paramKind + if err := v.authorizeCreate(ctx, obj); err != nil { + errs = append(errs, field.Forbidden(field.NewPath("spec", "paramKind"), err.Error())) + } + } + return errs +} + +// WarningsOnCreate returns warnings for the creation of the given object. +func (v *mutatingAdmissionPolicyStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { + return nil +} + +// Canonicalize normalizes the object after validation. +func (v *mutatingAdmissionPolicyStrategy) Canonicalize(obj runtime.Object) { +} + +// AllowCreateOnUpdate is false for MutatingAdmissionPolicy; this means you may not create one with a PUT request. +func (v *mutatingAdmissionPolicyStrategy) AllowCreateOnUpdate() bool { + return false +} + +// ValidateUpdate is the default update validation for an end user. +func (v *mutatingAdmissionPolicyStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { + errs := validation.ValidateMutatingAdmissionPolicyUpdate(obj.(*admissionregistration.MutatingAdmissionPolicy), old.(*admissionregistration.MutatingAdmissionPolicy)) + if len(errs) == 0 { + // if the object is well-formed, also authorize the paramKind + if err := v.authorizeUpdate(ctx, obj, old); err != nil { + errs = append(errs, field.Forbidden(field.NewPath("spec", "paramKind"), err.Error())) + } + } + return errs +} + +// WarningsOnUpdate returns warnings for the given update. +func (v *mutatingAdmissionPolicyStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { + return nil +} + +// AllowUnconditionalUpdate is the default update policy for MutatingAdmissionPolicy objects. Status update should +// only be allowed if version match. +func (v *mutatingAdmissionPolicyStrategy) AllowUnconditionalUpdate() bool { + return false +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicy/strategy_test.go b/pkg/registry/admissionregistration/mutatingadmissionpolicy/strategy_test.go new file mode 100644 index 00000000000..2b181cf74a5 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicy/strategy_test.go @@ -0,0 +1,114 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mutatingadmissionpolicy + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" +) + +func TestMutatingAdmissionPolicyStrategy(t *testing.T) { + strategy := NewStrategy(nil, replicaLimitsResolver) + ctx := genericapirequest.NewDefaultContext() + if strategy.NamespaceScoped() { + t.Error("MutatingAdmissionPolicy strategy must be cluster scoped") + } + if strategy.AllowCreateOnUpdate() { + t.Errorf("MutatingAdmissionPolicy should not allow create on update") + } + + configuration := validMutatingAdmissionPolicy() + strategy.PrepareForCreate(ctx, configuration) + errs := strategy.Validate(ctx, configuration) + if len(errs) != 0 { + t.Errorf("Unexpected error mutating %v", errs) + } + invalidConfiguration := &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: ""}, + } + strategy.PrepareForUpdate(ctx, invalidConfiguration, configuration) + errs = strategy.ValidateUpdate(ctx, invalidConfiguration, configuration) + if len(errs) == 0 { + t.Errorf("Expected a validation error") + } +} + +var replicaLimitsResolver resolver.ResourceResolverFunc = func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: "rules.example.com", + Version: "v1", + Resource: "replicalimits", + }, nil +} + +func validMutatingAdmissionPolicy() *admissionregistration.MutatingAdmissionPolicy { + ignore := admissionregistration.Ignore + return &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ParamKind: &admissionregistration.ParamKind{ + Kind: "ReplicaLimit", + APIVersion: "rules.example.com/v1", + }, + ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy, + Mutations: []admissionregistration.Mutation{ + { + PatchType: admissionregistration.PatchTypeApplyConfiguration, + ApplyConfiguration: &admissionregistration.ApplyConfiguration{ + Expression: `Object{ + spec: Object.spec{ + replicas: object.spec.replicas % 2 == 0?object.spec.replicas + 1:object.spec.replicas + } + }`, + }, + }, + }, + MatchConstraints: &admissionregistration.MatchResources{ + MatchPolicy: func() *admissionregistration.MatchPolicyType { + r := admissionregistration.MatchPolicyType("Exact") + return &r + }(), + ObjectSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + ResourceRules: []admissionregistration.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistration.RuleWithOperations{ + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, + FailurePolicy: &ignore, + }, + } +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/authz.go b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/authz.go new file mode 100644 index 00000000000..474b7656721 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/authz.go @@ -0,0 +1,137 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mutatingadmissionpolicybinding + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authorization/authorizer" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + rbacregistry "k8s.io/kubernetes/pkg/registry/rbac" +) + +func (v *mutatingAdmissionPolicyBindingStrategy) authorizeCreate(ctx context.Context, obj runtime.Object) error { + binding := obj.(*admissionregistration.MutatingAdmissionPolicyBinding) + if binding.Spec.ParamRef == nil { + // no paramRef in new object + return nil + } + + return v.authorize(ctx, binding) +} + +func (v *mutatingAdmissionPolicyBindingStrategy) authorizeUpdate(ctx context.Context, obj, old runtime.Object) error { + binding := obj.(*admissionregistration.MutatingAdmissionPolicyBinding) + if binding.Spec.ParamRef == nil { + // no paramRef in new object + return nil + } + + oldBinding := old.(*admissionregistration.MutatingAdmissionPolicyBinding) + if oldBinding.Spec.ParamRef != nil && *oldBinding.Spec.ParamRef == *binding.Spec.ParamRef && oldBinding.Spec.PolicyName == binding.Spec.PolicyName { + // identical paramRef and policy to old object + return nil + } + + return v.authorize(ctx, binding) +} + +func (v *mutatingAdmissionPolicyBindingStrategy) authorize(ctx context.Context, binding *admissionregistration.MutatingAdmissionPolicyBinding) error { + if v.resourceResolver == nil { + return fmt.Errorf(`unexpected internal error: resourceResolver is nil`) + } + if v.authorizer == nil || binding.Spec.ParamRef == nil { + return nil + } + + // for superuser, skip all checks + if rbacregistry.EscalationAllowed(ctx) { + return nil + } + + user, ok := genericapirequest.UserFrom(ctx) + if !ok { + return fmt.Errorf("cannot identify user to authorize read access to paramRef object") + } + + // default to requiring permissions on all group/version/resources + resource, apiGroup, apiVersion := "*", "*", "*" + + var policyErr, gvParseErr, gvrResolveErr error + + var policy *admissionregistration.MutatingAdmissionPolicy + policy, policyErr = v.policyGetter.GetMutatingAdmissionPolicy(ctx, binding.Spec.PolicyName) + if policyErr == nil && policy.Spec.ParamKind != nil { + paramKind := policy.Spec.ParamKind + var gv schema.GroupVersion + gv, gvParseErr = schema.ParseGroupVersion(paramKind.APIVersion) + if gvParseErr == nil { + // we only need to authorize the parsed group/version + apiGroup = gv.Group + apiVersion = gv.Version + var gvr schema.GroupVersionResource + gvr, gvrResolveErr = v.resourceResolver.Resolve(gv.WithKind(paramKind.Kind)) + if gvrResolveErr == nil { + // we only need to authorize the resolved resource + resource = gvr.Resource + } + } + } + + var attrs authorizer.AttributesRecord + + paramRef := binding.Spec.ParamRef + verb := "get" + + if len(paramRef.Name) == 0 { + verb = "list" + } + + attrs = authorizer.AttributesRecord{ + User: user, + Verb: verb, + ResourceRequest: true, + Name: paramRef.Name, + Namespace: paramRef.Namespace, // if empty, no namespace indicates get across all namespaces + APIGroup: apiGroup, + APIVersion: apiVersion, + Resource: resource, + } + + d, _, err := v.authorizer.Authorize(ctx, attrs) + if err != nil { + return err + } + if d != authorizer.DecisionAllow { + if policyErr != nil { + return fmt.Errorf(`unable to get policy %s to determine minimum required permissions and user %v does not have "%v" permission for all groups, versions and resources`, binding.Spec.PolicyName, user, verb) + } + if gvParseErr != nil { + return fmt.Errorf(`unable to parse paramKind %v to determine minimum required permissions and user %v does not have "%v" permission for all groups, versions and resources`, policy.Spec.ParamKind, user, verb) + } + if gvrResolveErr != nil { + return fmt.Errorf(`unable to resolve paramKind %v to determine minimum required permissions and user %v does not have "%v" permission for all groups, versions and resources`, policy.Spec.ParamKind, user, verb) + } + return fmt.Errorf(`user %v does not have "%v" permission on the object referenced by paramRef`, verb, user) + } + + return nil +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/authz_test.go b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/authz_test.go new file mode 100644 index 00000000000..b8229782d13 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/authz_test.go @@ -0,0 +1,233 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mutatingadmissionpolicybinding + +import ( + "context" + "errors" + "fmt" + "strings" + "testing" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" +) + +func TestAuthorization(t *testing.T) { + for _, tc := range []struct { + name string + userInfo user.Info + auth AuthFunc + policyGetter PolicyGetterFunc + resourceResolver resolver.ResourceResolverFunc + expectErrContains string + }{ + { + name: "superuser", // success despite always-denying authorizer + userInfo: &user.DefaultInfo{Groups: []string{user.SystemPrivilegedGroup}}, + auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + return authorizer.DecisionDeny, "", nil + }, + }, + { + name: "authorized", + userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}}, + auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + if a.GetResource() == "configmaps" { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "", nil + }, + policyGetter: func(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) { + return &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "replicalimit-policy.example.com"}, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ParamKind: &admissionregistration.ParamKind{Kind: "ConfigMap", APIVersion: "v1"}, + }, + }, nil + }, + resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "configmaps", + }, nil + }, + }, + { + name: "denied", + userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}}, + auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + if a.GetResource() == "configmaps" { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "", nil + }, + policyGetter: func(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) { + return &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "replicalimit-policy.example.com"}, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ParamKind: &admissionregistration.ParamKind{Kind: "Params", APIVersion: "foo.example.com/v1"}, + }, + }, nil + }, + resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: "foo.example.com", + Version: "v1", + Resource: "params", + }, nil + }, + expectErrContains: "permission on the object referenced by paramRef", + }, + { + name: "unable to parse paramRef", + userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}}, + auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + if a.GetResource() == "configmaps" { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "", nil + }, + policyGetter: func(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) { + return &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "replicalimit-policy.example.com"}, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ParamKind: &admissionregistration.ParamKind{Kind: "Params", APIVersion: "foo.example.com/v1"}, + }, + }, nil + }, + resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "configmaps", + }, nil + }, + expectErrContains: "unable to parse paramKind &{foo.example.com/v1 Params} to determine minimum required permissions", + }, + { + name: "unable to resolve param", + userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}}, + auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + if a.GetResource() == "configmaps" { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "", nil + }, + policyGetter: func(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) { + return &admissionregistration.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "replicalimit-policy.example.com"}, + Spec: admissionregistration.MutatingAdmissionPolicySpec{ + ParamKind: &admissionregistration.ParamKind{Kind: "Params", APIVersion: "foo.example.com/v1"}, + }, + }, nil + }, + resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{}, &meta.NoKindMatchError{GroupKind: gvk.GroupKind(), SearchedVersions: []string{gvk.Version}} + }, + expectErrContains: "unable to resolve paramKind &{foo.example.com/v1 Params} to determine minimum required permissions", + }, + { + name: "unable to get policy", + userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}}, + auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + if a.GetResource() == "configmaps" { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "", nil + }, + policyGetter: func(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) { + return nil, fmt.Errorf("no such policy") + }, + resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "configmaps", + }, nil + }, + expectErrContains: "unable to get policy replicalimit-policy.example.com to determine minimum required permissions", + }, + } { + t.Run(tc.name, func(t *testing.T) { + strategy := NewStrategy(tc.auth, tc.policyGetter, tc.resourceResolver) + t.Run("create", func(t *testing.T) { + ctx := request.WithUser(context.Background(), tc.userInfo) + for _, obj := range validPolicyBindings() { + errs := strategy.Validate(ctx, obj) + if len(errs) > 0 && !strings.Contains(errors.Join(errs.ToAggregate().Errors()...).Error(), tc.expectErrContains) { + t.Errorf("expected error to contain: %v but got error: %v", tc.expectErrContains, errs) + } + } + }) + t.Run("update", func(t *testing.T) { + ctx := request.WithUser(context.Background(), tc.userInfo) + for _, obj := range validPolicyBindings() { + objWithChangedParamRef := obj.DeepCopy() + if pr := objWithChangedParamRef.Spec.ParamRef; pr != nil { + if len(pr.Name) > 0 { + pr.Name = "changed" + } + + if pr.Selector != nil { + pr.Selector = &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "changed": "value", + }, + } + } + + if len(pr.Namespace) > 0 { + pr.Namespace = "othernamespace" + } + + if pr.ParameterNotFoundAction == nil || *pr.ParameterNotFoundAction == admissionregistration.AllowAction { + v := admissionregistration.DenyAction + pr.ParameterNotFoundAction = &v + } else { + v := admissionregistration.AllowAction + pr.ParameterNotFoundAction = &v + } + } + errs := strategy.ValidateUpdate(ctx, obj, objWithChangedParamRef) + if len(errs) > 0 && !strings.Contains(errors.Join(errs.ToAggregate().Errors()...).Error(), tc.expectErrContains) { + t.Errorf("expected error to contain: %v but got error: %v", tc.expectErrContains, errs) + } + } + }) + }) + } +} + +type AuthFunc func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) + +func (f AuthFunc) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + return f(ctx, a) +} + +type PolicyGetterFunc func(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) + +func (f PolicyGetterFunc) GetMutatingAdmissionPolicy(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) { + return f(ctx, name) +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/doc.go b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/doc.go new file mode 100644 index 00000000000..b19a2185a08 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/doc.go @@ -0,0 +1,17 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mutatingadmissionpolicybinding // import "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingadmissionpolicybinding" diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/storage/storage.go b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/storage/storage.go new file mode 100644 index 00000000000..146ae24b146 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/storage/storage.go @@ -0,0 +1,94 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package storage + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/registry/generic" + genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/printers" + printersinternal "k8s.io/kubernetes/pkg/printers/internalversion" + printerstorage "k8s.io/kubernetes/pkg/printers/storage" + mutatingadmissionpolicybinding "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingadmissionpolicybinding" + "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" +) + +// REST implements a RESTStorage for policyBinding against etcd +type REST struct { + *genericregistry.Store +} + +var groupResource = admissionregistration.Resource("mutatingadmissionpolicybindings") + +// NewREST returns a RESTStorage object that will work against policyBinding. +func NewREST(optsGetter generic.RESTOptionsGetter, authorizer authorizer.Authorizer, policyGetter PolicyGetter, resourceResolver resolver.ResourceResolver) (*REST, error) { + r := &REST{} + strategy := mutatingadmissionpolicybinding.NewStrategy(authorizer, policyGetter, resourceResolver) + store := &genericregistry.Store{ + NewFunc: func() runtime.Object { return &admissionregistration.MutatingAdmissionPolicyBinding{} }, + NewListFunc: func() runtime.Object { return &admissionregistration.MutatingAdmissionPolicyBindingList{} }, + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*admissionregistration.MutatingAdmissionPolicyBinding).Name, nil + }, + DefaultQualifiedResource: groupResource, + SingularQualifiedResource: admissionregistration.Resource("mutatingadmissionpolicybinding"), + + CreateStrategy: strategy, + UpdateStrategy: strategy, + DeleteStrategy: strategy, + + TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)}, + } + options := &generic.StoreOptions{RESTOptions: optsGetter} + if err := store.CompleteWithOptions(options); err != nil { + return nil, err + } + r.Store = store + return r, nil +} + +// Implement CategoriesProvider +var _ rest.CategoriesProvider = &REST{} + +// Categories implements the CategoriesProvider interface. Returns a list of categories a resource is part of. +func (r *REST) Categories() []string { + return []string{"api-extensions"} +} + +type PolicyGetter interface { + // GetMutatingAdmissionPolicy returns a GetMutatingAdmissionPolicy + // by its name. There is no namespace because it is cluster-scoped. + GetMutatingAdmissionPolicy(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) +} + +type DefaultPolicyGetter struct { + Getter rest.Getter +} + +func (g *DefaultPolicyGetter) GetMutatingAdmissionPolicy(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) { + p, err := g.Getter.Get(ctx, name, &metav1.GetOptions{}) + if err != nil { + return nil, err + } + return p.(*admissionregistration.MutatingAdmissionPolicy), err +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/storage/storage_test.go b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/storage/storage_test.go new file mode 100644 index 00000000000..1d406059884 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/storage/storage_test.go @@ -0,0 +1,260 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package storage + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/registry/generic" + genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing" + etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing" + + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" + "k8s.io/kubernetes/pkg/registry/registrytest" + + // Ensure that admissionregistration package is initialized. + _ "k8s.io/kubernetes/pkg/apis/admissionregistration/install" +) + +func TestCreate(t *testing.T) { + for _, configuration := range validPolicyBindings() { + t.Run(configuration.Name, func(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + + test.TestCreate( + // valid + configuration, + // invalid + newPolicyBinding(""), + ) + }) + } +} + +func TestUpdate(t *testing.T) { + for _, b := range validPolicyBindings() { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + t.Run(b.Name, func(t *testing.T) { + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestUpdate( + // valid + b, + // updateFunc + func(obj runtime.Object) runtime.Object { + object := obj.(*admissionregistration.MutatingAdmissionPolicyBinding) + object.Labels = map[string]string{"c": "d"} + return object + }, + // invalid updateFunc + func(obj runtime.Object) runtime.Object { + object := obj.(*admissionregistration.MutatingAdmissionPolicyBinding) + object.Name = "" + return object + }, + ) + }) + } +} + +func TestGet(t *testing.T) { + for _, b := range validPolicyBindings() { + t.Run(b.Name, func(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestGet(b) + }) + } +} + +func TestList(t *testing.T) { + for _, b := range validPolicyBindings() { + t.Run(b.Name, func(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestList(b) + }) + } +} + +func TestDelete(t *testing.T) { + for _, b := range validPolicyBindings() { + t.Run(b.Name, func(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestDelete(b) + }) + } +} + +func TestWatch(t *testing.T) { + for _, b := range validPolicyBindings() { + t.Run(b.Name, func(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestWatch( + b, + []labels.Set{}, + []labels.Set{ + {"hoo": "bar"}, + }, + []fields.Set{ + {"metadata.name": b.Name}, + }, + []fields.Set{ + {"metadata.name": "nomatch"}, + }, + ) + }) + } +} + +func validPolicyBindings() []*admissionregistration.MutatingAdmissionPolicyBinding { + denyAction := admissionregistration.DenyAction + return []*admissionregistration.MutatingAdmissionPolicyBinding{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "replicalimit-policy.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "replica-limit-test.example.com", + ParameterNotFoundAction: &denyAction, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-clusterwide", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "replicalimit-policy.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "replica-limit-test.example.com", + Namespace: "default", + ParameterNotFoundAction: &denyAction, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-selector", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "replicalimit-policy.example.com", + ParamRef: &admissionregistration.ParamRef{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "label": "value", + }, + }, + ParameterNotFoundAction: &denyAction, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-selector-clusterwide", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "replicalimit-policy.example.com", + ParamRef: &admissionregistration.ParamRef{ + Namespace: "mynamespace", + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "label": "value", + }, + }, + ParameterNotFoundAction: &denyAction, + }, + }, + }, + } +} + +func newPolicyBinding(name string) *admissionregistration.MutatingAdmissionPolicyBinding { + return &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{"foo": "bar"}, + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "replicalimit-policy.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "param-test", + Namespace: "default", + }, + MatchResources: &admissionregistration.MatchResources{}, + }, + } +} + +func newInsecureStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) { + return newStorage(t, nil, nil, replicaLimitsResolver) +} + +func newStorage(t *testing.T, authorizer authorizer.Authorizer, policyGetter PolicyGetter, resourceResolver resolver.ResourceResolver) (*REST, *etcd3testing.EtcdTestServer) { + etcdStorage, server := registrytest.NewEtcdStorageForResource(t, admissionregistration.Resource("mutatingadmissionpolicybindings")) + restOptions := generic.RESTOptions{ + StorageConfig: etcdStorage, + Decorator: generic.UndecoratedStorage, + DeleteCollectionWorkers: 1, + ResourcePrefix: "mutatingadmissionpolicybindings"} + storage, err := NewREST(restOptions, authorizer, policyGetter, resourceResolver) + if err != nil { + t.Fatalf("unexpected error from REST storage: %v", err) + } + return storage, server +} + +func TestCategories(t *testing.T) { + storage, server := newInsecureStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + expected := []string{"api-extensions"} + registrytest.AssertCategories(t, storage, expected) +} + +var replicaLimitsResolver resolver.ResourceResolverFunc = func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: "rules.example.com", + Version: "v1", + Resource: "replicalimits", + }, nil +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/strategy.go b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/strategy.go new file mode 100644 index 00000000000..16be56cbc8a --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/strategy.go @@ -0,0 +1,130 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mutatingadmissionpolicybinding + +import ( + "context" + + apiequality "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/storage/names" + "k8s.io/kubernetes/pkg/api/legacyscheme" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/apis/admissionregistration/validation" + "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" +) + +// MutatingAdmissionPolicyBindingStrategy implements verification logic for MutatingAdmissionPolicyBinding. +type mutatingAdmissionPolicyBindingStrategy struct { + runtime.ObjectTyper + names.NameGenerator + authorizer authorizer.Authorizer + policyGetter PolicyGetter + resourceResolver resolver.ResourceResolver +} + +type PolicyGetter interface { + // GetMutatingAdmissionPolicy returns a GetMutatingAdmissionPolicy + // by its name. There is no namespace because it is cluster-scoped. + GetMutatingAdmissionPolicy(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) +} + +// NewStrategy is the default logic that applies when creating and updating MutatingAdmissionPolicyBinding objects. +func NewStrategy(authorizer authorizer.Authorizer, policyGetter PolicyGetter, resourceResolver resolver.ResourceResolver) *mutatingAdmissionPolicyBindingStrategy { + return &mutatingAdmissionPolicyBindingStrategy{ + ObjectTyper: legacyscheme.Scheme, + NameGenerator: names.SimpleNameGenerator, + authorizer: authorizer, + policyGetter: policyGetter, + resourceResolver: resourceResolver, + } +} + +// NamespaceScoped returns false because MutatingAdmissionPolicyBinding is cluster-scoped resource. +func (v *mutatingAdmissionPolicyBindingStrategy) NamespaceScoped() bool { + return false +} + +// PrepareForCreate clears the status of an MutatingAdmissionPolicyBinding before creation. +func (v *mutatingAdmissionPolicyBindingStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { + ic := obj.(*admissionregistration.MutatingAdmissionPolicyBinding) + ic.Generation = 1 +} + +// PrepareForUpdate clears fields that are not allowed to be set by end users on update. +func (v *mutatingAdmissionPolicyBindingStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { + newIC := obj.(*admissionregistration.MutatingAdmissionPolicyBinding) + oldIC := old.(*admissionregistration.MutatingAdmissionPolicyBinding) + + // Any changes to the spec increment the generation number, any changes to the + // status should reflect the generation number of the corresponding object. + // See metav1.ObjectMeta description for more information on Generation. + if !apiequality.Semantic.DeepEqual(oldIC.Spec, newIC.Spec) { + newIC.Generation = oldIC.Generation + 1 + } +} + +// Validate validates a new MutatingAdmissionPolicyBinding. +func (v *mutatingAdmissionPolicyBindingStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { + errs := validation.ValidateMutatingAdmissionPolicyBinding(obj.(*admissionregistration.MutatingAdmissionPolicyBinding)) + if len(errs) == 0 { + // if the object is well-formed, also authorize the paramRef + if err := v.authorizeCreate(ctx, obj); err != nil { + errs = append(errs, field.Forbidden(field.NewPath("spec", "paramRef"), err.Error())) + } + } + return errs +} + +// WarningsOnCreate returns warnings for the creation of the given object. +func (v *mutatingAdmissionPolicyBindingStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { + return nil +} + +// Canonicalize normalizes the object after validation. +func (v *mutatingAdmissionPolicyBindingStrategy) Canonicalize(obj runtime.Object) { +} + +// AllowCreateOnUpdate is false for MutatingAdmissionPolicyBinding; this means you may not create one with a PUT request. +func (v *mutatingAdmissionPolicyBindingStrategy) AllowCreateOnUpdate() bool { + return false +} + +// ValidateUpdate is the default update validation for an end user. +func (v *mutatingAdmissionPolicyBindingStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { + errs := validation.ValidateMutatingAdmissionPolicyBindingUpdate(obj.(*admissionregistration.MutatingAdmissionPolicyBinding), old.(*admissionregistration.MutatingAdmissionPolicyBinding)) + if len(errs) == 0 { + // if the object is well-formed, also authorize the paramRef + if err := v.authorizeUpdate(ctx, obj, old); err != nil { + errs = append(errs, field.Forbidden(field.NewPath("spec", "paramRef"), err.Error())) + } + } + return errs +} + +// WarningsOnUpdate returns warnings for the given update. +func (v *mutatingAdmissionPolicyBindingStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { + return nil +} + +// AllowUnconditionalUpdate is the default update policy for MutatingAdmissionPolicyBinding objects. Status update should +// only be allowed if version match. +func (v *mutatingAdmissionPolicyBindingStrategy) AllowUnconditionalUpdate() bool { + return false +} diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/strategy_test.go b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/strategy_test.go new file mode 100644 index 00000000000..ddc2f010c20 --- /dev/null +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/strategy_test.go @@ -0,0 +1,127 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mutatingadmissionpolicybinding + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" + + "k8s.io/kubernetes/pkg/apis/admissionregistration" +) + +func TestPolicyBindingStrategy(t *testing.T) { + strategy := NewStrategy(nil, nil, replicaLimitsResolver) + ctx := genericapirequest.NewDefaultContext() + if strategy.NamespaceScoped() { + t.Error("PolicyBinding strategy must be cluster scoped") + } + if strategy.AllowCreateOnUpdate() { + t.Errorf("PolicyBinding should not allow create on update") + } + + for _, configuration := range validPolicyBindings() { + strategy.PrepareForCreate(ctx, configuration) + errs := strategy.Validate(ctx, configuration) + if len(errs) != 0 { + t.Errorf("Unexpected error validating %v", errs) + } + invalidConfiguration := &admissionregistration.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{Name: ""}, + } + strategy.PrepareForUpdate(ctx, invalidConfiguration, configuration) + errs = strategy.ValidateUpdate(ctx, invalidConfiguration, configuration) + if len(errs) == 0 { + t.Errorf("Expected a validation error") + } + } +} + +var replicaLimitsResolver resolver.ResourceResolverFunc = func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: "rules.example.com", + Version: "v1", + Resource: "replicalimits", + }, nil +} + +func validPolicyBindings() []*admissionregistration.MutatingAdmissionPolicyBinding { + denyAction := admissionregistration.DenyAction + return []*admissionregistration.MutatingAdmissionPolicyBinding{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "replicalimit-policy.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "replica-limit-test.example.com", + ParameterNotFoundAction: &denyAction, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-clusterwide", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "replicalimit-policy.example.com", + ParamRef: &admissionregistration.ParamRef{ + Name: "replica-limit-test.example.com", + Namespace: "default", + ParameterNotFoundAction: &denyAction, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-selector", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "replicalimit-policy.example.com", + ParamRef: &admissionregistration.ParamRef{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "label": "value", + }, + }, + ParameterNotFoundAction: &denyAction, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-selector-clusterwide", + }, + Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{ + PolicyName: "replicalimit-policy.example.com", + ParamRef: &admissionregistration.ParamRef{ + Namespace: "mynamespace", + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "label": "value", + }, + }, + ParameterNotFoundAction: &denyAction, + }, + }, + }, + } +} diff --git a/pkg/registry/admissionregistration/rest/storage_apiserver.go b/pkg/registry/admissionregistration/rest/storage_apiserver.go index 57922cda656..991dc507586 100644 --- a/pkg/registry/admissionregistration/rest/storage_apiserver.go +++ b/pkg/registry/admissionregistration/rest/storage_apiserver.go @@ -28,6 +28,8 @@ import ( "k8s.io/client-go/discovery" "k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/apis/admissionregistration" + mutatingadmissionpolicystorage "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingadmissionpolicy/storage" + mutationpolicybindingstorage "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/storage" mutatingwebhookconfigurationstorage "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingwebhookconfiguration/storage" "k8s.io/kubernetes/pkg/registry/admissionregistration/resolver" validatingadmissionpolicystorage "k8s.io/kubernetes/pkg/registry/admissionregistration/validatingadmissionpolicy/storage" @@ -148,6 +150,25 @@ func (p RESTStorageProvider) v1alpha1Storage(apiResourceConfigSource serverstora storage[resource] = policyBindingStorage } + // mutatingadmissionpolicies + if resource := "mutatingadmissionpolicies"; apiResourceConfigSource.ResourceEnabled(admissionregistrationv1alpha1.SchemeGroupVersion.WithResource(resource)) { + policyStorage, err := mutatingadmissionpolicystorage.NewREST(restOptionsGetter, p.Authorizer, r) + if err != nil { + return storage, err + } + policyGetter = policyStorage + storage[resource] = policyStorage + } + + // mutatingadmissionpolicybindings + if resource := "mutatingadmissionpolicybindings"; apiResourceConfigSource.ResourceEnabled(admissionregistrationv1alpha1.SchemeGroupVersion.WithResource(resource)) { + mutationpolicybindingstorage, err := mutationpolicybindingstorage.NewREST(restOptionsGetter, p.Authorizer, &mutationpolicybindingstorage.DefaultPolicyGetter{Getter: policyGetter}, r) + if err != nil { + return storage, err + } + storage[resource] = mutationpolicybindingstorage + } + return storage, nil } diff --git a/staging/src/k8s.io/api/admissionregistration/v1alpha1/register.go b/staging/src/k8s.io/api/admissionregistration/v1alpha1/register.go index d4c2fbe807f..eead376cc7d 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1alpha1/register.go +++ b/staging/src/k8s.io/api/admissionregistration/v1alpha1/register.go @@ -50,6 +50,10 @@ func addKnownTypes(scheme *runtime.Scheme) error { &ValidatingAdmissionPolicyList{}, &ValidatingAdmissionPolicyBinding{}, &ValidatingAdmissionPolicyBindingList{}, + &MutatingAdmissionPolicy{}, + &MutatingAdmissionPolicyList{}, + &MutatingAdmissionPolicyBinding{}, + &MutatingAdmissionPolicyBindingList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go b/staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go index 78d918bc72f..ee50fbe2d4c 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go +++ b/staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go @@ -663,3 +663,346 @@ const ( Delete OperationType = v1.Delete Connect OperationType = v1.Connect ) + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.32 + +// MutatingAdmissionPolicy describes the definition of an admission mutation policy that mutates the object coming into admission chain. +type MutatingAdmissionPolicy struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Specification of the desired behavior of the MutatingAdmissionPolicy. + Spec MutatingAdmissionPolicySpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.32 + +// MutatingAdmissionPolicyList is a list of MutatingAdmissionPolicy. +type MutatingAdmissionPolicyList struct { + metav1.TypeMeta `json:",inline"` + // Standard list metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // List of ValidatingAdmissionPolicy. + Items []MutatingAdmissionPolicy `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// MutatingAdmissionPolicySpec is the specification of the desired behavior of the admission policy. +type MutatingAdmissionPolicySpec struct { + // paramKind specifies the kind of resources used to parameterize this policy. + // If absent, there are no parameters for this policy and the param CEL variable will not be provided to validation expressions. + // If paramKind refers to a non-existent kind, this policy definition is mis-configured and the FailurePolicy is applied. + // If paramKind is specified but paramRef is unset in MutatingAdmissionPolicyBinding, the params variable will be null. + // +optional + ParamKind *ParamKind `json:"paramKind,omitempty" protobuf:"bytes,1,rep,name=paramKind"` + + // matchConstraints specifies what resources this policy is designed to validate. + // The MutatingAdmissionPolicy cares about a request if it matches _all_ Constraints. + // However, in order to prevent clusters from being put into an unstable state that cannot be recovered from via the API + // MutatingAdmissionPolicy cannot match MutatingAdmissionPolicy and MutatingAdmissionPolicyBinding. + // The CREATE, UPDATE and CONNECT operations are allowed. The DELETE operation may not be matched. + // '*' matches CREATE, UPDATE and CONNECT. + // Required. + MatchConstraints *MatchResources `json:"matchConstraints,omitempty" protobuf:"bytes,2,rep,name=matchConstraints"` + + // variables contain definitions of variables that can be used in composition of other expressions. + // Each variable is defined as a named CEL expression. + // The variables defined here will be available under `variables` in other expressions of the policy + // except matchConditions because matchConditions are evaluated before the rest of the policy. + // + // The expression of a variable can refer to other variables defined earlier in the list but not those after. + // Thus, variables must be sorted by the order of first appearance and acyclic. + // +listType=atomic + // +optional + Variables []Variable `json:"variables,omitempty" protobuf:"bytes,3,rep,name=variables"` + + // mutations contain operations to perform on matching objects. + // mutations may not be empty; a minimum of one mutation is required. + // mutations are evaluated in order, and are reinvoked according to + // the reinvocationPolicy. + // The mutations of a policy are invoked for each binding of this policy + // and reinvocation of mutations occurs on a per binding basis. + // + // +listType=atomic + // +optional + Mutations []Mutation `json:"mutations,omitempty" protobuf:"bytes,4,rep,name=mutations"` + + // failurePolicy defines how to handle failures for the admission policy. Failures can + // occur from CEL expression parse errors, type check errors, runtime errors and invalid + // or mis-configured policy definitions or bindings. + // + // A policy is invalid if paramKind refers to a non-existent Kind. + // A binding is invalid if paramRef.name refers to a non-existent resource. + // + // failurePolicy does not define how validations that evaluate to false are handled. + // + // Allowed values are Ignore or Fail. Defaults to Fail. + // +optional + FailurePolicy *FailurePolicyType `json:"failurePolicy,omitempty" protobuf:"bytes,5,opt,name=failurePolicy,casttype=FailurePolicyType"` + + // matchConditions is a list of conditions that must be met for a request to be validated. + // Match conditions filter requests that have already been matched by the matchConstraints. + // An empty list of matchConditions matches all requests. + // There are a maximum of 64 match conditions allowed. + // + // If a parameter object is provided, it can be accessed via the `params` handle in the same + // manner as validation expressions. + // + // The exact matching logic is (in order): + // 1. If ANY matchCondition evaluates to FALSE, the policy is skipped. + // 2. If ALL matchConditions evaluate to TRUE, the policy is evaluated. + // 3. If any matchCondition evaluates to an error (but none are FALSE): + // - If failurePolicy=Fail, reject the request + // - If failurePolicy=Ignore, the policy is skipped + // + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +optional + MatchConditions []MatchCondition `json:"matchConditions,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,6,rep,name=matchConditions"` + + // reinvocationPolicy indicates whether mutations may be called multiple times per MutatingAdmissionPolicyBinding + // as part of a single admission evaluation. + // Allowed values are "Never" and "IfNeeded". + // + // Never: These mutations will not be called more than once per binding in a single admission evaluation. + // + // IfNeeded: These mutations may be invoked more than once per binding for a single admission request and there is no guarantee of + // order with respect to other admission plugins, admission webhooks, bindings of this policy and admission policies. Mutations are only + // reinvoked when mutations change the object after this mutation is invoked. + // Required. + ReinvocationPolicy ReinvocationPolicyType `json:"reinvocationPolicy,omitempty" protobuf:"bytes,7,opt,name=reinvocationPolicy,casttype=ReinvocationPolicyType"` +} + +// Mutation specifies the CEL expression which is used to apply the Mutation. +type Mutation struct { + // patchType indicates the patch strategy used. + // Allowed values are "ApplyConfiguration" and "JSONPatch". + // Required. + // + // +unionDiscriminator + PatchType PatchType `json:"patchType" protobuf:"bytes,2,opt,name=patchType,casttype=PatchType"` + + // applyConfiguration defines the desired configuration values of an object. + // The configuration is applied to the admission object using + // [structured merge diff](https://github.com/kubernetes-sigs/structured-merge-diff). + // A CEL expression is used to create apply configuration. + ApplyConfiguration *ApplyConfiguration `json:"applyConfiguration,omitempty" protobuf:"bytes,3,opt,name=applyConfiguration"` + + // jsonPatch defines a [JSON patch](https://jsonpatch.com/) operation to perform a mutation to the object. + // A CEL expression is used to create the JSON patch. + JSONPatch *JSONPatch `json:"jsonPatch,omitempty" protobuf:"bytes,4,opt,name=jsonPatch"` +} + +// PatchType specifies the type of patch operation for a mutation. +// +enum +type PatchType string + +const ( + // ApplyConfiguration indicates that the mutation is using apply configuration to mutate the object. + PatchTypeApplyConfiguration PatchType = "ApplyConfiguration" + // JSONPatch indicates that the object is mutated through JSON Patch. + PatchTypeJSONPatch PatchType = "JSONPatch" +) + +// ApplyConfiguration defines the desired configuration values of an object. +type ApplyConfiguration struct { + // expression will be evaluated by CEL to create an apply configuration. + // ref: https://github.com/google/cel-spec + // + // Apply configurations are declared in CEL using object initialization. For example, this CEL expression + // returns an apply configuration to set a single field: + // + // Object{ + // spec: Object.spec{ + // serviceAccountName: "example" + // } + // } + // + // Apply configurations may not modify atomic structs, maps or arrays due to the risk of accidental deletion of + // values not included in the apply configuration. + // + // CEL expressions have access to the object types needed to create apply configurations: + // + // - 'Object' - CEL type of the resource object. + // - 'Object.' - CEL type of object field (such as 'Object.spec') + // - 'Object.....` - CEL type of nested field (such as 'Object.spec.containers') + // + // CEL expressions have access to the contents of the API request, organized into CEL variables as well as some other useful variables: + // + // - 'object' - The object from the incoming request. The value is null for DELETE requests. + // - 'oldObject' - The existing object. The value is null for CREATE requests. + // - 'request' - Attributes of the API request([ref](/pkg/apis/admission/types.go#AdmissionRequest)). + // - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind. + // - 'namespaceObject' - The namespace object that the incoming object belongs to. The value is null for cluster-scoped resources. + // - 'variables' - Map of composited variables, from its name to its lazily evaluated value. + // For example, a variable named 'foo' can be accessed as 'variables.foo'. + // - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request. + // See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz + // - 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the + // request resource. + // + // The `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the + // object. No other metadata properties are accessible. + // + // Only property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. + // Required. + Expression string `json:"expression,omitempty" protobuf:"bytes,1,opt,name=expression"` +} + +// JSONPatch defines a JSON Patch. +type JSONPatch struct { + // expression will be evaluated by CEL to create a [JSON patch](https://jsonpatch.com/). + // ref: https://github.com/google/cel-spec + // + // expression must return an array of JSONPatch values. + // + // For example, this CEL expression returns a JSON patch to conditionally modify a value: + // + // [ + // JSONPatch{op: "test", path: "/spec/example", value: "Red"}, + // JSONPatch{op: "replace", path: "/spec/example", value: "Green"} + // ] + // + // To define an object for the patch value, use Object types. For example: + // + // [ + // JSONPatch{ + // op: "add", + // path: "/spec/selector", + // value: Object.spec.selector{matchLabels: {"environment": "test"}} + // } + // ] + // + // To use strings containing '/' and '~' as JSONPatch path keys, use "jsonpatch.escapeKey". For example: + // + // [ + // JSONPatch{ + // op: "add", + // path: "/metadata/labels/" + jsonpatch.escapeKey("example.com/environment"), + // value: "test" + // }, + // ] + // + // CEL expressions have access to the types needed to create JSON patches and objects: + // + // - 'JSONPatch' - CEL type of JSON Patch operations. JSONPatch has the fields 'op', 'from', 'path' and 'value'. + // See [JSON patch](https://jsonpatch.com/) for more details. The 'value' field may be set to any of: string, + // integer, array, map or object. If set, the 'path' and 'from' fields must be set to a + // [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901/) string, where the 'jsonpatch.escapeKey()' CEL + // function may be used to escape path keys containing '/' and '~'. + // - 'Object' - CEL type of the resource object. + // - 'Object.' - CEL type of object field (such as 'Object.spec') + // - 'Object.....` - CEL type of nested field (such as 'Object.spec.containers') + // + // CEL expressions have access to the contents of the API request, organized into CEL variables as well as some other useful variables: + // + // - 'object' - The object from the incoming request. The value is null for DELETE requests. + // - 'oldObject' - The existing object. The value is null for CREATE requests. + // - 'request' - Attributes of the API request([ref](/pkg/apis/admission/types.go#AdmissionRequest)). + // - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind. + // - 'namespaceObject' - The namespace object that the incoming object belongs to. The value is null for cluster-scoped resources. + // - 'variables' - Map of composited variables, from its name to its lazily evaluated value. + // For example, a variable named 'foo' can be accessed as 'variables.foo'. + // - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request. + // See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz + // - 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the + // request resource. + // + // CEL expressions have access to [Kubernetes CEL function libraries](https://kubernetes.io/docs/reference/using-api/cel/#cel-options-language-features-and-libraries) + // as well as: + // + // - 'jsonpatch.escapeKey' - Performs JSONPatch key escaping. '~' and '/' are escaped as '~0' and `~1' respectively). + // + // + // Only property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. + // Required. + Expression string `json:"expression,omitempty" protobuf:"bytes,1,opt,name=expression"` +} + +// ReinvocationPolicyType specifies what type of policy the admission mutation uses. +// +enum +type ReinvocationPolicyType = v1.ReinvocationPolicyType + +const ( + // NeverReinvocationPolicy indicates that the mutation must not be called more than once in a + // single admission evaluation. + NeverReinvocationPolicy ReinvocationPolicyType = v1.NeverReinvocationPolicy + // IfNeededReinvocationPolicy indicates that the mutation may be called at least one + // additional time as part of the admission evaluation if the object being admitted is + // modified by other admission plugins after the initial mutation call. + IfNeededReinvocationPolicy ReinvocationPolicyType = v1.IfNeededReinvocationPolicy +) + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.32 + +// MutatingAdmissionPolicyBinding binds the MutatingAdmissionPolicy with parametrized resources. +// MutatingAdmissionPolicyBinding and the optional parameter resource together define how cluster administrators +// configure policies for clusters. +// +// For a given admission request, each binding will cause its policy to be +// evaluated N times, where N is 1 for policies/bindings that don't use +// params, otherwise N is the number of parameters selected by the binding. +// Each evaluation is constrained by a [runtime cost budget](https://kubernetes.io/docs/reference/using-api/cel/#runtime-cost-budget). +// +// Adding/removing policies, bindings, or params can not affect whether a +// given (policy, binding, param) combination is within its own CEL budget. +type MutatingAdmissionPolicyBinding struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Specification of the desired behavior of the MutatingAdmissionPolicyBinding. + Spec MutatingAdmissionPolicyBindingSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.32 + +// MutatingAdmissionPolicyBindingList is a list of MutatingAdmissionPolicyBinding. +type MutatingAdmissionPolicyBindingList struct { + metav1.TypeMeta `json:",inline"` + // Standard list metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // List of PolicyBinding. + Items []MutatingAdmissionPolicyBinding `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// MutatingAdmissionPolicyBindingSpec is the specification of the MutatingAdmissionPolicyBinding. +type MutatingAdmissionPolicyBindingSpec struct { + // policyName references a MutatingAdmissionPolicy name which the MutatingAdmissionPolicyBinding binds to. + // If the referenced resource does not exist, this binding is considered invalid and will be ignored + // Required. + PolicyName string `json:"policyName,omitempty" protobuf:"bytes,1,rep,name=policyName"` + + // paramRef specifies the parameter resource used to configure the admission control policy. + // It should point to a resource of the type specified in spec.ParamKind of the bound MutatingAdmissionPolicy. + // If the policy specifies a ParamKind and the resource referred to by ParamRef does not exist, this binding is considered mis-configured and the FailurePolicy of the MutatingAdmissionPolicy applied. + // If the policy does not specify a ParamKind then this field is ignored, and the rules are evaluated without a param. + // +optional + ParamRef *ParamRef `json:"paramRef,omitempty" protobuf:"bytes,2,rep,name=paramRef"` + + // matchResources limits what resources match this binding and may be mutated by it. + // Note that if matchResources matches a resource, the resource must also match a policy's matchConstraints and + // matchConditions before the resource may be mutated. + // When matchResources is unset, it does not constrain resource matching, and only the policy's matchConstraints + // and matchConditions must match for the resource to be mutated. + // Additionally, matchResources.resourceRules are optional and do not constraint matching when unset. + // Note that this is differs from MutatingAdmissionPolicy matchConstraints, where resourceRules are required. + // The CREATE, UPDATE and CONNECT operations are allowed. The DELETE operation may not be matched. + // '*' matches CREATE, UPDATE and CONNECT. + // +optional + MatchResources *MatchResources `json:"matchResources,omitempty" protobuf:"bytes,3,rep,name=matchResources"` +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch_test.go new file mode 100644 index 00000000000..58c4a55b838 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch_test.go @@ -0,0 +1 @@ +package patch diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules/rules.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules/rules.go index b926f65dc10..10bef0a8f69 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules/rules.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules/rules.go @@ -121,7 +121,7 @@ func (r *Matcher) resource() bool { func IsExemptAdmissionConfigurationResource(attr admission.Attributes) bool { gvk := attr.GetKind() if gvk.Group == "admissionregistration.k8s.io" { - if gvk.Kind == "ValidatingWebhookConfiguration" || gvk.Kind == "MutatingWebhookConfiguration" || gvk.Kind == "ValidatingAdmissionPolicy" || gvk.Kind == "ValidatingAdmissionPolicyBinding" { + if gvk.Kind == "ValidatingWebhookConfiguration" || gvk.Kind == "MutatingWebhookConfiguration" || gvk.Kind == "ValidatingAdmissionPolicy" || gvk.Kind == "ValidatingAdmissionPolicyBinding" || gvk.Kind == "MutatingAdmissionPolicy" || gvk.Kind == "MutatingAdmissionPolicyBinding" { return true } } diff --git a/test/integration/apiserver/admissionwebhook/admission_test.go b/test/integration/apiserver/admissionwebhook/admission_test.go index a29fc0b5e1f..a211bc6adca 100644 --- a/test/integration/apiserver/admissionwebhook/admission_test.go +++ b/test/integration/apiserver/admissionwebhook/admission_test.go @@ -148,6 +148,8 @@ var ( gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicies"): true, gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicies/status"): true, gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicybindings"): true, + gvr("admissionregistration.k8s.io", "v1alpha1", "mutatingadmissionpolicies"): true, + gvr("admissionregistration.k8s.io", "v1alpha1", "mutatingadmissionpolicybindings"): true, } parentResources = map[schema.GroupVersionResource]schema.GroupVersionResource{ diff --git a/test/integration/apiserver/cel/admission_test_util.go b/test/integration/apiserver/cel/admission_test_util.go index 9917c61b0bb..9ac02c7ec9c 100644 --- a/test/integration/apiserver/cel/admission_test_util.go +++ b/test/integration/apiserver/cel/admission_test_util.go @@ -146,6 +146,8 @@ var ( gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicies"): true, gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicies/status"): true, gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicybindings"): true, + gvr("admissionregistration.k8s.io", "v1alpha1", "mutatingadmissionpolicies"): true, + gvr("admissionregistration.k8s.io", "v1alpha1", "mutatingadmissionpolicybindings"): true, // transient resource exemption gvr("authentication.k8s.io", "v1", "selfsubjectreviews"): true, gvr("authentication.k8s.io", "v1beta1", "selfsubjectreviews"): true, diff --git a/test/integration/controlplane/generic_test.go b/test/integration/controlplane/generic_test.go index 259f279c330..85f6e7bbb5b 100644 --- a/test/integration/controlplane/generic_test.go +++ b/test/integration/controlplane/generic_test.go @@ -103,6 +103,8 @@ func TestGenericControlplaneStartUp(t *testing.T) { "validatingadmissionpolicies.admissionregistration.k8s.io", "validatingadmissionpolicybindings.admissionregistration.k8s.io", "validatingwebhookconfigurations.admissionregistration.k8s.io", + "mutatingadmissionpolicies.admissionregistration.k8s.io", + "mutatingadmissionpolicybindings.admissionregistration.k8s.io", ) if diff := cmp.Diff(sets.List(expected), sets.List(grs)); diff != "" { t.Fatalf("unexpected API groups: +want, -got\n%s", diff) diff --git a/test/integration/etcd/data.go b/test/integration/etcd/data.go index ad1f71aabdb..01dad0df1ec 100644 --- a/test/integration/etcd/data.go +++ b/test/integration/etcd/data.go @@ -362,6 +362,14 @@ func GetEtcdStorageDataForNamespace(namespace string) map[schema.GroupVersionRes ExpectedEtcdPath: "/registry/validatingadmissionpolicybindings/pb1a1", ExpectedGVK: gvkP("admissionregistration.k8s.io", "v1", "ValidatingAdmissionPolicyBinding"), }, + gvr("admissionregistration.k8s.io", "v1alpha1", "mutatingadmissionpolicies"): { + Stub: `{"metadata":{"name":"map1","creationTimestamp":null},"spec":{"paramKind":{"apiVersion":"test.example.com/v1","kind":"Example"},"matchConstraints":{"resourceRules": [{"resourceNames": ["fakeName"], "apiGroups":["apps"],"apiVersions":["v1"],"operations":["CREATE", "UPDATE"], "resources":["deployments"]}]},"reinvocationPolicy": "IfNeeded","mutations":[{"applyConfiguration": {"expression":"Object{metadata: Object.metadata{labels: {'example':'true'}}}"}, "patchType":"ApplyConfiguration"}]}}`, + ExpectedEtcdPath: "/registry/mutatingadmissionpolicies/map1", + }, + gvr("admissionregistration.k8s.io", "v1alpha1", "mutatingadmissionpolicybindings"): { + Stub: `{"metadata":{"name":"mpb1","creationTimestamp":null},"spec":{"policyName":"replicalimit-policy.example.com","paramRef":{"name":"replica-limit-test.example.com"}}}`, + ExpectedEtcdPath: "/registry/mutatingadmissionpolicybindings/mpb1", + }, // -- // k8s.io/kubernetes/pkg/apis/scheduling/v1 From fe3a7f529153db690ac8588ee9c31bdfda0ed5f8 Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Fri, 25 Oct 2024 13:46:37 -0400 Subject: [PATCH 05/16] generate code --- api/discovery/aggregated_v2.json | 46 + ...dmissionregistration.k8s.io__v1alpha1.json | 40 + api/openapi-spec/swagger.json | 1508 ++++++++++ ...registration.k8s.io__v1alpha1_openapi.json | 2564 +++++++++++++++++ .../v1alpha1/zz_generated.conversion.go | 385 ++- .../v1alpha1/zz_generated.defaults.go | 58 + .../zz_generated.deepcopy.go | 252 ++ pkg/generated/openapi/zz_generated.openapi.go | 415 ++- .../v1alpha1/generated.pb.go | 2462 +++++++++++++++- .../v1alpha1/generated.proto | 301 ++ .../v1alpha1/types_swagger_doc_generated.go | 95 + .../v1alpha1/zz_generated.deepcopy.go | 252 ++ .../zz_generated.prerelease-lifecycle.go | 72 + ...s.io.v1alpha1.MutatingAdmissionPolicy.json | 148 + ...k8s.io.v1alpha1.MutatingAdmissionPolicy.pb | Bin 0 -> 1013 bytes ...s.io.v1alpha1.MutatingAdmissionPolicy.yaml | 94 + ...alpha1.MutatingAdmissionPolicyBinding.json | 139 + ...v1alpha1.MutatingAdmissionPolicyBinding.pb | Bin 0 -> 984 bytes ...alpha1.MutatingAdmissionPolicyBinding.yaml | 90 + .../v1alpha1/applyconfiguration.go | 39 + .../v1alpha1/jsonpatch.go | 39 + .../v1alpha1/mutatingadmissionpolicy.go | 253 ++ .../mutatingadmissionpolicybinding.go | 253 ++ .../mutatingadmissionpolicybindingspec.go | 57 + .../v1alpha1/mutatingadmissionpolicyspec.go | 113 + .../v1alpha1/mutation.go | 61 + .../applyconfigurations/internal/internal.go | 106 + .../client-go/applyconfigurations/utils.go | 14 + .../v1alpha1/interface.go | 14 + .../v1alpha1/mutatingadmissionpolicy.go | 89 + .../mutatingadmissionpolicybinding.go | 89 + .../src/k8s.io/client-go/informers/generic.go | 4 + .../v1alpha1/admissionregistration_client.go | 10 + .../fake/fake_admissionregistration_client.go | 8 + .../fake/fake_mutatingadmissionpolicy.go | 151 + .../fake_mutatingadmissionpolicybinding.go | 151 + .../v1alpha1/generated_expansion.go | 4 + .../v1alpha1/mutatingadmissionpolicy.go | 75 + .../mutatingadmissionpolicybinding.go | 75 + .../v1alpha1/expansion_generated.go | 8 + .../v1alpha1/mutatingadmissionpolicy.go | 48 + .../mutatingadmissionpolicybinding.go | 48 + 42 files changed, 10514 insertions(+), 116 deletions(-) create mode 100644 staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1alpha1.MutatingAdmissionPolicy.json create mode 100644 staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1alpha1.MutatingAdmissionPolicy.pb create mode 100644 staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1alpha1.MutatingAdmissionPolicy.yaml create mode 100644 staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1alpha1.MutatingAdmissionPolicyBinding.json create mode 100644 staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1alpha1.MutatingAdmissionPolicyBinding.pb create mode 100644 staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1alpha1.MutatingAdmissionPolicyBinding.yaml create mode 100644 staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/applyconfiguration.go create mode 100644 staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/jsonpatch.go create mode 100644 staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/mutatingadmissionpolicy.go create mode 100644 staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/mutatingadmissionpolicybinding.go create mode 100644 staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/mutatingadmissionpolicybindingspec.go create mode 100644 staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/mutatingadmissionpolicyspec.go create mode 100644 staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/mutation.go create mode 100644 staging/src/k8s.io/client-go/informers/admissionregistration/v1alpha1/mutatingadmissionpolicy.go create mode 100644 staging/src/k8s.io/client-go/informers/admissionregistration/v1alpha1/mutatingadmissionpolicybinding.go create mode 100644 staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/fake/fake_mutatingadmissionpolicy.go create mode 100644 staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/fake/fake_mutatingadmissionpolicybinding.go create mode 100644 staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/mutatingadmissionpolicy.go create mode 100644 staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/mutatingadmissionpolicybinding.go create mode 100644 staging/src/k8s.io/client-go/listers/admissionregistration/v1alpha1/mutatingadmissionpolicy.go create mode 100644 staging/src/k8s.io/client-go/listers/admissionregistration/v1alpha1/mutatingadmissionpolicybinding.go diff --git a/api/discovery/aggregated_v2.json b/api/discovery/aggregated_v2.json index e0eb36ce418..13e35eb6d29 100644 --- a/api/discovery/aggregated_v2.json +++ b/api/discovery/aggregated_v2.json @@ -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" diff --git a/api/discovery/apis__admissionregistration.k8s.io__v1alpha1.json b/api/discovery/apis__admissionregistration.k8s.io__v1alpha1.json index 1e7ff835df1..16494e12cbc 100644 --- a/api/discovery/apis__admissionregistration.k8s.io__v1alpha1.json +++ b/api/discovery/apis__admissionregistration.k8s.io__v1alpha1.json @@ -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" diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index 5f47fe88d65..df272d474be 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -866,6 +866,16 @@ }, "type": "object" }, + "io.k8s.api.admissionregistration.v1alpha1.ApplyConfiguration": { + "description": "ApplyConfiguration defines the desired configuration values of an object.", + "properties": { + "expression": { + "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.' - CEL type of object field (such as 'Object.spec') - 'Object.....` - 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" + } + }, + "type": "object" + }, "io.k8s.api.admissionregistration.v1alpha1.AuditAnnotation": { "description": "AuditAnnotation describes how to produce an audit annotation for an API request.", "properties": { @@ -902,6 +912,16 @@ ], "type": "object" }, + "io.k8s.api.admissionregistration.v1alpha1.JSONPatch": { + "description": "JSONPatch defines a JSON Patch.", + "properties": { + "expression": { + "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.' - CEL type of object field (such as 'Object.spec') - 'Object.....` - 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" + } + }, + "type": "object" + }, "io.k8s.api.admissionregistration.v1alpha1.MatchCondition": { "properties": { "expression": { @@ -954,6 +974,224 @@ "type": "object", "x-kubernetes-map-type": "atomic" }, + "io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy": { + "description": "MutatingAdmissionPolicy describes the definition of an admission mutation policy that mutates the object coming into admission chain.", + "properties": { + "apiVersion": { + "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" + }, + "kind": { + "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" + }, + "metadata": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta", + "description": "Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata." + }, + "spec": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicySpec", + "description": "Specification of the desired behavior of the MutatingAdmissionPolicy." + } + }, + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicy", + "version": "v1alpha1" + } + ] + }, + "io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding": { + "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.", + "properties": { + "apiVersion": { + "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" + }, + "kind": { + "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" + }, + "metadata": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta", + "description": "Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata." + }, + "spec": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBindingSpec", + "description": "Specification of the desired behavior of the MutatingAdmissionPolicyBinding." + } + }, + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBinding", + "version": "v1alpha1" + } + ] + }, + "io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBindingList": { + "description": "MutatingAdmissionPolicyBindingList is a list of MutatingAdmissionPolicyBinding.", + "properties": { + "apiVersion": { + "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" + }, + "items": { + "description": "List of PolicyBinding.", + "items": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + }, + "type": "array" + }, + "kind": { + "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" + }, + "metadata": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta", + "description": "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds" + } + }, + "required": [ + "items" + ], + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBindingList", + "version": "v1alpha1" + } + ] + }, + "io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBindingSpec": { + "description": "MutatingAdmissionPolicyBindingSpec is the specification of the MutatingAdmissionPolicyBinding.", + "properties": { + "matchResources": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MatchResources", + "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." + }, + "paramRef": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.ParamRef", + "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." + }, + "policyName": { + "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" + } + }, + "type": "object" + }, + "io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyList": { + "description": "MutatingAdmissionPolicyList is a list of MutatingAdmissionPolicy.", + "properties": { + "apiVersion": { + "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" + }, + "items": { + "description": "List of ValidatingAdmissionPolicy.", + "items": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + }, + "type": "array" + }, + "kind": { + "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" + }, + "metadata": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta", + "description": "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds" + } + }, + "required": [ + "items" + ], + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyList", + "version": "v1alpha1" + } + ] + }, + "io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicySpec": { + "description": "MutatingAdmissionPolicySpec is the specification of the desired behavior of the admission policy.", + "properties": { + "failurePolicy": { + "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.", + "type": "string" + }, + "matchConditions": { + "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", + "items": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MatchCondition" + }, + "type": "array", + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, + "matchConstraints": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MatchResources", + "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." + }, + "mutations": { + "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.", + "items": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.Mutation" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "paramKind": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.ParamKind", + "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." + }, + "reinvocationPolicy": { + "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.", + "type": "string" + }, + "variables": { + "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.", + "items": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.Variable" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + } + }, + "type": "object" + }, + "io.k8s.api.admissionregistration.v1alpha1.Mutation": { + "description": "Mutation specifies the CEL expression which is used to apply the Mutation.", + "properties": { + "applyConfiguration": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.ApplyConfiguration", + "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." + }, + "jsonPatch": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.JSONPatch", + "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." + }, + "patchType": { + "description": "patchType indicates the patch strategy used. Allowed values are \"ApplyConfiguration\" and \"JSONPatch\". Required.", + "type": "string" + } + }, + "required": [ + "patchType" + ], + "type": "object" + }, "io.k8s.api.admissionregistration.v1alpha1.NamedRuleWithOperations": { "description": "NamedRuleWithOperations is a tuple of Operations and Resources with ResourceNames.", "properties": { @@ -38193,6 +38431,964 @@ ] } }, + "/apis/admissionregistration.k8s.io/v1alpha1/mutatingadmissionpolicies": { + "delete": { + "consumes": [ + "*/*" + ], + "description": "delete collection of MutatingAdmissionPolicy", + "operationId": "deleteAdmissionregistrationV1alpha1CollectionMutatingAdmissionPolicy", + "parameters": [ + { + "$ref": "#/parameters/body-2Y1dVQaQ" + }, + { + "$ref": "#/parameters/continue-QfD61s0i" + }, + { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "in": "query", + "name": "dryRun", + "type": "string", + "uniqueItems": true + }, + { + "$ref": "#/parameters/fieldSelector-xIcQKXFG" + }, + { + "$ref": "#/parameters/gracePeriodSeconds--K5HaBOS" + }, + { + "$ref": "#/parameters/labelSelector-5Zw57w4C" + }, + { + "$ref": "#/parameters/limit-1NfNmdNH" + }, + { + "$ref": "#/parameters/orphanDependents-uRB25kX5" + }, + { + "$ref": "#/parameters/propagationPolicy-6jk3prlO" + }, + { + "$ref": "#/parameters/resourceVersion-5WAnf1kx" + }, + { + "$ref": "#/parameters/resourceVersionMatch-t8XhRHeC" + }, + { + "$ref": "#/parameters/sendInitialEvents-rLXlEK_k" + }, + { + "$ref": "#/parameters/timeoutSeconds-yvYezaOC" + } + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "schemes": [ + "https" + ], + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "deletecollection", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicy", + "version": "v1alpha1" + } + }, + "get": { + "consumes": [ + "*/*" + ], + "description": "list or watch objects of kind MutatingAdmissionPolicy", + "operationId": "listAdmissionregistrationV1alpha1MutatingAdmissionPolicy", + "parameters": [ + { + "$ref": "#/parameters/allowWatchBookmarks-HC2hJt-J" + }, + { + "$ref": "#/parameters/continue-QfD61s0i" + }, + { + "$ref": "#/parameters/fieldSelector-xIcQKXFG" + }, + { + "$ref": "#/parameters/labelSelector-5Zw57w4C" + }, + { + "$ref": "#/parameters/limit-1NfNmdNH" + }, + { + "$ref": "#/parameters/resourceVersion-5WAnf1kx" + }, + { + "$ref": "#/parameters/resourceVersionMatch-t8XhRHeC" + }, + { + "$ref": "#/parameters/sendInitialEvents-rLXlEK_k" + }, + { + "$ref": "#/parameters/timeoutSeconds-yvYezaOC" + }, + { + "$ref": "#/parameters/watch-XNNPZGbK" + } + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf", + "application/json;stream=watch", + "application/vnd.kubernetes.protobuf;stream=watch" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyList" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "schemes": [ + "https" + ], + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicy", + "version": "v1alpha1" + } + }, + "parameters": [ + { + "$ref": "#/parameters/pretty-tJGM1-ng" + } + ], + "post": { + "consumes": [ + "*/*" + ], + "description": "create a MutatingAdmissionPolicy", + "operationId": "createAdmissionregistrationV1alpha1MutatingAdmissionPolicy", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "in": "query", + "name": "dryRun", + "type": "string", + "uniqueItems": true + }, + { + "$ref": "#/parameters/fieldManager-Qy4HdaTW" + }, + { + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "in": "query", + "name": "fieldValidation", + "type": "string", + "uniqueItems": true + } + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "schemes": [ + "https" + ], + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "post", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicy", + "version": "v1alpha1" + } + } + }, + "/apis/admissionregistration.k8s.io/v1alpha1/mutatingadmissionpolicies/{name}": { + "delete": { + "consumes": [ + "*/*" + ], + "description": "delete a MutatingAdmissionPolicy", + "operationId": "deleteAdmissionregistrationV1alpha1MutatingAdmissionPolicy", + "parameters": [ + { + "$ref": "#/parameters/body-2Y1dVQaQ" + }, + { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "in": "query", + "name": "dryRun", + "type": "string", + "uniqueItems": true + }, + { + "$ref": "#/parameters/gracePeriodSeconds--K5HaBOS" + }, + { + "$ref": "#/parameters/orphanDependents-uRB25kX5" + }, + { + "$ref": "#/parameters/propagationPolicy-6jk3prlO" + } + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "schemes": [ + "https" + ], + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "delete", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicy", + "version": "v1alpha1" + } + }, + "get": { + "consumes": [ + "*/*" + ], + "description": "read the specified MutatingAdmissionPolicy", + "operationId": "readAdmissionregistrationV1alpha1MutatingAdmissionPolicy", + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "schemes": [ + "https" + ], + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicy", + "version": "v1alpha1" + } + }, + "parameters": [ + { + "description": "name of the MutatingAdmissionPolicy", + "in": "path", + "name": "name", + "required": true, + "type": "string", + "uniqueItems": true + }, + { + "$ref": "#/parameters/pretty-tJGM1-ng" + } + ], + "patch": { + "consumes": [ + "application/json-patch+json", + "application/merge-patch+json", + "application/strategic-merge-patch+json", + "application/apply-patch+yaml" + ], + "description": "partially update the specified MutatingAdmissionPolicy", + "operationId": "patchAdmissionregistrationV1alpha1MutatingAdmissionPolicy", + "parameters": [ + { + "$ref": "#/parameters/body-78PwaGsr" + }, + { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "in": "query", + "name": "dryRun", + "type": "string", + "uniqueItems": true + }, + { + "$ref": "#/parameters/fieldManager-7c6nTn1T" + }, + { + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "in": "query", + "name": "fieldValidation", + "type": "string", + "uniqueItems": true + }, + { + "$ref": "#/parameters/force-tOGGb0Yi" + } + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "schemes": [ + "https" + ], + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicy", + "version": "v1alpha1" + } + }, + "put": { + "consumes": [ + "*/*" + ], + "description": "replace the specified MutatingAdmissionPolicy", + "operationId": "replaceAdmissionregistrationV1alpha1MutatingAdmissionPolicy", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "in": "query", + "name": "dryRun", + "type": "string", + "uniqueItems": true + }, + { + "$ref": "#/parameters/fieldManager-Qy4HdaTW" + }, + { + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "in": "query", + "name": "fieldValidation", + "type": "string", + "uniqueItems": true + } + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "schemes": [ + "https" + ], + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicy", + "version": "v1alpha1" + } + } + }, + "/apis/admissionregistration.k8s.io/v1alpha1/mutatingadmissionpolicybindings": { + "delete": { + "consumes": [ + "*/*" + ], + "description": "delete collection of MutatingAdmissionPolicyBinding", + "operationId": "deleteAdmissionregistrationV1alpha1CollectionMutatingAdmissionPolicyBinding", + "parameters": [ + { + "$ref": "#/parameters/body-2Y1dVQaQ" + }, + { + "$ref": "#/parameters/continue-QfD61s0i" + }, + { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "in": "query", + "name": "dryRun", + "type": "string", + "uniqueItems": true + }, + { + "$ref": "#/parameters/fieldSelector-xIcQKXFG" + }, + { + "$ref": "#/parameters/gracePeriodSeconds--K5HaBOS" + }, + { + "$ref": "#/parameters/labelSelector-5Zw57w4C" + }, + { + "$ref": "#/parameters/limit-1NfNmdNH" + }, + { + "$ref": "#/parameters/orphanDependents-uRB25kX5" + }, + { + "$ref": "#/parameters/propagationPolicy-6jk3prlO" + }, + { + "$ref": "#/parameters/resourceVersion-5WAnf1kx" + }, + { + "$ref": "#/parameters/resourceVersionMatch-t8XhRHeC" + }, + { + "$ref": "#/parameters/sendInitialEvents-rLXlEK_k" + }, + { + "$ref": "#/parameters/timeoutSeconds-yvYezaOC" + } + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "schemes": [ + "https" + ], + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "deletecollection", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBinding", + "version": "v1alpha1" + } + }, + "get": { + "consumes": [ + "*/*" + ], + "description": "list or watch objects of kind MutatingAdmissionPolicyBinding", + "operationId": "listAdmissionregistrationV1alpha1MutatingAdmissionPolicyBinding", + "parameters": [ + { + "$ref": "#/parameters/allowWatchBookmarks-HC2hJt-J" + }, + { + "$ref": "#/parameters/continue-QfD61s0i" + }, + { + "$ref": "#/parameters/fieldSelector-xIcQKXFG" + }, + { + "$ref": "#/parameters/labelSelector-5Zw57w4C" + }, + { + "$ref": "#/parameters/limit-1NfNmdNH" + }, + { + "$ref": "#/parameters/resourceVersion-5WAnf1kx" + }, + { + "$ref": "#/parameters/resourceVersionMatch-t8XhRHeC" + }, + { + "$ref": "#/parameters/sendInitialEvents-rLXlEK_k" + }, + { + "$ref": "#/parameters/timeoutSeconds-yvYezaOC" + }, + { + "$ref": "#/parameters/watch-XNNPZGbK" + } + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf", + "application/json;stream=watch", + "application/vnd.kubernetes.protobuf;stream=watch" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBindingList" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "schemes": [ + "https" + ], + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBinding", + "version": "v1alpha1" + } + }, + "parameters": [ + { + "$ref": "#/parameters/pretty-tJGM1-ng" + } + ], + "post": { + "consumes": [ + "*/*" + ], + "description": "create a MutatingAdmissionPolicyBinding", + "operationId": "createAdmissionregistrationV1alpha1MutatingAdmissionPolicyBinding", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "in": "query", + "name": "dryRun", + "type": "string", + "uniqueItems": true + }, + { + "$ref": "#/parameters/fieldManager-Qy4HdaTW" + }, + { + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "in": "query", + "name": "fieldValidation", + "type": "string", + "uniqueItems": true + } + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "schemes": [ + "https" + ], + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "post", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBinding", + "version": "v1alpha1" + } + } + }, + "/apis/admissionregistration.k8s.io/v1alpha1/mutatingadmissionpolicybindings/{name}": { + "delete": { + "consumes": [ + "*/*" + ], + "description": "delete a MutatingAdmissionPolicyBinding", + "operationId": "deleteAdmissionregistrationV1alpha1MutatingAdmissionPolicyBinding", + "parameters": [ + { + "$ref": "#/parameters/body-2Y1dVQaQ" + }, + { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "in": "query", + "name": "dryRun", + "type": "string", + "uniqueItems": true + }, + { + "$ref": "#/parameters/gracePeriodSeconds--K5HaBOS" + }, + { + "$ref": "#/parameters/orphanDependents-uRB25kX5" + }, + { + "$ref": "#/parameters/propagationPolicy-6jk3prlO" + } + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "schemes": [ + "https" + ], + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "delete", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBinding", + "version": "v1alpha1" + } + }, + "get": { + "consumes": [ + "*/*" + ], + "description": "read the specified MutatingAdmissionPolicyBinding", + "operationId": "readAdmissionregistrationV1alpha1MutatingAdmissionPolicyBinding", + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "schemes": [ + "https" + ], + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBinding", + "version": "v1alpha1" + } + }, + "parameters": [ + { + "description": "name of the MutatingAdmissionPolicyBinding", + "in": "path", + "name": "name", + "required": true, + "type": "string", + "uniqueItems": true + }, + { + "$ref": "#/parameters/pretty-tJGM1-ng" + } + ], + "patch": { + "consumes": [ + "application/json-patch+json", + "application/merge-patch+json", + "application/strategic-merge-patch+json", + "application/apply-patch+yaml" + ], + "description": "partially update the specified MutatingAdmissionPolicyBinding", + "operationId": "patchAdmissionregistrationV1alpha1MutatingAdmissionPolicyBinding", + "parameters": [ + { + "$ref": "#/parameters/body-78PwaGsr" + }, + { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "in": "query", + "name": "dryRun", + "type": "string", + "uniqueItems": true + }, + { + "$ref": "#/parameters/fieldManager-7c6nTn1T" + }, + { + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "in": "query", + "name": "fieldValidation", + "type": "string", + "uniqueItems": true + }, + { + "$ref": "#/parameters/force-tOGGb0Yi" + } + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "schemes": [ + "https" + ], + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBinding", + "version": "v1alpha1" + } + }, + "put": { + "consumes": [ + "*/*" + ], + "description": "replace the specified MutatingAdmissionPolicyBinding", + "operationId": "replaceAdmissionregistrationV1alpha1MutatingAdmissionPolicyBinding", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "in": "query", + "name": "dryRun", + "type": "string", + "uniqueItems": true + }, + { + "$ref": "#/parameters/fieldManager-Qy4HdaTW" + }, + { + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "in": "query", + "name": "fieldValidation", + "type": "string", + "uniqueItems": true + } + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "schemes": [ + "https" + ], + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBinding", + "version": "v1alpha1" + } + } + }, "/apis/admissionregistration.k8s.io/v1alpha1/validatingadmissionpolicies": { "delete": { "consumes": [ @@ -39338,6 +40534,318 @@ } } }, + "/apis/admissionregistration.k8s.io/v1alpha1/watch/mutatingadmissionpolicies": { + "get": { + "consumes": [ + "*/*" + ], + "description": "watch individual changes to a list of MutatingAdmissionPolicy. deprecated: use the 'watch' parameter with a list operation instead.", + "operationId": "watchAdmissionregistrationV1alpha1MutatingAdmissionPolicyList", + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf", + "application/json;stream=watch", + "application/vnd.kubernetes.protobuf;stream=watch" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "schemes": [ + "https" + ], + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "watchlist", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicy", + "version": "v1alpha1" + } + }, + "parameters": [ + { + "$ref": "#/parameters/allowWatchBookmarks-HC2hJt-J" + }, + { + "$ref": "#/parameters/continue-QfD61s0i" + }, + { + "$ref": "#/parameters/fieldSelector-xIcQKXFG" + }, + { + "$ref": "#/parameters/labelSelector-5Zw57w4C" + }, + { + "$ref": "#/parameters/limit-1NfNmdNH" + }, + { + "$ref": "#/parameters/pretty-tJGM1-ng" + }, + { + "$ref": "#/parameters/resourceVersion-5WAnf1kx" + }, + { + "$ref": "#/parameters/resourceVersionMatch-t8XhRHeC" + }, + { + "$ref": "#/parameters/sendInitialEvents-rLXlEK_k" + }, + { + "$ref": "#/parameters/timeoutSeconds-yvYezaOC" + }, + { + "$ref": "#/parameters/watch-XNNPZGbK" + } + ] + }, + "/apis/admissionregistration.k8s.io/v1alpha1/watch/mutatingadmissionpolicies/{name}": { + "get": { + "consumes": [ + "*/*" + ], + "description": "watch changes to an object of kind MutatingAdmissionPolicy. deprecated: use the 'watch' parameter with a list operation instead, filtered to a single item with the 'fieldSelector' parameter.", + "operationId": "watchAdmissionregistrationV1alpha1MutatingAdmissionPolicy", + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf", + "application/json;stream=watch", + "application/vnd.kubernetes.protobuf;stream=watch" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "schemes": [ + "https" + ], + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "watch", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicy", + "version": "v1alpha1" + } + }, + "parameters": [ + { + "$ref": "#/parameters/allowWatchBookmarks-HC2hJt-J" + }, + { + "$ref": "#/parameters/continue-QfD61s0i" + }, + { + "$ref": "#/parameters/fieldSelector-xIcQKXFG" + }, + { + "$ref": "#/parameters/labelSelector-5Zw57w4C" + }, + { + "$ref": "#/parameters/limit-1NfNmdNH" + }, + { + "description": "name of the MutatingAdmissionPolicy", + "in": "path", + "name": "name", + "required": true, + "type": "string", + "uniqueItems": true + }, + { + "$ref": "#/parameters/pretty-tJGM1-ng" + }, + { + "$ref": "#/parameters/resourceVersion-5WAnf1kx" + }, + { + "$ref": "#/parameters/resourceVersionMatch-t8XhRHeC" + }, + { + "$ref": "#/parameters/sendInitialEvents-rLXlEK_k" + }, + { + "$ref": "#/parameters/timeoutSeconds-yvYezaOC" + }, + { + "$ref": "#/parameters/watch-XNNPZGbK" + } + ] + }, + "/apis/admissionregistration.k8s.io/v1alpha1/watch/mutatingadmissionpolicybindings": { + "get": { + "consumes": [ + "*/*" + ], + "description": "watch individual changes to a list of MutatingAdmissionPolicyBinding. deprecated: use the 'watch' parameter with a list operation instead.", + "operationId": "watchAdmissionregistrationV1alpha1MutatingAdmissionPolicyBindingList", + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf", + "application/json;stream=watch", + "application/vnd.kubernetes.protobuf;stream=watch" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "schemes": [ + "https" + ], + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "watchlist", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBinding", + "version": "v1alpha1" + } + }, + "parameters": [ + { + "$ref": "#/parameters/allowWatchBookmarks-HC2hJt-J" + }, + { + "$ref": "#/parameters/continue-QfD61s0i" + }, + { + "$ref": "#/parameters/fieldSelector-xIcQKXFG" + }, + { + "$ref": "#/parameters/labelSelector-5Zw57w4C" + }, + { + "$ref": "#/parameters/limit-1NfNmdNH" + }, + { + "$ref": "#/parameters/pretty-tJGM1-ng" + }, + { + "$ref": "#/parameters/resourceVersion-5WAnf1kx" + }, + { + "$ref": "#/parameters/resourceVersionMatch-t8XhRHeC" + }, + { + "$ref": "#/parameters/sendInitialEvents-rLXlEK_k" + }, + { + "$ref": "#/parameters/timeoutSeconds-yvYezaOC" + }, + { + "$ref": "#/parameters/watch-XNNPZGbK" + } + ] + }, + "/apis/admissionregistration.k8s.io/v1alpha1/watch/mutatingadmissionpolicybindings/{name}": { + "get": { + "consumes": [ + "*/*" + ], + "description": "watch changes to an object of kind MutatingAdmissionPolicyBinding. deprecated: use the 'watch' parameter with a list operation instead, filtered to a single item with the 'fieldSelector' parameter.", + "operationId": "watchAdmissionregistrationV1alpha1MutatingAdmissionPolicyBinding", + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf", + "application/json;stream=watch", + "application/vnd.kubernetes.protobuf;stream=watch" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "schemes": [ + "https" + ], + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "watch", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBinding", + "version": "v1alpha1" + } + }, + "parameters": [ + { + "$ref": "#/parameters/allowWatchBookmarks-HC2hJt-J" + }, + { + "$ref": "#/parameters/continue-QfD61s0i" + }, + { + "$ref": "#/parameters/fieldSelector-xIcQKXFG" + }, + { + "$ref": "#/parameters/labelSelector-5Zw57w4C" + }, + { + "$ref": "#/parameters/limit-1NfNmdNH" + }, + { + "description": "name of the MutatingAdmissionPolicyBinding", + "in": "path", + "name": "name", + "required": true, + "type": "string", + "uniqueItems": true + }, + { + "$ref": "#/parameters/pretty-tJGM1-ng" + }, + { + "$ref": "#/parameters/resourceVersion-5WAnf1kx" + }, + { + "$ref": "#/parameters/resourceVersionMatch-t8XhRHeC" + }, + { + "$ref": "#/parameters/sendInitialEvents-rLXlEK_k" + }, + { + "$ref": "#/parameters/timeoutSeconds-yvYezaOC" + }, + { + "$ref": "#/parameters/watch-XNNPZGbK" + } + ] + }, "/apis/admissionregistration.k8s.io/v1alpha1/watch/validatingadmissionpolicies": { "get": { "consumes": [ diff --git a/api/openapi-spec/v3/apis__admissionregistration.k8s.io__v1alpha1_openapi.json b/api/openapi-spec/v3/apis__admissionregistration.k8s.io__v1alpha1_openapi.json index 9cbee0c85e7..444b7eafb8a 100644 --- a/api/openapi-spec/v3/apis__admissionregistration.k8s.io__v1alpha1_openapi.json +++ b/api/openapi-spec/v3/apis__admissionregistration.k8s.io__v1alpha1_openapi.json @@ -1,6 +1,16 @@ { "components": { "schemas": { + "io.k8s.api.admissionregistration.v1alpha1.ApplyConfiguration": { + "description": "ApplyConfiguration defines the desired configuration values of an object.", + "properties": { + "expression": { + "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.' - CEL type of object field (such as 'Object.spec') - 'Object.....` - 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" + } + }, + "type": "object" + }, "io.k8s.api.admissionregistration.v1alpha1.AuditAnnotation": { "description": "AuditAnnotation describes how to produce an audit annotation for an API request.", "properties": { @@ -41,6 +51,16 @@ ], "type": "object" }, + "io.k8s.api.admissionregistration.v1alpha1.JSONPatch": { + "description": "JSONPatch defines a JSON Patch.", + "properties": { + "expression": { + "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.' - CEL type of object field (such as 'Object.spec') - 'Object.....` - 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" + } + }, + "type": "object" + }, "io.k8s.api.admissionregistration.v1alpha1.MatchCondition": { "properties": { "expression": { @@ -113,6 +133,304 @@ "type": "object", "x-kubernetes-map-type": "atomic" }, + "io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy": { + "description": "MutatingAdmissionPolicy describes the definition of an admission mutation policy that mutates the object coming into admission chain.", + "properties": { + "apiVersion": { + "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" + }, + "kind": { + "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" + }, + "metadata": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ], + "default": {}, + "description": "Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata." + }, + "spec": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicySpec" + } + ], + "default": {}, + "description": "Specification of the desired behavior of the MutatingAdmissionPolicy." + } + }, + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicy", + "version": "v1alpha1" + } + ] + }, + "io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding": { + "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.", + "properties": { + "apiVersion": { + "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" + }, + "kind": { + "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" + }, + "metadata": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ], + "default": {}, + "description": "Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata." + }, + "spec": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBindingSpec" + } + ], + "default": {}, + "description": "Specification of the desired behavior of the MutatingAdmissionPolicyBinding." + } + }, + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBinding", + "version": "v1alpha1" + } + ] + }, + "io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBindingList": { + "description": "MutatingAdmissionPolicyBindingList is a list of MutatingAdmissionPolicyBinding.", + "properties": { + "apiVersion": { + "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" + }, + "items": { + "description": "List of PolicyBinding.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + ], + "default": {} + }, + "type": "array" + }, + "kind": { + "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" + }, + "metadata": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ], + "default": {}, + "description": "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds" + } + }, + "required": [ + "items" + ], + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBindingList", + "version": "v1alpha1" + } + ] + }, + "io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBindingSpec": { + "description": "MutatingAdmissionPolicyBindingSpec is the specification of the MutatingAdmissionPolicyBinding.", + "properties": { + "matchResources": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MatchResources" + } + ], + "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." + }, + "paramRef": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.ParamRef" + } + ], + "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." + }, + "policyName": { + "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" + } + }, + "type": "object" + }, + "io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyList": { + "description": "MutatingAdmissionPolicyList is a list of MutatingAdmissionPolicy.", + "properties": { + "apiVersion": { + "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" + }, + "items": { + "description": "List of ValidatingAdmissionPolicy.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + ], + "default": {} + }, + "type": "array" + }, + "kind": { + "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" + }, + "metadata": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ], + "default": {}, + "description": "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds" + } + }, + "required": [ + "items" + ], + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyList", + "version": "v1alpha1" + } + ] + }, + "io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicySpec": { + "description": "MutatingAdmissionPolicySpec is the specification of the desired behavior of the admission policy.", + "properties": { + "failurePolicy": { + "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.", + "type": "string" + }, + "matchConditions": { + "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", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MatchCondition" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, + "matchConstraints": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MatchResources" + } + ], + "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." + }, + "mutations": { + "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.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.Mutation" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "paramKind": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.ParamKind" + } + ], + "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." + }, + "reinvocationPolicy": { + "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.", + "type": "string" + }, + "variables": { + "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.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.Variable" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + } + }, + "type": "object" + }, + "io.k8s.api.admissionregistration.v1alpha1.Mutation": { + "description": "Mutation specifies the CEL expression which is used to apply the Mutation.", + "properties": { + "applyConfiguration": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.ApplyConfiguration" + } + ], + "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." + }, + "jsonPatch": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.JSONPatch" + } + ], + "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." + }, + "patchType": { + "default": "", + "description": "patchType indicates the patch strategy used. Allowed values are \"ApplyConfiguration\" and \"JSONPatch\". Required.", + "type": "string" + } + }, + "required": [ + "patchType" + ], + "type": "object" + }, "io.k8s.api.admissionregistration.v1alpha1.NamedRuleWithOperations": { "description": "NamedRuleWithOperations is a tuple of Operations and Resources with ResourceNames.", "properties": { @@ -1912,6 +2230,1628 @@ ] } }, + "/apis/admissionregistration.k8s.io/v1alpha1/mutatingadmissionpolicies": { + "delete": { + "description": "delete collection of MutatingAdmissionPolicy", + "operationId": "deleteAdmissionregistrationV1alpha1CollectionMutatingAdmissionPolicy", + "parameters": [ + { + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "in": "query", + "name": "continue", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "in": "query", + "name": "dryRun", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "in": "query", + "name": "fieldSelector", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "in": "query", + "name": "gracePeriodSeconds", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "in": "query", + "name": "labelSelector", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "in": "query", + "name": "orphanDependents", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "in": "query", + "name": "propagationPolicy", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "in": "query", + "name": "resourceVersion", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "in": "query", + "name": "resourceVersionMatch", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "in": "query", + "name": "sendInitialEvents", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "in": "query", + "name": "timeoutSeconds", + "schema": { + "type": "integer", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + }, + "description": "OK" + }, + "401": { + "description": "Unauthorized" + } + }, + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "deletecollection", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicy", + "version": "v1alpha1" + } + }, + "get": { + "description": "list or watch objects of kind MutatingAdmissionPolicy", + "operationId": "listAdmissionregistrationV1alpha1MutatingAdmissionPolicy", + "parameters": [ + { + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "in": "query", + "name": "allowWatchBookmarks", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "in": "query", + "name": "continue", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "in": "query", + "name": "fieldSelector", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "in": "query", + "name": "labelSelector", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "in": "query", + "name": "resourceVersion", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "in": "query", + "name": "resourceVersionMatch", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "in": "query", + "name": "sendInitialEvents", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "in": "query", + "name": "timeoutSeconds", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "in": "query", + "name": "watch", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyList" + } + } + }, + "description": "OK" + }, + "401": { + "description": "Unauthorized" + } + }, + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicy", + "version": "v1alpha1" + } + }, + "parameters": [ + { + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "in": "query", + "name": "pretty", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "post": { + "description": "create a MutatingAdmissionPolicy", + "operationId": "createAdmissionregistrationV1alpha1MutatingAdmissionPolicy", + "parameters": [ + { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "in": "query", + "name": "dryRun", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "in": "query", + "name": "fieldManager", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "in": "query", + "name": "fieldValidation", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + } + }, + "description": "OK" + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + } + }, + "description": "Created" + }, + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + } + }, + "description": "Accepted" + }, + "401": { + "description": "Unauthorized" + } + }, + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "post", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicy", + "version": "v1alpha1" + } + } + }, + "/apis/admissionregistration.k8s.io/v1alpha1/mutatingadmissionpolicies/{name}": { + "delete": { + "description": "delete a MutatingAdmissionPolicy", + "operationId": "deleteAdmissionregistrationV1alpha1MutatingAdmissionPolicy", + "parameters": [ + { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "in": "query", + "name": "dryRun", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "in": "query", + "name": "gracePeriodSeconds", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "in": "query", + "name": "orphanDependents", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "in": "query", + "name": "propagationPolicy", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + }, + "description": "OK" + }, + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + }, + "description": "Accepted" + }, + "401": { + "description": "Unauthorized" + } + }, + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "delete", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicy", + "version": "v1alpha1" + } + }, + "get": { + "description": "read the specified MutatingAdmissionPolicy", + "operationId": "readAdmissionregistrationV1alpha1MutatingAdmissionPolicy", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + } + }, + "description": "OK" + }, + "401": { + "description": "Unauthorized" + } + }, + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicy", + "version": "v1alpha1" + } + }, + "parameters": [ + { + "description": "name of the MutatingAdmissionPolicy", + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "in": "query", + "name": "pretty", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "patch": { + "description": "partially update the specified MutatingAdmissionPolicy", + "operationId": "patchAdmissionregistrationV1alpha1MutatingAdmissionPolicy", + "parameters": [ + { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "in": "query", + "name": "dryRun", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "in": "query", + "name": "fieldManager", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "in": "query", + "name": "fieldValidation", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "in": "query", + "name": "force", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + } + }, + "description": "OK" + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + } + }, + "description": "Created" + }, + "401": { + "description": "Unauthorized" + } + }, + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicy", + "version": "v1alpha1" + } + }, + "put": { + "description": "replace the specified MutatingAdmissionPolicy", + "operationId": "replaceAdmissionregistrationV1alpha1MutatingAdmissionPolicy", + "parameters": [ + { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "in": "query", + "name": "dryRun", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "in": "query", + "name": "fieldManager", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "in": "query", + "name": "fieldValidation", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + } + }, + "description": "OK" + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy" + } + } + }, + "description": "Created" + }, + "401": { + "description": "Unauthorized" + } + }, + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicy", + "version": "v1alpha1" + } + } + }, + "/apis/admissionregistration.k8s.io/v1alpha1/mutatingadmissionpolicybindings": { + "delete": { + "description": "delete collection of MutatingAdmissionPolicyBinding", + "operationId": "deleteAdmissionregistrationV1alpha1CollectionMutatingAdmissionPolicyBinding", + "parameters": [ + { + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "in": "query", + "name": "continue", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "in": "query", + "name": "dryRun", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "in": "query", + "name": "fieldSelector", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "in": "query", + "name": "gracePeriodSeconds", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "in": "query", + "name": "labelSelector", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "in": "query", + "name": "orphanDependents", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "in": "query", + "name": "propagationPolicy", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "in": "query", + "name": "resourceVersion", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "in": "query", + "name": "resourceVersionMatch", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "in": "query", + "name": "sendInitialEvents", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "in": "query", + "name": "timeoutSeconds", + "schema": { + "type": "integer", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + }, + "description": "OK" + }, + "401": { + "description": "Unauthorized" + } + }, + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "deletecollection", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBinding", + "version": "v1alpha1" + } + }, + "get": { + "description": "list or watch objects of kind MutatingAdmissionPolicyBinding", + "operationId": "listAdmissionregistrationV1alpha1MutatingAdmissionPolicyBinding", + "parameters": [ + { + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "in": "query", + "name": "allowWatchBookmarks", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "in": "query", + "name": "continue", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "in": "query", + "name": "fieldSelector", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "in": "query", + "name": "labelSelector", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "in": "query", + "name": "resourceVersion", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "in": "query", + "name": "resourceVersionMatch", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "in": "query", + "name": "sendInitialEvents", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "in": "query", + "name": "timeoutSeconds", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "in": "query", + "name": "watch", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBindingList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBindingList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBindingList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBindingList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBindingList" + } + } + }, + "description": "OK" + }, + "401": { + "description": "Unauthorized" + } + }, + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBinding", + "version": "v1alpha1" + } + }, + "parameters": [ + { + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "in": "query", + "name": "pretty", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "post": { + "description": "create a MutatingAdmissionPolicyBinding", + "operationId": "createAdmissionregistrationV1alpha1MutatingAdmissionPolicyBinding", + "parameters": [ + { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "in": "query", + "name": "dryRun", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "in": "query", + "name": "fieldManager", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "in": "query", + "name": "fieldValidation", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + } + }, + "description": "OK" + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + } + }, + "description": "Created" + }, + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + } + }, + "description": "Accepted" + }, + "401": { + "description": "Unauthorized" + } + }, + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "post", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBinding", + "version": "v1alpha1" + } + } + }, + "/apis/admissionregistration.k8s.io/v1alpha1/mutatingadmissionpolicybindings/{name}": { + "delete": { + "description": "delete a MutatingAdmissionPolicyBinding", + "operationId": "deleteAdmissionregistrationV1alpha1MutatingAdmissionPolicyBinding", + "parameters": [ + { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "in": "query", + "name": "dryRun", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "in": "query", + "name": "gracePeriodSeconds", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "in": "query", + "name": "orphanDependents", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "in": "query", + "name": "propagationPolicy", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + }, + "description": "OK" + }, + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + }, + "description": "Accepted" + }, + "401": { + "description": "Unauthorized" + } + }, + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "delete", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBinding", + "version": "v1alpha1" + } + }, + "get": { + "description": "read the specified MutatingAdmissionPolicyBinding", + "operationId": "readAdmissionregistrationV1alpha1MutatingAdmissionPolicyBinding", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + } + }, + "description": "OK" + }, + "401": { + "description": "Unauthorized" + } + }, + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBinding", + "version": "v1alpha1" + } + }, + "parameters": [ + { + "description": "name of the MutatingAdmissionPolicyBinding", + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "in": "query", + "name": "pretty", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "patch": { + "description": "partially update the specified MutatingAdmissionPolicyBinding", + "operationId": "patchAdmissionregistrationV1alpha1MutatingAdmissionPolicyBinding", + "parameters": [ + { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "in": "query", + "name": "dryRun", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "in": "query", + "name": "fieldManager", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "in": "query", + "name": "fieldValidation", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "in": "query", + "name": "force", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + } + }, + "description": "OK" + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + } + }, + "description": "Created" + }, + "401": { + "description": "Unauthorized" + } + }, + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBinding", + "version": "v1alpha1" + } + }, + "put": { + "description": "replace the specified MutatingAdmissionPolicyBinding", + "operationId": "replaceAdmissionregistrationV1alpha1MutatingAdmissionPolicyBinding", + "parameters": [ + { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "in": "query", + "name": "dryRun", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "in": "query", + "name": "fieldManager", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "in": "query", + "name": "fieldValidation", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + } + }, + "description": "OK" + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding" + } + } + }, + "description": "Created" + }, + "401": { + "description": "Unauthorized" + } + }, + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBinding", + "version": "v1alpha1" + } + } + }, "/apis/admissionregistration.k8s.io/v1alpha1/validatingadmissionpolicies": { "delete": { "description": "delete collection of ValidatingAdmissionPolicy", @@ -3813,6 +5753,630 @@ } } }, + "/apis/admissionregistration.k8s.io/v1alpha1/watch/mutatingadmissionpolicies": { + "get": { + "description": "watch individual changes to a list of MutatingAdmissionPolicy. deprecated: use the 'watch' parameter with a list operation instead.", + "operationId": "watchAdmissionregistrationV1alpha1MutatingAdmissionPolicyList", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + } + }, + "description": "OK" + }, + "401": { + "description": "Unauthorized" + } + }, + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "watchlist", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicy", + "version": "v1alpha1" + } + }, + "parameters": [ + { + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "in": "query", + "name": "allowWatchBookmarks", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "in": "query", + "name": "continue", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "in": "query", + "name": "fieldSelector", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "in": "query", + "name": "labelSelector", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "in": "query", + "name": "pretty", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "in": "query", + "name": "resourceVersion", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "in": "query", + "name": "resourceVersionMatch", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "in": "query", + "name": "sendInitialEvents", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "in": "query", + "name": "timeoutSeconds", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "in": "query", + "name": "watch", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ] + }, + "/apis/admissionregistration.k8s.io/v1alpha1/watch/mutatingadmissionpolicies/{name}": { + "get": { + "description": "watch changes to an object of kind MutatingAdmissionPolicy. deprecated: use the 'watch' parameter with a list operation instead, filtered to a single item with the 'fieldSelector' parameter.", + "operationId": "watchAdmissionregistrationV1alpha1MutatingAdmissionPolicy", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + } + }, + "description": "OK" + }, + "401": { + "description": "Unauthorized" + } + }, + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "watch", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicy", + "version": "v1alpha1" + } + }, + "parameters": [ + { + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "in": "query", + "name": "allowWatchBookmarks", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "in": "query", + "name": "continue", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "in": "query", + "name": "fieldSelector", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "in": "query", + "name": "labelSelector", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "description": "name of the MutatingAdmissionPolicy", + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "in": "query", + "name": "pretty", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "in": "query", + "name": "resourceVersion", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "in": "query", + "name": "resourceVersionMatch", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "in": "query", + "name": "sendInitialEvents", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "in": "query", + "name": "timeoutSeconds", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "in": "query", + "name": "watch", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ] + }, + "/apis/admissionregistration.k8s.io/v1alpha1/watch/mutatingadmissionpolicybindings": { + "get": { + "description": "watch individual changes to a list of MutatingAdmissionPolicyBinding. deprecated: use the 'watch' parameter with a list operation instead.", + "operationId": "watchAdmissionregistrationV1alpha1MutatingAdmissionPolicyBindingList", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + } + }, + "description": "OK" + }, + "401": { + "description": "Unauthorized" + } + }, + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "watchlist", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBinding", + "version": "v1alpha1" + } + }, + "parameters": [ + { + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "in": "query", + "name": "allowWatchBookmarks", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "in": "query", + "name": "continue", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "in": "query", + "name": "fieldSelector", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "in": "query", + "name": "labelSelector", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "in": "query", + "name": "pretty", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "in": "query", + "name": "resourceVersion", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "in": "query", + "name": "resourceVersionMatch", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "in": "query", + "name": "sendInitialEvents", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "in": "query", + "name": "timeoutSeconds", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "in": "query", + "name": "watch", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ] + }, + "/apis/admissionregistration.k8s.io/v1alpha1/watch/mutatingadmissionpolicybindings/{name}": { + "get": { + "description": "watch changes to an object of kind MutatingAdmissionPolicyBinding. deprecated: use the 'watch' parameter with a list operation instead, filtered to a single item with the 'fieldSelector' parameter.", + "operationId": "watchAdmissionregistrationV1alpha1MutatingAdmissionPolicyBinding", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + } + }, + "description": "OK" + }, + "401": { + "description": "Unauthorized" + } + }, + "tags": [ + "admissionregistration_v1alpha1" + ], + "x-kubernetes-action": "watch", + "x-kubernetes-group-version-kind": { + "group": "admissionregistration.k8s.io", + "kind": "MutatingAdmissionPolicyBinding", + "version": "v1alpha1" + } + }, + "parameters": [ + { + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "in": "query", + "name": "allowWatchBookmarks", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "in": "query", + "name": "continue", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "in": "query", + "name": "fieldSelector", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "in": "query", + "name": "labelSelector", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "description": "name of the MutatingAdmissionPolicyBinding", + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "in": "query", + "name": "pretty", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "in": "query", + "name": "resourceVersion", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "in": "query", + "name": "resourceVersionMatch", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "in": "query", + "name": "sendInitialEvents", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "in": "query", + "name": "timeoutSeconds", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "in": "query", + "name": "watch", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ] + }, "/apis/admissionregistration.k8s.io/v1alpha1/watch/validatingadmissionpolicies": { "get": { "description": "watch individual changes to a list of ValidatingAdmissionPolicy. deprecated: use the 'watch' parameter with a list operation instead.", diff --git a/pkg/apis/admissionregistration/v1alpha1/zz_generated.conversion.go b/pkg/apis/admissionregistration/v1alpha1/zz_generated.conversion.go index 35a26d202c5..838c8ff200d 100644 --- a/pkg/apis/admissionregistration/v1alpha1/zz_generated.conversion.go +++ b/pkg/apis/admissionregistration/v1alpha1/zz_generated.conversion.go @@ -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 diff --git a/pkg/apis/admissionregistration/v1alpha1/zz_generated.defaults.go b/pkg/apis/admissionregistration/v1alpha1/zz_generated.defaults.go index bbb7582f1ad..e42a7793e86 100644 --- a/pkg/apis/admissionregistration/v1alpha1/zz_generated.defaults.go +++ b/pkg/apis/admissionregistration/v1alpha1/zz_generated.defaults.go @@ -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 { diff --git a/pkg/apis/admissionregistration/zz_generated.deepcopy.go b/pkg/apis/admissionregistration/zz_generated.deepcopy.go index 1c286ee85ab..e4b841a0b16 100644 --- a/pkg/apis/admissionregistration/zz_generated.deepcopy.go +++ b/pkg/apis/admissionregistration/zz_generated.deepcopy.go @@ -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 diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index 41e0b0fa592..5fe9ad5b780 100644 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -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.' - CEL type of object field (such as 'Object.spec') - 'Object.....` - 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.' - CEL type of object field (such as 'Object.spec') - 'Object.....` - 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{ diff --git a/staging/src/k8s.io/api/admissionregistration/v1alpha1/generated.pb.go b/staging/src/k8s.io/api/admissionregistration/v1alpha1/generated.pb.go index 111cc728741..993ff6f20eb 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1alpha1/generated.pb.go +++ b/staging/src/k8s.io/api/admissionregistration/v1alpha1/generated.pb.go @@ -25,6 +25,7 @@ import ( io "io" proto "github.com/gogo/protobuf/proto" + k8s_io_api_admissionregistration_v1 "k8s.io/api/admissionregistration/v1" k8s_io_apimachinery_pkg_apis_meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -45,10 +46,38 @@ var _ = math.Inf // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package +func (m *ApplyConfiguration) Reset() { *m = ApplyConfiguration{} } +func (*ApplyConfiguration) ProtoMessage() {} +func (*ApplyConfiguration) Descriptor() ([]byte, []int) { + return fileDescriptor_2c49182728ae0af5, []int{0} +} +func (m *ApplyConfiguration) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *ApplyConfiguration) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil +} +func (m *ApplyConfiguration) XXX_Merge(src proto.Message) { + xxx_messageInfo_ApplyConfiguration.Merge(m, src) +} +func (m *ApplyConfiguration) XXX_Size() int { + return m.Size() +} +func (m *ApplyConfiguration) XXX_DiscardUnknown() { + xxx_messageInfo_ApplyConfiguration.DiscardUnknown(m) +} + +var xxx_messageInfo_ApplyConfiguration proto.InternalMessageInfo + func (m *AuditAnnotation) Reset() { *m = AuditAnnotation{} } func (*AuditAnnotation) ProtoMessage() {} func (*AuditAnnotation) Descriptor() ([]byte, []int) { - return fileDescriptor_2c49182728ae0af5, []int{0} + return fileDescriptor_2c49182728ae0af5, []int{1} } func (m *AuditAnnotation) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -76,7 +105,7 @@ var xxx_messageInfo_AuditAnnotation proto.InternalMessageInfo func (m *ExpressionWarning) Reset() { *m = ExpressionWarning{} } func (*ExpressionWarning) ProtoMessage() {} func (*ExpressionWarning) Descriptor() ([]byte, []int) { - return fileDescriptor_2c49182728ae0af5, []int{1} + return fileDescriptor_2c49182728ae0af5, []int{2} } func (m *ExpressionWarning) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -101,10 +130,38 @@ func (m *ExpressionWarning) XXX_DiscardUnknown() { var xxx_messageInfo_ExpressionWarning proto.InternalMessageInfo +func (m *JSONPatch) Reset() { *m = JSONPatch{} } +func (*JSONPatch) ProtoMessage() {} +func (*JSONPatch) Descriptor() ([]byte, []int) { + return fileDescriptor_2c49182728ae0af5, []int{3} +} +func (m *JSONPatch) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *JSONPatch) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil +} +func (m *JSONPatch) XXX_Merge(src proto.Message) { + xxx_messageInfo_JSONPatch.Merge(m, src) +} +func (m *JSONPatch) XXX_Size() int { + return m.Size() +} +func (m *JSONPatch) XXX_DiscardUnknown() { + xxx_messageInfo_JSONPatch.DiscardUnknown(m) +} + +var xxx_messageInfo_JSONPatch proto.InternalMessageInfo + func (m *MatchCondition) Reset() { *m = MatchCondition{} } func (*MatchCondition) ProtoMessage() {} func (*MatchCondition) Descriptor() ([]byte, []int) { - return fileDescriptor_2c49182728ae0af5, []int{2} + return fileDescriptor_2c49182728ae0af5, []int{4} } func (m *MatchCondition) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -132,7 +189,7 @@ var xxx_messageInfo_MatchCondition proto.InternalMessageInfo func (m *MatchResources) Reset() { *m = MatchResources{} } func (*MatchResources) ProtoMessage() {} func (*MatchResources) Descriptor() ([]byte, []int) { - return fileDescriptor_2c49182728ae0af5, []int{3} + return fileDescriptor_2c49182728ae0af5, []int{5} } func (m *MatchResources) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -157,10 +214,206 @@ func (m *MatchResources) XXX_DiscardUnknown() { var xxx_messageInfo_MatchResources proto.InternalMessageInfo +func (m *MutatingAdmissionPolicy) Reset() { *m = MutatingAdmissionPolicy{} } +func (*MutatingAdmissionPolicy) ProtoMessage() {} +func (*MutatingAdmissionPolicy) Descriptor() ([]byte, []int) { + return fileDescriptor_2c49182728ae0af5, []int{6} +} +func (m *MutatingAdmissionPolicy) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MutatingAdmissionPolicy) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil +} +func (m *MutatingAdmissionPolicy) XXX_Merge(src proto.Message) { + xxx_messageInfo_MutatingAdmissionPolicy.Merge(m, src) +} +func (m *MutatingAdmissionPolicy) XXX_Size() int { + return m.Size() +} +func (m *MutatingAdmissionPolicy) XXX_DiscardUnknown() { + xxx_messageInfo_MutatingAdmissionPolicy.DiscardUnknown(m) +} + +var xxx_messageInfo_MutatingAdmissionPolicy proto.InternalMessageInfo + +func (m *MutatingAdmissionPolicyBinding) Reset() { *m = MutatingAdmissionPolicyBinding{} } +func (*MutatingAdmissionPolicyBinding) ProtoMessage() {} +func (*MutatingAdmissionPolicyBinding) Descriptor() ([]byte, []int) { + return fileDescriptor_2c49182728ae0af5, []int{7} +} +func (m *MutatingAdmissionPolicyBinding) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MutatingAdmissionPolicyBinding) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil +} +func (m *MutatingAdmissionPolicyBinding) XXX_Merge(src proto.Message) { + xxx_messageInfo_MutatingAdmissionPolicyBinding.Merge(m, src) +} +func (m *MutatingAdmissionPolicyBinding) XXX_Size() int { + return m.Size() +} +func (m *MutatingAdmissionPolicyBinding) XXX_DiscardUnknown() { + xxx_messageInfo_MutatingAdmissionPolicyBinding.DiscardUnknown(m) +} + +var xxx_messageInfo_MutatingAdmissionPolicyBinding proto.InternalMessageInfo + +func (m *MutatingAdmissionPolicyBindingList) Reset() { *m = MutatingAdmissionPolicyBindingList{} } +func (*MutatingAdmissionPolicyBindingList) ProtoMessage() {} +func (*MutatingAdmissionPolicyBindingList) Descriptor() ([]byte, []int) { + return fileDescriptor_2c49182728ae0af5, []int{8} +} +func (m *MutatingAdmissionPolicyBindingList) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MutatingAdmissionPolicyBindingList) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil +} +func (m *MutatingAdmissionPolicyBindingList) XXX_Merge(src proto.Message) { + xxx_messageInfo_MutatingAdmissionPolicyBindingList.Merge(m, src) +} +func (m *MutatingAdmissionPolicyBindingList) XXX_Size() int { + return m.Size() +} +func (m *MutatingAdmissionPolicyBindingList) XXX_DiscardUnknown() { + xxx_messageInfo_MutatingAdmissionPolicyBindingList.DiscardUnknown(m) +} + +var xxx_messageInfo_MutatingAdmissionPolicyBindingList proto.InternalMessageInfo + +func (m *MutatingAdmissionPolicyBindingSpec) Reset() { *m = MutatingAdmissionPolicyBindingSpec{} } +func (*MutatingAdmissionPolicyBindingSpec) ProtoMessage() {} +func (*MutatingAdmissionPolicyBindingSpec) Descriptor() ([]byte, []int) { + return fileDescriptor_2c49182728ae0af5, []int{9} +} +func (m *MutatingAdmissionPolicyBindingSpec) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MutatingAdmissionPolicyBindingSpec) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil +} +func (m *MutatingAdmissionPolicyBindingSpec) XXX_Merge(src proto.Message) { + xxx_messageInfo_MutatingAdmissionPolicyBindingSpec.Merge(m, src) +} +func (m *MutatingAdmissionPolicyBindingSpec) XXX_Size() int { + return m.Size() +} +func (m *MutatingAdmissionPolicyBindingSpec) XXX_DiscardUnknown() { + xxx_messageInfo_MutatingAdmissionPolicyBindingSpec.DiscardUnknown(m) +} + +var xxx_messageInfo_MutatingAdmissionPolicyBindingSpec proto.InternalMessageInfo + +func (m *MutatingAdmissionPolicyList) Reset() { *m = MutatingAdmissionPolicyList{} } +func (*MutatingAdmissionPolicyList) ProtoMessage() {} +func (*MutatingAdmissionPolicyList) Descriptor() ([]byte, []int) { + return fileDescriptor_2c49182728ae0af5, []int{10} +} +func (m *MutatingAdmissionPolicyList) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MutatingAdmissionPolicyList) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil +} +func (m *MutatingAdmissionPolicyList) XXX_Merge(src proto.Message) { + xxx_messageInfo_MutatingAdmissionPolicyList.Merge(m, src) +} +func (m *MutatingAdmissionPolicyList) XXX_Size() int { + return m.Size() +} +func (m *MutatingAdmissionPolicyList) XXX_DiscardUnknown() { + xxx_messageInfo_MutatingAdmissionPolicyList.DiscardUnknown(m) +} + +var xxx_messageInfo_MutatingAdmissionPolicyList proto.InternalMessageInfo + +func (m *MutatingAdmissionPolicySpec) Reset() { *m = MutatingAdmissionPolicySpec{} } +func (*MutatingAdmissionPolicySpec) ProtoMessage() {} +func (*MutatingAdmissionPolicySpec) Descriptor() ([]byte, []int) { + return fileDescriptor_2c49182728ae0af5, []int{11} +} +func (m *MutatingAdmissionPolicySpec) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MutatingAdmissionPolicySpec) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil +} +func (m *MutatingAdmissionPolicySpec) XXX_Merge(src proto.Message) { + xxx_messageInfo_MutatingAdmissionPolicySpec.Merge(m, src) +} +func (m *MutatingAdmissionPolicySpec) XXX_Size() int { + return m.Size() +} +func (m *MutatingAdmissionPolicySpec) XXX_DiscardUnknown() { + xxx_messageInfo_MutatingAdmissionPolicySpec.DiscardUnknown(m) +} + +var xxx_messageInfo_MutatingAdmissionPolicySpec proto.InternalMessageInfo + +func (m *Mutation) Reset() { *m = Mutation{} } +func (*Mutation) ProtoMessage() {} +func (*Mutation) Descriptor() ([]byte, []int) { + return fileDescriptor_2c49182728ae0af5, []int{12} +} +func (m *Mutation) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Mutation) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil +} +func (m *Mutation) XXX_Merge(src proto.Message) { + xxx_messageInfo_Mutation.Merge(m, src) +} +func (m *Mutation) XXX_Size() int { + return m.Size() +} +func (m *Mutation) XXX_DiscardUnknown() { + xxx_messageInfo_Mutation.DiscardUnknown(m) +} + +var xxx_messageInfo_Mutation proto.InternalMessageInfo + func (m *NamedRuleWithOperations) Reset() { *m = NamedRuleWithOperations{} } func (*NamedRuleWithOperations) ProtoMessage() {} func (*NamedRuleWithOperations) Descriptor() ([]byte, []int) { - return fileDescriptor_2c49182728ae0af5, []int{4} + return fileDescriptor_2c49182728ae0af5, []int{13} } func (m *NamedRuleWithOperations) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -188,7 +441,7 @@ var xxx_messageInfo_NamedRuleWithOperations proto.InternalMessageInfo func (m *ParamKind) Reset() { *m = ParamKind{} } func (*ParamKind) ProtoMessage() {} func (*ParamKind) Descriptor() ([]byte, []int) { - return fileDescriptor_2c49182728ae0af5, []int{5} + return fileDescriptor_2c49182728ae0af5, []int{14} } func (m *ParamKind) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -216,7 +469,7 @@ var xxx_messageInfo_ParamKind proto.InternalMessageInfo func (m *ParamRef) Reset() { *m = ParamRef{} } func (*ParamRef) ProtoMessage() {} func (*ParamRef) Descriptor() ([]byte, []int) { - return fileDescriptor_2c49182728ae0af5, []int{6} + return fileDescriptor_2c49182728ae0af5, []int{15} } func (m *ParamRef) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -244,7 +497,7 @@ var xxx_messageInfo_ParamRef proto.InternalMessageInfo func (m *TypeChecking) Reset() { *m = TypeChecking{} } func (*TypeChecking) ProtoMessage() {} func (*TypeChecking) Descriptor() ([]byte, []int) { - return fileDescriptor_2c49182728ae0af5, []int{7} + return fileDescriptor_2c49182728ae0af5, []int{16} } func (m *TypeChecking) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -272,7 +525,7 @@ var xxx_messageInfo_TypeChecking proto.InternalMessageInfo func (m *ValidatingAdmissionPolicy) Reset() { *m = ValidatingAdmissionPolicy{} } func (*ValidatingAdmissionPolicy) ProtoMessage() {} func (*ValidatingAdmissionPolicy) Descriptor() ([]byte, []int) { - return fileDescriptor_2c49182728ae0af5, []int{8} + return fileDescriptor_2c49182728ae0af5, []int{17} } func (m *ValidatingAdmissionPolicy) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -300,7 +553,7 @@ var xxx_messageInfo_ValidatingAdmissionPolicy proto.InternalMessageInfo func (m *ValidatingAdmissionPolicyBinding) Reset() { *m = ValidatingAdmissionPolicyBinding{} } func (*ValidatingAdmissionPolicyBinding) ProtoMessage() {} func (*ValidatingAdmissionPolicyBinding) Descriptor() ([]byte, []int) { - return fileDescriptor_2c49182728ae0af5, []int{9} + return fileDescriptor_2c49182728ae0af5, []int{18} } func (m *ValidatingAdmissionPolicyBinding) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -328,7 +581,7 @@ var xxx_messageInfo_ValidatingAdmissionPolicyBinding proto.InternalMessageInfo func (m *ValidatingAdmissionPolicyBindingList) Reset() { *m = ValidatingAdmissionPolicyBindingList{} } func (*ValidatingAdmissionPolicyBindingList) ProtoMessage() {} func (*ValidatingAdmissionPolicyBindingList) Descriptor() ([]byte, []int) { - return fileDescriptor_2c49182728ae0af5, []int{10} + return fileDescriptor_2c49182728ae0af5, []int{19} } func (m *ValidatingAdmissionPolicyBindingList) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -356,7 +609,7 @@ var xxx_messageInfo_ValidatingAdmissionPolicyBindingList proto.InternalMessageIn func (m *ValidatingAdmissionPolicyBindingSpec) Reset() { *m = ValidatingAdmissionPolicyBindingSpec{} } func (*ValidatingAdmissionPolicyBindingSpec) ProtoMessage() {} func (*ValidatingAdmissionPolicyBindingSpec) Descriptor() ([]byte, []int) { - return fileDescriptor_2c49182728ae0af5, []int{11} + return fileDescriptor_2c49182728ae0af5, []int{20} } func (m *ValidatingAdmissionPolicyBindingSpec) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -384,7 +637,7 @@ var xxx_messageInfo_ValidatingAdmissionPolicyBindingSpec proto.InternalMessageIn func (m *ValidatingAdmissionPolicyList) Reset() { *m = ValidatingAdmissionPolicyList{} } func (*ValidatingAdmissionPolicyList) ProtoMessage() {} func (*ValidatingAdmissionPolicyList) Descriptor() ([]byte, []int) { - return fileDescriptor_2c49182728ae0af5, []int{12} + return fileDescriptor_2c49182728ae0af5, []int{21} } func (m *ValidatingAdmissionPolicyList) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -412,7 +665,7 @@ var xxx_messageInfo_ValidatingAdmissionPolicyList proto.InternalMessageInfo func (m *ValidatingAdmissionPolicySpec) Reset() { *m = ValidatingAdmissionPolicySpec{} } func (*ValidatingAdmissionPolicySpec) ProtoMessage() {} func (*ValidatingAdmissionPolicySpec) Descriptor() ([]byte, []int) { - return fileDescriptor_2c49182728ae0af5, []int{13} + return fileDescriptor_2c49182728ae0af5, []int{22} } func (m *ValidatingAdmissionPolicySpec) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -440,7 +693,7 @@ var xxx_messageInfo_ValidatingAdmissionPolicySpec proto.InternalMessageInfo func (m *ValidatingAdmissionPolicyStatus) Reset() { *m = ValidatingAdmissionPolicyStatus{} } func (*ValidatingAdmissionPolicyStatus) ProtoMessage() {} func (*ValidatingAdmissionPolicyStatus) Descriptor() ([]byte, []int) { - return fileDescriptor_2c49182728ae0af5, []int{14} + return fileDescriptor_2c49182728ae0af5, []int{23} } func (m *ValidatingAdmissionPolicyStatus) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -468,7 +721,7 @@ var xxx_messageInfo_ValidatingAdmissionPolicyStatus proto.InternalMessageInfo func (m *Validation) Reset() { *m = Validation{} } func (*Validation) ProtoMessage() {} func (*Validation) Descriptor() ([]byte, []int) { - return fileDescriptor_2c49182728ae0af5, []int{15} + return fileDescriptor_2c49182728ae0af5, []int{24} } func (m *Validation) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -496,7 +749,7 @@ var xxx_messageInfo_Validation proto.InternalMessageInfo func (m *Variable) Reset() { *m = Variable{} } func (*Variable) ProtoMessage() {} func (*Variable) Descriptor() ([]byte, []int) { - return fileDescriptor_2c49182728ae0af5, []int{16} + return fileDescriptor_2c49182728ae0af5, []int{25} } func (m *Variable) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -522,10 +775,19 @@ func (m *Variable) XXX_DiscardUnknown() { var xxx_messageInfo_Variable proto.InternalMessageInfo func init() { + proto.RegisterType((*ApplyConfiguration)(nil), "k8s.io.api.admissionregistration.v1alpha1.ApplyConfiguration") proto.RegisterType((*AuditAnnotation)(nil), "k8s.io.api.admissionregistration.v1alpha1.AuditAnnotation") proto.RegisterType((*ExpressionWarning)(nil), "k8s.io.api.admissionregistration.v1alpha1.ExpressionWarning") + proto.RegisterType((*JSONPatch)(nil), "k8s.io.api.admissionregistration.v1alpha1.JSONPatch") proto.RegisterType((*MatchCondition)(nil), "k8s.io.api.admissionregistration.v1alpha1.MatchCondition") proto.RegisterType((*MatchResources)(nil), "k8s.io.api.admissionregistration.v1alpha1.MatchResources") + proto.RegisterType((*MutatingAdmissionPolicy)(nil), "k8s.io.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy") + proto.RegisterType((*MutatingAdmissionPolicyBinding)(nil), "k8s.io.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding") + proto.RegisterType((*MutatingAdmissionPolicyBindingList)(nil), "k8s.io.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBindingList") + proto.RegisterType((*MutatingAdmissionPolicyBindingSpec)(nil), "k8s.io.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBindingSpec") + proto.RegisterType((*MutatingAdmissionPolicyList)(nil), "k8s.io.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyList") + proto.RegisterType((*MutatingAdmissionPolicySpec)(nil), "k8s.io.api.admissionregistration.v1alpha1.MutatingAdmissionPolicySpec") + proto.RegisterType((*Mutation)(nil), "k8s.io.api.admissionregistration.v1alpha1.Mutation") proto.RegisterType((*NamedRuleWithOperations)(nil), "k8s.io.api.admissionregistration.v1alpha1.NamedRuleWithOperations") proto.RegisterType((*ParamKind)(nil), "k8s.io.api.admissionregistration.v1alpha1.ParamKind") proto.RegisterType((*ParamRef)(nil), "k8s.io.api.admissionregistration.v1alpha1.ParamRef") @@ -546,101 +808,147 @@ func init() { } var fileDescriptor_2c49182728ae0af5 = []byte{ - // 1498 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x58, 0x5b, 0x6f, 0x1b, 0xc5, - 0x17, 0xcf, 0xc6, 0x6e, 0x12, 0x8f, 0x73, 0xb1, 0xe7, 0xdf, 0x2a, 0x6e, 0xfe, 0xd4, 0x1b, 0xad, - 0x2a, 0xd4, 0x48, 0xb0, 0x26, 0x69, 0xa1, 0xb4, 0x42, 0x42, 0xd9, 0xde, 0xe8, 0x25, 0x17, 0x4d, - 0x51, 0x22, 0x21, 0x90, 0x98, 0xec, 0x4e, 0xec, 0x69, 0xbc, 0x17, 0x76, 0xd6, 0xa1, 0x11, 0x48, - 0x54, 0xe2, 0x05, 0xde, 0x78, 0xe0, 0x85, 0x57, 0x3e, 0x02, 0xdf, 0x80, 0xb7, 0x3e, 0xf6, 0xb1, - 0x3c, 0x60, 0x51, 0xf3, 0xc2, 0x27, 0x00, 0x29, 0x2f, 0xa0, 0x99, 0x9d, 0xbd, 0xda, 0x26, 0x76, - 0x09, 0xbc, 0x79, 0xce, 0x9c, 0xf3, 0xfb, 0xcd, 0x39, 0x73, 0xce, 0xd9, 0x33, 0x06, 0xd7, 0x0e, - 0xde, 0x66, 0x3a, 0x75, 0x1b, 0xd8, 0xa3, 0x0d, 0x6c, 0xd9, 0x94, 0x31, 0xea, 0x3a, 0x3e, 0x69, - 0x52, 0x16, 0xf8, 0x38, 0xa0, 0xae, 0xd3, 0x38, 0x5c, 0xc5, 0x6d, 0xaf, 0x85, 0x57, 0x1b, 0x4d, - 0xe2, 0x10, 0x1f, 0x07, 0xc4, 0xd2, 0x3d, 0xdf, 0x0d, 0x5c, 0xb8, 0x12, 0x9a, 0xea, 0xd8, 0xa3, - 0xfa, 0x40, 0x53, 0x3d, 0x32, 0x5d, 0x7a, 0xbd, 0x49, 0x83, 0x56, 0x67, 0x4f, 0x37, 0x5d, 0xbb, - 0xd1, 0x74, 0x9b, 0x6e, 0x43, 0x20, 0xec, 0x75, 0xf6, 0xc5, 0x4a, 0x2c, 0xc4, 0xaf, 0x10, 0x79, - 0xe9, 0xf2, 0x08, 0x87, 0xca, 0x1f, 0x67, 0xe9, 0x4a, 0x62, 0x64, 0x63, 0xb3, 0x45, 0x1d, 0xe2, - 0x1f, 0x35, 0xbc, 0x83, 0x26, 0x17, 0xb0, 0x86, 0x4d, 0x02, 0x3c, 0xc8, 0xaa, 0x31, 0xcc, 0xca, - 0xef, 0x38, 0x01, 0xb5, 0x49, 0x9f, 0xc1, 0x5b, 0x27, 0x19, 0x30, 0xb3, 0x45, 0x6c, 0x9c, 0xb7, - 0xd3, 0x18, 0x58, 0x58, 0xef, 0x58, 0x34, 0x58, 0x77, 0x1c, 0x37, 0x10, 0x4e, 0xc0, 0x0b, 0xa0, - 0x70, 0x40, 0x8e, 0x6a, 0xca, 0xb2, 0x72, 0xa9, 0x64, 0x94, 0x9f, 0x76, 0xd5, 0x89, 0x5e, 0x57, - 0x2d, 0xdc, 0x27, 0x47, 0x88, 0xcb, 0xe1, 0x3a, 0x58, 0x38, 0xc4, 0xed, 0x0e, 0xb9, 0xf5, 0xd8, - 0xf3, 0x89, 0x08, 0x41, 0x6d, 0x52, 0xa8, 0x2e, 0x4a, 0xd5, 0x85, 0x9d, 0xec, 0x36, 0xca, 0xeb, - 0x6b, 0x6d, 0x50, 0x4d, 0x56, 0xbb, 0xd8, 0x77, 0xa8, 0xd3, 0x84, 0xaf, 0x81, 0x99, 0x7d, 0x4a, - 0xda, 0x16, 0x22, 0xfb, 0x12, 0xb0, 0x22, 0x01, 0x67, 0x6e, 0x4b, 0x39, 0x8a, 0x35, 0xe0, 0x0a, - 0x98, 0xfe, 0x34, 0x34, 0xac, 0x15, 0x84, 0xf2, 0x82, 0x54, 0x9e, 0x96, 0x78, 0x28, 0xda, 0xd7, - 0xf6, 0xc1, 0xfc, 0x06, 0x0e, 0xcc, 0xd6, 0x0d, 0xd7, 0xb1, 0xa8, 0xf0, 0x70, 0x19, 0x14, 0x1d, - 0x6c, 0x13, 0xe9, 0xe2, 0xac, 0xb4, 0x2c, 0x6e, 0x62, 0x9b, 0x20, 0xb1, 0x03, 0xd7, 0x00, 0x20, - 0x79, 0xff, 0xa0, 0xd4, 0x03, 0x29, 0xd7, 0x52, 0x5a, 0xda, 0x4f, 0x45, 0x49, 0x84, 0x08, 0x73, - 0x3b, 0xbe, 0x49, 0x18, 0x7c, 0x0c, 0xaa, 0x1c, 0x8e, 0x79, 0xd8, 0x24, 0x0f, 0x49, 0x9b, 0x98, - 0x81, 0xeb, 0x0b, 0xd6, 0xf2, 0xda, 0x65, 0x3d, 0xc9, 0xd3, 0xf8, 0xc6, 0x74, 0xef, 0xa0, 0xc9, - 0x05, 0x4c, 0xe7, 0x89, 0xa1, 0x1f, 0xae, 0xea, 0x0f, 0xf0, 0x1e, 0x69, 0x47, 0xa6, 0xc6, 0xb9, - 0x5e, 0x57, 0xad, 0x6e, 0xe6, 0x11, 0x51, 0x3f, 0x09, 0x74, 0xc1, 0xbc, 0xbb, 0xf7, 0x88, 0x98, - 0x41, 0x4c, 0x3b, 0xf9, 0xf2, 0xb4, 0xb0, 0xd7, 0x55, 0xe7, 0xb7, 0x32, 0x70, 0x28, 0x07, 0x0f, - 0xbf, 0x00, 0x73, 0xbe, 0xf4, 0x1b, 0x75, 0xda, 0x84, 0xd5, 0x0a, 0xcb, 0x85, 0x4b, 0xe5, 0x35, - 0x43, 0x1f, 0xb9, 0x1c, 0x75, 0xee, 0x98, 0xc5, 0x8d, 0x77, 0x69, 0xd0, 0xda, 0xf2, 0x48, 0xb8, - 0xcf, 0x8c, 0x73, 0x32, 0xf0, 0x73, 0x28, 0x4d, 0x80, 0xb2, 0x7c, 0xf0, 0x5b, 0x05, 0x9c, 0x25, - 0x8f, 0xcd, 0x76, 0xc7, 0x22, 0x19, 0xbd, 0x5a, 0xf1, 0xd4, 0x0e, 0xf2, 0x8a, 0x3c, 0xc8, 0xd9, - 0x5b, 0x03, 0x78, 0xd0, 0x40, 0x76, 0x78, 0x13, 0x94, 0x6d, 0x9e, 0x14, 0xdb, 0x6e, 0x9b, 0x9a, - 0x47, 0xb5, 0x69, 0x91, 0x4a, 0x5a, 0xaf, 0xab, 0x96, 0x37, 0x12, 0xf1, 0x71, 0x57, 0x5d, 0x48, - 0x2d, 0xdf, 0x3f, 0xf2, 0x08, 0x4a, 0x9b, 0x69, 0xcf, 0x15, 0xb0, 0x38, 0xe4, 0x54, 0xf0, 0x6a, - 0x12, 0x79, 0x91, 0x1a, 0x35, 0x65, 0xb9, 0x70, 0xa9, 0x64, 0x54, 0xd3, 0x11, 0x13, 0x1b, 0x28, - 0xab, 0x07, 0xbf, 0x54, 0x00, 0xf4, 0xfb, 0xf0, 0x64, 0xa2, 0x5c, 0x1d, 0x25, 0x5e, 0xfa, 0x80, - 0x20, 0x2d, 0xc9, 0x20, 0xc1, 0xfe, 0x3d, 0x34, 0x80, 0x4e, 0xc3, 0xa0, 0xb4, 0x8d, 0x7d, 0x6c, - 0xdf, 0xa7, 0x8e, 0xc5, 0xeb, 0x0e, 0x7b, 0x74, 0x87, 0xf8, 0xa2, 0xee, 0x94, 0x6c, 0xdd, 0xad, - 0x6f, 0xdf, 0x95, 0x3b, 0x28, 0xa5, 0xc5, 0xab, 0xf9, 0x80, 0x3a, 0x96, 0xac, 0xd2, 0xb8, 0x9a, - 0x39, 0x1e, 0x12, 0x3b, 0xda, 0x0f, 0x93, 0x60, 0x46, 0x70, 0xf0, 0xce, 0x71, 0x72, 0xf1, 0x37, - 0x40, 0x29, 0x2e, 0x28, 0x89, 0x5a, 0x95, 0x6a, 0xa5, 0xb8, 0xf8, 0x50, 0xa2, 0x03, 0x3f, 0x02, - 0x33, 0x2c, 0x2a, 0xb3, 0xc2, 0xcb, 0x97, 0xd9, 0x2c, 0xef, 0x75, 0x71, 0x81, 0xc5, 0x90, 0x30, - 0x00, 0x8b, 0x1e, 0x3f, 0x3d, 0x09, 0x88, 0xbf, 0xe9, 0x06, 0xb7, 0xdd, 0x8e, 0x63, 0xad, 0x9b, - 0x3c, 0x7a, 0xb5, 0xa2, 0x38, 0xdd, 0xf5, 0x5e, 0x57, 0x5d, 0xdc, 0x1e, 0xac, 0x72, 0xdc, 0x55, - 0xff, 0x3f, 0x64, 0x4b, 0xa4, 0xd9, 0x30, 0x68, 0xed, 0x3b, 0x05, 0xcc, 0x72, 0x8d, 0x1b, 0x2d, - 0x62, 0x1e, 0xf0, 0x06, 0xfd, 0x95, 0x02, 0x20, 0xc9, 0xb7, 0xed, 0x30, 0xdb, 0xca, 0x6b, 0xef, - 0x8c, 0x51, 0x5e, 0x7d, 0xbd, 0x3f, 0xc9, 0x99, 0xbe, 0x2d, 0x86, 0x06, 0x70, 0x6a, 0x3f, 0x4f, - 0x82, 0xf3, 0x3b, 0xb8, 0x4d, 0x2d, 0x1c, 0x50, 0xa7, 0xb9, 0x1e, 0xd1, 0x85, 0xc5, 0x02, 0x3f, - 0x06, 0x33, 0x3c, 0xc0, 0x16, 0x0e, 0xb0, 0x6c, 0xb6, 0x6f, 0x8c, 0x76, 0x1d, 0x61, 0x8b, 0xdb, - 0x20, 0x01, 0x4e, 0x92, 0x2e, 0x91, 0xa1, 0x18, 0x15, 0x3e, 0x02, 0x45, 0xe6, 0x11, 0x53, 0x96, - 0xca, 0x7b, 0x63, 0xf8, 0x3e, 0xf4, 0xd4, 0x0f, 0x3d, 0x62, 0x26, 0xd9, 0xc8, 0x57, 0x48, 0x70, - 0x40, 0x1f, 0x4c, 0xb1, 0x00, 0x07, 0x1d, 0x26, 0x53, 0xeb, 0xde, 0xa9, 0xb0, 0x09, 0x44, 0x63, - 0x5e, 0xf2, 0x4d, 0x85, 0x6b, 0x24, 0x99, 0xb4, 0x3f, 0x14, 0xb0, 0x3c, 0xd4, 0xd6, 0xa0, 0x8e, - 0xc5, 0xf3, 0xe1, 0xdf, 0x0f, 0xf3, 0x27, 0x99, 0x30, 0x6f, 0x9d, 0x86, 0xe3, 0xf2, 0xf0, 0xc3, - 0xa2, 0xad, 0xfd, 0xae, 0x80, 0x8b, 0x27, 0x19, 0x3f, 0xa0, 0x2c, 0x80, 0x1f, 0xf6, 0x79, 0xaf, - 0x8f, 0x58, 0xf3, 0x94, 0x85, 0xbe, 0xc7, 0xe3, 0x4d, 0x24, 0x49, 0x79, 0xee, 0x81, 0x33, 0x34, - 0x20, 0x36, 0x6f, 0xc6, 0xbc, 0xba, 0xee, 0x9f, 0xa2, 0xeb, 0xc6, 0x9c, 0xe4, 0x3d, 0x73, 0x97, - 0x33, 0xa0, 0x90, 0x48, 0xfb, 0xba, 0x70, 0xb2, 0xe3, 0x3c, 0x4e, 0xbc, 0x45, 0x7b, 0x42, 0xb8, - 0x99, 0x74, 0xd1, 0xf8, 0x1a, 0xb7, 0xe3, 0x1d, 0x94, 0xd2, 0xe2, 0x0d, 0xd2, 0x93, 0xfd, 0x77, - 0xc0, 0x1c, 0x72, 0x92, 0x47, 0x51, 0xeb, 0x0e, 0x1b, 0x64, 0xb4, 0x42, 0x31, 0x24, 0xec, 0x80, - 0x79, 0x3b, 0x33, 0x78, 0xc9, 0x52, 0xb9, 0x36, 0x06, 0x49, 0x76, 0x72, 0x0b, 0x47, 0x9e, 0xac, - 0x0c, 0xe5, 0x48, 0xe0, 0x2e, 0xa8, 0x1e, 0xca, 0x88, 0xb9, 0x4e, 0xd8, 0x35, 0xc3, 0x69, 0xa3, - 0x64, 0xac, 0xf0, 0x41, 0x6d, 0x27, 0xbf, 0x79, 0xdc, 0x55, 0x2b, 0x79, 0x21, 0xea, 0xc7, 0xd0, - 0x7e, 0x53, 0xc0, 0x85, 0xa1, 0x77, 0xf1, 0x1f, 0x64, 0x1f, 0xcd, 0x66, 0xdf, 0xcd, 0x53, 0xc9, - 0xbe, 0xc1, 0x69, 0xf7, 0xfd, 0xd4, 0xdf, 0xb8, 0x2a, 0xf2, 0x0d, 0x83, 0x92, 0x17, 0xcd, 0x07, - 0xd2, 0xd7, 0x2b, 0xe3, 0x26, 0x0f, 0xb7, 0x35, 0xe6, 0xf8, 0xf7, 0x3b, 0x5e, 0xa2, 0x04, 0x15, - 0x7e, 0x06, 0x2a, 0xb6, 0x7c, 0x21, 0x70, 0x00, 0xea, 0x04, 0xd1, 0x14, 0xf4, 0x0f, 0x32, 0xe8, - 0x6c, 0xaf, 0xab, 0x56, 0x36, 0x72, 0xb0, 0xa8, 0x8f, 0x08, 0xb6, 0x41, 0x39, 0xc9, 0x80, 0x68, - 0x6c, 0x7e, 0xf3, 0x25, 0x42, 0xee, 0x3a, 0xc6, 0xff, 0x64, 0x8c, 0xcb, 0x89, 0x8c, 0xa1, 0x34, - 0x3c, 0x7c, 0x00, 0xe6, 0xf6, 0x31, 0x6d, 0x77, 0x7c, 0x22, 0x07, 0xd2, 0x70, 0x82, 0x78, 0x95, - 0x0f, 0x8b, 0xb7, 0xd3, 0x1b, 0xc7, 0x5d, 0xb5, 0x9a, 0x11, 0x88, 0x69, 0x21, 0x6b, 0x0c, 0x9f, - 0x28, 0xa0, 0x82, 0xb3, 0xcf, 0x47, 0x56, 0x3b, 0x23, 0x3c, 0xb8, 0x3e, 0x86, 0x07, 0xb9, 0x17, - 0xa8, 0x51, 0x93, 0x6e, 0x54, 0x72, 0x1b, 0x0c, 0xf5, 0xb1, 0xc1, 0xcf, 0xc1, 0x82, 0x9d, 0x79, - 0xdd, 0xb1, 0xda, 0x94, 0x38, 0xc0, 0xd8, 0x57, 0x17, 0x23, 0x24, 0x2f, 0xd9, 0xac, 0x9c, 0xa1, - 0x3c, 0x15, 0xb4, 0x40, 0xe9, 0x10, 0xfb, 0x14, 0xef, 0xf1, 0x87, 0xc6, 0xb4, 0xe0, 0xbd, 0x3c, - 0xd6, 0xd5, 0x85, 0xb6, 0xc9, 0x7c, 0x19, 0x49, 0x18, 0x4a, 0x80, 0xb5, 0x1f, 0x27, 0x81, 0x7a, - 0xc2, 0xa7, 0x1c, 0xde, 0x03, 0xd0, 0xdd, 0x63, 0xc4, 0x3f, 0x24, 0xd6, 0x9d, 0xf0, 0x8d, 0x1f, - 0x4d, 0xd0, 0x85, 0x64, 0xbc, 0xda, 0xea, 0xd3, 0x40, 0x03, 0xac, 0xa0, 0x0d, 0x66, 0x83, 0xd4, - 0xe4, 0x37, 0xce, 0x8b, 0x40, 0x3a, 0x96, 0x1e, 0x1c, 0x8d, 0x4a, 0xaf, 0xab, 0x66, 0x46, 0x49, - 0x94, 0x81, 0x87, 0x26, 0x00, 0x66, 0x72, 0x7b, 0x61, 0x01, 0x34, 0x46, 0x6b, 0x67, 0xc9, 0x9d, - 0xc5, 0x9f, 0xa0, 0xd4, 0x75, 0xa5, 0x60, 0xb5, 0x3f, 0x15, 0x00, 0x92, 0xaa, 0x80, 0x17, 0x41, - 0xea, 0x19, 0x2f, 0xbf, 0x62, 0x45, 0x0e, 0x81, 0x52, 0x72, 0xb8, 0x02, 0xa6, 0x6d, 0xc2, 0x18, - 0x6e, 0x46, 0xef, 0x80, 0xf8, 0x5f, 0x86, 0x8d, 0x50, 0x8c, 0xa2, 0x7d, 0xb8, 0x0b, 0xa6, 0x7c, - 0x82, 0x99, 0xeb, 0xc8, 0xff, 0x23, 0xde, 0xe5, 0x63, 0x15, 0x12, 0x92, 0xe3, 0xae, 0xba, 0x3a, - 0xca, 0xbf, 0x40, 0xba, 0x9c, 0xc2, 0x84, 0x11, 0x92, 0x70, 0xf0, 0x0e, 0xa8, 0x4a, 0x8e, 0xd4, - 0x81, 0xc3, 0xaa, 0x3d, 0x2f, 0x4f, 0x53, 0xdd, 0xc8, 0x2b, 0xa0, 0x7e, 0x1b, 0xed, 0x1e, 0x98, - 0x89, 0xb2, 0x0b, 0xd6, 0x40, 0x31, 0xf5, 0xf9, 0x0e, 0x1d, 0x17, 0x92, 0x5c, 0x60, 0x26, 0x07, - 0x07, 0xc6, 0xd8, 0x7a, 0xfa, 0xa2, 0x3e, 0xf1, 0xec, 0x45, 0x7d, 0xe2, 0xf9, 0x8b, 0xfa, 0xc4, - 0x93, 0x5e, 0x5d, 0x79, 0xda, 0xab, 0x2b, 0xcf, 0x7a, 0x75, 0xe5, 0x79, 0xaf, 0xae, 0xfc, 0xd2, - 0xab, 0x2b, 0xdf, 0xfc, 0x5a, 0x9f, 0xf8, 0x60, 0x65, 0xe4, 0x7f, 0xf1, 0xfe, 0x0a, 0x00, 0x00, - 0xff, 0xff, 0x22, 0xbd, 0xc5, 0xc7, 0xf1, 0x13, 0x00, 0x00, + // 1783 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0x59, 0xdd, 0x6f, 0x1b, 0x4b, + 0x15, 0xcf, 0xda, 0xce, 0x87, 0xc7, 0xf9, 0xf2, 0xd0, 0x12, 0x37, 0xa5, 0xde, 0x68, 0x55, 0xa1, + 0x46, 0x82, 0x35, 0x49, 0x0b, 0xa5, 0x55, 0x51, 0x95, 0x6d, 0x9b, 0xb6, 0x69, 0x9d, 0x44, 0x53, + 0x94, 0x20, 0x04, 0x12, 0x93, 0xf5, 0xc4, 0xde, 0xc6, 0xfb, 0xc1, 0xce, 0x3a, 0x34, 0x02, 0x89, + 0x4a, 0x08, 0x09, 0xde, 0x78, 0xe0, 0x85, 0x37, 0xc4, 0x1f, 0xc0, 0x03, 0xfc, 0x05, 0xbc, 0xf5, + 0xb1, 0x8f, 0xe5, 0x81, 0x15, 0x35, 0x20, 0xf1, 0x0c, 0xd2, 0xbd, 0x52, 0x5e, 0xee, 0xd5, 0xcc, + 0xce, 0x7e, 0x79, 0xed, 0xc6, 0x4e, 0xd3, 0xf4, 0xe1, 0xde, 0x37, 0xcf, 0xf9, 0xf8, 0x9d, 0x39, + 0x67, 0xce, 0x9c, 0x39, 0xc7, 0x0b, 0x6e, 0x1d, 0x7c, 0x97, 0xaa, 0x86, 0x5d, 0xc3, 0x8e, 0x51, + 0xc3, 0x0d, 0xd3, 0xa0, 0xd4, 0xb0, 0x2d, 0x97, 0x34, 0x0d, 0xea, 0xb9, 0xd8, 0x33, 0x6c, 0xab, + 0x76, 0xb8, 0x82, 0xdb, 0x4e, 0x0b, 0xaf, 0xd4, 0x9a, 0xc4, 0x22, 0x2e, 0xf6, 0x48, 0x43, 0x75, + 0x5c, 0xdb, 0xb3, 0xe1, 0x72, 0xa0, 0xaa, 0x62, 0xc7, 0x50, 0xfb, 0xaa, 0xaa, 0xa1, 0xea, 0xe2, + 0x37, 0x9b, 0x86, 0xd7, 0xea, 0xec, 0xa9, 0xba, 0x6d, 0xd6, 0x9a, 0x76, 0xd3, 0xae, 0x71, 0x84, + 0xbd, 0xce, 0x3e, 0x5f, 0xf1, 0x05, 0xff, 0x15, 0x20, 0x2f, 0x5e, 0x1f, 0x62, 0x53, 0xbd, 0xdb, + 0x59, 0xbc, 0x11, 0x2b, 0x99, 0x58, 0x6f, 0x19, 0x16, 0x71, 0x8f, 0x6a, 0xce, 0x41, 0x93, 0x11, + 0x68, 0xcd, 0x24, 0x1e, 0xee, 0xa7, 0x55, 0x1b, 0xa4, 0xe5, 0x76, 0x2c, 0xcf, 0x30, 0x49, 0x46, + 0xe1, 0x3b, 0x27, 0x29, 0x50, 0xbd, 0x45, 0x4c, 0xdc, 0xab, 0xa7, 0x3c, 0x02, 0x70, 0xcd, 0x71, + 0xda, 0x47, 0xf7, 0x6c, 0x6b, 0xdf, 0x68, 0x76, 0x02, 0x3f, 0xe0, 0x2a, 0x00, 0xe4, 0x85, 0xe3, + 0x12, 0xee, 0x61, 0x45, 0x5a, 0x92, 0xae, 0x15, 0x35, 0xf8, 0xca, 0x97, 0xc7, 0xba, 0xbe, 0x0c, + 0x1e, 0x44, 0x1c, 0x94, 0x90, 0x52, 0x28, 0x98, 0x5b, 0xeb, 0x34, 0x0c, 0x6f, 0xcd, 0xb2, 0x6c, + 0x2f, 0x80, 0xb9, 0x02, 0xf2, 0x07, 0xe4, 0x48, 0xe8, 0x97, 0x84, 0x7e, 0xfe, 0x09, 0x39, 0x42, + 0x8c, 0x0e, 0xd7, 0xc0, 0xdc, 0x21, 0x6e, 0x77, 0x48, 0x0c, 0x58, 0xc9, 0x71, 0xd1, 0x05, 0x21, + 0x3a, 0xb7, 0x93, 0x66, 0xa3, 0x5e, 0x79, 0xa5, 0x0d, 0xca, 0xf1, 0x6a, 0x17, 0xbb, 0x96, 0x61, + 0x35, 0xe1, 0x37, 0xc0, 0xd4, 0xbe, 0x41, 0xda, 0x0d, 0x44, 0xf6, 0x05, 0xe0, 0xbc, 0x00, 0x9c, + 0x5a, 0x17, 0x74, 0x14, 0x49, 0xc0, 0x65, 0x30, 0xf9, 0xb3, 0x40, 0xb1, 0x92, 0xe7, 0xc2, 0x73, + 0x42, 0x78, 0x52, 0xe0, 0xa1, 0x90, 0xaf, 0xdc, 0x05, 0xc5, 0x8d, 0x67, 0x5b, 0x9b, 0xdb, 0xd8, + 0xd3, 0x5b, 0xa7, 0x8a, 0xd1, 0x3e, 0x98, 0xad, 0x33, 0xe5, 0x7b, 0xb6, 0xd5, 0x30, 0x78, 0x88, + 0x96, 0x40, 0xc1, 0xc2, 0x26, 0x11, 0xfa, 0xd3, 0x42, 0xbf, 0xb0, 0x89, 0x4d, 0x82, 0x38, 0xa7, + 0xc7, 0x4e, 0x6e, 0x28, 0x3b, 0x7f, 0x2f, 0x08, 0x43, 0x88, 0x50, 0xbb, 0xe3, 0xea, 0x84, 0xc2, + 0x17, 0xa0, 0xcc, 0xe0, 0xa8, 0x83, 0x75, 0xf2, 0x8c, 0xb4, 0x89, 0xee, 0xd9, 0x2e, 0xb7, 0x5a, + 0x5a, 0xbd, 0xae, 0xc6, 0x57, 0x26, 0x4a, 0x1e, 0xd5, 0x39, 0x68, 0x32, 0x02, 0x55, 0x59, 0x8e, + 0xaa, 0x87, 0x2b, 0xea, 0x53, 0xbc, 0x47, 0xda, 0xa1, 0xaa, 0x76, 0xb1, 0xeb, 0xcb, 0xe5, 0xcd, + 0x5e, 0x44, 0x94, 0x35, 0x02, 0x6d, 0x30, 0x6b, 0xef, 0x3d, 0x27, 0xba, 0x17, 0x99, 0xcd, 0x9d, + 0xde, 0x2c, 0xec, 0xfa, 0xf2, 0xec, 0x56, 0x0a, 0x0e, 0xf5, 0xc0, 0xc3, 0x5f, 0x82, 0x19, 0x57, + 0xf8, 0x8d, 0x3a, 0x6d, 0x42, 0x2b, 0xf9, 0xa5, 0xfc, 0xb5, 0xd2, 0xaa, 0xa6, 0x0e, 0x5d, 0x19, + 0x54, 0xe6, 0x58, 0x83, 0x29, 0xef, 0x1a, 0x5e, 0x6b, 0xcb, 0x21, 0x01, 0x9f, 0x6a, 0x17, 0x45, + 0xe0, 0x67, 0x50, 0xd2, 0x00, 0x4a, 0xdb, 0x83, 0xbf, 0x97, 0xc0, 0x05, 0xf2, 0x42, 0x6f, 0x77, + 0x1a, 0x24, 0x25, 0x57, 0x29, 0x9c, 0xd9, 0x46, 0xbe, 0x26, 0x36, 0x72, 0xe1, 0x41, 0x1f, 0x3b, + 0xa8, 0xaf, 0x75, 0x78, 0x1f, 0x94, 0x4c, 0x96, 0x14, 0xdb, 0x76, 0xdb, 0xd0, 0x8f, 0x2a, 0x93, + 0x3c, 0x95, 0x94, 0xae, 0x2f, 0x97, 0xea, 0x31, 0xf9, 0xd8, 0x97, 0xe7, 0x12, 0xcb, 0xef, 0x1f, + 0x39, 0x04, 0x25, 0xd5, 0x94, 0xff, 0x48, 0x60, 0xa1, 0xde, 0x61, 0x37, 0xdc, 0x6a, 0xae, 0x85, + 0x9b, 0x0f, 0x78, 0xf0, 0x27, 0x60, 0x8a, 0x1d, 0x5b, 0x03, 0x7b, 0x58, 0xe4, 0xd6, 0xb7, 0x86, + 0x3b, 0xe4, 0xe0, 0x44, 0xeb, 0xc4, 0xc3, 0x71, 0x6e, 0xc7, 0x34, 0x14, 0xa1, 0xc2, 0x16, 0x28, + 0x50, 0x87, 0xe8, 0x22, 0x85, 0xd6, 0x47, 0x88, 0xe4, 0x80, 0x3d, 0x3f, 0x73, 0x88, 0x1e, 0xdf, + 0x3b, 0xb6, 0x42, 0xdc, 0x82, 0xf2, 0x7f, 0x09, 0x54, 0x07, 0xe8, 0x68, 0x86, 0xd5, 0x60, 0x85, + 0xe6, 0xc3, 0xbb, 0x6b, 0xa7, 0xdc, 0xad, 0xbf, 0xbf, 0xbb, 0x62, 0xeb, 0x03, 0xbd, 0xfe, 0x9f, + 0x04, 0x94, 0x77, 0xab, 0x3e, 0x35, 0xa8, 0x07, 0x7f, 0x94, 0xf1, 0x5c, 0x1d, 0xf2, 0x36, 0x1b, + 0x34, 0xf0, 0x3b, 0x2a, 0xc9, 0x21, 0x25, 0xe1, 0xb5, 0x05, 0xc6, 0x0d, 0x8f, 0x98, 0xb4, 0x92, + 0xe3, 0xf7, 0xe5, 0xf1, 0x99, 0xb9, 0xad, 0xcd, 0x08, 0xab, 0xe3, 0x8f, 0x19, 0x3e, 0x0a, 0xcc, + 0x28, 0x7f, 0xce, 0x9d, 0xe4, 0x34, 0x8b, 0x10, 0xab, 0xc4, 0x0e, 0x27, 0x6e, 0xc6, 0x15, 0x3b, + 0x3a, 0xbe, 0xed, 0x88, 0x83, 0x12, 0x52, 0xf0, 0xc7, 0x60, 0xca, 0xc1, 0x2e, 0x36, 0xc3, 0xb7, + 0x28, 0x5d, 0xf6, 0x4e, 0xf2, 0x66, 0x5b, 0xa8, 0x6a, 0xd3, 0x2c, 0x52, 0xe1, 0x0a, 0x45, 0x90, + 0xb0, 0x03, 0x66, 0xcd, 0x54, 0x9d, 0xe7, 0x6f, 0x58, 0x69, 0xf5, 0xd6, 0x28, 0x21, 0x4b, 0x01, + 0x04, 0x15, 0x36, 0x4d, 0x43, 0x3d, 0x46, 0x94, 0x7f, 0x4b, 0xe0, 0xf2, 0x80, 0x80, 0x9d, 0x43, + 0x7a, 0x34, 0xd3, 0xe9, 0xa1, 0x9d, 0x41, 0x7a, 0xf4, 0xcf, 0x8b, 0x3f, 0x4e, 0x0c, 0x74, 0x93, + 0x27, 0x04, 0x06, 0x45, 0x7e, 0x12, 0x4f, 0x0c, 0xab, 0x21, 0xfc, 0xbc, 0x31, 0xea, 0xe9, 0x32, + 0x5d, 0x6d, 0xa6, 0xeb, 0xcb, 0xc5, 0x68, 0x89, 0x62, 0x54, 0xf8, 0x73, 0x30, 0x6f, 0x8a, 0x8e, + 0x81, 0x01, 0x18, 0x96, 0x47, 0x45, 0x1e, 0xbd, 0xc7, 0x11, 0x5f, 0xe8, 0xfa, 0xf2, 0x7c, 0xbd, + 0x07, 0x16, 0x65, 0x0c, 0xc1, 0x06, 0x28, 0x1e, 0x62, 0xd7, 0xc0, 0x7b, 0xf1, 0x23, 0x3a, 0x4a, + 0xf6, 0xee, 0x08, 0x5d, 0xad, 0x2c, 0xa2, 0x5b, 0x0c, 0x29, 0x14, 0xc5, 0xc0, 0xcc, 0x8a, 0xd9, + 0x09, 0x3a, 0xc6, 0xf0, 0x85, 0xbc, 0x3e, 0xf2, 0x91, 0xda, 0x56, 0x6c, 0x25, 0xa4, 0x50, 0x14, + 0x03, 0xc3, 0xa7, 0x60, 0x66, 0x1f, 0x1b, 0xed, 0x8e, 0x4b, 0xc4, 0xf3, 0x37, 0xce, 0xef, 0xef, + 0xd7, 0xd9, 0x63, 0xbe, 0x9e, 0x64, 0x1c, 0xfb, 0x72, 0x39, 0x45, 0xe0, 0x4f, 0x60, 0x5a, 0x19, + 0xfe, 0x02, 0xcc, 0x99, 0xa9, 0x46, 0x8e, 0x56, 0x26, 0xf8, 0xce, 0x47, 0x3e, 0x95, 0x08, 0x21, + 0xee, 0x7a, 0xd3, 0x74, 0x8a, 0x7a, 0x4d, 0xc1, 0xdf, 0x48, 0x00, 0xba, 0xc4, 0xb0, 0x0e, 0x6d, + 0x9d, 0x43, 0xa6, 0x1e, 0xf4, 0x1f, 0x08, 0x18, 0x88, 0x32, 0x12, 0xc7, 0xbe, 0x7c, 0x7b, 0x88, + 0x19, 0x46, 0xcd, 0x6a, 0xf2, 0x18, 0xf4, 0xb1, 0xa9, 0xfc, 0x35, 0x07, 0xa6, 0xc2, 0x78, 0xc3, + 0x3b, 0xec, 0x3e, 0x78, 0x7a, 0x8b, 0x49, 0x8b, 0x4e, 0xb5, 0x1a, 0x1e, 0xca, 0x76, 0xc8, 0x38, + 0x4e, 0x2e, 0x50, 0xac, 0x00, 0x7f, 0x2d, 0x01, 0x88, 0x33, 0xb3, 0x88, 0x28, 0x68, 0xdf, 0x1b, + 0x21, 0xae, 0xd9, 0x81, 0x46, 0xfb, 0x2a, 0x0b, 0x48, 0x96, 0x8e, 0xfa, 0x18, 0x64, 0xb7, 0xfa, + 0x39, 0xb5, 0x2d, 0xbe, 0xc7, 0x4a, 0x61, 0xe4, 0x5b, 0x1d, 0x4d, 0x08, 0xc1, 0xad, 0x8e, 0x96, + 0x28, 0x46, 0x55, 0xde, 0x48, 0x60, 0x61, 0x40, 0x67, 0x07, 0x6f, 0xc6, 0xdd, 0x2b, 0x6f, 0xaf, + 0x2b, 0xd2, 0x52, 0xfe, 0x5a, 0x51, 0x2b, 0x27, 0xbb, 0x4e, 0xce, 0x40, 0x69, 0x39, 0xf8, 0x2b, + 0x96, 0x15, 0x19, 0x3c, 0x51, 0x2d, 0x6e, 0x0e, 0xe3, 0x81, 0xda, 0xa7, 0xd1, 0x5c, 0x8c, 0xd2, + 0x29, 0xc3, 0x43, 0x7d, 0xcc, 0x29, 0x18, 0xc4, 0x85, 0x8c, 0xbd, 0x98, 0xd8, 0x31, 0x76, 0x88, + 0xdb, 0x6f, 0x46, 0x5a, 0xdb, 0x7e, 0x2c, 0x38, 0x28, 0x21, 0xc5, 0x26, 0xa2, 0x03, 0x56, 0x4f, + 0x73, 0xe9, 0x89, 0x88, 0x17, 0x46, 0xce, 0x51, 0xfe, 0x92, 0x03, 0xd1, 0x5b, 0x38, 0xc4, 0x00, + 0x55, 0x03, 0xc5, 0x68, 0x28, 0x11, 0xa8, 0x51, 0xa9, 0x88, 0x06, 0x18, 0x14, 0xcb, 0xb0, 0x37, + 0x9b, 0x86, 0xa3, 0x4a, 0xfe, 0xf4, 0xa3, 0x0a, 0x7f, 0xb3, 0xa3, 0x21, 0x25, 0x82, 0x84, 0x1e, + 0x58, 0xe0, 0xf5, 0x9d, 0x78, 0xc4, 0xdd, 0xb4, 0xbd, 0x75, 0xbb, 0x63, 0x35, 0xd6, 0x74, 0x9e, + 0xeb, 0x05, 0xbe, 0xbb, 0xdb, 0x5d, 0x5f, 0x5e, 0xd8, 0xee, 0x2f, 0x72, 0xec, 0xcb, 0x97, 0x07, + 0xb0, 0xf8, 0x7d, 0x1a, 0x04, 0xad, 0xfc, 0x41, 0x02, 0xd3, 0x4c, 0xe2, 0x5e, 0x8b, 0xe8, 0x07, + 0xac, 0x79, 0x65, 0x45, 0x84, 0xf4, 0xce, 0xce, 0x41, 0xb6, 0x95, 0x56, 0xef, 0x8c, 0x90, 0xf0, + 0x99, 0x01, 0x3c, 0xce, 0x99, 0x0c, 0x8b, 0xa2, 0x3e, 0x36, 0x95, 0x7f, 0xe4, 0xc0, 0xa5, 0x1d, + 0xdc, 0x36, 0x1a, 0x1f, 0x69, 0xa8, 0x78, 0x9e, 0xea, 0xb2, 0x1f, 0x8d, 0xf4, 0xc4, 0x0d, 0xd8, + 0xf5, 0xa0, 0x06, 0x1b, 0xba, 0x60, 0x82, 0x7a, 0xd8, 0xeb, 0x84, 0x9d, 0xda, 0xc6, 0x99, 0x58, + 0xe3, 0x88, 0xda, 0xac, 0xb0, 0x37, 0x11, 0xac, 0x91, 0xb0, 0xa4, 0x7c, 0x2a, 0x81, 0xa5, 0x81, + 0xba, 0xe7, 0x37, 0xcc, 0xfc, 0x34, 0x15, 0xe6, 0xad, 0xb3, 0x70, 0xfc, 0xa4, 0x71, 0xe6, 0x13, + 0x09, 0x5c, 0x3d, 0x49, 0xf9, 0x1c, 0x3a, 0x56, 0x27, 0xdd, 0xb1, 0x3e, 0x39, 0x43, 0xd7, 0x07, + 0xb4, 0xae, 0xbf, 0xcd, 0x9f, 0xec, 0xf8, 0x97, 0x43, 0x4d, 0xea, 0x1f, 0xb2, 0x5d, 0x50, 0x3e, + 0x14, 0x11, 0xb3, 0xad, 0xa0, 0x6a, 0x06, 0xfd, 0x68, 0x51, 0x5b, 0xee, 0xfa, 0x72, 0x79, 0xa7, + 0x97, 0x79, 0xec, 0xcb, 0xf3, 0xbd, 0x44, 0x94, 0xc5, 0x50, 0xfe, 0x2b, 0x81, 0x2b, 0x03, 0xcf, + 0xe2, 0x1c, 0xb2, 0xcf, 0x48, 0x67, 0xdf, 0xfd, 0x33, 0xc9, 0xbe, 0xfe, 0x69, 0xf7, 0xa7, 0x89, + 0x77, 0xb8, 0xfa, 0x85, 0x98, 0x99, 0xda, 0xa0, 0x14, 0x67, 0x40, 0x38, 0x35, 0x7d, 0xfb, 0x14, + 0x21, 0xb7, 0x2d, 0xed, 0x2b, 0x22, 0xc6, 0xa5, 0x98, 0x46, 0x51, 0x12, 0x3e, 0x3b, 0xd5, 0x14, + 0xde, 0x67, 0xaa, 0x79, 0x29, 0x81, 0x79, 0x9c, 0xfe, 0x0f, 0x9f, 0x56, 0xc6, 0xb9, 0x07, 0xb7, + 0x47, 0xe9, 0xbf, 0xd3, 0x10, 0x5a, 0x45, 0xb8, 0x31, 0xdf, 0xc3, 0xa0, 0x28, 0x63, 0xed, 0x23, + 0x0f, 0x56, 0xa9, 0x81, 0x77, 0xf2, 0x03, 0x0d, 0xbc, 0xca, 0xdf, 0x72, 0x40, 0x3e, 0xe1, 0x29, + 0x87, 0x1b, 0x00, 0xda, 0x7b, 0x94, 0xb8, 0x87, 0xa4, 0xf1, 0x30, 0xf8, 0x64, 0x13, 0x76, 0xd0, + 0xf9, 0xb8, 0xbd, 0xda, 0xca, 0x48, 0xa0, 0x3e, 0x5a, 0xd0, 0x04, 0xd3, 0x5e, 0xa2, 0xf3, 0x1b, + 0x65, 0x22, 0x10, 0x8e, 0x25, 0x1b, 0x47, 0x6d, 0xbe, 0xeb, 0xcb, 0xa9, 0x56, 0x12, 0xa5, 0xe0, + 0xa1, 0x0e, 0x80, 0x1e, 0x9f, 0x5e, 0x70, 0x01, 0x6a, 0xc3, 0x95, 0xb3, 0xf8, 0xcc, 0xa2, 0x27, + 0x28, 0x71, 0x5c, 0x09, 0x58, 0xe5, 0x33, 0x09, 0x80, 0xf8, 0x56, 0xc0, 0xab, 0x20, 0xf1, 0x29, + 0x44, 0xbc, 0x62, 0x05, 0x06, 0x81, 0x12, 0x74, 0xb8, 0x0c, 0x26, 0x4d, 0x42, 0x29, 0x6e, 0x86, + 0x73, 0x40, 0xf4, 0xa9, 0xa7, 0x1e, 0x90, 0x51, 0xc8, 0x87, 0xbb, 0x60, 0xc2, 0x25, 0x98, 0x8a, + 0xf9, 0xb3, 0xa8, 0xdd, 0x65, 0x6d, 0x15, 0xe2, 0x94, 0x63, 0x5f, 0x5e, 0x19, 0xe6, 0xa3, 0x9e, + 0x2a, 0xba, 0x30, 0xae, 0x84, 0x04, 0x1c, 0x7c, 0x08, 0xca, 0xc2, 0x46, 0x62, 0xc3, 0xc1, 0xad, + 0xbd, 0x24, 0x76, 0x53, 0xae, 0xf7, 0x0a, 0xa0, 0xac, 0x8e, 0xb2, 0x01, 0xa6, 0xc2, 0xec, 0x82, + 0x15, 0x50, 0x48, 0x3c, 0xdf, 0x81, 0xe3, 0x9c, 0xd2, 0x13, 0x98, 0x5c, 0xff, 0xc0, 0x68, 0x5b, + 0xaf, 0xde, 0x56, 0xc7, 0x5e, 0xbf, 0xad, 0x8e, 0xbd, 0x79, 0x5b, 0x1d, 0x7b, 0xd9, 0xad, 0x4a, + 0xaf, 0xba, 0x55, 0xe9, 0x75, 0xb7, 0x2a, 0xbd, 0xe9, 0x56, 0xa5, 0x7f, 0x76, 0xab, 0xd2, 0xef, + 0xfe, 0x55, 0x1d, 0xfb, 0xe1, 0xf2, 0xd0, 0x1f, 0x65, 0x3f, 0x0f, 0x00, 0x00, 0xff, 0xff, 0xac, + 0xc8, 0x8c, 0x78, 0xc0, 0x1d, 0x00, 0x00, +} + +func (m *ApplyConfiguration) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ApplyConfiguration) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *ApplyConfiguration) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + i -= len(m.Expression) + copy(dAtA[i:], m.Expression) + i = encodeVarintGenerated(dAtA, i, uint64(len(m.Expression))) + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil } func (m *AuditAnnotation) Marshal() (dAtA []byte, err error) { @@ -709,6 +1017,34 @@ func (m *ExpressionWarning) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *JSONPatch) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *JSONPatch) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *JSONPatch) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + i -= len(m.Expression) + copy(dAtA[i:], m.Expression) + i = encodeVarintGenerated(dAtA, i, uint64(len(m.Expression))) + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + func (m *MatchCondition) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -824,6 +1160,391 @@ func (m *MatchResources) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *MutatingAdmissionPolicy) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MutatingAdmissionPolicy) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MutatingAdmissionPolicy) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size, err := m.Spec.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + { + size, err := m.ObjectMeta.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + +func (m *MutatingAdmissionPolicyBinding) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MutatingAdmissionPolicyBinding) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MutatingAdmissionPolicyBinding) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size, err := m.Spec.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + { + size, err := m.ObjectMeta.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + +func (m *MutatingAdmissionPolicyBindingList) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MutatingAdmissionPolicyBindingList) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MutatingAdmissionPolicyBindingList) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Items) > 0 { + for iNdEx := len(m.Items) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Items[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + } + { + size, err := m.ListMeta.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + +func (m *MutatingAdmissionPolicyBindingSpec) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MutatingAdmissionPolicyBindingSpec) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MutatingAdmissionPolicyBindingSpec) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.MatchResources != nil { + { + size, err := m.MatchResources.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + if m.ParamRef != nil { + { + size, err := m.ParamRef.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + i -= len(m.PolicyName) + copy(dAtA[i:], m.PolicyName) + i = encodeVarintGenerated(dAtA, i, uint64(len(m.PolicyName))) + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + +func (m *MutatingAdmissionPolicyList) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MutatingAdmissionPolicyList) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MutatingAdmissionPolicyList) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Items) > 0 { + for iNdEx := len(m.Items) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Items[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + } + { + size, err := m.ListMeta.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + +func (m *MutatingAdmissionPolicySpec) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MutatingAdmissionPolicySpec) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MutatingAdmissionPolicySpec) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + i -= len(m.ReinvocationPolicy) + copy(dAtA[i:], m.ReinvocationPolicy) + i = encodeVarintGenerated(dAtA, i, uint64(len(m.ReinvocationPolicy))) + i-- + dAtA[i] = 0x3a + if len(m.MatchConditions) > 0 { + for iNdEx := len(m.MatchConditions) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.MatchConditions[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x32 + } + } + if m.FailurePolicy != nil { + i -= len(*m.FailurePolicy) + copy(dAtA[i:], *m.FailurePolicy) + i = encodeVarintGenerated(dAtA, i, uint64(len(*m.FailurePolicy))) + i-- + dAtA[i] = 0x2a + } + if len(m.Mutations) > 0 { + for iNdEx := len(m.Mutations) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Mutations[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + } + } + if len(m.Variables) > 0 { + for iNdEx := len(m.Variables) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Variables[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + } + if m.MatchConstraints != nil { + { + size, err := m.MatchConstraints.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + if m.ParamKind != nil { + { + size, err := m.ParamKind.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *Mutation) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Mutation) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Mutation) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.JSONPatch != nil { + { + size, err := m.JSONPatch.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + } + if m.ApplyConfiguration != nil { + { + size, err := m.ApplyConfiguration.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + i -= len(m.PatchType) + copy(dAtA[i:], m.PatchType) + i = encodeVarintGenerated(dAtA, i, uint64(len(m.PatchType))) + i-- + dAtA[i] = 0x12 + return len(dAtA) - i, nil +} + func (m *NamedRuleWithOperations) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -1490,6 +2211,17 @@ func encodeVarintGenerated(dAtA []byte, offset int, v uint64) int { dAtA[offset] = uint8(v) return base } +func (m *ApplyConfiguration) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Expression) + n += 1 + l + sovGenerated(uint64(l)) + return n +} + func (m *AuditAnnotation) Size() (n int) { if m == nil { return 0 @@ -1516,6 +2248,17 @@ func (m *ExpressionWarning) Size() (n int) { return n } +func (m *JSONPatch) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Expression) + n += 1 + l + sovGenerated(uint64(l)) + return n +} + func (m *MatchCondition) Size() (n int) { if m == nil { return 0 @@ -1562,6 +2305,145 @@ func (m *MatchResources) Size() (n int) { return n } +func (m *MutatingAdmissionPolicy) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = m.ObjectMeta.Size() + n += 1 + l + sovGenerated(uint64(l)) + l = m.Spec.Size() + n += 1 + l + sovGenerated(uint64(l)) + return n +} + +func (m *MutatingAdmissionPolicyBinding) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = m.ObjectMeta.Size() + n += 1 + l + sovGenerated(uint64(l)) + l = m.Spec.Size() + n += 1 + l + sovGenerated(uint64(l)) + return n +} + +func (m *MutatingAdmissionPolicyBindingList) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = m.ListMeta.Size() + n += 1 + l + sovGenerated(uint64(l)) + if len(m.Items) > 0 { + for _, e := range m.Items { + l = e.Size() + n += 1 + l + sovGenerated(uint64(l)) + } + } + return n +} + +func (m *MutatingAdmissionPolicyBindingSpec) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.PolicyName) + n += 1 + l + sovGenerated(uint64(l)) + if m.ParamRef != nil { + l = m.ParamRef.Size() + n += 1 + l + sovGenerated(uint64(l)) + } + if m.MatchResources != nil { + l = m.MatchResources.Size() + n += 1 + l + sovGenerated(uint64(l)) + } + return n +} + +func (m *MutatingAdmissionPolicyList) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = m.ListMeta.Size() + n += 1 + l + sovGenerated(uint64(l)) + if len(m.Items) > 0 { + for _, e := range m.Items { + l = e.Size() + n += 1 + l + sovGenerated(uint64(l)) + } + } + return n +} + +func (m *MutatingAdmissionPolicySpec) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.ParamKind != nil { + l = m.ParamKind.Size() + n += 1 + l + sovGenerated(uint64(l)) + } + if m.MatchConstraints != nil { + l = m.MatchConstraints.Size() + n += 1 + l + sovGenerated(uint64(l)) + } + if len(m.Variables) > 0 { + for _, e := range m.Variables { + l = e.Size() + n += 1 + l + sovGenerated(uint64(l)) + } + } + if len(m.Mutations) > 0 { + for _, e := range m.Mutations { + l = e.Size() + n += 1 + l + sovGenerated(uint64(l)) + } + } + if m.FailurePolicy != nil { + l = len(*m.FailurePolicy) + n += 1 + l + sovGenerated(uint64(l)) + } + if len(m.MatchConditions) > 0 { + for _, e := range m.MatchConditions { + l = e.Size() + n += 1 + l + sovGenerated(uint64(l)) + } + } + l = len(m.ReinvocationPolicy) + n += 1 + l + sovGenerated(uint64(l)) + return n +} + +func (m *Mutation) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.PatchType) + n += 1 + l + sovGenerated(uint64(l)) + if m.ApplyConfiguration != nil { + l = m.ApplyConfiguration.Size() + n += 1 + l + sovGenerated(uint64(l)) + } + if m.JSONPatch != nil { + l = m.JSONPatch.Size() + n += 1 + l + sovGenerated(uint64(l)) + } + return n +} + func (m *NamedRuleWithOperations) Size() (n int) { if m == nil { return 0 @@ -1818,6 +2700,16 @@ func sovGenerated(x uint64) (n int) { func sozGenerated(x uint64) (n int) { return sovGenerated(uint64((x << 1) ^ uint64((int64(x) >> 63)))) } +func (this *ApplyConfiguration) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&ApplyConfiguration{`, + `Expression:` + fmt.Sprintf("%v", this.Expression) + `,`, + `}`, + }, "") + return s +} func (this *AuditAnnotation) String() string { if this == nil { return "nil" @@ -1840,6 +2732,16 @@ func (this *ExpressionWarning) String() string { }, "") return s } +func (this *JSONPatch) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&JSONPatch{`, + `Expression:` + fmt.Sprintf("%v", this.Expression) + `,`, + `}`, + }, "") + return s +} func (this *MatchCondition) String() string { if this == nil { return "nil" @@ -1875,6 +2777,115 @@ func (this *MatchResources) String() string { }, "") return s } +func (this *MutatingAdmissionPolicy) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&MutatingAdmissionPolicy{`, + `ObjectMeta:` + strings.Replace(strings.Replace(fmt.Sprintf("%v", this.ObjectMeta), "ObjectMeta", "v1.ObjectMeta", 1), `&`, ``, 1) + `,`, + `Spec:` + strings.Replace(strings.Replace(this.Spec.String(), "MutatingAdmissionPolicySpec", "MutatingAdmissionPolicySpec", 1), `&`, ``, 1) + `,`, + `}`, + }, "") + return s +} +func (this *MutatingAdmissionPolicyBinding) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&MutatingAdmissionPolicyBinding{`, + `ObjectMeta:` + strings.Replace(strings.Replace(fmt.Sprintf("%v", this.ObjectMeta), "ObjectMeta", "v1.ObjectMeta", 1), `&`, ``, 1) + `,`, + `Spec:` + strings.Replace(strings.Replace(this.Spec.String(), "MutatingAdmissionPolicyBindingSpec", "MutatingAdmissionPolicyBindingSpec", 1), `&`, ``, 1) + `,`, + `}`, + }, "") + return s +} +func (this *MutatingAdmissionPolicyBindingList) String() string { + if this == nil { + return "nil" + } + repeatedStringForItems := "[]MutatingAdmissionPolicyBinding{" + for _, f := range this.Items { + repeatedStringForItems += strings.Replace(strings.Replace(f.String(), "MutatingAdmissionPolicyBinding", "MutatingAdmissionPolicyBinding", 1), `&`, ``, 1) + "," + } + repeatedStringForItems += "}" + s := strings.Join([]string{`&MutatingAdmissionPolicyBindingList{`, + `ListMeta:` + strings.Replace(strings.Replace(fmt.Sprintf("%v", this.ListMeta), "ListMeta", "v1.ListMeta", 1), `&`, ``, 1) + `,`, + `Items:` + repeatedStringForItems + `,`, + `}`, + }, "") + return s +} +func (this *MutatingAdmissionPolicyBindingSpec) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&MutatingAdmissionPolicyBindingSpec{`, + `PolicyName:` + fmt.Sprintf("%v", this.PolicyName) + `,`, + `ParamRef:` + strings.Replace(this.ParamRef.String(), "ParamRef", "ParamRef", 1) + `,`, + `MatchResources:` + strings.Replace(this.MatchResources.String(), "MatchResources", "MatchResources", 1) + `,`, + `}`, + }, "") + return s +} +func (this *MutatingAdmissionPolicyList) String() string { + if this == nil { + return "nil" + } + repeatedStringForItems := "[]MutatingAdmissionPolicy{" + for _, f := range this.Items { + repeatedStringForItems += strings.Replace(strings.Replace(f.String(), "MutatingAdmissionPolicy", "MutatingAdmissionPolicy", 1), `&`, ``, 1) + "," + } + repeatedStringForItems += "}" + s := strings.Join([]string{`&MutatingAdmissionPolicyList{`, + `ListMeta:` + strings.Replace(strings.Replace(fmt.Sprintf("%v", this.ListMeta), "ListMeta", "v1.ListMeta", 1), `&`, ``, 1) + `,`, + `Items:` + repeatedStringForItems + `,`, + `}`, + }, "") + return s +} +func (this *MutatingAdmissionPolicySpec) String() string { + if this == nil { + return "nil" + } + repeatedStringForVariables := "[]Variable{" + for _, f := range this.Variables { + repeatedStringForVariables += strings.Replace(strings.Replace(f.String(), "Variable", "Variable", 1), `&`, ``, 1) + "," + } + repeatedStringForVariables += "}" + repeatedStringForMutations := "[]Mutation{" + for _, f := range this.Mutations { + repeatedStringForMutations += strings.Replace(strings.Replace(f.String(), "Mutation", "Mutation", 1), `&`, ``, 1) + "," + } + repeatedStringForMutations += "}" + repeatedStringForMatchConditions := "[]MatchCondition{" + for _, f := range this.MatchConditions { + repeatedStringForMatchConditions += strings.Replace(strings.Replace(f.String(), "MatchCondition", "MatchCondition", 1), `&`, ``, 1) + "," + } + repeatedStringForMatchConditions += "}" + s := strings.Join([]string{`&MutatingAdmissionPolicySpec{`, + `ParamKind:` + strings.Replace(this.ParamKind.String(), "ParamKind", "ParamKind", 1) + `,`, + `MatchConstraints:` + strings.Replace(this.MatchConstraints.String(), "MatchResources", "MatchResources", 1) + `,`, + `Variables:` + repeatedStringForVariables + `,`, + `Mutations:` + repeatedStringForMutations + `,`, + `FailurePolicy:` + valueToStringGenerated(this.FailurePolicy) + `,`, + `MatchConditions:` + repeatedStringForMatchConditions + `,`, + `ReinvocationPolicy:` + fmt.Sprintf("%v", this.ReinvocationPolicy) + `,`, + `}`, + }, "") + return s +} +func (this *Mutation) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&Mutation{`, + `PatchType:` + fmt.Sprintf("%v", this.PatchType) + `,`, + `ApplyConfiguration:` + strings.Replace(this.ApplyConfiguration.String(), "ApplyConfiguration", "ApplyConfiguration", 1) + `,`, + `JSONPatch:` + strings.Replace(this.JSONPatch.String(), "JSONPatch", "JSONPatch", 1) + `,`, + `}`, + }, "") + return s +} func (this *NamedRuleWithOperations) String() string { if this == nil { return "nil" @@ -2078,6 +3089,88 @@ func valueToStringGenerated(v interface{}) string { pv := reflect.Indirect(rv).Interface() return fmt.Sprintf("*%v", pv) } +func (m *ApplyConfiguration) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ApplyConfiguration: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ApplyConfiguration: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Expression", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Expression = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenerated(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenerated + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *AuditAnnotation) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -2306,6 +3399,88 @@ func (m *ExpressionWarning) Unmarshal(dAtA []byte) error { } return nil } +func (m *JSONPatch) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: JSONPatch: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: JSONPatch: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Expression", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Expression = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenerated(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenerated + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *MatchCondition) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -2643,6 +3818,1069 @@ func (m *MatchResources) Unmarshal(dAtA []byte) error { } return nil } +func (m *MutatingAdmissionPolicy) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MutatingAdmissionPolicy: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MutatingAdmissionPolicy: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ObjectMeta", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.ObjectMeta.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Spec", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Spec.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenerated(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenerated + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MutatingAdmissionPolicyBinding) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MutatingAdmissionPolicyBinding: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MutatingAdmissionPolicyBinding: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ObjectMeta", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.ObjectMeta.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Spec", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Spec.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenerated(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenerated + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MutatingAdmissionPolicyBindingList) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MutatingAdmissionPolicyBindingList: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MutatingAdmissionPolicyBindingList: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ListMeta", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.ListMeta.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Items", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Items = append(m.Items, MutatingAdmissionPolicyBinding{}) + if err := m.Items[len(m.Items)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenerated(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenerated + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MutatingAdmissionPolicyBindingSpec) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MutatingAdmissionPolicyBindingSpec: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MutatingAdmissionPolicyBindingSpec: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field PolicyName", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.PolicyName = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ParamRef", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.ParamRef == nil { + m.ParamRef = &ParamRef{} + } + if err := m.ParamRef.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MatchResources", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.MatchResources == nil { + m.MatchResources = &MatchResources{} + } + if err := m.MatchResources.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenerated(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenerated + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MutatingAdmissionPolicyList) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MutatingAdmissionPolicyList: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MutatingAdmissionPolicyList: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ListMeta", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.ListMeta.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Items", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Items = append(m.Items, MutatingAdmissionPolicy{}) + if err := m.Items[len(m.Items)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenerated(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenerated + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MutatingAdmissionPolicySpec) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MutatingAdmissionPolicySpec: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MutatingAdmissionPolicySpec: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ParamKind", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.ParamKind == nil { + m.ParamKind = &ParamKind{} + } + if err := m.ParamKind.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MatchConstraints", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.MatchConstraints == nil { + m.MatchConstraints = &MatchResources{} + } + if err := m.MatchConstraints.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Variables", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Variables = append(m.Variables, Variable{}) + if err := m.Variables[len(m.Variables)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Mutations", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Mutations = append(m.Mutations, Mutation{}) + if err := m.Mutations[len(m.Mutations)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field FailurePolicy", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + s := FailurePolicyType(dAtA[iNdEx:postIndex]) + m.FailurePolicy = &s + iNdEx = postIndex + case 6: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MatchConditions", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.MatchConditions = append(m.MatchConditions, MatchCondition{}) + if err := m.MatchConditions[len(m.MatchConditions)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 7: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ReinvocationPolicy", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ReinvocationPolicy = k8s_io_api_admissionregistration_v1.ReinvocationPolicyType(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenerated(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenerated + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *Mutation) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Mutation: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Mutation: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field PatchType", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.PatchType = PatchType(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ApplyConfiguration", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.ApplyConfiguration == nil { + m.ApplyConfiguration = &ApplyConfiguration{} + } + if err := m.ApplyConfiguration.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field JSONPatch", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.JSONPatch == nil { + m.JSONPatch = &JSONPatch{} + } + if err := m.JSONPatch.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenerated(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenerated + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *NamedRuleWithOperations) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 diff --git a/staging/src/k8s.io/api/admissionregistration/v1alpha1/generated.proto b/staging/src/k8s.io/api/admissionregistration/v1alpha1/generated.proto index d5974d5ec4c..88344ce87ac 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1alpha1/generated.proto +++ b/staging/src/k8s.io/api/admissionregistration/v1alpha1/generated.proto @@ -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.' - CEL type of object field (such as 'Object.spec') + // - 'Object.....` - CEL type of nested field (such as 'Object.spec.containers') + // + // CEL expressions have access to the contents of the API request, organized into CEL variables as well as some other useful variables: + // + // - 'object' - The object from the incoming request. The value is null for DELETE requests. + // - 'oldObject' - The existing object. The value is null for CREATE requests. + // - 'request' - Attributes of the API request([ref](/pkg/apis/admission/types.go#AdmissionRequest)). + // - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind. + // - 'namespaceObject' - The namespace object that the incoming object belongs to. The value is null for cluster-scoped resources. + // - 'variables' - Map of composited variables, from its name to its lazily evaluated value. + // For example, a variable named 'foo' can be accessed as 'variables.foo'. + // - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request. + // See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz + // - 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the + // request resource. + // + // The `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the + // object. No other metadata properties are accessible. + // + // Only property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. + // Required. + 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.' - CEL type of object field (such as 'Object.spec') + // - 'Object.....` - CEL type of nested field (such as 'Object.spec.containers') + // + // CEL expressions have access to the contents of the API request, organized into CEL variables as well as some other useful variables: + // + // - 'object' - The object from the incoming request. The value is null for DELETE requests. + // - 'oldObject' - The existing object. The value is null for CREATE requests. + // - 'request' - Attributes of the API request([ref](/pkg/apis/admission/types.go#AdmissionRequest)). + // - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind. + // - 'namespaceObject' - The namespace object that the incoming object belongs to. The value is null for cluster-scoped resources. + // - 'variables' - Map of composited variables, from its name to its lazily evaluated value. + // For example, a variable named 'foo' can be accessed as 'variables.foo'. + // - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request. + // See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz + // - 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the + // request resource. + // + // CEL expressions have access to [Kubernetes CEL function libraries](https://kubernetes.io/docs/reference/using-api/cel/#cel-options-language-features-and-libraries) + // as well as: + // + // - 'jsonpatch.escapeKey' - Performs JSONPatch key escaping. '~' and '/' are escaped as '~0' and `~1' respectively). + // + // Only property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. + // Required. + 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 { diff --git a/staging/src/k8s.io/api/admissionregistration/v1alpha1/types_swagger_doc_generated.go b/staging/src/k8s.io/api/admissionregistration/v1alpha1/types_swagger_doc_generated.go index dcf46b324f1..32222a81b89 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1alpha1/types_swagger_doc_generated.go +++ b/staging/src/k8s.io/api/admissionregistration/v1alpha1/types_swagger_doc_generated.go @@ -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.' - CEL type of object field (such as 'Object.spec') - 'Object.....` - 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.' - CEL type of object field (such as 'Object.spec') - 'Object.....` - 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.", diff --git a/staging/src/k8s.io/api/admissionregistration/v1alpha1/zz_generated.deepcopy.go b/staging/src/k8s.io/api/admissionregistration/v1alpha1/zz_generated.deepcopy.go index 24cd0e4e9b4..97c159c74f0 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1alpha1/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/api/admissionregistration/v1alpha1/zz_generated.deepcopy.go @@ -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 diff --git a/staging/src/k8s.io/api/admissionregistration/v1alpha1/zz_generated.prerelease-lifecycle.go b/staging/src/k8s.io/api/admissionregistration/v1alpha1/zz_generated.prerelease-lifecycle.go index 692215b79d4..91c813d5f77 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1alpha1/zz_generated.prerelease-lifecycle.go +++ b/staging/src/k8s.io/api/admissionregistration/v1alpha1/zz_generated.prerelease-lifecycle.go @@ -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) { diff --git a/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1alpha1.MutatingAdmissionPolicy.json b/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1alpha1.MutatingAdmissionPolicy.json new file mode 100644 index 00000000000..985735ab110 --- /dev/null +++ b/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1alpha1.MutatingAdmissionPolicy.json @@ -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" + } +} \ No newline at end of file diff --git a/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1alpha1.MutatingAdmissionPolicy.pb b/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1alpha1.MutatingAdmissionPolicy.pb new file mode 100644 index 0000000000000000000000000000000000000000..fb9d898bc43fabddb989d81917baa8685f01c478 GIT binary patch literal 1013 zcmcgrPiqrF6yLOgCa-q09fZoz(}Esys5a0Na**1KpiwG_2XB+jWIOI;W|+x_#v*w3 zTX-w0WyUZmG)>g<>gOIjWsq&xZqOXH0lvywV?6YPubwSY61IM!%su z6D9~}AFV)#^QAF@ds8AvO0`R%P^!XzDIed4iLHlUSJ&%N(6eBakImbK@{<`fVeS0W zab)sQH3>J=?@ic++dyb#0DWGEAz0f}GD}W})l{O|*{gd5t&A>wyycZjFC1^q3odU9 z{ZllY0xAs^&tl2(20GQOmZ_(9bBaCJGFQt?wAzBt;IM@9=NL?4VVtG-e-RD*Wc?>_ z)Off43p(^nS(()%jt^0&irrMZ#@leG>29ivCWP@^(;8FnuS|m-O_`X>#67{W7Ml~h^^ArlI^2*%@l5JIB6DU2Z7=ug^@qFvziY1ZSx5Je ze{?j0ADhrf@R%g(3z?x68=4biADWmko)hQHl--5g^!z-6MyxfM#GCW?OE$YLEa)PU zkCk>swoWO-3H_xBnxi+BF|1B7!x51NKus=k{RMsR^%!4k`1kNsb)7>!j3l8tnF;Gm zrv^*s7d=;HTL~L!uVdgimoe*w!iY&8-O-pg45t^PqeS9n>4H(iI`beb0dD%{N7vy#v^Xq6ZB0)5iY pM$o?GnRr3m`4g~+*DOZa+y@otF~&jvy%u%sMcR*OR(p=`JOkNcS%&}s literal 0 HcmV?d00001 diff --git a/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1alpha1.MutatingAdmissionPolicyBinding.yaml b/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1alpha1.MutatingAdmissionPolicyBinding.yaml new file mode 100644 index 00000000000..251870e8e67 --- /dev/null +++ b/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1alpha1.MutatingAdmissionPolicyBinding.yaml @@ -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 diff --git a/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/applyconfiguration.go b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/applyconfiguration.go new file mode 100644 index 00000000000..b08ac722410 --- /dev/null +++ b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/applyconfiguration.go @@ -0,0 +1,39 @@ +/* +Copyright 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. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// ApplyConfigurationApplyConfiguration represents a declarative configuration of the ApplyConfiguration type for use +// with apply. +type ApplyConfigurationApplyConfiguration struct { + Expression *string `json:"expression,omitempty"` +} + +// ApplyConfigurationApplyConfiguration constructs a declarative configuration of the ApplyConfiguration type for use with +// apply. +func ApplyConfiguration() *ApplyConfigurationApplyConfiguration { + return &ApplyConfigurationApplyConfiguration{} +} + +// WithExpression sets the Expression field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Expression field is set to the value of the last call. +func (b *ApplyConfigurationApplyConfiguration) WithExpression(value string) *ApplyConfigurationApplyConfiguration { + b.Expression = &value + return b +} diff --git a/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/jsonpatch.go b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/jsonpatch.go new file mode 100644 index 00000000000..418d86a2b5f --- /dev/null +++ b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/jsonpatch.go @@ -0,0 +1,39 @@ +/* +Copyright 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. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// JSONPatchApplyConfiguration represents a declarative configuration of the JSONPatch type for use +// with apply. +type JSONPatchApplyConfiguration struct { + Expression *string `json:"expression,omitempty"` +} + +// JSONPatchApplyConfiguration constructs a declarative configuration of the JSONPatch type for use with +// apply. +func JSONPatch() *JSONPatchApplyConfiguration { + return &JSONPatchApplyConfiguration{} +} + +// WithExpression sets the Expression field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Expression field is set to the value of the last call. +func (b *JSONPatchApplyConfiguration) WithExpression(value string) *JSONPatchApplyConfiguration { + b.Expression = &value + return b +} diff --git a/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/mutatingadmissionpolicy.go b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/mutatingadmissionpolicy.go new file mode 100644 index 00000000000..d66071c187b --- /dev/null +++ b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/mutatingadmissionpolicy.go @@ -0,0 +1,253 @@ +/* +Copyright 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. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + managedfields "k8s.io/apimachinery/pkg/util/managedfields" + internal "k8s.io/client-go/applyconfigurations/internal" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// MutatingAdmissionPolicyApplyConfiguration represents a declarative configuration of the MutatingAdmissionPolicy type for use +// with apply. +type MutatingAdmissionPolicyApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:",inline"` + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + Spec *MutatingAdmissionPolicySpecApplyConfiguration `json:"spec,omitempty"` +} + +// MutatingAdmissionPolicy constructs a declarative configuration of the MutatingAdmissionPolicy type for use with +// apply. +func MutatingAdmissionPolicy(name string) *MutatingAdmissionPolicyApplyConfiguration { + b := &MutatingAdmissionPolicyApplyConfiguration{} + b.WithName(name) + b.WithKind("MutatingAdmissionPolicy") + b.WithAPIVersion("admissionregistration.k8s.io/v1alpha1") + return b +} + +// ExtractMutatingAdmissionPolicy extracts the applied configuration owned by fieldManager from +// mutatingAdmissionPolicy. If no managedFields are found in mutatingAdmissionPolicy for fieldManager, a +// MutatingAdmissionPolicyApplyConfiguration is returned with only the Name, Namespace (if applicable), +// APIVersion and Kind populated. It is possible that no managed fields were found for because other +// field managers have taken ownership of all the fields previously owned by fieldManager, or because +// the fieldManager never owned fields any fields. +// mutatingAdmissionPolicy must be a unmodified MutatingAdmissionPolicy API object that was retrieved from the Kubernetes API. +// ExtractMutatingAdmissionPolicy provides a way to perform a extract/modify-in-place/apply workflow. +// Note that an extracted apply configuration will contain fewer fields than what the fieldManager previously +// applied if another fieldManager has updated or force applied any of the previously applied fields. +// Experimental! +func ExtractMutatingAdmissionPolicy(mutatingAdmissionPolicy *admissionregistrationv1alpha1.MutatingAdmissionPolicy, fieldManager string) (*MutatingAdmissionPolicyApplyConfiguration, error) { + return extractMutatingAdmissionPolicy(mutatingAdmissionPolicy, fieldManager, "") +} + +// ExtractMutatingAdmissionPolicyStatus is the same as ExtractMutatingAdmissionPolicy except +// that it extracts the status subresource applied configuration. +// Experimental! +func ExtractMutatingAdmissionPolicyStatus(mutatingAdmissionPolicy *admissionregistrationv1alpha1.MutatingAdmissionPolicy, fieldManager string) (*MutatingAdmissionPolicyApplyConfiguration, error) { + return extractMutatingAdmissionPolicy(mutatingAdmissionPolicy, fieldManager, "status") +} + +func extractMutatingAdmissionPolicy(mutatingAdmissionPolicy *admissionregistrationv1alpha1.MutatingAdmissionPolicy, fieldManager string, subresource string) (*MutatingAdmissionPolicyApplyConfiguration, error) { + b := &MutatingAdmissionPolicyApplyConfiguration{} + err := managedfields.ExtractInto(mutatingAdmissionPolicy, internal.Parser().Type("io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy"), fieldManager, b, subresource) + if err != nil { + return nil, err + } + b.WithName(mutatingAdmissionPolicy.Name) + + b.WithKind("MutatingAdmissionPolicy") + b.WithAPIVersion("admissionregistration.k8s.io/v1alpha1") + return b, nil +} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *MutatingAdmissionPolicyApplyConfiguration) WithKind(value string) *MutatingAdmissionPolicyApplyConfiguration { + b.TypeMetaApplyConfiguration.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *MutatingAdmissionPolicyApplyConfiguration) WithAPIVersion(value string) *MutatingAdmissionPolicyApplyConfiguration { + b.TypeMetaApplyConfiguration.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *MutatingAdmissionPolicyApplyConfiguration) WithName(value string) *MutatingAdmissionPolicyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *MutatingAdmissionPolicyApplyConfiguration) WithGenerateName(value string) *MutatingAdmissionPolicyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *MutatingAdmissionPolicyApplyConfiguration) WithNamespace(value string) *MutatingAdmissionPolicyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *MutatingAdmissionPolicyApplyConfiguration) WithUID(value types.UID) *MutatingAdmissionPolicyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *MutatingAdmissionPolicyApplyConfiguration) WithResourceVersion(value string) *MutatingAdmissionPolicyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *MutatingAdmissionPolicyApplyConfiguration) WithGeneration(value int64) *MutatingAdmissionPolicyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *MutatingAdmissionPolicyApplyConfiguration) WithCreationTimestamp(value metav1.Time) *MutatingAdmissionPolicyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *MutatingAdmissionPolicyApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *MutatingAdmissionPolicyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *MutatingAdmissionPolicyApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *MutatingAdmissionPolicyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *MutatingAdmissionPolicyApplyConfiguration) WithLabels(entries map[string]string) *MutatingAdmissionPolicyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *MutatingAdmissionPolicyApplyConfiguration) WithAnnotations(entries map[string]string) *MutatingAdmissionPolicyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *MutatingAdmissionPolicyApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *MutatingAdmissionPolicyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *MutatingAdmissionPolicyApplyConfiguration) WithFinalizers(values ...string) *MutatingAdmissionPolicyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) + } + return b +} + +func (b *MutatingAdmissionPolicyApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *MutatingAdmissionPolicyApplyConfiguration) WithSpec(value *MutatingAdmissionPolicySpecApplyConfiguration) *MutatingAdmissionPolicyApplyConfiguration { + b.Spec = value + return b +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *MutatingAdmissionPolicyApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Name +} diff --git a/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/mutatingadmissionpolicybinding.go b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/mutatingadmissionpolicybinding.go new file mode 100644 index 00000000000..7cccd291b37 --- /dev/null +++ b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/mutatingadmissionpolicybinding.go @@ -0,0 +1,253 @@ +/* +Copyright 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. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + managedfields "k8s.io/apimachinery/pkg/util/managedfields" + internal "k8s.io/client-go/applyconfigurations/internal" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// MutatingAdmissionPolicyBindingApplyConfiguration represents a declarative configuration of the MutatingAdmissionPolicyBinding type for use +// with apply. +type MutatingAdmissionPolicyBindingApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:",inline"` + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + Spec *MutatingAdmissionPolicyBindingSpecApplyConfiguration `json:"spec,omitempty"` +} + +// MutatingAdmissionPolicyBinding constructs a declarative configuration of the MutatingAdmissionPolicyBinding type for use with +// apply. +func MutatingAdmissionPolicyBinding(name string) *MutatingAdmissionPolicyBindingApplyConfiguration { + b := &MutatingAdmissionPolicyBindingApplyConfiguration{} + b.WithName(name) + b.WithKind("MutatingAdmissionPolicyBinding") + b.WithAPIVersion("admissionregistration.k8s.io/v1alpha1") + return b +} + +// ExtractMutatingAdmissionPolicyBinding extracts the applied configuration owned by fieldManager from +// mutatingAdmissionPolicyBinding. If no managedFields are found in mutatingAdmissionPolicyBinding for fieldManager, a +// MutatingAdmissionPolicyBindingApplyConfiguration is returned with only the Name, Namespace (if applicable), +// APIVersion and Kind populated. It is possible that no managed fields were found for because other +// field managers have taken ownership of all the fields previously owned by fieldManager, or because +// the fieldManager never owned fields any fields. +// mutatingAdmissionPolicyBinding must be a unmodified MutatingAdmissionPolicyBinding API object that was retrieved from the Kubernetes API. +// ExtractMutatingAdmissionPolicyBinding provides a way to perform a extract/modify-in-place/apply workflow. +// Note that an extracted apply configuration will contain fewer fields than what the fieldManager previously +// applied if another fieldManager has updated or force applied any of the previously applied fields. +// Experimental! +func ExtractMutatingAdmissionPolicyBinding(mutatingAdmissionPolicyBinding *admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding, fieldManager string) (*MutatingAdmissionPolicyBindingApplyConfiguration, error) { + return extractMutatingAdmissionPolicyBinding(mutatingAdmissionPolicyBinding, fieldManager, "") +} + +// ExtractMutatingAdmissionPolicyBindingStatus is the same as ExtractMutatingAdmissionPolicyBinding except +// that it extracts the status subresource applied configuration. +// Experimental! +func ExtractMutatingAdmissionPolicyBindingStatus(mutatingAdmissionPolicyBinding *admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding, fieldManager string) (*MutatingAdmissionPolicyBindingApplyConfiguration, error) { + return extractMutatingAdmissionPolicyBinding(mutatingAdmissionPolicyBinding, fieldManager, "status") +} + +func extractMutatingAdmissionPolicyBinding(mutatingAdmissionPolicyBinding *admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding, fieldManager string, subresource string) (*MutatingAdmissionPolicyBindingApplyConfiguration, error) { + b := &MutatingAdmissionPolicyBindingApplyConfiguration{} + err := managedfields.ExtractInto(mutatingAdmissionPolicyBinding, internal.Parser().Type("io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding"), fieldManager, b, subresource) + if err != nil { + return nil, err + } + b.WithName(mutatingAdmissionPolicyBinding.Name) + + b.WithKind("MutatingAdmissionPolicyBinding") + b.WithAPIVersion("admissionregistration.k8s.io/v1alpha1") + return b, nil +} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *MutatingAdmissionPolicyBindingApplyConfiguration) WithKind(value string) *MutatingAdmissionPolicyBindingApplyConfiguration { + b.TypeMetaApplyConfiguration.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *MutatingAdmissionPolicyBindingApplyConfiguration) WithAPIVersion(value string) *MutatingAdmissionPolicyBindingApplyConfiguration { + b.TypeMetaApplyConfiguration.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *MutatingAdmissionPolicyBindingApplyConfiguration) WithName(value string) *MutatingAdmissionPolicyBindingApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *MutatingAdmissionPolicyBindingApplyConfiguration) WithGenerateName(value string) *MutatingAdmissionPolicyBindingApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *MutatingAdmissionPolicyBindingApplyConfiguration) WithNamespace(value string) *MutatingAdmissionPolicyBindingApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *MutatingAdmissionPolicyBindingApplyConfiguration) WithUID(value types.UID) *MutatingAdmissionPolicyBindingApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *MutatingAdmissionPolicyBindingApplyConfiguration) WithResourceVersion(value string) *MutatingAdmissionPolicyBindingApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *MutatingAdmissionPolicyBindingApplyConfiguration) WithGeneration(value int64) *MutatingAdmissionPolicyBindingApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *MutatingAdmissionPolicyBindingApplyConfiguration) WithCreationTimestamp(value metav1.Time) *MutatingAdmissionPolicyBindingApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *MutatingAdmissionPolicyBindingApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *MutatingAdmissionPolicyBindingApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *MutatingAdmissionPolicyBindingApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *MutatingAdmissionPolicyBindingApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *MutatingAdmissionPolicyBindingApplyConfiguration) WithLabels(entries map[string]string) *MutatingAdmissionPolicyBindingApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *MutatingAdmissionPolicyBindingApplyConfiguration) WithAnnotations(entries map[string]string) *MutatingAdmissionPolicyBindingApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *MutatingAdmissionPolicyBindingApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *MutatingAdmissionPolicyBindingApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *MutatingAdmissionPolicyBindingApplyConfiguration) WithFinalizers(values ...string) *MutatingAdmissionPolicyBindingApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) + } + return b +} + +func (b *MutatingAdmissionPolicyBindingApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *MutatingAdmissionPolicyBindingApplyConfiguration) WithSpec(value *MutatingAdmissionPolicyBindingSpecApplyConfiguration) *MutatingAdmissionPolicyBindingApplyConfiguration { + b.Spec = value + return b +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *MutatingAdmissionPolicyBindingApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Name +} diff --git a/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/mutatingadmissionpolicybindingspec.go b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/mutatingadmissionpolicybindingspec.go new file mode 100644 index 00000000000..04729f42b1f --- /dev/null +++ b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/mutatingadmissionpolicybindingspec.go @@ -0,0 +1,57 @@ +/* +Copyright 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. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// MutatingAdmissionPolicyBindingSpecApplyConfiguration represents a declarative configuration of the MutatingAdmissionPolicyBindingSpec type for use +// with apply. +type MutatingAdmissionPolicyBindingSpecApplyConfiguration struct { + PolicyName *string `json:"policyName,omitempty"` + ParamRef *ParamRefApplyConfiguration `json:"paramRef,omitempty"` + MatchResources *MatchResourcesApplyConfiguration `json:"matchResources,omitempty"` +} + +// MutatingAdmissionPolicyBindingSpecApplyConfiguration constructs a declarative configuration of the MutatingAdmissionPolicyBindingSpec type for use with +// apply. +func MutatingAdmissionPolicyBindingSpec() *MutatingAdmissionPolicyBindingSpecApplyConfiguration { + return &MutatingAdmissionPolicyBindingSpecApplyConfiguration{} +} + +// WithPolicyName sets the PolicyName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the PolicyName field is set to the value of the last call. +func (b *MutatingAdmissionPolicyBindingSpecApplyConfiguration) WithPolicyName(value string) *MutatingAdmissionPolicyBindingSpecApplyConfiguration { + b.PolicyName = &value + return b +} + +// WithParamRef sets the ParamRef field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ParamRef field is set to the value of the last call. +func (b *MutatingAdmissionPolicyBindingSpecApplyConfiguration) WithParamRef(value *ParamRefApplyConfiguration) *MutatingAdmissionPolicyBindingSpecApplyConfiguration { + b.ParamRef = value + return b +} + +// WithMatchResources sets the MatchResources field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the MatchResources field is set to the value of the last call. +func (b *MutatingAdmissionPolicyBindingSpecApplyConfiguration) WithMatchResources(value *MatchResourcesApplyConfiguration) *MutatingAdmissionPolicyBindingSpecApplyConfiguration { + b.MatchResources = value + return b +} diff --git a/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/mutatingadmissionpolicyspec.go b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/mutatingadmissionpolicyspec.go new file mode 100644 index 00000000000..334056a3724 --- /dev/null +++ b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/mutatingadmissionpolicyspec.go @@ -0,0 +1,113 @@ +/* +Copyright 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. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/api/admissionregistration/v1" + admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1" +) + +// MutatingAdmissionPolicySpecApplyConfiguration represents a declarative configuration of the MutatingAdmissionPolicySpec type for use +// with apply. +type MutatingAdmissionPolicySpecApplyConfiguration struct { + ParamKind *ParamKindApplyConfiguration `json:"paramKind,omitempty"` + MatchConstraints *MatchResourcesApplyConfiguration `json:"matchConstraints,omitempty"` + Variables []VariableApplyConfiguration `json:"variables,omitempty"` + Mutations []MutationApplyConfiguration `json:"mutations,omitempty"` + FailurePolicy *admissionregistrationv1alpha1.FailurePolicyType `json:"failurePolicy,omitempty"` + MatchConditions []MatchConditionApplyConfiguration `json:"matchConditions,omitempty"` + ReinvocationPolicy *v1.ReinvocationPolicyType `json:"reinvocationPolicy,omitempty"` +} + +// MutatingAdmissionPolicySpecApplyConfiguration constructs a declarative configuration of the MutatingAdmissionPolicySpec type for use with +// apply. +func MutatingAdmissionPolicySpec() *MutatingAdmissionPolicySpecApplyConfiguration { + return &MutatingAdmissionPolicySpecApplyConfiguration{} +} + +// WithParamKind sets the ParamKind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ParamKind field is set to the value of the last call. +func (b *MutatingAdmissionPolicySpecApplyConfiguration) WithParamKind(value *ParamKindApplyConfiguration) *MutatingAdmissionPolicySpecApplyConfiguration { + b.ParamKind = value + return b +} + +// WithMatchConstraints sets the MatchConstraints field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the MatchConstraints field is set to the value of the last call. +func (b *MutatingAdmissionPolicySpecApplyConfiguration) WithMatchConstraints(value *MatchResourcesApplyConfiguration) *MutatingAdmissionPolicySpecApplyConfiguration { + b.MatchConstraints = value + return b +} + +// WithVariables adds the given value to the Variables field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Variables field. +func (b *MutatingAdmissionPolicySpecApplyConfiguration) WithVariables(values ...*VariableApplyConfiguration) *MutatingAdmissionPolicySpecApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithVariables") + } + b.Variables = append(b.Variables, *values[i]) + } + return b +} + +// WithMutations adds the given value to the Mutations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Mutations field. +func (b *MutatingAdmissionPolicySpecApplyConfiguration) WithMutations(values ...*MutationApplyConfiguration) *MutatingAdmissionPolicySpecApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithMutations") + } + b.Mutations = append(b.Mutations, *values[i]) + } + return b +} + +// WithFailurePolicy sets the FailurePolicy field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the FailurePolicy field is set to the value of the last call. +func (b *MutatingAdmissionPolicySpecApplyConfiguration) WithFailurePolicy(value admissionregistrationv1alpha1.FailurePolicyType) *MutatingAdmissionPolicySpecApplyConfiguration { + b.FailurePolicy = &value + return b +} + +// WithMatchConditions adds the given value to the MatchConditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the MatchConditions field. +func (b *MutatingAdmissionPolicySpecApplyConfiguration) WithMatchConditions(values ...*MatchConditionApplyConfiguration) *MutatingAdmissionPolicySpecApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithMatchConditions") + } + b.MatchConditions = append(b.MatchConditions, *values[i]) + } + return b +} + +// WithReinvocationPolicy sets the ReinvocationPolicy field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ReinvocationPolicy field is set to the value of the last call. +func (b *MutatingAdmissionPolicySpecApplyConfiguration) WithReinvocationPolicy(value v1.ReinvocationPolicyType) *MutatingAdmissionPolicySpecApplyConfiguration { + b.ReinvocationPolicy = &value + return b +} diff --git a/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/mutation.go b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/mutation.go new file mode 100644 index 00000000000..4ed9d93fdbb --- /dev/null +++ b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1/mutation.go @@ -0,0 +1,61 @@ +/* +Copyright 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. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1" +) + +// MutationApplyConfiguration represents a declarative configuration of the Mutation type for use +// with apply. +type MutationApplyConfiguration struct { + PatchType *admissionregistrationv1alpha1.PatchType `json:"patchType,omitempty"` + ApplyConfiguration *ApplyConfigurationApplyConfiguration `json:"applyConfiguration,omitempty"` + JSONPatch *JSONPatchApplyConfiguration `json:"jsonPatch,omitempty"` +} + +// MutationApplyConfiguration constructs a declarative configuration of the Mutation type for use with +// apply. +func Mutation() *MutationApplyConfiguration { + return &MutationApplyConfiguration{} +} + +// WithPatchType sets the PatchType field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the PatchType field is set to the value of the last call. +func (b *MutationApplyConfiguration) WithPatchType(value admissionregistrationv1alpha1.PatchType) *MutationApplyConfiguration { + b.PatchType = &value + return b +} + +// WithApplyConfiguration sets the ApplyConfiguration field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ApplyConfiguration field is set to the value of the last call. +func (b *MutationApplyConfiguration) WithApplyConfiguration(value *ApplyConfigurationApplyConfiguration) *MutationApplyConfiguration { + b.ApplyConfiguration = value + return b +} + +// WithJSONPatch sets the JSONPatch field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the JSONPatch field is set to the value of the last call. +func (b *MutationApplyConfiguration) WithJSONPatch(value *JSONPatchApplyConfiguration) *MutationApplyConfiguration { + b.JSONPatch = value + return b +} diff --git a/staging/src/k8s.io/client-go/applyconfigurations/internal/internal.go b/staging/src/k8s.io/client-go/applyconfigurations/internal/internal.go index ec581483b35..9212c31ea54 100644 --- a/staging/src/k8s.io/client-go/applyconfigurations/internal/internal.go +++ b/staging/src/k8s.io/client-go/applyconfigurations/internal/internal.go @@ -512,6 +512,12 @@ var schemaYAML = typed.YAMLObject(`types: - name: url type: scalar: string +- name: io.k8s.api.admissionregistration.v1alpha1.ApplyConfiguration + map: + fields: + - name: expression + type: + scalar: string - name: io.k8s.api.admissionregistration.v1alpha1.AuditAnnotation map: fields: @@ -534,6 +540,12 @@ var schemaYAML = typed.YAMLObject(`types: type: scalar: string default: "" +- name: io.k8s.api.admissionregistration.v1alpha1.JSONPatch + map: + fields: + - name: expression + type: + scalar: string - name: io.k8s.api.admissionregistration.v1alpha1.MatchCondition map: fields: @@ -570,6 +582,100 @@ var schemaYAML = typed.YAMLObject(`types: namedType: io.k8s.api.admissionregistration.v1alpha1.NamedRuleWithOperations elementRelationship: atomic elementRelationship: atomic +- name: io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicy + map: + fields: + - name: apiVersion + type: + scalar: string + - name: kind + type: + scalar: string + - name: metadata + type: + namedType: io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta + default: {} + - name: spec + type: + namedType: io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicySpec + default: {} +- name: io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBinding + map: + fields: + - name: apiVersion + type: + scalar: string + - name: kind + type: + scalar: string + - name: metadata + type: + namedType: io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta + default: {} + - name: spec + type: + namedType: io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBindingSpec + default: {} +- name: io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicyBindingSpec + map: + fields: + - name: matchResources + type: + namedType: io.k8s.api.admissionregistration.v1alpha1.MatchResources + - name: paramRef + type: + namedType: io.k8s.api.admissionregistration.v1alpha1.ParamRef + - name: policyName + type: + scalar: string +- name: io.k8s.api.admissionregistration.v1alpha1.MutatingAdmissionPolicySpec + map: + fields: + - name: failurePolicy + type: + scalar: string + - name: matchConditions + type: + list: + elementType: + namedType: io.k8s.api.admissionregistration.v1alpha1.MatchCondition + elementRelationship: associative + keys: + - name + - name: matchConstraints + type: + namedType: io.k8s.api.admissionregistration.v1alpha1.MatchResources + - name: mutations + type: + list: + elementType: + namedType: io.k8s.api.admissionregistration.v1alpha1.Mutation + elementRelationship: atomic + - name: paramKind + type: + namedType: io.k8s.api.admissionregistration.v1alpha1.ParamKind + - name: reinvocationPolicy + type: + scalar: string + - name: variables + type: + list: + elementType: + namedType: io.k8s.api.admissionregistration.v1alpha1.Variable + elementRelationship: atomic +- name: io.k8s.api.admissionregistration.v1alpha1.Mutation + map: + fields: + - name: applyConfiguration + type: + namedType: io.k8s.api.admissionregistration.v1alpha1.ApplyConfiguration + - name: jsonPatch + type: + namedType: io.k8s.api.admissionregistration.v1alpha1.JSONPatch + - name: patchType + type: + scalar: string + default: "" - name: io.k8s.api.admissionregistration.v1alpha1.NamedRuleWithOperations map: fields: diff --git a/staging/src/k8s.io/client-go/applyconfigurations/utils.go b/staging/src/k8s.io/client-go/applyconfigurations/utils.go index 857f5bad8d2..21012912c87 100644 --- a/staging/src/k8s.io/client-go/applyconfigurations/utils.go +++ b/staging/src/k8s.io/client-go/applyconfigurations/utils.go @@ -178,14 +178,28 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &admissionregistrationv1.WebhookClientConfigApplyConfiguration{} // Group=admissionregistration.k8s.io, Version=v1alpha1 + case v1alpha1.SchemeGroupVersion.WithKind("ApplyConfiguration"): + return &admissionregistrationv1alpha1.ApplyConfigurationApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("AuditAnnotation"): return &admissionregistrationv1alpha1.AuditAnnotationApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ExpressionWarning"): return &admissionregistrationv1alpha1.ExpressionWarningApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("JSONPatch"): + return &admissionregistrationv1alpha1.JSONPatchApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("MatchCondition"): return &admissionregistrationv1alpha1.MatchConditionApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("MatchResources"): return &admissionregistrationv1alpha1.MatchResourcesApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("MutatingAdmissionPolicy"): + return &admissionregistrationv1alpha1.MutatingAdmissionPolicyApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("MutatingAdmissionPolicyBinding"): + return &admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("MutatingAdmissionPolicyBindingSpec"): + return &admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("MutatingAdmissionPolicySpec"): + return &admissionregistrationv1alpha1.MutatingAdmissionPolicySpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("Mutation"): + return &admissionregistrationv1alpha1.MutationApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("NamedRuleWithOperations"): return &admissionregistrationv1alpha1.NamedRuleWithOperationsApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ParamKind"): diff --git a/staging/src/k8s.io/client-go/informers/admissionregistration/v1alpha1/interface.go b/staging/src/k8s.io/client-go/informers/admissionregistration/v1alpha1/interface.go index 738063ee72e..68ae4e25cb8 100644 --- a/staging/src/k8s.io/client-go/informers/admissionregistration/v1alpha1/interface.go +++ b/staging/src/k8s.io/client-go/informers/admissionregistration/v1alpha1/interface.go @@ -24,6 +24,10 @@ import ( // Interface provides access to all the informers in this group version. type Interface interface { + // MutatingAdmissionPolicies returns a MutatingAdmissionPolicyInformer. + MutatingAdmissionPolicies() MutatingAdmissionPolicyInformer + // MutatingAdmissionPolicyBindings returns a MutatingAdmissionPolicyBindingInformer. + MutatingAdmissionPolicyBindings() MutatingAdmissionPolicyBindingInformer // ValidatingAdmissionPolicies returns a ValidatingAdmissionPolicyInformer. ValidatingAdmissionPolicies() ValidatingAdmissionPolicyInformer // ValidatingAdmissionPolicyBindings returns a ValidatingAdmissionPolicyBindingInformer. @@ -41,6 +45,16 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } +// MutatingAdmissionPolicies returns a MutatingAdmissionPolicyInformer. +func (v *version) MutatingAdmissionPolicies() MutatingAdmissionPolicyInformer { + return &mutatingAdmissionPolicyInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +} + +// MutatingAdmissionPolicyBindings returns a MutatingAdmissionPolicyBindingInformer. +func (v *version) MutatingAdmissionPolicyBindings() MutatingAdmissionPolicyBindingInformer { + return &mutatingAdmissionPolicyBindingInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +} + // ValidatingAdmissionPolicies returns a ValidatingAdmissionPolicyInformer. func (v *version) ValidatingAdmissionPolicies() ValidatingAdmissionPolicyInformer { return &validatingAdmissionPolicyInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} diff --git a/staging/src/k8s.io/client-go/informers/admissionregistration/v1alpha1/mutatingadmissionpolicy.go b/staging/src/k8s.io/client-go/informers/admissionregistration/v1alpha1/mutatingadmissionpolicy.go new file mode 100644 index 00000000000..5a23158bfa4 --- /dev/null +++ b/staging/src/k8s.io/client-go/informers/admissionregistration/v1alpha1/mutatingadmissionpolicy.go @@ -0,0 +1,89 @@ +/* +Copyright 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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + time "time" + + apiadmissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + internalinterfaces "k8s.io/client-go/informers/internalinterfaces" + kubernetes "k8s.io/client-go/kubernetes" + admissionregistrationv1alpha1 "k8s.io/client-go/listers/admissionregistration/v1alpha1" + cache "k8s.io/client-go/tools/cache" +) + +// MutatingAdmissionPolicyInformer provides access to a shared informer and lister for +// MutatingAdmissionPolicies. +type MutatingAdmissionPolicyInformer interface { + Informer() cache.SharedIndexInformer + Lister() admissionregistrationv1alpha1.MutatingAdmissionPolicyLister +} + +type mutatingAdmissionPolicyInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// NewMutatingAdmissionPolicyInformer constructs a new informer for MutatingAdmissionPolicy type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewMutatingAdmissionPolicyInformer(client kubernetes.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredMutatingAdmissionPolicyInformer(client, resyncPeriod, indexers, nil) +} + +// NewFilteredMutatingAdmissionPolicyInformer constructs a new informer for MutatingAdmissionPolicy type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredMutatingAdmissionPolicyInformer(client kubernetes.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.AdmissionregistrationV1alpha1().MutatingAdmissionPolicies().List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.AdmissionregistrationV1alpha1().MutatingAdmissionPolicies().Watch(context.TODO(), options) + }, + }, + &apiadmissionregistrationv1alpha1.MutatingAdmissionPolicy{}, + resyncPeriod, + indexers, + ) +} + +func (f *mutatingAdmissionPolicyInformer) defaultInformer(client kubernetes.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredMutatingAdmissionPolicyInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *mutatingAdmissionPolicyInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apiadmissionregistrationv1alpha1.MutatingAdmissionPolicy{}, f.defaultInformer) +} + +func (f *mutatingAdmissionPolicyInformer) Lister() admissionregistrationv1alpha1.MutatingAdmissionPolicyLister { + return admissionregistrationv1alpha1.NewMutatingAdmissionPolicyLister(f.Informer().GetIndexer()) +} diff --git a/staging/src/k8s.io/client-go/informers/admissionregistration/v1alpha1/mutatingadmissionpolicybinding.go b/staging/src/k8s.io/client-go/informers/admissionregistration/v1alpha1/mutatingadmissionpolicybinding.go new file mode 100644 index 00000000000..efa143fe555 --- /dev/null +++ b/staging/src/k8s.io/client-go/informers/admissionregistration/v1alpha1/mutatingadmissionpolicybinding.go @@ -0,0 +1,89 @@ +/* +Copyright 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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + time "time" + + apiadmissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + internalinterfaces "k8s.io/client-go/informers/internalinterfaces" + kubernetes "k8s.io/client-go/kubernetes" + admissionregistrationv1alpha1 "k8s.io/client-go/listers/admissionregistration/v1alpha1" + cache "k8s.io/client-go/tools/cache" +) + +// MutatingAdmissionPolicyBindingInformer provides access to a shared informer and lister for +// MutatingAdmissionPolicyBindings. +type MutatingAdmissionPolicyBindingInformer interface { + Informer() cache.SharedIndexInformer + Lister() admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingLister +} + +type mutatingAdmissionPolicyBindingInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// NewMutatingAdmissionPolicyBindingInformer constructs a new informer for MutatingAdmissionPolicyBinding type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewMutatingAdmissionPolicyBindingInformer(client kubernetes.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredMutatingAdmissionPolicyBindingInformer(client, resyncPeriod, indexers, nil) +} + +// NewFilteredMutatingAdmissionPolicyBindingInformer constructs a new informer for MutatingAdmissionPolicyBinding type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredMutatingAdmissionPolicyBindingInformer(client kubernetes.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.AdmissionregistrationV1alpha1().MutatingAdmissionPolicyBindings().List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.AdmissionregistrationV1alpha1().MutatingAdmissionPolicyBindings().Watch(context.TODO(), options) + }, + }, + &apiadmissionregistrationv1alpha1.MutatingAdmissionPolicyBinding{}, + resyncPeriod, + indexers, + ) +} + +func (f *mutatingAdmissionPolicyBindingInformer) defaultInformer(client kubernetes.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredMutatingAdmissionPolicyBindingInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *mutatingAdmissionPolicyBindingInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apiadmissionregistrationv1alpha1.MutatingAdmissionPolicyBinding{}, f.defaultInformer) +} + +func (f *mutatingAdmissionPolicyBindingInformer) Lister() admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingLister { + return admissionregistrationv1alpha1.NewMutatingAdmissionPolicyBindingLister(f.Informer().GetIndexer()) +} diff --git a/staging/src/k8s.io/client-go/informers/generic.go b/staging/src/k8s.io/client-go/informers/generic.go index d548adac5a0..1a5b228c7ff 100644 --- a/staging/src/k8s.io/client-go/informers/generic.go +++ b/staging/src/k8s.io/client-go/informers/generic.go @@ -110,6 +110,10 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Admissionregistration().V1().ValidatingWebhookConfigurations().Informer()}, nil // Group=admissionregistration.k8s.io, Version=v1alpha1 + case v1alpha1.SchemeGroupVersion.WithResource("mutatingadmissionpolicies"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Admissionregistration().V1alpha1().MutatingAdmissionPolicies().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("mutatingadmissionpolicybindings"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Admissionregistration().V1alpha1().MutatingAdmissionPolicyBindings().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("validatingadmissionpolicies"): return &genericInformer{resource: resource.GroupResource(), informer: f.Admissionregistration().V1alpha1().ValidatingAdmissionPolicies().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("validatingadmissionpolicybindings"): diff --git a/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/admissionregistration_client.go b/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/admissionregistration_client.go index 728390d3810..f8a67c6d895 100644 --- a/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/admissionregistration_client.go +++ b/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/admissionregistration_client.go @@ -28,6 +28,8 @@ import ( type AdmissionregistrationV1alpha1Interface interface { RESTClient() rest.Interface + MutatingAdmissionPoliciesGetter + MutatingAdmissionPolicyBindingsGetter ValidatingAdmissionPoliciesGetter ValidatingAdmissionPolicyBindingsGetter } @@ -37,6 +39,14 @@ type AdmissionregistrationV1alpha1Client struct { restClient rest.Interface } +func (c *AdmissionregistrationV1alpha1Client) MutatingAdmissionPolicies() MutatingAdmissionPolicyInterface { + return newMutatingAdmissionPolicies(c) +} + +func (c *AdmissionregistrationV1alpha1Client) MutatingAdmissionPolicyBindings() MutatingAdmissionPolicyBindingInterface { + return newMutatingAdmissionPolicyBindings(c) +} + func (c *AdmissionregistrationV1alpha1Client) ValidatingAdmissionPolicies() ValidatingAdmissionPolicyInterface { return newValidatingAdmissionPolicies(c) } diff --git a/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/fake/fake_admissionregistration_client.go b/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/fake/fake_admissionregistration_client.go index dc0e30ca4d0..19a1b4066ba 100644 --- a/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/fake/fake_admissionregistration_client.go +++ b/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/fake/fake_admissionregistration_client.go @@ -28,6 +28,14 @@ type FakeAdmissionregistrationV1alpha1 struct { *testing.Fake } +func (c *FakeAdmissionregistrationV1alpha1) MutatingAdmissionPolicies() v1alpha1.MutatingAdmissionPolicyInterface { + return &FakeMutatingAdmissionPolicies{c} +} + +func (c *FakeAdmissionregistrationV1alpha1) MutatingAdmissionPolicyBindings() v1alpha1.MutatingAdmissionPolicyBindingInterface { + return &FakeMutatingAdmissionPolicyBindings{c} +} + func (c *FakeAdmissionregistrationV1alpha1) ValidatingAdmissionPolicies() v1alpha1.ValidatingAdmissionPolicyInterface { return &FakeValidatingAdmissionPolicies{c} } diff --git a/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/fake/fake_mutatingadmissionpolicy.go b/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/fake/fake_mutatingadmissionpolicy.go new file mode 100644 index 00000000000..ee71107c02e --- /dev/null +++ b/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/fake/fake_mutatingadmissionpolicy.go @@ -0,0 +1,151 @@ +/* +Copyright 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + context "context" + json "encoding/json" + fmt "fmt" + + v1alpha1 "k8s.io/api/admissionregistration/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + admissionregistrationv1alpha1 "k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1" + testing "k8s.io/client-go/testing" +) + +// FakeMutatingAdmissionPolicies implements MutatingAdmissionPolicyInterface +type FakeMutatingAdmissionPolicies struct { + Fake *FakeAdmissionregistrationV1alpha1 +} + +var mutatingadmissionpoliciesResource = v1alpha1.SchemeGroupVersion.WithResource("mutatingadmissionpolicies") + +var mutatingadmissionpoliciesKind = v1alpha1.SchemeGroupVersion.WithKind("MutatingAdmissionPolicy") + +// Get takes name of the mutatingAdmissionPolicy, and returns the corresponding mutatingAdmissionPolicy object, and an error if there is any. +func (c *FakeMutatingAdmissionPolicies) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.MutatingAdmissionPolicy, err error) { + emptyResult := &v1alpha1.MutatingAdmissionPolicy{} + obj, err := c.Fake. + Invokes(testing.NewRootGetActionWithOptions(mutatingadmissionpoliciesResource, name, options), emptyResult) + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.MutatingAdmissionPolicy), err +} + +// List takes label and field selectors, and returns the list of MutatingAdmissionPolicies that match those selectors. +func (c *FakeMutatingAdmissionPolicies) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.MutatingAdmissionPolicyList, err error) { + emptyResult := &v1alpha1.MutatingAdmissionPolicyList{} + obj, err := c.Fake. + Invokes(testing.NewRootListActionWithOptions(mutatingadmissionpoliciesResource, mutatingadmissionpoliciesKind, opts), emptyResult) + if obj == nil { + return emptyResult, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.MutatingAdmissionPolicyList{ListMeta: obj.(*v1alpha1.MutatingAdmissionPolicyList).ListMeta} + for _, item := range obj.(*v1alpha1.MutatingAdmissionPolicyList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested mutatingAdmissionPolicies. +func (c *FakeMutatingAdmissionPolicies) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewRootWatchActionWithOptions(mutatingadmissionpoliciesResource, opts)) +} + +// Create takes the representation of a mutatingAdmissionPolicy and creates it. Returns the server's representation of the mutatingAdmissionPolicy, and an error, if there is any. +func (c *FakeMutatingAdmissionPolicies) Create(ctx context.Context, mutatingAdmissionPolicy *v1alpha1.MutatingAdmissionPolicy, opts v1.CreateOptions) (result *v1alpha1.MutatingAdmissionPolicy, err error) { + emptyResult := &v1alpha1.MutatingAdmissionPolicy{} + obj, err := c.Fake. + Invokes(testing.NewRootCreateActionWithOptions(mutatingadmissionpoliciesResource, mutatingAdmissionPolicy, opts), emptyResult) + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.MutatingAdmissionPolicy), err +} + +// Update takes the representation of a mutatingAdmissionPolicy and updates it. Returns the server's representation of the mutatingAdmissionPolicy, and an error, if there is any. +func (c *FakeMutatingAdmissionPolicies) Update(ctx context.Context, mutatingAdmissionPolicy *v1alpha1.MutatingAdmissionPolicy, opts v1.UpdateOptions) (result *v1alpha1.MutatingAdmissionPolicy, err error) { + emptyResult := &v1alpha1.MutatingAdmissionPolicy{} + obj, err := c.Fake. + Invokes(testing.NewRootUpdateActionWithOptions(mutatingadmissionpoliciesResource, mutatingAdmissionPolicy, opts), emptyResult) + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.MutatingAdmissionPolicy), err +} + +// Delete takes name of the mutatingAdmissionPolicy and deletes it. Returns an error if one occurs. +func (c *FakeMutatingAdmissionPolicies) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewRootDeleteActionWithOptions(mutatingadmissionpoliciesResource, name, opts), &v1alpha1.MutatingAdmissionPolicy{}) + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeMutatingAdmissionPolicies) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewRootDeleteCollectionActionWithOptions(mutatingadmissionpoliciesResource, opts, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.MutatingAdmissionPolicyList{}) + return err +} + +// Patch applies the patch and returns the patched mutatingAdmissionPolicy. +func (c *FakeMutatingAdmissionPolicies) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.MutatingAdmissionPolicy, err error) { + emptyResult := &v1alpha1.MutatingAdmissionPolicy{} + obj, err := c.Fake. + Invokes(testing.NewRootPatchSubresourceActionWithOptions(mutatingadmissionpoliciesResource, name, pt, data, opts, subresources...), emptyResult) + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.MutatingAdmissionPolicy), err +} + +// Apply takes the given apply declarative configuration, applies it and returns the applied mutatingAdmissionPolicy. +func (c *FakeMutatingAdmissionPolicies) Apply(ctx context.Context, mutatingAdmissionPolicy *admissionregistrationv1alpha1.MutatingAdmissionPolicyApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.MutatingAdmissionPolicy, err error) { + if mutatingAdmissionPolicy == nil { + return nil, fmt.Errorf("mutatingAdmissionPolicy provided to Apply must not be nil") + } + data, err := json.Marshal(mutatingAdmissionPolicy) + if err != nil { + return nil, err + } + name := mutatingAdmissionPolicy.Name + if name == nil { + return nil, fmt.Errorf("mutatingAdmissionPolicy.Name must be provided to Apply") + } + emptyResult := &v1alpha1.MutatingAdmissionPolicy{} + obj, err := c.Fake. + Invokes(testing.NewRootPatchSubresourceActionWithOptions(mutatingadmissionpoliciesResource, *name, types.ApplyPatchType, data, opts.ToPatchOptions()), emptyResult) + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.MutatingAdmissionPolicy), err +} diff --git a/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/fake/fake_mutatingadmissionpolicybinding.go b/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/fake/fake_mutatingadmissionpolicybinding.go new file mode 100644 index 00000000000..027ca68a8bd --- /dev/null +++ b/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/fake/fake_mutatingadmissionpolicybinding.go @@ -0,0 +1,151 @@ +/* +Copyright 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + context "context" + json "encoding/json" + fmt "fmt" + + v1alpha1 "k8s.io/api/admissionregistration/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + admissionregistrationv1alpha1 "k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1" + testing "k8s.io/client-go/testing" +) + +// FakeMutatingAdmissionPolicyBindings implements MutatingAdmissionPolicyBindingInterface +type FakeMutatingAdmissionPolicyBindings struct { + Fake *FakeAdmissionregistrationV1alpha1 +} + +var mutatingadmissionpolicybindingsResource = v1alpha1.SchemeGroupVersion.WithResource("mutatingadmissionpolicybindings") + +var mutatingadmissionpolicybindingsKind = v1alpha1.SchemeGroupVersion.WithKind("MutatingAdmissionPolicyBinding") + +// Get takes name of the mutatingAdmissionPolicyBinding, and returns the corresponding mutatingAdmissionPolicyBinding object, and an error if there is any. +func (c *FakeMutatingAdmissionPolicyBindings) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.MutatingAdmissionPolicyBinding, err error) { + emptyResult := &v1alpha1.MutatingAdmissionPolicyBinding{} + obj, err := c.Fake. + Invokes(testing.NewRootGetActionWithOptions(mutatingadmissionpolicybindingsResource, name, options), emptyResult) + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.MutatingAdmissionPolicyBinding), err +} + +// List takes label and field selectors, and returns the list of MutatingAdmissionPolicyBindings that match those selectors. +func (c *FakeMutatingAdmissionPolicyBindings) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.MutatingAdmissionPolicyBindingList, err error) { + emptyResult := &v1alpha1.MutatingAdmissionPolicyBindingList{} + obj, err := c.Fake. + Invokes(testing.NewRootListActionWithOptions(mutatingadmissionpolicybindingsResource, mutatingadmissionpolicybindingsKind, opts), emptyResult) + if obj == nil { + return emptyResult, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.MutatingAdmissionPolicyBindingList{ListMeta: obj.(*v1alpha1.MutatingAdmissionPolicyBindingList).ListMeta} + for _, item := range obj.(*v1alpha1.MutatingAdmissionPolicyBindingList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested mutatingAdmissionPolicyBindings. +func (c *FakeMutatingAdmissionPolicyBindings) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewRootWatchActionWithOptions(mutatingadmissionpolicybindingsResource, opts)) +} + +// Create takes the representation of a mutatingAdmissionPolicyBinding and creates it. Returns the server's representation of the mutatingAdmissionPolicyBinding, and an error, if there is any. +func (c *FakeMutatingAdmissionPolicyBindings) Create(ctx context.Context, mutatingAdmissionPolicyBinding *v1alpha1.MutatingAdmissionPolicyBinding, opts v1.CreateOptions) (result *v1alpha1.MutatingAdmissionPolicyBinding, err error) { + emptyResult := &v1alpha1.MutatingAdmissionPolicyBinding{} + obj, err := c.Fake. + Invokes(testing.NewRootCreateActionWithOptions(mutatingadmissionpolicybindingsResource, mutatingAdmissionPolicyBinding, opts), emptyResult) + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.MutatingAdmissionPolicyBinding), err +} + +// Update takes the representation of a mutatingAdmissionPolicyBinding and updates it. Returns the server's representation of the mutatingAdmissionPolicyBinding, and an error, if there is any. +func (c *FakeMutatingAdmissionPolicyBindings) Update(ctx context.Context, mutatingAdmissionPolicyBinding *v1alpha1.MutatingAdmissionPolicyBinding, opts v1.UpdateOptions) (result *v1alpha1.MutatingAdmissionPolicyBinding, err error) { + emptyResult := &v1alpha1.MutatingAdmissionPolicyBinding{} + obj, err := c.Fake. + Invokes(testing.NewRootUpdateActionWithOptions(mutatingadmissionpolicybindingsResource, mutatingAdmissionPolicyBinding, opts), emptyResult) + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.MutatingAdmissionPolicyBinding), err +} + +// Delete takes name of the mutatingAdmissionPolicyBinding and deletes it. Returns an error if one occurs. +func (c *FakeMutatingAdmissionPolicyBindings) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewRootDeleteActionWithOptions(mutatingadmissionpolicybindingsResource, name, opts), &v1alpha1.MutatingAdmissionPolicyBinding{}) + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeMutatingAdmissionPolicyBindings) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewRootDeleteCollectionActionWithOptions(mutatingadmissionpolicybindingsResource, opts, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.MutatingAdmissionPolicyBindingList{}) + return err +} + +// Patch applies the patch and returns the patched mutatingAdmissionPolicyBinding. +func (c *FakeMutatingAdmissionPolicyBindings) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.MutatingAdmissionPolicyBinding, err error) { + emptyResult := &v1alpha1.MutatingAdmissionPolicyBinding{} + obj, err := c.Fake. + Invokes(testing.NewRootPatchSubresourceActionWithOptions(mutatingadmissionpolicybindingsResource, name, pt, data, opts, subresources...), emptyResult) + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.MutatingAdmissionPolicyBinding), err +} + +// Apply takes the given apply declarative configuration, applies it and returns the applied mutatingAdmissionPolicyBinding. +func (c *FakeMutatingAdmissionPolicyBindings) Apply(ctx context.Context, mutatingAdmissionPolicyBinding *admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.MutatingAdmissionPolicyBinding, err error) { + if mutatingAdmissionPolicyBinding == nil { + return nil, fmt.Errorf("mutatingAdmissionPolicyBinding provided to Apply must not be nil") + } + data, err := json.Marshal(mutatingAdmissionPolicyBinding) + if err != nil { + return nil, err + } + name := mutatingAdmissionPolicyBinding.Name + if name == nil { + return nil, fmt.Errorf("mutatingAdmissionPolicyBinding.Name must be provided to Apply") + } + emptyResult := &v1alpha1.MutatingAdmissionPolicyBinding{} + obj, err := c.Fake. + Invokes(testing.NewRootPatchSubresourceActionWithOptions(mutatingadmissionpolicybindingsResource, *name, types.ApplyPatchType, data, opts.ToPatchOptions()), emptyResult) + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.MutatingAdmissionPolicyBinding), err +} diff --git a/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/generated_expansion.go b/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/generated_expansion.go index 94562da5943..676578c631f 100644 --- a/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/generated_expansion.go +++ b/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/generated_expansion.go @@ -18,6 +18,10 @@ limitations under the License. package v1alpha1 +type MutatingAdmissionPolicyExpansion interface{} + +type MutatingAdmissionPolicyBindingExpansion interface{} + type ValidatingAdmissionPolicyExpansion interface{} type ValidatingAdmissionPolicyBindingExpansion interface{} diff --git a/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/mutatingadmissionpolicy.go b/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/mutatingadmissionpolicy.go new file mode 100644 index 00000000000..4a781a60288 --- /dev/null +++ b/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/mutatingadmissionpolicy.go @@ -0,0 +1,75 @@ +/* +Copyright 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + applyconfigurationsadmissionregistrationv1alpha1 "k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1" + gentype "k8s.io/client-go/gentype" + scheme "k8s.io/client-go/kubernetes/scheme" +) + +// MutatingAdmissionPoliciesGetter has a method to return a MutatingAdmissionPolicyInterface. +// A group's client should implement this interface. +type MutatingAdmissionPoliciesGetter interface { + MutatingAdmissionPolicies() MutatingAdmissionPolicyInterface +} + +// MutatingAdmissionPolicyInterface has methods to work with MutatingAdmissionPolicy resources. +type MutatingAdmissionPolicyInterface interface { + Create(ctx context.Context, mutatingAdmissionPolicy *admissionregistrationv1alpha1.MutatingAdmissionPolicy, opts v1.CreateOptions) (*admissionregistrationv1alpha1.MutatingAdmissionPolicy, error) + Update(ctx context.Context, mutatingAdmissionPolicy *admissionregistrationv1alpha1.MutatingAdmissionPolicy, opts v1.UpdateOptions) (*admissionregistrationv1alpha1.MutatingAdmissionPolicy, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*admissionregistrationv1alpha1.MutatingAdmissionPolicy, error) + List(ctx context.Context, opts v1.ListOptions) (*admissionregistrationv1alpha1.MutatingAdmissionPolicyList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *admissionregistrationv1alpha1.MutatingAdmissionPolicy, err error) + Apply(ctx context.Context, mutatingAdmissionPolicy *applyconfigurationsadmissionregistrationv1alpha1.MutatingAdmissionPolicyApplyConfiguration, opts v1.ApplyOptions) (result *admissionregistrationv1alpha1.MutatingAdmissionPolicy, err error) + MutatingAdmissionPolicyExpansion +} + +// mutatingAdmissionPolicies implements MutatingAdmissionPolicyInterface +type mutatingAdmissionPolicies struct { + *gentype.ClientWithListAndApply[*admissionregistrationv1alpha1.MutatingAdmissionPolicy, *admissionregistrationv1alpha1.MutatingAdmissionPolicyList, *applyconfigurationsadmissionregistrationv1alpha1.MutatingAdmissionPolicyApplyConfiguration] +} + +// newMutatingAdmissionPolicies returns a MutatingAdmissionPolicies +func newMutatingAdmissionPolicies(c *AdmissionregistrationV1alpha1Client) *mutatingAdmissionPolicies { + return &mutatingAdmissionPolicies{ + gentype.NewClientWithListAndApply[*admissionregistrationv1alpha1.MutatingAdmissionPolicy, *admissionregistrationv1alpha1.MutatingAdmissionPolicyList, *applyconfigurationsadmissionregistrationv1alpha1.MutatingAdmissionPolicyApplyConfiguration]( + "mutatingadmissionpolicies", + c.RESTClient(), + scheme.ParameterCodec, + "", + func() *admissionregistrationv1alpha1.MutatingAdmissionPolicy { + return &admissionregistrationv1alpha1.MutatingAdmissionPolicy{} + }, + func() *admissionregistrationv1alpha1.MutatingAdmissionPolicyList { + return &admissionregistrationv1alpha1.MutatingAdmissionPolicyList{} + }, + gentype.PrefersProtobuf[*admissionregistrationv1alpha1.MutatingAdmissionPolicy](), + ), + } +} diff --git a/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/mutatingadmissionpolicybinding.go b/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/mutatingadmissionpolicybinding.go new file mode 100644 index 00000000000..78057e2001a --- /dev/null +++ b/staging/src/k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1/mutatingadmissionpolicybinding.go @@ -0,0 +1,75 @@ +/* +Copyright 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + applyconfigurationsadmissionregistrationv1alpha1 "k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1" + gentype "k8s.io/client-go/gentype" + scheme "k8s.io/client-go/kubernetes/scheme" +) + +// MutatingAdmissionPolicyBindingsGetter has a method to return a MutatingAdmissionPolicyBindingInterface. +// A group's client should implement this interface. +type MutatingAdmissionPolicyBindingsGetter interface { + MutatingAdmissionPolicyBindings() MutatingAdmissionPolicyBindingInterface +} + +// MutatingAdmissionPolicyBindingInterface has methods to work with MutatingAdmissionPolicyBinding resources. +type MutatingAdmissionPolicyBindingInterface interface { + Create(ctx context.Context, mutatingAdmissionPolicyBinding *admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding, opts v1.CreateOptions) (*admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding, error) + Update(ctx context.Context, mutatingAdmissionPolicyBinding *admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding, opts v1.UpdateOptions) (*admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding, error) + List(ctx context.Context, opts v1.ListOptions) (*admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding, err error) + Apply(ctx context.Context, mutatingAdmissionPolicyBinding *applyconfigurationsadmissionregistrationv1alpha1.MutatingAdmissionPolicyBindingApplyConfiguration, opts v1.ApplyOptions) (result *admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding, err error) + MutatingAdmissionPolicyBindingExpansion +} + +// mutatingAdmissionPolicyBindings implements MutatingAdmissionPolicyBindingInterface +type mutatingAdmissionPolicyBindings struct { + *gentype.ClientWithListAndApply[*admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding, *admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingList, *applyconfigurationsadmissionregistrationv1alpha1.MutatingAdmissionPolicyBindingApplyConfiguration] +} + +// newMutatingAdmissionPolicyBindings returns a MutatingAdmissionPolicyBindings +func newMutatingAdmissionPolicyBindings(c *AdmissionregistrationV1alpha1Client) *mutatingAdmissionPolicyBindings { + return &mutatingAdmissionPolicyBindings{ + gentype.NewClientWithListAndApply[*admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding, *admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingList, *applyconfigurationsadmissionregistrationv1alpha1.MutatingAdmissionPolicyBindingApplyConfiguration]( + "mutatingadmissionpolicybindings", + c.RESTClient(), + scheme.ParameterCodec, + "", + func() *admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding { + return &admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding{} + }, + func() *admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingList { + return &admissionregistrationv1alpha1.MutatingAdmissionPolicyBindingList{} + }, + gentype.PrefersProtobuf[*admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding](), + ), + } +} diff --git a/staging/src/k8s.io/client-go/listers/admissionregistration/v1alpha1/expansion_generated.go b/staging/src/k8s.io/client-go/listers/admissionregistration/v1alpha1/expansion_generated.go index 3f8b7819ce0..701784de00d 100644 --- a/staging/src/k8s.io/client-go/listers/admissionregistration/v1alpha1/expansion_generated.go +++ b/staging/src/k8s.io/client-go/listers/admissionregistration/v1alpha1/expansion_generated.go @@ -18,6 +18,14 @@ limitations under the License. package v1alpha1 +// MutatingAdmissionPolicyListerExpansion allows custom methods to be added to +// MutatingAdmissionPolicyLister. +type MutatingAdmissionPolicyListerExpansion interface{} + +// MutatingAdmissionPolicyBindingListerExpansion allows custom methods to be added to +// MutatingAdmissionPolicyBindingLister. +type MutatingAdmissionPolicyBindingListerExpansion interface{} + // ValidatingAdmissionPolicyListerExpansion allows custom methods to be added to // ValidatingAdmissionPolicyLister. type ValidatingAdmissionPolicyListerExpansion interface{} diff --git a/staging/src/k8s.io/client-go/listers/admissionregistration/v1alpha1/mutatingadmissionpolicy.go b/staging/src/k8s.io/client-go/listers/admissionregistration/v1alpha1/mutatingadmissionpolicy.go new file mode 100644 index 00000000000..debeb79d244 --- /dev/null +++ b/staging/src/k8s.io/client-go/listers/admissionregistration/v1alpha1/mutatingadmissionpolicy.go @@ -0,0 +1,48 @@ +/* +Copyright 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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// MutatingAdmissionPolicyLister helps list MutatingAdmissionPolicies. +// All objects returned here must be treated as read-only. +type MutatingAdmissionPolicyLister interface { + // List lists all MutatingAdmissionPolicies in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*admissionregistrationv1alpha1.MutatingAdmissionPolicy, err error) + // Get retrieves the MutatingAdmissionPolicy from the index for a given name. + // Objects returned here must be treated as read-only. + Get(name string) (*admissionregistrationv1alpha1.MutatingAdmissionPolicy, error) + MutatingAdmissionPolicyListerExpansion +} + +// mutatingAdmissionPolicyLister implements the MutatingAdmissionPolicyLister interface. +type mutatingAdmissionPolicyLister struct { + listers.ResourceIndexer[*admissionregistrationv1alpha1.MutatingAdmissionPolicy] +} + +// NewMutatingAdmissionPolicyLister returns a new MutatingAdmissionPolicyLister. +func NewMutatingAdmissionPolicyLister(indexer cache.Indexer) MutatingAdmissionPolicyLister { + return &mutatingAdmissionPolicyLister{listers.New[*admissionregistrationv1alpha1.MutatingAdmissionPolicy](indexer, admissionregistrationv1alpha1.Resource("mutatingadmissionpolicy"))} +} diff --git a/staging/src/k8s.io/client-go/listers/admissionregistration/v1alpha1/mutatingadmissionpolicybinding.go b/staging/src/k8s.io/client-go/listers/admissionregistration/v1alpha1/mutatingadmissionpolicybinding.go new file mode 100644 index 00000000000..bcad2904808 --- /dev/null +++ b/staging/src/k8s.io/client-go/listers/admissionregistration/v1alpha1/mutatingadmissionpolicybinding.go @@ -0,0 +1,48 @@ +/* +Copyright 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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// MutatingAdmissionPolicyBindingLister helps list MutatingAdmissionPolicyBindings. +// All objects returned here must be treated as read-only. +type MutatingAdmissionPolicyBindingLister interface { + // List lists all MutatingAdmissionPolicyBindings in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding, err error) + // Get retrieves the MutatingAdmissionPolicyBinding from the index for a given name. + // Objects returned here must be treated as read-only. + Get(name string) (*admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding, error) + MutatingAdmissionPolicyBindingListerExpansion +} + +// mutatingAdmissionPolicyBindingLister implements the MutatingAdmissionPolicyBindingLister interface. +type mutatingAdmissionPolicyBindingLister struct { + listers.ResourceIndexer[*admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding] +} + +// NewMutatingAdmissionPolicyBindingLister returns a new MutatingAdmissionPolicyBindingLister. +func NewMutatingAdmissionPolicyBindingLister(indexer cache.Indexer) MutatingAdmissionPolicyBindingLister { + return &mutatingAdmissionPolicyBindingLister{listers.New[*admissionregistrationv1alpha1.MutatingAdmissionPolicyBinding](indexer, admissionregistrationv1alpha1.Resource("mutatingadmissionpolicybinding"))} +} From 081353bf8ad963d43c5da6714a24f62cfe0b8401 Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Fri, 25 Oct 2024 14:37:17 -0400 Subject: [PATCH 06/16] Add mutation support into CompositedCompiler and reorganize for clarity --- .../pkg/admission/plugin/cel/activation.go | 190 +++++++++ .../pkg/admission/plugin/cel/compile.go | 110 ++++-- .../pkg/admission/plugin/cel/composition.go | 51 ++- .../admission/plugin/cel/composition_test.go | 4 +- .../pkg/admission/plugin/cel/condition.go | 216 +++++++++++ .../cel/{filter_test.go => condition_test.go} | 199 +++++----- .../pkg/admission/plugin/cel/filter.go | 361 ------------------ .../pkg/admission/plugin/cel/interface.go | 41 +- .../pkg/admission/plugin/cel/mutation.go | 73 ++++ .../pkg/admission/plugin/webhook/accessors.go | 10 +- .../plugin/webhook/generic/webhook.go | 5 +- .../plugin/webhook/generic/webhook_test.go | 2 +- .../plugin/webhook/matchconditions/matcher.go | 4 +- .../webhook/matchconditions/matcher_test.go | 2 +- 14 files changed, 734 insertions(+), 534 deletions(-) create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/activation.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/condition.go rename staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/{filter_test.go => condition_test.go} (93%) delete mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/mutation.go diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/activation.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/activation.go new file mode 100644 index 00000000000..9771ae9ae4b --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/activation.go @@ -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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go index 06035f6b9e9..8088c332566 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go @@ -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 + }, +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition.go index 9c449ecda2f..bf8715a1442 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition.go @@ -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) { diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition_test.go index 53d9b861284..13a373faabf 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition_test.go @@ -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) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/condition.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/condition.go new file mode 100644 index 00000000000..f28401f3e1b --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/condition.go @@ -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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/condition_test.go similarity index 93% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter_test.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/condition_test.go index f6e6f8fcf91..1684aa3cc65 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/condition_test.go @@ -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() diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter.go deleted file mode 100644 index 216a474d23e..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter.go +++ /dev/null @@ -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 -} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/interface.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/interface.go index ae61dc826c4..a9e35a226f1 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/interface.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/interface.go @@ -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 } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/mutation.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/mutation.go new file mode 100644 index 00000000000..8c609b94410 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/mutation.go @@ -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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/accessors.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/accessors.go index f23580cc09f..7ae29d5bb44 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/accessors.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/accessors.go @@ -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, diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go index 9b6ac5a8199..8db7d3ced94 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go @@ -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 } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook_test.go index 6c12323862d..f9780108ecf 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook_test.go @@ -76,7 +76,7 @@ type fakeWebhookAccessor struct { matchResult bool } -func (f *fakeWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher { +func (f *fakeWebhookAccessor) GetCompiledMatcher(compiler cel.ConditionCompiler) matchconditions.Matcher { return &fakeMatcher{ throwError: f.throwError, matchResult: f.matchResult, diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher.go index 21dd28f6c24..b9b4c9169e8 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher.go @@ -54,14 +54,14 @@ var _ Matcher = &matcher{} // matcher evaluates compiled cel expressions and determines if they match the given request or not type matcher struct { - filter celplugin.Filter + filter celplugin.ConditionEvaluator failPolicy v1.FailurePolicyType matcherType string matcherKind string objectName string } -func NewMatcher(filter celplugin.Filter, failPolicy *v1.FailurePolicyType, matcherKind, matcherType, objectName string) Matcher { +func NewMatcher(filter celplugin.ConditionEvaluator, failPolicy *v1.FailurePolicyType, matcherKind, matcherType, objectName string) Matcher { var f v1.FailurePolicyType if failPolicy == nil { f = v1.Fail diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher_test.go index 20d9aebfb7d..153ebb3b5af 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher_test.go @@ -35,7 +35,7 @@ import ( "k8s.io/apiserver/pkg/admission/plugin/cel" ) -var _ cel.Filter = &fakeCelFilter{} +var _ cel.ConditionEvaluator = &fakeCelFilter{} type fakeCelFilter struct { evaluations []cel.EvaluationResult From 25e11cd1c143ef136418c33bfbbbd4f24e32e529 Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Fri, 25 Oct 2024 13:46:58 -0400 Subject: [PATCH 07/16] Add MutatingAdmissionPolicy plugin to admission chain This expands the generic plugin support to both validating and mutating policies. It also adds the mutating policy admission plugin using the generics plugin support. This also implements both ApplyConfiguration and JSONPatch support. Co-authored-by: Alexander Zielensk --- pkg/kubeapiserver/options/plugins.go | 4 + pkg/kubeapiserver/options/plugins_test.go | 4 +- .../plugin/policy/generic/accessor.go | 1 + .../plugin/policy/generic/interfaces.go | 3 + .../admission/plugin/policy/generic/plugin.go | 12 +- .../policy/generic/policy_dispatcher.go | 157 ++- .../policy/generic/policy_source_test.go | 18 +- .../plugin/policy/mutating/accessor.go | 144 +++ .../plugin/policy/mutating/compilation.go | 81 ++ .../policy/mutating/compilation_test.go | 1045 +++++++++++++++++ .../plugin/policy/mutating/dispatcher.go | 284 +++++ .../plugin/policy/mutating/interface.go | 60 + .../plugin/policy/mutating/patch/interface.go | 43 + .../policy/mutating/patch/json_patch.go | 173 +++ .../plugin/policy/mutating/patch/smd.go | 194 +++ .../policy/mutating/patch/typeconverter.go | 187 +++ .../plugin/policy/mutating/plugin.go | 151 +++ .../plugin/policy/mutating/plugin_test.go | 292 +++++ .../policy/mutating/reinvocationcontext.go | 76 ++ .../plugin/policy/validating/accessor.go | 4 + .../policy/validating/admission_test.go | 3 +- .../plugin/policy/validating/dispatcher.go | 7 +- .../plugin/policy/validating/plugin.go | 10 +- .../plugin/policy/validating/typechecking.go | 2 +- .../apiserver/pkg/server/options/admission.go | 3 +- .../pkg/server/options/admission_test.go | 2 +- .../k8s.io/apiserver/pkg/server/plugins.go | 2 + 27 files changed, 2899 insertions(+), 63 deletions(-) create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/accessor.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/compilation.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/compilation_test.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/dispatcher.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/interface.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/interface.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/smd.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/typeconverter.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin_test.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/reinvocationcontext.go diff --git a/pkg/kubeapiserver/options/plugins.go b/pkg/kubeapiserver/options/plugins.go index 7f0bc9ae23c..e9f8c41b814 100644 --- a/pkg/kubeapiserver/options/plugins.go +++ b/pkg/kubeapiserver/options/plugins.go @@ -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 ) diff --git a/pkg/kubeapiserver/options/plugins_test.go b/pkg/kubeapiserver/options/plugins_test.go index 81784dd75c4..7a3140086ff 100644 --- a/pkg/kubeapiserver/options/plugins_test.go +++ b/pkg/kubeapiserver/options/plugins_test.go @@ -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) } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/accessor.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/accessor.go index 85b18612f87..515634f0062 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/accessor.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/accessor.go @@ -26,6 +26,7 @@ type PolicyAccessor interface { GetNamespace() string GetParamKind() *v1.ParamKind GetMatchConstraints() *v1.MatchResources + GetFailurePolicy() *v1.FailurePolicyType } type BindingAccessor interface { diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/interfaces.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/interfaces.go index d4dbfb0aa52..58f4a374dca 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/interfaces.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/interfaces.go @@ -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 { + // Run the dispatcher. This method should be called only once at startup. + Run(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 diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/plugin.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/plugin.go index ed1c621bc8e..fa1f851892b 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/plugin.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/plugin.go @@ -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.Run(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() }) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_dispatcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_dispatcher.go index 62ed7bc6c69..6586f486b94 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_dispatcher.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_dispatcher.go @@ -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]) Run(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 '%s' denied request: %s", c.Policy.GetName(), c.Message.Error()) +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_source_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_source_test.go index 218b0861100..6f95ae25715 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_source_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_source_test.go @@ -17,6 +17,7 @@ limitations under the License. package generic_test import ( + "context" "testing" "github.com/stretchr/testify/require" @@ -24,15 +25,26 @@ import ( 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) Run(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 +219,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 } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/accessor.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/accessor.go new file mode 100644 index 00000000000..e5ef242fa37 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/accessor.go @@ -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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/compilation.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/compilation.go new file mode 100644 index 00000000000..54d5938dd62 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/compilation.go @@ -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 := &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 := &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} +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/compilation_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/compilation_test.go new file mode 100644 index 00000000000..c9de4a343eb --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/compilation_test.go @@ -0,0 +1,1045 @@ +/* +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" + + "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" + "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/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"} + 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: "jsonPatch with false test operation", + policy: jsonPatches(policy("d1"), + v1alpha1.JSONPatch{ + 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", + policy: jsonPatches(policy("d1"), + v1alpha1.JSONPatch{ + 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", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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 + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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 + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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 replace end of list with - not allowed", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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", + }, + { + name: "jsonPatch replace 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: "jsonPatch with CEL initializer", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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 add map entry by key and value", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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", + policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ + 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: "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: "apply configuration add to listType=map", + policy: applyConfigurations(policy("d1"), + `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", + policy: applyConfigurations(policy("d1"), + `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", + policy: applyConfigurations(policy("d1"), ` + 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", + policy: applyConfigurations(policy("d1"), + `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: "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: "complex apply configuration initialization", + policy: applyConfigurations(policy("d1"), + `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 invalid type name", + policy: applyConfigurations(policy("d1"), + `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", + policy: applyConfigurations(policy("d1"), + `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", + policy: applyConfigurations(policy("d1"), + `"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", + policy: applyConfigurations(policy("d1"), + `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", + }, + { + 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: "apply configuration with change to atomic", + policy: applyConfigurations(policy("d1"), + `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: "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()) + defer cancel() + tcManager := patch.NewTypeConverterManager(nil, openapitest.NewEmbeddedFileClient()) + go tcManager.Run(ctx) + + 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.GetName(), metaAccessor.GetNamespace(), 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 +} + +type fakeAuthorizer struct{} + +func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { + return authorizer.DecisionAllow, "", nil +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/dispatcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/dispatcher.go new file mode 100644 index 00000000000..0d93661c863 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/dispatcher.go @@ -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, + //!TODO: pass in static type converter to reduce network calls + 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) Run(ctx context.Context) error { + go d.typeConverterManager.Run(ctx) + return d.Dispatcher.Run(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", + invocation.Policy.Name, len(invocation.Policy.Spec.Mutations), 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) + } + + // 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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/interface.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/interface.go new file mode 100644 index 00000000000..bf196461b0f --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/interface.go @@ -0,0 +1,60 @@ +/* +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 ( + celgo "github.com/google/cel-go/cel" + celtypes "github.com/google/cel-go/common/types" + + "k8s.io/apiserver/pkg/admission/plugin/cel" +) + +var _ cel.ExpressionAccessor = &ApplyConfigurationCondition{} + +// ApplyConfigurationCondition contains the inputs needed to compile and evaluate a cel expression +// that returns an apply configuration +type ApplyConfigurationCondition struct { + Expression string +} + +func (v *ApplyConfigurationCondition) GetExpression() string { + return v.Expression +} + +func (v *ApplyConfigurationCondition) ReturnTypes() []*celgo.Type { + return []*celgo.Type{applyConfigObjectType} +} + +var applyConfigObjectType = celtypes.NewObjectType("Object") + +var _ cel.ExpressionAccessor = &JSONPatchCondition{} + +// JSONPatchCondition contains the inputs needed to compile and evaluate a cel expression +// that returns a JSON patch value. +type JSONPatchCondition struct { + Expression string +} + +func (v *JSONPatchCondition) GetExpression() string { + return v.Expression +} + +func (v *JSONPatchCondition) ReturnTypes() []*celgo.Type { + return []*celgo.Type{celgo.ListType(jsonPatchType)} +} + +var jsonPatchType = celtypes.NewObjectType("JSONPatch") diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/interface.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/interface.go new file mode 100644 index 00000000000..d00d9837316 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/interface.go @@ -0,0 +1,43 @@ +/* +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(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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch.go new file mode 100644 index 00000000000..b5cf919ef16 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch.go @@ -0,0 +1,173 @@ +/* +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" + "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" +) + +// 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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/smd.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/smd.go new file mode 100644 index 00000000000..d0e5ca1fa05 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/smd.go @@ -0,0 +1,194 @@ +/* +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" + "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" +) + +// 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. +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() +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/typeconverter.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/typeconverter.go new file mode 100644 index 00000000000..96ca7f03724 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/typeconverter.go @@ -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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin.go new file mode 100644 index 00000000000..fa84539efe3 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin.go @@ -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 + }) +} + +// Plugin is an implementation of admission.Interface. +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 +} + +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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin_test.go new file mode 100644 index 00000000000..884e7dec1e9 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin_test.go @@ -0,0 +1,292 @@ +/* +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 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) +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/reinvocationcontext.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/reinvocationcontext.go new file mode 100644 index 00000000000..4ba030c283d --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/reinvocationcontext.go @@ -0,0 +1,76 @@ +/* +Copyright 2019 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]() + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/accessor.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/accessor.go index 97cef091480..628e3a65329 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/accessor.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/accessor.go @@ -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 } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/admission_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/admission_test.go index 0b542e540fa..14f33b17594 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/admission_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/admission_test.go @@ -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) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/dispatcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/dispatcher.go index f0601142530..5d47c94a25d 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/dispatcher.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/dispatcher.go @@ -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) Run(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 diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/plugin.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/plugin.go index 1621f368e03..85db23cd8a6 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/plugin.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/plugin.go @@ -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, ) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking.go index 192be9621bd..aa5a0f29407 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking.go @@ -436,7 +436,7 @@ func buildEnvSet(hasParams bool, hasAuthorizer bool, types typeOverwrite) (*envi ) } -// createVariableOpts creates a slice of EnvOption +// createVariableOpts creates a slice of ResolverEnvOption // that can be used for creating a CEL env containing variables of declType. // declType can be nil, in which case the variables will be of DynType. func createVariableOpts(declType *apiservercel.DeclType, variables ...string) []cel.EnvOption { diff --git a/staging/src/k8s.io/apiserver/pkg/server/options/admission.go b/staging/src/k8s.io/apiserver/pkg/server/options/admission.go index 0a49cdc64dd..6b4669e4506 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/options/admission.go +++ b/staging/src/k8s.io/apiserver/pkg/server/options/admission.go @@ -31,6 +31,7 @@ import ( "k8s.io/apiserver/pkg/admission/initializer" admissionmetrics "k8s.io/apiserver/pkg/admission/metrics" "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" mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating" validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating" @@ -90,7 +91,7 @@ func NewAdmissionOptions() *AdmissionOptions { // admission plugins. The apiserver always runs the validating ones // after all the mutating ones, so their relative order in this list // doesn't matter. - RecommendedPluginOrder: []string{lifecycle.PluginName, mutatingwebhook.PluginName, validatingadmissionpolicy.PluginName, validatingwebhook.PluginName}, + RecommendedPluginOrder: []string{lifecycle.PluginName, mutatingadmissionpolicy.PluginName, mutatingwebhook.PluginName, validatingadmissionpolicy.PluginName, validatingwebhook.PluginName}, DefaultOffPlugins: sets.Set[string]{}, } server.RegisterAllAdmissionPlugins(options.Plugins) diff --git a/staging/src/k8s.io/apiserver/pkg/server/options/admission_test.go b/staging/src/k8s.io/apiserver/pkg/server/options/admission_test.go index 3a4255e4355..50b39544c91 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/options/admission_test.go +++ b/staging/src/k8s.io/apiserver/pkg/server/options/admission_test.go @@ -36,7 +36,7 @@ func TestEnabledPluginNames(t *testing.T) { }{ // scenario 0: check if a call to enabledPluginNames sets expected values. { - expectedPluginNames: []string{"NamespaceLifecycle", "MutatingAdmissionWebhook", "ValidatingAdmissionPolicy", "ValidatingAdmissionWebhook"}, + expectedPluginNames: []string{"NamespaceLifecycle", "MutatingAdmissionPolicy", "MutatingAdmissionWebhook", "ValidatingAdmissionPolicy", "ValidatingAdmissionWebhook"}, }, // scenario 1: use default off plugins if no specified diff --git a/staging/src/k8s.io/apiserver/pkg/server/plugins.go b/staging/src/k8s.io/apiserver/pkg/server/plugins.go index 2390446419c..37a49af6b18 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/plugins.go +++ b/staging/src/k8s.io/apiserver/pkg/server/plugins.go @@ -20,6 +20,7 @@ package server import ( "k8s.io/apiserver/pkg/admission" "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" mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating" validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating" @@ -31,4 +32,5 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) { validatingwebhook.Register(plugins) mutatingwebhook.Register(plugins) validatingadmissionpolicy.Register(plugins) + mutatingadmissionpolicy.Register(plugins) } From 712cc20996aedad5ac525184d76ea581f98d6e35 Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Fri, 25 Oct 2024 13:50:25 -0400 Subject: [PATCH 08/16] Add jsonpatch.escapeKey CEL function --- .../k8s.io/apiserver/pkg/cel/library/cost.go | 4 +- .../apiserver/pkg/cel/library/cost_test.go | 7 ++ .../apiserver/pkg/cel/library/jsonpatch.go | 89 +++++++++++++++++++ .../apiserver/pkg/cel/library/libraries.go | 1 + .../cel/library/library_compatibility_test.go | 2 + 5 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 staging/src/k8s.io/apiserver/pkg/cel/library/jsonpatch.go diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/cost.go b/staging/src/k8s.io/apiserver/pkg/cel/library/cost.go index 14b74dc6b02..500fd8b0986 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/cost.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/cost.go @@ -97,7 +97,7 @@ func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, re cost += traversalCost(args[0]) // these O(n) operations all cost roughly the cost of a single traversal } return &cost - case "url", "lowerAscii", "upperAscii", "substring", "trim": + case "url", "lowerAscii", "upperAscii", "substring", "trim", "jsonpatch.escapeKey": if len(args) >= 1 { cost := uint64(math.Ceil(float64(actualSize(args[0])) * common.StringTraversalCostFactor)) return &cost @@ -294,7 +294,7 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch return &checker.CallEstimate{CostEstimate: l.sizeEstimate(*target).MultiplyByCostFactor(common.StringTraversalCostFactor)} } } - case "url": + case "url", "jsonpatch.escapeKey": if len(args) == 1 { sz := l.sizeEstimate(args[0]) return &checker.CallEstimate{CostEstimate: sz.MultiplyByCostFactor(common.StringTraversalCostFactor), ResultSize: &sz} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/cost_test.go b/staging/src/k8s.io/apiserver/pkg/cel/library/cost_test.go index 4da1934f66d..a818c795570 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/cost_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/cost_test.go @@ -629,6 +629,12 @@ func TestStringLibrary(t *testing.T) { expectEsimatedCost: checker.CostEstimate{Min: 2, Max: 2}, expectRuntimeCost: 2, }, + { + name: "jsonpatch.escapeKey", + expr: "jsonpatch.escapeKey('abc/def~ abc/def~')", + expectEsimatedCost: checker.CostEstimate{Min: 2, Max: 2}, + expectRuntimeCost: 2, + }, } for _, tc := range cases { @@ -1122,6 +1128,7 @@ func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate IP(), CIDR(), Format(), + JSONPatch(), cel.OptionalTypes(), // cel-go v0.17.7 introduced CostEstimatorOptions. // Previous the presence has a cost of 0 but cel fixed it to 1. We still set to 0 here to avoid breaking changes. diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/jsonpatch.go b/staging/src/k8s.io/apiserver/pkg/cel/library/jsonpatch.go new file mode 100644 index 00000000000..bdcb6d852b0 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/jsonpatch.go @@ -0,0 +1,89 @@ +/* +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 library + +import ( + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "strings" +) + +// JSONPatch provides a CEL function library extension of JSONPatch functions. +// +// jsonpatch.escapeKey +// +// Escapes a string for use as a JSONPatch path key. +// +// jsonpatch.escapeKey() +// +// Examples: +// +// "/metadata/labels/" + jsonpatch.escapeKey('k8s.io/my~label') // returns "/metadata/labels/k8s.io~1my~0label" +func JSONPatch() cel.EnvOption { + return cel.Lib(jsonPatchLib) +} + +var jsonPatchLib = &jsonPatch{} + +type jsonPatch struct{} + +func (*jsonPatch) LibraryName() string { + return "kubernetes.jsonpatch" +} + +func (*jsonPatch) declarations() map[string][]cel.FunctionOpt { + return jsonPatchLibraryDecls +} + +func (*jsonPatch) Types() []*cel.Type { + return []*cel.Type{} +} + +var jsonPatchLibraryDecls = map[string][]cel.FunctionOpt{ + "jsonpatch.escapeKey": { + cel.Overload("string_jsonpatch_escapeKey_string", []*cel.Type{cel.StringType}, cel.StringType, + cel.UnaryBinding(escape)), + }, +} + +func (*jsonPatch) CompileOptions() []cel.EnvOption { + var options []cel.EnvOption + for name, overloads := range jsonPatchLibraryDecls { + options = append(options, cel.Function(name, overloads...)) + } + return options +} + +func (*jsonPatch) ProgramOptions() []cel.ProgramOption { + return []cel.ProgramOption{} +} + +var jsonPatchReplacer = strings.NewReplacer("/", "~1", "~", "~0") + +func escapeKey(k string) string { + return jsonPatchReplacer.Replace(k) +} + +func escape(arg ref.Val) ref.Val { + s, ok := arg.Value().(string) + if !ok { + return types.MaybeNoSuchOverloadErr(arg) + } + escaped := escapeKey(s) + return types.String(escaped) +} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/libraries.go b/staging/src/k8s.io/apiserver/pkg/cel/library/libraries.go index e3689e3e0eb..dc436973e5a 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/libraries.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/libraries.go @@ -45,6 +45,7 @@ func KnownLibraries() []Library { cidrsLib, formatLib, semverLib, + jsonPatchLib, } } diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/library_compatibility_test.go b/staging/src/k8s.io/apiserver/pkg/cel/library/library_compatibility_test.go index 50b5d22882e..fc56651061f 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/library_compatibility_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/library_compatibility_test.go @@ -54,6 +54,8 @@ func TestLibraryCompatibility(t *testing.T) { "ip", "family", "isUnspecified", "isLoopback", "isLinkLocalMulticast", "isLinkLocalUnicast", "isGlobalUnicast", "ip.isCanonical", "isIP", "cidr", "containsIP", "containsCIDR", "masked", "prefixLength", "isCIDR", "string", // Kubernetes <1.31>: "fieldSelector", "labelSelector", "validate", "format.named", "isSemver", "major", "minor", "patch", "semver", + // Kubernetes <1.32>: + "jsonpatch.escapeKey", // Kubernetes <1.??>: ) From a0f419fe56ead7407548100801895d1950e77863 Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Fri, 25 Oct 2024 13:50:38 -0400 Subject: [PATCH 09/16] Add integration tests Co-authored-by: cici37 Co-authored-by: Alexander Zielensk --- .../cel/mutatingadmissionpolicy_test.go | 1414 +++++++++++++++++ 1 file changed, 1414 insertions(+) create mode 100644 test/integration/apiserver/cel/mutatingadmissionpolicy_test.go diff --git a/test/integration/apiserver/cel/mutatingadmissionpolicy_test.go b/test/integration/apiserver/cel/mutatingadmissionpolicy_test.go new file mode 100644 index 00000000000..011a521a28f --- /dev/null +++ b/test/integration/apiserver/cel/mutatingadmissionpolicy_test.go @@ -0,0 +1,1414 @@ +/* +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" + "errors" + "fmt" + "github.com/google/go-cmp/cmp/cmpopts" + "reflect" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + "k8s.io/api/admissionregistration/v1alpha1" + corev1 "k8s.io/api/core/v1" + apiextensions "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "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/apimachinery/pkg/util/uuid" + "k8s.io/apimachinery/pkg/util/wait" + genericfeatures "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + featuregatetesting "k8s.io/component-base/featuregate/testing" + apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" + "k8s.io/kubernetes/test/integration/etcd" + "k8s.io/kubernetes/test/integration/framework" +) + +// TestMutatingAdmissionPolicy tests MutatingAdmissionPolicy using a shared apiserver for all tests +// and waiting for bindings to become ready by dry-running marker requests until the binding successfully +// mutates a marker, and then verifies the policy exactly once. +func TestMutatingAdmissionPolicy(t *testing.T) { + matchEndpointResources := v1alpha1.MatchResources{ + ResourceRules: []v1alpha1.NamedRuleWithOperations{ + { + RuleWithOperations: v1alpha1.RuleWithOperations{ + Operations: []admissionregistrationv1.OperationType{"*"}, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"endpoints"}, + }, + }, + }, + }, + } + + cases := []struct { + name string + policies []*v1alpha1.MutatingAdmissionPolicy + bindings []*v1alpha1.MutatingAdmissionPolicyBinding + params []*corev1.ConfigMap + + requestOperation admissionregistrationv1.OperationType + requestResource schema.GroupVersionResource + subresources []string // Only supported for requestOperation=Update since subresources can not be created + requestObject runtime.Object + expected runtime.Object + }{ + { + name: "basic", + policies: []*v1alpha1.MutatingAdmissionPolicy{ + mutatingPolicy("basic-policy", v1alpha1.NeverReinvocationPolicy, matchEndpointResources, nil, v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: ` + Object{ + metadata: Object.metadata{ + annotations: { + "my-foo-annotation": "myAnnotationValue" + } + } + }`, + }, + }), + }, + bindings: []*v1alpha1.MutatingAdmissionPolicyBinding{ + mutatingBinding("basic-policy", nil, nil), + }, + requestOperation: admissionregistrationv1.Create, + requestResource: corev1.SchemeGroupVersion.WithResource("endpoints"), + requestObject: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic-policy-object", + Namespace: "default", + }, + }, + expected: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic-policy-object", + Namespace: "default", + Annotations: map[string]string{ + "my-foo-annotation": "myAnnotationValue", + }, + }, + }, + }, + { + name: "multiple policies", + policies: []*v1alpha1.MutatingAdmissionPolicy{ + mutatingPolicy("multi-policy-1", v1alpha1.NeverReinvocationPolicy, matchEndpointResources, nil, v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: ` + Object{ + metadata: Object.metadata{ + annotations: { + "foo1": "foo1Value" + } + } + }`, + }, + }), + mutatingPolicy("multi-policy-2", v1alpha1.NeverReinvocationPolicy, matchEndpointResources, nil, v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: ` + Object{ + metadata: Object.metadata{ + annotations: { + "foo2": "foo2Value" + } + } + }`, + }, + }), + }, + bindings: []*v1alpha1.MutatingAdmissionPolicyBinding{ + mutatingBinding("multi-policy-1", nil, nil), + mutatingBinding("multi-policy-2", nil, nil), + }, + requestOperation: admissionregistrationv1.Create, + requestResource: corev1.SchemeGroupVersion.WithResource("endpoints"), + requestObject: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "multi-policy-object", + Namespace: "default", + }, + }, + expected: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "multi-policy-object", + Namespace: "default", + Annotations: map[string]string{ + "foo1": "foo1Value", + "foo2": "foo2Value", + }, + }, + }, + }, + { + name: "policy with native param", + policies: []*v1alpha1.MutatingAdmissionPolicy{ + mutatingPolicy("policy-with-native", v1alpha1.NeverReinvocationPolicy, matchEndpointResources, &v1alpha1.ParamKind{ + APIVersion: "v1", + Kind: "ConfigMap", + }, v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: ` + Object{ + metadata: Object.metadata{ + annotations: { + params.data["key"]: params.data["value"] + } + } + }`, + }, + }), + }, + bindings: []*v1alpha1.MutatingAdmissionPolicyBinding{ + mutatingBinding("policy-with-native", &v1alpha1.ParamRef{ + Name: "policy-with-native-param", + }, nil), + }, + params: []*corev1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "policy-with-native-param", + Namespace: "default", + }, + Data: map[string]string{ + "key": "myFooKey", + "value": "myFooValue", + }, + }, + }, + requestOperation: admissionregistrationv1.Create, + requestResource: corev1.SchemeGroupVersion.WithResource("endpoints"), + requestObject: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "policy-with-native-object", + Namespace: "default", + }, + }, + expected: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "policy-with-native-object", + Namespace: "default", + Annotations: map[string]string{ + "myFooKey": "myFooValue", + }, + }, + }, + }, + { + name: "policy with multiple params quantified by single binding", + policies: []*v1alpha1.MutatingAdmissionPolicy{ + mutatingPolicy("multi-param-binding", v1alpha1.NeverReinvocationPolicy, matchEndpointResources, &v1alpha1.ParamKind{ + APIVersion: "v1", + Kind: "ConfigMap", + }, v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: `Object{metadata: Object.metadata{annotations: params.data}}`, + }, + }), + }, + bindings: []*v1alpha1.MutatingAdmissionPolicyBinding{ + mutatingBinding("multi-param-binding", &v1alpha1.ParamRef{ + // note empty namespace. all params matching request namespace + // will be used + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "multi-param-binding-param": "true", + }, + }, + Namespace: "default", + }, nil), + }, + params: []*corev1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "multi-param-binding-param-1", + Namespace: "default", + Labels: map[string]string{ + "multi-param-binding-param": "true", + }, + }, + Data: map[string]string{ + "multi-param-binding-key1": "value1", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "multi-param-binding-param-2", + Namespace: "default", + Labels: map[string]string{ + "multi-param-binding-param": "true", + }, + }, + Data: map[string]string{ + "multi-param-binding-key2": "value2", + "multi-param-binding-key3": "value3", + }, + }, + }, + requestOperation: admissionregistrationv1.Create, + requestResource: corev1.SchemeGroupVersion.WithResource("endpoints"), + requestObject: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "multi-param-binding-object", + Namespace: "default", + }, + }, + expected: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "multi-param-binding-object", + Namespace: "default", + Annotations: map[string]string{ + "multi-param-binding-key1": "value1", + "multi-param-binding-key2": "value2", + "multi-param-binding-key3": "value3", + }, + }, + }, + }, + { + name: "policy with variables", + policies: []*v1alpha1.MutatingAdmissionPolicy{ + withMutatingVariables([]v1alpha1.Variable{ + {Name: "foo1", Expression: `"foo1" + "Value"`}, + {Name: "foo2", Expression: `variables.foo1.replace("1", "2")`}, + }, + mutatingPolicy("policy-with-multiple-mutations", v1alpha1.NeverReinvocationPolicy, matchEndpointResources, nil, + v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: ` + Object{ + metadata: Object.metadata{ + annotations: { + "foo1": variables.foo1 + } + } + }`, + }, + }, + v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeJSONPatch, + JSONPatch: &v1alpha1.JSONPatch{ + Expression: `[ + JSONPatch{op: "test", path: "/metadata/annotations", value: {"foo1": variables.foo1}}, + JSONPatch{op: "add", path: "/metadata/annotations/foo2", value: variables.foo2}, + ]`, + }, + }, + v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: ` + Object{ + metadata: Object.metadata{ + annotations: { + "foo3": "foo3Value" + } + } + }`, + }, + }, + )), + }, + bindings: []*v1alpha1.MutatingAdmissionPolicyBinding{ + mutatingBinding("policy-with-multiple-mutations", nil, nil), + }, + requestOperation: admissionregistrationv1.Create, + requestResource: corev1.SchemeGroupVersion.WithResource("endpoints"), + requestObject: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "policy-with-multiple-mutations-object", + Namespace: "default", + }, + }, + expected: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "policy-with-multiple-mutations-object", + Namespace: "default", + Annotations: map[string]string{ + "foo1": "foo1Value", + "foo2": "foo2Value", + "foo3": "foo3Value", + }, + }, + }, + }, + { + name: "match condition matches", + policies: []*v1alpha1.MutatingAdmissionPolicy{ + withMutatingMatchConditions([]v1alpha1.MatchCondition{{Name: "test-only", Expression: `object.metadata.?labels["environment"] == optional.of("test")`}}, + mutatingPolicy("policy-match-condition", v1alpha1.NeverReinvocationPolicy, matchEndpointResources, nil, v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: `Object{metadata: Object.metadata{labels: {"applied": "updated"}}}`, + }, + })), + }, + bindings: []*v1alpha1.MutatingAdmissionPolicyBinding{ + mutatingBinding("policy-match-condition", nil, nil), + }, + requestOperation: admissionregistrationv1.Create, + requestResource: corev1.SchemeGroupVersion.WithResource("endpoints"), + requestObject: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "default", + Labels: map[string]string{"environment": "test"}, + }, + }, + expected: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "default", + Labels: map[string]string{"environment": "test", "applied": "updated"}, + }, + }, + }, + { + // same as the multiple mutations test, but the mutations are split + // across multiple policies + name: "multiple policies requiring reinvocation", + policies: []*v1alpha1.MutatingAdmissionPolicy{ + mutatingPolicy("policy-1", v1alpha1.IfNeededReinvocationPolicy, matchEndpointResources, nil, + v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: ` + Object{ + metadata: Object.metadata{ + annotations: { + ?"foo": optional.of(string(int(object.metadata.annotations["foo"]) + 1)), + "firstApplied": "true" + } + } + }`, + }, + }, + ), + mutatingPolicy("policy-2", v1alpha1.IfNeededReinvocationPolicy, matchEndpointResources, nil, + v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: ` + Object{ + metadata: Object.metadata{ + annotations: { + ?"foo": optional.of(string(int(object.metadata.annotations["foo"]) + 1)), + "secondApplied": "true" + } + } + }`, + }, + }, + ), + mutatingPolicy("policy-3", v1alpha1.NeverReinvocationPolicy, matchEndpointResources, nil, + v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: ` + Object{ + metadata: Object.metadata{ + annotations: { + ?"foo": optional.of(string(int(object.metadata.annotations["foo"]) + 1)), + "thirdApplied": "true" + } + } + }`, + }, + }, + ), + }, + bindings: []*v1alpha1.MutatingAdmissionPolicyBinding{ + mutatingBinding("policy-1", nil, nil), + mutatingBinding("policy-2", nil, nil), + mutatingBinding("policy-3", nil, nil), + }, + requestOperation: admissionregistrationv1.Create, + requestResource: corev1.SchemeGroupVersion.WithResource("endpoints"), + requestObject: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "default", + Annotations: map[string]string{ + "foo": "0", + }, + }, + }, + expected: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "default", + Annotations: map[string]string{ + // First mutation 0->1 + // Second mutation 1->2 + // Third Mutation 2->3 + // First Mutation Reinvocation 3->4 + // Second Mutation Reinvocation 4->5 + // (Third Mutation is set to never reinvocation, so it's not reinvoked) + // No future reinvocation passes (we only do a single reinvocation) + "foo": "5", + "firstApplied": "true", + "secondApplied": "true", + "thirdApplied": "true", + }, + }, + }, + }, + } + + // Run all tests in a shared apiserver + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.MutatingAdmissionPolicy, true) + server, err := apiservertesting.StartTestServer(t, nil, nil, framework.SharedEtcd()) + require.NoError(t, err) + defer server.TearDownFn() + + client, err := kubernetes.NewForConfig(server.ClientConfig) + require.NoError(t, err) + + dynClient, err := dynamic.NewForConfig(server.ClientConfig) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + for i, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + + // Create the policies, bindings and params. + for _, param := range tc.params { + _, err = client.CoreV1().ConfigMaps(param.GetNamespace()).Create(ctx, param, metav1.CreateOptions{FieldManager: "integration-test"}) + require.NoError(t, err) + } + + for _, p := range tc.policies { + // Modify each policy to also mutate marker requests. + p = withMutatingWaitReadyConstraintAndExpression(p, fmt.Sprintf("%d-%s", i, p.Name)) + _, err = client.AdmissionregistrationV1alpha1().MutatingAdmissionPolicies().Create(ctx, p, metav1.CreateOptions{FieldManager: "integration-test"}) + require.NoError(t, err) + } + + for _, b := range tc.bindings { + // After creating each binding, wait until a marker request is successfully mutated. + err = createAndWaitReadyMutating(ctx, t, client, b, fmt.Sprintf("%d-%s", i, b.Spec.PolicyName)) + require.NoError(t, err) + } + + unstructuredRequestObj := toUnstructured(t, tc.requestObject) + unstructuredExpectedObj := toUnstructured(t, tc.expected) + wipeUncheckedFields(t, unstructuredExpectedObj) + + defer func() { + if cleanupErr := cleanupMutatingPolicy(ctx, t, client, tc.policies, tc.bindings, tc.params); cleanupErr != nil { + t.Logf("error while cleaning up policy and its bindings: %v", cleanupErr) + } + }() + + // Verify that the policy is working as expected. + // Note that we do NOT retry requests here. Once the bindings are verified as working via marker + // requests, we expect the policy to work consistently for all subsequent requests. + var resultObj runtime.Object + rsrcClient := clientForType(t, unstructuredRequestObj, tc.requestResource, dynClient) + switch tc.requestOperation { + case admissionregistrationv1.Create: + resultObj, err = rsrcClient.Create(ctx, unstructuredRequestObj, metav1.CreateOptions{ + DryRun: []string{metav1.DryRunAll}, + FieldManager: "integration-test", + }, tc.subresources...) + case admissionregistrationv1.Update: + resultObj, err = rsrcClient.Update(ctx, unstructuredRequestObj, metav1.UpdateOptions{ + DryRun: []string{metav1.DryRunAll}, + FieldManager: "integration-test", + }, tc.subresources...) + require.NoError(t, err) + default: + t.Fatalf("unsupported operation: %v", tc.requestOperation) + } + wipeUncheckedFields(t, resultObj) + if !cmp.Equal(unstructuredExpectedObj, resultObj, cmpopts.EquateEmpty()) { + t.Errorf("unexpected diff:\n%s\n", cmp.Diff(unstructuredRequestObj, resultObj, cmpopts.EquateEmpty())) + } + }) + } +} + +// TestMutatingAdmissionPolicy_Slow tests policies by waiting until a request is successfully mutated. +// This is slower because it creates an apiserver for each whereas TestMutatingAdmissionPolicy creates +// a single apiserver and then uses marker requests to check that a binding is ready before testing it exactly once. +// Only test cases that cannot be run in TestMutatingAdmissionPolicy should be added here. +func TestMutatingAdmissionPolicy_Slow(t *testing.T) { + matchEndpointResources := v1alpha1.MatchResources{ + ResourceRules: []v1alpha1.NamedRuleWithOperations{ + { + RuleWithOperations: v1alpha1.RuleWithOperations{ + Operations: []admissionregistrationv1.OperationType{"*"}, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"endpoints"}, + }, + }, + }, + }, + } + + cases := []struct { + name string + policies []*v1alpha1.MutatingAdmissionPolicy + bindings []*v1alpha1.MutatingAdmissionPolicyBinding + params []*corev1.ConfigMap + + requestOperation admissionregistrationv1.OperationType + requestResource schema.GroupVersionResource + subresources []string // Only supported for requestOperation=Update since subresources can not be created + initialObject runtime.Object // For requestOperation=Update, this may be used to create the initial object state + requestObject runtime.Object + expected runtime.Object + }{ + { + name: "unbound policy is no-op", + policies: []*v1alpha1.MutatingAdmissionPolicy{ + mutatingPolicy("unbound-policy", v1alpha1.NeverReinvocationPolicy, matchEndpointResources, nil, v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: ` + Object{ + metadata: Object.metadata{ + annotations: { + "foo": "fooValue" + } + } + }`, + }, + }), + }, + requestOperation: admissionregistrationv1.Create, + requestResource: corev1.SchemeGroupVersion.WithResource("endpoints"), + requestObject: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "default", + }, + }, + expected: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "default", + }, + }, + }, + { + name: "failure policy ignore", + policies: []*v1alpha1.MutatingAdmissionPolicy{ + withMutatingFailurePolicy(v1alpha1.Ignore, + mutatingPolicy("policy", v1alpha1.NeverReinvocationPolicy, matchEndpointResources, nil, v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: `Object{spec: Object.spec{invalidField: "invalid apply configuration"}}`, + }, + })), + }, + bindings: []*v1alpha1.MutatingAdmissionPolicyBinding{ + mutatingBinding("policy", nil, nil), + }, + requestOperation: admissionregistrationv1.Create, + requestResource: corev1.SchemeGroupVersion.WithResource("endpoints"), + requestObject: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "default", + }, + }, + expected: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "default", + }, + }, + }, + { + name: "match condition does not match", + policies: []*v1alpha1.MutatingAdmissionPolicy{ + withMutatingMatchConditions([]v1alpha1.MatchCondition{{Name: "test-only", Expression: `object.metadata.?labels["environment"] == optional.of("test")`}}, + mutatingPolicy("policy-no-match-condition", v1alpha1.NeverReinvocationPolicy, matchEndpointResources, nil, v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: `Object{metadata: Object.metadata{labels: {"applied": "updated"}}}`, + }, + })), + }, + bindings: []*v1alpha1.MutatingAdmissionPolicyBinding{ + mutatingBinding("policy-no-match-condition", nil, nil), + }, + requestOperation: admissionregistrationv1.Create, + requestResource: corev1.SchemeGroupVersion.WithResource("endpoints"), + requestObject: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "default", + Labels: map[string]string{"environment": "production"}, + }, + }, + expected: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "default", + Labels: map[string]string{"environment": "production"}, + }, + }, + }, + { + name: "some policy conditions match", + policies: []*v1alpha1.MutatingAdmissionPolicy{ + withMutatingMatchConditions([]v1alpha1.MatchCondition{{Name: "test-only", Expression: `object.metadata.?labels["environment"] == optional.of("production")`}}, + mutatingPolicy("policy-1", v1alpha1.NeverReinvocationPolicy, matchEndpointResources, nil, v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: `Object{metadata: Object.metadata{labels: {"applied": "wrong"}}}`, + }, + })), + withMutatingMatchConditions([]v1alpha1.MatchCondition{{Name: "test-only", Expression: `object.metadata.?labels["environment"] == optional.of("test")`}}, + mutatingPolicy("policy-2", v1alpha1.NeverReinvocationPolicy, matchEndpointResources, nil, v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: `Object{metadata: Object.metadata{labels: {"applied": "updated"}}}`, + }, + })), + }, + bindings: []*v1alpha1.MutatingAdmissionPolicyBinding{ + mutatingBinding("policy-1", nil, nil), + mutatingBinding("policy-2", nil, nil), + }, + requestOperation: admissionregistrationv1.Create, + requestResource: corev1.SchemeGroupVersion.WithResource("endpoints"), + requestObject: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "default", + Labels: map[string]string{"environment": "test"}, + }, + }, + expected: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "default", + Labels: map[string]string{"environment": "test", "applied": "updated"}, + }, + }, + }, + { + name: "mutate status subresource", + policies: []*v1alpha1.MutatingAdmissionPolicy{ + mutatingPolicy("subresource-status", v1alpha1.NeverReinvocationPolicy, v1alpha1.MatchResources{ + ResourceRules: []v1alpha1.NamedRuleWithOperations{ + { + RuleWithOperations: v1alpha1.RuleWithOperations{ + Operations: []admissionregistrationv1.OperationType{"*"}, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"namespaces/status"}, + }, + }, + }, + }, + }, nil, v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: `Object{ + status: Object.status{ + conditions: [Object.status.conditions{ + type: "NamespaceDeletionContentFailure", + message: "mutated" + }] + } + }`, + }, + }), + }, + bindings: []*v1alpha1.MutatingAdmissionPolicyBinding{ + mutatingBinding("subresource-status", nil, nil), + }, + requestOperation: admissionregistrationv1.Update, + subresources: []string{"status"}, + requestResource: corev1.SchemeGroupVersion.WithResource("namespaces"), + initialObject: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-namespace", + }, + }, + requestObject: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-namespace", + }, + Status: corev1.NamespaceStatus{ + Conditions: []corev1.NamespaceCondition{{ + Type: corev1.NamespaceDeletionContentFailure, + Status: corev1.ConditionUnknown, + }}, + }, + }, + expected: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-namespace", + Labels: map[string]string{"kubernetes.io/metadata.name": "test-namespace"}, + }, + Spec: corev1.NamespaceSpec{ + Finalizers: []corev1.FinalizerName{"kubernetes"}, + }, + Status: corev1.NamespaceStatus{ + Conditions: []corev1.NamespaceCondition{{ + Type: corev1.NamespaceDeletionContentFailure, + Status: corev1.ConditionUnknown, + Message: "mutated", + }}, + Phase: corev1.NamespaceActive, + }, + }, + }, + { + name: "multiple bindings with different params", + policies: []*v1alpha1.MutatingAdmissionPolicy{ + mutatingPolicy("multi-binding", v1alpha1.NeverReinvocationPolicy, matchEndpointResources, &v1alpha1.ParamKind{ + APIVersion: "v1", + Kind: "ConfigMap", + }, v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: `Object{metadata: Object.metadata{annotations: params.data}}`, + }, + }), + }, + bindings: []*v1alpha1.MutatingAdmissionPolicyBinding{ + mutatingBinding("multi-binding", &v1alpha1.ParamRef{ + Name: "multi-binding-param-1", + Namespace: "default", + }, nil), + mutatingBinding("multi-binding", &v1alpha1.ParamRef{ + Name: "multi-binding-param-2", + Namespace: "default", + }, nil), + }, + params: []*corev1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "multi-binding-param-1", + Namespace: "default", + }, + Data: map[string]string{ + "multi-binding-key1": "value1", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "multi-binding-param-2", + Namespace: "default", + }, + Data: map[string]string{ + "multi-binding-key2": "value2", + "multi-binding-key3": "value3", + }, + }, + }, + requestOperation: admissionregistrationv1.Create, + requestResource: corev1.SchemeGroupVersion.WithResource("endpoints"), + requestObject: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "multi-binding-object", + Namespace: "default", + }, + }, + expected: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "multi-binding-object", + Namespace: "default", + Annotations: map[string]string{ + "multi-binding-key1": "value1", + "multi-binding-key2": "value2", + "multi-binding-key3": "value3", + }, + }, + }, + }, + { + // Same as the other cases, but the reinvocation is caused by + // multiple params bound + name: "multiple params causing reinvocation", + policies: []*v1alpha1.MutatingAdmissionPolicy{ + mutatingPolicy( + "multi-param-reinvocation", + v1alpha1.IfNeededReinvocationPolicy, + matchEndpointResources, + &v1alpha1.ParamKind{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: `Object{metadata: Object.metadata{annotations: params.data}}`, + }, + }, + v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: ` + Object{ + metadata: Object.metadata{ + annotations: { + ?"foo": optional.of(string(int(object.metadata.annotations["foo"]) + 1)), + } + } + }`, + }, + }, + ), + mutatingPolicy( + "policy-with-param-no-reinvoke", + v1alpha1.NeverReinvocationPolicy, + matchEndpointResources, + &v1alpha1.ParamKind{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: `Object{metadata: Object.metadata{annotations: params.data}}`, + }, + }, + v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: ` + Object{ + metadata: Object.metadata{ + annotations: { + ?"foo": optional.of(string(int(object.metadata.annotations["foo"]) + 1)), + } + } + }`, + }, + }, + ), + }, + bindings: []*v1alpha1.MutatingAdmissionPolicyBinding{ + mutatingBinding("multi-param-reinvocation", &v1alpha1.ParamRef{ + // note empty namespace. all params matching request namespace + // will be used + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "multi-param-reinvocation": "true", + }, + }, + }, nil), + mutatingBinding("policy-with-param-no-reinvoke", &v1alpha1.ParamRef{ + // note empty namespace. all params matching request namespace + // will be used + Name: "multi-param-reinvocation-param-3", + }, nil), + }, + params: []*corev1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "multi-param-reinvocation-param-1", + Namespace: "default", + Labels: map[string]string{ + "multi-param-reinvocation": "true", + }, + }, + Data: map[string]string{ + "firstApplied": "true", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "multi-param-reinvocation-param-2", + Namespace: "default", + Labels: map[string]string{ + "multi-param-reinvocation": "true", + }, + }, + Data: map[string]string{ + "secondApplied": "true", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "multi-param-reinvocation-param-3", + Namespace: "default", + }, + Data: map[string]string{ + "thirdApplied": "true", + }, + }, + }, + requestOperation: admissionregistrationv1.Create, + requestResource: corev1.SchemeGroupVersion.WithResource("endpoints"), + requestObject: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "multi-param-reinvocation-object", + Namespace: "default", + Annotations: map[string]string{ + "foo": "0", + }, + }, + }, + expected: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "multi-param-reinvocation-object", + Namespace: "default", + Annotations: map[string]string{ + "firstApplied": "true", + "secondApplied": "true", + "thirdApplied": "true", + "foo": "5", + }, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.MutatingAdmissionPolicy, true) + server, err := apiservertesting.StartTestServer(t, nil, nil, framework.SharedEtcd()) + require.NoError(t, err) + defer server.TearDownFn() + + client, err := kubernetes.NewForConfig(server.ClientConfig) + require.NoError(t, err) + + dynClient, err := dynamic.NewForConfig(server.ClientConfig) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + for _, param := range tc.params { + _, err = client.CoreV1().ConfigMaps(param.GetNamespace()).Create(ctx, param, metav1.CreateOptions{FieldManager: "integration-test"}) + require.NoError(t, err) + } + + for _, p := range tc.policies { + _, err = client.AdmissionregistrationV1alpha1().MutatingAdmissionPolicies().Create(ctx, p, metav1.CreateOptions{FieldManager: "integration-test"}) + require.NoError(t, err) + } + + for _, b := range tc.bindings { + _, err = client.AdmissionregistrationV1alpha1().MutatingAdmissionPolicyBindings().Create(ctx, b, metav1.CreateOptions{FieldManager: "integration-test"}) + require.NoError(t, err) + } + + if tc.initialObject != nil { + initClient := clientForType(t, tc.initialObject, tc.requestResource, dynClient) + _, err = initClient.Create(ctx, toUnstructured(t, tc.initialObject), metav1.CreateOptions{}) + require.NoError(t, err) + } + + unstructuredRequestObj := toUnstructured(t, tc.requestObject) + unstructuredExpectedObj := toUnstructured(t, tc.expected) + wipeUncheckedFields(t, unstructuredExpectedObj) + + // Dry Run the request until we get the expected mutated response + var resultObj runtime.Object + err = wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, 5*time.Second, false, func(ctx context.Context) (done bool, err error) { + rsrcClient := clientForType(t, unstructuredRequestObj, tc.requestResource, dynClient) + switch tc.requestOperation { + case admissionregistrationv1.Create: + resultObj, err = rsrcClient.Create(ctx, unstructuredRequestObj, metav1.CreateOptions{ + DryRun: []string{metav1.DryRunAll}, + FieldManager: "integration-test", + }, tc.subresources...) + if err != nil { + t.Logf("error while waiting: %v", err) + return false, nil + } + wipeUncheckedFields(t, resultObj) + return reflect.DeepEqual(unstructuredExpectedObj, resultObj), nil + case admissionregistrationv1.Update: + resultObj, err = rsrcClient.Update(ctx, unstructuredRequestObj, metav1.UpdateOptions{ + DryRun: []string{metav1.DryRunAll}, + FieldManager: "integration-test", + }, tc.subresources...) + if err != nil { + t.Logf("error while waiting: %v", err) + return false, nil + } + wipeUncheckedFields(t, resultObj) + return reflect.DeepEqual(unstructuredExpectedObj, resultObj), nil + default: + t.Fatalf("unsupported operation: %v", tc.requestOperation) + } + return false, nil + }) + + if errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("failed to get expected result before timeout: %v", cmp.Diff(unstructuredExpectedObj, resultObj)) + } else if err != nil { + t.Fatal(err) + } + }) + } +} + +// Test_MutatingAdmissionPolicy_CustomResources tests a custom resource mutation. +// CRDs are also ideal for testing version conversion since old version are not removed, so version conversion is also +// tested. +func Test_MutatingAdmissionPolicy_CustomResources(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.MutatingAdmissionPolicy, true) + server, err := apiservertesting.StartTestServer(t, nil, nil, framework.SharedEtcd()) + etcd.CreateTestCRDs(t, apiextensions.NewForConfigOrDie(server.ClientConfig), false, versionedCustomResourceDefinition()) + if err != nil { + t.Fatal(err) + } + defer server.TearDownFn() + + config := server.ClientConfig + + client, err := kubernetes.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + policy := withMutatingFailurePolicy(v1alpha1.Fail, mutatingPolicy("match-by-match-policy-equivalent", v1alpha1.IfNeededReinvocationPolicy, v1alpha1.MatchResources{ + ResourceRules: []v1alpha1.NamedRuleWithOperations{ + { + RuleWithOperations: v1alpha1.RuleWithOperations{ + Operations: []v1alpha1.OperationType{ + "*", + }, + Rule: v1alpha1.Rule{ + APIGroups: []string{ + "awesome.bears.com", + }, + APIVersions: []string{ + "v1", + }, + Resources: []string{ + "pandas", + }, + }, + }, + }, + }}, + nil, + v1alpha1.Mutation{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + Expression: `Object{ metadata: Object.metadata{ labels: {"mutated-panda": "true"} } }`, + }, + }, + )) + testID := "policy-equivalent" + policy = withMutatingWaitReadyConstraintAndExpression(policy, testID) + if _, err := client.AdmissionregistrationV1alpha1().MutatingAdmissionPolicies().Create(ctx, policy, metav1.CreateOptions{}); err != nil { + t.Fatal(err) + } + + policyBinding := mutatingBinding("match-by-match-policy-equivalent", nil, nil) + if err := createAndWaitReadyMutating(ctx, t, client, policyBinding, testID); err != nil { + t.Fatal(err) + } + + v1Resource := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "awesome.bears.com" + "/" + "v1", + "kind": "Panda", + "metadata": map[string]interface{}{ + "name": "v1-bears", + }, + }, + } + + v2Resource := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "awesome.bears.com" + "/" + "v2", + "kind": "Panda", + "metadata": map[string]interface{}{ + "name": "v2-bears", + }, + }, + } + + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + + // Wait for CRDs to register + err = wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, wait.ForeverTestTimeout, true, func(ctx context.Context) (bool, error) { + createdv1, err := dynamicClient.Resource(schema.GroupVersionResource{Group: "awesome.bears.com", Version: "v1", Resource: "pandas"}).Create(ctx, v1Resource, metav1.CreateOptions{}) + if err != nil { + if strings.Contains(err.Error(), "Resource kind awesome.bears.com/v1, Kind=Panda not found") { + return false, nil + } + return false, nil + } + if createdv1.GetLabels()["mutated-panda"] != "true" { + t.Errorf("expected mutated-panda to be true, got %s", createdv1.GetLabels()) + } + return true, nil + }) + if err != nil { + t.Fatal(err) + } + + err = wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, wait.ForeverTestTimeout, true, func(ctx context.Context) (bool, error) { + createdv2, err := dynamicClient.Resource(schema.GroupVersionResource{Group: "awesome.bears.com", Version: "v2", Resource: "pandas"}).Create(ctx, v2Resource, metav1.CreateOptions{}) + if err != nil { + if strings.Contains(err.Error(), "Resource kind awesome.bears.com/v2, Kind=Panda not found") { + return false, nil + } + return false, nil + } + if createdv2.GetLabels()["mutated-panda"] != "true" { + t.Errorf("expected mutated-panda to be true, got %s", createdv2.GetLabels()) + } + return true, nil + }) + if err != nil { + t.Fatal(err) + } + +} + +func mutatingPolicy(name string, reinvocationPolicy v1alpha1.ReinvocationPolicyType, matchResources v1alpha1.MatchResources, paramKind *v1alpha1.ParamKind, mutations ...v1alpha1.Mutation) *v1alpha1.MutatingAdmissionPolicy { + return &v1alpha1.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: v1alpha1.MutatingAdmissionPolicySpec{ + MatchConstraints: &matchResources, + ParamKind: paramKind, + Mutations: mutations, + ReinvocationPolicy: reinvocationPolicy, + }, + } +} + +func withMutatingVariables(variables []v1alpha1.Variable, policy *v1alpha1.MutatingAdmissionPolicy) *v1alpha1.MutatingAdmissionPolicy { + policy.Spec.Variables = variables + return policy +} + +func withMutatingFailurePolicy(failure v1alpha1.FailurePolicyType, policy *v1alpha1.MutatingAdmissionPolicy) *v1alpha1.MutatingAdmissionPolicy { + policy.Spec.FailurePolicy = &failure + return policy +} + +func withMutatingMatchConditions(matchConditions []v1alpha1.MatchCondition, policy *v1alpha1.MutatingAdmissionPolicy) *v1alpha1.MutatingAdmissionPolicy { + policy.Spec.MatchConditions = matchConditions + return policy +} + +func withMutatingWaitReadyConstraintAndExpression(policy *v1alpha1.MutatingAdmissionPolicy, testID string) *v1alpha1.MutatingAdmissionPolicy { + policy = policy.DeepCopy() + policy.Spec.MatchConstraints.ResourceRules = append(policy.Spec.MatchConstraints.ResourceRules, v1alpha1.NamedRuleWithOperations{ + ResourceNames: []string{"test-marker"}, + RuleWithOperations: admissionregistrationv1.RuleWithOperations{ + Operations: []admissionregistrationv1.OperationType{ + "CREATE", + }, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{ + "", + }, + APIVersions: []string{ + "v1", + }, + Resources: []string{ + "endpoints", + }, + }, + }, + }) + for i, mc := range policy.Spec.MatchConditions { + mc.Expression = `object.metadata.?labels["mutation-marker"].hasValue() || ` + mc.Expression + policy.Spec.MatchConditions[i] = mc + } + for _, m := range policy.Spec.Mutations { + if m.ApplyConfiguration != nil { + bypass := `object.metadata.?labels["mutation-marker"].hasValue() ? Object{} : ` + m.ApplyConfiguration.Expression = bypass + m.ApplyConfiguration.Expression + } + } + policy.Spec.Mutations = append([]v1alpha1.Mutation{{ + PatchType: v1alpha1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1alpha1.ApplyConfiguration{ + // Only mutate mutation-markers. + Expression: fmt.Sprintf(`object.metadata.?labels["mutation-marker"] == optional.of("%v") ? Object{ metadata: Object.metadata{ labels: {"mutated":"%v"}}}: Object{}`, testID, testID), + }, + }}, policy.Spec.Mutations...) + return policy +} + +func mutatingBinding(policyName string, paramRef *v1alpha1.ParamRef, matchResources *v1alpha1.MatchResources) *v1alpha1.MutatingAdmissionPolicyBinding { + return &v1alpha1.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: policyName + "-binding-" + string(uuid.NewUUID()), + }, + Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{ + PolicyName: policyName, + ParamRef: paramRef, + MatchResources: matchResources, + }, + } +} + +func createAndWaitReadyMutating(ctx context.Context, t *testing.T, client kubernetes.Interface, binding *v1alpha1.MutatingAdmissionPolicyBinding, testID string) error { + return createAndWaitReadyNamespacedWithWarnHandlerMutating(ctx, t, client, binding, "default", testID) +} + +func createAndWaitReadyNamespacedWithWarnHandlerMutating(ctx context.Context, t *testing.T, client kubernetes.Interface, binding *v1alpha1.MutatingAdmissionPolicyBinding, ns string, testID string) error { + _, err := client.AdmissionregistrationV1alpha1().MutatingAdmissionPolicyBindings().Create(ctx, binding, metav1.CreateOptions{}) + if err != nil { + t.Fatal(err) + } + marker := &corev1.Endpoints{ObjectMeta: metav1.ObjectMeta{Name: "test-marker", Namespace: ns, Labels: map[string]string{"mutation-marker": testID}}} + if waitErr := wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, wait.ForeverTestTimeout, true, func(ctx context.Context) (bool, error) { + result, err := client.CoreV1().Endpoints(ns).Create(ctx, marker, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}, FieldManager: "mutation-marker-sender"}) + if err != nil { + if strings.Contains(err.Error(), "no params found for policy binding") { // wait for params to register + return false, nil + } else { + return false, err + } + } + if result.Labels["mutated"] == testID { + return true, nil + } + return false, nil + }); waitErr != nil { + return waitErr + } + t.Logf("Marker ready: %v", marker) + return nil +} + +func cleanupMutatingPolicy(ctx context.Context, t *testing.T, client kubernetes.Interface, policies []*v1alpha1.MutatingAdmissionPolicy, bindings []*v1alpha1.MutatingAdmissionPolicyBinding, params []*corev1.ConfigMap) error { + for _, policy := range policies { + if err := client.AdmissionregistrationV1alpha1().MutatingAdmissionPolicies().Delete(ctx, policy.Name, metav1.DeleteOptions{}); err != nil { + t.Fatal(err) + } + + if waitErr := wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, time.Minute, true, func(ctx context.Context) (bool, error) { + _, err := client.AdmissionregistrationV1alpha1().MutatingAdmissionPolicies().Get(ctx, policy.Name, metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + return true, nil + } + return false, nil + }); waitErr != nil { + t.Fatalf("timed out waiting for policy to be cleaned up: %v", waitErr) + } + } + + for _, binding := range bindings { + err := client.AdmissionregistrationV1alpha1().MutatingAdmissionPolicyBindings().Delete(ctx, binding.Name, metav1.DeleteOptions{}) + if err != nil { + t.Fatal(err) + } + + if waitErr := wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, time.Minute, true, func(ctx context.Context) (bool, error) { + _, err := client.AdmissionregistrationV1alpha1().MutatingAdmissionPolicyBindings().Get(ctx, binding.Name, metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + return true, nil + } + return false, err + }); waitErr != nil { + t.Fatalf("timed out waiting for policy binding to be cleaned up: %v", err) + } + } + + for _, param := range params { + if waitErr := wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, time.Minute, true, func(ctx context.Context) (bool, error) { + if err := client.CoreV1().ConfigMaps(param.GetNamespace()).Delete(ctx, param.Name, metav1.DeleteOptions{}); err != nil { + if apierrors.IsNotFound(err) { + return true, nil + } + return false, nil + } + _, err := client.CoreV1().ConfigMaps(param.GetNamespace()).Get(ctx, param.Name, metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + return true, nil + } + return false, nil + }); waitErr != nil { + t.Fatalf("timed out waiting for policy to be cleaned up: %v", waitErr) + } + } + + return nil +} + +func clientForType(t *testing.T, obj runtime.Object, resource schema.GroupVersionResource, dynClient *dynamic.DynamicClient) dynamic.ResourceInterface { + acc, err := meta.Accessor(obj) + require.NoError(t, err) + var rsrcClient dynamic.ResourceInterface = dynClient.Resource(resource) + if len(acc.GetNamespace()) > 0 { + rsrcClient = rsrcClient.(dynamic.NamespaceableResourceInterface).Namespace(acc.GetNamespace()) + } + return rsrcClient +} + +func toUnstructured(t *testing.T, obj runtime.Object) *unstructured.Unstructured { + unstructuredRequestMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + require.NoError(t, err) + + unstructuredRequestObj := &unstructured.Unstructured{ + Object: unstructuredRequestMap, + } + return unstructuredRequestObj +} + +func wipeUncheckedFields(t *testing.T, obj runtime.Object) { + acc, err := meta.Accessor(obj) + require.NoError(t, err) + + // GVK can't be patched, and not always on our test objects, so + // clear for convenience + obj.GetObjectKind().SetGroupVersionKind(schema.GroupVersionKind{}) + + // Will be set by server, should be wiped + acc.SetResourceVersion("") + acc.SetUID("") + acc.SetCreationTimestamp(metav1.Time{}) + acc.SetManagedFields(nil) +} From 700e3b566428bca46d2d37514c8fd7af60c7ff34 Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Fri, 25 Oct 2024 13:51:03 -0400 Subject: [PATCH 10/16] Update OpenAPI and fix openAPI tests to handle unexported jsonreferences Co-authored-by: Alexander Zielensk --- go.mod | 2 +- pkg/generated/openapi/openapi_test.go | 16 +++++++++++++--- .../client-go/openapi/cached/groupversion.go | 4 ++++ .../src/k8s.io/client-go/openapi/groupversion.go | 12 ++++++++++++ .../client-go/openapi/openapitest/fakeclient.go | 5 +++++ .../client-go/openapi/openapitest/fileclient.go | 5 +++++ 6 files changed, 40 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index ae60b971438..f85602f3b5c 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,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/groupcache v0.0.0-20210331224755-41bb18bfe9da @@ -153,7 +154,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 diff --git a/pkg/generated/openapi/openapi_test.go b/pkg/generated/openapi/openapi_test.go index 1c9776ea857..1b1d1c302f9 100644 --- a/pkg/generated/openapi/openapi_test.go +++ b/pkg/generated/openapi/openapi_test.go @@ -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 } }) diff --git a/staging/src/k8s.io/client-go/openapi/cached/groupversion.go b/staging/src/k8s.io/client-go/openapi/cached/groupversion.go index 65a4189f7a8..73730c51be1 100644 --- a/staging/src/k8s.io/client-go/openapi/cached/groupversion.go +++ b/staging/src/k8s.io/client-go/openapi/cached/groupversion.go @@ -56,3 +56,7 @@ func (g *groupversion) Schema(contentType string) ([]byte, error) { return cachedInfo.data, cachedInfo.err } + +func (c *groupversion) ServerRelativeURL() string { + return c.delegate.ServerRelativeURL() +} diff --git a/staging/src/k8s.io/client-go/openapi/groupversion.go b/staging/src/k8s.io/client-go/openapi/groupversion.go index 601dcbe3ccb..40d91b9a533 100644 --- a/staging/src/k8s.io/client-go/openapi/groupversion.go +++ b/staging/src/k8s.io/client-go/openapi/groupversion.go @@ -27,6 +27,12 @@ const ContentTypeOpenAPIV3PB = "application/com.github.proto-openapi.spec.v3@v1. type GroupVersion interface { Schema(contentType string) ([]byte, error) + + // ServerRelativeURL. Returns the path and parameters used to fetch the schema. + // You should use the Schema method to fetch it, but this value can be used + // to key the current version of the schema in a cache since it contains a + // hash string which changes upon schema update. + ServerRelativeURL() string } type groupversion struct { @@ -68,3 +74,9 @@ func (g *groupversion) Schema(contentType string) ([]byte, error) { return path.Do(context.TODO()).Raw() } + +// URL used for fetching the schema. The URL includes a hash and can be used +// to key the current version of the schema in a cache. +func (g *groupversion) ServerRelativeURL() string { + return g.item.ServerRelativeURL +} diff --git a/staging/src/k8s.io/client-go/openapi/openapitest/fakeclient.go b/staging/src/k8s.io/client-go/openapi/openapitest/fakeclient.go index ec3d84322ec..43b5f679e86 100644 --- a/staging/src/k8s.io/client-go/openapi/openapitest/fakeclient.go +++ b/staging/src/k8s.io/client-go/openapi/openapitest/fakeclient.go @@ -77,3 +77,8 @@ func (f FakeGroupVersion) Schema(contentType string) ([]byte, error) { } return f.GVSpec, nil } + +// ServerRelativeURL returns an empty string. +func (f FakeGroupVersion) ServerRelativeURL() string { + panic("unimplemented") +} diff --git a/staging/src/k8s.io/client-go/openapi/openapitest/fileclient.go b/staging/src/k8s.io/client-go/openapi/openapitest/fileclient.go index d53f63a16bc..96b882de91b 100644 --- a/staging/src/k8s.io/client-go/openapi/openapitest/fileclient.go +++ b/staging/src/k8s.io/client-go/openapi/openapitest/fileclient.go @@ -94,3 +94,8 @@ func (f *fileGroupVersion) Schema(contentType string) ([]byte, error) { } return fs.ReadFile(f.f, f.filename) } + +// ServerRelativeURL returns an empty string. +func (f *fileGroupVersion) ServerRelativeURL() string { + return f.filename +} From c0f9c813380ea348b79eef5f23e5d27d4cb9e7c3 Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Fri, 25 Oct 2024 13:51:31 -0400 Subject: [PATCH 11/16] Add MutatingAdmissionPolicy to samples --- .../apiserver/samples/generic/server/admission.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/controlplane/apiserver/samples/generic/server/admission.go b/pkg/controlplane/apiserver/samples/generic/server/admission.go index 18269b945ec..eba588534c7 100644 --- a/pkg/controlplane/apiserver/samples/generic/server/admission.go +++ b/pkg/controlplane/apiserver/samples/generic/server/admission.go @@ -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) From 1ad6fd7a0fa454cc3302b579dc73eb5c9afec49a Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Fri, 25 Oct 2024 13:52:34 -0400 Subject: [PATCH 12/16] Improve error messaging for validating admission policy authz --- .../validatingadmissionpolicy/authz_test.go | 28 +- .../storage/storage_test.go | 11 +- .../strategy_test.go | 13 +- .../validatingadmissionpolicybinding/authz.go | 30 +- .../authz_test.go | 101 +++- .../storage/storage_test.go | 11 +- .../strategy_test.go | 12 +- .../policy/validating/caching_authorizer.go | 152 ----- .../validating/caching_authorizer_test.go | 523 ------------------ .../plugin/policy/validating/validator.go | 8 +- .../policy/validating/validator_test.go | 6 +- 11 files changed, 185 insertions(+), 710 deletions(-) delete mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/caching_authorizer.go delete mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/caching_authorizer_test.go diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicy/authz_test.go b/pkg/registry/admissionregistration/validatingadmissionpolicy/authz_test.go index dc4a7e5d017..8b2e56a7baa 100644 --- a/pkg/registry/admissionregistration/validatingadmissionpolicy/authz_test.go +++ b/pkg/registry/admissionregistration/validatingadmissionpolicy/authz_test.go @@ -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) } diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage_test.go b/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage_test.go index 29c35559873..98112f765bd 100644 --- a/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage_test.go +++ b/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage_test.go @@ -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, 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) } + +var replicaLimitsResolver resolver.ResourceResolverFunc = func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: "rules.example.com", + Version: "v1", + Resource: "replicalimits", + }, nil +} diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicy/strategy_test.go b/pkg/registry/admissionregistration/validatingadmissionpolicy/strategy_test.go index cb993f72f71..03282596779 100644 --- a/pkg/registry/admissionregistration/validatingadmissionpolicy/strategy_test.go +++ b/pkg/registry/admissionregistration/validatingadmissionpolicy/strategy_test.go @@ -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{ diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/authz.go b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/authz.go index 77ac8b2bd47..68466c37f0e 100644 --- a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/authz.go +++ b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/authz.go @@ -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) } diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/authz_test.go b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/authz_test.go index c72e9f92d2a..554374f0e40 100644 --- a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/authz_test.go +++ b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/authz_test.go @@ -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) } } }) diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage/storage_test.go b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage/storage_test.go index f177f57ea67..d679b76d12c 100644 --- a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage/storage_test.go +++ b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage/storage_test.go @@ -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 +} diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/strategy_test.go b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/strategy_test.go index 55b310db31a..cf26c09075e 100644 --- a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/strategy_test.go +++ b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/strategy_test.go @@ -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{ diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/caching_authorizer.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/caching_authorizer.go deleted file mode 100644 index ac13dbeee37..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/caching_authorizer.go +++ /dev/null @@ -1,152 +0,0 @@ -/* -Copyright 2023 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 validating - -import ( - "context" - "encoding/json" - "sort" - "strings" - - "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apiserver/pkg/authentication/user" - "k8s.io/apiserver/pkg/authorization/authorizer" -) - -type authzResult struct { - authorized authorizer.Decision - reason string - err error -} - -type cachingAuthorizer struct { - authorizer authorizer.Authorizer - decisions map[string]authzResult -} - -func newCachingAuthorizer(in authorizer.Authorizer) authorizer.Authorizer { - return &cachingAuthorizer{ - authorizer: in, - decisions: make(map[string]authzResult), - } -} - -// The attribute accessors known to cache key construction. If this fails to compile, the cache -// implementation may need to be updated. -var _ authorizer.Attributes = (interface { - GetUser() user.Info - GetVerb() string - IsReadOnly() bool - GetNamespace() string - GetResource() string - GetSubresource() string - GetName() string - GetAPIGroup() string - GetAPIVersion() string - IsResourceRequest() bool - GetPath() string - GetFieldSelector() (fields.Requirements, error) - GetLabelSelector() (labels.Requirements, error) -})(nil) - -// The user info accessors known to cache key construction. If this fails to compile, the cache -// implementation may need to be updated. -var _ user.Info = (interface { - GetName() string - GetUID() string - GetGroups() []string - GetExtra() map[string][]string -})(nil) - -// Authorize returns an authorization decision by delegating to another Authorizer. If an equivalent -// check has already been performed, a cached result is returned. Not safe for concurrent use. -func (ca *cachingAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { - type SerializableAttributes struct { - authorizer.AttributesRecord - LabelSelector string - } - - serializableAttributes := SerializableAttributes{ - AttributesRecord: authorizer.AttributesRecord{ - Verb: a.GetVerb(), - Namespace: a.GetNamespace(), - APIGroup: a.GetAPIGroup(), - APIVersion: a.GetAPIVersion(), - Resource: a.GetResource(), - Subresource: a.GetSubresource(), - Name: a.GetName(), - ResourceRequest: a.IsResourceRequest(), - Path: a.GetPath(), - }, - } - // in the error case, we won't honor this field selector, so the cache doesn't need it. - if fieldSelector, err := a.GetFieldSelector(); len(fieldSelector) > 0 { - serializableAttributes.FieldSelectorRequirements, serializableAttributes.FieldSelectorParsingErr = fieldSelector, err - } - if labelSelector, _ := a.GetLabelSelector(); len(labelSelector) > 0 { - // the labels requirements have private elements so those don't help us serialize to a unique key - serializableAttributes.LabelSelector = labelSelector.String() - } - - if u := a.GetUser(); u != nil { - di := &user.DefaultInfo{ - Name: u.GetName(), - UID: u.GetUID(), - } - - // Differently-ordered groups or extras could cause otherwise-equivalent checks to - // have distinct cache keys. - if groups := u.GetGroups(); len(groups) > 0 { - di.Groups = make([]string, len(groups)) - copy(di.Groups, groups) - sort.Strings(di.Groups) - } - - if extra := u.GetExtra(); len(extra) > 0 { - di.Extra = make(map[string][]string, len(extra)) - for k, vs := range extra { - vdupe := make([]string, len(vs)) - copy(vdupe, vs) - sort.Strings(vdupe) - di.Extra[k] = vdupe - } - } - - serializableAttributes.User = di - } - - var b strings.Builder - if err := json.NewEncoder(&b).Encode(serializableAttributes); err != nil { - return authorizer.DecisionNoOpinion, "", err - } - key := b.String() - - if cached, ok := ca.decisions[key]; ok { - return cached.authorized, cached.reason, cached.err - } - - authorized, reason, err := ca.authorizer.Authorize(ctx, a) - - ca.decisions[key] = authzResult{ - authorized: authorized, - reason: reason, - err: err, - } - - return authorized, reason, err -} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/caching_authorizer_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/caching_authorizer_test.go deleted file mode 100644 index 5831cc3febf..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/caching_authorizer_test.go +++ /dev/null @@ -1,523 +0,0 @@ -/* -Copyright 2023 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 validating - -import ( - "context" - "errors" - "fmt" - "testing" - - "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apiserver/pkg/authentication/user" - "k8s.io/apiserver/pkg/authorization/authorizer" - genericfeatures "k8s.io/apiserver/pkg/features" - utilfeature "k8s.io/apiserver/pkg/util/feature" - featuregatetesting "k8s.io/component-base/featuregate/testing" -) - -func mustParseLabelSelector(str string) labels.Requirements { - ret, err := labels.Parse(str) - if err != nil { - panic(err) - } - retRequirements, _ /*selectable*/ := ret.Requirements() - return retRequirements -} - -func TestCachingAuthorizer(t *testing.T) { - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true) - - type result struct { - decision authorizer.Decision - reason string - error error - } - - type invocation struct { - attributes authorizer.Attributes - expected result - } - - for _, tc := range []struct { - name string - calls []invocation - backend []result - }{ - { - name: "hit", - calls: []invocation{ - { - attributes: authorizer.AttributesRecord{Name: "test name"}, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - { - attributes: authorizer.AttributesRecord{Name: "test name"}, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - }, - backend: []result{ - { - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - }, - { - name: "hit with differently-ordered groups", - calls: []invocation{ - { - attributes: authorizer.AttributesRecord{ - User: &user.DefaultInfo{ - Groups: []string{"a", "b", "c"}, - }, - }, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - { - attributes: authorizer.AttributesRecord{ - User: &user.DefaultInfo{ - Groups: []string{"c", "b", "a"}, - }, - }, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - }, - backend: []result{ - { - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - }, - { - name: "hit with differently-ordered extra", - calls: []invocation{ - { - attributes: authorizer.AttributesRecord{ - User: &user.DefaultInfo{ - Extra: map[string][]string{ - "k": {"a", "b", "c"}, - }, - }, - }, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - { - attributes: authorizer.AttributesRecord{ - User: &user.DefaultInfo{ - Extra: map[string][]string{ - "k": {"c", "b", "a"}, - }, - }, - }, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - }, - backend: []result{ - { - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - }, - { - name: "miss due to different name", - calls: []invocation{ - { - attributes: authorizer.AttributesRecord{Name: "alpha"}, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason alpha", - error: fmt.Errorf("test error alpha"), - }, - }, - { - attributes: authorizer.AttributesRecord{Name: "beta"}, - expected: result{ - decision: authorizer.DecisionDeny, - reason: "test reason beta", - error: fmt.Errorf("test error beta"), - }, - }, - }, - backend: []result{ - { - decision: authorizer.DecisionAllow, - reason: "test reason alpha", - error: fmt.Errorf("test error alpha"), - }, - { - decision: authorizer.DecisionDeny, - reason: "test reason beta", - error: fmt.Errorf("test error beta"), - }, - }, - }, - { - name: "miss due to different user", - calls: []invocation{ - { - attributes: authorizer.AttributesRecord{ - User: &user.DefaultInfo{Name: "alpha"}, - }, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason alpha", - error: fmt.Errorf("test error alpha"), - }, - }, - { - attributes: authorizer.AttributesRecord{ - User: &user.DefaultInfo{Name: "beta"}, - }, - expected: result{ - decision: authorizer.DecisionDeny, - reason: "test reason beta", - error: fmt.Errorf("test error beta"), - }, - }, - }, - backend: []result{ - { - decision: authorizer.DecisionAllow, - reason: "test reason alpha", - error: fmt.Errorf("test error alpha"), - }, - { - decision: authorizer.DecisionDeny, - reason: "test reason beta", - error: fmt.Errorf("test error beta"), - }, - }, - }, - { - name: "honor good field selector", - calls: []invocation{ - { - attributes: authorizer.AttributesRecord{ - Name: "test name", - FieldSelectorRequirements: fields.ParseSelectorOrDie("foo=bar").Requirements(), - }, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - { - attributes: authorizer.AttributesRecord{ - Name: "test name", - }, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason 2", - error: fmt.Errorf("test error 2"), - }, - }, - { - // now this should be cached - attributes: authorizer.AttributesRecord{ - Name: "test name", - FieldSelectorRequirements: fields.ParseSelectorOrDie("foo=bar").Requirements(), - }, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - }, - backend: []result{ - { - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - { - decision: authorizer.DecisionAllow, - reason: "test reason 2", - error: fmt.Errorf("test error 2"), - }, - }, - }, - { - name: "ignore malformed field selector first", - calls: []invocation{ - { - attributes: authorizer.AttributesRecord{ - Name: "test name", - FieldSelectorParsingErr: errors.New("malformed"), - }, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - { - // notice that this does not have the malformed field selector. - // it should use the cached result - attributes: authorizer.AttributesRecord{ - Name: "test name", - }, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - }, - backend: []result{ - { - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - }, - { - name: "ignore malformed field selector second", - calls: []invocation{ - { - attributes: authorizer.AttributesRecord{ - Name: "test name", - }, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - { - // this should use the broader cached value because the selector will be ignored - attributes: authorizer.AttributesRecord{ - Name: "test name", - FieldSelectorParsingErr: errors.New("malformed"), - }, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - }, - backend: []result{ - { - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - }, - - { - name: "honor good label selector", - calls: []invocation{ - { - attributes: authorizer.AttributesRecord{ - Name: "test name", - LabelSelectorRequirements: mustParseLabelSelector("foo=bar"), - }, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - { - attributes: authorizer.AttributesRecord{ - Name: "test name", - }, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason 2", - error: fmt.Errorf("test error 2"), - }, - }, - { - // now this should be cached - attributes: authorizer.AttributesRecord{ - Name: "test name", - LabelSelectorRequirements: mustParseLabelSelector("foo=bar"), - }, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - { - attributes: authorizer.AttributesRecord{ - Name: "test name", - LabelSelectorRequirements: mustParseLabelSelector("diff=zero"), - }, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason 3", - error: fmt.Errorf("test error 3"), - }, - }, - }, - backend: []result{ - { - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - { - decision: authorizer.DecisionAllow, - reason: "test reason 2", - error: fmt.Errorf("test error 2"), - }, - { - decision: authorizer.DecisionAllow, - reason: "test reason 3", - error: fmt.Errorf("test error 3"), - }, - }, - }, - { - name: "ignore malformed label selector first", - calls: []invocation{ - { - attributes: authorizer.AttributesRecord{ - Name: "test name", - LabelSelectorParsingErr: errors.New("malformed mess"), - }, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - { - // notice that this does not have the malformed field selector. - // it should use the cached result - attributes: authorizer.AttributesRecord{ - Name: "test name", - }, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - }, - backend: []result{ - { - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - }, - { - name: "ignore malformed label selector second", - calls: []invocation{ - { - attributes: authorizer.AttributesRecord{ - Name: "test name", - }, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - { - // this should use the broader cached value because the selector will be ignored - attributes: authorizer.AttributesRecord{ - Name: "test name", - LabelSelectorParsingErr: errors.New("malformed mess"), - }, - expected: result{ - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - }, - backend: []result{ - { - decision: authorizer.DecisionAllow, - reason: "test reason", - error: fmt.Errorf("test error"), - }, - }, - }, - } { - t.Run(tc.name, func(t *testing.T) { - var misses int - 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)) - } - result := tc.backend[misses] - misses++ - return result.decision, result.reason, result.error - }) - }()) - - for i, invocation := range tc.calls { - decision, reason, err := frontend.Authorize(context.TODO(), invocation.attributes) - if decision != invocation.expected.decision { - t.Errorf("(call %d of %d) expected decision %v, got %v", i+1, len(tc.calls), invocation.expected.decision, decision) - } - if reason != invocation.expected.reason { - t.Errorf("(call %d of %d) expected reason %q, got %q", i+1, len(tc.calls), invocation.expected.reason, reason) - } - if err.Error() != invocation.expected.error.Error() { - t.Errorf("(call %d of %d) expected error %q, got %q", i+1, len(tc.calls), invocation.expected.error.Error(), err.Error()) - } - } - - if len(tc.backend) > misses { - t.Errorf("expected %d backend invocations, got %d", len(tc.backend), misses) - } - }) - } -} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator.go index b11f2e8f4eb..02ef81141d0 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator.go @@ -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, diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator_test.go index 44d79e60f88..eb5704da5c7 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator_test.go @@ -41,7 +41,7 @@ import ( "k8s.io/apiserver/pkg/cel/environment" ) -var _ cel.Filter = &fakeCelFilter{} +var _ cel.ConditionEvaluator = &fakeCelFilter{} type fakeCelFilter struct { evaluations []cel.EvaluationResult @@ -932,8 +932,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}, From 1031e07e8325fc043d741b697c197d5416bfcc98 Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Fri, 25 Oct 2024 14:37:39 -0400 Subject: [PATCH 13/16] Fix comment in mutating webhook dispatcher --- .../pkg/admission/plugin/webhook/mutating/dispatcher.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/dispatcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/dispatcher.go index 71328de7f7f..77fac3c1141 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/dispatcher.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/dispatcher.go @@ -190,7 +190,7 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib admissionmetrics.Metrics.ObserveWebhook(ctx, hook.Name, time.Since(t), rejected, versionedAttr.Attributes, "admit", 200) } if changed { - // Patch had changed the object. Prepare to reinvoke all previous webhooks that are eligible for re-invocation. + // Patch had changed the object. Prepare to reinvoke all previous mutations that are eligible for re-invocation. webhookReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins() reinvokeCtx.SetShouldReinvoke() } From 0cb90973b0274d7782306a0ebc9d880ac99d2a3b Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Fri, 25 Oct 2024 18:42:54 -0400 Subject: [PATCH 14/16] Add authz test for deny when relevant fields are not changed --- .../authz_test.go | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/authz_test.go b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/authz_test.go index b8229782d13..f7fe0e390b6 100644 --- a/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/authz_test.go +++ b/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/authz_test.go @@ -100,6 +100,28 @@ func TestAuthorization(t *testing.T) { }, 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}}, From 0dfbc85cd9f7c0578ed385928e6990a496112e6c Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Fri, 25 Oct 2024 18:44:10 -0400 Subject: [PATCH 15/16] Rename dispatcher Run to Start to match naming conventions --- .../pkg/admission/plugin/policy/generic/interfaces.go | 4 ++-- .../apiserver/pkg/admission/plugin/policy/generic/plugin.go | 2 +- .../pkg/admission/plugin/policy/generic/policy_dispatcher.go | 4 ++-- .../pkg/admission/plugin/policy/generic/policy_source_test.go | 3 ++- .../pkg/admission/plugin/policy/mutating/dispatcher.go | 4 ++-- .../pkg/admission/plugin/policy/validating/dispatcher.go | 2 +- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/interfaces.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/interfaces.go index 58f4a374dca..29e6eee6731 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/interfaces.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/interfaces.go @@ -49,8 +49,8 @@ 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 { - // Run the dispatcher. This method should be called only once at startup. - Run(ctx context.Context) error + // 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 diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/plugin.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/plugin.go index fa1f851892b..03aebdd58ac 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/plugin.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/plugin.go @@ -186,7 +186,7 @@ func (c *Plugin[H]) ValidateInitialization() error { } }() - err := c.dispatcher.Run(pluginContext) + err := c.dispatcher.Start(pluginContext) if err != nil && !errors.Is(err, context.Canceled) { utilruntime.HandleError(fmt.Errorf("policy dispatcher context unexpectedly closed: %w", err)) } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_dispatcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_dispatcher.go index 6586f486b94..c520d38203c 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_dispatcher.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_dispatcher.go @@ -95,12 +95,12 @@ func NewPolicyDispatcher[P runtime.Object, B runtime.Object, E Evaluator]( } } -// Dispatch implements generic.Dispatcher. It loops through all active hooks +// Start implements generic.Dispatcher Start. It loops through all active hooks // (policy x binding pairs) and selects those which are active for the current // 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]) Run(ctx context.Context) error { +func (d *policyDispatcher[P, B, E]) Start(ctx context.Context) error { return nil } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_source_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_source_test.go index 6f95ae25715..d67d5402fc9 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_source_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_source_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "k8s.io/api/admissionregistration/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -38,7 +39,7 @@ 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) Run(context.Context) error { +func (fd *fakeDispatcher) Start(context.Context) error { return nil } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/dispatcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/dispatcher.go index 0d93661c863..d8d0953c22e 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/dispatcher.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/dispatcher.go @@ -63,9 +63,9 @@ type dispatcher struct { generic.Dispatcher[PolicyHook] } -func (d *dispatcher) Run(ctx context.Context) error { +func (d *dispatcher) Start(ctx context.Context) error { go d.typeConverterManager.Run(ctx) - return d.Dispatcher.Run(ctx) + return d.Dispatcher.Start(ctx) } func (d *dispatcher) dispatchInvocations( diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/dispatcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/dispatcher.go index 5d47c94a25d..8f3e22f64dc 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/dispatcher.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/dispatcher.go @@ -64,7 +64,7 @@ type policyDecisionWithMetadata struct { Binding *admissionregistrationv1.ValidatingAdmissionPolicyBinding } -func (c *dispatcher) Run(ctx context.Context) error { +func (c *dispatcher) Start(ctx context.Context) error { return nil } From 0dc08eded95c2d620de70648dee07254f2e771b3 Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Mon, 4 Nov 2024 10:50:53 -0500 Subject: [PATCH 16/16] Reorganize and expand unit test coverage Also apply reviewer feedback --- .../validation/validation.go | 6 +- .../storage/storage_test.go | 4 +- .../policy/generic/policy_dispatcher.go | 4 +- .../plugin/policy/mutating/compilation.go | 4 +- .../policy/mutating/compilation_test.go | 638 +---------------- .../plugin/policy/mutating/dispatcher.go | 8 +- .../plugin/policy/mutating/dispatcher_test.go | 675 ++++++++++++++++++ .../plugin/policy/mutating/interface.go | 60 -- .../plugin/policy/mutating/patch/interface.go | 2 + .../policy/mutating/patch/json_patch.go | 19 + .../policy/mutating/patch/json_patch_test.go | 485 +++++++++++++ .../plugin/policy/mutating/patch/smd.go | 23 + .../plugin/policy/mutating/patch/smd_test.go | 375 ++++++++++ .../mutating/patch/typeconverter_test.go | 100 +++ .../plugin/policy/mutating/plugin.go | 2 +- .../plugin/policy/mutating/plugin_test.go | 62 ++ .../policy/mutating/reinvocationcontext.go | 2 +- .../mutating/reinvocationcontext_test.go | 147 ++++ .../plugin/policy/validating/typechecking.go | 2 +- .../pkg/cel/mutation/dynamic/objects.go | 4 +- .../apiserver/pkg/cel/mutation/jsonpatch.go | 11 +- 21 files changed, 1940 insertions(+), 693 deletions(-) create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/dispatcher_test.go delete mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/interface.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/smd_test.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/typeconverter_test.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/reinvocationcontext_test.go diff --git a/pkg/apis/admissionregistration/validation/validation.go b/pkg/apis/admissionregistration/validation/validation.go index 0fbf8833252..4953104033a 100644 --- a/pkg/apis/admissionregistration/validation/validation.go +++ b/pkg/apis/admissionregistration/validation/validation.go @@ -33,7 +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" + "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" @@ -1493,7 +1493,7 @@ func validateApplyConfiguration(compiler plugincel.Compiler, applyConfig *admiss if opts.preexistingExpressions.applyConfigurationExpressions.Has(applyConfig.Expression) { envType = environment.StoredExpressions } - accessor := &mutating.ApplyConfigurationCondition{ + accessor := &patch.ApplyConfigurationCondition{ Expression: trimmedExpression, } opts := plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true, StrictCost: true, HasPatchTypes: true} @@ -1516,7 +1516,7 @@ func validateJSONPatch(compiler plugincel.Compiler, jsonPatch *admissionregistra if opts.preexistingExpressions.applyConfigurationExpressions.Has(jsonPatch.Expression) { envType = environment.StoredExpressions } - accessor := &mutating.JSONPatchCondition{ + accessor := &patch.JSONPatchCondition{ Expression: trimmedExpression, } opts := plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true, StrictCost: true, HasPatchTypes: true} diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage_test.go b/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage_test.go index 98112f765bd..0b0046339bd 100644 --- a/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage_test.go +++ b/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage_test.go @@ -202,7 +202,7 @@ func newValidatingAdmissionPolicy(name string) *admissionregistration.Validating } func newInsecureStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) { - return newStorage(t, nil, replicaLimitsResolver) + return newStorage(t, nil, resolver.ResourceResolverFunc(replicaLimitsResolver)) } func newStorage(t *testing.T, authorizer authorizer.Authorizer, resourceResolver resolver.ResourceResolver) (*REST, *etcd3testing.EtcdTestServer) { @@ -227,7 +227,7 @@ func TestCategories(t *testing.T) { registrytest.AssertCategories(t, storage, expected) } -var replicaLimitsResolver resolver.ResourceResolverFunc = func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { +func replicaLimitsResolver(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { return schema.GroupVersionResource{ Group: "rules.example.com", Version: "v1", diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_dispatcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_dispatcher.go index c520d38203c..62214a3092e 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_dispatcher.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_dispatcher.go @@ -95,7 +95,7 @@ func NewPolicyDispatcher[P runtime.Object, B runtime.Object, E Evaluator]( } } -// Start implements generic.Dispatcher Start. It loops through all active hooks +// Dispatch implements generic.Dispatcher. It loops through all active hooks // (policy x binding pairs) and selects those which are active for the current // request. It then resolves all params and creates an Invocation for each // matching policy-binding-param tuple. The delegate is then called with the @@ -413,5 +413,5 @@ func (c PolicyError) Error() string { return fmt.Sprintf("policy '%s' with binding '%s' denied request: %s", c.Policy.GetName(), c.Binding.GetName(), c.Message.Error()) } - return fmt.Sprintf("policy '%s' denied request: %s", c.Policy.GetName(), c.Message.Error()) + return fmt.Sprintf("policy %q denied request: %s", c.Policy.GetName(), c.Message.Error()) } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/compilation.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/compilation.go index 54d5938dd62..710b8ef1ea4 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/compilation.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/compilation.go @@ -64,13 +64,13 @@ func compilePolicy(policy *Policy) PolicyEvaluator { switch m.PatchType { case v1alpha1.PatchTypeJSONPatch: if m.JSONPatch != nil { - accessor := &JSONPatchCondition{Expression: m.JSONPatch.Expression} + 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 := &ApplyConfigurationCondition{Expression: m.ApplyConfiguration.Expression} + accessor := &patch.ApplyConfigurationCondition{Expression: m.ApplyConfiguration.Expression} compileResult := compiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions) patchers = append(patchers, patch.NewApplyConfigurationPatcher(compileResult)) } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/compilation_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/compilation_test.go index c9de4a343eb..ef0859d3a6e 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/compilation_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/compilation_test.go @@ -21,16 +21,17 @@ import ( "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" - "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" "k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch" @@ -45,6 +46,7 @@ import ( // 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 @@ -56,429 +58,6 @@ func TestCompilation(t *testing.T) { expectedErr string expectedResult runtime.Object }{ - { - name: "jsonPatch with false test operation", - policy: jsonPatches(policy("d1"), - v1alpha1.JSONPatch{ - 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", - policy: jsonPatches(policy("d1"), - v1alpha1.JSONPatch{ - 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", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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 - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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 - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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 replace end of list with - not allowed", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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", - }, - { - name: "jsonPatch replace 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: "jsonPatch with CEL initializer", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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 add map entry by key and value", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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", - policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ - 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: "applyConfiguration then jsonPatch", policy: mutations(policy("d1"), v1alpha1.Mutation{ @@ -529,92 +108,15 @@ func TestCompilation(t *testing.T) { expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](111)}}, }, { - name: "apply configuration add to listType=map", - policy: applyConfigurations(policy("d1"), - `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", - policy: applyConfigurations(policy("d1"), - `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", - policy: applyConfigurations(policy("d1"), ` - 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", - policy: applyConfigurations(policy("d1"), - `Object{ - spec: Object.spec{ - replicas: oldObject.spec.replicas % 2 == 0?oldObject.spec.replicas + 1:oldObject.spec.replicas - } - }`), + 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)}}, - oldObject: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](2)}}, - expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](3)}}, + expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](11)}}, }, { name: "apply configuration with variable", @@ -641,95 +143,6 @@ func TestCompilation(t *testing.T) { object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](100)}}, }, - { - name: "complex apply configuration initialization", - policy: applyConfigurations(policy("d1"), - `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 invalid type name", - policy: applyConfigurations(policy("d1"), - `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", - policy: applyConfigurations(policy("d1"), - `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", - policy: applyConfigurations(policy("d1"), - `"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", - policy: applyConfigurations(policy("d1"), - `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", - }, { 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{ @@ -793,20 +206,6 @@ func TestCompilation(t *testing.T) { object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](10)}}, }, - { - name: "apply configuration with change to atomic", - policy: applyConfigurations(policy("d1"), - `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: "object type has field access", policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{ @@ -901,10 +300,18 @@ func TestCompilation(t *testing.T) { } ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + 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 @@ -939,7 +346,7 @@ func TestCompilation(t *testing.T) { for _, patcher := range policyEvaluator.Mutators { attrs := admission.NewAttributesRecord(obj, tc.oldObject, gvk, - metaAccessor.GetName(), metaAccessor.GetNamespace(), tc.gvr, + metaAccessor.GetNamespace(), metaAccessor.GetName(), tc.gvr, "", admission.Create, &metav1.CreateOptions{}, false, nil) vAttrs := &admission.VersionedAttributes{ Attributes: attrs, @@ -1038,6 +445,11 @@ func mutations(policy *v1alpha1.MutatingAdmissionPolicy, mutations ...v1alpha1.M 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) { diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/dispatcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/dispatcher.go index d8d0953c22e..832682552b7 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/dispatcher.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/dispatcher.go @@ -42,9 +42,8 @@ import ( func NewDispatcher(a authorizer.Authorizer, m *matching.Matcher, tcm patch.TypeConverterManager) generic.Dispatcher[PolicyHook] { res := &dispatcher{ - matcher: m, - authz: a, - //!TODO: pass in static type converter to reduce network calls + matcher: m, + authz: a, typeConverterManager: tcm, } res.Dispatcher = generic.NewPolicyDispatcher[*Policy, *PolicyBinding, PolicyEvaluator]( @@ -138,7 +137,7 @@ func (d *dispatcher) dispatchInvocations( // 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", - invocation.Policy.Name, len(invocation.Policy.Spec.Mutations), len(invocation.Evaluator.Mutators))) + len(invocation.Policy.Spec.Mutations), invocation.Policy.Name, len(invocation.Evaluator.Mutators))) } versionedAttr, err := versionedAttributes.VersionedAttribute(invocation.Kind) @@ -152,6 +151,7 @@ func (d *dispatcher) dispatchInvocations( 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 diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/dispatcher_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/dispatcher_test.go new file mode 100644 index 00000000000..443e2879a6d --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/dispatcher_test.go @@ -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{} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/interface.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/interface.go deleted file mode 100644 index bf196461b0f..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/interface.go +++ /dev/null @@ -1,60 +0,0 @@ -/* -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 ( - celgo "github.com/google/cel-go/cel" - celtypes "github.com/google/cel-go/common/types" - - "k8s.io/apiserver/pkg/admission/plugin/cel" -) - -var _ cel.ExpressionAccessor = &ApplyConfigurationCondition{} - -// ApplyConfigurationCondition contains the inputs needed to compile and evaluate a cel expression -// that returns an apply configuration -type ApplyConfigurationCondition struct { - Expression string -} - -func (v *ApplyConfigurationCondition) GetExpression() string { - return v.Expression -} - -func (v *ApplyConfigurationCondition) ReturnTypes() []*celgo.Type { - return []*celgo.Type{applyConfigObjectType} -} - -var applyConfigObjectType = celtypes.NewObjectType("Object") - -var _ cel.ExpressionAccessor = &JSONPatchCondition{} - -// JSONPatchCondition contains the inputs needed to compile and evaluate a cel expression -// that returns a JSON patch value. -type JSONPatchCondition struct { - Expression string -} - -func (v *JSONPatchCondition) GetExpression() string { - return v.Expression -} - -func (v *JSONPatchCondition) ReturnTypes() []*celgo.Type { - return []*celgo.Type{celgo.ListType(jsonPatchType)} -} - -var jsonPatchType = celtypes.NewObjectType("JSONPatch") diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/interface.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/interface.go index d00d9837316..d717adc2997 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/interface.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/interface.go @@ -29,6 +29,8 @@ import ( // 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) } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch.go index b5cf919ef16..26f73dd3415 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch.go @@ -21,6 +21,7 @@ import ( gojson "encoding/json" "errors" "fmt" + celgo "github.com/google/cel-go/cel" "reflect" "strconv" @@ -41,6 +42,24 @@ import ( 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} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch_test.go index 58c4a55b838..c01b2d3cf50 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/json_patch_test.go @@ -1 +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)) + } + }) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/smd.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/smd.go index d0e5ca1fa05..cb078b77753 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/smd.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/smd.go @@ -20,6 +20,8 @@ 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" @@ -35,6 +37,24 @@ import ( "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} @@ -147,6 +167,9 @@ func ApplyStructuredMergeDiff( // 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 { diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/smd_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/smd_test.go new file mode 100644 index 00000000000..5fdae77a468 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/smd_test.go @@ -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)) + } + }) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/typeconverter_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/typeconverter_test.go new file mode 100644 index 00000000000..37bc18e9c7a --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch/typeconverter_test.go @@ -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)) + } + }) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin.go index fa84539efe3..527bc6a53c0 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin.go @@ -53,7 +53,6 @@ func Register(plugins *admission.Plugins) { }) } -// Plugin is an implementation of admission.Interface. type Policy = v1alpha1.MutatingAdmissionPolicy type PolicyBinding = v1alpha1.MutatingAdmissionPolicyBinding type PolicyMutation = v1alpha1.Mutation @@ -80,6 +79,7 @@ type PolicyEvaluator struct { Error error } +// Plugin is an implementation of admission.Interface. type Plugin struct { *generic.Plugin[PolicyHook] } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin_test.go index 884e7dec1e9..9d7652fd04d 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/plugin_test.go @@ -119,6 +119,68 @@ func TestBasicPatch(t *testing.T) { 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{}{ diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/reinvocationcontext.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/reinvocationcontext.go index 4ba030c283d..764ce392789 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/reinvocationcontext.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/reinvocationcontext.go @@ -1,5 +1,5 @@ /* -Copyright 2019 The Kubernetes Authors. +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. diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/reinvocationcontext_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/reinvocationcontext_test.go new file mode 100644 index 00000000000..bc85bb56b34 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/mutating/reinvocationcontext_test.go @@ -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)) + +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking.go index aa5a0f29407..192be9621bd 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking.go @@ -436,7 +436,7 @@ func buildEnvSet(hasParams bool, hasAuthorizer bool, types typeOverwrite) (*envi ) } -// createVariableOpts creates a slice of ResolverEnvOption +// createVariableOpts creates a slice of EnvOption // that can be used for creating a CEL env containing variables of declType. // declType can be nil, in which case the variables will be of DynType. func createVariableOpts(declType *apiservercel.DeclType, variables ...string) []cel.EnvOption { diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects.go index b00db23de4e..8dd38281b0c 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects.go @@ -235,11 +235,11 @@ func convertField(value ref.Val) (any, error) { // unstructured maps, as seen in annotations // map keys must be strings if mapOfVal, ok := value.Value().(map[ref.Val]ref.Val); ok { - result := make(map[string]any) + result := make(map[string]any, len(mapOfVal)) for k, v := range mapOfVal { stringKey, ok := k.Value().(string) if !ok { - return nil, fmt.Errorf("map key %q is of type %t, not string", k, k) + return nil, fmt.Errorf("map key %q is of type %T, not string", k, k) } result[stringKey] = v.Value() } diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/jsonpatch.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/jsonpatch.go index 1cb018db957..1f6d63fa22e 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/jsonpatch.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/mutation/jsonpatch.go @@ -121,14 +121,21 @@ func (p *JSONPatchVal) ConvertToType(typeValue ref.Type) ref.Val { } else if typeValue == types.TypeType { return types.NewTypeTypeWithParam(jsonPatchType) } - return types.NewErr("Unsupported type: %s", typeValue.TypeName()) + return types.NewErr("unsupported type: %s", typeValue.TypeName()) } func (p *JSONPatchVal) Equal(other ref.Val) ref.Val { if o, ok := other.(*JSONPatchVal); ok && p != nil && o != nil { - if *p == *o { + if p.Op != o.Op || p.From != o.From || p.Path != o.Path { + return types.False + } + if (p.Val == nil) != (o.Val == nil) { + return types.False + } + if p.Val == nil { return types.True } + return p.Val.Equal(o.Val) } return types.False }