Refactor Issuer interface to allow returning updated Status

This commit is contained in:
James Munnelly 2017-09-08 21:41:15 +01:00
parent 8e39ddfc2e
commit 00389b6da3
11 changed files with 135 additions and 184 deletions

View File

@ -6,17 +6,14 @@ import (
"crypto/x509"
"encoding/pem"
"fmt"
"log"
"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"
"k8s.io/client-go/kubernetes"
corev1listers "k8s.io/client-go/listers/core/v1"
"github.com/jetstack-experimental/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack-experimental/cert-manager/pkg/log"
"github.com/jetstack-experimental/cert-manager/pkg/util"
)
@ -24,43 +21,11 @@ const (
acmeAccountPrivateKeyKey = "key.pem"
)
type account struct {
issuer *v1alpha1.Issuer
client kubernetes.Interface
secretsLister corev1listers.SecretLister
}
func newAccount(issuer *v1alpha1.Issuer, client kubernetes.Interface, secretsLister corev1listers.SecretLister) *account {
return &account{issuer, client, secretsLister}
}
func (a *account) uri() string {
if a.issuer.Status.ACME == nil {
return ""
}
return a.issuer.Status.ACME.URI
}
func (a *account) email() string {
if a.issuer.Spec.ACME == nil {
return ""
}
return a.issuer.Spec.ACME.Email
}
func (a *account) server() string {
if a.issuer.Spec.ACME == nil {
return ""
}
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) {
func (a *Acme) accountPrivateKey() (*rsa.PrivateKey, error) {
if a.issuer.Spec.ACME == nil {
return nil, fmt.Errorf("acme spec block cannot be empty")
}
@ -99,7 +64,7 @@ func (a *account) privateKey() (*rsa.PrivateKey, error) {
}
// verify verifies an acme account is valid with the acme server
func (a *account) verify() error {
func (a *Acme) verifyAccount() error {
if a.issuer.Spec.ACME.Server == "" {
return fmt.Errorf("acme server url must be set")
}
@ -107,10 +72,9 @@ func (a *account) verify() error {
return fmt.Errorf("acme account uri must be set")
}
privateKey, err := a.privateKey()
privateKey, err := a.accountPrivateKey()
if err != nil {
a.issuer.Status.Ready = false
return err
}
@ -133,25 +97,25 @@ func (a *account) verify() error {
}
// register will register an account with the acme server and store the account
// details in the context
// details in the API server. It returns the registered account URI, or an error
// TODO: break this function down
func (a *account) register() error {
func (a *Acme) registerAccount() (string, error) {
if a.issuer.Spec.ACME.Server == "" {
return fmt.Errorf("acme server url must be set")
return "", fmt.Errorf("acme server url must be set")
}
privateKey, err := a.privateKey()
privateKey, err := a.accountPrivateKey()
var privateKeyPem []byte
if err != nil {
if !k8sErrors.IsNotFound(err) {
return fmt.Errorf("error getting private key: %s", err.Error())
return "", fmt.Errorf("error getting private key: %s", err.Error())
}
// TODO (@munnerz): allow changing the keysize
privateKeyPem, privateKey, err = generatePrivateKey(2048)
if err != nil {
return fmt.Errorf("error generating private key: %s", err.Error())
return "", fmt.Errorf("error generating private key: %s", err.Error())
}
_, err = util.EnsureSecret(a.client, &api.Secret{
@ -165,7 +129,7 @@ func (a *account) register() error {
})
if err != nil {
return fmt.Errorf("error saving private key: %s", err.Error())
return "", fmt.Errorf("error saving private key: %s", err.Error())
}
}
@ -186,19 +150,17 @@ func (a *account) register() error {
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())
return "", fmt.Errorf("error registering acme account: %s", err.Error())
}
if a.issuer.Status.ACME == nil || a.issuer.Status.ACME.URI == "" {
return fmt.Errorf("private key already registered but user URI not found. delete existing private key or set acme account 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())
return "", fmt.Errorf("error updating acme account registration: %s", err.Error())
}
}
a.issuer.Status.ACMEStatus().URI = account.URI
return nil
return account.URI, nil
}

View File

@ -3,25 +3,28 @@ package acme
import (
"context"
"fmt"
"time"
"k8s.io/client-go/informers"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
coreinformers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes"
corelisters "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
"github.com/jetstack-experimental/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack-experimental/cert-manager/pkg/client"
cminformers "github.com/jetstack-experimental/cert-manager/pkg/informers"
"github.com/jetstack-experimental/cert-manager/pkg/issuer"
"github.com/jetstack-experimental/cert-manager/pkg/issuer/acme/dns"
"github.com/jetstack-experimental/cert-manager/pkg/issuer/acme/http"
)
type Acme struct {
account *account
issuer *v1alpha1.Issuer
client kubernetes.Interface
cmClient client.Interface
factory informers.SharedInformerFactory
cmFactory cminformers.SharedInformerFactory
client kubernetes.Interface
cmClient client.Interface
secretsLister corelisters.SecretLister
dnsSolver solver
httpSolver solver
@ -30,16 +33,19 @@ type Acme struct {
func New(issuer *v1alpha1.Issuer,
client kubernetes.Interface,
cmClient client.Interface,
factory informers.SharedInformerFactory,
cmFactory cminformers.SharedInformerFactory) (issuer.Interface, error) {
secretsInformer cache.SharedIndexInformer) (issuer.Interface, error) {
if issuer.Spec.ACME == nil {
return nil, fmt.Errorf("acme config may not be empty")
}
secretsLister := corelisters.NewSecretLister(secretsInformer.GetIndexer())
return &Acme{
account: newAccount(issuer, client, factory.Core().V1().Secrets().Lister()),
client: client,
cmClient: cmClient,
factory: factory,
cmFactory: cmFactory,
dnsSolver: dns.NewSolver(issuer, client, factory.Core().V1().Secrets().Lister()),
httpSolver: http.NewSolver(issuer, client, factory.Core().V1().Secrets().Lister()),
issuer: issuer,
client: client,
cmClient: cmClient,
secretsLister: secretsLister,
dnsSolver: dns.NewSolver(issuer, client, secretsLister),
httpSolver: http.NewSolver(issuer, client, secretsLister),
}, nil
}
@ -60,5 +66,15 @@ func (a *Acme) solverFor(challengeType string) (solver, error) {
}
func init() {
issuer.SharedFactory().Register(issuer.IssuerACME, New)
issuer.Register(issuer.IssuerACME, func(i *v1alpha1.Issuer, ctx *issuer.Context) (issuer.Interface, error) {
return New(
i,
ctx.Client,
ctx.CMClient,
ctx.SharedInformerFactory.InformerFor(
ctx.Namespace,
metav1.GroupVersionKind{Version: "v1", Kind: "Secret"},
coreinformers.NewSecretInformer(ctx.Client, ctx.Namespace, time.Second*30, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})),
)
})
}

View File

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"strings"
@ -23,7 +24,6 @@ import (
"github.com/jetstack-experimental/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack-experimental/cert-manager/pkg/issuer/acme/http/solver"
"github.com/jetstack-experimental/cert-manager/pkg/log"
"github.com/jetstack-experimental/cert-manager/pkg/util"
)

View File

@ -9,17 +9,17 @@ import (
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"log"
"golang.org/x/crypto/acme"
api "k8s.io/api/core/v1"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
"github.com/jetstack-experimental/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack-experimental/cert-manager/pkg/log"
)
func (a *Acme) getCertificatePrivateKey(crt *v1alpha1.Certificate) ([]byte, crypto.Signer, error) {
crtSecret, err := a.factory.Core().V1().Secrets().Lister().Secrets(crt.Namespace).Get(crt.Spec.SecretName)
crtSecret, err := a.secretsLister.Secrets(crt.Namespace).Get(crt.Spec.SecretName)
if err != nil {
if !k8sErrors.IsNotFound(err) {
return nil, nil, fmt.Errorf("error reading certificate private key for certificate '%s': %s", crt.Name, err.Error())
@ -53,7 +53,7 @@ func (a *Acme) obtainCertificate(crt *v1alpha1.Certificate) (privateKeyPem []byt
return nil, nil, fmt.Errorf("no domains specified")
}
privKey, err := a.account.privateKey()
privKey, err := a.accountPrivateKey()
if err != nil {
return nil, nil, fmt.Errorf("error getting acme account private key: %s", err.Error())
@ -61,7 +61,7 @@ func (a *Acme) obtainCertificate(crt *v1alpha1.Certificate) (privateKeyPem []byt
cl := &acme.Client{
Key: privKey,
DirectoryURL: a.account.server(),
DirectoryURL: a.issuer.Spec.ACME.Server,
}
template := x509.CertificateRequest{

View File

@ -3,12 +3,12 @@ package acme
import (
"context"
"fmt"
"log"
"reflect"
"golang.org/x/crypto/acme"
"github.com/jetstack-experimental/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack-experimental/cert-manager/pkg/log"
"github.com/jetstack-experimental/cert-manager/pkg/util"
)
@ -71,8 +71,8 @@ func (a *Acme) prepare(crt *v1alpha1.Certificate) error {
return fmt.Errorf("acme config must be specified")
}
log.Printf("getting private key for acme issuer %s/%s", a.account.issuer.Namespace, a.account.issuer.Name)
privKey, err := a.account.privateKey()
log.Printf("getting private key for acme issuer %s/%s", a.issuer.Namespace, a.issuer.Name)
privKey, err := a.accountPrivateKey()
if err != nil {
return fmt.Errorf("error getting acme account private key: %s", err.Error())
@ -80,7 +80,7 @@ func (a *Acme) prepare(crt *v1alpha1.Certificate) error {
cl := &acme.Client{
Key: privKey,
DirectoryURL: a.account.server(),
DirectoryURL: a.issuer.Spec.ACME.Server,
}
// step one: check issuer to see if we already have authorizations

View File

@ -2,49 +2,28 @@ package acme
import (
"fmt"
"reflect"
"github.com/jetstack-experimental/cert-manager/pkg/apis/certmanager/v1alpha1"
)
func (a *Acme) Setup() (err error) {
issuerBefore := a.account.issuer.DeepCopy()
func (a *Acme) Setup() (v1alpha1.IssuerStatus, error) {
updateStatus := a.issuer.Status.DeepCopy()
defer func() {
if !reflect.DeepEqual(issuerBefore, a.account.issuer) {
if err == nil {
_, err = a.cmClient.CertmanagerV1alpha1().Issuers(a.account.issuer.Namespace).Update(a.account.issuer)
}
}
}()
err = a.ensureSetup()
if err != nil {
return err
}
return nil
}
// ensureSetup will ensure that this issuer is ready to issue certificates.
// it
func (a *Acme) ensureSetup() error {
err := a.account.verify()
err := a.verifyAccount()
if err == nil {
a.account.issuer.Status.Ready = true
return nil
updateStatus.Ready = true
return *updateStatus, nil
}
a.account.issuer.Status.Ready = false
err = a.account.register()
uri, err := a.registerAccount()
if err != nil {
// don't write updated state as an actual error occurred
return fmt.Errorf("error registering acme account: %s", err.Error())
updateStatus.Ready = false
return *updateStatus, fmt.Errorf("error registering acme account: %s", err.Error())
}
a.account.issuer.Status.Ready = true
updateStatus.ACMEStatus().URI = uri
return nil
return *updateStatus, nil
}

17
pkg/issuer/context.go Normal file
View File

@ -0,0 +1,17 @@
package issuer
import (
"k8s.io/client-go/kubernetes"
"github.com/jetstack-experimental/cert-manager/pkg/client"
"github.com/jetstack-experimental/cert-manager/pkg/kube"
)
type Context struct {
Client kubernetes.Interface
CMClient client.Interface
SharedInformerFactory kube.SharedInformerFactory
Namespace string
}

View File

@ -2,47 +2,32 @@ package issuer
import (
"fmt"
"sync"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"github.com/jetstack-experimental/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack-experimental/cert-manager/pkg/client"
cminformers "github.com/jetstack-experimental/cert-manager/pkg/informers"
"github.com/jetstack-experimental/cert-manager/pkg/log"
)
type Factory struct {
constructors map[string]Constructor
constructorsLock sync.RWMutex
client kubernetes.Interface
cmClient client.Interface
factory informers.SharedInformerFactory
cmFactory cminformers.SharedInformerFactory
type Factory interface {
IssuerFor(*v1alpha1.Issuer) (Interface, error)
}
func (f *Factory) Setup(client kubernetes.Interface,
cmClient client.Interface,
factory informers.SharedInformerFactory,
cmFactory cminformers.SharedInformerFactory) {
f.client = client
f.cmClient = cmClient
f.factory = factory
f.cmFactory = cmFactory
func NewFactory(ctx *Context) Factory {
return &factory{ctx: ctx}
}
func (f *Factory) IssuerFor(issuer *v1alpha1.Issuer) (Interface, error) {
type factory struct {
ctx *Context
}
func (f *factory) IssuerFor(issuer *v1alpha1.Issuer) (Interface, error) {
issuerType, err := nameForIssuer(issuer)
if err != nil {
return nil, fmt.Errorf("could not get issuer type: %s", err.Error())
}
f.constructorsLock.RLock()
defer f.constructorsLock.RUnlock()
if constructor, ok := f.constructors[issuerType]; ok {
return constructor(issuer, f.client, f.cmClient, f.factory, f.cmFactory)
constructorsLock.RLock()
defer constructorsLock.RUnlock()
if constructor, ok := constructors[issuerType]; ok {
return constructor(issuer, f.ctx)
}
return nil, fmt.Errorf("issuer '%s' not registered", issuerType)
}
@ -50,9 +35,8 @@ func (f *Factory) IssuerFor(issuer *v1alpha1.Issuer) (Interface, error) {
// Register will register an issuer constructor so it can be used within the
// application. 'name' should be unique, and should be used to identify this
// issuer.
func (f *Factory) Register(name string, c Constructor) {
f.constructorsLock.Lock()
defer f.constructorsLock.Unlock()
log.Printf("registered issuer '%s'", name)
f.constructors[name] = c
func Register(name string, c Constructor) {
constructorsLock.Lock()
defer constructorsLock.Unlock()
constructors[name] = c
}

View File

@ -1,39 +0,0 @@
package issuer
import (
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"github.com/jetstack-experimental/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack-experimental/cert-manager/pkg/client"
cminformers "github.com/jetstack-experimental/cert-manager/pkg/informers"
)
var sharedFactory = &Factory{
constructors: make(map[string]Constructor),
}
func SharedFactory() *Factory {
return sharedFactory
}
type Constructor func(issuer *v1alpha1.Issuer,
client kubernetes.Interface,
cmClient client.Interface,
factory informers.SharedInformerFactory,
cmFactory cminformers.SharedInformerFactory) (Interface, error)
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
// Prepare
Prepare(*v1alpha1.Certificate) 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)
}

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

@ -0,0 +1,18 @@
package issuer
import "github.com/jetstack-experimental/cert-manager/pkg/apis/certmanager/v1alpha1"
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() (v1alpha1.IssuerStatus, error)
// Prepare
Prepare(*v1alpha1.Certificate) 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)
}

14
pkg/issuer/register.go Normal file
View File

@ -0,0 +1,14 @@
package issuer
import (
"sync"
"github.com/jetstack-experimental/cert-manager/pkg/apis/certmanager/v1alpha1"
)
type Constructor func(*v1alpha1.Issuer, *Context) (Interface, error)
var (
constructors = make(map[string]Constructor)
constructorsLock sync.RWMutex
)