cert-manager/pkg/controller/certificates/sync.go
2018-08-08 13:34:30 +01:00

337 lines
12 KiB
Go

package certificates
import (
"context"
"fmt"
"reflect"
"strings"
"time"
api "k8s.io/api/core/v1"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/runtime"
"github.com/golang/glog"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/validation"
"github.com/jetstack/cert-manager/pkg/issuer"
"github.com/jetstack/cert-manager/pkg/util"
"github.com/jetstack/cert-manager/pkg/util/errors"
"github.com/jetstack/cert-manager/pkg/util/kube"
"github.com/jetstack/cert-manager/pkg/util/pki"
)
const (
errorIssuerNotFound = "IssuerNotFound"
errorIssuerNotReady = "IssuerNotReady"
errorIssuerInit = "IssuerInitError"
errorSavingCertificate = "SaveCertError"
errorConfig = "ConfigError"
reasonIssuingCertificate = "IssueCert"
reasonRenewingCertificate = "RenewCert"
successCertificateIssued = "CertIssued"
successCertificateRenewed = "CertRenewed"
messageErrorSavingCertificate = "Error saving TLS certificate: "
messageIssuingCertificate = "Issuing certificate..."
messageRenewingCertificate = "Renewing certificate..."
messageCertificateIssued = "Certificate issued successfully"
messageCertificateRenewed = "Certificate renewed successfully"
)
func (c *Controller) Sync(ctx context.Context, crt *v1alpha1.Certificate) (err error) {
crtCopy := crt.DeepCopy()
defer func() {
if _, saveErr := c.updateCertificateStatus(crt, crtCopy); saveErr != nil {
err = utilerrors.NewAggregate([]error{saveErr, err})
}
}()
el := validation.ValidateCertificate(crtCopy)
if len(el) > 0 {
msg := fmt.Sprintf("Resource validation failed: %v", el.ToAggregate())
crtCopy.UpdateStatusCondition(v1alpha1.CertificateConditionReady, v1alpha1.ConditionFalse, errorConfig, msg, false)
return
} else {
for i, c := range crtCopy.Status.Conditions {
if c.Type == v1alpha1.CertificateConditionReady {
if c.Reason == errorConfig && c.Status == v1alpha1.ConditionFalse {
crtCopy.Status.Conditions = append(crtCopy.Status.Conditions[:i], crtCopy.Status.Conditions[i+1:]...)
break
}
}
}
}
// step zero: check if the referenced issuer exists and is ready
issuerObj, err := c.getGenericIssuer(crtCopy)
if err != nil {
s := fmt.Sprintf("Issuer %s does not exist", err.Error())
glog.Info(s)
c.Recorder.Event(crtCopy, api.EventTypeWarning, errorIssuerNotFound, s)
return err
}
el = validation.ValidateCertificateForIssuer(crtCopy, issuerObj)
if len(el) > 0 {
msg := fmt.Sprintf("Resource validation failed: %v", el.ToAggregate())
crtCopy.UpdateStatusCondition(v1alpha1.CertificateConditionReady, v1alpha1.ConditionFalse, errorConfig, msg, false)
return
} else {
for i, c := range crtCopy.Status.Conditions {
if c.Type == v1alpha1.CertificateConditionReady {
if c.Reason == errorConfig && c.Status == v1alpha1.ConditionFalse {
crtCopy.Status.Conditions = append(crtCopy.Status.Conditions[:i], crtCopy.Status.Conditions[i+1:]...)
break
}
}
}
}
issuerReady := issuerObj.HasCondition(v1alpha1.IssuerCondition{
Type: v1alpha1.IssuerConditionReady,
Status: v1alpha1.ConditionTrue,
})
if !issuerReady {
s := fmt.Sprintf("Issuer %s not ready", issuerObj.GetObjectMeta().Name)
glog.Info(s)
c.Recorder.Event(crtCopy, api.EventTypeWarning, errorIssuerNotReady, s)
return fmt.Errorf(s)
}
i, err := c.IssuerFactory().IssuerFor(issuerObj)
if err != nil {
s := "Error initializing issuer: " + err.Error()
glog.Info(s)
c.Recorder.Event(crtCopy, api.EventTypeWarning, errorIssuerInit, s)
return err
}
expectedCN := pki.CommonNameForCertificate(crtCopy)
expectedDNSNames := pki.DNSNamesForCertificate(crtCopy)
if expectedCN == "" || len(expectedDNSNames) == 0 {
// TODO: Set certificate invalid condition on certificate resource
// TODO: remove this check in favour of resource validation
return fmt.Errorf("certificate must specify at least one of dnsNames or commonName")
}
// grab existing certificate and validate private key
cert, err := kube.SecretTLSCert(c.secretLister, crtCopy.Namespace, crtCopy.Spec.SecretName)
// if an error is returned, and that error is something other than
// IsNotFound or invalid data, then we should return the error.
if err != nil && !k8sErrors.IsNotFound(err) && !errors.IsInvalidData(err) {
return err
}
// as there is an existing certificate, or we may create one below, we will
// run scheduleRenewal to schedule a renewal if required at the end of
// execution.
defer c.scheduleRenewal(crtCopy)
// if the certificate was not found, or the certificate data is invalid, we
// should issue a new certificate.
// if the certificate is valid for a list of domains other than those
// listed in the certificate spec, we should re-issue the certificate.
if k8sErrors.IsNotFound(err) || errors.IsInvalidData(err) ||
expectedCN != cert.Subject.CommonName || !util.EqualUnsorted(cert.DNSNames, expectedDNSNames) {
return c.issue(ctx, i, crtCopy)
}
// if we should being attempting to renew now, then trigger a renewal
if c.Context.IssuerOptions.CertificateNeedsRenew(cert) {
return c.renew(ctx, i, crtCopy)
}
return nil
}
// TODO: replace with a call to controllerpkg.Helper.GetGenericIssuer
func (c *Controller) getGenericIssuer(crt *v1alpha1.Certificate) (v1alpha1.GenericIssuer, error) {
switch crt.Spec.IssuerRef.Kind {
case "", v1alpha1.IssuerKind:
return c.issuerLister.Issuers(crt.Namespace).Get(crt.Spec.IssuerRef.Name)
case v1alpha1.ClusterIssuerKind:
if c.clusterIssuerLister == nil {
return nil, fmt.Errorf("cannot get ClusterIssuer for %q as cert-manager is scoped to a single namespace", crt.Name)
}
return c.clusterIssuerLister.Get(crt.Spec.IssuerRef.Name)
default:
return nil, fmt.Errorf(`invalid value %q for certificate issuer kind. Must be empty, %q or %q`, crt.Spec.IssuerRef.Kind, v1alpha1.IssuerKind, v1alpha1.ClusterIssuerKind)
}
}
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 := kube.SecretTLSCert(c.secretLister, 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 - c.Context.IssuerOptions.RenewBeforeExpiryDuration
c.scheduledWorkQueue.Add(key, renewIn)
glog.Infof("Certificate %s/%s scheduled for renewal in %d hours", crt.Namespace, crt.Name, renewIn/time.Hour)
}
// issuerKind returns the kind of issuer for a certificate
func issuerKind(crt *v1alpha1.Certificate) string {
if crt.Spec.IssuerRef.Kind == "" {
return v1alpha1.IssuerKind
} else {
return crt.Spec.IssuerRef.Kind
}
}
func (c *Controller) updateSecret(crt *v1alpha1.Certificate, namespace string, cert, key []byte) (*api.Secret, error) {
secret, err := c.Client.CoreV1().Secrets(namespace).Get(crt.Spec.SecretName, metav1.GetOptions{})
if err != nil && !k8sErrors.IsNotFound(err) {
return nil, err
}
if k8sErrors.IsNotFound(err) {
secret = &api.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: crt.Spec.SecretName,
Namespace: namespace,
},
Type: api.SecretTypeTLS,
Data: map[string][]byte{},
}
}
secret.Data[api.TLSCertKey] = cert
secret.Data[api.TLSPrivateKeyKey] = key
if secret.Annotations == nil {
secret.Annotations = make(map[string]string)
}
// Note: since this sets annotations based on certificate resource, incorrect
// annotations will be set if resource and actual certificate somehow get out
// of sync
dnsNames := pki.DNSNamesForCertificate(crt)
cn := pki.CommonNameForCertificate(crt)
secret.Annotations[v1alpha1.AltNamesAnnotationKey] = strings.Join(dnsNames, ",")
secret.Annotations[v1alpha1.CommonNameAnnotationKey] = cn
secret.Annotations[v1alpha1.IssuerNameAnnotationKey] = crt.Spec.IssuerRef.Name
secret.Annotations[v1alpha1.IssuerKindAnnotationKey] = issuerKind(crt)
if secret.Labels == nil {
secret.Labels = make(map[string]string)
}
secret.Labels[v1alpha1.CertificateNameKey] = crt.Name
// if it is a new resource
if secret.SelfLink == "" {
secret, err = c.Client.CoreV1().Secrets(namespace).Create(secret)
} else {
secret, err = c.Client.CoreV1().Secrets(namespace).Update(secret)
}
if err != nil {
return nil, err
}
return secret, nil
}
// 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(ctx context.Context, issuer issuer.Interface, crt *v1alpha1.Certificate) error {
var err error
glog.Infof("Preparing certificate %s/%s with issuer", crt.Namespace, crt.Name)
if err = issuer.Prepare(ctx, crt); err != nil {
glog.Infof("Error preparing issuer for certificate %s/%s: %v", crt.Namespace, crt.Name, err)
return err
}
s := messageIssuingCertificate
glog.Info(s)
c.Recorder.Event(crt, api.EventTypeNormal, reasonIssuingCertificate, s)
var key, cert []byte
key, cert, err = issuer.Issue(ctx, crt)
if err != nil {
glog.Infof("Error issuing certificate for %s/%s: %v", crt.Namespace, crt.Name, err)
return err
}
if _, err := c.updateSecret(crt, crt.Namespace, cert, key); err != nil {
s := messageErrorSavingCertificate + err.Error()
glog.Info(s)
c.Recorder.Event(crt, api.EventTypeWarning, errorSavingCertificate, s)
return err
}
s = messageCertificateIssued
glog.Info(s)
c.Recorder.Event(crt, api.EventTypeNormal, successCertificateIssued, s)
crt.UpdateStatusCondition(v1alpha1.CertificateConditionReady, v1alpha1.ConditionTrue, successCertificateIssued, s, true)
return nil
}
// renew will attempt to renew a certificate from the specified issuer, or
// 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(ctx context.Context, issuer issuer.Interface, crt *v1alpha1.Certificate) error {
var err error
glog.Infof("Preparing certificate %s/%s with issuer", crt.Namespace, crt.Name)
if err = issuer.Prepare(ctx, crt); err != nil {
glog.Infof("Error preparing issuer for certificate %s/%s: %v", crt.Namespace, crt.Name, err)
return err
}
s := messageRenewingCertificate
glog.Info(s)
c.Recorder.Event(crt, api.EventTypeNormal, reasonRenewingCertificate, s)
var key, cert []byte
key, cert, err = issuer.Renew(ctx, crt)
if err != nil {
return err
}
if _, err := c.updateSecret(crt, crt.Namespace, cert, key); err != nil {
s := messageErrorSavingCertificate + err.Error()
glog.Info(s)
c.Recorder.Event(crt, api.EventTypeWarning, errorSavingCertificate, s)
return err
}
s = messageCertificateRenewed
glog.Info(s)
c.Recorder.Event(crt, api.EventTypeNormal, successCertificateRenewed, s)
crt.UpdateStatusCondition(v1alpha1.CertificateConditionReady, v1alpha1.ConditionTrue, successCertificateRenewed, s, true)
return nil
}
func (c *Controller) updateCertificateStatus(old, new *v1alpha1.Certificate) (*v1alpha1.Certificate, error) {
if reflect.DeepEqual(old.Status, new.Status) {
return nil, nil
}
// TODO: replace Update call with UpdateStatus. This requires a custom API
// server with the /status subresource enabled and/or subresource support
// for CRDs (https://github.com/kubernetes/kubernetes/issues/38113)
return c.CMClient.CertmanagerV1alpha1().Certificates(new.Namespace).Update(new)
}