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 <mok@vmware.com>
This commit is contained in:
Monis Khan 2021-07-23 11:15:47 -04:00
parent 4d08582d1f
commit fef7d0ef1e
No known key found for this signature in database
GPG Key ID: 52C90ADA01B269B8
13 changed files with 142 additions and 64 deletions

View File

@ -35,6 +35,7 @@ import (
"k8s.io/apiserver/pkg/authentication/token/tokenfile"
tokenunion "k8s.io/apiserver/pkg/authentication/token/union"
"k8s.io/apiserver/pkg/server/dynamiccertificates"
webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/apiserver/plugin/pkg/authenticator/token/oidc"
"k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
"k8s.io/kube-openapi/pkg/validation/spec"
@ -299,7 +300,11 @@ func newWebhookTokenAuthenticator(config Config) (authenticator.Token, error) {
return nil, errors.New("retry backoff parameters for authentication webhook has not been specified")
}
webhookTokenAuthenticator, err := webhook.New(config.WebhookTokenAuthnConfigFile, config.WebhookTokenAuthnVersion, config.APIAudiences, *config.WebhookRetryBackoff, config.CustomDial)
clientConfig, err := webhookutil.LoadKubeconfig(config.WebhookTokenAuthnConfigFile, config.CustomDial)
if err != nil {
return nil, err
}
webhookTokenAuthenticator, err := webhook.New(clientConfig, config.WebhookTokenAuthnVersion, config.APIAudiences, *config.WebhookRetryBackoff)
if err != nil {
return nil, err
}

View File

@ -26,6 +26,7 @@ import (
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/authorization/authorizerfactory"
"k8s.io/apiserver/pkg/authorization/union"
webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/apiserver/plugin/pkg/authorizer/webhook"
versionedinformers "k8s.io/client-go/informers"
"k8s.io/kubernetes/pkg/auth/authorizer/abac"
@ -114,12 +115,16 @@ func (config Config) New() (authorizer.Authorizer, authorizer.RuleResolver, erro
if config.WebhookRetryBackoff == nil {
return nil, nil, errors.New("retry backoff parameters for authorization webhook has not been specified")
}
webhookAuthorizer, err := webhook.New(config.WebhookConfigFile,
clientConfig, err := webhookutil.LoadKubeconfig(config.WebhookConfigFile, config.CustomDial)
if err != nil {
return nil, nil, err
}
webhookAuthorizer, err := webhook.New(clientConfig,
config.WebhookVersion,
config.WebhookCacheAuthorizedTTL,
config.WebhookCacheUnauthorizedTTL,
*config.WebhookRetryBackoff,
config.CustomDial)
)
if err != nil {
return nil, nil, err
}

View File

@ -261,8 +261,12 @@ func NewImagePolicyWebhook(configFile io.Reader) (*Plugin, error) {
return nil, err
}
clientConfig, err := webhook.LoadKubeconfig(whConfig.KubeConfigFile, nil)
if err != nil {
return nil, err
}
retryBackoff := webhook.DefaultRetryBackoffWithInitialDelay(whConfig.RetryBackoff)
gw, err := webhook.NewGenericWebhook(legacyscheme.Scheme, legacyscheme.Codecs, whConfig.KubeConfigFile, groupVersions, retryBackoff, nil)
gw, err := webhook.NewGenericWebhook(legacyscheme.Scheme, legacyscheme.Codecs, clientConfig, groupVersions, retryBackoff)
if err != nil {
return nil, err
}

View File

@ -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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -57,6 +57,7 @@ import (
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/authorization/authorizerfactory"
webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/apiserver/plugin/pkg/authenticator/token/tokentest"
"k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
clientset "k8s.io/client-go/kubernetes"
@ -109,7 +110,13 @@ func getTestWebhookTokenAuth(serverURL string, customDial utilnet.DialFunc) (aut
Jitter: 0.2,
Steps: 5,
}
webhookTokenAuth, err := webhook.New(kubecfgFile.Name(), "v1beta1", nil, retryBackoff, customDial)
clientConfig, err := webhookutil.LoadKubeconfig(kubecfgFile.Name(), customDial)
if err != nil {
return nil, err
}
webhookTokenAuth, err := webhook.New(clientConfig, "v1beta1", nil, retryBackoff)
if err != nil {
return nil, err
}