diff --git a/pkg/apis/certmanager/v1alpha1/helpers.go b/pkg/apis/certmanager/v1alpha1/helpers.go index 4a6f66b89..a619d325c 100644 --- a/pkg/apis/certmanager/v1alpha1/helpers.go +++ b/pkg/apis/certmanager/v1alpha1/helpers.go @@ -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 + } + } + } +} diff --git a/pkg/apis/certmanager/v1alpha1/types.go b/pkg/apis/certmanager/v1alpha1/types.go index bfc9c497f..1a2e4b00e 100644 --- a/pkg/apis/certmanager/v1alpha1/types.go +++ b/pkg/apis/certmanager/v1alpha1/types.go @@ -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"` diff --git a/pkg/apis/certmanager/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/certmanager/v1alpha1/zz_generated.deepcopy.go index ab29bc12d..a84025c38 100644 --- a/pkg/apis/certmanager/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/certmanager/v1alpha1/zz_generated.deepcopy.go @@ -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 diff --git a/pkg/controller/issuers/sync.go b/pkg/controller/issuers/sync.go index 9ee7f8dc5..73ae984ca 100644 --- a/pkg/controller/issuers/sync.go +++ b/pkg/controller/issuers/sync.go @@ -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 } diff --git a/pkg/issuer/acme/prepare.go b/pkg/issuer/acme/prepare.go index 21e457785..580012e7f 100644 --- a/pkg/issuer/acme/prepare.go +++ b/pkg/issuer/acme/prepare.go @@ -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 } diff --git a/pkg/issuer/acme/setup.go b/pkg/issuer/acme/setup.go index 9bd529919..146d2d1b5 100644 --- a/pkg/issuer/acme/setup.go +++ b/pkg/issuer/acme/setup.go @@ -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