Merge pull request #81 from jetstack-experimental/acme-events
Add Events for ACME authorisation flow
This commit is contained in:
commit
fbe7f542bd
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user