337 lines
12 KiB
Go
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)
|
|
}
|