From 1e2c7fdd8806d65dc384687c5e365619f9bd2df8 Mon Sep 17 00:00:00 2001 From: p0lyn0mial Date: Mon, 18 Sep 2017 21:56:38 +0200 Subject: [PATCH] default service resolver for webhook admission the rationale behind is that webhook plugins names can be resolved by a dns server working inside a cluster. --- pkg/kubeapiserver/admission/initializer.go | 3 - plugin/pkg/admission/webhook/BUILD | 2 + plugin/pkg/admission/webhook/admission.go | 7 ++- .../pkg/admission/webhook/serviceresolver.go | 42 ++++++++++++++ .../admission/webhook/serviceresolver_test.go | 57 +++++++++++++++++++ 5 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 plugin/pkg/admission/webhook/serviceresolver.go create mode 100644 plugin/pkg/admission/webhook/serviceresolver_test.go diff --git a/pkg/kubeapiserver/admission/initializer.go b/pkg/kubeapiserver/admission/initializer.go index fed03e9db2d..1c62cfec2af 100644 --- a/pkg/kubeapiserver/admission/initializer.go +++ b/pkg/kubeapiserver/admission/initializer.go @@ -188,9 +188,6 @@ func (i *PluginInitializer) Initialize(plugin admission.Interface) { } if wants, ok := plugin.(WantsServiceResolver); ok { - if i.serviceResolver == nil { - panic("An admission plugin wants the service resolver, but it was not provided.") - } wants.SetServiceResolver(i.serviceResolver) } diff --git a/plugin/pkg/admission/webhook/BUILD b/plugin/pkg/admission/webhook/BUILD index 0221e3ffbe3..c90a9320dab 100644 --- a/plugin/pkg/admission/webhook/BUILD +++ b/plugin/pkg/admission/webhook/BUILD @@ -12,6 +12,7 @@ go_test( "admission_test.go", "certs_test.go", "rules_test.go", + "serviceresolver_test.go", ], library = ":go_default_library", deps = [ @@ -32,6 +33,7 @@ go_library( "admission.go", "doc.go", "rules.go", + "serviceresolver.go", ], deps = [ "//pkg/api:go_default_library", diff --git a/plugin/pkg/admission/webhook/admission.go b/plugin/pkg/admission/webhook/admission.go index fa208cdb78b..63dd7903c1d 100644 --- a/plugin/pkg/admission/webhook/admission.go +++ b/plugin/pkg/admission/webhook/admission.go @@ -97,6 +97,7 @@ func NewGenericAdmissionWebhook() (*GenericAdmissionWebhook, error) { negotiatedSerializer: serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{ Serializer: api.Codecs.LegacyCodec(admissionv1alpha1.SchemeGroupVersion), }), + serviceResolver: defaultServiceResolver{}, }, nil } @@ -121,8 +122,12 @@ func (a *GenericAdmissionWebhook) SetProxyTransport(pt *http.Transport) { a.proxyTransport = pt } +// 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 admissioninit.ServiceResolver) { - a.serviceResolver = sr + if sr != nil { + a.serviceResolver = sr + } } func (a *GenericAdmissionWebhook) SetClientCert(cert, key []byte) { diff --git a/plugin/pkg/admission/webhook/serviceresolver.go b/plugin/pkg/admission/webhook/serviceresolver.go new file mode 100644 index 00000000000..018402934fe --- /dev/null +++ b/plugin/pkg/admission/webhook/serviceresolver.go @@ -0,0 +1,42 @@ +/* +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 webhook checks a webhook for configured operation admission +package webhook + +import ( + "errors" + "fmt" + "net/url" + + admissioninit "k8s.io/kubernetes/pkg/kubeapiserver/admission" +) + +type defaultServiceResolver struct{} + +var _ admissioninit.ServiceResolver = 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: +// name=ross namespace=andromeda resolves to https://ross.andromeda.svc +func (sr defaultServiceResolver) ResolveEndpoint(namespace, name string) (*url.URL, error) { + if len(name) == 0 || len(namespace) == 0 { + return &url.URL{}, errors.New("cannot resolve an empty service name or namespace") + } + + return url.Parse(fmt.Sprintf("https://%s.%s.svc", name, namespace)) +} diff --git a/plugin/pkg/admission/webhook/serviceresolver_test.go b/plugin/pkg/admission/webhook/serviceresolver_test.go new file mode 100644 index 00000000000..3fb2f7a53c5 --- /dev/null +++ b/plugin/pkg/admission/webhook/serviceresolver_test.go @@ -0,0 +1,57 @@ +/* +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 webhook checks a webhook for configured operation admission +package webhook + +import ( + "fmt" + "testing" +) + +func TestDefaultServiceResolver(t *testing.T) { + scenarios := []struct { + serviceName string + serviceNamespace string + expectedOutput string + expectError bool + }{ + // scenario 1: a service name along with a namespace resolves + {serviceName: "ross", serviceNamespace: "andromeda", expectedOutput: "https://ross.andromeda.svc"}, + // scenario 2: a service name without a namespace does not resolve + {serviceName: "ross", expectError: true}, + // scenario 3: cannot resolve an empty service name + {serviceNamespace: "andromeda", expectError: true}, + } + + // act + for index, scenario := range scenarios { + t.Run(fmt.Sprintf("scenario %d", index), func(t *testing.T) { + target := defaultServiceResolver{} + serviceURL, err := target.ResolveEndpoint(scenario.serviceNamespace, scenario.serviceName) + + if err != nil && !scenario.expectError { + t.Errorf("unexpected error has occurred = %v", err) + } + if err == nil && scenario.expectError { + t.Error("expected an error but got nothing") + } + if serviceURL.String() != scenario.expectedOutput { + t.Errorf("expected = %s, got = %s", scenario.expectedOutput, serviceURL.String()) + } + }) + } +}