From 2d35015235c25f8c91d109a51dc05aa4693065a8 Mon Sep 17 00:00:00 2001 From: Monis Khan Date: Fri, 23 Jul 2021 11:15:47 -0400 Subject: [PATCH] webhook: use rest.Config instead of kubeconfig file as input This change updates the generic webhook logic to use a rest.Config as its input instead of a kubeconfig file. This exposes all of the rest.Config knobs to the caller instead of the more limited set available through the kubeconfig format. This is useful when this code is being used as a library outside of core Kubernetes. For example, a downstream consumer may want to override the webhook's internals such as its TLS configuration. Signed-off-by: Monis Khan Kubernetes-commit: fef7d0ef1e1fbff65e8d445256036704bb9dbcbd --- pkg/util/webhook/webhook.go | 55 ++++++++++--------- pkg/util/webhook/webhook_test.go | 40 +++++++++++--- plugin/pkg/audit/webhook/webhook.go | 8 ++- .../authenticator/token/webhook/webhook.go | 17 +++--- .../token/webhook/webhook_v1_test.go | 8 ++- .../token/webhook/webhook_v1beta1_test.go | 8 ++- plugin/pkg/authorizer/webhook/webhook.go | 13 ++--- .../pkg/authorizer/webhook/webhook_v1_test.go | 13 ++++- .../webhook/webhook_v1beta1_test.go | 13 ++++- 9 files changed, 116 insertions(+), 59 deletions(-) diff --git a/pkg/util/webhook/webhook.go b/pkg/util/webhook/webhook.go index a17ab8f48..80de7d8b0 100644 --- a/pkg/util/webhook/webhook.go +++ b/pkg/util/webhook/webhook.go @@ -72,42 +72,19 @@ func DefaultShouldRetry(err error) bool { return false } -// NewGenericWebhook creates a new GenericWebhook from the provided kubeconfig file. -func NewGenericWebhook(scheme *runtime.Scheme, codecFactory serializer.CodecFactory, kubeConfigFile string, groupVersions []schema.GroupVersion, retryBackoff wait.Backoff, customDial utilnet.DialFunc) (*GenericWebhook, error) { - return newGenericWebhook(scheme, codecFactory, kubeConfigFile, groupVersions, retryBackoff, defaultRequestTimeout, customDial) -} - -func newGenericWebhook(scheme *runtime.Scheme, codecFactory serializer.CodecFactory, kubeConfigFile string, groupVersions []schema.GroupVersion, retryBackoff wait.Backoff, requestTimeout time.Duration, customDial utilnet.DialFunc) (*GenericWebhook, error) { +// NewGenericWebhook creates a new GenericWebhook from the provided rest.Config. +func NewGenericWebhook(scheme *runtime.Scheme, codecFactory serializer.CodecFactory, config *rest.Config, groupVersions []schema.GroupVersion, retryBackoff wait.Backoff) (*GenericWebhook, error) { for _, groupVersion := range groupVersions { if !scheme.IsVersionRegistered(groupVersion) { return nil, fmt.Errorf("webhook plugin requires enabling extension resource: %s", groupVersion) } } - loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() - loadingRules.ExplicitPath = kubeConfigFile - loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{}) - - clientConfig, err := loader.ClientConfig() - if err != nil { - return nil, err - } - - // Kubeconfigs can't set a timeout, this can only be set through a command line flag. - // - // https://github.com/kubernetes/client-go/blob/master/tools/clientcmd/overrides.go - // - // Set this to something reasonable so request to webhooks don't hang forever. - clientConfig.Timeout = requestTimeout - - // Avoid client-side rate limiting talking to the webhook backend. - // Rate limiting should happen when deciding how many requests to serve. - clientConfig.QPS = -1 + clientConfig := rest.CopyConfig(config) codec := codecFactory.LegacyCodec(groupVersions...) clientConfig.ContentConfig.NegotiatedSerializer = serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: codec}) - clientConfig.Dial = customDial clientConfig.Wrap(x509metrics.NewMissingSANRoundTripperWrapperConstructor(x509MissingSANCounter)) restClient, err := rest.UnversionedRESTClientFor(clientConfig) @@ -162,3 +139,29 @@ func WithExponentialBackoff(ctx context.Context, retryBackoff wait.Backoff, webh return nil } } + +func LoadKubeconfig(kubeConfigFile string, customDial utilnet.DialFunc) (*rest.Config, error) { + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + loadingRules.ExplicitPath = kubeConfigFile + loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{}) + + clientConfig, err := loader.ClientConfig() + if err != nil { + return nil, err + } + + clientConfig.Dial = customDial + + // Kubeconfigs can't set a timeout, this can only be set through a command line flag. + // + // https://github.com/kubernetes/client-go/blob/master/tools/clientcmd/overrides.go + // + // Set this to something reasonable so request to webhooks don't hang forever. + clientConfig.Timeout = defaultRequestTimeout + + // Avoid client-side rate limiting talking to the webhook backend. + // Rate limiting should happen when deciding how many requests to serve. + clientConfig.QPS = -1 + + return clientConfig, nil +} diff --git a/pkg/util/webhook/webhook_test.go b/pkg/util/webhook/webhook_test.go index 475c7e3cf..016f9744b 100644 --- a/pkg/util/webhook/webhook_test.go +++ b/pkg/util/webhook/webhook_test.go @@ -288,13 +288,18 @@ MIIDGTCCAgGgAwIBAgIUOS2M kubeConfig.CurrentContext = tt.currentContext kubeConfigFile, err := newKubeConfigFile(kubeConfig) - - if err == nil { - defer os.Remove(kubeConfigFile) - - _, err = NewGenericWebhook(runtime.NewScheme(), scheme.Codecs, kubeConfigFile, groupVersions, retryBackoff, nil) + if err != nil { + return err } + defer os.Remove(kubeConfigFile) + + config, err := LoadKubeconfig(kubeConfigFile, nil) + if err != nil { + return err + } + + _, err = NewGenericWebhook(runtime.NewScheme(), scheme.Codecs, config, groupVersions, retryBackoff) return err }() @@ -316,7 +321,7 @@ MIIDGTCCAgGgAwIBAgIUOS2M // TestMissingKubeConfigFile ensures that a kube config path to a missing file is handled properly func TestMissingKubeConfigFile(t *testing.T) { kubeConfigPath := "/some/missing/path" - _, err := NewGenericWebhook(runtime.NewScheme(), scheme.Codecs, kubeConfigPath, groupVersions, retryBackoff, nil) + _, err := LoadKubeconfig(kubeConfigPath, nil) if err == nil { t.Errorf("creating the webhook should had failed") @@ -445,7 +450,12 @@ func TestTLSConfig(t *testing.T) { defer os.Remove(configFile) - wh, err := NewGenericWebhook(runtime.NewScheme(), scheme.Codecs, configFile, groupVersions, retryBackoff, nil) + config, err := LoadKubeconfig(configFile, nil) + if err != nil { + t.Fatal(err) + } + + wh, err := NewGenericWebhook(runtime.NewScheme(), scheme.Codecs, config, groupVersions, retryBackoff) if err == nil { err = wh.RestClient.Get().Do(context.TODO()).Error() @@ -520,7 +530,14 @@ func TestRequestTimeout(t *testing.T) { var requestTimeout = 10 * time.Millisecond - wh, err := newGenericWebhook(runtime.NewScheme(), scheme.Codecs, configFile, groupVersions, retryBackoff, requestTimeout, nil) + config, err := LoadKubeconfig(configFile, nil) + if err != nil { + t.Fatal(err) + } + + config.Timeout = requestTimeout + + wh, err := NewGenericWebhook(runtime.NewScheme(), scheme.Codecs, config, groupVersions, retryBackoff) if err != nil { t.Fatalf("failed to create the webhook: %v", err) } @@ -606,7 +623,12 @@ func TestWithExponentialBackoff(t *testing.T) { defer os.Remove(configFile) - wh, err := NewGenericWebhook(runtime.NewScheme(), scheme.Codecs, configFile, groupVersions, retryBackoff, nil) + config, err := LoadKubeconfig(configFile, nil) + if err != nil { + t.Fatal(err) + } + + wh, err := NewGenericWebhook(runtime.NewScheme(), scheme.Codecs, config, groupVersions, retryBackoff) if err != nil { t.Fatalf("failed to create the webhook: %v", err) diff --git a/plugin/pkg/audit/webhook/webhook.go b/plugin/pkg/audit/webhook/webhook.go index 0a2aa7078..b476aa61c 100644 --- a/plugin/pkg/audit/webhook/webhook.go +++ b/plugin/pkg/audit/webhook/webhook.go @@ -63,8 +63,12 @@ func retryOnError(err error) bool { } func loadWebhook(configFile string, groupVersion schema.GroupVersion, retryBackoff wait.Backoff, customDial utilnet.DialFunc) (*webhook.GenericWebhook, error) { - w, err := webhook.NewGenericWebhook(audit.Scheme, audit.Codecs, configFile, - []schema.GroupVersion{groupVersion}, retryBackoff, customDial) + clientConfig, err := webhook.LoadKubeconfig(configFile, customDial) + if err != nil { + return nil, err + } + w, err := webhook.NewGenericWebhook(audit.Scheme, audit.Codecs, clientConfig, + []schema.GroupVersion{groupVersion}, retryBackoff) if err != nil { return nil, err } diff --git a/plugin/pkg/authenticator/token/webhook/webhook.go b/plugin/pkg/authenticator/token/webhook/webhook.go index af7f07757..7d19b4b7a 100644 --- a/plugin/pkg/authenticator/token/webhook/webhook.go +++ b/plugin/pkg/authenticator/token/webhook/webhook.go @@ -29,7 +29,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - utilnet "k8s.io/apimachinery/pkg/util/net" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/user" @@ -70,12 +69,12 @@ func NewFromInterface(tokenReview authenticationv1client.AuthenticationV1Interfa return newWithBackoff(tokenReviewClient, retryBackoff, implicitAuds, requestTimeout, metrics) } -// New creates a new WebhookTokenAuthenticator from the provided kubeconfig -// file. It is recommend to wrap this authenticator with the token cache +// New creates a new WebhookTokenAuthenticator from the provided rest +// config. It is recommend to wrap this authenticator with the token cache // authenticator implemented in // k8s.io/apiserver/pkg/authentication/token/cache. -func New(kubeConfigFile string, version string, implicitAuds authenticator.Audiences, retryBackoff wait.Backoff, customDial utilnet.DialFunc) (*WebhookTokenAuthenticator, error) { - tokenReview, err := tokenReviewInterfaceFromKubeconfig(kubeConfigFile, version, retryBackoff, customDial) +func New(config *rest.Config, version string, implicitAuds authenticator.Audiences, retryBackoff wait.Backoff) (*WebhookTokenAuthenticator, error) { + tokenReview, err := tokenReviewInterfaceFromConfig(config, version, retryBackoff) if err != nil { return nil, err } @@ -195,10 +194,10 @@ func (w *WebhookTokenAuthenticator) AuthenticateToken(ctx context.Context, token }, true, nil } -// tokenReviewInterfaceFromKubeconfig builds a client from the specified kubeconfig file, +// tokenReviewInterfaceFromConfig builds a client from the specified kubeconfig file, // and returns a TokenReviewInterface that uses that client. Note that the client submits TokenReview // requests to the exact path specified in the kubeconfig file, so arbitrary non-API servers can be targeted. -func tokenReviewInterfaceFromKubeconfig(kubeConfigFile string, version string, retryBackoff wait.Backoff, customDial utilnet.DialFunc) (tokenReviewer, error) { +func tokenReviewInterfaceFromConfig(config *rest.Config, version string, retryBackoff wait.Backoff) (tokenReviewer, error) { localScheme := runtime.NewScheme() if err := scheme.AddToScheme(localScheme); err != nil { return nil, err @@ -210,7 +209,7 @@ func tokenReviewInterfaceFromKubeconfig(kubeConfigFile string, version string, r if err := localScheme.SetVersionPriority(groupVersions...); err != nil { return nil, err } - gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, kubeConfigFile, groupVersions, retryBackoff, customDial) + gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, config, groupVersions, retryBackoff) if err != nil { return nil, err } @@ -221,7 +220,7 @@ func tokenReviewInterfaceFromKubeconfig(kubeConfigFile string, version string, r if err := localScheme.SetVersionPriority(groupVersions...); err != nil { return nil, err } - gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, kubeConfigFile, groupVersions, retryBackoff, customDial) + gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, config, groupVersions, retryBackoff) if err != nil { return nil, err } diff --git a/plugin/pkg/authenticator/token/webhook/webhook_v1_test.go b/plugin/pkg/authenticator/token/webhook/webhook_v1_test.go index c10204deb..754bce613 100644 --- a/plugin/pkg/authenticator/token/webhook/webhook_v1_test.go +++ b/plugin/pkg/authenticator/token/webhook/webhook_v1_test.go @@ -37,6 +37,7 @@ import ( "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/token/cache" "k8s.io/apiserver/pkg/authentication/user" + webhookutil "k8s.io/apiserver/pkg/util/webhook" v1 "k8s.io/client-go/tools/clientcmd/api/v1" ) @@ -201,7 +202,12 @@ func newV1TokenAuthenticator(serverURL string, clientCert, clientKey, ca []byte, return nil, err } - c, err := tokenReviewInterfaceFromKubeconfig(p, "v1", testRetryBackoff, nil) + clientConfig, err := webhookutil.LoadKubeconfig(p, nil) + if err != nil { + return nil, err + } + + c, err := tokenReviewInterfaceFromConfig(clientConfig, "v1", testRetryBackoff) if err != nil { return nil, err } diff --git a/plugin/pkg/authenticator/token/webhook/webhook_v1beta1_test.go b/plugin/pkg/authenticator/token/webhook/webhook_v1beta1_test.go index f7f2f5035..d1dc1585f 100644 --- a/plugin/pkg/authenticator/token/webhook/webhook_v1beta1_test.go +++ b/plugin/pkg/authenticator/token/webhook/webhook_v1beta1_test.go @@ -36,6 +36,7 @@ import ( "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/token/cache" "k8s.io/apiserver/pkg/authentication/user" + webhookutil "k8s.io/apiserver/pkg/util/webhook" v1 "k8s.io/client-go/tools/clientcmd/api/v1" ) @@ -195,7 +196,12 @@ func newV1beta1TokenAuthenticator(serverURL string, clientCert, clientKey, ca [] return nil, err } - c, err := tokenReviewInterfaceFromKubeconfig(p, "v1beta1", testRetryBackoff, nil) + clientConfig, err := webhookutil.LoadKubeconfig(p, nil) + if err != nil { + return nil, err + } + + c, err := tokenReviewInterfaceFromConfig(clientConfig, "v1beta1", testRetryBackoff) if err != nil { return nil, err } diff --git a/plugin/pkg/authorizer/webhook/webhook.go b/plugin/pkg/authorizer/webhook/webhook.go index 4dd08c45e..bae5f0e40 100644 --- a/plugin/pkg/authorizer/webhook/webhook.go +++ b/plugin/pkg/authorizer/webhook/webhook.go @@ -30,7 +30,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/cache" - utilnet "k8s.io/apimachinery/pkg/util/net" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authorization/authorizer" @@ -93,8 +92,8 @@ func NewFromInterface(subjectAccessReview authorizationv1client.AuthorizationV1I // // For additional HTTP configuration, refer to the kubeconfig documentation // https://kubernetes.io/docs/user-guide/kubeconfig-file/. -func New(kubeConfigFile string, version string, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, customDial utilnet.DialFunc) (*WebhookAuthorizer, error) { - subjectAccessReview, err := subjectAccessReviewInterfaceFromKubeconfig(kubeConfigFile, version, retryBackoff, customDial) +func New(config *rest.Config, version string, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff) (*WebhookAuthorizer, error) { + subjectAccessReview, err := subjectAccessReviewInterfaceFromConfig(config, version, retryBackoff) if err != nil { return nil, err } @@ -269,10 +268,10 @@ func convertToSARExtra(extra map[string][]string) map[string]authorizationv1.Ext return ret } -// subjectAccessReviewInterfaceFromKubeconfig builds a client from the specified kubeconfig file, +// subjectAccessReviewInterfaceFromConfig builds a client from the specified kubeconfig file, // and returns a SubjectAccessReviewInterface that uses that client. Note that the client submits SubjectAccessReview // requests to the exact path specified in the kubeconfig file, so arbitrary non-API servers can be targeted. -func subjectAccessReviewInterfaceFromKubeconfig(kubeConfigFile string, version string, retryBackoff wait.Backoff, customDial utilnet.DialFunc) (subjectAccessReviewer, error) { +func subjectAccessReviewInterfaceFromConfig(config *rest.Config, version string, retryBackoff wait.Backoff) (subjectAccessReviewer, error) { localScheme := runtime.NewScheme() if err := scheme.AddToScheme(localScheme); err != nil { return nil, err @@ -284,7 +283,7 @@ func subjectAccessReviewInterfaceFromKubeconfig(kubeConfigFile string, version s if err := localScheme.SetVersionPriority(groupVersions...); err != nil { return nil, err } - gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, kubeConfigFile, groupVersions, retryBackoff, customDial) + gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, config, groupVersions, retryBackoff) if err != nil { return nil, err } @@ -295,7 +294,7 @@ func subjectAccessReviewInterfaceFromKubeconfig(kubeConfigFile string, version s if err := localScheme.SetVersionPriority(groupVersions...); err != nil { return nil, err } - gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, kubeConfigFile, groupVersions, retryBackoff, customDial) + gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, config, groupVersions, retryBackoff) if err != nil { return nil, err } diff --git a/plugin/pkg/authorizer/webhook/webhook_v1_test.go b/plugin/pkg/authorizer/webhook/webhook_v1_test.go index 6617bcea9..07b3d4148 100644 --- a/plugin/pkg/authorizer/webhook/webhook_v1_test.go +++ b/plugin/pkg/authorizer/webhook/webhook_v1_test.go @@ -40,6 +40,7 @@ import ( "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authorization/authorizer" + webhookutil "k8s.io/apiserver/pkg/util/webhook" v1 "k8s.io/client-go/tools/clientcmd/api/v1" ) @@ -194,7 +195,11 @@ current-context: default return fmt.Errorf("failed to execute test template: %v", err) } // Create a new authorizer - sarClient, err := subjectAccessReviewInterfaceFromKubeconfig(p, "v1", testRetryBackoff, nil) + clientConfig, err := webhookutil.LoadKubeconfig(p, nil) + if err != nil { + return err + } + sarClient, err := subjectAccessReviewInterfaceFromConfig(clientConfig, "v1", testRetryBackoff) if err != nil { return fmt.Errorf("error building sar client: %v", err) } @@ -333,7 +338,11 @@ func newV1Authorizer(callbackURL string, clientCert, clientKey, ca []byte, cache if err := json.NewEncoder(tempfile).Encode(config); err != nil { return nil, err } - sarClient, err := subjectAccessReviewInterfaceFromKubeconfig(p, "v1", testRetryBackoff, nil) + clientConfig, err := webhookutil.LoadKubeconfig(p, nil) + if err != nil { + return nil, err + } + sarClient, err := subjectAccessReviewInterfaceFromConfig(clientConfig, "v1", testRetryBackoff) if err != nil { return nil, fmt.Errorf("error building sar client: %v", err) } diff --git a/plugin/pkg/authorizer/webhook/webhook_v1beta1_test.go b/plugin/pkg/authorizer/webhook/webhook_v1beta1_test.go index 77f90f3e8..d3ba93db4 100644 --- a/plugin/pkg/authorizer/webhook/webhook_v1beta1_test.go +++ b/plugin/pkg/authorizer/webhook/webhook_v1beta1_test.go @@ -39,6 +39,7 @@ import ( "k8s.io/apimachinery/pkg/util/diff" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authorization/authorizer" + webhookutil "k8s.io/apiserver/pkg/util/webhook" v1 "k8s.io/client-go/tools/clientcmd/api/v1" ) @@ -186,7 +187,11 @@ current-context: default return fmt.Errorf("failed to execute test template: %v", err) } // Create a new authorizer - sarClient, err := subjectAccessReviewInterfaceFromKubeconfig(p, "v1beta1", testRetryBackoff, nil) + clientConfig, err := webhookutil.LoadKubeconfig(p, nil) + if err != nil { + return err + } + sarClient, err := subjectAccessReviewInterfaceFromConfig(clientConfig, "v1beta1", testRetryBackoff) if err != nil { return fmt.Errorf("error building sar client: %v", err) } @@ -325,7 +330,11 @@ func newV1beta1Authorizer(callbackURL string, clientCert, clientKey, ca []byte, if err := json.NewEncoder(tempfile).Encode(config); err != nil { return nil, err } - sarClient, err := subjectAccessReviewInterfaceFromKubeconfig(p, "v1beta1", testRetryBackoff, nil) + clientConfig, err := webhookutil.LoadKubeconfig(p, nil) + if err != nil { + return nil, err + } + sarClient, err := subjectAccessReviewInterfaceFromConfig(clientConfig, "v1beta1", testRetryBackoff) if err != nil { return nil, fmt.Errorf("error building sar client: %v", err) }