
This change updates the apicerts controllers to return an error when they cannot successfully complete their Sync func. i.e. `return nil` is reserved for cases where the controller has fully completed its job with no errors. This makes it clear when a controller has wedged - i.e. it is waiting on some other controller or process to perform some action before it can complete. The controller lib's queue will exponentially back off and thus there is no need to be concerned with returning an error indefinitely or infinite log spam. Even when the kubelet throws away container logs, it will be clear what controllers are wedged based on the last hour or so of logs. Signed-off-by: Monis Khan <mok@vmware.com>
127 lines
4.3 KiB
Go
127 lines
4.3 KiB
Go
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package apicerts
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"time"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
corev1informers "k8s.io/client-go/informers/core/v1"
|
|
"k8s.io/client-go/kubernetes"
|
|
"k8s.io/klog/v2"
|
|
|
|
"go.pinniped.dev/internal/constable"
|
|
pinnipedcontroller "go.pinniped.dev/internal/controller"
|
|
"go.pinniped.dev/internal/controllerlib"
|
|
)
|
|
|
|
type certsExpirerController struct {
|
|
namespace string
|
|
certsSecretResourceName string
|
|
k8sClient kubernetes.Interface
|
|
secretInformer corev1informers.SecretInformer
|
|
|
|
// renewBefore is the amount of time after the cert's issuance where
|
|
// this controller will start to try to rotate it.
|
|
renewBefore time.Duration
|
|
}
|
|
|
|
// NewCertsExpirerController returns a controllerlib.Controller that will delete a
|
|
// certificate secret once it gets within some threshold of its expiration time. The
|
|
// deletion forces rotation of the secret with the help of other controllers.
|
|
func NewCertsExpirerController(
|
|
namespace string,
|
|
certsSecretResourceName string,
|
|
k8sClient kubernetes.Interface,
|
|
secretInformer corev1informers.SecretInformer,
|
|
withInformer pinnipedcontroller.WithInformerOptionFunc,
|
|
renewBefore time.Duration,
|
|
) controllerlib.Controller {
|
|
return controllerlib.New(
|
|
controllerlib.Config{
|
|
Name: "certs-expirer-controller",
|
|
Syncer: &certsExpirerController{
|
|
namespace: namespace,
|
|
certsSecretResourceName: certsSecretResourceName,
|
|
k8sClient: k8sClient,
|
|
secretInformer: secretInformer,
|
|
renewBefore: renewBefore,
|
|
},
|
|
},
|
|
withInformer(
|
|
secretInformer,
|
|
pinnipedcontroller.NameAndNamespaceExactMatchFilterFactory(certsSecretResourceName, namespace),
|
|
controllerlib.InformerOption{},
|
|
),
|
|
)
|
|
}
|
|
|
|
// Sync implements controller.Syncer.Sync.
|
|
func (c *certsExpirerController) Sync(ctx controllerlib.Context) error {
|
|
secret, err := c.secretInformer.Lister().Secrets(c.namespace).Get(c.certsSecretResourceName)
|
|
notFound := k8serrors.IsNotFound(err)
|
|
if err != nil && !notFound {
|
|
return fmt.Errorf("failed to get %s/%s secret: %w", c.namespace, c.certsSecretResourceName, err)
|
|
}
|
|
if notFound {
|
|
klog.Info("certsExpirerController Sync found that the secret does not exist yet or was deleted")
|
|
//nolint: goerr113
|
|
return fmt.Errorf("certsExpirerController missing pre-requirements, secret %s/%s does not exist: %w",
|
|
c.namespace, c.certsSecretResourceName, controllerlib.ErrSyntheticRequeue)
|
|
}
|
|
|
|
notBefore, notAfter, err := getCertBounds(secret)
|
|
if err != nil {
|
|
// If we can't read the cert, then we are wedged and need to complain loudly.
|
|
// The controller lib code will retry indefinitely, but will back off exponentially.
|
|
//nolint: goerr113
|
|
return fmt.Errorf("certsExpirerController Sync found that the secret is malformed: %w", err)
|
|
}
|
|
|
|
certAge := time.Since(notBefore)
|
|
renewDelta := certAge - c.renewBefore
|
|
klog.Infof("certsExpirerController Sync found a renew delta of %s", renewDelta)
|
|
if renewDelta >= 0 || time.Now().After(notAfter) {
|
|
err := c.k8sClient.
|
|
CoreV1().
|
|
Secrets(c.namespace).
|
|
Delete(ctx.Context, c.certsSecretResourceName, metav1.DeleteOptions{})
|
|
if err != nil {
|
|
// Do return an error here so that the controller library will reschedule
|
|
// us to try deleting this cert again.
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getCertBounds returns the NotBefore and NotAfter fields of the TLS
|
|
// certificate in the provided secret, or an error. Not that it expects the
|
|
// provided secret to contain the well-known data keys from this package (see
|
|
// certs_manager.go).
|
|
func getCertBounds(secret *corev1.Secret) (time.Time, time.Time, error) {
|
|
certPEM := secret.Data[tlsCertificateChainSecretKey]
|
|
if certPEM == nil {
|
|
return time.Time{}, time.Time{}, constable.Error("failed to find certificate")
|
|
}
|
|
|
|
certBlock, _ := pem.Decode(certPEM)
|
|
if certBlock == nil {
|
|
return time.Time{}, time.Time{}, constable.Error("failed to decode certificate PEM")
|
|
}
|
|
|
|
cert, err := x509.ParseCertificate(certBlock.Bytes)
|
|
if err != nil {
|
|
return time.Time{}, time.Time{}, fmt.Errorf("failed to parse certificate: %w", err)
|
|
}
|
|
|
|
return cert.NotBefore, cert.NotAfter, nil
|
|
}
|