diff --git a/cmd/kube-apiserver/app/BUILD b/cmd/kube-apiserver/app/BUILD index 264083a9539..d0fb814fe65 100644 --- a/cmd/kube-apiserver/app/BUILD +++ b/cmd/kube-apiserver/app/BUILD @@ -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", diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index c60331ffa30..6fa464256ca 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -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 != "" { diff --git a/hack/.golint_failures b/hack/.golint_failures index 90f6c9f8a0e..eda7eb21b3c 100644 --- a/hack/.golint_failures +++ b/hack/.golint_failures @@ -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 diff --git a/pkg/kubeapiserver/admission/BUILD b/pkg/kubeapiserver/admission/BUILD index 25880122a92..ec2d84cafcd 100644 --- a/pkg/kubeapiserver/admission/BUILD +++ b/pkg/kubeapiserver/admission/BUILD @@ -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", ], diff --git a/pkg/kubeapiserver/admission/init_test.go b/pkg/kubeapiserver/admission/init_test.go index b1b2c1e90dc..910f03f94a2 100644 --- a/pkg/kubeapiserver/admission/init_test.go +++ b/pkg/kubeapiserver/admission/init_test.go @@ -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{} diff --git a/pkg/kubeapiserver/admission/initializer.go b/pkg/kubeapiserver/admission/initializer.go index 0a93b8ae1f1..826ce74322f 100644 --- a/pkg/kubeapiserver/admission/initializer.go +++ b/pkg/kubeapiserver/admission/initializer.go @@ -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, diff --git a/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json b/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json index d235d3aa14b..bf47c87127f 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json +++ b/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json @@ -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" }, { diff --git a/staging/src/k8s.io/apiserver/pkg/admission/BUILD b/staging/src/k8s.io/apiserver/pkg/admission/BUILD index 0102e5b38c7..291df5b2ff9 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/admission/BUILD @@ -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"], ) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/BUILD b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/BUILD new file mode 100644 index 00000000000..0f9ff984c4b --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/BUILD @@ -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"], +) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/authentication.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/authentication.go similarity index 88% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/authentication.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/authentication.go index 40479658287..dd956f140a7 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/authentication.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/authentication.go @@ -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 } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/authentication_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/authentication_test.go similarity index 99% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/authentication_test.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/authentication_test.go index d91a428c062..cd63bd94bbd 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/authentication_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/authentication_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package webhook +package config import ( "testing" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/client.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/client.go new file mode 100644 index 00000000000..8203bb7dac8 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/client.go @@ -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) +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/errors.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/errors.go new file mode 100644 index 00000000000..fa259758fda --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/errors.go @@ -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) +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/kubeconfig.go similarity index 55% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/kubeconfig.go index 7285d4e9f05..1fdb221955f 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/kubeconfig.go @@ -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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/serviceresolver.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/serviceresolver.go similarity index 81% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/serviceresolver.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/serviceresolver.go index 274e6881bbf..47b96a709fe 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/serviceresolver.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/serviceresolver.go @@ -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: diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/serviceresolver_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/serviceresolver_test.go similarity index 95% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/serviceresolver_test.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/serviceresolver_test.go index bf50af520a5..6fd24d06333 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/serviceresolver_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/serviceresolver_test.go @@ -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" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules/BUILD b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules/BUILD new file mode 100644 index 00000000000..2b05eea012c --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules/BUILD @@ -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"], +) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules/rules.go similarity index 86% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules/rules.go index 6fc734e8de7..d13ea5928e4 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules/rules.go @@ -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) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules/rules_test.go similarity index 96% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules_test.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules/rules_test.go index 0e464aa202d..40c0f36d636 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules/rules_test.go @@ -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) } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/BUILD b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/BUILD similarity index 78% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/BUILD rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/BUILD index 792c23bb796..63b1402f243 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/BUILD @@ -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", ], ) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/admission.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/admission.go similarity index 73% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/admission.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/admission.go index 4461372188d..98df6433a5a 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/admission.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/admission.go @@ -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) -} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/admission_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/admission_test.go similarity index 94% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/admission_test.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/admission_test.go index d16c3605fa0..e773a9b6480 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/admission_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/admission_test.go @@ -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 } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/admissionreview.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/admissionreview.go similarity index 97% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/admissionreview.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/admissionreview.go index c9e139c713a..cdde9bc04f1 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/admissionreview.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/admissionreview.go @@ -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() diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/certs_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/certs_test.go similarity index 99% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/certs_test.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/certs_test.go index 9026bd05644..57605ffc819 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/certs_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/certs_test.go @@ -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+ diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/conversion_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/conversion_test.go similarity index 99% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/conversion_test.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/conversion_test.go index 684745fd979..e0be84f8b9f 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/conversion_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/conversion_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package webhook +package validating import ( "reflect" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/doc.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/doc.go similarity index 76% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/doc.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/doc.go index 6a21529eda4..0241c8e537b 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/doc.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/doc.go @@ -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" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/gencerts.sh b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/gencerts.sh similarity index 98% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/gencerts.sh rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/gencerts.sh index 7c0c52227b1..f03124933d2 100755 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/gencerts.sh +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/gencerts.sh @@ -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 diff --git a/staging/src/k8s.io/apiserver/pkg/server/BUILD b/staging/src/k8s.io/apiserver/pkg/server/BUILD index 68c2d540802..246ce68e01c 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/server/BUILD @@ -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", diff --git a/staging/src/k8s.io/apiserver/pkg/server/options/BUILD b/staging/src/k8s.io/apiserver/pkg/server/options/BUILD index 6672d1a5cc0..0d72e6a80c6 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/options/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/server/options/BUILD @@ -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", diff --git a/staging/src/k8s.io/apiserver/pkg/server/options/admission.go b/staging/src/k8s.io/apiserver/pkg/server/options/admission.go index b769d855fa0..c5ed7f9b8a4 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/options/admission.go +++ b/staging/src/k8s.io/apiserver/pkg/server/options/admission.go @@ -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 diff --git a/staging/src/k8s.io/apiserver/pkg/server/plugins.go b/staging/src/k8s.io/apiserver/pkg/server/plugins.go index e744c8ae2a0..54cd5396196 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/plugins.go +++ b/staging/src/k8s.io/apiserver/pkg/server/plugins.go @@ -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) } diff --git a/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json b/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json index b15737ea7d9..ff0e3e4f675 100644 --- a/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json +++ b/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json @@ -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" }, { diff --git a/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json b/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json index c610fbbaf3c..2e37744435d 100644 --- a/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json +++ b/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json @@ -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" }, {