diff --git a/pkg/controller/certificates/controller.go b/pkg/controller/certificates/controller.go index d5eb7197f..3d4797d1e 100644 --- a/pkg/controller/certificates/controller.go +++ b/pkg/controller/certificates/controller.go @@ -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 } diff --git a/pkg/controller/certificates/sync.go b/pkg/controller/certificates/sync.go index d718591ed..30a284024 100644 --- a/pkg/controller/certificates/sync.go +++ b/pkg/controller/certificates/sync.go @@ -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 } diff --git a/pkg/scheduler/scheduler.go b/pkg/scheduler/scheduler.go new file mode 100644 index 000000000..6f177e3ed --- /dev/null +++ b/pkg/scheduler/scheduler.go @@ -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) + } +}