Merge pull request #54889 from lavalamp/wh-api

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>.

Fix webhook API to also support URLs

ref: https://github.com/kubernetes/features/issues/492

```release-note
The dynamic admission webhook now supports a URL in addition to a service reference, to accommodate out-of-cluster webhooks.
```

Kubernetes-commit: e93819049db49694718bc9c96e67050d366c6f63
This commit is contained in:
Kubernetes Publisher 2017-11-11 23:01:39 -08:00
commit 916c93f31b
2 changed files with 179 additions and 74 deletions

View File

@ -20,6 +20,7 @@ package webhook
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
@ -55,6 +56,10 @@ const (
defaultCacheSize = 200
)
var (
ErrNeedServiceOrURL = errors.New("webhook configuration must have either service or URL")
)
type ErrCallingWebhook struct {
WebhookName string
Reason error
@ -395,47 +400,71 @@ func toStatusErr(name string, result *metav1.Status) *apierrors.StatusError {
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
}
serverName := h.ClientConfig.Service.Name + "." + h.ClientConfig.Service.Namespace + ".svc"
restConfig, err := a.authInfoResolver.ClientConfigFor(serverName)
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)
host := serverName + ":443"
cfg.Host = "https://" + host
cfg.APIPath = h.ClientConfig.URLPath
cfg.TLSClientConfig.ServerName = serverName
cfg.TLSClientConfig.CAData = h.ClientConfig.CABundle
cfg.ContentConfig.NegotiatedSerializer = a.negotiatedSerializer
cfg.ContentConfig.ContentType = runtime.ContentTypeJSON
cfg.Host = u.Host
cfg.APIPath = u.Path
// TODO: test if this is needed: cfg.TLSClientConfig.ServerName = u.Host
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(h.ClientConfig.Service.Namespace, h.ClientConfig.Service.Name)
if err != nil {
return nil, err
}
addr = u.Host
}
return delegateDialer(network, addr)
}
client, err := rest.UnversionedRESTClientFor(cfg)
if err == nil {
a.cache.Add(string(cacheKey), client)
}
return client, err
return complete(cfg)
}

View File

@ -89,6 +89,33 @@ func (f fakeNamespaceLister) Get(name string) (*corev1.Namespace, error) {
return nil, errors.NewNotFound(corev1.Resource("namespaces"), name)
}
// ccfgSVC returns a client config using the service reference mechanism.
func ccfgSVC(urlPath string) registrationv1alpha1.WebhookClientConfig {
return registrationv1alpha1.WebhookClientConfig{
Service: &registrationv1alpha1.ServiceReference{
Name: "webhook-test",
Namespace: "default",
Path: &urlPath,
},
CABundle: caCert,
}
}
type urlConfigGenerator struct {
baseURL *url.URL
}
// ccfgURL returns a client config using the URL mechanism.
func (c urlConfigGenerator) ccfgURL(urlPath string) registrationv1alpha1.WebhookClientConfig {
u2 := *c.baseURL
u2.Path = urlPath
urlString := u2.String()
return registrationv1alpha1.WebhookClientConfig{
URL: &urlString,
CABundle: caCert,
}
}
// TestAdmit tests that GenericAdmissionWebhook#Admit works as expected
func TestAdmit(t *testing.T) {
scheme := runtime.NewScheme()
@ -148,6 +175,8 @@ func TestAdmit(t *testing.T) {
UID: "webhook-test",
}
ccfgURL := urlConfigGenerator{serverURL}.ccfgURL
type test struct {
hookSource fakeHookSource
path string
@ -155,6 +184,15 @@ func TestAdmit(t *testing.T) {
errorContains string
}
matchEverythingRules := []registrationv1alpha1.RuleWithOperations{{
Operations: []registrationv1alpha1.OperationType{registrationv1alpha1.OperationAll},
Rule: registrationv1alpha1.Rule{
APIGroups: []string{"*"},
APIVersions: []string{"*"},
Resources: []string{"*/*"},
},
}}
policyFail := registrationv1alpha1.Fail
policyIgnore := registrationv1alpha1.Ignore
@ -163,7 +201,7 @@ func TestAdmit(t *testing.T) {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "nomatch",
ClientConfig: newFakeHookClientConfig("disallow"),
ClientConfig: ccfgSVC("disallow"),
Rules: []registrationv1alpha1.RuleWithOperations{{
Operations: []registrationv1alpha1.OperationType{registrationv1alpha1.Create},
}},
@ -175,8 +213,8 @@ func TestAdmit(t *testing.T) {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "allow",
ClientConfig: newFakeHookClientConfig("allow"),
Rules: newMatchEverythingRules(),
ClientConfig: ccfgSVC("allow"),
Rules: matchEverythingRules,
}},
},
expectAllow: true,
@ -185,8 +223,8 @@ func TestAdmit(t *testing.T) {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "disallow",
ClientConfig: newFakeHookClientConfig("disallow"),
Rules: newMatchEverythingRules(),
ClientConfig: ccfgSVC("disallow"),
Rules: matchEverythingRules,
}},
},
errorContains: "without explanation",
@ -195,8 +233,8 @@ func TestAdmit(t *testing.T) {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "disallowReason",
ClientConfig: newFakeHookClientConfig("disallowReason"),
Rules: newMatchEverythingRules(),
ClientConfig: ccfgSVC("disallowReason"),
Rules: matchEverythingRules,
}},
},
errorContains: "you shall not pass",
@ -205,7 +243,7 @@ func TestAdmit(t *testing.T) {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "disallow",
ClientConfig: newFakeHookClientConfig("disallow"),
ClientConfig: ccfgSVC("disallow"),
Rules: newMatchEverythingRules(),
NamespaceSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{{
@ -222,7 +260,7 @@ func TestAdmit(t *testing.T) {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "disallow",
ClientConfig: newFakeHookClientConfig("disallow"),
ClientConfig: ccfgSVC("disallow"),
Rules: newMatchEverythingRules(),
NamespaceSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{{
@ -239,18 +277,18 @@ func TestAdmit(t *testing.T) {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "internalErr A",
ClientConfig: newFakeHookClientConfig("internalErr"),
Rules: newMatchEverythingRules(),
ClientConfig: ccfgSVC("internalErr"),
Rules: matchEverythingRules,
FailurePolicy: &policyIgnore,
}, {
Name: "internalErr B",
ClientConfig: newFakeHookClientConfig("internalErr"),
Rules: newMatchEverythingRules(),
ClientConfig: ccfgSVC("internalErr"),
Rules: matchEverythingRules,
FailurePolicy: &policyIgnore,
}, {
Name: "internalErr C",
ClientConfig: newFakeHookClientConfig("internalErr"),
Rules: newMatchEverythingRules(),
ClientConfig: ccfgSVC("internalErr"),
Rules: matchEverythingRules,
FailurePolicy: &policyIgnore,
}},
},
@ -260,16 +298,16 @@ func TestAdmit(t *testing.T) {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "internalErr A",
ClientConfig: newFakeHookClientConfig("internalErr"),
Rules: newMatchEverythingRules(),
ClientConfig: ccfgSVC("internalErr"),
Rules: matchEverythingRules,
}, {
Name: "internalErr B",
ClientConfig: newFakeHookClientConfig("internalErr"),
Rules: newMatchEverythingRules(),
ClientConfig: ccfgSVC("internalErr"),
Rules: matchEverythingRules,
}, {
Name: "internalErr C",
ClientConfig: newFakeHookClientConfig("internalErr"),
Rules: newMatchEverythingRules(),
ClientConfig: ccfgSVC("internalErr"),
Rules: matchEverythingRules,
}},
},
expectAllow: false,
@ -278,23 +316,45 @@ func TestAdmit(t *testing.T) {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "internalErr A",
ClientConfig: newFakeHookClientConfig("internalErr"),
Rules: newMatchEverythingRules(),
ClientConfig: ccfgSVC("internalErr"),
Rules: matchEverythingRules,
FailurePolicy: &policyFail,
}, {
Name: "internalErr B",
ClientConfig: newFakeHookClientConfig("internalErr"),
Rules: newMatchEverythingRules(),
ClientConfig: ccfgSVC("internalErr"),
Rules: matchEverythingRules,
FailurePolicy: &policyFail,
}, {
Name: "internalErr C",
ClientConfig: newFakeHookClientConfig("internalErr"),
Rules: newMatchEverythingRules(),
ClientConfig: ccfgSVC("internalErr"),
Rules: matchEverythingRules,
FailurePolicy: &policyFail,
}},
},
expectAllow: false,
},
"match & allow (url)": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "allow",
ClientConfig: ccfgURL("allow"),
Rules: matchEverythingRules,
}},
},
expectAllow: true,
},
"match & disallow (url)": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "disallow",
ClientConfig: ccfgURL("disallow"),
Rules: matchEverythingRules,
}},
},
errorContains: "without explanation",
},
// No need to test everything with the url case, since only the
// connection is different.
}
for name, tt := range table {
@ -378,6 +438,7 @@ func TestAdmitCachedClient(t *testing.T) {
Name: "webhook-test",
UID: "webhook-test",
}
ccfgURL := urlConfigGenerator{serverURL}.ccfgURL
type test struct {
name string
@ -393,7 +454,7 @@ func TestAdmitCachedClient(t *testing.T) {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "cache1",
ClientConfig: newFakeHookClientConfig("allow"),
ClientConfig: ccfgSVC("allow"),
Rules: newMatchEverythingRules(),
FailurePolicy: &policyIgnore,
}},
@ -406,7 +467,7 @@ func TestAdmitCachedClient(t *testing.T) {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "cache2",
ClientConfig: newFakeHookClientConfig("internalErr"),
ClientConfig: ccfgSVC("internalErr"),
Rules: newMatchEverythingRules(),
FailurePolicy: &policyIgnore,
}},
@ -419,7 +480,33 @@ func TestAdmitCachedClient(t *testing.T) {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "cache3",
ClientConfig: newFakeHookClientConfig("allow"),
ClientConfig: ccfgSVC("allow"),
Rules: newMatchEverythingRules(),
FailurePolicy: &policyIgnore,
}},
},
expectAllow: true,
expectCache: false,
},
{
name: "cache 4",
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "cache4",
ClientConfig: ccfgURL("allow"),
Rules: newMatchEverythingRules(),
FailurePolicy: &policyIgnore,
}},
},
expectAllow: true,
expectCache: true,
},
{
name: "cache 5",
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "cache5",
ClientConfig: ccfgURL("allow"),
Rules: newMatchEverythingRules(),
FailurePolicy: &policyIgnore,
}},
@ -581,17 +668,6 @@ func TestToStatusErr(t *testing.T) {
}
}
func newFakeHookClientConfig(urlPath string) registrationv1alpha1.WebhookClientConfig {
return registrationv1alpha1.WebhookClientConfig{
Service: registrationv1alpha1.ServiceReference{
Name: "webhook-test",
Namespace: "default",
},
URLPath: urlPath,
CABundle: caCert,
}
}
func newMatchEverythingRules() []registrationv1alpha1.RuleWithOperations {
return []registrationv1alpha1.RuleWithOperations{{
Operations: []registrationv1alpha1.OperationType{registrationv1alpha1.OperationAll},