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:
commit
916c93f31b
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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: ®istrationv1alpha1.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},
|
||||
|
|
Loading…
Reference in New Issue