cert-manager/pkg/controller/acmechallenges/sync.go
James Munnelly 5cc3240145 Fire Events when challenges succeed or fail
Signed-off-by: James Munnelly <james@munnelly.eu>
2018-11-07 20:38:22 +00:00

231 lines
7.3 KiB
Go

/*
Copyright 2018 The Jetstack cert-manager contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package acmechallenges
import (
"context"
"fmt"
"reflect"
"github.com/golang/glog"
corev1 "k8s.io/api/core/v1"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"github.com/jetstack/cert-manager/pkg/acme"
acmecl "github.com/jetstack/cert-manager/pkg/acme/client"
cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
acmeapi "github.com/jetstack/cert-manager/third_party/crypto/acme"
)
const (
reasonDomainVerified = "DomainVerified"
)
// solver solves ACME challenges by presenting the given token and key in an
// appropriate way given the config in the Issuer and Certificate.
type solver interface {
// Present the challenge value with the given solver.
Present(ctx context.Context, issuer cmapi.GenericIssuer, ch *cmapi.Challenge) error
// Check should return Error only if propagation check cannot be performed.
// It MUST return `false, nil` if can contact all relevant services and all is
// doing is waiting for propagation
Check(ch *cmapi.Challenge) (bool, error)
// CleanUp will remove challenge records for a given solver.
// This may involve deleting resources in the Kubernetes API Server, or
// communicating with other external components (e.g. DNS providers).
CleanUp(ctx context.Context, issuer cmapi.GenericIssuer, ch *cmapi.Challenge) error
}
// Sync will process this ACME Challenge.
// It is the core control function for ACME challenges, and handles:
// - TODO
func (c *Controller) Sync(ctx context.Context, ch *cmapi.Challenge) (err error) {
oldChal := ch
ch = ch.DeepCopy()
// bail out early on if processing=false, as this challenge has not been
// scheduled yet.
if ch.Status.Processing == false {
return nil
}
defer func() {
// TODO: replace with more efficient comparison
if reflect.DeepEqual(oldChal.Status, ch.Status) {
return
}
_, updateErr := c.CMClient.CertmanagerV1alpha1().Challenges(ch.Namespace).Update(ch)
if err != nil {
err = utilerrors.NewAggregate([]error{err, updateErr})
}
}()
// if a challenge is in a final state, we bail out early as there is nothing
// left for us to do here.
if acme.IsFinalState(ch.Status.State) {
// we set processing to false now, as this item has finished being processed.
ch.Status.Processing = false
return nil
}
genericIssuer, err := c.helper.GetGenericIssuer(ch.Spec.IssuerRef, ch.Namespace)
if err != nil {
return fmt.Errorf("error reading (cluster)issuer %q: %v", ch.Spec.IssuerRef.Name, err)
}
cl, err := c.acmeHelper.ClientForIssuer(genericIssuer)
if err != nil {
return err
}
if ch.Status.State == "" {
err := c.syncChallengeStatus(ctx, cl, ch)
if err != nil {
// TODO: check acme error types and potentially mark the challenge
// as failed if there is some known error
return err
}
// if the state has not changed, return an error
if ch.Status.State == "" {
return fmt.Errorf("could not determine acme challenge status. retrying after applying back-off")
}
// the change in the challenges status will trigger a resync.
// this ensures our cache is consistent so we don't call Present twice
// due to the http01 solver creating resources that this controller
// watches/syncs on
return nil
}
solver, err := c.solverFor(ch.Spec.Type)
if err != nil {
return err
}
if !ch.Status.Presented {
err := solver.Present(ctx, genericIssuer, ch)
if err != nil {
return err
}
ch.Status.Presented = true
c.Recorder.Eventf(ch, corev1.EventTypeNormal, "Presented", "Presented challenge using %s challenge mechanism", ch.Spec.Type)
}
ok, err := solver.Check(ch)
if err != nil {
return err
}
if !ok {
ch.Status.Reason = fmt.Sprintf("Waiting for %s challenge propagation", ch.Spec.Type)
return fmt.Errorf(ch.Status.Reason)
}
err = c.acceptChallenge(ctx, cl, ch)
if err != nil {
return err
}
glog.Infof("Cleaning up challenge %s/%s", ch.Namespace, ch.Name)
err = solver.CleanUp(ctx, genericIssuer, ch)
if err != nil {
return err
}
return nil
}
// syncChallengeStatus will communicate with the ACME server to retrieve the current
// state of the Challenge. It will then update the Challenge's status block with the new
// state of the Challenge.
func (c *Controller) syncChallengeStatus(ctx context.Context, cl acmecl.Interface, ch *cmapi.Challenge) error {
if ch.Spec.URL == "" {
return fmt.Errorf("challenge URL is blank - challenge has not been created yet")
}
acmeChallenge, err := cl.GetChallenge(ctx, ch.Spec.URL)
if err != nil {
return err
}
// TODO: should we validate the State returned by the ACME server here?
cmState := cmapi.State(acmeChallenge.Status)
ch.Status.State = cmState
return nil
}
// acceptChallenge will accept the challenge with the acme server and then wait
// for the authorization to reach a 'final' state.
// It will update the challenge's status to reflect the final state of the
// challenge if it failed, or the final state of the challenge's authorization
// if accepting the challenge succeeds.
func (c *Controller) acceptChallenge(ctx context.Context, cl acmecl.Interface, ch *cmapi.Challenge) error {
glog.Infof("Accepting challenge for domain %q", ch.Spec.DNSName)
// We manually construct an ACME challenge here from our own internal type
// to save additional round trips to the ACME server.
acmeChal := &acmeapi.Challenge{
URL: ch.Spec.URL,
Token: ch.Spec.Token,
}
acmeChal, err := cl.AcceptChallenge(ctx, acmeChal)
if acmeChal != nil {
ch.Status.State = cmapi.State(acmeChal.Status)
}
if err != nil {
glog.Infof("%s: Error accepting challenge: %v", ch.Name, err)
ch.Status.Reason = fmt.Sprintf("Error accepting challenge: %v", err)
return err
}
glog.Infof("Waiting for authorization for domain %q", ch.Spec.DNSName)
authorization, err := cl.WaitAuthorization(ctx, ch.Spec.AuthzURL)
if err != nil {
authErr, ok := err.(acmeapi.AuthorizationError)
if !ok {
glog.Infof("%s: Unexpected error waiting for authorization: %v", ch.Name, err)
return err
}
ch.Status.State = cmapi.State(authErr.Authorization.Status)
ch.Status.Reason = fmt.Sprintf("Error accepting authorization: %v", authErr)
c.Recorder.Eventf(ch, corev1.EventTypeWarning, "Failed", "Accepting challenge authorization failed: %v", authErr)
// return nil here, as accepting the challenge did not error, the challenge
// simply failed
return nil
}
ch.Status.State = cmapi.State(authorization.Status)
ch.Status.Reason = "Successfully authorized domain"
c.Context.Recorder.Eventf(ch, corev1.EventTypeNormal, reasonDomainVerified, "Domain %q verified with %q validation", ch.Spec.DNSName, ch.Spec.Type)
return nil
}
func (c *Controller) solverFor(challengeType string) (solver, error) {
switch challengeType {
case "http-01":
return c.httpSolver, nil
case "dns-01":
return c.dnsSolver, nil
}
return nil, fmt.Errorf("no solver for %q implemented", challengeType)
}