Add ACME Order & Challenge controllers
Signed-off-by: James Munnelly <james@munnelly.eu>
This commit is contained in:
parent
65487e1d2b
commit
967a48e1dc
@ -25,6 +25,8 @@ import (
|
||||
|
||||
"github.com/jetstack/cert-manager/pkg/util"
|
||||
|
||||
challengescontroller "github.com/jetstack/cert-manager/pkg/controller/acmechallenges"
|
||||
orderscontroller "github.com/jetstack/cert-manager/pkg/controller/acmeorders"
|
||||
certificatescontroller "github.com/jetstack/cert-manager/pkg/controller/certificates"
|
||||
clusterissuerscontroller "github.com/jetstack/cert-manager/pkg/controller/clusterissuers"
|
||||
ingressshimcontroller "github.com/jetstack/cert-manager/pkg/controller/ingress-shim"
|
||||
@ -95,6 +97,8 @@ var (
|
||||
clusterissuerscontroller.ControllerName,
|
||||
certificatescontroller.ControllerName,
|
||||
ingressshimcontroller.ControllerName,
|
||||
orderscontroller.ControllerName,
|
||||
challengescontroller.ControllerName,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -27,6 +27,8 @@ import (
|
||||
|
||||
"github.com/jetstack/cert-manager/cmd/controller/app"
|
||||
"github.com/jetstack/cert-manager/cmd/controller/app/options"
|
||||
_ "github.com/jetstack/cert-manager/pkg/controller/acmechallenges"
|
||||
_ "github.com/jetstack/cert-manager/pkg/controller/acmeorders"
|
||||
_ "github.com/jetstack/cert-manager/pkg/controller/certificates"
|
||||
_ "github.com/jetstack/cert-manager/pkg/controller/clusterissuers"
|
||||
_ "github.com/jetstack/cert-manager/pkg/controller/ingress-shim"
|
||||
|
||||
3
pkg/controller/acmechallenges/checks.go
Normal file
3
pkg/controller/acmechallenges/checks.go
Normal file
@ -0,0 +1,3 @@
|
||||
package acmechallenges
|
||||
|
||||
// no checks for the acme orders controller yet
|
||||
184
pkg/controller/acmechallenges/controller.go
Normal file
184
pkg/controller/acmechallenges/controller.go
Normal file
@ -0,0 +1,184 @@
|
||||
package acmechallenges
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
corelisters "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
|
||||
cmlisters "github.com/jetstack/cert-manager/pkg/client/listers/certmanager/v1alpha1"
|
||||
controllerpkg "github.com/jetstack/cert-manager/pkg/controller"
|
||||
"github.com/jetstack/cert-manager/pkg/issuer/acme/dns"
|
||||
"github.com/jetstack/cert-manager/pkg/issuer/acme/http"
|
||||
"github.com/jetstack/cert-manager/pkg/util"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
controllerpkg.Context
|
||||
|
||||
helper *controllerpkg.Helper
|
||||
|
||||
// To allow injection for testing.
|
||||
syncHandler func(ctx context.Context, key string) error
|
||||
|
||||
challengeLister cmlisters.ChallengeLister
|
||||
issuerLister cmlisters.IssuerLister
|
||||
clusterIssuerLister cmlisters.ClusterIssuerLister
|
||||
secretLister corelisters.SecretLister
|
||||
|
||||
// ACME challenge solvers are instantiated once at the time of controller
|
||||
// construction.
|
||||
// This also allows for easy mocking of the different challenge mechanisms.
|
||||
dnsSolver solver
|
||||
httpSolver solver
|
||||
|
||||
watchedInformers []cache.InformerSynced
|
||||
queue workqueue.RateLimitingInterface
|
||||
}
|
||||
|
||||
func New(ctx *controllerpkg.Context) *Controller {
|
||||
ctrl := &Controller{Context: *ctx}
|
||||
ctrl.syncHandler = ctrl.processNextWorkItem
|
||||
|
||||
// exponentially back-off self checks, with a base of 2s and max wait of 20s
|
||||
ctrl.queue = workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*2, time.Second*20), "challenges")
|
||||
|
||||
challengeInformer := ctrl.SharedInformerFactory.Certmanager().V1alpha1().Challenges()
|
||||
challengeInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: ctrl.queue})
|
||||
ctrl.watchedInformers = append(ctrl.watchedInformers, challengeInformer.Informer().HasSynced)
|
||||
ctrl.challengeLister = challengeInformer.Lister()
|
||||
|
||||
// issuerInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: ctrl.queue})
|
||||
issuerInformer := ctrl.SharedInformerFactory.Certmanager().V1alpha1().Issuers()
|
||||
ctrl.watchedInformers = append(ctrl.watchedInformers, issuerInformer.Informer().HasSynced)
|
||||
ctrl.issuerLister = issuerInformer.Lister()
|
||||
|
||||
// clusterIssuerInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: ctrl.queue})
|
||||
clusterIssuerInformer := ctrl.SharedInformerFactory.Certmanager().V1alpha1().ClusterIssuers()
|
||||
ctrl.watchedInformers = append(ctrl.watchedInformers, clusterIssuerInformer.Informer().HasSynced)
|
||||
ctrl.clusterIssuerLister = clusterIssuerInformer.Lister()
|
||||
|
||||
secretInformer := ctrl.KubeSharedInformerFactory.Core().V1().Secrets()
|
||||
ctrl.watchedInformers = append(ctrl.watchedInformers, secretInformer.Informer().HasSynced)
|
||||
ctrl.secretLister = secretInformer.Lister()
|
||||
|
||||
// instantiate listers used by the http01 solver
|
||||
podInformer := ctrl.KubeSharedInformerFactory.Core().V1().Pods()
|
||||
serviceInformer := ctrl.KubeSharedInformerFactory.Core().V1().Services()
|
||||
ingressInformer := ctrl.KubeSharedInformerFactory.Extensions().V1beta1().Ingresses()
|
||||
ctrl.watchedInformers = append(ctrl.watchedInformers, podInformer.Informer().HasSynced)
|
||||
ctrl.watchedInformers = append(ctrl.watchedInformers, serviceInformer.Informer().HasSynced)
|
||||
ctrl.watchedInformers = append(ctrl.watchedInformers, ingressInformer.Informer().HasSynced)
|
||||
|
||||
ctrl.helper = controllerpkg.NewHelper(ctrl.issuerLister, ctrl.clusterIssuerLister)
|
||||
|
||||
ctrl.httpSolver = http.NewSolver(ctx)
|
||||
ctrl.dnsSolver = dns.NewSolver(ctx)
|
||||
|
||||
return ctrl
|
||||
}
|
||||
|
||||
func (c *Controller) Run(workers int, stopCh <-chan struct{}) error {
|
||||
glog.V(4).Infof("Starting %s control loop", ControllerName)
|
||||
// wait for all the informer caches we depend on are synced
|
||||
if !cache.WaitForCacheSync(stopCh, c.watchedInformers...) {
|
||||
// c.challengeInformerSynced) {
|
||||
return fmt.Errorf("error waiting for informer caches to sync")
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < workers; i++ {
|
||||
wg.Add(1)
|
||||
// TODO (@munnerz): make time.Second duration configurable
|
||||
go wait.Until(func() {
|
||||
defer wg.Done()
|
||||
c.worker(stopCh)
|
||||
},
|
||||
time.Second, stopCh)
|
||||
}
|
||||
<-stopCh
|
||||
glog.V(4).Infof("Shutting down queue as workqueue signaled shutdown")
|
||||
c.queue.ShutDown()
|
||||
glog.V(4).Infof("Waiting for workers to exit...")
|
||||
wg.Wait()
|
||||
glog.V(4).Infof("Workers exited.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) worker(stopCh <-chan struct{}) {
|
||||
glog.V(4).Infof("Starting %q worker", ControllerName)
|
||||
for {
|
||||
obj, shutdown := c.queue.Get()
|
||||
if shutdown {
|
||||
break
|
||||
}
|
||||
|
||||
var key string
|
||||
err := func(obj interface{}) error {
|
||||
defer c.queue.Done(obj)
|
||||
var ok bool
|
||||
if key, ok = obj.(string); !ok {
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx = util.ContextWithStopCh(ctx, stopCh)
|
||||
glog.Infof("%s controller: syncing item '%s'", ControllerName, key)
|
||||
if err := c.syncHandler(ctx, key); err != nil {
|
||||
return err
|
||||
}
|
||||
c.queue.Forget(obj)
|
||||
return nil
|
||||
}(obj)
|
||||
|
||||
if err != nil {
|
||||
glog.Errorf("%s controller: Re-queuing item %q due to error processing: %s", ControllerName, key, err.Error())
|
||||
c.queue.AddRateLimited(obj)
|
||||
continue
|
||||
}
|
||||
|
||||
glog.Infof("%s controller: Finished processing work item %q", ControllerName, key)
|
||||
}
|
||||
glog.V(4).Infof("Exiting %q worker loop", ControllerName)
|
||||
}
|
||||
|
||||
func (c *Controller) processNextWorkItem(ctx context.Context, key string) error {
|
||||
namespace, name, err := cache.SplitMetaNamespaceKey(key)
|
||||
if err != nil {
|
||||
runtime.HandleError(fmt.Errorf("invalid resource key: %s", key))
|
||||
return nil
|
||||
}
|
||||
|
||||
ch, err := c.challengeLister.Challenges(namespace).Get(name)
|
||||
|
||||
if err != nil {
|
||||
if k8sErrors.IsNotFound(err) {
|
||||
runtime.HandleError(fmt.Errorf("ch '%s' in work queue no longer exists", key))
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Sync(ctx, ch)
|
||||
}
|
||||
|
||||
var keyFunc = controllerpkg.KeyFunc
|
||||
|
||||
const (
|
||||
ControllerName = "challenges"
|
||||
)
|
||||
|
||||
func init() {
|
||||
controllerpkg.Register(ControllerName, func(ctx *controllerpkg.Context) controllerpkg.Interface {
|
||||
return New(ctx).Run
|
||||
})
|
||||
}
|
||||
224
pkg/controller/acmechallenges/sync.go
Normal file
224
pkg/controller/acmechallenges/sync.go
Normal file
@ -0,0 +1,224 @@
|
||||
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()
|
||||
|
||||
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) || ch.Status.State == cmapi.Valid {
|
||||
return nil
|
||||
}
|
||||
|
||||
acmeHelper := &acme.Helper{
|
||||
SecretLister: c.secretLister,
|
||||
ClusterResourceNamespace: c.Context.ClusterResourceNamespace,
|
||||
}
|
||||
|
||||
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 := acmeHelper.ClientForIssuer(genericIssuer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ch.Status.State == "" {
|
||||
err := c.syncChallengeStatus(ctx, cl, ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// we reperform the check from above now that we have updated the status
|
||||
// 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) || ch.Status.State == cmapi.Valid {
|
||||
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
|
||||
}
|
||||
|
||||
ok, err := solver.Check(ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
ch.Status.Reason = fmt.Sprintf("Self check failed - %s challenge still propagating. Will retry after applying back-off.", 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
|
||||
}
|
||||
|
||||
// presentChallenge will process a challenge by talking to the acme server and
|
||||
// obtaining up to date status information.
|
||||
// If the challenge is still in a pending state, it will first check propagation
|
||||
// status of a challenge from previous attempt, and if missing it will 'present' the
|
||||
// new challenge using the appropriate solver.
|
||||
// If the check fails, an error will be returned.
|
||||
// Otherwise, it will return nil.
|
||||
func (c *Controller) presentChallenge(ctx context.Context, issuer cmapi.GenericIssuer, ch *cmapi.Challenge) error {
|
||||
solver, err := c.solverFor(ch.Spec.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: make sure that solver.Present is noop if challenge
|
||||
// is already present and all we do is waiting for propagation,
|
||||
// otherwise it is spamming with errors which are not really erros
|
||||
// as we are just waiting for propagation
|
||||
err = solver.Present(ctx, issuer, ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ch.Status.Presented = true
|
||||
|
||||
// We return an error here instead of nil, as the only way for 'presentChallenge'
|
||||
// to return without error is if the self check passes, which we check above.
|
||||
return nil
|
||||
}
|
||||
|
||||
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 err != nil {
|
||||
ch.Status.State = cmapi.State(acmeChal.Status)
|
||||
if acmeErr, ok := err.(*acmeapi.Error); ok {
|
||||
ch.Status.Reason = fmt.Sprintf("Error accepting challenge: %v", acmeErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
glog.Infof("Waiting for authorization for domain %q", ch.Spec.DNSName)
|
||||
authorization, err := cl.WaitAuthorization(ctx, ch.Spec.AuthzURL)
|
||||
if err != nil {
|
||||
ch.Status.State = cmapi.State(authorization.Status)
|
||||
if acmeErr, ok := err.(*acmeapi.Error); ok {
|
||||
ch.Status.Reason = fmt.Sprintf("Error accepting challenge: %v", acmeErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
ch.Status.State = cmapi.State(authorization.Status)
|
||||
|
||||
if authorization.Status != acmeapi.StatusValid {
|
||||
ch.Status.Reason = fmt.Sprintf("Authorization status is %q and not 'valid'", authorization.Status)
|
||||
return fmt.Errorf("expected acme domain authorization status for %q to be valid, but it is %q", authorization.Identifier.Value, 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)
|
||||
}
|
||||
3
pkg/controller/acmeorders/checks.go
Normal file
3
pkg/controller/acmeorders/checks.go
Normal file
@ -0,0 +1,3 @@
|
||||
package acmeorders
|
||||
|
||||
// no checks for the acme orders controller yet
|
||||
205
pkg/controller/acmeorders/controller.go
Normal file
205
pkg/controller/acmeorders/controller.go
Normal file
@ -0,0 +1,205 @@
|
||||
package acmeorders
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
corelisters "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
|
||||
cmlisters "github.com/jetstack/cert-manager/pkg/client/listers/certmanager/v1alpha1"
|
||||
controllerpkg "github.com/jetstack/cert-manager/pkg/controller"
|
||||
"github.com/jetstack/cert-manager/pkg/util"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
controllerpkg.Context
|
||||
|
||||
helper *controllerpkg.Helper
|
||||
|
||||
// To allow injection for testing.
|
||||
syncHandler func(ctx context.Context, key string) error
|
||||
|
||||
orderLister cmlisters.OrderLister
|
||||
challengeLister cmlisters.ChallengeLister
|
||||
issuerLister cmlisters.IssuerLister
|
||||
clusterIssuerLister cmlisters.ClusterIssuerLister
|
||||
secretLister corelisters.SecretLister
|
||||
|
||||
watchedInformers []cache.InformerSynced
|
||||
queue workqueue.RateLimitingInterface
|
||||
}
|
||||
|
||||
func New(ctx *controllerpkg.Context) *Controller {
|
||||
ctrl := &Controller{Context: *ctx}
|
||||
ctrl.syncHandler = ctrl.processNextWorkItem
|
||||
|
||||
ctrl.queue = workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*2, time.Minute*1), "orders")
|
||||
|
||||
orderInformer := ctrl.SharedInformerFactory.Certmanager().V1alpha1().Orders()
|
||||
orderInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: ctrl.queue})
|
||||
ctrl.watchedInformers = append(ctrl.watchedInformers, orderInformer.Informer().HasSynced)
|
||||
ctrl.orderLister = orderInformer.Lister()
|
||||
|
||||
// issuerInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: ctrl.queue})
|
||||
issuerInformer := ctrl.SharedInformerFactory.Certmanager().V1alpha1().Issuers()
|
||||
ctrl.watchedInformers = append(ctrl.watchedInformers, issuerInformer.Informer().HasSynced)
|
||||
ctrl.issuerLister = issuerInformer.Lister()
|
||||
|
||||
// clusterIssuerInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: ctrl.queue})
|
||||
clusterIssuerInformer := ctrl.SharedInformerFactory.Certmanager().V1alpha1().ClusterIssuers()
|
||||
ctrl.watchedInformers = append(ctrl.watchedInformers, clusterIssuerInformer.Informer().HasSynced)
|
||||
ctrl.clusterIssuerLister = clusterIssuerInformer.Lister()
|
||||
|
||||
challengeInformer := ctrl.SharedInformerFactory.Certmanager().V1alpha1().Challenges()
|
||||
challengeInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: ctrl.handleOwnedResource})
|
||||
ctrl.watchedInformers = append(ctrl.watchedInformers, challengeInformer.Informer().HasSynced)
|
||||
ctrl.challengeLister = challengeInformer.Lister()
|
||||
|
||||
secretInformer := ctrl.KubeSharedInformerFactory.Core().V1().Secrets()
|
||||
ctrl.watchedInformers = append(ctrl.watchedInformers, secretInformer.Informer().HasSynced)
|
||||
ctrl.secretLister = secretInformer.Lister()
|
||||
|
||||
ctrl.helper = controllerpkg.NewHelper(ctrl.issuerLister, ctrl.clusterIssuerLister)
|
||||
|
||||
return ctrl
|
||||
}
|
||||
|
||||
func (c *Controller) handleOwnedResource(obj interface{}) {
|
||||
metaobj, ok := obj.(metav1.Object)
|
||||
if !ok {
|
||||
glog.Errorf("item passed to handleOwnedResource does not implement ObjectMetaAccessor")
|
||||
return
|
||||
}
|
||||
|
||||
ownerRefs := metaobj.GetOwnerReferences()
|
||||
for _, ref := range ownerRefs {
|
||||
// Parse the Group out of the OwnerReference to compare it to what was parsed out of the requested OwnerType
|
||||
refGV, err := schema.ParseGroupVersion(ref.APIVersion)
|
||||
if err != nil {
|
||||
glog.Errorf("Could not parse OwnerReference GroupVersion: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if refGV.Group == orderGvk.Group && ref.Kind == orderGvk.Kind {
|
||||
// TODO: how to handle namespace of owner references?
|
||||
order, err := c.orderLister.Orders(metaobj.GetNamespace()).Get(ref.Name)
|
||||
if err != nil {
|
||||
glog.Errorf("Error getting Order %q referenced by resource %q", ref.Name, metaobj.GetName())
|
||||
continue
|
||||
}
|
||||
objKey, err := keyFunc(order)
|
||||
if err != nil {
|
||||
runtime.HandleError(err)
|
||||
continue
|
||||
}
|
||||
c.queue.Add(objKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) Run(workers int, stopCh <-chan struct{}) error {
|
||||
glog.V(4).Infof("Starting %s control loop", ControllerName)
|
||||
// wait for all the informer caches we depend on are synced
|
||||
if !cache.WaitForCacheSync(stopCh, c.watchedInformers...) {
|
||||
// c.challengeInformerSynced) {
|
||||
return fmt.Errorf("error waiting for informer caches to sync")
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < workers; i++ {
|
||||
wg.Add(1)
|
||||
// TODO (@munnerz): make time.Second duration configurable
|
||||
go wait.Until(func() {
|
||||
defer wg.Done()
|
||||
c.worker(stopCh)
|
||||
},
|
||||
time.Second, stopCh)
|
||||
}
|
||||
<-stopCh
|
||||
glog.V(4).Infof("Shutting down queue as workqueue signaled shutdown")
|
||||
c.queue.ShutDown()
|
||||
glog.V(4).Infof("Waiting for workers to exit...")
|
||||
wg.Wait()
|
||||
glog.V(4).Infof("Workers exited.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) worker(stopCh <-chan struct{}) {
|
||||
glog.V(4).Infof("Starting %q worker", ControllerName)
|
||||
for {
|
||||
obj, shutdown := c.queue.Get()
|
||||
if shutdown {
|
||||
break
|
||||
}
|
||||
|
||||
var key string
|
||||
err := func(obj interface{}) error {
|
||||
defer c.queue.Done(obj)
|
||||
var ok bool
|
||||
if key, ok = obj.(string); !ok {
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx = util.ContextWithStopCh(ctx, stopCh)
|
||||
glog.Infof("%s controller: syncing item '%s'", ControllerName, key)
|
||||
if err := c.syncHandler(ctx, key); err != nil {
|
||||
return err
|
||||
}
|
||||
c.queue.Forget(obj)
|
||||
return nil
|
||||
}(obj)
|
||||
|
||||
if err != nil {
|
||||
glog.Errorf("%s controller: Re-queuing item %q due to error processing: %s", ControllerName, key, err.Error())
|
||||
c.queue.AddRateLimited(obj)
|
||||
continue
|
||||
}
|
||||
|
||||
glog.Infof("%s controller: Finished processing work item %q", ControllerName, key)
|
||||
}
|
||||
glog.V(4).Infof("Exiting %q worker loop", ControllerName)
|
||||
}
|
||||
|
||||
func (c *Controller) processNextWorkItem(ctx context.Context, key string) error {
|
||||
namespace, name, err := cache.SplitMetaNamespaceKey(key)
|
||||
if err != nil {
|
||||
runtime.HandleError(fmt.Errorf("invalid resource key: %s", key))
|
||||
return nil
|
||||
}
|
||||
|
||||
order, err := c.orderLister.Orders(namespace).Get(name)
|
||||
|
||||
if err != nil {
|
||||
if k8sErrors.IsNotFound(err) {
|
||||
runtime.HandleError(fmt.Errorf("order '%s' in work queue no longer exists", key))
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Sync(ctx, order)
|
||||
}
|
||||
|
||||
var keyFunc = controllerpkg.KeyFunc
|
||||
|
||||
const (
|
||||
ControllerName = "orders"
|
||||
)
|
||||
|
||||
func init() {
|
||||
controllerpkg.Register(ControllerName, func(ctx *controllerpkg.Context) controllerpkg.Interface {
|
||||
return New(ctx).Run
|
||||
})
|
||||
}
|
||||
452
pkg/controller/acmeorders/sync.go
Normal file
452
pkg/controller/acmeorders/sync.go
Normal file
@ -0,0 +1,452 @@
|
||||
package acmeorders
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
orderGvk = cmapi.SchemeGroupVersion.WithKind("Order")
|
||||
)
|
||||
|
||||
// Sync will process this ACME Order.
|
||||
// It is the core control function for ACME Orders, and handles:
|
||||
// - creating orders
|
||||
// - deciding/validated configured challenge mechanisms
|
||||
// - create a Challenge resource in order to fulfill required validations
|
||||
// - waiting for Challenge resources to enter the 'ready' state
|
||||
func (c *Controller) Sync(ctx context.Context, o *cmapi.Order) (err error) {
|
||||
oldOrder := o
|
||||
o = o.DeepCopy()
|
||||
|
||||
defer func() {
|
||||
// TODO: replace with more efficient comparison
|
||||
if reflect.DeepEqual(oldOrder.Status, o.Status) {
|
||||
return
|
||||
}
|
||||
_, updateErr := c.CMClient.CertmanagerV1alpha1().Orders(o.Namespace).Update(o)
|
||||
if err != nil {
|
||||
err = utilerrors.NewAggregate([]error{err, updateErr})
|
||||
}
|
||||
}()
|
||||
|
||||
acmeHelper := &acme.Helper{
|
||||
SecretLister: c.secretLister,
|
||||
ClusterResourceNamespace: c.Context.ClusterResourceNamespace,
|
||||
}
|
||||
|
||||
genericIssuer, err := c.helper.GetGenericIssuer(o.Spec.IssuerRef, o.Namespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading (cluster)issuer %q: %v", o.Spec.IssuerRef.Name, err)
|
||||
}
|
||||
|
||||
cl, err := acmeHelper.ClientForIssuer(genericIssuer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.Status.URL == "" {
|
||||
err := c.createOrder(ctx, cl, genericIssuer, o)
|
||||
// TODO: check for error types (perm or transient?)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// if an order is in a final state, we bail out early as there is nothing
|
||||
// left for us to do here.
|
||||
if acme.IsFinalState(o.Status.State) {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch o.Status.State {
|
||||
|
||||
// if the status field is not set, we should check the Order with the ACME
|
||||
// server to try and populate it.
|
||||
// If this is not possible - what should we do? (???)
|
||||
case cmapi.Unknown:
|
||||
err := c.syncOrderStatus(ctx, cl, o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: we should do something more intelligent than just returning an
|
||||
// error here.
|
||||
return fmt.Errorf("updated unknown order state. Retrying processing after applying back-off")
|
||||
|
||||
// if the current state is 'valid', we should keep polling the ACME server
|
||||
// until the Order automatically progresses to the 'ready' state.
|
||||
// This *should* happen automatically, after **some period of time**.
|
||||
case cmapi.Valid:
|
||||
waitTimeout := time.Second * 60
|
||||
// wait up to 60s for the order to enter 'ready' state
|
||||
ctx, cancel := context.WithTimeout(ctx, waitTimeout)
|
||||
defer cancel()
|
||||
|
||||
existingState := o.Status.State
|
||||
// wait for a state change (i.e. transitioning to a 'ready' state)
|
||||
newState, err := c.pollForStateChange(ctx, cl, o, time.Second*5)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if the state has not changed, we return an error so the order can be
|
||||
// re-queued.
|
||||
if existingState == newState {
|
||||
// TODO: should we mark the order as failed if the state doesn't transition?
|
||||
// For now, we will return an error which will cause the Order to be
|
||||
// requeued after a back-off has been applied.
|
||||
return fmt.Errorf("expected order to transition from %q to 'ready' state, but it did not after %s", existingState, waitTimeout)
|
||||
}
|
||||
|
||||
// if the state has changed, but is not in a 'ready' state, then we return
|
||||
// an error here.
|
||||
// When the Sync function gets called again, the appropriate action will
|
||||
// be taken if the order is now in a failed 'final' state for some reason.
|
||||
if newState != cmapi.Ready {
|
||||
return fmt.Errorf("expected order to transition to the %q state, but it is %q", cmapi.Ready, newState)
|
||||
}
|
||||
|
||||
if acme.IsFinalState(newState) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// this *should* be unreachable, because an order cannot transition from 'valid'
|
||||
// to another non-final state, and if it does then it should be caught by
|
||||
// the clauses above
|
||||
return fmt.Errorf("unexpected error: order state is %q - this case should not occur, and is likely a bug", newState)
|
||||
|
||||
// if the order is still pending or processing, we should continue to check
|
||||
// the state of all Challenge resources (or create challenge resources)
|
||||
case cmapi.Pending, cmapi.Processing:
|
||||
// continue
|
||||
|
||||
// this is the catch-all base case for order states that we do not recognise
|
||||
default:
|
||||
return fmt.Errorf("unknown order state %q", o.Status.State)
|
||||
}
|
||||
|
||||
// create a selector that we can use to find all existing Challenges for the order
|
||||
sel, err := challengeSelectorForOrder(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get the list of exising challenges for this order
|
||||
existingChallenges, err := c.challengeLister.Challenges(o.Namespace).List(sel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var specsToCreate []cmapi.ChallengeSpec
|
||||
for _, s := range o.Status.Challenges {
|
||||
create := true
|
||||
for _, ch := range existingChallenges {
|
||||
if s.DNSName == ch.Spec.DNSName {
|
||||
create = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !create {
|
||||
break
|
||||
}
|
||||
|
||||
specsToCreate = append(specsToCreate, s)
|
||||
}
|
||||
|
||||
glog.Infof("Need to create %d challenges", len(specsToCreate))
|
||||
|
||||
var errs []error
|
||||
for _, spec := range specsToCreate {
|
||||
ch, err := buildChallenge(o, spec)
|
||||
if err != nil {
|
||||
// TODO: check if this is a perma-fail
|
||||
return err
|
||||
}
|
||||
|
||||
ch, err = c.CMClient.CertmanagerV1alpha1().Challenges(o.Namespace).Create(ch)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
existingChallenges = append(existingChallenges, ch)
|
||||
}
|
||||
|
||||
err = utilerrors.NewAggregate(errs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error ensuring Challenge resources for Order: %v", err)
|
||||
}
|
||||
|
||||
// if all
|
||||
recheckOrderStatus := true
|
||||
anyChallengesFailed := false
|
||||
for _, ch := range existingChallenges {
|
||||
switch ch.Status.State {
|
||||
case cmapi.Pending, cmapi.Processing:
|
||||
recheckOrderStatus = false
|
||||
case cmapi.Failed, cmapi.Expired:
|
||||
anyChallengesFailed = true
|
||||
}
|
||||
}
|
||||
|
||||
// if at least 1 order is not valid, AND no orders have failed, we should
|
||||
// just return early and not query the ACME API.
|
||||
if !recheckOrderStatus && !anyChallengesFailed {
|
||||
glog.Infof("Waiting for all challenges for order %q to enter 'ready' state", o.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// otherwise, sync the order state with the ACME API.
|
||||
err = c.syncOrderStatus(ctx, cl, o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
orderNameLabelKey = "acme.cert-manager.io/order-name"
|
||||
)
|
||||
|
||||
func (c *Controller) createOrder(ctx context.Context, cl acmecl.Interface, issuer cmapi.GenericIssuer, o *cmapi.Order) error {
|
||||
if o.Status.URL != "" {
|
||||
return fmt.Errorf("refusing to recreate a new order for Order %q. Please create a new Order resource to initiate a new order", o.Name)
|
||||
}
|
||||
|
||||
// create a new order with the acme server
|
||||
orderTemplate := acmeapi.NewOrder(o.Spec.DNSNames...)
|
||||
acmeOrder, err := cl.CreateOrder(ctx, orderTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating new order: %v", err)
|
||||
}
|
||||
|
||||
setOrderStatus(&o.Status, acmeOrder)
|
||||
|
||||
chals := make([]cmapi.ChallengeSpec, len(acmeOrder.Authorizations))
|
||||
// we only set the status.challenges field when we first create the order,
|
||||
// because we only create one order per Order resource.
|
||||
for i, authzURL := range acmeOrder.Authorizations {
|
||||
authz, err := cl.GetAuthorization(ctx, authzURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cs, err := c.challengeSpecForAuthorization(ctx, cl, issuer, o, authz)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error constructing Challenge resource for Authorization: %v", err)
|
||||
}
|
||||
|
||||
chals[i] = *cs
|
||||
}
|
||||
o.Status.Challenges = chals
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) challengeSpecForAuthorization(ctx context.Context, cl acmecl.Interface, issuer cmapi.GenericIssuer, o *cmapi.Order, authz *acmeapi.Authorization) (*cmapi.ChallengeSpec, error) {
|
||||
cfg, err := solverConfigurationForAuthorization(o.Spec.Config, authz)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
acmeSpec := issuer.GetSpec().ACME
|
||||
if acmeSpec == nil {
|
||||
return nil, fmt.Errorf("issuer %q is not configured as an ACME Issuer. Cannot be used for creating ACME orders", issuer.GetObjectMeta().Name)
|
||||
}
|
||||
|
||||
var challenge *acmeapi.Challenge
|
||||
for _, ch := range authz.Challenges {
|
||||
switch {
|
||||
case ch.Type == "http-01" && cfg.HTTP01 != nil && acmeSpec.HTTP01 != nil:
|
||||
challenge = ch
|
||||
case ch.Type == "dns-01" && cfg.DNS01 != nil && acmeSpec.DNS01 != nil:
|
||||
challenge = ch
|
||||
}
|
||||
}
|
||||
|
||||
domain := authz.Identifier.Value
|
||||
if challenge == nil {
|
||||
return nil, fmt.Errorf("ACME server does not allow selected challenge type or no provider is configured for domain %q", domain)
|
||||
}
|
||||
|
||||
key, err := keyForChallenge(cl, challenge)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cmapi.ChallengeSpec{
|
||||
AuthzURL: authz.URL,
|
||||
Type: challenge.Type,
|
||||
URL: challenge.URL,
|
||||
DNSName: domain,
|
||||
Token: challenge.Token,
|
||||
Key: key,
|
||||
Config: *cfg,
|
||||
Wildcard: authz.Wildcard,
|
||||
IssuerRef: o.Spec.IssuerRef,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func keyForChallenge(cl acmecl.Interface, challenge *acmeapi.Challenge) (string, error) {
|
||||
var err error
|
||||
switch challenge.Type {
|
||||
case "http-01":
|
||||
return cl.HTTP01ChallengeResponse(challenge.Token)
|
||||
case "dns-01":
|
||||
return cl.DNS01ChallengeRecord(challenge.Token)
|
||||
default:
|
||||
err = fmt.Errorf("unsupported challenge type %s", challenge.Type)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
func solverConfigurationForAuthorization(cfgs []cmapi.DomainSolverConfig, authz *acmeapi.Authorization) (*cmapi.SolverConfig, error) {
|
||||
domainToFind := authz.Identifier.Value
|
||||
if authz.Wildcard {
|
||||
domainToFind = "*." + domainToFind
|
||||
}
|
||||
for _, d := range cfgs {
|
||||
for _, dom := range d.Domains {
|
||||
if dom != domainToFind {
|
||||
continue
|
||||
}
|
||||
return &d.SolverConfig, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("solver configuration for domain %q not found. Ensure you have configured a challenge mechanism using the certificate.spec.acme.config field", domainToFind)
|
||||
}
|
||||
|
||||
// syncOrderStatus will communicate with the ACME server to retrieve the current
|
||||
// state of the Order. It will then update the Order's status block with the new
|
||||
// state of the order.
|
||||
func (c *Controller) syncOrderStatus(ctx context.Context, cl acmecl.Interface, o *cmapi.Order) error {
|
||||
if o.Status.URL == "" {
|
||||
return fmt.Errorf("order URL is blank - order has not been created yet")
|
||||
}
|
||||
|
||||
acmeOrder, err := cl.GetOrder(ctx, o.Status.URL)
|
||||
if err != nil {
|
||||
// TODO: handle 404 acme responses and mark the order as failed
|
||||
return err
|
||||
}
|
||||
|
||||
setOrderStatus(&o.Status, acmeOrder)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildChallenge(o *cmapi.Order, chalSpec cmapi.ChallengeSpec) (*cmapi.Challenge, error) {
|
||||
// TODO: select challenge to use and set these fields appropriately
|
||||
ch := &cmapi.Challenge{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: o.Name + "-",
|
||||
Labels: challengeLabelsForOrder(o),
|
||||
OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(o, orderGvk)},
|
||||
},
|
||||
Spec: chalSpec,
|
||||
}
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
// setOrderStatus will populate the given OrderStatus struct with the details from
|
||||
// the provided ACME Order.
|
||||
func setOrderStatus(o *cmapi.OrderStatus, acmeOrder *acmeapi.Order) {
|
||||
// TODO: should we validate the State returned by the ACME server here?
|
||||
cmState := cmapi.State(acmeOrder.Status)
|
||||
setOrderState(o, cmState)
|
||||
|
||||
o.URL = acmeOrder.URL
|
||||
o.FinalizeURL = acmeOrder.FinalizeURL
|
||||
}
|
||||
|
||||
func challengeLabelsForOrder(o *cmapi.Order) map[string]string {
|
||||
return map[string]string{
|
||||
orderNameLabelKey: o.Name,
|
||||
}
|
||||
}
|
||||
|
||||
// challengeSelectorForOrder will construct a labels.Selector that can be used to
|
||||
// find Challenges associated with the given Order.
|
||||
func challengeSelectorForOrder(o *cmapi.Order) (labels.Selector, error) {
|
||||
lbls := challengeLabelsForOrder(o)
|
||||
var reqs []labels.Requirement
|
||||
for k, v := range lbls {
|
||||
req, err := labels.NewRequirement(k, selection.Equals, []string{v})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqs = append(reqs, *req)
|
||||
}
|
||||
return labels.NewSelector().Add(reqs...), nil
|
||||
}
|
||||
|
||||
// pollForStateChange will poll the ACME API every pollInterval for a change in
|
||||
// the Orders state.
|
||||
// This is primarily used to wait for the Order to transition from a 'valid' to
|
||||
// a 'ready' state.
|
||||
// If the state does not change before the context deadline is reached, the old
|
||||
// state will be returned and **no error** will be returned. It is up to the caller
|
||||
// to detect and handle this case appropriately.
|
||||
func (c *Controller) pollForStateChange(ctx context.Context, cl acmecl.Interface, o *cmapi.Order, pollInterval time.Duration) (cmapi.State, error) {
|
||||
oldState := o.Status.State
|
||||
for {
|
||||
// we define err here outside of the go func, so we can detect errors
|
||||
// caused by attempting to sync the order state without an extra struct
|
||||
// that contains (cmapi.State, error).
|
||||
// This should be okay (at least for now), because there will never be two
|
||||
// go funcs that are running at once which may access err at the same time.
|
||||
// If this assumption is wrong however, a race may occur, so we may want
|
||||
// to consider create a 'wrapper struct' in future.
|
||||
var err error
|
||||
select {
|
||||
case newState := <-func() <-chan cmapi.State {
|
||||
out := make(chan cmapi.State)
|
||||
go func() {
|
||||
defer close(out)
|
||||
err = c.syncOrderStatus(ctx, cl, o)
|
||||
out <- o.Status.State
|
||||
}()
|
||||
return out
|
||||
}():
|
||||
if err != nil {
|
||||
return newState, err
|
||||
}
|
||||
if newState != oldState {
|
||||
return newState, nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return oldState, fmt.Errorf("timeout whilst waiting for ACME order state to change from %q", oldState)
|
||||
}
|
||||
|
||||
// wait for pollInterval until we re-poll the ACME server for a new state
|
||||
time.Sleep(pollInterval)
|
||||
}
|
||||
}
|
||||
|
||||
// setOrderState will set the 'State' field of the given Order to 's'.
|
||||
// It will set the Orders failureTime field if the state provided is classed as
|
||||
// a failure state.
|
||||
func setOrderState(o *cmapi.OrderStatus, s cmapi.State) {
|
||||
o.State = s
|
||||
// if the order is in a failure state, we should set the `failureTime` field
|
||||
if acme.IsFailureState(o.State) {
|
||||
t := metav1.NewTime(time.Now())
|
||||
o.FailureTime = &t
|
||||
}
|
||||
}
|
||||
167
pkg/controller/acmeorders/sync_test.go
Normal file
167
pkg/controller/acmeorders/sync_test.go
Normal file
@ -0,0 +1,167 @@
|
||||
package acmeorders
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
|
||||
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
|
||||
"github.com/jetstack/cert-manager/third_party/crypto/acme"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTestNamespace = "default"
|
||||
)
|
||||
|
||||
func TestSolverConfigurationForAuthorization(t *testing.T) {
|
||||
type testT struct {
|
||||
cfg []v1alpha1.DomainSolverConfig
|
||||
authz *acme.Authorization
|
||||
expectedCfg *v1alpha1.SolverConfig
|
||||
expectedErr bool
|
||||
}
|
||||
tests := map[string]testT{
|
||||
"correctly selects normal domain": testT{
|
||||
cfg: []v1alpha1.DomainSolverConfig{
|
||||
{
|
||||
Domains: []string{"example.com"},
|
||||
SolverConfig: v1alpha1.SolverConfig{
|
||||
DNS01: &v1alpha1.DNS01SolverConfig{
|
||||
Provider: "correctdns",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
authz: &acme.Authorization{
|
||||
Identifier: acme.AuthzID{
|
||||
Value: "example.com",
|
||||
},
|
||||
},
|
||||
expectedCfg: &v1alpha1.SolverConfig{
|
||||
DNS01: &v1alpha1.DNS01SolverConfig{
|
||||
Provider: "correctdns",
|
||||
},
|
||||
},
|
||||
},
|
||||
"correctly selects normal domain with multiple domains configured": testT{
|
||||
cfg: []v1alpha1.DomainSolverConfig{
|
||||
{
|
||||
Domains: []string{"notexample.com", "example.com"},
|
||||
SolverConfig: v1alpha1.SolverConfig{
|
||||
DNS01: &v1alpha1.DNS01SolverConfig{
|
||||
Provider: "correctdns",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
authz: &acme.Authorization{
|
||||
Identifier: acme.AuthzID{
|
||||
Value: "example.com",
|
||||
},
|
||||
},
|
||||
expectedCfg: &v1alpha1.SolverConfig{
|
||||
DNS01: &v1alpha1.DNS01SolverConfig{
|
||||
Provider: "correctdns",
|
||||
},
|
||||
},
|
||||
},
|
||||
"correctly selects normal domain with multiple domains configured separately": testT{
|
||||
cfg: []v1alpha1.DomainSolverConfig{
|
||||
{
|
||||
Domains: []string{"example.com"},
|
||||
SolverConfig: v1alpha1.SolverConfig{
|
||||
DNS01: &v1alpha1.DNS01SolverConfig{
|
||||
Provider: "correctdns",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Domains: []string{"notexample.com"},
|
||||
SolverConfig: v1alpha1.SolverConfig{
|
||||
DNS01: &v1alpha1.DNS01SolverConfig{
|
||||
Provider: "incorrectdns",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
authz: &acme.Authorization{
|
||||
Identifier: acme.AuthzID{
|
||||
Value: "example.com",
|
||||
},
|
||||
},
|
||||
expectedCfg: &v1alpha1.SolverConfig{
|
||||
DNS01: &v1alpha1.DNS01SolverConfig{
|
||||
Provider: "correctdns",
|
||||
},
|
||||
},
|
||||
},
|
||||
"correctly selects configuration for wildcard domain": testT{
|
||||
cfg: []v1alpha1.DomainSolverConfig{
|
||||
{
|
||||
Domains: []string{"example.com"},
|
||||
SolverConfig: v1alpha1.SolverConfig{
|
||||
DNS01: &v1alpha1.DNS01SolverConfig{
|
||||
Provider: "incorrectdns",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Domains: []string{"*.example.com"},
|
||||
SolverConfig: v1alpha1.SolverConfig{
|
||||
DNS01: &v1alpha1.DNS01SolverConfig{
|
||||
Provider: "correctdns",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
authz: &acme.Authorization{
|
||||
Wildcard: true,
|
||||
Identifier: acme.AuthzID{
|
||||
// identifiers for wildcards do not include the *. prefix and
|
||||
// instead set the Wildcard field on the Authz object
|
||||
Value: "example.com",
|
||||
},
|
||||
},
|
||||
expectedCfg: &v1alpha1.SolverConfig{
|
||||
DNS01: &v1alpha1.DNS01SolverConfig{
|
||||
Provider: "correctdns",
|
||||
},
|
||||
},
|
||||
},
|
||||
"returns an error when configuration for the domain is not found": testT{
|
||||
cfg: []v1alpha1.DomainSolverConfig{
|
||||
{
|
||||
Domains: []string{"notexample.com"},
|
||||
SolverConfig: v1alpha1.SolverConfig{
|
||||
DNS01: &v1alpha1.DNS01SolverConfig{
|
||||
Provider: "incorrectdns",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
authz: &acme.Authorization{
|
||||
Identifier: acme.AuthzID{
|
||||
Value: "example.com",
|
||||
},
|
||||
},
|
||||
expectedErr: true,
|
||||
},
|
||||
}
|
||||
for n, test := range tests {
|
||||
t.Run(n, func(t *testing.T) {
|
||||
actualCfg, err := solverConfigurationForAuthorization(test.cfg, test.authz)
|
||||
if err != nil && !test.expectedErr {
|
||||
t.Errorf("Expected to return non-nil error, but got %v", err)
|
||||
return
|
||||
}
|
||||
if err == nil && test.expectedErr {
|
||||
t.Errorf("Expected error, but got none")
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(test.expectedCfg, actualCfg) {
|
||||
t.Errorf("Expected did not equal actual: %v", diff.ObjectDiff(test.expectedCfg, actualCfg))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user