mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-07-16 23:11:52 -04:00
Exclude auth/authz virtual resources from admission webhooks
This commit is contained in:
parent
26ae7380e6
commit
73ad530e91
14 changed files with 757 additions and 35 deletions
87
pkg/apis/admissionregistration/validation/warnings.go
Normal file
87
pkg/apis/admissionregistration/validation/warnings.go
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
)
|
||||
|
||||
// WarningsForValidatingWebhookRules returns warnings for rules in a ValidatingWebhookConfiguration
|
||||
// that explicitly name a resource in excludedResources (virtual resources that admission webhooks
|
||||
// do not intercept). The excluded set is supplied by the caller to avoid depending on server-side
|
||||
// packages from here.
|
||||
func WarningsForValidatingWebhookRules(webhooks []admissionregistration.ValidatingWebhook, excludedResources []schema.GroupResource) []string {
|
||||
excluded := sets.New(excludedResources...)
|
||||
webhooksPath := field.NewPath("webhooks")
|
||||
var warnings []string
|
||||
for i := range webhooks {
|
||||
warnings = append(warnings, warningsForExcludedRules(webhooksPath.Index(i).Child("rules"), webhooks[i].Rules, excluded)...)
|
||||
}
|
||||
return warnings
|
||||
}
|
||||
|
||||
// WarningsForMutatingWebhookRules returns warnings for rules in a MutatingWebhookConfiguration
|
||||
// that explicitly name a resource in excludedResources.
|
||||
func WarningsForMutatingWebhookRules(webhooks []admissionregistration.MutatingWebhook, excludedResources []schema.GroupResource) []string {
|
||||
excluded := sets.New(excludedResources...)
|
||||
webhooksPath := field.NewPath("webhooks")
|
||||
var warnings []string
|
||||
for i := range webhooks {
|
||||
warnings = append(warnings, warningsForExcludedRules(webhooksPath.Index(i).Child("rules"), webhooks[i].Rules, excluded)...)
|
||||
}
|
||||
return warnings
|
||||
}
|
||||
|
||||
func warningsForExcludedRules(rulesPath *field.Path, rules []admissionregistration.RuleWithOperations, excluded sets.Set[schema.GroupResource]) []string {
|
||||
var warnings []string
|
||||
for i := range rules {
|
||||
for _, gr := range excludedResourcesNamedByRule(rules[i].APIGroups, rules[i].Resources, excluded) {
|
||||
warnings = append(warnings, fmt.Sprintf("%s: %s is excluded from admission webhooks; this rule will have no effect", rulesPath.Index(i), gr))
|
||||
}
|
||||
}
|
||||
return warnings
|
||||
}
|
||||
|
||||
// excludedResourcesNamedByRule returns the excluded GroupResources a rule explicitly names.
|
||||
// A rule that uses a wildcard ("*") in apiGroups or resources is not flagged because its intent
|
||||
// toward the excluded resource is ambiguous. apiVersions are not considered because all versions
|
||||
// of an excluded resource are excluded.
|
||||
func excludedResourcesNamedByRule(apiGroups, resources []string, excluded sets.Set[schema.GroupResource]) []schema.GroupResource {
|
||||
if slices.Contains(apiGroups, "*") || slices.Contains(resources, "*") {
|
||||
return nil
|
||||
}
|
||||
// Dedupe with a set so a rule that repeats a group/resource is only reported once,
|
||||
// preserving the order the rule lists them in.
|
||||
seen := sets.New[schema.GroupResource]()
|
||||
var matched []schema.GroupResource
|
||||
for _, group := range apiGroups {
|
||||
for _, resource := range resources {
|
||||
gr := schema.GroupResource{Group: group, Resource: resource}
|
||||
if excluded.Has(gr) && !seen.Has(gr) {
|
||||
seen.Insert(gr)
|
||||
matched = append(matched, gr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return matched
|
||||
}
|
||||
110
pkg/apis/admissionregistration/validation/warnings_test.go
Normal file
110
pkg/apis/admissionregistration/validation/warnings_test.go
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
)
|
||||
|
||||
// testExcludedResources is a representative excluded set; the production set is supplied by the
|
||||
// caller (see pkg/kubeapiserver/admission/exclusion), so these helpers don't depend on it.
|
||||
var testExcludedResources = []schema.GroupResource{
|
||||
{Group: "authentication.k8s.io", Resource: "tokenreviews"},
|
||||
{Group: "authorization.k8s.io", Resource: "subjectaccessreviews"},
|
||||
}
|
||||
|
||||
func rule(groups, versions, resources []string) admissionregistration.RuleWithOperations {
|
||||
return admissionregistration.RuleWithOperations{
|
||||
Rule: admissionregistration.Rule{
|
||||
APIGroups: groups,
|
||||
APIVersions: versions,
|
||||
Resources: resources,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestWarningsForWebhookRules(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rules []admissionregistration.RuleWithOperations
|
||||
wantWarn []string
|
||||
}{
|
||||
{
|
||||
name: "no warnings for normal resources",
|
||||
rules: []admissionregistration.RuleWithOperations{rule([]string{""}, []string{"v1"}, []string{"pods", "configmaps"})},
|
||||
wantWarn: nil,
|
||||
},
|
||||
{
|
||||
name: "warning for explicit excluded resource",
|
||||
rules: []admissionregistration.RuleWithOperations{rule([]string{"authentication.k8s.io"}, []string{"v1"}, []string{"tokenreviews"})},
|
||||
wantWarn: []string{
|
||||
`webhooks[0].rules[0]: tokenreviews.authentication.k8s.io is excluded from admission webhooks; this rule will have no effect`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "warning only for the excluded resource in a mixed rule",
|
||||
rules: []admissionregistration.RuleWithOperations{rule([]string{"authorization.k8s.io"}, []string{"v1"}, []string{"subjectaccessreviews", "somethingelse"})},
|
||||
wantWarn: []string{
|
||||
`webhooks[0].rules[0]: subjectaccessreviews.authorization.k8s.io is excluded from admission webhooks; this rule will have no effect`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no warning for all wildcards",
|
||||
rules: []admissionregistration.RuleWithOperations{rule([]string{"*"}, []string{"*"}, []string{"*"})},
|
||||
wantWarn: nil,
|
||||
},
|
||||
{
|
||||
name: "no warning for wildcard apiGroups",
|
||||
rules: []admissionregistration.RuleWithOperations{rule([]string{"*"}, []string{"v1"}, []string{"tokenreviews"})},
|
||||
wantWarn: nil,
|
||||
},
|
||||
{
|
||||
name: "warning for wildcard apiVersions naming an excluded resource",
|
||||
rules: []admissionregistration.RuleWithOperations{rule([]string{"authentication.k8s.io"}, []string{"*"}, []string{"tokenreviews"})},
|
||||
wantWarn: []string{
|
||||
`webhooks[0].rules[0]: tokenreviews.authentication.k8s.io is excluded from admission webhooks; this rule will have no effect`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no warning for wildcard resources",
|
||||
rules: []admissionregistration.RuleWithOperations{rule([]string{"authentication.k8s.io"}, []string{"v1"}, []string{"*"})},
|
||||
wantWarn: nil,
|
||||
},
|
||||
{
|
||||
name: "no warning for excluded resource in wrong group",
|
||||
rules: []admissionregistration.RuleWithOperations{rule([]string{"example.com"}, []string{"v1"}, []string{"tokenreviews"})},
|
||||
wantWarn: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
validating := []admissionregistration.ValidatingWebhook{{Name: "w", Rules: tc.rules}}
|
||||
if got := WarningsForValidatingWebhookRules(validating, testExcludedResources); !reflect.DeepEqual(got, tc.wantWarn) {
|
||||
t.Errorf("WarningsForValidatingWebhookRules() = %v, want %v", got, tc.wantWarn)
|
||||
}
|
||||
mutating := []admissionregistration.MutatingWebhook{{Name: "w", Rules: tc.rules}}
|
||||
if got := WarningsForMutatingWebhookRules(mutating, testExcludedResources); !reflect.DeepEqual(got, tc.wantWarn) {
|
||||
t.Errorf("WarningsForMutatingWebhookRules() = %v, want %v", got, tc.wantWarn)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ import (
|
|||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration/validation"
|
||||
"k8s.io/kubernetes/pkg/kubeapiserver/admission/exclusion"
|
||||
)
|
||||
|
||||
// mutatingWebhookConfigurationStrategy implements verification logic for mutatingWebhookConfiguration.
|
||||
|
|
@ -54,10 +55,14 @@ func (mutatingWebhookConfigurationStrategy) PrepareForCreate(ctx context.Context
|
|||
// WarningsOnCreate returns warnings for the creation of the given object.
|
||||
func (mutatingWebhookConfigurationStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
|
||||
ic := obj.(*admissionregistration.MutatingWebhookConfiguration)
|
||||
var warnings []string
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.ManifestBasedAdmissionControlConfig) {
|
||||
return validation.WarningsForStaticSuffix(ic.Name)
|
||||
warnings = append(warnings, validation.WarningsForStaticSuffix(ic.Name)...)
|
||||
}
|
||||
return nil
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ExcludeAdmissionWebhookVirtualResources) {
|
||||
warnings = append(warnings, validation.WarningsForMutatingWebhookRules(ic.Webhooks, exclusion.Excluded())...)
|
||||
}
|
||||
return warnings
|
||||
}
|
||||
|
||||
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
|
||||
|
|
@ -103,7 +108,12 @@ func (mutatingWebhookConfigurationStrategy) ValidateUpdate(ctx context.Context,
|
|||
// WarningsOnUpdate returns warnings for the given update.
|
||||
func (mutatingWebhookConfigurationStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
|
||||
newIC := obj.(*admissionregistration.MutatingWebhookConfiguration)
|
||||
return validation.WarningsForStaticSuffix(newIC.Name)
|
||||
var warnings []string
|
||||
warnings = append(warnings, validation.WarningsForStaticSuffix(newIC.Name)...)
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ExcludeAdmissionWebhookVirtualResources) {
|
||||
warnings = append(warnings, validation.WarningsForMutatingWebhookRules(newIC.Webhooks, exclusion.Excluded())...)
|
||||
}
|
||||
return warnings
|
||||
}
|
||||
|
||||
// AllowUnconditionalUpdate is the default update policy for mutatingWebhookConfiguration objects. Status update should
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import (
|
|||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration/validation"
|
||||
"k8s.io/kubernetes/pkg/kubeapiserver/admission/exclusion"
|
||||
)
|
||||
|
||||
// validatingWebhookConfigurationStrategy implements verification logic for validatingWebhookConfiguration.
|
||||
|
|
@ -77,10 +78,14 @@ func (validatingWebhookConfigurationStrategy) Validate(ctx context.Context, obj
|
|||
// WarningsOnCreate returns warnings for the creation of the given object.
|
||||
func (validatingWebhookConfigurationStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
|
||||
ic := obj.(*admissionregistration.ValidatingWebhookConfiguration)
|
||||
var warnings []string
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.ManifestBasedAdmissionControlConfig) {
|
||||
return validation.WarningsForStaticSuffix(ic.Name)
|
||||
warnings = append(warnings, validation.WarningsForStaticSuffix(ic.Name)...)
|
||||
}
|
||||
return nil
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ExcludeAdmissionWebhookVirtualResources) {
|
||||
warnings = append(warnings, validation.WarningsForValidatingWebhookRules(ic.Webhooks, exclusion.Excluded())...)
|
||||
}
|
||||
return warnings
|
||||
}
|
||||
|
||||
// Canonicalize normalizes the object after validation.
|
||||
|
|
@ -103,7 +108,12 @@ func (validatingWebhookConfigurationStrategy) ValidateUpdate(ctx context.Context
|
|||
// WarningsOnUpdate returns warnings for the given update.
|
||||
func (validatingWebhookConfigurationStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
|
||||
newIC := obj.(*admissionregistration.ValidatingWebhookConfiguration)
|
||||
return validation.WarningsForStaticSuffix(newIC.Name)
|
||||
var warnings []string
|
||||
warnings = append(warnings, validation.WarningsForStaticSuffix(newIC.Name)...)
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ExcludeAdmissionWebhookVirtualResources) {
|
||||
warnings = append(warnings, validation.WarningsForValidatingWebhookRules(newIC.Webhooks, exclusion.Excluded())...)
|
||||
}
|
||||
return warnings
|
||||
}
|
||||
|
||||
// AllowUnconditionalUpdate is the default update policy for validatingWebhookConfiguration objects. Status update should
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ import (
|
|||
|
||||
v1 "k8s.io/api/admissionregistration/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||
"k8s.io/client-go/informers"
|
||||
|
|
@ -46,15 +48,17 @@ type mutatingWebhookConfigurationManager struct {
|
|||
// This function is defined as field instead of a struct method to allow injection
|
||||
// during tests
|
||||
createMutatingWebhookAccessor mutatingWebhookAccessorCreator
|
||||
excludedWebhookResources sets.Set[schema.GroupResource]
|
||||
}
|
||||
|
||||
var _ generic.Source = &mutatingWebhookConfigurationManager{}
|
||||
|
||||
func NewMutatingWebhookConfigurationManager(f informers.SharedInformerFactory) generic.Source {
|
||||
func NewMutatingWebhookConfigurationManager(f informers.SharedInformerFactory, excludedWebhookResources sets.Set[schema.GroupResource]) generic.Source {
|
||||
informer := f.Admissionregistration().V1().MutatingWebhookConfigurations()
|
||||
manager := &mutatingWebhookConfigurationManager{
|
||||
lister: informer.Lister(),
|
||||
createMutatingWebhookAccessor: webhook.NewMutatingWebhookAccessor,
|
||||
excludedWebhookResources: excludedWebhookResources,
|
||||
}
|
||||
manager.lazy.Evaluate = manager.getConfiguration
|
||||
|
||||
|
|
@ -133,6 +137,8 @@ func (m *mutatingWebhookConfigurationManager) getMutatingWebhookConfigurations(c
|
|||
continue
|
||||
}
|
||||
|
||||
logExcludedResourcesForMutatingWebhook(c.Name, c.Webhooks, m.excludedWebhookResources)
|
||||
|
||||
// webhook names are not validated for uniqueness, so we check for duplicates and
|
||||
// add a int suffix to distinguish between them
|
||||
names := map[string]int{}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ func testGetMutatingWebhookConfig(t *testing.T) {
|
|||
stop := make(chan struct{})
|
||||
defer close(stop)
|
||||
|
||||
manager := NewMutatingWebhookConfigurationManager(informerFactory).(*mutatingWebhookConfigurationManager)
|
||||
manager := NewMutatingWebhookConfigurationManager(informerFactory, nil).(*mutatingWebhookConfigurationManager)
|
||||
informerFactory.Start(stop)
|
||||
informerFactory.WaitForCacheSync(stop)
|
||||
|
||||
|
|
@ -212,7 +212,7 @@ func TestGetMutatingWebhookConfigSmartReload(t *testing.T) {
|
|||
informerFactory := informers.NewSharedInformerFactory(client, 0)
|
||||
stop := make(chan struct{})
|
||||
defer close(stop)
|
||||
manager := NewMutatingWebhookConfigurationManager(informerFactory)
|
||||
manager := NewMutatingWebhookConfigurationManager(informerFactory, nil)
|
||||
managerStructPtr := manager.(*mutatingWebhookConfigurationManager)
|
||||
fakeWebhookAccessorCreator := &mockCreateMutatingWebhookAccessor{}
|
||||
managerStructPtr.createMutatingWebhookAccessor = fakeWebhookAccessorCreator.fn
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ import (
|
|||
|
||||
v1 "k8s.io/api/admissionregistration/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||
"k8s.io/client-go/informers"
|
||||
|
|
@ -46,15 +48,17 @@ type validatingWebhookConfigurationManager struct {
|
|||
// This function is defined as field instead of a struct method to allow injection
|
||||
// during tests
|
||||
createValidatingWebhookAccessor validatingWebhookAccessorCreator
|
||||
excludedWebhookResources sets.Set[schema.GroupResource]
|
||||
}
|
||||
|
||||
var _ generic.Source = &validatingWebhookConfigurationManager{}
|
||||
|
||||
func NewValidatingWebhookConfigurationManager(f informers.SharedInformerFactory) generic.Source {
|
||||
func NewValidatingWebhookConfigurationManager(f informers.SharedInformerFactory, excludedWebhookResources sets.Set[schema.GroupResource]) generic.Source {
|
||||
informer := f.Admissionregistration().V1().ValidatingWebhookConfigurations()
|
||||
manager := &validatingWebhookConfigurationManager{
|
||||
lister: informer.Lister(),
|
||||
createValidatingWebhookAccessor: webhook.NewValidatingWebhookAccessor,
|
||||
excludedWebhookResources: excludedWebhookResources,
|
||||
}
|
||||
manager.lazy.Evaluate = manager.getConfiguration
|
||||
|
||||
|
|
@ -130,6 +134,8 @@ func (v *validatingWebhookConfigurationManager) getValidatingWebhookConfiguratio
|
|||
continue
|
||||
}
|
||||
|
||||
logExcludedResourcesForValidatingWebhook(c.Name, c.Webhooks, v.excludedWebhookResources)
|
||||
|
||||
// webhook names are not validated for uniqueness, so we check for duplicates and
|
||||
// add a int suffix to distinguish between them
|
||||
names := map[string]int{}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ func testGetValidatingWebhookConfig(t *testing.T) {
|
|||
stop := make(chan struct{})
|
||||
defer close(stop)
|
||||
|
||||
manager := NewValidatingWebhookConfigurationManager(informerFactory)
|
||||
manager := NewValidatingWebhookConfigurationManager(informerFactory, nil)
|
||||
informerFactory.Start(stop)
|
||||
informerFactory.WaitForCacheSync(stop)
|
||||
|
||||
|
|
@ -211,7 +211,7 @@ func TestGetValidatingWebhookConfigSmartReload(t *testing.T) {
|
|||
informerFactory := informers.NewSharedInformerFactory(client, 0)
|
||||
stop := make(chan struct{})
|
||||
defer close(stop)
|
||||
manager := NewValidatingWebhookConfigurationManager(informerFactory)
|
||||
manager := NewValidatingWebhookConfigurationManager(informerFactory, nil)
|
||||
managerStructPtr := manager.(*validatingWebhookConfigurationManager)
|
||||
fakeWebhookAccessorCreator := &mockCreateValidatingWebhookAccessor{}
|
||||
managerStructPtr.createValidatingWebhookAccessor = fakeWebhookAccessorCreator.fn
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
||||
package configuration
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
v1 "k8s.io/api/admissionregistration/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// logExcludedResourcesForValidatingWebhook logs an advisory for a ValidatingWebhookConfiguration whose rules name resources excluded from admission webhooks.
|
||||
// It is a no-op when excludedWebhookResources is empty.
|
||||
func logExcludedResourcesForValidatingWebhook(name string, webhooks []v1.ValidatingWebhook, excludedWebhookResources sets.Set[schema.GroupResource]) {
|
||||
if excludedWebhookResources.Len() == 0 {
|
||||
return
|
||||
}
|
||||
var rules []v1.RuleWithOperations
|
||||
for _, w := range webhooks {
|
||||
rules = append(rules, w.Rules...)
|
||||
}
|
||||
logExcludedWebhookResources("ValidatingWebhookConfiguration", name, rules, excludedWebhookResources)
|
||||
}
|
||||
|
||||
// logExcludedResourcesForMutatingWebhook logs an advisory for a MutatingWebhookConfiguration whose rules name resources excluded from admission webhooks.
|
||||
// It is a no-op when excludedWebhookResources is empty.
|
||||
func logExcludedResourcesForMutatingWebhook(name string, webhooks []v1.MutatingWebhook, excludedWebhookResources sets.Set[schema.GroupResource]) {
|
||||
if excludedWebhookResources.Len() == 0 {
|
||||
return
|
||||
}
|
||||
var rules []v1.RuleWithOperations
|
||||
for _, w := range webhooks {
|
||||
rules = append(rules, w.Rules...)
|
||||
}
|
||||
logExcludedWebhookResources("MutatingWebhookConfiguration", name, rules, excludedWebhookResources)
|
||||
}
|
||||
|
||||
func logExcludedWebhookResources(kind, name string, rules []v1.RuleWithOperations, excludedWebhookResources sets.Set[schema.GroupResource]) {
|
||||
excluded := sets.New[schema.GroupResource]()
|
||||
for _, r := range rules {
|
||||
excluded.Insert(excludedResourcesNamedByRule(r.APIGroups, r.Resources, excludedWebhookResources)...)
|
||||
}
|
||||
if excluded.Len() == 0 {
|
||||
return
|
||||
}
|
||||
resources := make([]string, 0, excluded.Len())
|
||||
for gr := range excluded {
|
||||
resources = append(resources, gr.String())
|
||||
}
|
||||
slices.Sort(resources)
|
||||
klog.InfoS("Admission webhook configuration names resources that are excluded from admission webhooks; the matching rules will have no effect",
|
||||
"kind", kind, "name", name, "excludedResources", resources)
|
||||
}
|
||||
|
||||
// excludedResourcesNamedByRule returns the excluded GroupResources a rule explicitly names.
|
||||
// A rule that uses a wildcard ("*") in apiGroups or resources is not flagged because its intent
|
||||
// toward the excluded resource is ambiguous. apiVersions are not considered because all versions
|
||||
// of an excluded resource are excluded.
|
||||
func excludedResourcesNamedByRule(apiGroups, resources []string, excludedWebhookResources sets.Set[schema.GroupResource]) []schema.GroupResource {
|
||||
if slices.Contains(apiGroups, "*") || slices.Contains(resources, "*") {
|
||||
return nil
|
||||
}
|
||||
var matched []schema.GroupResource
|
||||
for _, group := range apiGroups {
|
||||
for _, resource := range resources {
|
||||
gr := schema.GroupResource{Group: group, Resource: resource}
|
||||
if excludedWebhookResources.Has(gr) {
|
||||
matched = append(matched, gr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return matched
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package configuration
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
v1 "k8s.io/api/admissionregistration/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
)
|
||||
|
||||
func TestExcludedResourcesNamedByRule(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
apiGroups []string
|
||||
resources []string
|
||||
want []schema.GroupResource
|
||||
}{
|
||||
{
|
||||
name: "explicit excluded resource",
|
||||
apiGroups: []string{"authentication.k8s.io"}, resources: []string{"tokenreviews"},
|
||||
want: []schema.GroupResource{{Group: "authentication.k8s.io", Resource: "tokenreviews"}},
|
||||
},
|
||||
{
|
||||
name: "non-excluded resource",
|
||||
apiGroups: []string{""}, resources: []string{"pods"},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "wildcard group is not flagged",
|
||||
apiGroups: []string{"*"}, resources: []string{"tokenreviews"},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "wildcard resource is not flagged",
|
||||
apiGroups: []string{"authentication.k8s.io"}, resources: []string{"*"},
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
excluded := sets.New(
|
||||
schema.GroupResource{Group: "authentication.k8s.io", Resource: "tokenreviews"},
|
||||
schema.GroupResource{Group: "authorization.k8s.io", Resource: "subjectaccessreviews"},
|
||||
)
|
||||
if got := excludedResourcesNamedByRule(tc.apiGroups, tc.resources, excluded); !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("excludedResourcesNamedByRule() = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLogExcludedResourcesForWebhook is a smoke test ensuring the logging helpers run without
|
||||
// panicking when no resources are excluded.
|
||||
func TestLogExcludedResourcesForWebhook(t *testing.T) {
|
||||
logExcludedResourcesForValidatingWebhook("test-config", []v1.ValidatingWebhook{{
|
||||
Name: "test-webhook",
|
||||
Rules: []v1.RuleWithOperations{{Rule: v1.Rule{APIGroups: []string{"authentication.k8s.io"}, APIVersions: []string{"v1"}, Resources: []string{"tokenreviews"}}}},
|
||||
}}, nil)
|
||||
logExcludedResourcesForMutatingWebhook("test-config", []v1.MutatingWebhook{{
|
||||
Name: "test-mutating-webhook",
|
||||
Rules: []v1.RuleWithOperations{{Rule: v1.Rule{APIGroups: []string{"authorization.k8s.io"}, APIVersions: []string{"v1"}, Resources: []string{"subjectaccessreviews"}}}},
|
||||
}}, nil)
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ import (
|
|||
v1 "k8s.io/api/admissionregistration/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
|
||||
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
|
||||
|
|
@ -45,6 +46,7 @@ import (
|
|||
"k8s.io/client-go/informers"
|
||||
coreinformers "k8s.io/client-go/informers/core/v1"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
"k8s.io/component-base/featuregate"
|
||||
)
|
||||
|
||||
// Webhook is an abstract admission plugin with all the infrastructure to define Admit or Validate on-top.
|
||||
|
|
@ -52,6 +54,7 @@ type Webhook struct {
|
|||
*admission.Handler
|
||||
|
||||
// Factories for creating webhook sources.
|
||||
apiSourceInformers informers.SharedInformerFactory
|
||||
apiSourceFactory sourceFactory
|
||||
staticSourceFactory StaticSourceFactory
|
||||
|
||||
|
|
@ -80,16 +83,26 @@ type Webhook struct {
|
|||
|
||||
// Lifecycle.
|
||||
stopCh <-chan struct{}
|
||||
|
||||
// excludedAdmissionResources are virtual resources (auth/authz reviews) that admission
|
||||
// webhooks must not intercept when excludeVirtualResources is set. It is populated from
|
||||
// the authoritative exclusion list via SetExcludedAdmissionResources.
|
||||
excludedAdmissionResources sets.Set[schema.GroupResource]
|
||||
// excludeVirtualResources caches whether the ExcludeAdmissionWebhookVirtualResources
|
||||
// feature is enabled, set once via InspectFeatureGates to avoid a gate lookup per request.
|
||||
excludeVirtualResources bool
|
||||
}
|
||||
|
||||
var (
|
||||
_ genericadmissioninit.WantsExternalKubeClientSet = &Webhook{}
|
||||
_ genericadmissioninit.WantsDrainedNotification = &Webhook{}
|
||||
_ genericadmissioninit.WantsAPIServerID = &Webhook{}
|
||||
_ admission.Interface = &Webhook{}
|
||||
_ genericadmissioninit.WantsExternalKubeClientSet = &Webhook{}
|
||||
_ genericadmissioninit.WantsDrainedNotification = &Webhook{}
|
||||
_ genericadmissioninit.WantsAPIServerID = &Webhook{}
|
||||
_ genericadmissioninit.WantsExcludedAdmissionResources = &Webhook{}
|
||||
_ genericadmissioninit.WantsFeatures = &Webhook{}
|
||||
_ admission.Interface = &Webhook{}
|
||||
)
|
||||
|
||||
type sourceFactory func(f informers.SharedInformerFactory) Source
|
||||
type sourceFactory func(f informers.SharedInformerFactory, excludedWebhookResources sets.Set[schema.GroupResource]) Source
|
||||
type dispatcherFactory func(cm *webhookutil.ClientManager) Dispatcher
|
||||
|
||||
// ReloadableSource extends Source with a method to run a reload loop
|
||||
|
|
@ -132,14 +145,15 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory
|
|||
cm.SetServiceResolver(webhookutil.NewDefaultServiceResolver())
|
||||
|
||||
return &Webhook{
|
||||
Handler: handler,
|
||||
apiSourceFactory: sourceFactory,
|
||||
staticManifestsDir: cfg.StaticManifestsDir,
|
||||
clientManager: &cm,
|
||||
namespaceMatcher: &namespace.Matcher{},
|
||||
objectMatcher: &object.Matcher{},
|
||||
dispatcher: dispatcherFactory(&cm),
|
||||
filterCompiler: cel.NewConditionCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())),
|
||||
Handler: handler,
|
||||
apiSourceFactory: sourceFactory,
|
||||
staticManifestsDir: cfg.StaticManifestsDir,
|
||||
clientManager: &cm,
|
||||
namespaceMatcher: &namespace.Matcher{},
|
||||
objectMatcher: &object.Matcher{},
|
||||
dispatcher: dispatcherFactory(&cm),
|
||||
filterCompiler: cel.NewConditionCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())),
|
||||
excludedAdmissionResources: sets.New[schema.GroupResource](),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -179,6 +193,18 @@ func (a *Webhook) SetDrainedNotification(stopCh <-chan struct{}) {
|
|||
a.stopCh = stopCh
|
||||
}
|
||||
|
||||
// SetExcludedAdmissionResources implements the WantsExcludedAdmissionResources interface.
|
||||
func (a *Webhook) SetExcludedAdmissionResources(excludedResources []schema.GroupResource) {
|
||||
a.excludedAdmissionResources.Insert(excludedResources...)
|
||||
}
|
||||
|
||||
// InspectFeatureGates implements the WantsFeatures interface. It caches whether the
|
||||
// ExcludeAdmissionWebhookVirtualResources feature is enabled so Dispatch avoids a gate
|
||||
// lookup on every request.
|
||||
func (a *Webhook) InspectFeatureGates(featureGates featuregate.FeatureGate) {
|
||||
a.excludeVirtualResources = featureGates.Enabled(features.ExcludeAdmissionWebhookVirtualResources)
|
||||
}
|
||||
|
||||
// SetExternalKubeClientSet implements the WantsExternalKubeInformerFactory interface.
|
||||
// It sets external ClientSet for admission plugins that need it
|
||||
func (a *Webhook) SetExternalKubeClientSet(client clientset.Interface) {
|
||||
|
|
@ -191,8 +217,11 @@ func (a *Webhook) SetExternalKubeInformerFactory(f informers.SharedInformerFacto
|
|||
a.namespaceMatcher.NamespaceLister = namespaceInformer.Lister()
|
||||
a.namespaceInformer = namespaceInformer
|
||||
|
||||
// Create the API-based source (stored for later use in ValidateInitialization)
|
||||
a.apiSource = a.apiSourceFactory(f)
|
||||
// Stored for constructing apiSource in ValidateInitialization (after all initializers have
|
||||
// run, so the excluded resource set is available). Reset apiSource so that re-setting the
|
||||
// informer factory rebuilds it from the new factory.
|
||||
a.apiSourceInformers = f
|
||||
a.apiSource = nil
|
||||
}
|
||||
|
||||
func (a *Webhook) SetUnconditionalAuthorizer(authorizer authorizer.UnconditionalAuthorizer) {
|
||||
|
|
@ -200,12 +229,21 @@ func (a *Webhook) SetUnconditionalAuthorizer(authorizer authorizer.Unconditional
|
|||
}
|
||||
|
||||
// ValidateInitialization implements the InitializationValidator interface.
|
||||
// Static source creation happens here (after all initializers have run) because
|
||||
// SetManifestLoaders may be called after SetExternalKubeInformerFactory.
|
||||
// API and static source creation happens here (after all initializers have run) because
|
||||
// initialization setter order is not guaranteed.
|
||||
func (a *Webhook) ValidateInitialization() error {
|
||||
// Construct apiSource if needed
|
||||
if a.apiSource == nil {
|
||||
return fmt.Errorf("kubernetes client is not properly setup")
|
||||
if a.apiSourceInformers == nil {
|
||||
return fmt.Errorf("kubernetes client is not properly setup")
|
||||
}
|
||||
if a.excludeVirtualResources {
|
||||
a.apiSource = a.apiSourceFactory(a.apiSourceInformers, a.excludedAdmissionResources)
|
||||
} else {
|
||||
a.apiSource = a.apiSourceFactory(a.apiSourceInformers, nil)
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.namespaceMatcher.Validate(); err != nil {
|
||||
return fmt.Errorf("namespaceMatcher is not properly setup: %v", err)
|
||||
}
|
||||
|
|
@ -353,12 +391,31 @@ type attrWithResourceOverride struct {
|
|||
|
||||
func (a *attrWithResourceOverride) GetResource() schema.GroupVersionResource { return a.resource }
|
||||
|
||||
// isExcludedFromAllHooks returns true for non-persisted virtual resources (auth/authz reviews)
|
||||
// that must not be intercepted by any webhook, static or REST-based. It returns false (no-op)
|
||||
// when the ExcludeAdmissionWebhookVirtualResources feature is disabled.
|
||||
func (a *Webhook) isExcludedFromAllHooks(gr schema.GroupResource) bool {
|
||||
return a.excludeVirtualResources && a.excludedAdmissionResources.Has(gr)
|
||||
}
|
||||
|
||||
// isExcludedFromAPIHooks returns true for admission configuration resources (webhook
|
||||
// configurations and admission policies/bindings) that are excluded from API-based webhooks to
|
||||
// prevent circular dependencies, but may still be evaluated by static (manifest-based) webhooks.
|
||||
func (a *Webhook) isExcludedFromAPIHooks(attr admission.Attributes) bool {
|
||||
return rules.IsExemptAdmissionConfigurationResource(attr)
|
||||
}
|
||||
|
||||
// Dispatch is called by the downstream Validate or Admit methods.
|
||||
func (a *Webhook) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error {
|
||||
if rules.IsExemptAdmissionConfigurationResource(attr) {
|
||||
// Admission config resources are excluded from API-based webhooks to
|
||||
// prevent circular dependencies. However, static (manifest-based) webhooks
|
||||
// are safe to evaluate since they don't have self-referential concerns.
|
||||
if a.isExcludedFromAllHooks(attr.GetResource().GroupResource()) {
|
||||
// Virtual auth/authz resources are excluded from all webhooks (static and REST-based)
|
||||
// so that admission cannot wedge a cluster out of its own auth path.
|
||||
return nil
|
||||
}
|
||||
if a.isExcludedFromAPIHooks(attr) {
|
||||
// Admission config resources are excluded from API-based webhooks to prevent circular
|
||||
// dependencies. However, static (manifest-based) webhooks are safe to evaluate since
|
||||
// they don't have self-referential concerns.
|
||||
if a.staticSource != nil {
|
||||
if !a.staticSource.HasSynced() {
|
||||
return admission.NewForbidden(attr, fmt.Errorf("not yet ready to handle request"))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package mutating
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
registrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts"
|
||||
webhooktesting "k8s.io/apiserver/pkg/admission/plugin/webhook/testing"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
"k8s.io/component-base/featuregate"
|
||||
)
|
||||
|
||||
// TestExcludeVirtualResources verifies the MutatingAdmissionWebhook plugin implements the
|
||||
// WantsExcludedAdmissionResources initializer and, when ExcludeAdmissionWebhookVirtualResources is
|
||||
// enabled, skips dispatch for excluded resources (so a rejecting webhook is never called); when the
|
||||
// gate is disabled it dispatches as before. The production excluded set is auth/authz virtual
|
||||
// resources; here a core resource is marked excluded so the mechanism can be exercised with the
|
||||
// fixtures the webhook test harness supports.
|
||||
func TestExcludeVirtualResources(t *testing.T) {
|
||||
testServer := webhooktesting.NewTestServer(t)
|
||||
testServer.StartTLS()
|
||||
defer testServer.Close()
|
||||
serverURL, err := url.ParseRequestURI(testServer.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stopCh := make(chan struct{})
|
||||
defer close(stopCh)
|
||||
|
||||
fail := registrationv1.Fail
|
||||
none := registrationv1.SideEffectClassNone
|
||||
path := "disallow"
|
||||
webhooks := []registrationv1.MutatingWebhook{{
|
||||
Name: "test-webhook.k8s.io",
|
||||
Rules: []registrationv1.RuleWithOperations{{
|
||||
Operations: []registrationv1.OperationType{registrationv1.OperationAll},
|
||||
Rule: registrationv1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
|
||||
}},
|
||||
ClientConfig: registrationv1.WebhookClientConfig{
|
||||
Service: ®istrationv1.ServiceReference{Name: "webhook-test", Namespace: "default", Path: &path},
|
||||
CABundle: testcerts.CACert,
|
||||
},
|
||||
FailurePolicy: &fail,
|
||||
SideEffects: &none,
|
||||
// Empty (non-nil) selectors match everything; nil would parse to "matches nothing".
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
AdmissionReviewVersions: []string{"v1"},
|
||||
}}
|
||||
|
||||
wh, err := NewMutatingWebhook(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create mutating webhook: %v", err)
|
||||
}
|
||||
|
||||
// WantsExcludedAdmissionResources wiring. NewAttribute issues a request for the core "pod"
|
||||
// resource, so mark that excluded.
|
||||
wants, ok := interface{}(wh).(initializer.WantsExcludedAdmissionResources)
|
||||
if !ok {
|
||||
t.Fatal("plugin does not implement WantsExcludedAdmissionResources")
|
||||
}
|
||||
wants.SetExcludedAdmissionResources([]schema.GroupResource{{Group: "", Resource: "pod"}})
|
||||
|
||||
wantsFeatures, ok := interface{}(wh).(initializer.WantsFeatures)
|
||||
if !ok {
|
||||
t.Fatal("plugin does not implement WantsFeatures")
|
||||
}
|
||||
|
||||
client, informer := webhooktesting.NewFakeMutatingDataSource("default", webhooks, stopCh)
|
||||
wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32))))
|
||||
wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL))
|
||||
wh.SetExternalKubeClientSet(client)
|
||||
wh.SetExternalKubeInformerFactory(informer)
|
||||
if err := wh.ValidateInitialization(); err != nil {
|
||||
t.Fatalf("failed to validate initialization: %v", err)
|
||||
}
|
||||
|
||||
informer.Start(stopCh)
|
||||
informer.WaitForCacheSync(stopCh)
|
||||
|
||||
attr := webhooktesting.NewAttribute("default", nil, false)
|
||||
o := webhooktesting.NewObjectInterfacesForTest()
|
||||
|
||||
// Gate enabled: the excluded resource is skipped, so the rejecting webhook is never called.
|
||||
wantsFeatures.InspectFeatureGates(newExcludeGate(t, true))
|
||||
if err := wh.Admit(context.TODO(), attr, o); err != nil {
|
||||
t.Errorf("with gate enabled, expected the excluded resource to be skipped (nil), got: %v", err)
|
||||
}
|
||||
|
||||
// Gate disabled: the webhook is dispatched and rejects the request.
|
||||
wantsFeatures.InspectFeatureGates(newExcludeGate(t, false))
|
||||
if err := wh.Admit(context.TODO(), attr, o); err == nil {
|
||||
t.Error("with gate disabled, expected the webhook to reject the request, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func newExcludeGate(t *testing.T, enabled bool) featuregate.FeatureGate {
|
||||
t.Helper()
|
||||
gate := featuregate.NewFeatureGate()
|
||||
if err := gate.Add(map[featuregate.Feature]featuregate.FeatureSpec{
|
||||
features.ExcludeAdmissionWebhookVirtualResources: {Default: enabled, PreRelease: featuregate.Beta},
|
||||
}); err != nil {
|
||||
t.Fatalf("failed to build feature gate: %v", err)
|
||||
}
|
||||
return gate
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package validating
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
registrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts"
|
||||
webhooktesting "k8s.io/apiserver/pkg/admission/plugin/webhook/testing"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
"k8s.io/component-base/featuregate"
|
||||
)
|
||||
|
||||
// TestExcludeVirtualResources verifies the ValidatingAdmissionWebhook plugin implements the
|
||||
// WantsExcludedAdmissionResources initializer and, when ExcludeAdmissionWebhookVirtualResources is
|
||||
// enabled, skips dispatch for excluded resources (so a rejecting webhook is never called); when the
|
||||
// gate is disabled it dispatches as before. The production excluded set is auth/authz virtual
|
||||
// resources; here a core resource is marked excluded so the mechanism can be exercised with the
|
||||
// fixtures the webhook test harness supports.
|
||||
func TestExcludeVirtualResources(t *testing.T) {
|
||||
testServer := webhooktesting.NewTestServer(t)
|
||||
testServer.StartTLS()
|
||||
defer testServer.Close()
|
||||
serverURL, err := url.ParseRequestURI(testServer.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stopCh := make(chan struct{})
|
||||
defer close(stopCh)
|
||||
|
||||
fail := registrationv1.Fail
|
||||
none := registrationv1.SideEffectClassNone
|
||||
path := "disallow"
|
||||
webhooks := []registrationv1.ValidatingWebhook{{
|
||||
Name: "test-webhook.k8s.io",
|
||||
Rules: []registrationv1.RuleWithOperations{{
|
||||
Operations: []registrationv1.OperationType{registrationv1.OperationAll},
|
||||
Rule: registrationv1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
|
||||
}},
|
||||
ClientConfig: registrationv1.WebhookClientConfig{
|
||||
Service: ®istrationv1.ServiceReference{Name: "webhook-test", Namespace: "default", Path: &path},
|
||||
CABundle: testcerts.CACert,
|
||||
},
|
||||
FailurePolicy: &fail,
|
||||
SideEffects: &none,
|
||||
// Empty (non-nil) selectors match everything; nil would parse to "matches nothing".
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
AdmissionReviewVersions: []string{"v1"},
|
||||
}}
|
||||
|
||||
wh, err := NewValidatingAdmissionWebhook(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create validating webhook: %v", err)
|
||||
}
|
||||
|
||||
// WantsExcludedAdmissionResources wiring. NewAttribute issues a request for the core "pod"
|
||||
// resource, so mark that excluded.
|
||||
wants, ok := interface{}(wh).(initializer.WantsExcludedAdmissionResources)
|
||||
if !ok {
|
||||
t.Fatal("plugin does not implement WantsExcludedAdmissionResources")
|
||||
}
|
||||
wants.SetExcludedAdmissionResources([]schema.GroupResource{{Group: "", Resource: "pod"}})
|
||||
|
||||
wantsFeatures, ok := interface{}(wh).(initializer.WantsFeatures)
|
||||
if !ok {
|
||||
t.Fatal("plugin does not implement WantsFeatures")
|
||||
}
|
||||
|
||||
client, informer := webhooktesting.NewFakeValidatingDataSource("default", webhooks, stopCh)
|
||||
wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32))))
|
||||
wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL))
|
||||
wh.SetExternalKubeClientSet(client)
|
||||
wh.SetExternalKubeInformerFactory(informer)
|
||||
if err := wh.ValidateInitialization(); err != nil {
|
||||
t.Fatalf("failed to validate initialization: %v", err)
|
||||
}
|
||||
|
||||
informer.Start(stopCh)
|
||||
informer.WaitForCacheSync(stopCh)
|
||||
|
||||
attr := webhooktesting.NewAttribute("default", nil, false)
|
||||
o := webhooktesting.NewObjectInterfacesForTest()
|
||||
|
||||
// Gate enabled: the excluded resource is skipped, so the rejecting webhook is never called.
|
||||
wantsFeatures.InspectFeatureGates(newExcludeGate(t, true))
|
||||
if err := wh.Validate(context.TODO(), attr, o); err != nil {
|
||||
t.Errorf("with gate enabled, expected the excluded resource to be skipped (nil), got: %v", err)
|
||||
}
|
||||
|
||||
// Gate disabled: the webhook is dispatched and rejects the request.
|
||||
wantsFeatures.InspectFeatureGates(newExcludeGate(t, false))
|
||||
if err := wh.Validate(context.TODO(), attr, o); err == nil {
|
||||
t.Error("with gate disabled, expected the webhook to reject the request, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func newExcludeGate(t *testing.T, enabled bool) featuregate.FeatureGate {
|
||||
t.Helper()
|
||||
gate := featuregate.NewFeatureGate()
|
||||
if err := gate.Add(map[featuregate.Feature]featuregate.FeatureSpec{
|
||||
features.ExcludeAdmissionWebhookVirtualResources: {Default: enabled, PreRelease: featuregate.Beta},
|
||||
}); err != nil {
|
||||
t.Fatalf("failed to build feature gate: %v", err)
|
||||
}
|
||||
return gate
|
||||
}
|
||||
|
|
@ -63,6 +63,7 @@ import (
|
|||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
apisv1beta1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1beta1"
|
||||
"k8s.io/kubernetes/pkg/kubeapiserver/admission/exclusion"
|
||||
"k8s.io/kubernetes/test/integration/etcd"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
|
@ -161,6 +162,10 @@ var (
|
|||
gvr("admissionregistration.k8s.io", "v1", "mutatingadmissionpolicybindings"): true,
|
||||
}
|
||||
|
||||
// webhookExcludedResources are the virtual resources admission webhooks skip when
|
||||
// ExcludeAdmissionWebhookVirtualResources is enabled (the same set as VAP/MAP).
|
||||
webhookExcludedResources = sets.New(exclusion.Excluded()...)
|
||||
|
||||
parentResources = map[schema.GroupVersionResource]schema.GroupVersionResource{
|
||||
gvr("extensions", "v1beta1", "replicationcontrollers/scale"): gvr("", "v1", "replicationcontrollers"),
|
||||
}
|
||||
|
|
@ -361,7 +366,13 @@ func (h *holder) verifyRequest(webhookOptions webhookOptions, request *admission
|
|||
converted := webhookOptions.converted
|
||||
|
||||
// Check if current resource should be exempted from Admission processing
|
||||
if admissionExemptResources[gvr(h.recordGVR.Group, h.recordGVR.Version, h.recordGVR.Resource)] {
|
||||
gvrKey := gvr(h.recordGVR.Group, h.recordGVR.Version, h.recordGVR.Resource)
|
||||
isExempt := admissionExemptResources[gvrKey]
|
||||
if !isExempt && utilfeature.DefaultFeatureGate.Enabled(features.ExcludeAdmissionWebhookVirtualResources) &&
|
||||
webhookExcludedResources.Has(gvrKey.GroupResource()) {
|
||||
isExempt = true
|
||||
}
|
||||
if isExempt {
|
||||
if request == nil {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue