Add acme issuer. Implement 'Setup' method. Now manages ACME accounts.

This commit is contained in:
James Munnelly 2017-07-21 15:18:39 +01:00
parent aa03460d21
commit 95cba8ab5f
15 changed files with 799 additions and 109 deletions

View File

@ -26,10 +26,10 @@ import (
rest "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
_ "github.com/jetstack/cert-manager/pkg/apis/certmanager/install"
"github.com/jetstack/cert-manager/pkg/client"
"github.com/jetstack/cert-manager/pkg/controller"
"github.com/jetstack/cert-manager/pkg/controller/certificates"
"github.com/jetstack/cert-manager/pkg/controller/ingress"
"github.com/jetstack/cert-manager/pkg/controller/issuers"
"github.com/jetstack/cert-manager/pkg/informers/externalversions"
logpkg "github.com/jetstack/cert-manager/pkg/log"
)
@ -74,23 +74,17 @@ func main() {
Logger: log,
}
ingressCtrl, err := ingress.New(ctx)
if err != nil {
log.Fatalf("error creating ingress control loop: %s", err.Error())
}
certificatesCtrl, err := certificates.New(ctx)
if err != nil {
log.Fatalf("error creating certificates control loop: %s", err.Error())
}
issuerCtrl := issuers.New(ctx)
// certificatesCtrl := certificates.New(ctx)
stopCh := make(chan struct{})
factory.Start(stopCh)
cmFactory.Start(stopCh)
ingressCtrl.Run(5, stopCh)
certificatesCtrl.Run(5, stopCh)
go issuerCtrl.Run(5, stopCh)
// go certificatesCtrl.Run(5, stopCh)
<-stopCh
}
// kubeConfig will return a rest.Config for communicating with the Kubernetes API server.

View File

@ -1,62 +0,0 @@
package controller
import (
"reflect"
"time"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
)
type Base struct {
Context Context
// TODO (@munnerz): come up with some way to swap out this queue type
Queue workqueue.RateLimitingInterface
Worker func() bool
hasSynced []cache.InformerSynced
}
func (b *Base) AddHandler(informer cache.SharedIndexInformer) {
informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: b.Queue.Add,
UpdateFunc: func(old, cur interface{}) {
if !reflect.DeepEqual(old, cur) {
b.Queue.Add(cur)
}
},
DeleteFunc: b.Queue.Add,
})
b.hasSynced = append(b.hasSynced, informer.HasSynced)
}
// Run will start this controllers run loop, with the specified number of
// worker goroutines. It will block until a message is placed onto the stopCh.
func (b *Base) Run(workers int, stopCh <-chan struct{}) {
defer b.Queue.ShutDown()
b.Context.Logger.Printf("Starting control loop")
// wait for all the informer caches we depend on are synced
if !cache.WaitForCacheSync(stopCh, b.hasSynced...) {
b.Context.Logger.Errorf("error waiting for informer caches to sync")
return
}
for i := 0; i < workers; i++ {
// TODO (@munnerz): make time.Second duration configurable
go wait.Until(b.worker, time.Second, stopCh)
}
<-stopCh
b.Context.Logger.Printf("shutting down queue as workqueue signalled shutdown")
}
func (b *Base) worker() {
b.Context.Logger.Printf("starting worker")
for b.Worker() {
b.Context.Logger.Printf("finished processing work item")
}
b.Context.Logger.Printf("exiting worker loop")
}

View File

@ -1,46 +1,46 @@
package certificates
import (
"time"
api "k8s.io/api/core/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/controller"
"github.com/juju/ratelimit"
)
type Controller struct {
*controller.Base
func New(ctx controller.Context) controller.Controller {
return controller.Controller{
Context: &ctx,
Queue: workqueue.NewRateLimitingQueue(
workqueue.NewMaxOfRateLimiter(
workqueue.NewItemExponentialFailureRateLimiter(15*time.Second, time.Minute),
// 10 qps, 100 bucket size. This is only for retry speed and its only the overall factor (not per item)
&workqueue.BucketRateLimiter{Bucket: ratelimit.NewBucketWithRate(float64(10), int64(100))},
),
),
Worker: processNextWorkItem,
Informers: []cache.SharedIndexInformer{
ctx.CertManagerInformerFactory.Certmanager().V1alpha1().Certificates().Informer(),
ctx.InformerFactory.Core().V1().Secrets().Informer(),
},
}
}
func New(ctx controller.Context) (*Controller, error) {
ctrl := &Controller{}
base := &controller.Base{
Context: ctx,
Queue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
Worker: ctrl.processNextWorkItem,
}
ctrl.Base = base
// Start watching for changes to Certificate resources
ctrl.AddHandler(ctx.CertManagerInformerFactory.Certmanager().V1alpha1().Certificates().Informer())
return ctrl, nil
}
func (c *Controller) processNextWorkItem() bool {
obj, shutdown := c.Queue.Get()
if shutdown {
return false
}
defer c.Queue.Done(obj)
switch obj.(type) {
func processNextWorkItem(ctx controller.Context, obj interface{}) error {
ctx.Logger.Printf("obj of type %T", obj)
switch v := obj.(type) {
case *v1alpha1.Certificate:
// TODO (@munnerz): lookup ingress for this certificate resource, and
// add it the the workqueue in order to ensure the ingress and
// certificate resource are in sync.
if err := sync(&ctx, v); err != nil {
return err
}
case *api.Secret:
ctx.Logger.Printf("unhandled change to Secret resource: %+v", v)
default:
c.Context.Logger.Errorf("unexpected resource type (%T) in work queue", obj)
ctx.Logger.Errorf("unexpected resource type (%T) in work queue", obj)
}
return true
return nil
}

View File

@ -2,8 +2,151 @@ package certificates
import (
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/controller"
)
func (c *Controller) sync(crt *v1alpha1.Certificate) error {
func sync(ctx *controller.Context, crt *v1alpha1.Certificate) error {
// // step zero: check if the referenced issuer exists and is ready
// issuer, err := ctx.CertManagerInformerFactory.Certmanager().V1alpha1().Issuers().Lister().Issuers(crt.Namespace).Get(crt.Spec.Issuer)
// if err != nil {
// return fmt.Errorf("issuer '%s' for certificate '%s' does not exist", crt.Spec.Issuer, crt.Name)
// }
// // step one: check if referenced secret exists, if not, trigger issue event
// secret, err := ctx.InformerFactory.Core().V1().Secrets().Lister().Secrets(crt.Namespace).Get(crt.Spec.SecretName)
// if err != nil {
// // TODO (@munnerz): only issue a certificate if the call failed due to
// // no resource being found
// return c.issue(crt)
// }
// certBytes, okcert := secret.Data[api.TLSCertKey]
// keyBytes, okkey := secret.Data[api.TLSPrivateKeyKey]
// // check if the certificate and private key exist, if not, trigger an issue
// if !okcert || !okkey {
// return c.issue(crt)
// }
// // decode the tls certificate pem
// block, _ := pem.Decode(certBytes)
// if block == nil {
// ctx.Logger.Printf("error decoding cert PEM block in '%s'", crt.Spec.SecretName)
// return c.issue(crt)
// }
// // parse the tls certificate
// cert, err := x509.ParseCertificate(block.Bytes)
// if err != nil {
// ctx.Logger.Printf("error parsing TLS certificate in '%s': %s", crt.Spec.SecretName, err.Error())
// return c.issue(crt)
// }
// // decode the private key pem
// block, _ = pem.Decode(keyBytes)
// if block == nil {
// ctx.Logger.Printf("error decoding private key PEM block in '%s'", crt.Spec.SecretName)
// return c.issue(crt)
// }
// // parse the private key
// key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
// if err != nil {
// ctx.Logger.Printf("error parsing private key in '%s': %s", crt.Spec.SecretName, err.Error())
// return c.issue(crt)
// }
// // validate the private key
// if err = key.Validate(); err != nil {
// ctx.Logger.Printf("private key failed validation in '%s': %s", crt.Spec.SecretName, err.Error())
// return c.issue(crt)
// }
// // step two: check if referenced secret is valid for listed domains. if not, return failure
// if !equalUnsorted(crt.Spec.Domains, cert.DNSNames) {
// ctx.Logger.Printf("list of domains on certificate do not match domains in spec")
// return c.issue(crt)
// }
// // step three: check if referenced secret is valid (after start & before expiry)
// if time.Now().Sub(cert.NotAfter) > time.Hour*(24*30) {
// return c.renew(crt)
// }
return nil
}
// // issue will attempt to retrieve a certificate from the specified issuer, or
// // return an error on failure. If retrieval is succesful, the certificate data
// // and private key will be stored in the named secret
// func (c *Controller) issue(crt *v1alpha1.Certificate) error {
// i, err := issuer.IssuerFor(crt)
// if err != nil {
// return err
// }
// cert, key, err := i.Issue(&ctx, crt)
// if err != nil {
// return fmt.Errorf("error issuing certificate: %s", err.Error())
// }
// // TODO: support updating resources
// _, err = ctx.Client.Secrets(crt.Namespace).Create(&api.Secret{
// ObjectMeta: metav1.ObjectMeta{
// Name: crt.Spec.SecretName,
// Namespace: crt.Namespace,
// },
// Data: map[string][]byte{
// api.TLSCertKey: cert,
// api.TLSPrivateKeyKey: key,
// },
// })
// if err != nil {
// return fmt.Errorf("error saving certificate: %s", err.Error())
// }
// return nil
// }
// // renew will attempt to renew a certificate from the specified issuer, or
// // return an error on failure. If renewal is succesful, the certificate data
// // and private key will be stored in the named secret
// func (c *Controller) renew(crt *v1alpha1.Certificate) error {
// i, err := issuer.IssuerFor(crt)
// if err != nil {
// return err
// }
// cert, key, err := i.Renew(&ctx, crt)
// if err != nil {
// return fmt.Errorf("error renewing certificate: %s", err.Error())
// }
// _, err = ctx.Client.Secrets(crt.Namespace).Update(&api.Secret{
// ObjectMeta: metav1.ObjectMeta{
// Name: crt.Spec.SecretName,
// Namespace: crt.Namespace,
// },
// Data: map[string][]byte{
// api.TLSCertKey: cert,
// api.TLSPrivateKeyKey: key,
// },
// })
// if err != nil {
// return fmt.Errorf("error saving certificate: %s", err.Error())
// }
// return nil
// }
// func equalUnsorted(s1 []string, s2 []string) bool {
// if len(s1) != len(s2) {
// return false
// }
// s1_2, s2_2 := make([]string, len(s1)), make([]string, len(s2))
// sort.Strings(s1)
// sort.Strings(s2)
// for i, s := range s1_2 {
// if s != s2_2[i] {
// return false
// }
// }
// return true
// }

View File

@ -0,0 +1,49 @@
package certificates
import (
"testing"
)
func TestEqualUnsorted(t *testing.T) {
type testT struct {
desc string
s1 []string
s2 []string
equal bool
}
tests := []testT{
{
desc: "equal but out of order slices should be equal",
s1: []string{"a", "b", "c"},
s2: []string{"b", "a", "c"},
equal: true,
},
{
desc: "non-equal but ordered slices should not be equal",
s1: []string{"a", "b"},
s2: []string{"a", "b", "c"},
equal: false,
},
{
desc: "non-equal but ordered slices should not be equal",
s1: []string{"a", "b", "c"},
s2: []string{"a", "b"},
equal: false,
},
{
desc: "equal and ordered slices should be equal",
s1: []string{"a", "b", "c"},
s2: []string{"a", "b", "c"},
equal: true,
},
}
for _, test := range tests {
t.Run(test.desc, func(test testT) func(*testing.T) {
return func(t *testing.T) {
if actual := equalUnsorted(test.s1, test.s2); actual != test.equal {
t.Errorf("equalUnsorted(%+v, %+v) = %t, but expected %t", test.s1, test.s2, actual, test.equal)
}
}
}(test))
}
}

View File

@ -0,0 +1,81 @@
package controller
import (
"reflect"
"time"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
)
type Controller struct {
Context *Context
// TODO (@munnerz): come up with some way to swap out this queue type
Queue workqueue.RateLimitingInterface
Worker func(Context, interface{}) error
Informers []cache.SharedIndexInformer
}
// Run will start this controllers run loop, with the specified number of
// worker goroutines. It will block until a message is placed onto the stopCh.
func (c *Controller) Run(workers int, stopCh <-chan struct{}) {
defer c.Queue.ShutDown()
hasSynced := make([]cache.InformerSynced, len(c.Informers))
for i, informer := range c.Informers {
informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: c.Queue.Add,
UpdateFunc: func(old, cur interface{}) {
if !reflect.DeepEqual(old, cur) {
c.Queue.Add(cur)
}
},
DeleteFunc: c.Queue.Add,
})
hasSynced[i] = informer.HasSynced
}
c.Context.Logger.Printf("Starting control loop")
// wait for all the informer caches we depend on are synced
if !cache.WaitForCacheSync(stopCh, hasSynced...) {
c.Context.Logger.Errorf("error waiting for informer caches to sync")
return
}
for i := 0; i < workers; i++ {
// TODO (@munnerz): make time.Second duration configurable
go wait.Until(c.worker, time.Second, stopCh)
}
<-stopCh
c.Context.Logger.Printf("shutting down queue as workqueue signalled shutdown")
}
func (c *Controller) worker() {
c.Context.Logger.Printf("starting worker")
for {
obj, shutdown := c.Queue.Get()
if shutdown {
break
}
err := func(obj interface{}) error {
defer c.Queue.Done(obj)
if err := c.Worker(*c.Context, obj); err != nil {
return err
}
c.Queue.Forget(obj)
return nil
}(obj)
if err != nil {
c.Context.Logger.Printf("requeuing item due to error processing: %s", err.Error())
c.Queue.AddRateLimited(obj)
}
c.Context.Logger.Printf("finished processing work item")
}
c.Context.Logger.Printf("exiting worker loop")
}

View File

@ -0,0 +1,46 @@
package issuers
import (
"time"
"github.com/juju/ratelimit"
api "k8s.io/api/core/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/controller"
)
func New(ctx controller.Context) controller.Controller {
return controller.Controller{
Context: &ctx,
Queue: workqueue.NewRateLimitingQueue(
workqueue.NewMaxOfRateLimiter(
workqueue.NewItemExponentialFailureRateLimiter(15*time.Second, time.Minute),
// 10 qps, 100 bucket size. This is only for retry speed and its only the overall factor (not per item)
&workqueue.BucketRateLimiter{Bucket: ratelimit.NewBucketWithRate(float64(10), int64(100))},
),
),
Worker: processNextWorkItem,
Informers: []cache.SharedIndexInformer{
ctx.CertManagerInformerFactory.Certmanager().V1alpha1().Issuers().Informer(),
ctx.InformerFactory.Core().V1().Secrets().Informer(),
},
}
}
func processNextWorkItem(ctx controller.Context, obj interface{}) error {
switch v := obj.(type) {
case *v1alpha1.Issuer:
if err := sync(&ctx, v.Namespace, v.Name); err != nil {
return err
}
case *api.Secret:
ctx.Logger.Printf("got secret %s/%s, nothing implemented to handle yet", v.Namespace, v.Name)
default:
ctx.Logger.Errorf("unexpected resource type (%T) in work queue", obj)
}
return nil
}

View File

@ -0,0 +1,30 @@
package issuers
import (
"fmt"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
"github.com/jetstack/cert-manager/pkg/controller"
"github.com/jetstack/cert-manager/pkg/issuer"
)
func sync(ctx *controller.Context, namespace, name string) error {
acc, err := ctx.CertManagerInformerFactory.Certmanager().V1alpha1().Issuers().Lister().Issuers(namespace).Get(name)
if err != nil {
if k8sErrors.IsNotFound(err) {
ctx.Logger.Printf("issuer '%s/%s' in sync queue has been deleted", namespace, name)
return nil
}
return fmt.Errorf("error retreiving issuer: %s", err.Error())
}
i, err := issuer.IssuerFor(*ctx, acc)
if err != nil {
return err
}
return i.Setup()
}

186
pkg/issuer/acme/account.go Normal file
View File

@ -0,0 +1,186 @@
package acme
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"strings"
"golang.org/x/crypto/acme"
api "k8s.io/api/core/v1"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/controller"
)
const (
acmeAccountPrivateKeyKey = "key.pem"
)
type account struct {
ctx *controller.Context
issuer *v1alpha1.Issuer
}
func (a *account) uri() string {
return a.issuer.Spec.ACME.URI
}
func (a *account) email() string {
return a.issuer.Spec.ACME.Email
}
func (a *account) server() string {
return a.issuer.Spec.ACME.Server
}
// privateKey returns the private key for this account from the given context,
// or an error
// TODO (@munnerz): how can we support different types of private keys other
// than rsa?
func (a *account) privateKey() (*rsa.PrivateKey, error) {
keyName := a.issuer.Spec.ACME.PrivateKey
keySecret, err := a.ctx.InformerFactory.Core().V1().Secrets().Lister().Secrets(a.issuer.Namespace).Get(keyName)
if err != nil {
// we return the plain error here so k8sErrors.IsNotFound can be used
return nil, err
}
keyBytes, okkey := keySecret.Data[acmeAccountPrivateKeyKey]
// TODO: should we automatically recover from this situation by creating the key?
if !okkey {
return nil, fmt.Errorf("no '%s' key set in account secret", api.TLSPrivateKeyKey)
}
// decode the private key pem
block, _ := pem.Decode(keyBytes)
if block == nil {
return nil, fmt.Errorf("error decoding private key PEM block in '%s'", keyName)
}
// parse the private key
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("error parsing private key in '%s': %s", keyName, err.Error())
}
// validate the private key
if err = key.Validate(); err != nil {
return nil, fmt.Errorf("private key failed validation in '%s': %s", keyName, err.Error())
}
return key, nil
}
// verify verifies an acme account is valid with the acme server
func (a *account) verify() error {
if a.issuer.Spec.ACME.Server == "" {
a.ctx.Logger.Printf("acme server url must be set")
return nil
}
privateKey, err := a.privateKey()
if err != nil {
a.issuer.Status.Ready = false
return err
}
a.ctx.Logger.Printf("using acme server '%s' for verification", a.issuer.Spec.ACME.Server)
cl := acme.Client{
Key: privateKey,
DirectoryURL: a.issuer.Spec.ACME.Server,
}
_, err = cl.GetReg(context.Background(), a.issuer.Spec.ACME.URI)
if err != nil {
return fmt.Errorf("error getting acme registration: %s", err.Error())
}
// TODO: come up with some way to verify the private key is valid for this
// account
return nil
}
// register will register an account with the acme server and store the account
// details in the context
func (a *account) register() error {
if a.issuer.Spec.ACME.Server == "" {
a.ctx.Logger.Printf("acme server url must be set")
return nil
}
privateKey, err := a.privateKey()
if err != nil {
if !k8sErrors.IsNotFound(err) {
return fmt.Errorf("error getting private key: %s", err.Error())
}
// TODO (@munnerz): allow changing the keysize
privateKey, err = a.generatePrivateKey(2048)
if err != nil {
return fmt.Errorf("error generating private key: %s", err.Error())
}
_, err = a.ctx.Client.Secrets(a.issuer.Namespace).Create(&api.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: a.issuer.Spec.ACME.PrivateKey,
Namespace: a.issuer.Namespace,
},
Data: map[string][]byte{
acmeAccountPrivateKeyKey: pem.EncodeToMemory(
&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)},
),
},
})
if err != nil {
return fmt.Errorf("error saving private key: %s", err.Error())
}
}
a.ctx.Logger.Printf("using acme server '%s' for registration", a.issuer.Spec.ACME.Server)
cl := acme.Client{
Key: privateKey,
DirectoryURL: a.issuer.Spec.ACME.Server,
}
acc := &acme.Account{
Contact: []string{fmt.Sprintf("mailto:%s", strings.ToLower(a.issuer.Spec.ACME.Email))},
}
// todo (@munnerz): don't use ctx.Background() here
account, err := cl.Register(context.Background(), acc, acme.AcceptTOS)
if err != nil {
var acmeErr *acme.Error
var ok bool
if acmeErr, ok = err.(*acme.Error); !ok || (acmeErr.StatusCode != 409) {
return fmt.Errorf("error registering acme account: %s", err.Error())
}
if a.issuer.Spec.ACME.URI == "" {
return fmt.Errorf("private key already registered but user URI not found. delete existing private key or set acme account URI")
}
if account, err = cl.UpdateReg(context.Background(), acc); err != nil {
return fmt.Errorf("error updating acme account registration: %s", err.Error())
}
}
a.issuer.Spec.ACME.URI = account.URI
return nil
}
func (a *account) generatePrivateKey(bits int) (*rsa.PrivateKey, error) {
return rsa.GenerateKey(rand.Reader, bits)
}

99
pkg/issuer/acme/acme.go Normal file
View File

@ -0,0 +1,99 @@
package acme
import (
"fmt"
"reflect"
"strings"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/client/scheme"
"github.com/jetstack/cert-manager/pkg/controller"
)
type Acme struct {
ctx *controller.Context
issuer *v1alpha1.Issuer
account *account
}
func New(ctx *controller.Context, issuer *v1alpha1.Issuer) (*Acme, error) {
if issuer.Spec.ACME == nil {
return nil, fmt.Errorf("acme config is not set")
}
return &Acme{ctx, issuer, &account{ctx, issuer}}, nil
}
func (a *Acme) Setup() error {
before, err := scheme.Scheme.DeepCopy(a.issuer)
if err != nil {
return fmt.Errorf("internal error creating deepcopy for issuer: %s", err.Error())
}
defer func() {
if !reflect.DeepEqual(before, a.issuer) {
a.saveIssuer()
}
}()
err = a.ensureSetup()
if err != nil {
return err
}
return nil
}
// saveIssuer will save the contained issuer resource in the API server
func (a *Acme) saveIssuer() error {
_, err := a.ctx.CertManagerClient.Issuers(a.issuer.Namespace).Update(a.issuer)
return err
}
// ensureSetup will ensure that this issuer is ready to issue certificates.
// it
func (a *Acme) ensureSetup() error {
err := a.account.verify()
if err == nil {
a.issuer.Status.Ready = true
return nil
}
a.issuer.Status.Ready = false
err = a.account.register()
if err != nil {
// don't write updated state as an actual error occurred
return fmt.Errorf("error registering acme account: %s", err.Error())
}
a.issuer.Status.Ready = true
return nil
}
func (a *Acme) Issue(crt *v1alpha1.Certificate) ([]byte, []byte, error) {
if crt.Spec.ACME == nil {
return nil, nil, fmt.Errorf("acme config must be specified")
}
// TODO (@munnerz): tidy this weird horrible line up
switch v1alpha1.ACMEChallengeType(strings.ToUpper(string(crt.Spec.ACME.Challenge))) {
case v1alpha1.ACMEChallengeTypeHTTP01:
a.ctx.Logger.Printf("Obtaining certificates for %+v", crt.Spec.Domains)
// todo: use acme library to obtain challenge details and pass them to the solver
case v1alpha1.ACMEChallengeTypeTLSSNI01:
case v1alpha1.ACMEChallengeTypeDNS01:
default:
return nil, nil, fmt.Errorf("invalid acme challenge type '%s'", crt.Spec.ACME.Challenge)
}
return nil, nil, nil
}
func (a *Acme) Renew(crt *v1alpha1.Certificate) ([]byte, []byte, error) {
return nil, nil, nil
}

View File

@ -0,0 +1 @@
package dns

View File

@ -0,0 +1,6 @@
package http
const (
// HTTPChallengePath is the path prefix used for http-01 challenge requests
HTTPChallengePath = "/.well-known/acme-challenge"
)

View File

@ -0,0 +1,73 @@
package http
import (
"fmt"
"net/http"
"path"
"strings"
"sync"
)
// Challenge is a Host/Token pair that is used for verify ownership of domains
type challenge struct {
host, token string
}
// httpSolver is an ACME HTTP-01 challenge solver. It will listen on a given
// port and respond with the appropriate response given the listen of valid
// Challenge structures registered with it.
type httpSolver struct {
// challenges is a list of challenge resources to validate
challenges []challenge
// challengeMutex is used to guarantee sync between different goroutines
// accessing the list of valid challenges
challengeMutex sync.Mutex
// ListenPort is the port cert-manager should listen on for ACME HTTP-01
// challenge requests
ListenPort int
}
// AddChallenge will add a challenge structure to the list of valid challenges
func (h *httpSolver) addChallenge(c challenge) {
h.challengeMutex.Lock()
defer h.challengeMutex.Unlock()
h.challenges = append(h.challenges, c)
}
// Listen will begin listening for connections on the given port, and
// validating requests that contain a valid domain, path & token
func (h *httpSolver) listen() error {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.challengeMutex.Lock()
defer h.challengeMutex.Unlock()
// extract vars from the request
host := strings.Split(r.Host, ":")[0]
basePath := path.Dir(r.URL.EscapedPath())
token := path.Base(r.URL.EscapedPath())
// verify the base path is correct
if basePath != HTTPChallengePath {
http.NotFound(w, r)
return
}
for i, c := range h.challenges {
// if either the host or the token don't match what is expected,
// we should continue to the next loop iteration
if c.host != host || c.token != token {
continue
}
// otherwise, this is a valid request and we're going to approve it
w.WriteHeader(http.StatusOK)
// remove this challenge from the list so we don't store indefinitely
h.challenges = append(h.challenges[:i], h.challenges[i+1:]...)
return
}
// if nothing else, we return a 404 here
http.NotFound(w, r)
})
return http.ListenAndServe(fmt.Sprintf(":%d", h.ListenPort), handler)
}

30
pkg/issuer/issuer.go Normal file
View File

@ -0,0 +1,30 @@
package issuer
import (
"fmt"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/controller"
"github.com/jetstack/cert-manager/pkg/issuer/acme"
)
type Interface interface {
// Setup initialises the issuer. This may include registering accounts with
// a service, creating a CA and storing it somewhere, or verifying
// credentials and authorization with a remote server.
Setup() error
// Issue attempts to issue a certificate as described by the certificate
// resource given
Issue(*v1alpha1.Certificate) ([]byte, []byte, error)
// Renew attempts to renew the certificate describe by the certificate
// resource given. If no certificate exists, an error is returned.
Renew(*v1alpha1.Certificate) ([]byte, []byte, error)
}
func IssuerFor(ctx controller.Context, issuer *v1alpha1.Issuer) (Interface, error) {
switch {
case issuer.Spec.ACME != nil:
return acme.New(&ctx, issuer)
}
return nil, fmt.Errorf("issuer '%s' does not have an issuer specification", issuer.Name)
}

14
pkg/util/util.go Normal file
View File

@ -0,0 +1,14 @@
package util
func OnlyOneNotNil(items ...interface{}) (any bool, one bool) {
oneNotNil := false
for _, i := range items {
if i != nil {
if oneNotNil {
return true, false
}
oneNotNil = true
}
}
return oneNotNil, oneNotNil
}