mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 11:50:44 +00:00
Merge pull request #55132 from caesarxuchao/webhook-move-shared-code
Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. Reorganize admission webhook code ref: https://github.com/kubernetes/features/issues/492 * Moved client and kubeconfig related code to webhook/config; * Moved the rule matcher to webhook/rules; * Left TODOs saying we are going to move some other common utilities; * Other code is moved to webhook/validation. This is to prepare adding the mutating webhook. See https://github.com/kubernetes/kubernetes/pull/54892.
This commit is contained in:
commit
ff7934fdee
@ -59,7 +59,7 @@ go_library(
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/server:go_default_library",
|
||||
|
@ -44,7 +44,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
utilwait "k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||
webhookconfig "k8s.io/apiserver/pkg/admission/plugin/webhook/config"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
@ -446,8 +446,8 @@ func BuildGenericConfig(s *options.ServerRunOptions, proxyTransport *http.Transp
|
||||
genericConfig.DisabledPostStartHooks.Insert(rbacrest.PostStartHookName)
|
||||
}
|
||||
|
||||
webhookAuthResolver := func(delegate webhook.AuthenticationInfoResolver) webhook.AuthenticationInfoResolver {
|
||||
return webhook.AuthenticationInfoResolverFunc(func(server string) (*rest.Config, error) {
|
||||
webhookAuthResolver := func(delegate webhookconfig.AuthenticationInfoResolver) webhookconfig.AuthenticationInfoResolver {
|
||||
return webhookconfig.AuthenticationInfoResolverFunc(func(server string) (*rest.Config, error) {
|
||||
if server == "kubernetes.default.svc" {
|
||||
return genericConfig.LoopbackClientConfig, nil
|
||||
}
|
||||
@ -486,7 +486,7 @@ func BuildGenericConfig(s *options.ServerRunOptions, proxyTransport *http.Transp
|
||||
}
|
||||
|
||||
// BuildAdmissionPluginInitializer constructs the admission plugin initializer
|
||||
func BuildAdmissionPluginInitializer(s *options.ServerRunOptions, client internalclientset.Interface, sharedInformers informers.SharedInformerFactory, serviceResolver aggregatorapiserver.ServiceResolver, webhookAuthWrapper webhook.AuthenticationInfoResolverWrapper) (admission.PluginInitializer, error) {
|
||||
func BuildAdmissionPluginInitializer(s *options.ServerRunOptions, client internalclientset.Interface, sharedInformers informers.SharedInformerFactory, serviceResolver aggregatorapiserver.ServiceResolver, webhookAuthWrapper webhookconfig.AuthenticationInfoResolverWrapper) (admission.PluginInitializer, error) {
|
||||
var cloudConfig []byte
|
||||
|
||||
if s.CloudProvider.CloudConfigFile != "" {
|
||||
|
@ -542,7 +542,8 @@ staging/src/k8s.io/apiserver/pkg/admission
|
||||
staging/src/k8s.io/apiserver/pkg/admission/configuration
|
||||
staging/src/k8s.io/apiserver/pkg/admission/plugin/initialization
|
||||
staging/src/k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle
|
||||
staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook
|
||||
staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config
|
||||
staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating
|
||||
staging/src/k8s.io/apiserver/pkg/apis/apiserver
|
||||
staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1
|
||||
staging/src/k8s.io/apiserver/pkg/apis/audit
|
||||
|
@ -13,7 +13,7 @@ go_test(
|
||||
library = ":go_default_library",
|
||||
deps = [
|
||||
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
@ -27,7 +27,7 @@ go_library(
|
||||
"//pkg/quota:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
|
||||
],
|
||||
|
@ -21,7 +21,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/config"
|
||||
)
|
||||
|
||||
type doNothingAdmission struct{}
|
||||
@ -61,7 +61,7 @@ type serviceWanter struct {
|
||||
got ServiceResolver
|
||||
}
|
||||
|
||||
func (s *serviceWanter) SetServiceResolver(sr webhook.ServiceResolver) { s.got = sr }
|
||||
func (s *serviceWanter) SetServiceResolver(sr config.ServiceResolver) { s.got = sr }
|
||||
|
||||
func TestWantsServiceResolver(t *testing.T) {
|
||||
sw := &serviceWanter{}
|
||||
|
@ -21,7 +21,7 @@ import (
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||
webhookconfig "k8s.io/apiserver/pkg/admission/plugin/webhook/config"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
|
||||
@ -62,7 +62,7 @@ type WantsQuotaConfiguration interface {
|
||||
// WantsServiceResolver defines a fuction that accepts a ServiceResolver for
|
||||
// admission plugins that need to make calls to services.
|
||||
type WantsServiceResolver interface {
|
||||
SetServiceResolver(webhook.ServiceResolver)
|
||||
SetServiceResolver(webhookconfig.ServiceResolver)
|
||||
}
|
||||
|
||||
// ServiceResolver knows how to convert a service reference into an actual
|
||||
@ -74,7 +74,7 @@ type ServiceResolver interface {
|
||||
// WantsAuthenticationInfoResolverWrapper defines a function that wraps the standard AuthenticationInfoResolver
|
||||
// to allow the apiserver to control what is returned as auth info
|
||||
type WantsAuthenticationInfoResolverWrapper interface {
|
||||
SetAuthenticationInfoResolverWrapper(webhook.AuthenticationInfoResolverWrapper)
|
||||
SetAuthenticationInfoResolverWrapper(webhookconfig.AuthenticationInfoResolverWrapper)
|
||||
admission.InitializationValidator
|
||||
}
|
||||
|
||||
@ -86,8 +86,8 @@ type PluginInitializer struct {
|
||||
cloudConfig []byte
|
||||
restMapper meta.RESTMapper
|
||||
quotaConfiguration quota.Configuration
|
||||
serviceResolver webhook.ServiceResolver
|
||||
authenticationInfoResolverWrapper webhook.AuthenticationInfoResolverWrapper
|
||||
serviceResolver webhookconfig.ServiceResolver
|
||||
authenticationInfoResolverWrapper webhookconfig.AuthenticationInfoResolverWrapper
|
||||
}
|
||||
|
||||
var _ admission.PluginInitializer = &PluginInitializer{}
|
||||
@ -101,8 +101,8 @@ func NewPluginInitializer(
|
||||
cloudConfig []byte,
|
||||
restMapper meta.RESTMapper,
|
||||
quotaConfiguration quota.Configuration,
|
||||
authenticationInfoResolverWrapper webhook.AuthenticationInfoResolverWrapper,
|
||||
serviceResolver webhook.ServiceResolver,
|
||||
authenticationInfoResolverWrapper webhookconfig.AuthenticationInfoResolverWrapper,
|
||||
serviceResolver webhookconfig.ServiceResolver,
|
||||
) *PluginInitializer {
|
||||
return &PluginInitializer{
|
||||
internalClient: internalClient,
|
||||
|
@ -875,7 +875,15 @@
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook",
|
||||
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/config",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/rules",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/validating",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
|
@ -78,7 +78,9 @@ filegroup(
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/initializer:all-srcs",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/initialization:all-srcs",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle:all-srcs",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook:all-srcs",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config:all-srcs",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules:all-srcs",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
@ -0,0 +1,54 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"authentication.go",
|
||||
"client.go",
|
||||
"errors.go",
|
||||
"kubeconfig.go",
|
||||
"serviceresolver.go",
|
||||
],
|
||||
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/config",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//vendor/github.com/hashicorp/golang-lru:go_default_library",
|
||||
"//vendor/k8s.io/api/admissionregistration/v1alpha1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/yaml:go_default_library",
|
||||
"//vendor/k8s.io/client-go/rest:go_default_library",
|
||||
"//vendor/k8s.io/client-go/tools/clientcmd:go_default_library",
|
||||
"//vendor/k8s.io/client-go/tools/clientcmd/api:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"authentication_test.go",
|
||||
"serviceresolver_test.go",
|
||||
],
|
||||
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/config",
|
||||
library = ":go_default_library",
|
||||
deps = [
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/diff:go_default_library",
|
||||
"//vendor/k8s.io/client-go/rest:go_default_library",
|
||||
"//vendor/k8s.io/client-go/tools/clientcmd/api:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webhook
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -27,14 +27,19 @@ import (
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
// AuthenticationInfoResolverWrapper can be used to inject Dial function to the
|
||||
// rest.Config generated by the resolver.
|
||||
type AuthenticationInfoResolverWrapper func(AuthenticationInfoResolver) AuthenticationInfoResolver
|
||||
|
||||
// AuthenticationInfoResolver builds rest.Config base on the server name.
|
||||
type AuthenticationInfoResolver interface {
|
||||
ClientConfigFor(server string) (*rest.Config, error)
|
||||
}
|
||||
|
||||
// AuthenticationInfoResolverFunc implements AuthenticationInfoResolver.
|
||||
type AuthenticationInfoResolverFunc func(server string) (*rest.Config, error)
|
||||
|
||||
//ClientConfigFor implements AuthenticationInfoResolver.
|
||||
func (a AuthenticationInfoResolverFunc) ClientConfigFor(server string) (*rest.Config, error) {
|
||||
return a(server)
|
||||
}
|
||||
@ -43,7 +48,10 @@ type defaultAuthenticationInfoResolver struct {
|
||||
kubeconfig clientcmdapi.Config
|
||||
}
|
||||
|
||||
func newDefaultAuthenticationInfoResolver(kubeconfigFile string) (AuthenticationInfoResolver, error) {
|
||||
// NewDefaultAuthenticationInfoResolver generates an AuthenticationInfoResolver
|
||||
// that builds rest.Config based on the kubeconfig file. kubeconfigFile is the
|
||||
// path to the kubeconfig.
|
||||
func NewDefaultAuthenticationInfoResolver(kubeconfigFile string) (AuthenticationInfoResolver, error) {
|
||||
if len(kubeconfigFile) == 0 {
|
||||
return &defaultAuthenticationInfoResolver{}, nil
|
||||
}
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webhook
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
@ -0,0 +1,174 @@
|
||||
/*
|
||||
Copyright 2017 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 config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
|
||||
lru "github.com/hashicorp/golang-lru"
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultCacheSize = 200
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNeedServiceOrURL = errors.New("webhook configuration must have either service or URL")
|
||||
)
|
||||
|
||||
// ClientManager builds REST clients to talk to webhooks. It caches the clients
|
||||
// to avoid duplicate creation.
|
||||
type ClientManager struct {
|
||||
authInfoResolver AuthenticationInfoResolver
|
||||
serviceResolver ServiceResolver
|
||||
negotiatedSerializer runtime.NegotiatedSerializer
|
||||
cache *lru.Cache
|
||||
}
|
||||
|
||||
// NewClientManager creates a ClientManager.
|
||||
func NewClientManager() (ClientManager, error) {
|
||||
cache, err := lru.New(defaultCacheSize)
|
||||
if err != nil {
|
||||
return ClientManager{}, err
|
||||
}
|
||||
return ClientManager{
|
||||
cache: cache,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetAuthenticationInfoResolverWrapper sets the
|
||||
// AuthenticationInfoResolverWrapper.
|
||||
func (cm *ClientManager) SetAuthenticationInfoResolverWrapper(wrapper AuthenticationInfoResolverWrapper) {
|
||||
if wrapper != nil {
|
||||
cm.authInfoResolver = wrapper(cm.authInfoResolver)
|
||||
}
|
||||
}
|
||||
|
||||
// SetAuthenticationInfoResolver sets the AuthenticationInfoResolver.
|
||||
func (cm *ClientManager) SetAuthenticationInfoResolver(resolver AuthenticationInfoResolver) {
|
||||
cm.authInfoResolver = resolver
|
||||
}
|
||||
|
||||
// SetServiceResolver sets the ServiceResolver.
|
||||
func (cm *ClientManager) SetServiceResolver(sr ServiceResolver) {
|
||||
if sr != nil {
|
||||
cm.serviceResolver = sr
|
||||
}
|
||||
}
|
||||
|
||||
// SetNegotiatedSerializer sets the NegotiatedSerializer.
|
||||
func (cm *ClientManager) SetNegotiatedSerializer(n runtime.NegotiatedSerializer) {
|
||||
cm.negotiatedSerializer = n
|
||||
}
|
||||
|
||||
// Validate checks if ClientManager is properly set up.
|
||||
func (cm *ClientManager) Validate() error {
|
||||
var errs []error
|
||||
if cm.negotiatedSerializer == nil {
|
||||
errs = append(errs, fmt.Errorf("the ClientManager requires a negotiatedSerializer"))
|
||||
}
|
||||
if cm.serviceResolver == nil {
|
||||
errs = append(errs, fmt.Errorf("the ClientManager requires a serviceResolver"))
|
||||
}
|
||||
if cm.authInfoResolver == nil {
|
||||
errs = append(errs, fmt.Errorf("the ClientManager requires an authInfoResolver"))
|
||||
}
|
||||
return utilerrors.NewAggregate(errs)
|
||||
}
|
||||
|
||||
// HookClient get a RESTClient from the cache, or constructs one based on the
|
||||
// webhook configuration.
|
||||
func (cm *ClientManager) HookClient(h *v1alpha1.Webhook) (*rest.RESTClient, error) {
|
||||
cacheKey, err := json.Marshal(h.ClientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if client, ok := cm.cache.Get(string(cacheKey)); ok {
|
||||
return client.(*rest.RESTClient), nil
|
||||
}
|
||||
|
||||
complete := func(cfg *rest.Config) (*rest.RESTClient, error) {
|
||||
cfg.TLSClientConfig.CAData = h.ClientConfig.CABundle
|
||||
cfg.ContentConfig.NegotiatedSerializer = cm.negotiatedSerializer
|
||||
cfg.ContentConfig.ContentType = runtime.ContentTypeJSON
|
||||
client, err := rest.UnversionedRESTClientFor(cfg)
|
||||
if err == nil {
|
||||
cm.cache.Add(string(cacheKey), client)
|
||||
}
|
||||
return client, err
|
||||
}
|
||||
|
||||
if svc := h.ClientConfig.Service; svc != nil {
|
||||
serverName := svc.Name + "." + svc.Namespace + ".svc"
|
||||
restConfig, err := cm.authInfoResolver.ClientConfigFor(serverName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg := rest.CopyConfig(restConfig)
|
||||
host := serverName + ":443"
|
||||
cfg.Host = "https://" + host
|
||||
if svc.Path != nil {
|
||||
cfg.APIPath = *svc.Path
|
||||
}
|
||||
cfg.TLSClientConfig.ServerName = serverName
|
||||
|
||||
delegateDialer := cfg.Dial
|
||||
if delegateDialer == nil {
|
||||
delegateDialer = net.Dial
|
||||
}
|
||||
cfg.Dial = func(network, addr string) (net.Conn, error) {
|
||||
if addr == host {
|
||||
u, err := cm.serviceResolver.ResolveEndpoint(svc.Namespace, svc.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addr = u.Host
|
||||
}
|
||||
return delegateDialer(network, addr)
|
||||
}
|
||||
|
||||
return complete(cfg)
|
||||
}
|
||||
|
||||
if h.ClientConfig.URL == nil {
|
||||
return nil, &ErrCallingWebhook{WebhookName: h.Name, Reason: ErrNeedServiceOrURL}
|
||||
}
|
||||
|
||||
u, err := url.Parse(*h.ClientConfig.URL)
|
||||
if err != nil {
|
||||
return nil, &ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Unparsable URL: %v", err)}
|
||||
}
|
||||
|
||||
restConfig, err := cm.authInfoResolver.ClientConfigFor(u.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := rest.CopyConfig(restConfig)
|
||||
cfg.Host = u.Host
|
||||
cfg.APIPath = u.Path
|
||||
|
||||
return complete(cfg)
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/*
|
||||
Copyright 2017 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 config
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ErrCallingWebhook is returned for transport-layer errors calling webhooks. It
|
||||
// represents a failure to talk to the webhook, not the webhook rejecting a
|
||||
// request.
|
||||
type ErrCallingWebhook struct {
|
||||
WebhookName string
|
||||
Reason error
|
||||
}
|
||||
|
||||
func (e *ErrCallingWebhook) Error() string {
|
||||
if e.Reason != nil {
|
||||
return fmt.Sprintf("failed calling admission webhook %q: %v", e.WebhookName, e.Reason)
|
||||
}
|
||||
return fmt.Sprintf("failed calling admission webhook %q; no further details available", e.WebhookName)
|
||||
}
|
@ -14,9 +14,32 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webhook
|
||||
package config
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
)
|
||||
|
||||
// AdmissionConfig holds config data that is unique to each API server.
|
||||
type AdmissionConfig struct {
|
||||
// KubeConfigFile is the path to the kubeconfig file.
|
||||
KubeConfigFile string `json:"kubeConfigFile"`
|
||||
}
|
||||
|
||||
// LoadConfig extract the KubeConfigFile from configFile
|
||||
func LoadConfig(configFile io.Reader) (string, error) {
|
||||
var kubeconfigFile string
|
||||
if configFile != nil {
|
||||
// TODO: move this to a versioned configuration file format
|
||||
var config AdmissionConfig
|
||||
d := yaml.NewYAMLOrJSONDecoder(configFile, 4096)
|
||||
err := d.Decode(&config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
kubeconfigFile = config.KubeConfigFile
|
||||
}
|
||||
return kubeconfigFile, nil
|
||||
}
|
@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package webhook checks a webhook for configured operation admission
|
||||
package webhook
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@ -23,8 +22,17 @@ import (
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// ServiceResolver knows how to convert a service reference into an actual location.
|
||||
type ServiceResolver interface {
|
||||
ResolveEndpoint(namespace, name string) (*url.URL, error)
|
||||
}
|
||||
|
||||
type defaultServiceResolver struct{}
|
||||
|
||||
func NewDefaultServiceResolver() ServiceResolver {
|
||||
return &defaultServiceResolver{}
|
||||
}
|
||||
|
||||
// ResolveEndpoint constructs a service URL from a given namespace and name
|
||||
// note that the name and namespace are required and by default all created addresses use HTTPS scheme.
|
||||
// for example:
|
@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package webhook checks a webhook for configured operation admission
|
||||
package webhook
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
@ -0,0 +1,38 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["rules.go"],
|
||||
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/rules",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//vendor/k8s.io/api/admissionregistration/v1alpha1:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["rules_test.go"],
|
||||
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/rules",
|
||||
library = ":go_default_library",
|
||||
deps = [
|
||||
"//vendor/k8s.io/api/admissionregistration/v1alpha1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package webhook checks a webhook for configured operation admission
|
||||
package webhook
|
||||
package rules
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@ -24,12 +23,14 @@ import (
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
)
|
||||
|
||||
type RuleMatcher struct {
|
||||
// Matcher determines if the Attr matches the Rule.
|
||||
type Matcher struct {
|
||||
Rule v1alpha1.RuleWithOperations
|
||||
Attr admission.Attributes
|
||||
}
|
||||
|
||||
func (r *RuleMatcher) Matches() bool {
|
||||
// Matches returns if the Attr matches the Rule.
|
||||
func (r *Matcher) Matches() bool {
|
||||
return r.operation() &&
|
||||
r.group() &&
|
||||
r.version() &&
|
||||
@ -49,15 +50,15 @@ func exactOrWildcard(items []string, requested string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *RuleMatcher) group() bool {
|
||||
func (r *Matcher) group() bool {
|
||||
return exactOrWildcard(r.Rule.APIGroups, r.Attr.GetResource().Group)
|
||||
}
|
||||
|
||||
func (r *RuleMatcher) version() bool {
|
||||
func (r *Matcher) version() bool {
|
||||
return exactOrWildcard(r.Rule.APIVersions, r.Attr.GetResource().Version)
|
||||
}
|
||||
|
||||
func (r *RuleMatcher) operation() bool {
|
||||
func (r *Matcher) operation() bool {
|
||||
attrOp := r.Attr.GetOperation()
|
||||
for _, op := range r.Rule.Operations {
|
||||
if op == v1alpha1.OperationAll {
|
||||
@ -80,7 +81,7 @@ func splitResource(resSub string) (res, sub string) {
|
||||
return parts[0], ""
|
||||
}
|
||||
|
||||
func (r *RuleMatcher) resource() bool {
|
||||
func (r *Matcher) resource() bool {
|
||||
opRes, opSub := r.Attr.GetResource().Resource, r.Attr.GetSubresource()
|
||||
for _, res := range r.Rule.Resources {
|
||||
res, sub := splitResource(res)
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webhook
|
||||
package rules
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@ -77,13 +77,13 @@ func TestGroup(t *testing.T) {
|
||||
|
||||
for name, tt := range table {
|
||||
for _, m := range tt.match {
|
||||
r := RuleMatcher{tt.rule, m}
|
||||
r := Matcher{tt.rule, m}
|
||||
if !r.group() {
|
||||
t.Errorf("%v: expected match %#v", name, m)
|
||||
}
|
||||
}
|
||||
for _, m := range tt.noMatch {
|
||||
r := RuleMatcher{tt.rule, m}
|
||||
r := Matcher{tt.rule, m}
|
||||
if r.group() {
|
||||
t.Errorf("%v: expected no match %#v", name, m)
|
||||
}
|
||||
@ -121,13 +121,13 @@ func TestVersion(t *testing.T) {
|
||||
}
|
||||
for name, tt := range table {
|
||||
for _, m := range tt.match {
|
||||
r := RuleMatcher{tt.rule, m}
|
||||
r := Matcher{tt.rule, m}
|
||||
if !r.version() {
|
||||
t.Errorf("%v: expected match %#v", name, m)
|
||||
}
|
||||
}
|
||||
for _, m := range tt.noMatch {
|
||||
r := RuleMatcher{tt.rule, m}
|
||||
r := Matcher{tt.rule, m}
|
||||
if r.version() {
|
||||
t.Errorf("%v: expected no match %#v", name, m)
|
||||
}
|
||||
@ -204,13 +204,13 @@ func TestOperation(t *testing.T) {
|
||||
}
|
||||
for name, tt := range table {
|
||||
for _, m := range tt.match {
|
||||
r := RuleMatcher{tt.rule, m}
|
||||
r := Matcher{tt.rule, m}
|
||||
if !r.operation() {
|
||||
t.Errorf("%v: expected match %#v", name, m)
|
||||
}
|
||||
}
|
||||
for _, m := range tt.noMatch {
|
||||
r := RuleMatcher{tt.rule, m}
|
||||
r := Matcher{tt.rule, m}
|
||||
if r.operation() {
|
||||
t.Errorf("%v: expected no match %#v", name, m)
|
||||
}
|
||||
@ -285,13 +285,13 @@ func TestResource(t *testing.T) {
|
||||
}
|
||||
for name, tt := range table {
|
||||
for _, m := range tt.match {
|
||||
r := RuleMatcher{tt.rule, m}
|
||||
r := Matcher{tt.rule, m}
|
||||
if !r.resource() {
|
||||
t.Errorf("%v: expected match %#v", name, m)
|
||||
}
|
||||
}
|
||||
for _, m := range tt.noMatch {
|
||||
r := RuleMatcher{tt.rule, m}
|
||||
r := Matcher{tt.rule, m}
|
||||
if r.resource() {
|
||||
t.Errorf("%v: expected no match %#v", name, m)
|
||||
}
|
@ -5,17 +5,12 @@ go_library(
|
||||
srcs = [
|
||||
"admission.go",
|
||||
"admissionreview.go",
|
||||
"authentication.go",
|
||||
"config.go",
|
||||
"doc.go",
|
||||
"rules.go",
|
||||
"serviceresolver.go",
|
||||
],
|
||||
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook",
|
||||
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/validating",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
"//vendor/github.com/hashicorp/golang-lru:go_default_library",
|
||||
"//vendor/k8s.io/api/admission/v1alpha1:go_default_library",
|
||||
"//vendor/k8s.io/api/admissionregistration/v1alpha1:go_default_library",
|
||||
"//vendor/k8s.io/api/authentication/v1:go_default_library",
|
||||
@ -28,16 +23,14 @@ go_library(
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/yaml:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission/configuration:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/rules:go_default_library",
|
||||
"//vendor/k8s.io/client-go/informers:go_default_library",
|
||||
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
|
||||
"//vendor/k8s.io/client-go/listers/core/v1:go_default_library",
|
||||
"//vendor/k8s.io/client-go/rest:go_default_library",
|
||||
"//vendor/k8s.io/client-go/tools/clientcmd:go_default_library",
|
||||
"//vendor/k8s.io/client-go/tools/clientcmd/api:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
@ -45,33 +38,28 @@ go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"admission_test.go",
|
||||
"authentication_test.go",
|
||||
"certs_test.go",
|
||||
"conversion_test.go",
|
||||
"rules_test.go",
|
||||
"serviceresolver_test.go",
|
||||
],
|
||||
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook",
|
||||
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/validating",
|
||||
library = ":go_default_library",
|
||||
deps = [
|
||||
"//vendor/k8s.io/api/admission/v1alpha1:go_default_library",
|
||||
"//vendor/k8s.io/api/admissionregistration/v1alpha1:go_default_library",
|
||||
"//vendor/k8s.io/api/core/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/diff:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/apis/example:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/apis/example/v1:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/apis/example2/v1:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
"//vendor/k8s.io/client-go/rest:go_default_library",
|
||||
"//vendor/k8s.io/client-go/tools/clientcmd/api:go_default_library",
|
||||
],
|
||||
)
|
||||
|
@ -14,22 +14,18 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package webhook delegates admission checks to dynamically configured webhooks.
|
||||
package webhook
|
||||
// Package validating delegates admission checks to dynamically configured
|
||||
// validating webhooks.
|
||||
package validating
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
lru "github.com/hashicorp/golang-lru"
|
||||
|
||||
admissionv1alpha1 "k8s.io/api/admission/v1alpha1"
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
@ -42,38 +38,21 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/configuration"
|
||||
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/config"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/rules"
|
||||
"k8s.io/client-go/informers"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
corelisters "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
const (
|
||||
// Name of admission plug-in
|
||||
PluginName = "GenericAdmissionWebhook"
|
||||
defaultCacheSize = 200
|
||||
PluginName = "GenericAdmissionWebhook"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNeedServiceOrURL = errors.New("webhook configuration must have either service or URL")
|
||||
)
|
||||
|
||||
type ErrCallingWebhook struct {
|
||||
WebhookName string
|
||||
Reason error
|
||||
}
|
||||
|
||||
func (e *ErrCallingWebhook) Error() string {
|
||||
if e.Reason != nil {
|
||||
return fmt.Sprintf("failed calling admission webhook %q: %v", e.WebhookName, e.Reason)
|
||||
}
|
||||
return fmt.Sprintf("failed calling admission webhook %q; no further details available", e.WebhookName)
|
||||
}
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(configFile io.Reader) (admission.Interface, error) {
|
||||
@ -94,26 +73,22 @@ type WebhookSource interface {
|
||||
|
||||
// NewGenericAdmissionWebhook returns a generic admission webhook plugin.
|
||||
func NewGenericAdmissionWebhook(configFile io.Reader) (*GenericAdmissionWebhook, error) {
|
||||
kubeconfigFile := ""
|
||||
if configFile != nil {
|
||||
// TODO: move this to a versioned configuration file format
|
||||
var config AdmissionConfig
|
||||
d := yaml.NewYAMLOrJSONDecoder(configFile, 4096)
|
||||
err := d.Decode(&config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kubeconfigFile = config.KubeConfigFile
|
||||
}
|
||||
authInfoResolver, err := newDefaultAuthenticationInfoResolver(kubeconfigFile)
|
||||
kubeconfigFile, err := config.LoadConfig(configFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cache, err := lru.New(defaultCacheSize)
|
||||
cm, err := config.NewClientManager()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authInfoResolver, err := config.NewDefaultAuthenticationInfoResolver(kubeconfigFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Set defaults which may be overridden later.
|
||||
cm.SetAuthenticationInfoResolver(authInfoResolver)
|
||||
cm.SetServiceResolver(config.NewDefaultServiceResolver())
|
||||
|
||||
return &GenericAdmissionWebhook{
|
||||
Handler: admission.NewHandler(
|
||||
@ -122,30 +97,19 @@ func NewGenericAdmissionWebhook(configFile io.Reader) (*GenericAdmissionWebhook,
|
||||
admission.Delete,
|
||||
admission.Update,
|
||||
),
|
||||
authInfoResolver: authInfoResolver,
|
||||
serviceResolver: defaultServiceResolver{},
|
||||
cache: cache,
|
||||
clientManager: cm,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GenericAdmissionWebhook is an implementation of admission.Interface.
|
||||
type GenericAdmissionWebhook struct {
|
||||
*admission.Handler
|
||||
hookSource WebhookSource
|
||||
serviceResolver ServiceResolver
|
||||
negotiatedSerializer runtime.NegotiatedSerializer
|
||||
namespaceLister corelisters.NamespaceLister
|
||||
client clientset.Interface
|
||||
convertor runtime.ObjectConvertor
|
||||
creator runtime.ObjectCreater
|
||||
|
||||
authInfoResolver AuthenticationInfoResolver
|
||||
cache *lru.Cache
|
||||
}
|
||||
|
||||
// serviceResolver knows how to convert a service reference into an actual location.
|
||||
type ServiceResolver interface {
|
||||
ResolveEndpoint(namespace, name string) (*url.URL, error)
|
||||
hookSource WebhookSource
|
||||
namespaceLister corelisters.NamespaceLister
|
||||
client clientset.Interface
|
||||
convertor runtime.ObjectConvertor
|
||||
creator runtime.ObjectCreater
|
||||
clientManager config.ClientManager
|
||||
}
|
||||
|
||||
var (
|
||||
@ -153,26 +117,22 @@ var (
|
||||
)
|
||||
|
||||
// TODO find a better way wire this, but keep this pull small for now.
|
||||
func (a *GenericAdmissionWebhook) SetAuthenticationInfoResolverWrapper(wrapper AuthenticationInfoResolverWrapper) {
|
||||
if wrapper != nil {
|
||||
a.authInfoResolver = wrapper(a.authInfoResolver)
|
||||
}
|
||||
func (a *GenericAdmissionWebhook) SetAuthenticationInfoResolverWrapper(wrapper config.AuthenticationInfoResolverWrapper) {
|
||||
a.clientManager.SetAuthenticationInfoResolverWrapper(wrapper)
|
||||
}
|
||||
|
||||
// SetServiceResolver sets a service resolver for the webhook admission plugin.
|
||||
// Passing a nil resolver does not have an effect, instead a default one will be used.
|
||||
func (a *GenericAdmissionWebhook) SetServiceResolver(sr ServiceResolver) {
|
||||
if sr != nil {
|
||||
a.serviceResolver = sr
|
||||
}
|
||||
func (a *GenericAdmissionWebhook) SetServiceResolver(sr config.ServiceResolver) {
|
||||
a.clientManager.SetServiceResolver(sr)
|
||||
}
|
||||
|
||||
// SetScheme sets a serializer(NegotiatedSerializer) which is derived from the scheme
|
||||
func (a *GenericAdmissionWebhook) SetScheme(scheme *runtime.Scheme) {
|
||||
if scheme != nil {
|
||||
a.negotiatedSerializer = serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{
|
||||
a.clientManager.SetNegotiatedSerializer(serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{
|
||||
Serializer: serializer.NewCodecFactory(scheme).LegacyCodec(admissionv1alpha1.SchemeGroupVersion),
|
||||
})
|
||||
}))
|
||||
a.convertor = scheme
|
||||
a.creator = scheme
|
||||
}
|
||||
@ -196,12 +156,12 @@ func (a *GenericAdmissionWebhook) ValidateInitialization() error {
|
||||
if a.hookSource == nil {
|
||||
return fmt.Errorf("the GenericAdmissionWebhook admission plugin requires a Kubernetes client to be provided")
|
||||
}
|
||||
if a.negotiatedSerializer == nil {
|
||||
return fmt.Errorf("the GenericAdmissionWebhook admission plugin requires a runtime.Scheme to be provided to derive a serializer")
|
||||
}
|
||||
if a.namespaceLister == nil {
|
||||
return fmt.Errorf("the GenericAdmissionWebhook admission plugin requires a namespaceLister")
|
||||
}
|
||||
if err := a.clientManager.Validate(); err != nil {
|
||||
return fmt.Errorf("the GenericAdmissionWebhook.clientManager is not properly setup: %v", err)
|
||||
}
|
||||
go a.hookSource.Run(wait.NeverStop)
|
||||
return nil
|
||||
}
|
||||
@ -225,6 +185,7 @@ func (a *GenericAdmissionWebhook) loadConfiguration(attr admission.Attributes) (
|
||||
return hookConfig, nil
|
||||
}
|
||||
|
||||
// TODO: move this object to a common package
|
||||
type versionedAttributes struct {
|
||||
admission.Attributes
|
||||
oldObject runtime.Object
|
||||
@ -239,6 +200,7 @@ func (v versionedAttributes) GetOldObject() runtime.Object {
|
||||
return v.oldObject
|
||||
}
|
||||
|
||||
// TODO: move this method to a common package
|
||||
func (a *GenericAdmissionWebhook) convertToGVK(obj runtime.Object, gvk schema.GroupVersionKind) (runtime.Object, error) {
|
||||
// Unlike other resources, custom resources do not have internal version, so
|
||||
// if obj is a custom resource, it should not need conversion.
|
||||
@ -315,7 +277,7 @@ func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error {
|
||||
}
|
||||
|
||||
ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == v1alpha1.Ignore
|
||||
if callErr, ok := err.(*ErrCallingWebhook); ok {
|
||||
if callErr, ok := err.(*config.ErrCallingWebhook); ok {
|
||||
if ignoreClientCallFailures {
|
||||
glog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr)
|
||||
utilruntime.HandleError(callErr)
|
||||
@ -351,6 +313,7 @@ func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error {
|
||||
return errs[0]
|
||||
}
|
||||
|
||||
// TODO: move this method to a common package
|
||||
func (a *GenericAdmissionWebhook) getNamespaceLabels(attr admission.Attributes) (map[string]string, error) {
|
||||
// If the request itself is creating or updating a namespace, then get the
|
||||
// labels from attr.Object, because namespaceLister doesn't have the latest
|
||||
@ -385,6 +348,7 @@ func (a *GenericAdmissionWebhook) getNamespaceLabels(attr admission.Attributes)
|
||||
return namespace.Labels, nil
|
||||
}
|
||||
|
||||
// TODO: move this method to a common package
|
||||
// whether the request is exempted by the webhook because of the
|
||||
// namespaceSelector of the webhook.
|
||||
func (a *GenericAdmissionWebhook) exemptedByNamespaceSelector(h *v1alpha1.Webhook, attr admission.Attributes) (bool, *apierrors.StatusError) {
|
||||
@ -417,10 +381,11 @@ func (a *GenericAdmissionWebhook) exemptedByNamespaceSelector(h *v1alpha1.Webhoo
|
||||
return !selector.Matches(labels.Set(namespaceLabels)), nil
|
||||
}
|
||||
|
||||
// TODO: move this method to a common package
|
||||
func (a *GenericAdmissionWebhook) shouldCallHook(h *v1alpha1.Webhook, attr admission.Attributes) (bool, *apierrors.StatusError) {
|
||||
var matches bool
|
||||
for _, r := range h.Rules {
|
||||
m := RuleMatcher{Rule: r, Attr: attr}
|
||||
m := rules.Matcher{Rule: r, Attr: attr}
|
||||
if m.Matches() {
|
||||
matches = true
|
||||
break
|
||||
@ -440,13 +405,13 @@ func (a *GenericAdmissionWebhook) shouldCallHook(h *v1alpha1.Webhook, attr admis
|
||||
func (a *GenericAdmissionWebhook) callHook(ctx context.Context, h *v1alpha1.Webhook, attr admission.Attributes) error {
|
||||
// Make the webhook request
|
||||
request := createAdmissionReview(attr)
|
||||
client, err := a.hookClient(h)
|
||||
client, err := a.clientManager.HookClient(h)
|
||||
if err != nil {
|
||||
return &ErrCallingWebhook{WebhookName: h.Name, Reason: err}
|
||||
return &config.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
|
||||
}
|
||||
response := &admissionv1alpha1.AdmissionReview{}
|
||||
if err := client.Post().Context(ctx).Body(&request).Do().Into(response); err != nil {
|
||||
return &ErrCallingWebhook{WebhookName: h.Name, Reason: err}
|
||||
return &config.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
|
||||
}
|
||||
|
||||
if response.Status.Allowed {
|
||||
@ -456,6 +421,7 @@ func (a *GenericAdmissionWebhook) callHook(ctx context.Context, h *v1alpha1.Webh
|
||||
return toStatusErr(h.Name, response.Status.Result)
|
||||
}
|
||||
|
||||
// TODO: move this function to a common package
|
||||
// toStatusErr returns a StatusError with information about the webhook controller
|
||||
func toStatusErr(name string, result *metav1.Status) *apierrors.StatusError {
|
||||
deniedBy := fmt.Sprintf("admission webhook %q denied the request", name)
|
||||
@ -478,76 +444,3 @@ func toStatusErr(name string, result *metav1.Status) *apierrors.StatusError {
|
||||
ErrStatus: *result,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *GenericAdmissionWebhook) hookClient(h *v1alpha1.Webhook) (*rest.RESTClient, error) {
|
||||
cacheKey, err := json.Marshal(h.ClientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if client, ok := a.cache.Get(string(cacheKey)); ok {
|
||||
return client.(*rest.RESTClient), nil
|
||||
}
|
||||
|
||||
complete := func(cfg *rest.Config) (*rest.RESTClient, error) {
|
||||
cfg.TLSClientConfig.CAData = h.ClientConfig.CABundle
|
||||
cfg.ContentConfig.NegotiatedSerializer = a.negotiatedSerializer
|
||||
cfg.ContentConfig.ContentType = runtime.ContentTypeJSON
|
||||
client, err := rest.UnversionedRESTClientFor(cfg)
|
||||
if err == nil {
|
||||
a.cache.Add(string(cacheKey), client)
|
||||
}
|
||||
return client, err
|
||||
}
|
||||
|
||||
if svc := h.ClientConfig.Service; svc != nil {
|
||||
serverName := svc.Name + "." + svc.Namespace + ".svc"
|
||||
restConfig, err := a.authInfoResolver.ClientConfigFor(serverName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg := rest.CopyConfig(restConfig)
|
||||
host := serverName + ":443"
|
||||
cfg.Host = "https://" + host
|
||||
if svc.Path != nil {
|
||||
cfg.APIPath = *svc.Path
|
||||
}
|
||||
cfg.TLSClientConfig.ServerName = serverName
|
||||
|
||||
delegateDialer := cfg.Dial
|
||||
if delegateDialer == nil {
|
||||
delegateDialer = net.Dial
|
||||
}
|
||||
cfg.Dial = func(network, addr string) (net.Conn, error) {
|
||||
if addr == host {
|
||||
u, err := a.serviceResolver.ResolveEndpoint(svc.Namespace, svc.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addr = u.Host
|
||||
}
|
||||
return delegateDialer(network, addr)
|
||||
}
|
||||
|
||||
return complete(cfg)
|
||||
}
|
||||
|
||||
if h.ClientConfig.URL == nil {
|
||||
return nil, &ErrCallingWebhook{WebhookName: h.Name, Reason: ErrNeedServiceOrURL}
|
||||
}
|
||||
|
||||
u, err := url.Parse(*h.ClientConfig.URL)
|
||||
if err != nil {
|
||||
return nil, &ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Unparsable URL: %v", err)}
|
||||
}
|
||||
|
||||
restConfig, err := a.authInfoResolver.ClientConfigFor(u.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := rest.CopyConfig(restConfig)
|
||||
cfg.Host = u.Host
|
||||
cfg.APIPath = u.Path
|
||||
|
||||
return complete(cfg)
|
||||
}
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webhook
|
||||
package validating
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
@ -39,6 +39,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/config"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
@ -133,9 +134,17 @@ func TestAdmit(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wh.authInfoResolver = newFakeAuthenticationInfoResolver()
|
||||
wh.serviceResolver = fakeServiceResolver{base: *serverURL}
|
||||
cm, err := config.NewClientManager()
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create client manager: %v", err)
|
||||
}
|
||||
cm.SetAuthenticationInfoResolver(newFakeAuthenticationInfoResolver(new(int32)))
|
||||
cm.SetServiceResolver(fakeServiceResolver{base: *serverURL})
|
||||
wh.clientManager = cm
|
||||
wh.SetScheme(scheme)
|
||||
if err = wh.clientManager.Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
namespace := "webhook-test"
|
||||
wh.namespaceLister = fakeNamespaceLister{map[string]*corev1.Namespace{
|
||||
namespace: {
|
||||
@ -397,8 +406,12 @@ func TestAdmitCachedClient(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wh.authInfoResolver = newFakeAuthenticationInfoResolver()
|
||||
wh.serviceResolver = fakeServiceResolver{base: *serverURL}
|
||||
cm, err := config.NewClientManager()
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create client manager: %v", err)
|
||||
}
|
||||
cm.SetServiceResolver(fakeServiceResolver{base: *serverURL})
|
||||
wh.clientManager = cm
|
||||
wh.SetScheme(scheme)
|
||||
namespace := "webhook-test"
|
||||
wh.namespaceLister = fakeNamespaceLister{map[string]*corev1.Namespace{
|
||||
@ -519,18 +532,23 @@ func TestAdmitCachedClient(t *testing.T) {
|
||||
for _, testcase := range cases {
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
wh.hookSource = &testcase.hookSource
|
||||
wh.authInfoResolver.(*fakeAuthenticationInfoResolver).cachedCount = 0
|
||||
authInfoResolverCount := new(int32)
|
||||
r := newFakeAuthenticationInfoResolver(authInfoResolverCount)
|
||||
wh.clientManager.SetAuthenticationInfoResolver(r)
|
||||
if err = wh.clientManager.Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = wh.Admit(admission.NewAttributesRecord(&object, &oldObject, kind, namespace, testcase.name, resource, subResource, operation, &userInfo))
|
||||
if testcase.expectAllow != (err == nil) {
|
||||
t.Errorf("expected allowed=%v, but got err=%v", testcase.expectAllow, err)
|
||||
}
|
||||
|
||||
if testcase.expectCache && wh.authInfoResolver.(*fakeAuthenticationInfoResolver).cachedCount != 1 {
|
||||
if testcase.expectCache && *authInfoResolverCount != 1 {
|
||||
t.Errorf("expected cacheclient, but got none")
|
||||
}
|
||||
|
||||
if !testcase.expectCache && wh.authInfoResolver.(*fakeAuthenticationInfoResolver).cachedCount != 0 {
|
||||
if !testcase.expectCache && *authInfoResolverCount != 0 {
|
||||
t.Errorf("expected not cacheclient, but got cache")
|
||||
}
|
||||
})
|
||||
@ -597,7 +615,7 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func newFakeAuthenticationInfoResolver() *fakeAuthenticationInfoResolver {
|
||||
func newFakeAuthenticationInfoResolver(count *int32) *fakeAuthenticationInfoResolver {
|
||||
return &fakeAuthenticationInfoResolver{
|
||||
restConfig: &rest.Config{
|
||||
TLSClientConfig: rest.TLSClientConfig{
|
||||
@ -606,16 +624,17 @@ func newFakeAuthenticationInfoResolver() *fakeAuthenticationInfoResolver {
|
||||
KeyData: clientKey,
|
||||
},
|
||||
},
|
||||
cachedCount: count,
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAuthenticationInfoResolver struct {
|
||||
restConfig *rest.Config
|
||||
cachedCount int32
|
||||
cachedCount *int32
|
||||
}
|
||||
|
||||
func (c *fakeAuthenticationInfoResolver) ClientConfigFor(server string) (*rest.Config, error) {
|
||||
atomic.AddInt32(&c.cachedCount, 1)
|
||||
atomic.AddInt32(c.cachedCount, 1)
|
||||
return c.restConfig, nil
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
// Package webhook delegates admission checks to dynamically configured webhooks.
|
||||
package webhook
|
||||
package validating
|
||||
|
||||
import (
|
||||
admissionv1alpha1 "k8s.io/api/admission/v1alpha1"
|
||||
@ -25,6 +25,7 @@ import (
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
)
|
||||
|
||||
// TODO: move this function to a common package
|
||||
// createAdmissionReview creates an AdmissionReview for the provided admission.Attributes
|
||||
func createAdmissionReview(attr admission.Attributes) admissionv1alpha1.AdmissionReview {
|
||||
gvk := attr.GetKind()
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||
// This file was generated using openssl by the gencerts.sh script
|
||||
// and holds raw certificates for the webhook tests.
|
||||
|
||||
package webhook
|
||||
package validating
|
||||
|
||||
var caKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAt8E1XykA4860Tj7mypnsSU+hW0taUEvz26a5rgFSrwgKe1g+
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webhook
|
||||
package validating
|
||||
|
||||
import (
|
||||
"reflect"
|
@ -14,5 +14,5 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package webhook checks a webhook for configured operation admission
|
||||
package webhook // import "k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||
// Package validating checks a non-mutating webhook for configured operation admission
|
||||
package validating // import "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
|
@ -95,7 +95,7 @@ EOF
|
||||
echo "// This file was generated using openssl by the gencerts.sh script" >> $outfile
|
||||
echo "// and holds raw certificates for the webhook tests." >> $outfile
|
||||
echo "" >> $outfile
|
||||
echo "package webhook" >> $outfile
|
||||
echo "package validating" >> $outfile
|
||||
for file in caKey caCert badCAKey badCACert serverKey serverCert clientKey clientCert; do
|
||||
data=$(cat ${file}.pem)
|
||||
echo "" >> $outfile
|
@ -88,7 +88,7 @@ go_library(
|
||||
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission/plugin/initialization:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/validating:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/apis/apiserver/install:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/audit:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/audit/policy:go_default_library",
|
||||
|
@ -32,7 +32,7 @@ go_library(
|
||||
"//vendor/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission/plugin/initialization:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/validating:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/apis/audit/v1beta1:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/audit:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/audit/policy:go_default_library",
|
||||
|
@ -26,7 +26,7 @@ import (
|
||||
"k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/initialization"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
|
||||
"k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
@ -56,8 +56,8 @@ func NewAdmissionOptions() *AdmissionOptions {
|
||||
options := &AdmissionOptions{
|
||||
Plugins: &admission.Plugins{},
|
||||
PluginNames: []string{},
|
||||
RecommendedPluginOrder: []string{lifecycle.PluginName, initialization.PluginName, webhook.PluginName},
|
||||
DefaultOffPlugins: []string{initialization.PluginName, webhook.PluginName},
|
||||
RecommendedPluginOrder: []string{lifecycle.PluginName, initialization.PluginName, validatingwebhook.PluginName},
|
||||
DefaultOffPlugins: []string{initialization.PluginName, validatingwebhook.PluginName},
|
||||
}
|
||||
server.RegisterAllAdmissionPlugins(options.Plugins)
|
||||
return options
|
||||
|
@ -21,12 +21,12 @@ import (
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/initialization"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
|
||||
)
|
||||
|
||||
// RegisterAllAdmissionPlugins registers all admission plugins
|
||||
func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
|
||||
lifecycle.Register(plugins)
|
||||
initialization.Register(plugins)
|
||||
webhook.Register(plugins)
|
||||
validatingwebhook.Register(plugins)
|
||||
}
|
||||
|
@ -843,7 +843,15 @@
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook",
|
||||
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/config",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/rules",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/validating",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
|
@ -839,7 +839,15 @@
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook",
|
||||
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/config",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/rules",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/validating",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user