add start of authorization
This commit is contained in:
parent
ab18029189
commit
afbe0ba9c5
@ -29,6 +29,7 @@ import (
|
||||
_ "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/issuers"
|
||||
"github.com/jetstack/cert-manager/pkg/informers/externalversions"
|
||||
logpkg "github.com/jetstack/cert-manager/pkg/log"
|
||||
@ -75,14 +76,14 @@ func main() {
|
||||
}
|
||||
|
||||
issuerCtrl := issuers.New(ctx)
|
||||
// certificatesCtrl := certificates.New(ctx)
|
||||
certificatesCtrl := certificates.New(ctx)
|
||||
|
||||
stopCh := make(chan struct{})
|
||||
factory.Start(stopCh)
|
||||
cmFactory.Start(stopCh)
|
||||
|
||||
go issuerCtrl.Run(5, stopCh)
|
||||
// go certificatesCtrl.Run(5, stopCh)
|
||||
go certificatesCtrl.Run(5, stopCh)
|
||||
|
||||
<-stopCh
|
||||
}
|
||||
|
||||
@ -38,7 +38,6 @@ func processNextWorkItem(ctx controller.Context, obj interface{}) error {
|
||||
return err
|
||||
}
|
||||
case *api.Secret:
|
||||
ctx.Logger.Printf("unhandled change to Secret resource: %+v", v)
|
||||
default:
|
||||
ctx.Logger.Errorf("unexpected resource type (%T) in work queue", obj)
|
||||
}
|
||||
|
||||
@ -1,17 +1,36 @@
|
||||
package certificates
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
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)
|
||||
// step zero: check if the referenced issuer exists and is ready
|
||||
issuerObj, 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)
|
||||
// }
|
||||
if err != nil {
|
||||
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
|
||||
// secret, err := ctx.InformerFactory.Core().V1().Secrets().Lister().Secrets(crt.Namespace).Get(crt.Spec.SecretName)
|
||||
|
||||
@ -3,7 +3,6 @@ package acme
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
|
||||
"github.com/jetstack/cert-manager/pkg/client/scheme"
|
||||
@ -75,25 +74,6 @@ func (a *Acme) ensureSetup() error {
|
||||
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
|
||||
}
|
||||
|
||||
17
pkg/issuer/acme/interface.go
Normal file
17
pkg/issuer/acme/interface.go
Normal 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
11
pkg/issuer/acme/issue.go
Normal 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
232
pkg/issuer/acme/prepare.go
Normal 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")
|
||||
}
|
||||
@ -13,6 +13,8 @@ type Interface interface {
|
||||
// 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)
|
||||
|
||||
57
pkg/util/filter.go
Normal file
57
pkg/util/filter.go
Normal 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
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user