Merge pull request #81 from jetstack-experimental/acme-events

Add Events for ACME authorisation flow
This commit is contained in:
James Munnelly 2017-09-11 10:40:08 +01:00 committed by GitHub
commit fbe7f542bd
6 changed files with 155 additions and 38 deletions

View File

@ -94,3 +94,46 @@ func (iss *Issuer) UpdateStatusCondition(conditionType IssuerConditionType, stat
}
}
}
func (crt *Certificate) HasCondition(condition CertificateCondition) bool {
if len(crt.Status.Conditions) == 0 {
return false
}
for _, cond := range crt.Status.Conditions {
if condition.Type == cond.Type && condition.Status == cond.Status {
return true
}
}
return false
}
func (crt *Certificate) UpdateStatusCondition(conditionType CertificateConditionType, status ConditionStatus, reason, message string) {
newCondition := CertificateCondition{
Type: conditionType,
Status: status,
Reason: reason,
Message: message,
}
t := time.Now()
if len(crt.Status.Conditions) == 0 {
glog.Infof("Setting lastTransitionTime for Certificate %q condition %q to %v", crt.Name, conditionType, t)
newCondition.LastTransitionTime = metav1.NewTime(t)
crt.Status.Conditions = []CertificateCondition{newCondition}
} else {
for i, cond := range crt.Status.Conditions {
if cond.Type == conditionType {
if cond.Status != newCondition.Status {
glog.Infof("Found status change for Certificate %q condition %q: %q -> %q; setting lastTransitionTime to %v", crt.Name, conditionType, cond.Status, status, t)
newCondition.LastTransitionTime = metav1.NewTime(t)
} else {
newCondition.LastTransitionTime = cond.LastTransitionTime
}
crt.Status.Conditions[i] = newCondition
break
}
}
}
}

View File

@ -220,9 +220,40 @@ type ACMECertificateDNS01Config struct {
// CertificateStatus defines the observed state of Certificate
type CertificateStatus struct {
ACME *CertificateACMEStatus `json:"acme,omitempty"`
Conditions []CertificateCondition `json:"conditions"`
ACME *CertificateACMEStatus `json:"acme,omitempty"`
}
// CertificateCondition contains condition information for an Certificate.
type CertificateCondition struct {
// Type of the condition, currently ('Ready').
Type CertificateConditionType `json:"type"`
// Status of the condition, one of ('True', 'False', 'Unknown').
Status ConditionStatus `json:"status"`
// LastTransitionTime is the timestamp corresponding to the last status
// change of this condition.
LastTransitionTime metav1.Time `json:"lastTransitionTime"`
// Reason is a brief machine readable explanation for the condition's last
// transition.
Reason string `json:"reason"`
// Message is a human readable description of the details of the last
// transition, complementing reason.
Message string `json:"message"`
}
// CertificateConditionType represents an Certificate condition value.
type CertificateConditionType string
const (
// CertificateConditionReady represents the fact that a given Certificate condition
// is in ready state.
CertificateConditionReady CertificateConditionType = "Ready"
)
// CertificateACMEStatus holds the status for an ACME issuer
type CertificateACMEStatus struct {
Authorizations []ACMEDomainAuthorization `json:"acme"`

View File

@ -92,6 +92,10 @@ func RegisterDeepCopies(scheme *runtime.Scheme) error {
in.(*CertificateACMEStatus).DeepCopyInto(out.(*CertificateACMEStatus))
return nil
}, InType: reflect.TypeOf(&CertificateACMEStatus{})},
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
in.(*CertificateCondition).DeepCopyInto(out.(*CertificateCondition))
return nil
}, InType: reflect.TypeOf(&CertificateCondition{})},
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
in.(*CertificateList).DeepCopyInto(out.(*CertificateList))
return nil
@ -462,6 +466,23 @@ func (in *CertificateACMEStatus) DeepCopy() *CertificateACMEStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CertificateCondition) DeepCopyInto(out *CertificateCondition) {
*out = *in
in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateCondition.
func (in *CertificateCondition) DeepCopy() *CertificateCondition {
if in == nil {
return nil
}
out := new(CertificateCondition)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CertificateList) DeepCopyInto(out *CertificateList) {
*out = *in

View File

@ -1,11 +1,19 @@
package issuers
import (
"github.com/golang/glog"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/errors"
"github.com/jetstack-experimental/cert-manager/pkg/apis/certmanager/v1alpha1"
)
const (
errorInitIssuer = "ErrInitIssuer"
messageErrorInitIssuer = "Error initializing issuer: "
)
func (c *Controller) Sync(iss *v1alpha1.Issuer) (err error) {
i, err := c.issuerFactory.IssuerFor(iss)
@ -27,6 +35,9 @@ func (c *Controller) Sync(iss *v1alpha1.Issuer) (err error) {
}()
if err != nil {
s := messageErrorInitIssuer + err.Error()
glog.Info(s)
c.recorder.Event(iss, v1.EventTypeWarning, errorInitIssuer, s)
return err
}

View File

@ -2,40 +2,52 @@ package acme
import (
"context"
"errors"
"fmt"
"log"
"sync"
"golang.org/x/crypto/acme"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/api/core/v1"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"github.com/golang/glog"
"github.com/jetstack-experimental/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack-experimental/cert-manager/pkg/util"
"github.com/jetstack-experimental/cert-manager/pkg/util/kube"
)
const (
successObtainedAuthorization = "ObtainAuthorization"
reasonPresentChallenge = "PresentChallenge"
reasonSelfCheck = "SelfCheck"
errorGetACMEAccount = "ErrGetACMEAccount"
errorCheckAuthorization = "ErrCheckAuthorization"
errorObtainAuthorization = "ErrObtainAuthorization"
messageObtainedAuthorization = "Obtained authorization for domain %s"
messagePresentChallenge = "Presenting %s challenge for domain %s"
messageSelfCheck = "Performing self-check for domain %s"
messageErrorGetACMEAccount = "Error getting ACME account: "
messageErrorCheckAuthorization = "Error checking ACME domain validation: "
messageErrorObtainAuthorization = "Error obtaining ACME domain authorization: "
)
// Prepare will ensure the issuer has been initialised and is ready to issue
// certificates for the domains listed on the Certificate resource.
//
// It will send the appropriate Letsencrypt authorizations, and complete
// challenge requests if neccessary.
func (a *Acme) Prepare(crt *v1alpha1.Certificate) (v1alpha1.CertificateStatus, error) {
updateStatus := *crt.Status.DeepCopy()
if crt.Spec.ACME == nil {
return updateStatus, fmt.Errorf("acme config must be specified")
}
update := crt.DeepCopy()
log.Printf("getting private key for acme issuer %s/%s", a.issuer.Namespace, a.issuer.Name)
accountPrivKey, err := kube.SecretTLSKey(a.secretsLister, a.issuer.Namespace, a.issuer.Spec.ACME.PrivateKey)
if k8sErrors.IsNotFound(err) {
return updateStatus, err
}
if err != nil {
return updateStatus, fmt.Errorf("error getting acme account private key: %s", err.Error())
s := messageErrorGetACMEAccount + err.Error()
update.UpdateStatusCondition(v1alpha1.CertificateConditionReady, v1alpha1.ConditionFalse, errorGetACMEAccount, s)
return update.Status, errors.New(s)
}
cl := &acme.Client{
@ -47,21 +59,25 @@ func (a *Acme) Prepare(crt *v1alpha1.Certificate) (v1alpha1.CertificateStatus, e
toAuthorize, err := authorizationsToObtain(cl, *crt)
if err != nil {
return updateStatus, err
s := messageErrorCheckAuthorization + err.Error()
update.UpdateStatusCondition(v1alpha1.CertificateConditionReady, v1alpha1.ConditionFalse, errorCheckAuthorization, s)
return update.Status, errors.New(s)
}
log.Printf("need to get authorizations for %v", toAuthorize)
// step two: if there are any domains that we don't have authorization for,
// we should attempt to authorize those domains
if len(toAuthorize) == 0 {
return updateStatus, nil
// TODO: set a field in the status block to show authorizations have
// been obtained so we can periodically update the auth status
return update.Status, nil
}
auths, err := getAuthorizations(cl, toAuthorize...)
if err != nil {
return updateStatus, err
s := messageErrorCheckAuthorization + err.Error()
update.UpdateStatusCondition(v1alpha1.CertificateConditionReady, v1alpha1.ConditionFalse, errorCheckAuthorization, s)
return update.Status, errors.New(s)
}
log.Printf("requested authorizations for %v", toAuthorize)
@ -71,7 +87,7 @@ func (a *Acme) Prepare(crt *v1alpha1.Certificate) (v1alpha1.CertificateStatus, e
authResponse
*acme.Authorization
error
})
}, len(auths))
for _, auth := range auths {
wg.Add(1)
go func(auth authResponse) {
@ -103,10 +119,13 @@ func (a *Acme) Prepare(crt *v1alpha1.Certificate) (v1alpha1.CertificateStatus, e
}
if len(errs) > 0 {
return updateStatus, utilerrors.NewAggregate(errs)
err = utilerrors.NewAggregate(errs)
s := messageErrorCheckAuthorization + err.Error()
update.UpdateStatusCondition(v1alpha1.CertificateConditionReady, v1alpha1.ConditionFalse, errorCheckAuthorization, s)
return update.Status, err
}
return updateStatus, nil
return update.Status, nil
}
func keyForChallenge(cl *acme.Client, challenge *acme.Challenge) (string, error) {
@ -123,12 +142,12 @@ func keyForChallenge(cl *acme.Client, challenge *acme.Challenge) (string, error)
}
func (a *Acme) authorize(cl *acme.Client, crt *v1alpha1.Certificate, auth authResponse) (*acme.Authorization, error) {
log.Printf("picking challenge type for domain '%s'", auth.domain)
glog.V(4).Infof("picking challenge type for domain '%s'", auth.domain)
challengeType, err := pickChallengeType(auth.domain, auth.auth, crt.Spec.ACME.Config)
if err != nil {
return nil, fmt.Errorf("error picking challenge type to use for domain '%s': %s", auth.domain, err.Error())
}
log.Printf("using challenge type %s for domain '%s'", challengeType, auth.domain)
glog.V(4).Infof("using challenge type %s for domain '%s'", challengeType, auth.domain)
challenge, err := challengeForAuthorization(cl, auth.auth, challengeType)
if err != nil {
return nil, fmt.Errorf("error getting challenge for domain '%s': %s", auth.domain, err.Error())
@ -145,34 +164,34 @@ func (a *Acme) authorize(cl *acme.Client, crt *v1alpha1.Certificate, auth authRe
defer solver.CleanUp(context.Background(), crt, auth.domain, token, key)
log.Printf("presenting challenge for domain %s, token %s key %s", auth.domain, token, key)
a.recorder.Eventf(crt, v1.EventTypeNormal, reasonPresentChallenge, messagePresentChallenge, challengeType, auth.domain)
err = solver.Present(context.Background(), crt, auth.domain, token, key)
if err != nil {
return nil, fmt.Errorf("error presenting acme authorization for domain '%s': %s", auth.domain, err.Error())
}
log.Printf("waiting for key to be available to acme servers for domain %s", auth.domain)
a.recorder.Eventf(crt, v1.EventTypeNormal, reasonSelfCheck, messageSelfCheck, auth.domain)
err = solver.Wait(context.Background(), crt, auth.domain, token, key)
if err != nil {
return nil, fmt.Errorf("error waiting for key to be available for domain '%s': %s", auth.domain, err.Error())
}
log.Printf("accepting %s challenge for domain %s", challengeType, auth.domain)
challenge, err = cl.Accept(context.Background(), challenge)
if err != nil {
return nil, fmt.Errorf("error accepting acme challenge for domain '%s': %s", auth.domain, err.Error())
}
log.Printf("waiting for authorization for domain %s (%s)...", auth.domain, challenge.URI)
glog.V(4).Infof("waiting for authorization for domain %s (%s)...", auth.domain, challenge.URI)
authorization, err := cl.WaitAuthorization(context.Background(), challenge.URI)
if err != nil {
return nil, fmt.Errorf("error waiting for authorization for domain '%s': %s", auth.domain, err.Error())
}
if authorization.Status != acme.StatusValid {
return nil, fmt.Errorf("expected acme domain authorization status for '%s' to be valid, but it's %s", auth.domain, authorization.Status)
return nil, fmt.Errorf("expected acme domain authorization status for '%s' to be valid, but it is %s", auth.domain, authorization.Status)
}
log.Printf("got successful authorization for domain %s", auth.domain)
a.recorder.Eventf(crt, v1.EventTypeNormal, successObtainedAuthorization, messageObtainedAuthorization, auth.domain)
return authorization, nil
}

View File

@ -18,8 +18,8 @@ import (
)
const (
errorAccountRegistrationFailed = "ErrorRegisteringACMEAccount"
errorAccountVerificationFailed = "ErrorVerifyingACMEAccount"
errorAccountRegistrationFailed = "ErrRegisterACMEAccount"
errorAccountVerificationFailed = "ErrVerifyACMEAccount"
successAccountRegistered = "ACMEAccountRegistered"
successAccountVerified = "ACMEAccountVerified"
@ -41,8 +41,6 @@ func (a *Acme) Setup() (v1alpha1.IssuerStatus, error) {
if err != nil {
s := messageAccountRegistrationFailed + err.Error()
glog.Info(s)
a.recorder.Event(a.issuer, v1.EventTypeWarning, errorAccountRegistrationFailed, s)
update.UpdateStatusCondition(v1alpha1.IssuerConditionReady, v1alpha1.ConditionFalse, errorAccountRegistrationFailed, s)
return update.Status, fmt.Errorf(s)
}
@ -55,8 +53,6 @@ func (a *Acme) Setup() (v1alpha1.IssuerStatus, error) {
_, err = cl.GetReg(context.Background(), a.issuer.Status.ACMEStatus().URI)
if err == nil {
glog.Info(messageAccountVerified)
a.recorder.Event(a.issuer, v1.EventTypeNormal, successAccountVerified, messageAccountVerified)
update.UpdateStatusCondition(v1alpha1.IssuerConditionReady, v1alpha1.ConditionTrue, successAccountVerified, messageAccountVerified)
return update.Status, nil
}
@ -74,14 +70,10 @@ func (a *Acme) Setup() (v1alpha1.IssuerStatus, error) {
if err != nil {
s := messageAccountRegistrationFailed + err.Error()
glog.Info(s)
a.recorder.Event(a.issuer, v1.EventTypeWarning, errorAccountRegistrationFailed, s)
update.UpdateStatusCondition(v1alpha1.IssuerConditionReady, v1alpha1.ConditionFalse, errorAccountRegistrationFailed, s)
return update.Status, err
}
glog.V(4).Info(messageAccountRegistered)
a.recorder.Event(a.issuer, v1.EventTypeNormal, successAccountRegistered, messageAccountRegistered)
update.UpdateStatusCondition(v1alpha1.IssuerConditionReady, v1alpha1.ConditionTrue, successAccountRegistered, messageAccountRegistered)
update.Status.ACMEStatus().URI = account.URI