Set up scheduled work queue for certificate renewals
This commit is contained in:
parent
d7321d0bad
commit
dd02061738
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
39
pkg/scheduler/scheduler.go
Normal file
39
pkg/scheduler/scheduler.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user