Set up scheduled work queue for certificate renewals

This commit is contained in:
James Munnelly 2017-08-05 22:51:52 +01:00
parent d7321d0bad
commit dd02061738
3 changed files with 146 additions and 35 deletions

View File

@ -23,6 +23,7 @@ import (
"github.com/munnerz/cert-manager/pkg/informers/externalversions"
cmlisters "github.com/munnerz/cert-manager/pkg/listers/certmanager/v1alpha1"
"github.com/munnerz/cert-manager/pkg/log"
"github.com/munnerz/cert-manager/pkg/scheduler"
)
var _ controllerpkg.Constructor = New
@ -48,7 +49,8 @@ type controller struct {
ingressInformerSynced cache.InformerSynced
ingressLister extlisters.IngressLister
queue workqueue.RateLimitingInterface
queue workqueue.RateLimitingInterface
scheduledWorkQueue *scheduler.ScheduledWorkQueue
}
// New returns a new Certificates controller. It sets up the informer handler
@ -61,6 +63,10 @@ func New(client kubernetes.Interface,
ctrl := &controller{client: client, cmClient: cmClient}
ctrl.syncHandler = ctrl.processNextWorkItem
ctrl.queue = workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "certificates")
// Create a scheduled work queue that calls the ctrl.queue.Add method for
// each object in the queue. This is used to schedule re-checks of
// Certificate resources when they get near to expiry
ctrl.scheduledWorkQueue = scheduler.NewScheduledWorkQueue(ctrl.queue.Add)
secretsInformer := factory.Core().V1().Secrets()
ingressInformer := factory.Extensions().V1beta1().Ingresses()
@ -70,6 +76,7 @@ func New(client kubernetes.Interface,
certificatesInformer.Informer().AddEventHandlerWithResyncPeriod(cache.ResourceEventHandlerFuncs{
AddFunc: ctrl.certificateAdded,
UpdateFunc: ctrl.certificateUpdated,
DeleteFunc: ctrl.certificateDeleted,
}, time.Minute*5)
ctrl.certificateInformerSynced = certificatesInformer.Informer().HasSynced
ctrl.certificateLister = certificatesInformer.Lister()
@ -99,9 +106,13 @@ func (c *controller) certificateAdded(obj interface{}) {
runtime.HandleError(fmt.Errorf("expected *Certificate but got %T in work queue", obj))
return
}
c.enqueueCertificate(certificate)
}
func (c *controller) enqueueCertificate(crt *v1alpha1.Certificate) {
var key string
var err error
if key, err = keyFunc(certificate); err != nil {
if key, err = keyFunc(crt); err != nil {
runtime.HandleError(err)
return
}
@ -118,13 +129,24 @@ func (c *controller) certificateUpdated(prev, obj interface{}) {
if reflect.DeepEqual(prev, obj) {
return
}
var key string
var err error
if key, err = keyFunc(certificate); err != nil {
runtime.HandleError(err)
return
c.enqueueCertificate(certificate)
}
func (c *controller) certificateDeleted(obj interface{}) {
certificate, ok := obj.(*v1alpha1.Certificate)
if !ok {
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
if !ok {
runtime.HandleError(fmt.Errorf("Couldn't get object from tombstone %#v", obj))
return
}
certificate, ok = tombstone.Obj.(*v1alpha1.Certificate)
if !ok {
runtime.HandleError(fmt.Errorf("Tombstone contained object that is not a Secret %#v", obj))
return
}
}
c.queue.Add(key)
c.enqueueCertificate(certificate)
}
func (c *controller) secretDeleted(obj interface{}) {
@ -224,7 +246,7 @@ func (c *controller) worker() {
runtime.HandleError(fmt.Errorf("expected string in workqueue but got %T", obj))
return nil
}
if err := c.processNextWorkItem(key); err != nil {
if err := c.syncHandler(key); err != nil {
return err
}
c.queue.Forget(obj)
@ -253,6 +275,7 @@ func (c *controller) processNextWorkItem(key string) error {
if err != nil {
if k8sErrors.IsNotFound(err) {
c.scheduledWorkQueue.Forget(key)
runtime.HandleError(fmt.Errorf("certificate '%s' in work queue no longer exists", key))
return nil
}

View File

@ -1,21 +1,28 @@
package certificates
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"log"
"time"
api "k8s.io/api/core/v1"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/runtime"
"github.com/munnerz/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/munnerz/cert-manager/pkg/issuer"
"github.com/munnerz/cert-manager/pkg/util"
)
func (c *controller) sync(crt *v1alpha1.Certificate) error {
const renewBefore = time.Hour * 24 * 30
var errInvalidCertificateData = fmt.Errorf("invalid certificate data")
func (c *controller) sync(crt *v1alpha1.Certificate) (err error) {
// step zero: check if the referenced issuer exists and is ready
issuerObj, err := c.issuerLister.Issuers(crt.Namespace).Get(crt.Spec.Issuer)
@ -42,69 +49,106 @@ func (c *controller) sync(crt *v1alpha1.Certificate) error {
}
log.Printf("Finished preparing with Issuer '%s/%s' and Certificate '%s/%s'", issuerObj.Namespace, issuerObj.Name, crt.Namespace, crt.Name)
defer c.scheduleRenewal(crt)
// step one: check if referenced secret exists, if not, trigger issue event
secret, err := c.secretLister.Secrets(crt.Namespace).Get(crt.Spec.SecretName)
cert, _, err := c.getCertificate(crt.Namespace, crt.Spec.SecretName)
if err != nil {
if k8sErrors.IsNotFound(err) {
if k8sErrors.IsNotFound(err) || err == errInvalidCertificateData {
return c.issue(i, crt)
}
return err
}
// step two: check if referenced secret is valid for listed domains. if not, return failure
if !util.EqualUnsorted(crt.Spec.Domains, cert.DNSNames) {
log.Printf("list of domains on certificate do not match domains in spec")
return c.issue(i, crt)
}
durationUntilExpiry := cert.NotAfter.Sub(time.Now())
renewIn := durationUntilExpiry - renewBefore
// step three: check if referenced secret is valid (after start & before expiry)
if renewIn <= 0 {
return c.renew(i, crt)
}
return nil
}
func (c *controller) getCertificate(namespace, name string) (*x509.Certificate, *rsa.PrivateKey, error) {
secret, err := c.client.CoreV1().Secrets(namespace).Get(name, metav1.GetOptions{})
if err != nil {
return nil, nil, err
}
certBytes, okcert := secret.Data[api.TLSCertKey]
keyBytes, okkey := secret.Data[api.TLSPrivateKeyKey]
// check if the certificate and private key exist, if not, trigger an issue
if !okcert || !okkey {
return c.issue(i, crt)
return nil, nil, fmt.Errorf("invalid certificate data")
}
// decode the tls certificate pem
block, _ := pem.Decode(certBytes)
if block == nil {
log.Printf("error decoding cert PEM block in '%s'", crt.Spec.SecretName)
return c.issue(i, crt)
log.Printf("error decoding cert PEM block in '%s/%s'", namespace, name)
return nil, nil, errInvalidCertificateData
}
// parse the tls certificate
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
log.Printf("error parsing TLS certificate in '%s': %s", crt.Spec.SecretName, err.Error())
return c.issue(i, crt)
log.Printf("error parsing TLS certificate in '%s/%s': %s", namespace, name, err.Error())
return nil, nil, errInvalidCertificateData
}
// decode the private key pem
block, _ = pem.Decode(keyBytes)
if block == nil {
log.Printf("error decoding private key PEM block in '%s'", crt.Spec.SecretName)
return c.issue(i, crt)
log.Printf("error decoding private key PEM block in '%s/%s'", namespace, name)
return nil, nil, errInvalidCertificateData
}
// parse the private key
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
log.Printf("error parsing private key in '%s': %s", crt.Spec.SecretName, err.Error())
return c.issue(i, crt)
log.Printf("error parsing private key in '%s/%s': %s", namespace, name, err.Error())
return nil, nil, errInvalidCertificateData
}
// validate the private key
if err = key.Validate(); err != nil {
log.Printf("private key failed validation in '%s': %s", crt.Spec.SecretName, err.Error())
return c.issue(i, crt)
log.Printf("private key failed validation in '%s/%s': %s", namespace, name, err.Error())
return nil, nil, errInvalidCertificateData
}
// step two: check if referenced secret is valid for listed domains. if not, return failure
if !util.EqualUnsorted(crt.Spec.Domains, cert.DNSNames) {
log.Printf("list of domains on certificate do not match domains in spec")
return c.issue(i, crt)
}
// step three: check if referenced secret is valid (after start & before expiry)
// if time.Now().Sub(cert.NotAfter) > time.Hour*(24*30) {
// return c.renew(crt)
// }
return nil
return cert, key, nil
}
func (c *controller) scheduleRenewal(crt *v1alpha1.Certificate) {
key, err := keyFunc(crt)
if err != nil {
runtime.HandleError(fmt.Errorf("error getting key for certificate resource: %s", err.Error()))
return
}
cert, _, err := c.getCertificate(crt.Namespace, crt.Spec.SecretName)
if err != nil {
runtime.HandleError(fmt.Errorf("[%s/%s] Error getting certificate '%s': %s", crt.Namespace, crt.Name, crt.Spec.SecretName, err.Error()))
return
}
durationUntilExpiry := cert.NotAfter.Sub(time.Now())
renewIn := durationUntilExpiry - renewBefore
log.Printf("[%s/%s] Scheduling renewal in %d hours", crt.Namespace, crt.Name, renewIn/time.Hour)
c.scheduledWorkQueue.Add(key, renewIn)
}
// issue will attempt to retrieve a certificate from the specified issuer, or
// return an error on failure. If retrieval is succesful, the certificate data
// and private key will be stored in the named secret
func (c *controller) issue(issuer issuer.Interface, crt *v1alpha1.Certificate) error {
log.Printf("[%s/%s] Issuing certificate...", crt.Namespace, crt.Name)
key, cert, err := issuer.Issue(crt)
if err != nil {
return fmt.Errorf("error issuing certificate: %s", err.Error())
@ -125,6 +169,8 @@ func (c *controller) issue(issuer issuer.Interface, crt *v1alpha1.Certificate) e
return fmt.Errorf("error saving certificate: %s", err.Error())
}
log.Printf("[%s/%s] Successfully issued certificate (%s)", crt.Namespace, crt.Name, crt.Spec.SecretName)
return nil
}
@ -132,6 +178,7 @@ func (c *controller) issue(issuer issuer.Interface, crt *v1alpha1.Certificate) e
// return an error on failure. If renewal is succesful, the certificate data
// and private key will be stored in the named secret
func (c *controller) renew(issuer issuer.Interface, crt *v1alpha1.Certificate) error {
log.Printf("[%s/%s] Renewing certificate...", crt.Namespace, crt.Name)
key, cert, err := issuer.Renew(crt)
if err != nil {
return fmt.Errorf("error renewing certificate: %s", err.Error())
@ -152,5 +199,7 @@ func (c *controller) renew(issuer issuer.Interface, crt *v1alpha1.Certificate) e
return fmt.Errorf("error saving certificate: %s", err.Error())
}
log.Printf("[%s/%s] Successfully renewed certificate (%s)", crt.Namespace, crt.Name, crt.Spec.SecretName)
return nil
}

View File

@ -0,0 +1,39 @@
package scheduler
import (
"sync"
"time"
)
type ProcessFunc func(interface{})
type ScheduledWorkQueue struct {
processFunc ProcessFunc
work map[interface{}]*time.Timer
workLock sync.Mutex
}
func NewScheduledWorkQueue(processFunc ProcessFunc) *ScheduledWorkQueue {
return &ScheduledWorkQueue{processFunc, make(map[interface{}]*time.Timer), sync.Mutex{}}
}
func (s *ScheduledWorkQueue) Add(obj interface{}, duration time.Duration) {
s.clearTimer(obj)
s.work[obj] = time.AfterFunc(duration, func() {
defer s.clearTimer(obj)
s.processFunc(obj)
})
}
func (s *ScheduledWorkQueue) Forget(obj interface{}) {
s.clearTimer(obj)
}
func (s *ScheduledWorkQueue) clearTimer(obj interface{}) {
s.workLock.Lock()
defer s.workLock.Unlock()
if timer, ok := s.work[obj]; ok {
timer.Stop()
delete(s.work, obj)
}
}