Add acme issuer. Implement 'Setup' method. Now manages ACME accounts.
This commit is contained in:
parent
aa03460d21
commit
95cba8ab5f
@ -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.
|
||||
|
||||
@ -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")
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
// }
|
||||
|
||||
49
pkg/controller/certificates/sync_test.go
Normal file
49
pkg/controller/certificates/sync_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
81
pkg/controller/controller.go
Normal file
81
pkg/controller/controller.go
Normal 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")
|
||||
}
|
||||
46
pkg/controller/issuers/controller.go
Normal file
46
pkg/controller/issuers/controller.go
Normal 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
|
||||
}
|
||||
30
pkg/controller/issuers/sync.go
Normal file
30
pkg/controller/issuers/sync.go
Normal 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
186
pkg/issuer/acme/account.go
Normal 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
99
pkg/issuer/acme/acme.go
Normal 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
|
||||
}
|
||||
1
pkg/issuer/acme/dns/dns.go
Normal file
1
pkg/issuer/acme/dns/dns.go
Normal file
@ -0,0 +1 @@
|
||||
package dns
|
||||
6
pkg/issuer/acme/http/constants.go
Normal file
6
pkg/issuer/acme/http/constants.go
Normal file
@ -0,0 +1,6 @@
|
||||
package http
|
||||
|
||||
const (
|
||||
// HTTPChallengePath is the path prefix used for http-01 challenge requests
|
||||
HTTPChallengePath = "/.well-known/acme-challenge"
|
||||
)
|
||||
73
pkg/issuer/acme/http/http.go
Normal file
73
pkg/issuer/acme/http/http.go
Normal 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
30
pkg/issuer/issuer.go
Normal 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
14
pkg/util/util.go
Normal 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
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user