Start the webhook before informers sync. (#1180)

* Start the webhook before informers sync.

Some webhooks (e.g. conversion) are required to list resources, so by delaying those until after informers have synced, we create a deadlock when they run in the same process.  This change has two key parts:
1. Start the webhook immediately when our process starts, and issue a callback from sharedmain when the informers have synced.
2. Block `Admit` calls until informers have synced (all conversions are exempt), unless they have been designated by implementing `webhook.StatelessAdmissionController`.

Our built-in admission controllers (defaulting, validation, configmap validation) have all been marked as stateless, the main case where we want to block `Admit` calls is when we require the informer to have synchronized to populate indices for Bindings.

* Add missing err declaration
This commit is contained in:
Matt Moore 2020-03-30 18:14:50 -07:00 committed by GitHub
parent f40c61abc6
commit e2ee5bed78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 116 additions and 44 deletions

View File

@ -232,6 +232,26 @@ func WebhookMainWithConfig(ctx context.Context, component string, cfg *rest.Conf
WatchLoggingConfigOrDie(ctx, cmw, logger, atomicLevel, component) WatchLoggingConfigOrDie(ctx, cmw, logger, atomicLevel, component)
WatchObservabilityConfigOrDie(ctx, cmw, profilingHandler, logger, component) WatchObservabilityConfigOrDie(ctx, cmw, profilingHandler, logger, component)
eg, egCtx := errgroup.WithContext(ctx)
eg.Go(profilingServer.ListenAndServe)
// If we have one or more admission controllers, then start the webhook
// and pass them in.
var wh *webhook.Webhook
var err error
if len(webhooks) > 0 {
// Register webhook metrics
webhook.RegisterMetrics()
wh, err = webhook.New(ctx, webhooks)
if err != nil {
logger.Fatalw("Failed to create webhook", zap.Error(err))
}
eg.Go(func() error {
return wh.Run(ctx.Done())
})
}
logger.Info("Starting configuration manager...") logger.Info("Starting configuration manager...")
if err := cmw.Start(ctx.Done()); err != nil { if err := cmw.Start(ctx.Done()); err != nil {
logger.Fatalw("Failed to start configuration manager", zap.Error(err)) logger.Fatalw("Failed to start configuration manager", zap.Error(err))
@ -240,27 +260,12 @@ func WebhookMainWithConfig(ctx context.Context, component string, cfg *rest.Conf
if err := controller.StartInformers(ctx.Done(), informers...); err != nil { if err := controller.StartInformers(ctx.Done(), informers...); err != nil {
logger.Fatalw("Failed to start informers", zap.Error(err)) logger.Fatalw("Failed to start informers", zap.Error(err))
} }
if wh != nil {
wh.InformersHaveSynced()
}
logger.Info("Starting controllers...") logger.Info("Starting controllers...")
go controller.StartAll(ctx.Done(), controllers...) go controller.StartAll(ctx.Done(), controllers...)
eg, egCtx := errgroup.WithContext(ctx)
eg.Go(profilingServer.ListenAndServe)
// If we have one or more admission controllers, then start the webhook
// and pass them in.
if len(webhooks) > 0 {
// Register webhook metrics
webhook.RegisterMetrics()
wh, err := webhook.New(ctx, webhooks)
if err != nil {
logger.Fatalw("Failed to create webhook", zap.Error(err))
}
eg.Go(func() error {
return wh.Run(ctx.Done())
})
}
// This will block until either a signal arrives or one of the grouped functions // This will block until either a signal arrives or one of the grouped functions
// returns an error. // returns an error.
<-egCtx.Done() <-egCtx.Done()

View File

@ -39,6 +39,14 @@ type AdmissionController interface {
Admit(context.Context, *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse Admit(context.Context, *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse
} }
// StatelessAdmissionController is implemented by AdmissionControllers where Admit may be safely
// called before informers have finished syncing. This is implemented by inlining
// StatelessAdmissionImpl in your Go type.
type StatelessAdmissionController interface {
// A silly name that should avoid collisions.
ThisTypeDoesNotDependOnInformerState()
}
// MakeErrorStatus creates an 'BadRequest' error AdmissionResponse // MakeErrorStatus creates an 'BadRequest' error AdmissionResponse
func MakeErrorStatus(reason string, args ...interface{}) *admissionv1beta1.AdmissionResponse { func MakeErrorStatus(reason string, args ...interface{}) *admissionv1beta1.AdmissionResponse {
result := apierrors.NewBadRequest(fmt.Sprintf(reason, args...)).Status() result := apierrors.NewBadRequest(fmt.Sprintf(reason, args...)).Status()
@ -48,8 +56,17 @@ func MakeErrorStatus(reason string, args ...interface{}) *admissionv1beta1.Admis
} }
} }
func admissionHandler(rootLogger *zap.SugaredLogger, stats StatsReporter, c AdmissionController) http.HandlerFunc { func admissionHandler(rootLogger *zap.SugaredLogger, stats StatsReporter, c AdmissionController, synced <-chan struct{}) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if _, ok := c.(StatelessAdmissionController); ok {
// Stateless admission controllers do not require Informers to have
// finished syncing before Admit is called.
} else {
// Don't allow admission control requests through until we have been
// notified that informers have been synchronized.
<-synced
}
var ttStart = time.Now() var ttStart = time.Now()
logger := rootLogger logger := rootLogger
logger.Infof("Webhook ServeHTTP request=%#v", r) logger.Infof("Webhook ServeHTTP request=%#v", r)
@ -92,3 +109,8 @@ func admissionHandler(rootLogger *zap.SugaredLogger, stats StatsReporter, c Admi
} }
} }
} }
// Inline this type to implement StatelessAdmissionController.
type StatelessAdmissionImpl struct{}
func (sai StatelessAdmissionImpl) ThisTypeDoesNotDependOnInformerState() {}

View File

@ -26,6 +26,7 @@ import (
"path" "path"
"strings" "strings"
"testing" "testing"
"time"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
jsonpatch "gomodules.xyz/jsonpatch/v2" jsonpatch "gomodules.xyz/jsonpatch/v2"
@ -126,6 +127,9 @@ func TestAdmissionValidResponseForResource(t *testing.T) {
} }
req.Header.Add("Content-Type", "application/json") req.Header.Add("Content-Type", "application/json")
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
response, err := tlsClient.Do(req) response, err := tlsClient.Do(req)
if err != nil { if err != nil {
t.Fatalf("Failed to get response %v", err) t.Fatalf("Failed to get response %v", err)
@ -147,6 +151,24 @@ func TestAdmissionValidResponseForResource(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Failed to decode response: %v", err) t.Fatalf("Failed to decode response: %v", err)
} }
}()
// Check that Admit calls block when they are initiated before informers sync.
select {
case <-time.After(5 * time.Second):
case <-doneCh:
t.Fatal("Admit was called before informers had synced.")
}
// Signal the webhook that informers have synced.
wh.InformersHaveSynced()
// Check that after informers have synced that things start completing immediately (including outstanding requests).
select {
case <-doneCh:
case <-time.After(5 * time.Second):
t.Error("Timed out waiting on Admit to complete after informers synced.")
}
metricstest.CheckStatsReported(t, requestCountName, requestLatenciesName) metricstest.CheckStatsReported(t, requestCountName, requestLatenciesName)
} }
@ -164,6 +186,7 @@ func TestAdmissionInvalidResponseForResource(t *testing.T) {
eg, _ := errgroup.WithContext(ctx) eg, _ := errgroup.WithContext(ctx)
eg.Go(func() error { return wh.Run(ctx.Done()) }) eg.Go(func() error { return wh.Run(ctx.Done()) })
wh.InformersHaveSynced()
defer func() { defer func() {
cancel() cancel()
if err := eg.Wait(); err != nil { if err := eg.Wait(); err != nil {

View File

@ -43,6 +43,8 @@ import (
// reconciler implements the AdmissionController for ConfigMaps // reconciler implements the AdmissionController for ConfigMaps
type reconciler struct { type reconciler struct {
webhook.StatelessAdmissionImpl
name string name string
path string path string
constructors map[string]reflect.Value constructors map[string]reflect.Value
@ -56,6 +58,7 @@ type reconciler struct {
var _ controller.Reconciler = (*reconciler)(nil) var _ controller.Reconciler = (*reconciler)(nil)
var _ webhook.AdmissionController = (*reconciler)(nil) var _ webhook.AdmissionController = (*reconciler)(nil)
var _ webhook.StatelessAdmissionController = (*reconciler)(nil)
// Reconcile implements controller.Reconciler // Reconcile implements controller.Reconciler
func (ac *reconciler) Reconcile(ctx context.Context, key string) error { func (ac *reconciler) Reconcile(ctx context.Context, key string) error {

View File

@ -50,6 +50,8 @@ var errMissingNewObject = errors.New("the new object may not be nil")
// reconciler implements the AdmissionController for resources // reconciler implements the AdmissionController for resources
type reconciler struct { type reconciler struct {
webhook.StatelessAdmissionImpl
name string name string
path string path string
handlers map[schema.GroupVersionKind]resourcesemantics.GenericCRD handlers map[schema.GroupVersionKind]resourcesemantics.GenericCRD
@ -66,6 +68,7 @@ type reconciler struct {
var _ controller.Reconciler = (*reconciler)(nil) var _ controller.Reconciler = (*reconciler)(nil)
var _ webhook.AdmissionController = (*reconciler)(nil) var _ webhook.AdmissionController = (*reconciler)(nil)
var _ webhook.StatelessAdmissionController = (*reconciler)(nil)
// Reconcile implements controller.Reconciler // Reconcile implements controller.Reconciler
func (ac *reconciler) Reconcile(ctx context.Context, key string) error { func (ac *reconciler) Reconcile(ctx context.Context, key string) error {

View File

@ -48,6 +48,8 @@ var errMissingNewObject = errors.New("the new object may not be nil")
// reconciler implements the AdmissionController for resources // reconciler implements the AdmissionController for resources
type reconciler struct { type reconciler struct {
webhook.StatelessAdmissionImpl
name string name string
path string path string
handlers map[schema.GroupVersionKind]resourcesemantics.GenericCRD handlers map[schema.GroupVersionKind]resourcesemantics.GenericCRD
@ -64,6 +66,7 @@ type reconciler struct {
var _ controller.Reconciler = (*reconciler)(nil) var _ controller.Reconciler = (*reconciler)(nil)
var _ webhook.AdmissionController = (*reconciler)(nil) var _ webhook.AdmissionController = (*reconciler)(nil)
var _ webhook.StatelessAdmissionController = (*reconciler)(nil)
// Reconcile implements controller.Reconciler // Reconcile implements controller.Reconciler
func (ac *reconciler) Reconcile(ctx context.Context, key string) error { func (ac *reconciler) Reconcile(ctx context.Context, key string) error {

View File

@ -65,6 +65,9 @@ type Webhook struct {
Options Options Options Options
Logger *zap.SugaredLogger Logger *zap.SugaredLogger
// synced is function that is called when the informers have been synced.
synced context.CancelFunc
mux http.ServeMux mux http.ServeMux
secretlister corelisters.SecretLister secretlister corelisters.SecretLister
} }
@ -105,11 +108,14 @@ func New(
opts.StatsReporter = reporter opts.StatsReporter = reporter
} }
syncCtx, cancel := context.WithCancel(context.Background())
webhook = &Webhook{ webhook = &Webhook{
Client: client, Client: client,
Options: *opts, Options: *opts,
secretlister: secretInformer.Lister(), secretlister: secretInformer.Lister(),
Logger: logger, Logger: logger,
synced: cancel,
} }
webhook.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { webhook.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
@ -117,26 +123,31 @@ func New(
}) })
for _, controller := range controllers { for _, controller := range controllers {
var handler http.Handler
var path string
switch c := controller.(type) { switch c := controller.(type) {
case AdmissionController: case AdmissionController:
handler = admissionHandler(logger, opts.StatsReporter, c) handler := admissionHandler(logger, opts.StatsReporter, c, syncCtx.Done())
path = c.Path() webhook.mux.Handle(c.Path(), handler)
case ConversionController: case ConversionController:
handler = conversionHandler(logger, opts.StatsReporter, c) handler := conversionHandler(logger, opts.StatsReporter, c)
path = c.Path() webhook.mux.Handle(c.Path(), handler)
default: default:
return nil, fmt.Errorf("unknown webhook controller type: %T", controller) return nil, fmt.Errorf("unknown webhook controller type: %T", controller)
} }
webhook.mux.Handle(path, handler)
} }
return return
} }
// InformersHaveSynced is called when the informers have all been synced, which allows any outstanding
// admission webhooks through.
func (wh *Webhook) InformersHaveSynced() {
wh.synced()
wh.Logger.Info("Informers have been synced, unblocking admission webhooks.")
}
// Run implements the admission controller run loop. // Run implements the admission controller run loop.
func (wh *Webhook) Run(stop <-chan struct{}) error { func (wh *Webhook) Run(stop <-chan struct{}) error {
logger := wh.Logger logger := wh.Logger

View File

@ -58,6 +58,7 @@ func TestMissingContentType(t *testing.T) {
eg, _ := errgroup.WithContext(ctx) eg, _ := errgroup.WithContext(ctx)
eg.Go(func() error { return wh.Run(ctx.Done()) }) eg.Go(func() error { return wh.Run(ctx.Done()) })
wh.InformersHaveSynced()
defer func() { defer func() {
cancel() cancel()
if err := eg.Wait(); err != nil { if err := eg.Wait(); err != nil {
@ -111,6 +112,7 @@ func testEmptyRequestBody(t *testing.T, controller interface{}) {
eg, _ := errgroup.WithContext(ctx) eg, _ := errgroup.WithContext(ctx)
eg.Go(func() error { return wh.Run(ctx.Done()) }) eg.Go(func() error { return wh.Run(ctx.Done()) })
wh.InformersHaveSynced()
defer func() { defer func() {
cancel() cancel()
if err := eg.Wait(); err != nil { if err := eg.Wait(); err != nil {