mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-12 13:31:52 +00:00
add resource filter to admission initializer.
This commit is contained in:
parent
b0ee334374
commit
5b1fffa3e4
@ -29,13 +29,14 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
webhookinit "k8s.io/apiserver/pkg/admission/plugin/webhook/initializer"
|
webhookinit "k8s.io/apiserver/pkg/admission/plugin/webhook/initializer"
|
||||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||||
egressselector "k8s.io/apiserver/pkg/server/egressselector"
|
"k8s.io/apiserver/pkg/server/egressselector"
|
||||||
"k8s.io/apiserver/pkg/util/webhook"
|
"k8s.io/apiserver/pkg/util/webhook"
|
||||||
cacheddiscovery "k8s.io/client-go/discovery/cached/memory"
|
cacheddiscovery "k8s.io/client-go/discovery/cached/memory"
|
||||||
externalinformers "k8s.io/client-go/informers"
|
externalinformers "k8s.io/client-go/informers"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
"k8s.io/client-go/restmapper"
|
"k8s.io/client-go/restmapper"
|
||||||
|
"k8s.io/kubernetes/pkg/kubeapiserver/admission/exclusion"
|
||||||
quotainstall "k8s.io/kubernetes/pkg/quota/v1/install"
|
quotainstall "k8s.io/kubernetes/pkg/quota/v1/install"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -69,6 +70,7 @@ func (c *Config) New(proxyTransport *http.Transport, egressSelector *egressselec
|
|||||||
cloudConfig,
|
cloudConfig,
|
||||||
discoveryRESTMapper,
|
discoveryRESTMapper,
|
||||||
quotainstall.NewQuotaConfigurationForAdmission(),
|
quotainstall.NewQuotaConfigurationForAdmission(),
|
||||||
|
exclusion.NewFilter(),
|
||||||
)
|
)
|
||||||
|
|
||||||
admissionPostStartHook := func(context genericapiserver.PostStartHookContext) error {
|
admissionPostStartHook := func(context genericapiserver.PostStartHookContext) error {
|
||||||
|
42
pkg/kubeapiserver/admission/exclusion/filter.go
Normal file
42
pkg/kubeapiserver/admission/exclusion/filter.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
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 exclusion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
"k8s.io/apiserver/pkg/admission"
|
||||||
|
"k8s.io/apiserver/pkg/admission/resourcefilter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewFilter creates a resource filter with the built-in exclusion list.
|
||||||
|
func NewFilter() resourcefilter.Interface {
|
||||||
|
return &filter{excluded: sets.New[schema.GroupResource](excluded...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
type filter struct {
|
||||||
|
excluded sets.Set[schema.GroupResource]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *filter) ShouldHandle(a admission.Attributes) bool {
|
||||||
|
gvr := a.GetResource()
|
||||||
|
// ignore the version for the decision-making
|
||||||
|
// because putting different versions into different category
|
||||||
|
// is almost always a mistake.
|
||||||
|
gr := gvr.GroupResource()
|
||||||
|
return !f.excluded.Has(gr)
|
||||||
|
}
|
66
pkg/kubeapiserver/admission/exclusion/resources.go
Normal file
66
pkg/kubeapiserver/admission/exclusion/resources.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
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 exclusion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// include is the list of resources that the expression-based admission controllers
|
||||||
|
// should intercept.
|
||||||
|
// The version is omitted, all versions of the same GroupResource are treated the same.
|
||||||
|
// If a resource is transient, i.e., not persisted in the storage, the resource must be
|
||||||
|
// in either include or excluded list.
|
||||||
|
var included = []schema.GroupResource{
|
||||||
|
{Group: "", Resource: "bindings"},
|
||||||
|
{Group: "", Resource: "pods/attach"},
|
||||||
|
{Group: "", Resource: "pods/binding"},
|
||||||
|
{Group: "", Resource: "pods/eviction"},
|
||||||
|
{Group: "", Resource: "pods/exec"},
|
||||||
|
{Group: "", Resource: "pods/portforward"},
|
||||||
|
|
||||||
|
// ref: https://github.com/kubernetes/kubernetes/issues/122205#issuecomment-1927390823
|
||||||
|
{Group: "", Resource: "serviceaccounts/token"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// excluded is the list of resources that the expression-based admission controllers
|
||||||
|
// should ignore.
|
||||||
|
// The version is omitted, all versions of the same GroupResource are treated the same.
|
||||||
|
var excluded = []schema.GroupResource{
|
||||||
|
// BEGIN interception of these non-persisted resources may break the cluster
|
||||||
|
{Group: "authentication.k8s.io", Resource: "selfsubjectreviews"},
|
||||||
|
{Group: "authentication.k8s.io", Resource: "tokenreviews"},
|
||||||
|
{Group: "authorization.k8s.io", Resource: "localsubjectaccessreviews"},
|
||||||
|
{Group: "authorization.k8s.io", Resource: "selfsubjectaccessreviews"},
|
||||||
|
{Group: "authorization.k8s.io", Resource: "selfsubjectrulesreviews"},
|
||||||
|
{Group: "authorization.k8s.io", Resource: "subjectaccessreviews"},
|
||||||
|
// END interception of these non-persisted resources may break the cluster
|
||||||
|
}
|
||||||
|
|
||||||
|
// Included returns a copy of the list of resources that the expression-based admission controllers
|
||||||
|
// should intercept.
|
||||||
|
func Included() []schema.GroupResource {
|
||||||
|
return slices.Clone(included)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Excluded returns a copy of the list of resources that the expression-based admission controllers
|
||||||
|
// should ignore.
|
||||||
|
func Excluded() []schema.GroupResource {
|
||||||
|
return slices.Clone(excluded)
|
||||||
|
}
|
@ -20,6 +20,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/admission/initializer"
|
"k8s.io/apiserver/pkg/admission/initializer"
|
||||||
|
"k8s.io/apiserver/pkg/admission/resourcefilter"
|
||||||
quota "k8s.io/apiserver/pkg/quota/v1"
|
quota "k8s.io/apiserver/pkg/quota/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -35,6 +36,7 @@ type PluginInitializer struct {
|
|||||||
cloudConfig []byte
|
cloudConfig []byte
|
||||||
restMapper meta.RESTMapper
|
restMapper meta.RESTMapper
|
||||||
quotaConfiguration quota.Configuration
|
quotaConfiguration quota.Configuration
|
||||||
|
resourceFilter resourcefilter.Interface
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ admission.PluginInitializer = &PluginInitializer{}
|
var _ admission.PluginInitializer = &PluginInitializer{}
|
||||||
@ -46,11 +48,13 @@ func NewPluginInitializer(
|
|||||||
cloudConfig []byte,
|
cloudConfig []byte,
|
||||||
restMapper meta.RESTMapper,
|
restMapper meta.RESTMapper,
|
||||||
quotaConfiguration quota.Configuration,
|
quotaConfiguration quota.Configuration,
|
||||||
|
resourceFilter resourcefilter.Interface,
|
||||||
) *PluginInitializer {
|
) *PluginInitializer {
|
||||||
return &PluginInitializer{
|
return &PluginInitializer{
|
||||||
cloudConfig: cloudConfig,
|
cloudConfig: cloudConfig,
|
||||||
restMapper: restMapper,
|
restMapper: restMapper,
|
||||||
quotaConfiguration: quotaConfiguration,
|
quotaConfiguration: quotaConfiguration,
|
||||||
|
resourceFilter: resourceFilter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,4 +72,8 @@ func (i *PluginInitializer) Initialize(plugin admission.Interface) {
|
|||||||
if wants, ok := plugin.(initializer.WantsQuotaConfiguration); ok {
|
if wants, ok := plugin.(initializer.WantsQuotaConfiguration); ok {
|
||||||
wants.SetQuotaConfiguration(i.quotaConfiguration)
|
wants.SetQuotaConfiguration(i.quotaConfiguration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if wants, ok := plugin.(initializer.WantsResourceFilter); ok {
|
||||||
|
wants.SetResourceFilter(i.resourceFilter)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
quota "k8s.io/apiserver/pkg/quota/v1"
|
quota "k8s.io/apiserver/pkg/quota/v1"
|
||||||
|
"k8s.io/kubernetes/pkg/kubeapiserver/admission/exclusion"
|
||||||
)
|
)
|
||||||
|
|
||||||
type doNothingAdmission struct{}
|
type doNothingAdmission struct{}
|
||||||
@ -49,7 +50,7 @@ func (p *WantsCloudConfigAdmissionPlugin) SetCloudConfig(cloudConfig []byte) {
|
|||||||
|
|
||||||
func TestCloudConfigAdmissionPlugin(t *testing.T) {
|
func TestCloudConfigAdmissionPlugin(t *testing.T) {
|
||||||
cloudConfig := []byte("cloud-configuration")
|
cloudConfig := []byte("cloud-configuration")
|
||||||
initializer := NewPluginInitializer(cloudConfig, nil, nil)
|
initializer := NewPluginInitializer(cloudConfig, nil, nil, exclusion.NewFilter())
|
||||||
wantsCloudConfigAdmission := &WantsCloudConfigAdmissionPlugin{}
|
wantsCloudConfigAdmission := &WantsCloudConfigAdmissionPlugin{}
|
||||||
initializer.Initialize(wantsCloudConfigAdmission)
|
initializer.Initialize(wantsCloudConfigAdmission)
|
||||||
|
|
||||||
@ -94,7 +95,7 @@ func (p *WantsRESTMapperAdmissionPlugin) SetRESTMapper(mapper meta.RESTMapper) {
|
|||||||
|
|
||||||
func TestRESTMapperAdmissionPlugin(t *testing.T) {
|
func TestRESTMapperAdmissionPlugin(t *testing.T) {
|
||||||
mapper := doNothingRESTMapper{}
|
mapper := doNothingRESTMapper{}
|
||||||
initializer := NewPluginInitializer(nil, mapper, nil)
|
initializer := NewPluginInitializer(nil, mapper, nil, exclusion.NewFilter())
|
||||||
wantsRESTMapperAdmission := &WantsRESTMapperAdmissionPlugin{}
|
wantsRESTMapperAdmission := &WantsRESTMapperAdmissionPlugin{}
|
||||||
initializer.Initialize(wantsRESTMapperAdmission)
|
initializer.Initialize(wantsRESTMapperAdmission)
|
||||||
|
|
||||||
@ -121,7 +122,7 @@ func (p *WantsQuotaConfigurationAdmissionPlugin) SetQuotaConfiguration(config qu
|
|||||||
|
|
||||||
func TestQuotaConfigurationAdmissionPlugin(t *testing.T) {
|
func TestQuotaConfigurationAdmissionPlugin(t *testing.T) {
|
||||||
config := doNothingQuotaConfiguration{}
|
config := doNothingQuotaConfiguration{}
|
||||||
initializer := NewPluginInitializer(nil, nil, config)
|
initializer := NewPluginInitializer(nil, nil, config, exclusion.NewFilter())
|
||||||
wantsQuotaConfigurationAdmission := &WantsQuotaConfigurationAdmissionPlugin{}
|
wantsQuotaConfigurationAdmission := &WantsQuotaConfigurationAdmissionPlugin{}
|
||||||
initializer.Initialize(wantsQuotaConfigurationAdmission)
|
initializer.Initialize(wantsQuotaConfigurationAdmission)
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ package initializer
|
|||||||
import (
|
import (
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
|
"k8s.io/apiserver/pkg/admission/resourcefilter"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/apiserver/pkg/cel/openapi/resolver"
|
"k8s.io/apiserver/pkg/cel/openapi/resolver"
|
||||||
quota "k8s.io/apiserver/pkg/quota/v1"
|
quota "k8s.io/apiserver/pkg/quota/v1"
|
||||||
@ -89,3 +90,10 @@ type WantsSchemaResolver interface {
|
|||||||
SetSchemaResolver(resolver resolver.SchemaResolver)
|
SetSchemaResolver(resolver resolver.SchemaResolver)
|
||||||
admission.InitializationValidator
|
admission.InitializationValidator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WantsResourceFilter defines a function which sets the ResourceFilter for
|
||||||
|
// an admission plugin that needs it.
|
||||||
|
type WantsResourceFilter interface {
|
||||||
|
SetResourceFilter(filter resourcefilter.Interface)
|
||||||
|
admission.InitializationValidator
|
||||||
|
}
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
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 resourcefilter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apiserver/pkg/admission"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface is a resource filter that takes an Attributes and
|
||||||
|
// check if it should be handled or ignored by the admission plugin.
|
||||||
|
type Interface interface {
|
||||||
|
// ShouldHandle returns true if the admission plugin should handle the request,
|
||||||
|
// considering the given Attributes, or false otherwise.
|
||||||
|
ShouldHandle(admission.Attributes) bool
|
||||||
|
}
|
106
test/integration/apiserver/cel/excludedresources_test.go
Normal file
106
test/integration/apiserver/cel/excludedresources_test.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
"k8s.io/client-go/discovery"
|
||||||
|
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||||
|
"k8s.io/kubernetes/pkg/kubeapiserver/admission/exclusion"
|
||||||
|
"k8s.io/kubernetes/test/integration/framework"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestExcludedResources is an open-box test to ensure that a resource that is not persisted
|
||||||
|
// is either in the allow-list or block-list of the CEL admission.
|
||||||
|
//
|
||||||
|
// see staging/src/k8s.io/apiserver/pkg/admission/exclusion/exclusion.go
|
||||||
|
// for the lists that the test is about.
|
||||||
|
func TestExcludedResources(t *testing.T) {
|
||||||
|
server, err := apiservertesting.StartTestServer(t,
|
||||||
|
nil, // no extra options
|
||||||
|
// enable all APIs to cover all defined resources
|
||||||
|
// note that it does not require feature flags enabled to
|
||||||
|
// discovery GVRs of disabled features.
|
||||||
|
[]string{"--runtime-config=api/all=true"},
|
||||||
|
framework.SharedEtcd())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer server.TearDownFn()
|
||||||
|
|
||||||
|
config := server.ClientConfig
|
||||||
|
|
||||||
|
discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, resourceLists, _, err := discoveryClient.GroupsAndMaybeResources()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
interestedGRs := sets.New[schema.GroupResource]()
|
||||||
|
interestedVerbCombinations := []metav1.Verbs{
|
||||||
|
{"create"},
|
||||||
|
{"create", "get"},
|
||||||
|
}
|
||||||
|
for _, rl := range resourceLists {
|
||||||
|
for _, r := range rl.APIResources {
|
||||||
|
slices.Sort(r.Verbs)
|
||||||
|
for _, c := range interestedVerbCombinations {
|
||||||
|
if slices.Equal(r.Verbs, c) {
|
||||||
|
gv, err := schema.ParseGroupVersion(rl.GroupVersion)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("internal error: cannot parse GV from %q: %v", rl.GroupVersion, err)
|
||||||
|
}
|
||||||
|
interestedGRs.Insert(gv.WithResource(r.Name).GroupResource())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
existing := sets.New[schema.GroupResource]()
|
||||||
|
existing.Insert(exclusion.Included()...)
|
||||||
|
existing.Insert(exclusion.Excluded()...)
|
||||||
|
shouldAdd, shouldRemove := interestedGRs.Difference(existing), existing.Difference(interestedGRs)
|
||||||
|
if shouldAdd.Len() > 0 {
|
||||||
|
t.Errorf("the following resources should either be in Included or Excluded in\n"+
|
||||||
|
"pkg/kubeapiserver/admission/exclusion/resources.go\n%s",
|
||||||
|
formatGRs(shouldAdd.UnsortedList()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if shouldRemove.Len() > 0 {
|
||||||
|
t.Errorf("the following resources are in pkg/kubeapiserver/admission/exclusion/resources.go\n"+
|
||||||
|
"but does not seem to be transient.\n%s",
|
||||||
|
formatGRs(shouldRemove.UnsortedList()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatGRs(grs []schema.GroupResource) string {
|
||||||
|
lines := make([]string, 0, len(grs))
|
||||||
|
for _, gvr := range grs {
|
||||||
|
item := fmt.Sprintf("%#v,", gvr)
|
||||||
|
lines = append(lines, item)
|
||||||
|
}
|
||||||
|
slices.Sort(lines)
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user