mirror of https://github.com/k3s-io/k3s
Add URL beside service
parent
dbcab6d744
commit
a0cb2ce697
|
@ -266,26 +266,60 @@ const (
|
||||||
// WebhookClientConfig contains the information to make a TLS
|
// WebhookClientConfig contains the information to make a TLS
|
||||||
// connection with the webhook
|
// connection with the webhook
|
||||||
type WebhookClientConfig struct {
|
type WebhookClientConfig struct {
|
||||||
// Service is a reference to the service for this webhook. If there is only
|
// `url` gives the location of the webhook, in standard URL form
|
||||||
// one port open for the service, that port will be used. If there are multiple
|
// (`[scheme://]host:port/path`). Exactly one of `url` or `service`
|
||||||
// ports open, port 443 will be used if it is open, otherwise it is an error.
|
// must be specified.
|
||||||
// Required
|
//
|
||||||
Service ServiceReference
|
// The `host` should not refer to a service running in the cluster; use
|
||||||
|
// the `service` field instead. The host might be resolved via external
|
||||||
|
// DNS in some apiservers (e.g., `kube-apiserver` cannot resolve
|
||||||
|
// in-cluster DNS as that would be a layering violation). `host` may
|
||||||
|
// also be an IP address.
|
||||||
|
//
|
||||||
|
// Please note that using `localhost` or `127.0.0.1` as a `host` is
|
||||||
|
// risky unless you take great care to run this webhook on all hosts
|
||||||
|
// which run an apiserver which might need to make calls to this
|
||||||
|
// webhook. Such installs are likely to be non-portable, i.e., not easy
|
||||||
|
// to turn up in a new cluster.
|
||||||
|
//
|
||||||
|
// If the scheme is present, it must be "https://".
|
||||||
|
//
|
||||||
|
// A path is optional, and if present may be any string permissible in
|
||||||
|
// a URL. You may use the path to pass an arbitrary string to the
|
||||||
|
// webhook, for example, a cluster identifier.
|
||||||
|
//
|
||||||
|
// +optional
|
||||||
|
URL *string
|
||||||
|
|
||||||
// URLPath is an optional field that specifies the URL path to use when posting the AdmissionReview object.
|
// `service` is a reference to the service for this webhook. Either
|
||||||
URLPath string
|
// `service` or `url` must be specified.
|
||||||
|
//
|
||||||
|
// If the webhook is running within the cluster, then you should use `service`.
|
||||||
|
//
|
||||||
|
// If there is only one port open for the service, that port will be
|
||||||
|
// used. If there are multiple ports open, port 443 will be used if it
|
||||||
|
// is open, otherwise it is an error.
|
||||||
|
//
|
||||||
|
// +optional
|
||||||
|
Service *ServiceReference
|
||||||
|
|
||||||
// CABundle is a PEM encoded CA bundle which will be used to validate webhook's server certificate.
|
// `caBundle` is a PEM encoded CA bundle which will be used to validate
|
||||||
// Required
|
// the webhook's server certificate.
|
||||||
|
// Required.
|
||||||
CABundle []byte
|
CABundle []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceReference holds a reference to Service.legacy.k8s.io
|
// ServiceReference holds a reference to Service.legacy.k8s.io
|
||||||
type ServiceReference struct {
|
type ServiceReference struct {
|
||||||
// Namespace is the namespace of the service
|
// `namespace` is the namespace of the service.
|
||||||
// Required
|
// Required
|
||||||
Namespace string
|
Namespace string
|
||||||
// Name is the name of the service
|
// `name` is the name of the service.
|
||||||
// Required
|
// Required
|
||||||
Name string
|
Name string
|
||||||
|
|
||||||
|
// `path` is an optional URL path which will be sent in any request to
|
||||||
|
// this service.
|
||||||
|
// +optional
|
||||||
|
Path *string
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package validation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
|
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||||
|
@ -192,29 +193,69 @@ func validateWebhook(hook *admissionregistration.Webhook, fldPath *field.Path) f
|
||||||
allErrors = append(allErrors, field.NotSupported(fldPath.Child("failurePolicy"), *hook.FailurePolicy, supportedFailurePolicies.List()))
|
allErrors = append(allErrors, field.NotSupported(fldPath.Child("failurePolicy"), *hook.FailurePolicy, supportedFailurePolicies.List()))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(hook.ClientConfig.URLPath) != 0 {
|
|
||||||
allErrors = append(allErrors, validateURLPath(fldPath.Child("clientConfig", "urlPath"), hook.ClientConfig.URLPath)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hook.NamespaceSelector != nil {
|
if hook.NamespaceSelector != nil {
|
||||||
allErrors = append(allErrors, metav1validation.ValidateLabelSelector(hook.NamespaceSelector, fldPath.Child("namespaceSelector"))...)
|
allErrors = append(allErrors, metav1validation.ValidateLabelSelector(hook.NamespaceSelector, fldPath.Child("namespaceSelector"))...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allErrors = append(allErrors, validateWebhookClientConfig(fldPath.Child("clientConfig"), &hook.ClientConfig)...)
|
||||||
|
|
||||||
return allErrors
|
return allErrors
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateURLPath(fldPath *field.Path, urlPath string) field.ErrorList {
|
func validateWebhookClientConfig(fldPath *field.Path, cc *admissionregistration.WebhookClientConfig) field.ErrorList {
|
||||||
var allErrors field.ErrorList
|
var allErrors field.ErrorList
|
||||||
|
if (cc.URL == nil) == (cc.Service == nil) {
|
||||||
|
allErrors = append(allErrors, field.Required(fldPath.Child("url"), "exactly one of url or service is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if cc.URL != nil {
|
||||||
|
const form = "; desired format: https://host[/path]"
|
||||||
|
if u, err := url.Parse(*cc.URL); err != nil {
|
||||||
|
allErrors = append(allErrors, field.Required(fldPath.Child("url"), "url must be a valid URL: "+err.Error()+form))
|
||||||
|
} else {
|
||||||
|
if u.Scheme != "" && u.Scheme != "https" {
|
||||||
|
allErrors = append(allErrors, field.Required(fldPath.Child("url"), "'https' is the only allowed URL scheme"+form))
|
||||||
|
}
|
||||||
|
if len(u.Host) == 0 {
|
||||||
|
allErrors = append(allErrors, field.Required(fldPath.Child("url"), "host must be provided"+form))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cc.Service != nil {
|
||||||
|
allErrors = append(allErrors, validateWebhookService(fldPath.Child("service"), cc.Service)...)
|
||||||
|
}
|
||||||
|
return allErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateWebhookService(fldPath *field.Path, svc *admissionregistration.ServiceReference) field.ErrorList {
|
||||||
|
var allErrors field.ErrorList
|
||||||
|
|
||||||
|
if len(svc.Name) == 0 {
|
||||||
|
allErrors = append(allErrors, field.Required(fldPath.Child("name"), "service name is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(svc.Namespace) == 0 {
|
||||||
|
allErrors = append(allErrors, field.Required(fldPath.Child("namespace"), "service namespace is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if svc.Path == nil {
|
||||||
|
return allErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: replace below with url.Parse + verifying that host is empty?
|
||||||
|
|
||||||
|
urlPath := *svc.Path
|
||||||
if urlPath == "/" || len(urlPath) == 0 {
|
if urlPath == "/" || len(urlPath) == 0 {
|
||||||
return allErrors
|
return allErrors
|
||||||
}
|
}
|
||||||
if urlPath == "//" {
|
if urlPath == "//" {
|
||||||
allErrors = append(allErrors, field.Invalid(fldPath, urlPath, "segment[0] may not be empty"))
|
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, "segment[0] may not be empty"))
|
||||||
return allErrors
|
return allErrors
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(urlPath, "/") {
|
if !strings.HasPrefix(urlPath, "/") {
|
||||||
allErrors = append(allErrors, field.Invalid(fldPath, urlPath, "must start with a '/'"))
|
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, "must start with a '/'"))
|
||||||
}
|
}
|
||||||
|
|
||||||
urlPathToCheck := urlPath[1:]
|
urlPathToCheck := urlPath[1:]
|
||||||
|
@ -224,12 +265,12 @@ func validateURLPath(fldPath *field.Path, urlPath string) field.ErrorList {
|
||||||
steps := strings.Split(urlPathToCheck, "/")
|
steps := strings.Split(urlPathToCheck, "/")
|
||||||
for i, step := range steps {
|
for i, step := range steps {
|
||||||
if len(step) == 0 {
|
if len(step) == 0 {
|
||||||
allErrors = append(allErrors, field.Invalid(fldPath, urlPath, fmt.Sprintf("segment[%d] may not be empty", i)))
|
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, fmt.Sprintf("segment[%d] may not be empty", i)))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
failures := validation.IsDNS1123Subdomain(step)
|
failures := validation.IsDNS1123Subdomain(step)
|
||||||
for _, failure := range failures {
|
for _, failure := range failures {
|
||||||
allErrors = append(allErrors, field.Invalid(fldPath, urlPath, fmt.Sprintf("segment[%d]: %v", i, failure)))
|
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, fmt.Sprintf("segment[%d]: %v", i, failure)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -231,6 +231,8 @@ func TestValidateInitializerConfiguration(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func strPtr(s string) *string { return &s }
|
||||||
|
|
||||||
func newValidatingWebhookConfiguration(hooks []admissionregistration.Webhook) *admissionregistration.ValidatingWebhookConfiguration {
|
func newValidatingWebhookConfiguration(hooks []admissionregistration.Webhook) *admissionregistration.ValidatingWebhookConfiguration {
|
||||||
return &admissionregistration.ValidatingWebhookConfiguration{
|
return &admissionregistration.ValidatingWebhookConfiguration{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
@ -243,6 +245,9 @@ func newValidatingWebhookConfiguration(hooks []admissionregistration.Webhook) *a
|
||||||
// TODO: Add TestValidateMutatingWebhookConfiguration to test validation for mutating webhooks.
|
// TODO: Add TestValidateMutatingWebhookConfiguration to test validation for mutating webhooks.
|
||||||
|
|
||||||
func TestValidateValidatingWebhookConfiguration(t *testing.T) {
|
func TestValidateValidatingWebhookConfiguration(t *testing.T) {
|
||||||
|
validClientConfig := admissionregistration.WebhookClientConfig{
|
||||||
|
URL: strPtr("https://example.com"),
|
||||||
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
config *admissionregistration.ValidatingWebhookConfiguration
|
config *admissionregistration.ValidatingWebhookConfiguration
|
||||||
|
@ -253,13 +258,16 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) {
|
||||||
config: newValidatingWebhookConfiguration(
|
config: newValidatingWebhookConfiguration(
|
||||||
[]admissionregistration.Webhook{
|
[]admissionregistration.Webhook{
|
||||||
{
|
{
|
||||||
Name: "webhook.k8s.io",
|
Name: "webhook.k8s.io",
|
||||||
|
ClientConfig: validClientConfig,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "k8s.io",
|
Name: "k8s.io",
|
||||||
|
ClientConfig: validClientConfig,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "",
|
Name: "",
|
||||||
|
ClientConfig: validClientConfig,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
expectedError: `webhooks[1].name: Invalid value: "k8s.io": should be a domain with at least three segments separated by dots, webhooks[2].name: Required value`,
|
expectedError: `webhooks[1].name: Invalid value: "k8s.io": should be a domain with at least three segments separated by dots, webhooks[2].name: Required value`,
|
||||||
|
@ -357,7 +365,8 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) {
|
||||||
config: newValidatingWebhookConfiguration(
|
config: newValidatingWebhookConfiguration(
|
||||||
[]admissionregistration.Webhook{
|
[]admissionregistration.Webhook{
|
||||||
{
|
{
|
||||||
Name: "webhook.k8s.io",
|
Name: "webhook.k8s.io",
|
||||||
|
ClientConfig: validClientConfig,
|
||||||
Rules: []admissionregistration.RuleWithOperations{
|
Rules: []admissionregistration.RuleWithOperations{
|
||||||
{
|
{
|
||||||
Operations: []admissionregistration.OperationType{"CREATE"},
|
Operations: []admissionregistration.OperationType{"CREATE"},
|
||||||
|
@ -376,7 +385,8 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) {
|
||||||
config: newValidatingWebhookConfiguration(
|
config: newValidatingWebhookConfiguration(
|
||||||
[]admissionregistration.Webhook{
|
[]admissionregistration.Webhook{
|
||||||
{
|
{
|
||||||
Name: "webhook.k8s.io",
|
Name: "webhook.k8s.io",
|
||||||
|
ClientConfig: validClientConfig,
|
||||||
Rules: []admissionregistration.RuleWithOperations{
|
Rules: []admissionregistration.RuleWithOperations{
|
||||||
{
|
{
|
||||||
Operations: []admissionregistration.OperationType{"CREATE"},
|
Operations: []admissionregistration.OperationType{"CREATE"},
|
||||||
|
@ -396,7 +406,8 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) {
|
||||||
config: newValidatingWebhookConfiguration(
|
config: newValidatingWebhookConfiguration(
|
||||||
[]admissionregistration.Webhook{
|
[]admissionregistration.Webhook{
|
||||||
{
|
{
|
||||||
Name: "webhook.k8s.io",
|
Name: "webhook.k8s.io",
|
||||||
|
ClientConfig: validClientConfig,
|
||||||
Rules: []admissionregistration.RuleWithOperations{
|
Rules: []admissionregistration.RuleWithOperations{
|
||||||
{
|
{
|
||||||
Operations: []admissionregistration.OperationType{"CREATE"},
|
Operations: []admissionregistration.OperationType{"CREATE"},
|
||||||
|
@ -416,7 +427,8 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) {
|
||||||
config: newValidatingWebhookConfiguration(
|
config: newValidatingWebhookConfiguration(
|
||||||
[]admissionregistration.Webhook{
|
[]admissionregistration.Webhook{
|
||||||
{
|
{
|
||||||
Name: "webhook.k8s.io",
|
Name: "webhook.k8s.io",
|
||||||
|
ClientConfig: validClientConfig,
|
||||||
Rules: []admissionregistration.RuleWithOperations{
|
Rules: []admissionregistration.RuleWithOperations{
|
||||||
{
|
{
|
||||||
Operations: []admissionregistration.OperationType{"CREATE"},
|
Operations: []admissionregistration.OperationType{"CREATE"},
|
||||||
|
@ -435,7 +447,8 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) {
|
||||||
config: newValidatingWebhookConfiguration(
|
config: newValidatingWebhookConfiguration(
|
||||||
[]admissionregistration.Webhook{
|
[]admissionregistration.Webhook{
|
||||||
{
|
{
|
||||||
Name: "webhook.k8s.io",
|
Name: "webhook.k8s.io",
|
||||||
|
ClientConfig: validClientConfig,
|
||||||
Rules: []admissionregistration.RuleWithOperations{
|
Rules: []admissionregistration.RuleWithOperations{
|
||||||
{
|
{
|
||||||
Operations: []admissionregistration.OperationType{"CREATE"},
|
Operations: []admissionregistration.OperationType{"CREATE"},
|
||||||
|
@ -455,7 +468,8 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) {
|
||||||
config: newValidatingWebhookConfiguration(
|
config: newValidatingWebhookConfiguration(
|
||||||
[]admissionregistration.Webhook{
|
[]admissionregistration.Webhook{
|
||||||
{
|
{
|
||||||
Name: "webhook.k8s.io",
|
Name: "webhook.k8s.io",
|
||||||
|
ClientConfig: validClientConfig,
|
||||||
Rules: []admissionregistration.RuleWithOperations{
|
Rules: []admissionregistration.RuleWithOperations{
|
||||||
{
|
{
|
||||||
Operations: []admissionregistration.OperationType{"CREATE"},
|
Operations: []admissionregistration.OperationType{"CREATE"},
|
||||||
|
@ -475,7 +489,8 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) {
|
||||||
config: newValidatingWebhookConfiguration(
|
config: newValidatingWebhookConfiguration(
|
||||||
[]admissionregistration.Webhook{
|
[]admissionregistration.Webhook{
|
||||||
{
|
{
|
||||||
Name: "webhook.k8s.io",
|
Name: "webhook.k8s.io",
|
||||||
|
ClientConfig: validClientConfig,
|
||||||
FailurePolicy: func() *admissionregistration.FailurePolicyType {
|
FailurePolicy: func() *admissionregistration.FailurePolicyType {
|
||||||
r := admissionregistration.FailurePolicyType("other")
|
r := admissionregistration.FailurePolicyType("other")
|
||||||
return &r
|
return &r
|
||||||
|
@ -485,94 +500,202 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) {
|
||||||
expectedError: `webhooks[0].failurePolicy: Unsupported value: "other": supported values: "Fail", "Ignore"`,
|
expectedError: `webhooks[0].failurePolicy: Unsupported value: "other": supported values: "Fail", "Ignore"`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "URLPath must start with slash",
|
name: "both service and URL missing",
|
||||||
|
config: newValidatingWebhookConfiguration(
|
||||||
|
[]admissionregistration.Webhook{
|
||||||
|
{
|
||||||
|
Name: "webhook.k8s.io",
|
||||||
|
ClientConfig: admissionregistration.WebhookClientConfig{},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
expectedError: `exactly one of`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "both service and URL provided",
|
||||||
config: newValidatingWebhookConfiguration(
|
config: newValidatingWebhookConfiguration(
|
||||||
[]admissionregistration.Webhook{
|
[]admissionregistration.Webhook{
|
||||||
{
|
{
|
||||||
Name: "webhook.k8s.io",
|
Name: "webhook.k8s.io",
|
||||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||||
URLPath: "foo/",
|
Service: &admissionregistration.ServiceReference{
|
||||||
|
Namespace: "ns",
|
||||||
|
Name: "n",
|
||||||
|
},
|
||||||
|
URL: strPtr("example.com/k8s/webhook"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
expectedError: `clientConfig.urlPath: Invalid value: "foo/": must start with a '/'`,
|
expectedError: `[0].clientConfig.url: Required value: exactly one of url or service is required`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "URLPath accepts slash",
|
name: "blank URL",
|
||||||
config: newValidatingWebhookConfiguration(
|
config: newValidatingWebhookConfiguration(
|
||||||
[]admissionregistration.Webhook{
|
[]admissionregistration.Webhook{
|
||||||
{
|
{
|
||||||
Name: "webhook.k8s.io",
|
Name: "webhook.k8s.io",
|
||||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||||
URLPath: "/",
|
URL: strPtr(""),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
expectedError: `[0].clientConfig.url: Required value: host must be provided`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong scheme",
|
||||||
|
config: newValidatingWebhookConfiguration(
|
||||||
|
[]admissionregistration.Webhook{
|
||||||
|
{
|
||||||
|
Name: "webhook.k8s.io",
|
||||||
|
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||||
|
URL: strPtr("http://example.com"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
expectedError: `https`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing host",
|
||||||
|
config: newValidatingWebhookConfiguration(
|
||||||
|
[]admissionregistration.Webhook{
|
||||||
|
{
|
||||||
|
Name: "webhook.k8s.io",
|
||||||
|
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||||
|
URL: strPtr("https:///fancy/webhook"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
expectedError: `host must be provided`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "just totally wrong",
|
||||||
|
config: newValidatingWebhookConfiguration(
|
||||||
|
[]admissionregistration.Webhook{
|
||||||
|
{
|
||||||
|
Name: "webhook.k8s.io",
|
||||||
|
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||||
|
URL: strPtr("arg#backwards=thisis?html.index/port:host//:https"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
expectedError: `host must be provided`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "path must start with slash",
|
||||||
|
config: newValidatingWebhookConfiguration(
|
||||||
|
[]admissionregistration.Webhook{
|
||||||
|
{
|
||||||
|
Name: "webhook.k8s.io",
|
||||||
|
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||||
|
Service: &admissionregistration.ServiceReference{
|
||||||
|
Namespace: "ns",
|
||||||
|
Name: "n",
|
||||||
|
Path: strPtr("foo/"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
expectedError: `clientConfig.service.path: Invalid value: "foo/": must start with a '/'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "path accepts slash",
|
||||||
|
config: newValidatingWebhookConfiguration(
|
||||||
|
[]admissionregistration.Webhook{
|
||||||
|
{
|
||||||
|
Name: "webhook.k8s.io",
|
||||||
|
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||||
|
Service: &admissionregistration.ServiceReference{
|
||||||
|
Namespace: "ns",
|
||||||
|
Name: "n",
|
||||||
|
Path: strPtr("/"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
expectedError: ``,
|
expectedError: ``,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "URLPath accepts no trailing slash",
|
name: "path accepts no trailing slash",
|
||||||
config: newValidatingWebhookConfiguration(
|
config: newValidatingWebhookConfiguration(
|
||||||
[]admissionregistration.Webhook{
|
[]admissionregistration.Webhook{
|
||||||
{
|
{
|
||||||
Name: "webhook.k8s.io",
|
Name: "webhook.k8s.io",
|
||||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||||
URLPath: "/foo",
|
Service: &admissionregistration.ServiceReference{
|
||||||
|
Namespace: "ns",
|
||||||
|
Name: "n",
|
||||||
|
Path: strPtr("/foo"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
expectedError: ``,
|
expectedError: ``,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "URLPath fails //",
|
name: "path fails //",
|
||||||
config: newValidatingWebhookConfiguration(
|
config: newValidatingWebhookConfiguration(
|
||||||
[]admissionregistration.Webhook{
|
[]admissionregistration.Webhook{
|
||||||
{
|
{
|
||||||
Name: "webhook.k8s.io",
|
Name: "webhook.k8s.io",
|
||||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||||
URLPath: "//",
|
Service: &admissionregistration.ServiceReference{
|
||||||
|
Namespace: "ns",
|
||||||
|
Name: "n",
|
||||||
|
Path: strPtr("//"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
expectedError: `clientConfig.urlPath: Invalid value: "//": segment[0] may not be empty`,
|
expectedError: `clientConfig.service.path: Invalid value: "//": segment[0] may not be empty`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "URLPath no empty step",
|
name: "path no empty step",
|
||||||
config: newValidatingWebhookConfiguration(
|
config: newValidatingWebhookConfiguration(
|
||||||
[]admissionregistration.Webhook{
|
[]admissionregistration.Webhook{
|
||||||
{
|
{
|
||||||
Name: "webhook.k8s.io",
|
Name: "webhook.k8s.io",
|
||||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||||
URLPath: "/foo//bar/",
|
Service: &admissionregistration.ServiceReference{
|
||||||
|
Namespace: "ns",
|
||||||
|
Name: "n",
|
||||||
|
Path: strPtr("/foo//bar/"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
expectedError: `clientConfig.urlPath: Invalid value: "/foo//bar/": segment[1] may not be empty`,
|
expectedError: `clientConfig.service.path: Invalid value: "/foo//bar/": segment[1] may not be empty`,
|
||||||
}, {
|
}, {
|
||||||
name: "URLPath no empty step 2",
|
name: "path no empty step 2",
|
||||||
config: newValidatingWebhookConfiguration(
|
config: newValidatingWebhookConfiguration(
|
||||||
[]admissionregistration.Webhook{
|
[]admissionregistration.Webhook{
|
||||||
{
|
{
|
||||||
Name: "webhook.k8s.io",
|
Name: "webhook.k8s.io",
|
||||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||||
URLPath: "/foo/bar//",
|
Service: &admissionregistration.ServiceReference{
|
||||||
|
Namespace: "ns",
|
||||||
|
Name: "n",
|
||||||
|
Path: strPtr("/foo/bar//"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
expectedError: `clientConfig.urlPath: Invalid value: "/foo/bar//": segment[2] may not be empty`,
|
expectedError: `clientConfig.service.path: Invalid value: "/foo/bar//": segment[2] may not be empty`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "URLPath no non-subdomain",
|
name: "path no non-subdomain",
|
||||||
config: newValidatingWebhookConfiguration(
|
config: newValidatingWebhookConfiguration(
|
||||||
[]admissionregistration.Webhook{
|
[]admissionregistration.Webhook{
|
||||||
{
|
{
|
||||||
Name: "webhook.k8s.io",
|
Name: "webhook.k8s.io",
|
||||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||||
URLPath: "/apis/foo.bar/v1alpha1/--bad",
|
Service: &admissionregistration.ServiceReference{
|
||||||
|
Namespace: "ns",
|
||||||
|
Name: "n",
|
||||||
|
Path: strPtr("/apis/foo.bar/v1alpha1/--bad"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
expectedError: `clientConfig.urlPath: Invalid value: "/apis/foo.bar/v1alpha1/--bad": segment[3]: a DNS-1123 subdomain`,
|
expectedError: `clientConfig.service.path: Invalid value: "/apis/foo.bar/v1alpha1/--bad": segment[3]: a DNS-1123 subdomain`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
|
|
|
@ -272,26 +272,60 @@ const (
|
||||||
// WebhookClientConfig contains the information to make a TLS
|
// WebhookClientConfig contains the information to make a TLS
|
||||||
// connection with the webhook
|
// connection with the webhook
|
||||||
type WebhookClientConfig struct {
|
type WebhookClientConfig struct {
|
||||||
// Service is a reference to the service for this webhook. If there is only
|
// `url` gives the location of the webhook, in standard URL form
|
||||||
// one port open for the service, that port will be used. If there are multiple
|
// (`[scheme://]host:port/path`). Exactly one of `url` or `service`
|
||||||
// ports open, port 443 will be used if it is open, otherwise it is an error.
|
// must be specified.
|
||||||
// Required
|
//
|
||||||
Service ServiceReference `json:"service" protobuf:"bytes,1,opt,name=service"`
|
// The `host` should not refer to a service running in the cluster; use
|
||||||
|
// the `service` field instead. The host might be resolved via external
|
||||||
|
// DNS in some apiservers (e.g., `kube-apiserver` cannot resolve
|
||||||
|
// in-cluster DNS as that would be a layering violation). `host` may
|
||||||
|
// also be an IP address.
|
||||||
|
//
|
||||||
|
// Please note that using `localhost` or `127.0.0.1` as a `host` is
|
||||||
|
// risky unless you take great care to run this webhook on all hosts
|
||||||
|
// which run an apiserver which might need to make calls to this
|
||||||
|
// webhook. Such installs are likely to be non-portable, i.e., not easy
|
||||||
|
// to turn up in a new cluster.
|
||||||
|
//
|
||||||
|
// If the scheme is present, it must be "https://".
|
||||||
|
//
|
||||||
|
// A path is optional, and if present may be any string permissible in
|
||||||
|
// a URL. You may use the path to pass an arbitrary string to the
|
||||||
|
// webhook, for example, a cluster identifier.
|
||||||
|
//
|
||||||
|
// +optional
|
||||||
|
URL *string `json:"url,omitempty"`
|
||||||
|
|
||||||
// URLPath is an optional field that specifies the URL path to use when posting the AdmissionReview object.
|
// `service` is a reference to the service for this webhook. Either
|
||||||
URLPath string `json:"urlPath" protobuf:"bytes,3,opt,name=urlPath"`
|
// `service` or `url` must be specified.
|
||||||
|
//
|
||||||
|
// If the webhook is running within the cluster, then you should use `service`.
|
||||||
|
//
|
||||||
|
// If there is only one port open for the service, that port will be
|
||||||
|
// used. If there are multiple ports open, port 443 will be used if it
|
||||||
|
// is open, otherwise it is an error.
|
||||||
|
//
|
||||||
|
// +optional
|
||||||
|
Service *ServiceReference `json:"service" protobuf:"bytes,1,opt,name=service"`
|
||||||
|
|
||||||
// CABundle is a PEM encoded CA bundle which will be used to validate webhook's server certificate.
|
// `caBundle` is a PEM encoded CA bundle which will be used to validate
|
||||||
// Required
|
// the webhook's server certificate.
|
||||||
|
// Required.
|
||||||
CABundle []byte `json:"caBundle" protobuf:"bytes,2,opt,name=caBundle"`
|
CABundle []byte `json:"caBundle" protobuf:"bytes,2,opt,name=caBundle"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceReference holds a reference to Service.legacy.k8s.io
|
// ServiceReference holds a reference to Service.legacy.k8s.io
|
||||||
type ServiceReference struct {
|
type ServiceReference struct {
|
||||||
// Namespace is the namespace of the service
|
// `namespace` is the namespace of the service.
|
||||||
// Required
|
// Required
|
||||||
Namespace string `json:"namespace" protobuf:"bytes,1,opt,name=namespace"`
|
Namespace string `json:"namespace" protobuf:"bytes,1,opt,name=namespace"`
|
||||||
// Name is the name of the service
|
// `name` is the name of the service.
|
||||||
// Required
|
// Required
|
||||||
Name string `json:"name" protobuf:"bytes,2,opt,name=name"`
|
Name string `json:"name" protobuf:"bytes,2,opt,name=name"`
|
||||||
|
|
||||||
|
// `path` is an optional URL path which will be sent in any request to
|
||||||
|
// this service.
|
||||||
|
// +optional
|
||||||
|
Path *string `json:"path,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ package webhook
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
@ -55,6 +56,10 @@ const (
|
||||||
defaultCacheSize = 200
|
defaultCacheSize = 200
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNeedServiceOrURL = errors.New("webhook configuration must have either service or URL")
|
||||||
|
)
|
||||||
|
|
||||||
type ErrCallingWebhook struct {
|
type ErrCallingWebhook struct {
|
||||||
WebhookName string
|
WebhookName string
|
||||||
Reason error
|
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) {
|
func (a *GenericAdmissionWebhook) hookClient(h *v1alpha1.Webhook) (*rest.RESTClient, error) {
|
||||||
cacheKey, err := json.Marshal(h.ClientConfig)
|
cacheKey, err := json.Marshal(h.ClientConfig)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if client, ok := a.cache.Get(string(cacheKey)); ok {
|
if client, ok := a.cache.Get(string(cacheKey)); ok {
|
||||||
return client.(*rest.RESTClient), nil
|
return client.(*rest.RESTClient), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
serverName := h.ClientConfig.Service.Name + "." + h.ClientConfig.Service.Namespace + ".svc"
|
complete := func(cfg *rest.Config) (*rest.RESTClient, error) {
|
||||||
restConfig, err := a.authInfoResolver.ClientConfigFor(serverName)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := rest.CopyConfig(restConfig)
|
cfg := rest.CopyConfig(restConfig)
|
||||||
host := serverName + ":443"
|
cfg.Host = u.Host
|
||||||
cfg.Host = "https://" + host
|
cfg.APIPath = u.Path
|
||||||
cfg.APIPath = h.ClientConfig.URLPath
|
// TODO: test if this is needed: cfg.TLSClientConfig.ServerName = u.Host
|
||||||
cfg.TLSClientConfig.ServerName = serverName
|
|
||||||
cfg.TLSClientConfig.CAData = h.ClientConfig.CABundle
|
|
||||||
cfg.ContentConfig.NegotiatedSerializer = a.negotiatedSerializer
|
|
||||||
cfg.ContentConfig.ContentType = runtime.ContentTypeJSON
|
|
||||||
|
|
||||||
delegateDialer := cfg.Dial
|
return complete(cfg)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,33 @@ func (f fakeNamespaceLister) Get(name string) (*corev1.Namespace, error) {
|
||||||
return nil, errors.NewNotFound(corev1.Resource("namespaces"), name)
|
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
|
// TestAdmit tests that GenericAdmissionWebhook#Admit works as expected
|
||||||
func TestAdmit(t *testing.T) {
|
func TestAdmit(t *testing.T) {
|
||||||
scheme := runtime.NewScheme()
|
scheme := runtime.NewScheme()
|
||||||
|
@ -148,6 +175,8 @@ func TestAdmit(t *testing.T) {
|
||||||
UID: "webhook-test",
|
UID: "webhook-test",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ccfgURL := urlConfigGenerator{serverURL}.ccfgURL
|
||||||
|
|
||||||
type test struct {
|
type test struct {
|
||||||
hookSource fakeHookSource
|
hookSource fakeHookSource
|
||||||
path string
|
path string
|
||||||
|
@ -155,6 +184,15 @@ func TestAdmit(t *testing.T) {
|
||||||
errorContains string
|
errorContains string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
matchEverythingRules := []registrationv1alpha1.RuleWithOperations{{
|
||||||
|
Operations: []registrationv1alpha1.OperationType{registrationv1alpha1.OperationAll},
|
||||||
|
Rule: registrationv1alpha1.Rule{
|
||||||
|
APIGroups: []string{"*"},
|
||||||
|
APIVersions: []string{"*"},
|
||||||
|
Resources: []string{"*/*"},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
policyFail := registrationv1alpha1.Fail
|
policyFail := registrationv1alpha1.Fail
|
||||||
policyIgnore := registrationv1alpha1.Ignore
|
policyIgnore := registrationv1alpha1.Ignore
|
||||||
|
|
||||||
|
@ -163,7 +201,7 @@ func TestAdmit(t *testing.T) {
|
||||||
hookSource: fakeHookSource{
|
hookSource: fakeHookSource{
|
||||||
hooks: []registrationv1alpha1.Webhook{{
|
hooks: []registrationv1alpha1.Webhook{{
|
||||||
Name: "nomatch",
|
Name: "nomatch",
|
||||||
ClientConfig: newFakeHookClientConfig("disallow"),
|
ClientConfig: ccfgSVC("disallow"),
|
||||||
Rules: []registrationv1alpha1.RuleWithOperations{{
|
Rules: []registrationv1alpha1.RuleWithOperations{{
|
||||||
Operations: []registrationv1alpha1.OperationType{registrationv1alpha1.Create},
|
Operations: []registrationv1alpha1.OperationType{registrationv1alpha1.Create},
|
||||||
}},
|
}},
|
||||||
|
@ -175,8 +213,8 @@ func TestAdmit(t *testing.T) {
|
||||||
hookSource: fakeHookSource{
|
hookSource: fakeHookSource{
|
||||||
hooks: []registrationv1alpha1.Webhook{{
|
hooks: []registrationv1alpha1.Webhook{{
|
||||||
Name: "allow",
|
Name: "allow",
|
||||||
ClientConfig: newFakeHookClientConfig("allow"),
|
ClientConfig: ccfgSVC("allow"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: matchEverythingRules,
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
expectAllow: true,
|
expectAllow: true,
|
||||||
|
@ -185,8 +223,8 @@ func TestAdmit(t *testing.T) {
|
||||||
hookSource: fakeHookSource{
|
hookSource: fakeHookSource{
|
||||||
hooks: []registrationv1alpha1.Webhook{{
|
hooks: []registrationv1alpha1.Webhook{{
|
||||||
Name: "disallow",
|
Name: "disallow",
|
||||||
ClientConfig: newFakeHookClientConfig("disallow"),
|
ClientConfig: ccfgSVC("disallow"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: matchEverythingRules,
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
errorContains: "without explanation",
|
errorContains: "without explanation",
|
||||||
|
@ -195,8 +233,8 @@ func TestAdmit(t *testing.T) {
|
||||||
hookSource: fakeHookSource{
|
hookSource: fakeHookSource{
|
||||||
hooks: []registrationv1alpha1.Webhook{{
|
hooks: []registrationv1alpha1.Webhook{{
|
||||||
Name: "disallowReason",
|
Name: "disallowReason",
|
||||||
ClientConfig: newFakeHookClientConfig("disallowReason"),
|
ClientConfig: ccfgSVC("disallowReason"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: matchEverythingRules,
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
errorContains: "you shall not pass",
|
errorContains: "you shall not pass",
|
||||||
|
@ -205,7 +243,7 @@ func TestAdmit(t *testing.T) {
|
||||||
hookSource: fakeHookSource{
|
hookSource: fakeHookSource{
|
||||||
hooks: []registrationv1alpha1.Webhook{{
|
hooks: []registrationv1alpha1.Webhook{{
|
||||||
Name: "disallow",
|
Name: "disallow",
|
||||||
ClientConfig: newFakeHookClientConfig("disallow"),
|
ClientConfig: ccfgSVC("disallow"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: newMatchEverythingRules(),
|
||||||
NamespaceSelector: &metav1.LabelSelector{
|
NamespaceSelector: &metav1.LabelSelector{
|
||||||
MatchExpressions: []metav1.LabelSelectorRequirement{{
|
MatchExpressions: []metav1.LabelSelectorRequirement{{
|
||||||
|
@ -222,7 +260,7 @@ func TestAdmit(t *testing.T) {
|
||||||
hookSource: fakeHookSource{
|
hookSource: fakeHookSource{
|
||||||
hooks: []registrationv1alpha1.Webhook{{
|
hooks: []registrationv1alpha1.Webhook{{
|
||||||
Name: "disallow",
|
Name: "disallow",
|
||||||
ClientConfig: newFakeHookClientConfig("disallow"),
|
ClientConfig: ccfgSVC("disallow"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: newMatchEverythingRules(),
|
||||||
NamespaceSelector: &metav1.LabelSelector{
|
NamespaceSelector: &metav1.LabelSelector{
|
||||||
MatchExpressions: []metav1.LabelSelectorRequirement{{
|
MatchExpressions: []metav1.LabelSelectorRequirement{{
|
||||||
|
@ -239,18 +277,18 @@ func TestAdmit(t *testing.T) {
|
||||||
hookSource: fakeHookSource{
|
hookSource: fakeHookSource{
|
||||||
hooks: []registrationv1alpha1.Webhook{{
|
hooks: []registrationv1alpha1.Webhook{{
|
||||||
Name: "internalErr A",
|
Name: "internalErr A",
|
||||||
ClientConfig: newFakeHookClientConfig("internalErr"),
|
ClientConfig: ccfgSVC("internalErr"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: matchEverythingRules,
|
||||||
FailurePolicy: &policyIgnore,
|
FailurePolicy: &policyIgnore,
|
||||||
}, {
|
}, {
|
||||||
Name: "internalErr B",
|
Name: "internalErr B",
|
||||||
ClientConfig: newFakeHookClientConfig("internalErr"),
|
ClientConfig: ccfgSVC("internalErr"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: matchEverythingRules,
|
||||||
FailurePolicy: &policyIgnore,
|
FailurePolicy: &policyIgnore,
|
||||||
}, {
|
}, {
|
||||||
Name: "internalErr C",
|
Name: "internalErr C",
|
||||||
ClientConfig: newFakeHookClientConfig("internalErr"),
|
ClientConfig: ccfgSVC("internalErr"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: matchEverythingRules,
|
||||||
FailurePolicy: &policyIgnore,
|
FailurePolicy: &policyIgnore,
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
|
@ -260,16 +298,16 @@ func TestAdmit(t *testing.T) {
|
||||||
hookSource: fakeHookSource{
|
hookSource: fakeHookSource{
|
||||||
hooks: []registrationv1alpha1.Webhook{{
|
hooks: []registrationv1alpha1.Webhook{{
|
||||||
Name: "internalErr A",
|
Name: "internalErr A",
|
||||||
ClientConfig: newFakeHookClientConfig("internalErr"),
|
ClientConfig: ccfgSVC("internalErr"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: matchEverythingRules,
|
||||||
}, {
|
}, {
|
||||||
Name: "internalErr B",
|
Name: "internalErr B",
|
||||||
ClientConfig: newFakeHookClientConfig("internalErr"),
|
ClientConfig: ccfgSVC("internalErr"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: matchEverythingRules,
|
||||||
}, {
|
}, {
|
||||||
Name: "internalErr C",
|
Name: "internalErr C",
|
||||||
ClientConfig: newFakeHookClientConfig("internalErr"),
|
ClientConfig: ccfgSVC("internalErr"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: matchEverythingRules,
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
expectAllow: false,
|
expectAllow: false,
|
||||||
|
@ -278,23 +316,45 @@ func TestAdmit(t *testing.T) {
|
||||||
hookSource: fakeHookSource{
|
hookSource: fakeHookSource{
|
||||||
hooks: []registrationv1alpha1.Webhook{{
|
hooks: []registrationv1alpha1.Webhook{{
|
||||||
Name: "internalErr A",
|
Name: "internalErr A",
|
||||||
ClientConfig: newFakeHookClientConfig("internalErr"),
|
ClientConfig: ccfgSVC("internalErr"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: matchEverythingRules,
|
||||||
FailurePolicy: &policyFail,
|
FailurePolicy: &policyFail,
|
||||||
}, {
|
}, {
|
||||||
Name: "internalErr B",
|
Name: "internalErr B",
|
||||||
ClientConfig: newFakeHookClientConfig("internalErr"),
|
ClientConfig: ccfgSVC("internalErr"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: matchEverythingRules,
|
||||||
FailurePolicy: &policyFail,
|
FailurePolicy: &policyFail,
|
||||||
}, {
|
}, {
|
||||||
Name: "internalErr C",
|
Name: "internalErr C",
|
||||||
ClientConfig: newFakeHookClientConfig("internalErr"),
|
ClientConfig: ccfgSVC("internalErr"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: matchEverythingRules,
|
||||||
FailurePolicy: &policyFail,
|
FailurePolicy: &policyFail,
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
expectAllow: false,
|
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 {
|
for name, tt := range table {
|
||||||
|
@ -378,6 +438,7 @@ func TestAdmitCachedClient(t *testing.T) {
|
||||||
Name: "webhook-test",
|
Name: "webhook-test",
|
||||||
UID: "webhook-test",
|
UID: "webhook-test",
|
||||||
}
|
}
|
||||||
|
ccfgURL := urlConfigGenerator{serverURL}.ccfgURL
|
||||||
|
|
||||||
type test struct {
|
type test struct {
|
||||||
name string
|
name string
|
||||||
|
@ -393,7 +454,7 @@ func TestAdmitCachedClient(t *testing.T) {
|
||||||
hookSource: fakeHookSource{
|
hookSource: fakeHookSource{
|
||||||
hooks: []registrationv1alpha1.Webhook{{
|
hooks: []registrationv1alpha1.Webhook{{
|
||||||
Name: "cache1",
|
Name: "cache1",
|
||||||
ClientConfig: newFakeHookClientConfig("allow"),
|
ClientConfig: ccfgSVC("allow"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: newMatchEverythingRules(),
|
||||||
FailurePolicy: &policyIgnore,
|
FailurePolicy: &policyIgnore,
|
||||||
}},
|
}},
|
||||||
|
@ -406,7 +467,7 @@ func TestAdmitCachedClient(t *testing.T) {
|
||||||
hookSource: fakeHookSource{
|
hookSource: fakeHookSource{
|
||||||
hooks: []registrationv1alpha1.Webhook{{
|
hooks: []registrationv1alpha1.Webhook{{
|
||||||
Name: "cache2",
|
Name: "cache2",
|
||||||
ClientConfig: newFakeHookClientConfig("internalErr"),
|
ClientConfig: ccfgSVC("internalErr"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: newMatchEverythingRules(),
|
||||||
FailurePolicy: &policyIgnore,
|
FailurePolicy: &policyIgnore,
|
||||||
}},
|
}},
|
||||||
|
@ -419,7 +480,33 @@ func TestAdmitCachedClient(t *testing.T) {
|
||||||
hookSource: fakeHookSource{
|
hookSource: fakeHookSource{
|
||||||
hooks: []registrationv1alpha1.Webhook{{
|
hooks: []registrationv1alpha1.Webhook{{
|
||||||
Name: "cache3",
|
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(),
|
Rules: newMatchEverythingRules(),
|
||||||
FailurePolicy: &policyIgnore,
|
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 {
|
func newMatchEverythingRules() []registrationv1alpha1.RuleWithOperations {
|
||||||
return []registrationv1alpha1.RuleWithOperations{{
|
return []registrationv1alpha1.RuleWithOperations{{
|
||||||
Operations: []registrationv1alpha1.OperationType{registrationv1alpha1.OperationAll},
|
Operations: []registrationv1alpha1.OperationType{registrationv1alpha1.OperationAll},
|
||||||
|
|
|
@ -218,6 +218,8 @@ func deployWebhookAndService(f *framework.Framework, image string, context *cert
|
||||||
framework.ExpectNoError(err, "waiting for service %s/%s have %d endpoint", namespace, serviceName, 1)
|
framework.ExpectNoError(err, "waiting for service %s/%s have %d endpoint", namespace, serviceName, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func strPtr(s string) *string { return &s }
|
||||||
|
|
||||||
func registerWebhook(f *framework.Framework, context *certContext) {
|
func registerWebhook(f *framework.Framework, context *certContext) {
|
||||||
client := f.ClientSet
|
client := f.ClientSet
|
||||||
By("Registering the webhook via the AdmissionRegistration API")
|
By("Registering the webhook via the AdmissionRegistration API")
|
||||||
|
@ -239,11 +241,11 @@ func registerWebhook(f *framework.Framework, context *certContext) {
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
ClientConfig: v1alpha1.WebhookClientConfig{
|
ClientConfig: v1alpha1.WebhookClientConfig{
|
||||||
Service: v1alpha1.ServiceReference{
|
Service: &v1alpha1.ServiceReference{
|
||||||
Namespace: namespace,
|
Namespace: namespace,
|
||||||
Name: serviceName,
|
Name: serviceName,
|
||||||
|
Path: strPtr("/pods"),
|
||||||
},
|
},
|
||||||
URLPath: "/pods",
|
|
||||||
CABundle: context.signingCert,
|
CABundle: context.signingCert,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -268,11 +270,11 @@ func registerWebhook(f *framework.Framework, context *certContext) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ClientConfig: v1alpha1.WebhookClientConfig{
|
ClientConfig: v1alpha1.WebhookClientConfig{
|
||||||
Service: v1alpha1.ServiceReference{
|
Service: &v1alpha1.ServiceReference{
|
||||||
Namespace: namespace,
|
Namespace: namespace,
|
||||||
Name: serviceName,
|
Name: serviceName,
|
||||||
|
Path: strPtr("/configmaps"),
|
||||||
},
|
},
|
||||||
URLPath: "/configmaps",
|
|
||||||
CABundle: context.signingCert,
|
CABundle: context.signingCert,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -382,11 +382,11 @@ var etcdStorageData = map[schema.GroupVersionResource]struct {
|
||||||
expectedEtcdPath: "/registry/initializerconfigurations/ic1",
|
expectedEtcdPath: "/registry/initializerconfigurations/ic1",
|
||||||
},
|
},
|
||||||
gvr("admissionregistration.k8s.io", "v1alpha1", "validatingwebhookconfigurations"): {
|
gvr("admissionregistration.k8s.io", "v1alpha1", "validatingwebhookconfigurations"): {
|
||||||
stub: `{"metadata":{"name":"hook1","creationTimestamp":null},"webhooks":[{"name":"externaladmissionhook.k8s.io","clientConfig":{"service":{"namespace":"","name":""},"caBundle":null},"rules":[{"operations":["CREATE"],"apiGroups":["group"],"apiVersions":["version"],"resources":["resource"]}],"failurePolicy":"Ignore"}]}`,
|
stub: `{"metadata":{"name":"hook1","creationTimestamp":null},"webhooks":[{"name":"externaladmissionhook.k8s.io","clientConfig":{"service":{"namespace":"ns","name":"n"},"caBundle":null},"rules":[{"operations":["CREATE"],"apiGroups":["group"],"apiVersions":["version"],"resources":["resource"]}],"failurePolicy":"Ignore"}]}`,
|
||||||
expectedEtcdPath: "/registry/validatingwebhookconfigurations/hook1",
|
expectedEtcdPath: "/registry/validatingwebhookconfigurations/hook1",
|
||||||
},
|
},
|
||||||
gvr("admissionregistration.k8s.io", "v1alpha1", "mutatingwebhookconfigurations"): {
|
gvr("admissionregistration.k8s.io", "v1alpha1", "mutatingwebhookconfigurations"): {
|
||||||
stub: `{"metadata":{"name":"hook1","creationTimestamp":null},"webhooks":[{"name":"externaladmissionhook.k8s.io","clientConfig":{"service":{"namespace":"","name":""},"caBundle":null},"rules":[{"operations":["CREATE"],"apiGroups":["group"],"apiVersions":["version"],"resources":["resource"]}],"failurePolicy":"Ignore"}]}`,
|
stub: `{"metadata":{"name":"hook1","creationTimestamp":null},"webhooks":[{"name":"externaladmissionhook.k8s.io","clientConfig":{"service":{"namespace":"ns","name":"n"},"caBundle":null},"rules":[{"operations":["CREATE"],"apiGroups":["group"],"apiVersions":["version"],"resources":["resource"]}],"failurePolicy":"Ignore"}]}`,
|
||||||
expectedEtcdPath: "/registry/mutatingwebhookconfigurations/hook1",
|
expectedEtcdPath: "/registry/mutatingwebhookconfigurations/hook1",
|
||||||
},
|
},
|
||||||
// --
|
// --
|
||||||
|
|
Loading…
Reference in New Issue