1
0
Fork 0
mirror of https://github.com/external-secrets/external-secrets.git synced 2024-12-14 11:57:59 +00:00

feat: fix cert-controller readiness probe (#2857)

readiness probes are being executed independently from the
leader election status. The current implementation depends on
leader election (client cache etc.) to run properly.
This commit fixes that by short-circuiting the readiness probes
when the mgr is not the leader.

This bug surfaces when `leader-election=true` and cert-controller `replicas>=2`.

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
This commit is contained in:
Moritz Johner 2023-11-07 09:51:27 +01:00 committed by GitHub
parent e0c1d93f9b
commit f5cd6816aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 84 additions and 38 deletions

View file

@ -87,7 +87,8 @@ var certcontrollerCmd = &cobra.Command{
setupLog.Error(err, "unable to start manager") setupLog.Error(err, "unable to start manager")
os.Exit(1) os.Exit(1)
} }
crdctrl := crds.New(mgr.GetClient(), mgr.GetScheme(),
crdctrl := crds.New(mgr.GetClient(), mgr.GetScheme(), mgr.Elected(),
ctrl.Log.WithName("controllers").WithName("webhook-certs-updater"), ctrl.Log.WithName("controllers").WithName("webhook-certs-updater"),
crdRequeueInterval, serviceName, serviceNamespace, secretName, secretNamespace, crdNames) crdRequeueInterval, serviceName, serviceNamespace, secretName, secretNamespace, crdNames)
if err := crdctrl.SetupWithManager(mgr, controller.Options{ if err := crdctrl.SetupWithManager(mgr, controller.Options{
@ -97,7 +98,7 @@ var certcontrollerCmd = &cobra.Command{
os.Exit(1) os.Exit(1)
} }
whc := webhookconfig.New(mgr.GetClient(), mgr.GetScheme(), whc := webhookconfig.New(mgr.GetClient(), mgr.GetScheme(), mgr.Elected(),
ctrl.Log.WithName("controllers").WithName("webhook-certs-updater"), ctrl.Log.WithName("controllers").WithName("webhook-certs-updater"),
serviceName, serviceNamespace, serviceName, serviceNamespace,
secretName, secretNamespace, crdRequeueInterval) secretName, secretNamespace, crdRequeueInterval)

View file

@ -72,26 +72,30 @@ type Reconciler struct {
RequeueInterval time.Duration RequeueInterval time.Duration
// the controller is ready when all crds are injected // the controller is ready when all crds are injected
rdyMu *sync.Mutex // and the controller is elected as leader
readyStatusMap map[string]bool leaderChan <-chan struct{}
leaderElected bool
readyStatusMapMu *sync.Mutex
readyStatusMap map[string]bool
} }
func New(k8sClient client.Client, scheme *runtime.Scheme, logger logr.Logger, func New(k8sClient client.Client, scheme *runtime.Scheme, leaderChan <-chan struct{}, logger logr.Logger,
interval time.Duration, svcName, svcNamespace, secretName, secretNamespace string, resources []string) *Reconciler { interval time.Duration, svcName, svcNamespace, secretName, secretNamespace string, resources []string) *Reconciler {
return &Reconciler{ return &Reconciler{
Client: k8sClient, Client: k8sClient,
Log: logger, Log: logger,
Scheme: scheme, Scheme: scheme,
SvcName: svcName, SvcName: svcName,
SvcNamespace: svcNamespace, SvcNamespace: svcNamespace,
SecretName: secretName, SecretName: secretName,
SecretNamespace: secretNamespace, SecretNamespace: secretNamespace,
RequeueInterval: interval, RequeueInterval: interval,
CrdResources: resources, CrdResources: resources,
CAName: "external-secrets", CAName: "external-secrets",
CAOrganization: "external-secrets", CAOrganization: "external-secrets",
rdyMu: &sync.Mutex{}, leaderChan: leaderChan,
readyStatusMap: map[string]bool{}, readyStatusMapMu: &sync.Mutex{},
readyStatusMap: map[string]bool{},
} }
} }
@ -117,14 +121,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
err := r.updateCRD(ctx, req) err := r.updateCRD(ctx, req)
if err != nil { if err != nil {
log.Error(err, "failed to inject conversion webhook") log.Error(err, "failed to inject conversion webhook")
r.rdyMu.Lock() r.readyStatusMapMu.Lock()
r.readyStatusMap[req.NamespacedName.Name] = false r.readyStatusMap[req.NamespacedName.Name] = false
r.rdyMu.Unlock() r.readyStatusMapMu.Unlock()
return ctrl.Result{}, err return ctrl.Result{}, err
} }
r.rdyMu.Lock() r.readyStatusMapMu.Lock()
r.readyStatusMap[req.NamespacedName.Name] = true r.readyStatusMap[req.NamespacedName.Name] = true
r.rdyMu.Unlock() r.readyStatusMapMu.Unlock()
} }
return ctrl.Result{RequeueAfter: r.RequeueInterval}, nil return ctrl.Result{RequeueAfter: r.RequeueInterval}, nil
} }
@ -132,14 +136,35 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
// ReadyCheck reviews if all webhook configs have been injected into the CRDs // ReadyCheck reviews if all webhook configs have been injected into the CRDs
// and if the referenced webhook service is ready. // and if the referenced webhook service is ready.
func (r *Reconciler) ReadyCheck(_ *http.Request) error { func (r *Reconciler) ReadyCheck(_ *http.Request) error {
// skip readiness check if we're not leader
// as we depend on caches and being able to reconcile Webhooks
if !r.leaderElected {
select {
case <-r.leaderChan:
r.leaderElected = true
default:
return nil
}
}
if err := r.checkCRDs(); err != nil {
return err
}
return r.checkEndpoints()
}
func (r Reconciler) checkCRDs() error {
for _, res := range r.CrdResources { for _, res := range r.CrdResources {
r.rdyMu.Lock() r.readyStatusMapMu.Lock()
rdy := r.readyStatusMap[res] rdy := r.readyStatusMap[res]
r.rdyMu.Unlock() r.readyStatusMapMu.Unlock()
if !rdy { if !rdy {
return fmt.Errorf(errResNotReady, res) return fmt.Errorf(errResNotReady, res)
} }
} }
return nil
}
func (r Reconciler) checkEndpoints() error {
var eps corev1.Endpoints var eps corev1.Endpoints
err := r.Get(context.TODO(), types.NamespacedName{ err := r.Get(context.TODO(), types.NamespacedName{
Name: r.SvcName, Name: r.SvcName,

View file

@ -77,7 +77,9 @@ var _ = BeforeSuite(func() {
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil()) Expect(k8sClient).ToNot(BeNil())
rec := New(k8sClient, k8sManager.GetScheme(), log, time.Second*1, leaderChan := make(chan struct{})
close(leaderChan)
rec := New(k8sClient, k8sManager.GetScheme(), leaderChan, log, time.Second*1,
"foo", "default", "foo", "default", []string{ "foo", "default", "foo", "default", []string{
"secretstores.test.io", "secretstores.test.io",
}) })

View file

@ -84,8 +84,9 @@ var _ = BeforeSuite(func() {
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil()) Expect(k8sClient).ToNot(BeNil())
leaderChan := make(chan struct{})
reconciler = New(k8sClient, k8sManager.GetScheme(), ctrl.Log, ctrlSvcName, ctrlSvcNamespace, ctrlSecretName, ctrlSecretNamespace, time.Second) close(leaderChan)
reconciler = New(k8sClient, k8sManager.GetScheme(), leaderChan, ctrl.Log, ctrlSvcName, ctrlSvcNamespace, ctrlSecretName, ctrlSecretNamespace, time.Second)
reconciler.SetupWithManager(k8sManager, controller.Options{}) reconciler.SetupWithManager(k8sManager, controller.Options{})
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())

View file

@ -46,11 +46,16 @@ type Reconciler struct {
SecretName string SecretName string
SecretNamespace string SecretNamespace string
rdyMu *sync.Mutex // store state for the readiness probe.
ready bool // we're ready when we're not the leader or
// if we've reconciled the webhook config when we're the leader.
leaderChan <-chan struct{}
leaderElected bool
webhookReadyMu *sync.Mutex
webhookReady bool
} }
func New(k8sClient client.Client, scheme *runtime.Scheme, func New(k8sClient client.Client, scheme *runtime.Scheme, leaderChan <-chan struct{},
log logr.Logger, svcName, svcNamespace, secretName, secretNamespace string, log logr.Logger, svcName, svcNamespace, secretName, secretNamespace string,
requeueInterval time.Duration) *Reconciler { requeueInterval time.Duration) *Reconciler {
return &Reconciler{ return &Reconciler{
@ -62,8 +67,10 @@ func New(k8sClient client.Client, scheme *runtime.Scheme,
SvcNamespace: svcNamespace, SvcNamespace: svcNamespace,
SecretName: secretName, SecretName: secretName,
SecretNamespace: secretNamespace, SecretNamespace: secretNamespace,
rdyMu: &sync.Mutex{}, leaderChan: leaderChan,
ready: false, leaderElected: false,
webhookReadyMu: &sync.Mutex{},
webhookReady: false,
} }
} }
@ -109,9 +116,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
// right now we only have one single // right now we only have one single
// webhook config we care about // webhook config we care about
r.rdyMu.Lock() r.webhookReadyMu.Lock()
defer r.rdyMu.Unlock() defer r.webhookReadyMu.Unlock()
r.ready = true r.webhookReady = true
return ctrl.Result{ return ctrl.Result{
RequeueAfter: r.RequeueDuration, RequeueAfter: r.RequeueDuration,
}, nil }, nil
@ -126,9 +133,19 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options)
} }
func (r *Reconciler) ReadyCheck(_ *http.Request) error { func (r *Reconciler) ReadyCheck(_ *http.Request) error {
r.rdyMu.Lock() // skip readiness check if we're not leader
defer r.rdyMu.Unlock() // as we depend on caches and being able to reconcile Webhooks
if !r.ready { if !r.leaderElected {
select {
case <-r.leaderChan:
r.leaderElected = true
default:
return nil
}
}
r.webhookReadyMu.Lock()
defer r.webhookReadyMu.Unlock()
if !r.webhookReady {
return fmt.Errorf(errWebhookNotReady) return fmt.Errorf(errWebhookNotReady)
} }
var eps v1.Endpoints var eps v1.Endpoints