add start of authorization

This commit is contained in:
James Munnelly 2017-07-22 01:23:25 +01:00
parent ab18029189
commit afbe0ba9c5
9 changed files with 346 additions and 28 deletions

View File

@ -29,6 +29,7 @@ import (
_ "github.com/jetstack/cert-manager/pkg/apis/certmanager/install" _ "github.com/jetstack/cert-manager/pkg/apis/certmanager/install"
"github.com/jetstack/cert-manager/pkg/client" "github.com/jetstack/cert-manager/pkg/client"
"github.com/jetstack/cert-manager/pkg/controller" "github.com/jetstack/cert-manager/pkg/controller"
"github.com/jetstack/cert-manager/pkg/controller/certificates"
"github.com/jetstack/cert-manager/pkg/controller/issuers" "github.com/jetstack/cert-manager/pkg/controller/issuers"
"github.com/jetstack/cert-manager/pkg/informers/externalversions" "github.com/jetstack/cert-manager/pkg/informers/externalversions"
logpkg "github.com/jetstack/cert-manager/pkg/log" logpkg "github.com/jetstack/cert-manager/pkg/log"
@ -75,14 +76,14 @@ func main() {
} }
issuerCtrl := issuers.New(ctx) issuerCtrl := issuers.New(ctx)
// certificatesCtrl := certificates.New(ctx) certificatesCtrl := certificates.New(ctx)
stopCh := make(chan struct{}) stopCh := make(chan struct{})
factory.Start(stopCh) factory.Start(stopCh)
cmFactory.Start(stopCh) cmFactory.Start(stopCh)
go issuerCtrl.Run(5, stopCh) go issuerCtrl.Run(5, stopCh)
// go certificatesCtrl.Run(5, stopCh) go certificatesCtrl.Run(5, stopCh)
<-stopCh <-stopCh
} }

View File

@ -38,7 +38,6 @@ func processNextWorkItem(ctx controller.Context, obj interface{}) error {
return err return err
} }
case *api.Secret: case *api.Secret:
ctx.Logger.Printf("unhandled change to Secret resource: %+v", v)
default: default:
ctx.Logger.Errorf("unexpected resource type (%T) in work queue", obj) ctx.Logger.Errorf("unexpected resource type (%T) in work queue", obj)
} }

View File

@ -1,17 +1,36 @@
package certificates package certificates
import ( import (
"fmt"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/controller" "github.com/jetstack/cert-manager/pkg/controller"
"github.com/jetstack/cert-manager/pkg/issuer"
) )
func sync(ctx *controller.Context, crt *v1alpha1.Certificate) error { func sync(ctx *controller.Context, crt *v1alpha1.Certificate) error {
// // step zero: check if the referenced issuer exists and is ready // 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) issuerObj, err := ctx.CertManagerInformerFactory.Certmanager().V1alpha1().Issuers().Lister().Issuers(crt.Namespace).Get(crt.Spec.Issuer)
// if err != nil { if err != nil {
// return fmt.Errorf("issuer '%s' for certificate '%s' does not exist", crt.Spec.Issuer, crt.Name) return fmt.Errorf("issuer '%s' for certificate '%s' does not exist", crt.Spec.Issuer, crt.Name)
// } }
if !issuerObj.Status.Ready {
return fmt.Errorf("issuer '%s/%s' for certificate '%s' not ready", issuerObj.Namespace, issuerObj.Name, crt.Name)
}
i, err := issuer.IssuerFor(*ctx, issuerObj)
if err != nil {
return fmt.Errorf("error getting issuer implementation for issuer '%s': %s", issuerObj.Name, err.Error())
}
err = i.Prepare(crt)
if err != nil {
return err
}
// // step one: check if referenced secret exists, if not, trigger issue event // // 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) // secret, err := ctx.InformerFactory.Core().V1().Secrets().Lister().Secrets(crt.Namespace).Get(crt.Spec.SecretName)

View File

@ -3,7 +3,6 @@ package acme
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"strings"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/client/scheme" "github.com/jetstack/cert-manager/pkg/client/scheme"
@ -75,25 +74,6 @@ func (a *Acme) ensureSetup() error {
return nil 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) { func (a *Acme) Renew(crt *v1alpha1.Certificate) ([]byte, []byte, error) {
return nil, nil, nil return nil, nil, nil
} }

View File

@ -0,0 +1,17 @@
package acme
import (
"fmt"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/controller"
)
type solver interface {
Present(ctx controller.Context, crt *v1alpha1.Certificate, domain, token, key string) error
Cleanup(ctx controller.Context, crt *v1alpha1.Certificate, domain, token string) error
}
func solverFor(challengeType string) (solver, error) {
return nil, fmt.Errorf("no solver implemented")
}

11
pkg/issuer/acme/issue.go Normal file
View File

@ -0,0 +1,11 @@
package acme
import (
"fmt"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
)
func (a *Acme) Issue(crt *v1alpha1.Certificate) ([]byte, []byte, error) {
return nil, nil, fmt.Errorf("not implemented")
}

232
pkg/issuer/acme/prepare.go Normal file
View File

@ -0,0 +1,232 @@
package acme
import (
"context"
"fmt"
"golang.org/x/crypto/acme"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/log"
"github.com/jetstack/cert-manager/pkg/util"
)
func authorizationsMap(list []v1alpha1.ACMEDomainAuthorization) map[string]v1alpha1.ACMEDomainAuthorization {
out := make(map[string]v1alpha1.ACMEDomainAuthorization, len(list))
for _, a := range list {
out[a.Domain] = a
}
return out
}
func authorizationsToObtain(cl *acme.Client, crt v1alpha1.Certificate) ([]string, error) {
authMap := authorizationsMap(crt.Status.ACME.Authorizations)
toAuthorize := util.StringFilter(func(domain string) (bool, error) {
auth, ok := authMap[domain]
if !ok {
return false, nil
}
return checkAuthorization(cl, auth.URI)
}, crt.Spec.Domains...)
domains := make([]string, len(toAuthorize))
for i, v := range toAuthorize {
if v.Err != nil {
return nil, fmt.Errorf("error checking authorization status for %s: %s", v.String, v.Err)
}
domains[i] = v.String
}
return domains, nil
}
// Prepare will ensure the issuer has been initialised and is ready to issue
// certificates for the domains listed on the Certificate resource.
//
// It will send the appropriate Letsencrypt authorizations, and complete
// challenge requests if neccessary.
func (a *Acme) Prepare(crt *v1alpha1.Certificate) error {
if crt.Spec.ACME == nil {
return fmt.Errorf("acme config must be specified")
}
privKey, err := a.account.privateKey()
if err != nil {
return fmt.Errorf("error getting acme account private key: %s", err.Error())
}
cl := &acme.Client{
Key: privKey,
DirectoryURL: a.account.server(),
}
// step one: check issuer to see if we already have authorizations
toAuthorize, err := authorizationsToObtain(cl, *crt)
if err != nil {
return err
}
a.ctx.Logger.Printf("need to get authorizations for %v", toAuthorize)
// step two: if there are any domains that we don't have authorization for,
// we should attempt to authorize those domains
if len(toAuthorize) == 0 {
return nil
}
auths, err := getAuthorizations(cl, toAuthorize...)
if err != nil {
return err
}
a.ctx.Logger.Printf("requested authorizations for %v", toAuthorize)
// todo: parallelize this
// todo: refactor into own function
for _, auth := range auths {
a.ctx.Logger.Printf("picking challenge type for domain '%s'", auth.domain)
challengeType, err := pickChallengeType(a.ctx.Logger, auth.domain, auth.auth, crt.Spec.ACME.Config)
if err != nil {
return fmt.Errorf("error challenge type to use for domain '%s': %s", auth.domain, err.Error())
}
a.ctx.Logger.Printf("using challenge type %s for domain '%s'", challengeType, auth.domain)
challenge, err := challengeForAuthorization(cl, auth.auth, challengeType)
if err != nil {
return fmt.Errorf("error getting challenge for domain '%s': %s", auth.domain, err.Error())
}
token := challenge.Token
var key string
switch challenge.Type {
case "http-01":
key, err = cl.HTTP01ChallengeResponse(challenge.Token)
case "dns-01":
key, err = cl.DNS01ChallengeRecord(challenge.Token)
default:
err = fmt.Errorf("unsupported challenge type %s", challenge.Type)
}
if err != nil {
return fmt.Errorf("error getting key for acme challenge for domain '%s': %s", auth.domain, err.Error())
}
solver, err := solverFor(challengeType)
if err != nil {
return fmt.Errorf("error getting solver for challenge type '%s': %s", challengeType, err.Error())
}
err = solver.Present(*a.ctx, crt, auth.domain, token, key)
if err != nil {
return fmt.Errorf("error presenting acme authorization for domain '%s': %s", auth.domain, err.Error())
}
authorization, err := cl.WaitAuthorization(context.Background(), challenge.URI)
if err != nil {
return fmt.Errorf("error waiting for authorization for domain '%s': %s", auth.domain, err.Error())
}
if authorization.Status != acme.StatusValid {
return fmt.Errorf("expected acme domain authorization status for '%s' to be valid, but it's %s", auth.domain, authorization.Status)
}
crt.Status.ACME.SaveAuthorization(v1alpha1.ACMEDomainAuthorization{
Domain: auth.domain,
URI: authorization.URI,
})
}
return nil
}
func checkAuthorization(cl *acme.Client, uri string) (bool, error) {
a, err := cl.GetAuthorization(context.Background(), uri)
if err != nil {
return false, err
}
if a.Status == acme.StatusValid {
return true, nil
}
return false, nil
}
type authResponses []authResponse
type authResponse struct {
domain string
auth *acme.Authorization
err error
}
// Error returns an error if any one of the authResponses contains an error
func (a authResponses) Error() error {
var errs []error
for _, r := range a {
if r.err != nil {
errs = append(errs, fmt.Errorf("'%s': %s", r.domain, r.err))
}
}
if len(errs) > 0 {
return fmt.Errorf("error getting authorization for domains: %v", errs)
}
return nil
}
func getAuthorizations(cl *acme.Client, domains ...string) ([]authResponse, error) {
respCh := make(chan authResponse)
defer close(respCh)
for _, d := range domains {
go func(domain string) {
auth, err := cl.Authorize(context.Background(), domain)
if err != nil {
respCh <- authResponse{"", nil, fmt.Errorf("getting acme authorization failed: %s", err.Error())}
return
}
respCh <- authResponse{domain, auth, nil}
}(d)
}
responses := make([]authResponse, len(domains))
for i := 0; i < len(domains); i++ {
responses[i] = <-respCh
}
return responses, authResponses(responses).Error()
}
func pickChallengeType(log log.Logger, domain string, auth *acme.Authorization, cfg []v1alpha1.ACMECertificateDomainConfig) (string, error) {
for _, d := range cfg {
log.Printf("checking config %v", d)
for _, dom := range d.Domains {
log.Printf("checking domain %s", dom)
if dom == domain {
for _, challenge := range auth.Challenges {
switch {
case challenge.Type == "http-01" && d.HTTP01 != nil:
return challenge.Type, nil
case challenge.Type == "dns-01" && d.DNS01 != nil:
return challenge.Type, nil
}
log.Printf("cannot use %s challenge for domain %s", challenge.Type, domain)
}
}
}
}
return "", fmt.Errorf("no configured and supported challenge type found")
}
func challengeForAuthorization(cl *acme.Client, auth *acme.Authorization, challengeType string) (*acme.Challenge, error) {
for _, challenge := range auth.Challenges {
if challenge.Type != challengeType {
continue
}
return challenge, nil
}
return nil, fmt.Errorf("no supported challenges offered")
}

View File

@ -13,6 +13,8 @@ type Interface interface {
// a service, creating a CA and storing it somewhere, or verifying // a service, creating a CA and storing it somewhere, or verifying
// credentials and authorization with a remote server. // credentials and authorization with a remote server.
Setup() error Setup() error
// Prepare
Prepare(*v1alpha1.Certificate) error
// Issue attempts to issue a certificate as described by the certificate // Issue attempts to issue a certificate as described by the certificate
// resource given // resource given
Issue(*v1alpha1.Certificate) ([]byte, []byte, error) Issue(*v1alpha1.Certificate) ([]byte, []byte, error)

57
pkg/util/filter.go Normal file
View File

@ -0,0 +1,57 @@
package util
import (
"fmt"
"sync"
)
// StringFilterT is a tuple for a value that has not been filtered out
type StringFilterT struct {
String string
Err error
}
type StringFilterWrapper []StringFilterT
func (f StringFilterWrapper) Error() error {
var errs []error
for _, r := range f {
if r.Err != nil {
errs = append(errs, fmt.Errorf("'%s': %s", r.String, r.Err))
}
}
if len(errs) > 0 {
return fmt.Errorf("%v", errs)
}
return nil
}
// FilterFn is a function used to filter a list of string. If the
// function returns false or a non-nil error, it will not be filtered.
type FilterFn func(string) (filter bool, err error)
// StringFilter will run fn with each element of in, filtering out elements.
// it will return a slice of results where fn returned ok, or a non-nil error.
// it will also call each instance of fn in it's own goroutine.
func StringFilter(fn FilterFn, in ...string) StringFilterWrapper {
outCh := make(chan StringFilterT, len(in))
var wg sync.WaitGroup
for i, s := range in {
wg.Add(1)
go func(i int, s string) {
defer wg.Done()
if filter, err := fn(s); err != nil || !filter {
outCh <- StringFilterT{s, err}
}
}(i, s)
}
wg.Wait()
close(outCh)
res := make([]StringFilterT, len(outCh))
i := 0
for o := range outCh {
res[i] = o
i++
}
return res
}