Add ACME store

Signed-off-by: Emile Vauge <emile@vauge.com>
This commit is contained in:
Emile Vauge 2016-08-18 14:20:11 +02:00
parent bea5ad3f13
commit a42845502e
No known key found for this signature in database
GPG key ID: D808B4C167352E59
30 changed files with 781 additions and 374 deletions

176
acme/account.go Normal file
View file

@ -0,0 +1,176 @@
package acme
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"errors"
"github.com/containous/traefik/log"
"github.com/xenolf/lego/acme"
"reflect"
"sync"
"time"
)
// Account is used to store lets encrypt registration info
type Account struct {
Email string
Registration *acme.RegistrationResource
PrivateKey []byte
DomainsCertificate DomainsCertificates
ChallengeCerts map[string][]byte
}
// Init inits acccount struct
func (a *Account) Init() error {
err := a.DomainsCertificate.Init()
if err != nil {
return err
}
return nil
}
func NewAccount(email string) (*Account, error) {
// Create a user. New accounts need an email and private key to start
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return nil, err
}
domainsCerts := DomainsCertificates{Certs: []*DomainsCertificate{}}
domainsCerts.Init()
return &Account{
Email: email,
PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey),
DomainsCertificate: domainsCerts,
ChallengeCerts: map[string][]byte{}}, nil
}
// GetEmail returns email
func (a Account) GetEmail() string {
return a.Email
}
// GetRegistration returns lets encrypt registration resource
func (a Account) GetRegistration() *acme.RegistrationResource {
return a.Registration
}
// GetPrivateKey returns private key
func (a Account) GetPrivateKey() crypto.PrivateKey {
if privateKey, err := x509.ParsePKCS1PrivateKey(a.PrivateKey); err == nil {
return privateKey
}
log.Errorf("Cannot unmarshall private key %+v", a.PrivateKey)
return nil
}
// Certificate is used to store certificate info
type Certificate struct {
Domain string
CertURL string
CertStableURL string
PrivateKey []byte
Certificate []byte
}
// DomainsCertificates stores a certificate for multiple domains
type DomainsCertificates struct {
Certs []*DomainsCertificate
lock sync.RWMutex
}
func (dc *DomainsCertificates) Init() error {
dc.lock.Lock()
defer dc.lock.Unlock()
for _, domainsCertificate := range dc.Certs {
tlsCert, err := tls.X509KeyPair(domainsCertificate.Certificate.Certificate, domainsCertificate.Certificate.PrivateKey)
if err != nil {
return err
}
domainsCertificate.tlsCert = &tlsCert
}
return nil
}
func (dc *DomainsCertificates) renewCertificates(acmeCert *Certificate, domain Domain) error {
dc.lock.Lock()
defer dc.lock.Unlock()
for _, domainsCertificate := range dc.Certs {
if reflect.DeepEqual(domain, domainsCertificate.Domains) {
tlsCert, err := tls.X509KeyPair(acmeCert.Certificate, acmeCert.PrivateKey)
if err != nil {
return err
}
domainsCertificate.Certificate = acmeCert
domainsCertificate.tlsCert = &tlsCert
return nil
}
}
return errors.New("Certificate to renew not found for domain " + domain.Main)
}
func (dc *DomainsCertificates) addCertificateForDomains(acmeCert *Certificate, domain Domain) (*DomainsCertificate, error) {
dc.lock.Lock()
defer dc.lock.Unlock()
tlsCert, err := tls.X509KeyPair(acmeCert.Certificate, acmeCert.PrivateKey)
if err != nil {
return nil, err
}
cert := DomainsCertificate{Domains: domain, Certificate: acmeCert, tlsCert: &tlsCert}
dc.Certs = append(dc.Certs, &cert)
return &cert, nil
}
func (dc *DomainsCertificates) getCertificateForDomain(domainToFind string) (*DomainsCertificate, bool) {
dc.lock.RLock()
defer dc.lock.RUnlock()
for _, domainsCertificate := range dc.Certs {
domains := []string{}
domains = append(domains, domainsCertificate.Domains.Main)
domains = append(domains, domainsCertificate.Domains.SANs...)
for _, domain := range domains {
if domain == domainToFind {
return domainsCertificate, true
}
}
}
return nil, false
}
func (dc *DomainsCertificates) exists(domainToFind Domain) (*DomainsCertificate, bool) {
dc.lock.RLock()
defer dc.lock.RUnlock()
for _, domainsCertificate := range dc.Certs {
if reflect.DeepEqual(domainToFind, domainsCertificate.Domains) {
return domainsCertificate, true
}
}
return nil, false
}
// DomainsCertificate contains a certificate for multiple domains
type DomainsCertificate struct {
Domains Domain
Certificate *Certificate
tlsCert *tls.Certificate
}
func (dc *DomainsCertificate) needRenew() bool {
for _, c := range dc.tlsCert.Certificate {
crt, err := x509.ParseCertificate(c)
if err != nil {
// If there's an error, we assume the cert is broken, and needs update
return true
}
// <= 7 days left, renew certificate
if crt.NotAfter.Before(time.Now().Add(time.Duration(24 * 7 * time.Hour))) {
return true
}
}
return false
}

View file

@ -1,179 +1,36 @@
package acme package acme
import ( import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/tls" "crypto/tls"
"crypto/x509"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/containous/staert"
"github.com/containous/traefik/cluster"
"github.com/containous/traefik/log"
"github.com/containous/traefik/safe"
"github.com/xenolf/lego/acme"
"golang.org/x/net/context"
"io/ioutil" "io/ioutil"
fmtlog "log" fmtlog "log"
"os" "os"
"reflect"
"strings" "strings"
"sync"
"time" "time"
log "github.com/Sirupsen/logrus"
"github.com/containous/traefik/safe"
"github.com/xenolf/lego/acme"
) )
// Account is used to store lets encrypt registration info
type Account struct {
Email string
Registration *acme.RegistrationResource
PrivateKey []byte
DomainsCertificate DomainsCertificates
}
// GetEmail returns email
func (a Account) GetEmail() string {
return a.Email
}
// GetRegistration returns lets encrypt registration resource
func (a Account) GetRegistration() *acme.RegistrationResource {
return a.Registration
}
// GetPrivateKey returns private key
func (a Account) GetPrivateKey() crypto.PrivateKey {
if privateKey, err := x509.ParsePKCS1PrivateKey(a.PrivateKey); err == nil {
return privateKey
}
log.Errorf("Cannot unmarshall private key %+v", a.PrivateKey)
return nil
}
// Certificate is used to store certificate info
type Certificate struct {
Domain string
CertURL string
CertStableURL string
PrivateKey []byte
Certificate []byte
}
// DomainsCertificates stores a certificate for multiple domains
type DomainsCertificates struct {
Certs []*DomainsCertificate
lock *sync.RWMutex
}
func (dc *DomainsCertificates) init() error {
if dc.lock == nil {
dc.lock = &sync.RWMutex{}
}
dc.lock.Lock()
defer dc.lock.Unlock()
for _, domainsCertificate := range dc.Certs {
tlsCert, err := tls.X509KeyPair(domainsCertificate.Certificate.Certificate, domainsCertificate.Certificate.PrivateKey)
if err != nil {
return err
}
domainsCertificate.tlsCert = &tlsCert
}
return nil
}
func (dc *DomainsCertificates) renewCertificates(acmeCert *Certificate, domain Domain) error {
dc.lock.Lock()
defer dc.lock.Unlock()
for _, domainsCertificate := range dc.Certs {
if reflect.DeepEqual(domain, domainsCertificate.Domains) {
tlsCert, err := tls.X509KeyPair(acmeCert.Certificate, acmeCert.PrivateKey)
if err != nil {
return err
}
domainsCertificate.Certificate = acmeCert
domainsCertificate.tlsCert = &tlsCert
return nil
}
}
return errors.New("Certificate to renew not found for domain " + domain.Main)
}
func (dc *DomainsCertificates) addCertificateForDomains(acmeCert *Certificate, domain Domain) (*DomainsCertificate, error) {
dc.lock.Lock()
defer dc.lock.Unlock()
tlsCert, err := tls.X509KeyPair(acmeCert.Certificate, acmeCert.PrivateKey)
if err != nil {
return nil, err
}
cert := DomainsCertificate{Domains: domain, Certificate: acmeCert, tlsCert: &tlsCert}
dc.Certs = append(dc.Certs, &cert)
return &cert, nil
}
func (dc *DomainsCertificates) getCertificateForDomain(domainToFind string) (*DomainsCertificate, bool) {
dc.lock.RLock()
defer dc.lock.RUnlock()
for _, domainsCertificate := range dc.Certs {
domains := []string{}
domains = append(domains, domainsCertificate.Domains.Main)
domains = append(domains, domainsCertificate.Domains.SANs...)
for _, domain := range domains {
if domain == domainToFind {
return domainsCertificate, true
}
}
}
return nil, false
}
func (dc *DomainsCertificates) exists(domainToFind Domain) (*DomainsCertificate, bool) {
dc.lock.RLock()
defer dc.lock.RUnlock()
for _, domainsCertificate := range dc.Certs {
if reflect.DeepEqual(domainToFind, domainsCertificate.Domains) {
return domainsCertificate, true
}
}
return nil, false
}
// DomainsCertificate contains a certificate for multiple domains
type DomainsCertificate struct {
Domains Domain
Certificate *Certificate
tlsCert *tls.Certificate
}
func (dc *DomainsCertificate) needRenew() bool {
for _, c := range dc.tlsCert.Certificate {
crt, err := x509.ParseCertificate(c)
if err != nil {
// If there's an error, we assume the cert is broken, and needs update
return true
}
// <= 30 days left, renew certificate
if crt.NotAfter.Before(time.Now().Add(time.Duration(24 * 30 * time.Hour))) {
return true
}
}
return false
}
// ACME allows to connect to lets encrypt and retrieve certs // ACME allows to connect to lets encrypt and retrieve certs
type ACME struct { type ACME struct {
Email string `description:"Email address used for registration"` Email string `description:"Email address used for registration"`
Domains []Domain `description:"SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='main.net,san1.net,san2.net'"` Domains []Domain `description:"SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='main.net,san1.net,san2.net'"`
StorageFile string `description:"File used for certificates storage."` Storage string `description:"File or key used for certificates storage."`
OnDemand bool `description:"Enable on demand certificate. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."` OnDemand bool `description:"Enable on demand certificate. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."`
OnHostRule bool `description:"Enable certificate generation on frontends Host rules."` OnHostRule bool `description:"Enable certificate generation on frontends Host rules."`
CAServer string `description:"CA server to use."` CAServer string `description:"CA server to use."`
EntryPoint string `description:"Entrypoint to proxy acme challenge to."` EntryPoint string `description:"Entrypoint to proxy acme challenge to."`
storageLock sync.RWMutex
client *acme.Client client *acme.Client
account *Account
defaultCertificate *tls.Certificate defaultCertificate *tls.Certificate
store cluster.Store
challengeProvider *challengeProvider
checkOnDemandDomain func(domain string) bool
} }
//Domains parse []Domain //Domains parse []Domain
@ -218,11 +75,7 @@ type Domain struct {
} }
func (a *ACME) init() error { func (a *ACME) init() error {
if len(a.Store) == 0 {
a.Store = a.StorageFile
}
acme.Logger = fmtlog.New(ioutil.Discard, "", 0) acme.Logger = fmtlog.New(ioutil.Discard, "", 0)
log.Debugf("Generating default certificate...")
// no certificates in TLS config, so we add a default one // no certificates in TLS config, so we add a default one
cert, err := generateDefaultCertificate() cert, err := generateDefaultCertificate()
if err != nil { if err != nil {
@ -232,78 +85,174 @@ func (a *ACME) init() error {
return nil return nil
} }
// CreateClusterConfig creates a tls.config from using ACME configuration // CreateClusterConfig creates a tls.config using ACME configuration in cluster mode
func (a *ACME) CreateClusterConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(domain string) bool) error { func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tls.Config, checkOnDemandDomain func(domain string) bool) error {
err := a.init() err := a.init()
if err != nil { if err != nil {
return err return err
} }
if len(a.Store) == 0 { if len(a.Storage) == 0 {
return errors.New("Empty Store, please provide a filename/key for certs storage") return errors.New("Empty Store, please provide a key for certs storage")
} }
a.checkOnDemandDomain = checkOnDemandDomain
tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate) tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate)
tlsConfig.GetCertificate = a.getCertificate
datastore, err := cluster.NewDataStore(
staert.KvSource{
Store: leadership.Store,
Prefix: leadership.Store.Prefix + "/acme/account",
},
leadership.Pool.Ctx(), &Account{},
func(object cluster.Object) error {
account := object.(*Account)
account.Init()
if !leadership.IsLeader() {
a.client, err = a.buildACMEClient(account)
if err != nil {
log.Errorf("Error building ACME client %+v: %s", object, err.Error())
}
}
return nil
})
if err != nil {
return err
}
a.store = datastore
a.challengeProvider = newMemoryChallengeProvider(a.store)
ticker := time.NewTicker(24 * time.Hour)
leadership.Pool.AddGoCtx(func(ctx context.Context) {
log.Infof("Starting ACME renew job...")
defer log.Infof("Stopped ACME renew job...")
select {
case <-ctx.Done():
return
case <-ticker.C:
if err := a.renewCertificates(); err != nil {
log.Errorf("Error renewing ACME certificate: %s", err.Error())
}
}
})
leadership.AddListener(func(elected bool) error {
if elected {
object, err := a.store.Load()
if err != nil {
return err
}
transaction, object, err := a.store.Begin()
if err != nil {
return err
}
account := object.(*Account)
account.Init()
var needRegister bool
if account == nil || len(account.Email) == 0 {
account, err = NewAccount(a.Email)
if err != nil {
return err
}
needRegister = true
}
if err != nil {
return err
}
log.Debugf("buildACMEClient...")
a.client, err = a.buildACMEClient(account)
if err != nil {
return err
}
if needRegister {
// New users will need to register; be sure to save it
log.Debugf("Register...")
reg, err := a.client.Register()
if err != nil {
return err
}
account.Registration = reg
}
// The client has a URL to the current Let's Encrypt Subscriber
// Agreement. The user will need to agree to it.
log.Debugf("AgreeToTOS...")
err = a.client.AgreeToTOS()
if err != nil {
return err
}
err = transaction.Commit(account)
if err != nil {
return err
}
safe.Go(func() {
a.retrieveCertificates()
if err := a.renewCertificates(); err != nil {
log.Errorf("Error renewing ACME certificate %+v: %s", account, err.Error())
}
})
}
return nil
})
return nil return nil
} }
// CreateLocalConfig creates a tls.config from using ACME configuration // CreateLocalConfig creates a tls.config using local ACME configuration
func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(domain string) bool) error { func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, checkOnDemandDomain func(domain string) bool) error {
err := a.init() err := a.init()
if err != nil { if err != nil {
return err return err
} }
if len(a.Store) == 0 { if len(a.Storage) == 0 {
return errors.New("Empty Store, please provide a filename/key for certs storage") return errors.New("Empty Store, please provide a filename for certs storage")
} }
a.checkOnDemandDomain = checkOnDemandDomain
tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate) tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate)
tlsConfig.GetCertificate = a.getCertificate
localStore := NewLocalStore(a.Storage)
a.store = localStore
a.challengeProvider = newMemoryChallengeProvider(a.store)
var needRegister bool var needRegister bool
var err error var account *Account
// if certificates in storage, load them if fileInfo, fileErr := os.Stat(a.Storage); fileErr == nil && fileInfo.Size() != 0 {
if fileInfo, fileErr := os.Stat(a.Store); fileErr == nil && fileInfo.Size() != 0 { log.Infof("Loading ACME Account...")
log.Infof("Loading ACME certificates...")
// load account // load account
a.account, err = a.loadAccount(a) object, err := localStore.Load()
if err != nil { if err != nil {
return err return err
} }
account = object.(*Account)
} else { } else {
log.Infof("Generating ACME Account...") log.Infof("Generating ACME Account...")
// Create a user. New accounts need an email and private key to start account, err = NewAccount(a.Email)
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil { if err != nil {
return err return err
} }
a.account = &Account{
Email: a.Email,
PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey),
}
a.account.DomainsCertificate = DomainsCertificates{Certs: []*DomainsCertificate{}, lock: &sync.RWMutex{}}
needRegister = true needRegister = true
} }
a.client, err = a.buildACMEClient() log.Infof("buildACMEClient...")
if err != nil { a.client, err = a.buildACMEClient(account)
return err
}
a.client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01})
wrapperChallengeProvider := newWrapperChallengeProvider()
err = client.SetChallengeProvider(acme.TLSSNI01, wrapperChallengeProvider)
if err != nil { if err != nil {
return err return err
} }
if needRegister { if needRegister {
// New users will need to register; be sure to save it // New users will need to register; be sure to save it
log.Infof("Register...")
reg, err := a.client.Register() reg, err := a.client.Register()
if err != nil { if err != nil {
return err return err
} }
a.account.Registration = reg account.Registration = reg
} }
// The client has a URL to the current Let's Encrypt Subscriber // The client has a URL to the current Let's Encrypt Subscriber
// Agreement. The user will need to agree to it. // Agreement. The user will need to agree to it.
log.Infof("AgreeToTOS...")
err = a.client.AgreeToTOS() err = a.client.AgreeToTOS()
if err != nil { if err != nil {
// Let's Encrypt Subscriber Agreement renew ? // Let's Encrypt Subscriber Agreement renew ?
@ -311,45 +260,33 @@ func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, CheckOnDemandDomain func
if err != nil { if err != nil {
return err return err
} }
a.account.Registration = reg account.Registration = reg
err = a.client.AgreeToTOS() err = a.client.AgreeToTOS()
if err != nil { if err != nil {
log.Errorf("Error sending ACME agreement to TOS: %+v: %s", a.account, err.Error()) log.Errorf("Error sending ACME agreement to TOS: %+v: %s", account, err.Error())
} }
} }
// save account // save account
err = a.saveAccount() transaction, _, err := a.store.Begin()
if err != nil {
return err
}
err = transaction.Commit(account)
if err != nil { if err != nil {
return err return err
} }
safe.Go(func() { safe.Go(func() {
a.retrieveCertificates(a.client) a.retrieveCertificates()
if err := a.renewCertificates(a.client); err != nil { if err := a.renewCertificates(); err != nil {
log.Errorf("Error renewing ACME certificate %+v: %s", a.account, err.Error()) log.Errorf("Error renewing ACME certificate %+v: %s", account, err.Error())
} }
}) })
tlsConfig.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
if challengeCert, ok := wrapperChallengeProvider.getCertificate(clientHello.ServerName); ok {
return challengeCert, nil
}
if domainCert, ok := a.account.DomainsCertificate.getCertificateForDomain(clientHello.ServerName); ok {
return domainCert.tlsCert, nil
}
if a.OnDemand {
if CheckOnDemandDomain != nil && !CheckOnDemandDomain(clientHello.ServerName) {
return nil, nil
}
return a.loadCertificateOnDemand(clientHello)
}
return nil, nil
}
ticker := time.NewTicker(24 * time.Hour) ticker := time.NewTicker(24 * time.Hour)
safe.Go(func() { safe.Go(func() {
for range ticker.C { for range ticker.C {
if err := a.renewCertificates(client, account); err != nil { if err := a.renewCertificates(); err != nil {
log.Errorf("Error renewing ACME certificate %+v: %s", account, err.Error()) log.Errorf("Error renewing ACME certificate %+v: %s", account, err.Error())
} }
} }
@ -358,26 +295,54 @@ func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, CheckOnDemandDomain func
return nil return nil
} }
func (a *ACME) retrieveCertificates(client *acme.Client) { func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
account := a.store.Get().(*Account)
if challengeCert, ok := a.challengeProvider.getCertificate(clientHello.ServerName); ok {
log.Debugf("ACME got challenge %s", clientHello.ServerName)
return challengeCert, nil
}
if domainCert, ok := account.DomainsCertificate.getCertificateForDomain(clientHello.ServerName); ok {
log.Debugf("ACME got domaincert %s", clientHello.ServerName)
return domainCert.tlsCert, nil
}
if a.OnDemand {
if a.checkOnDemandDomain != nil && !a.checkOnDemandDomain(clientHello.ServerName) {
return nil, nil
}
return a.loadCertificateOnDemand(clientHello)
}
log.Debugf("ACME got nothing %s", clientHello.ServerName)
return nil, nil
}
func (a *ACME) retrieveCertificates() {
log.Infof("Retrieving ACME certificates...") log.Infof("Retrieving ACME certificates...")
for _, domain := range a.Domains { for _, domain := range a.Domains {
// check if cert isn't already loaded // check if cert isn't already loaded
if _, exists := a.account.DomainsCertificate.exists(domain); !exists { account := a.store.Get().(*Account)
if _, exists := account.DomainsCertificate.exists(domain); !exists {
domains := []string{} domains := []string{}
domains = append(domains, domain.Main) domains = append(domains, domain.Main)
domains = append(domains, domain.SANs...) domains = append(domains, domain.SANs...)
certificateResource, err := a.getDomainsCertificates(client, domains) certificateResource, err := a.getDomainsCertificates(domains)
if err != nil { if err != nil {
log.Errorf("Error getting ACME certificate for domain %s: %s", domains, err.Error()) log.Errorf("Error getting ACME certificate for domain %s: %s", domains, err.Error())
continue continue
} }
_, err = a.account.DomainsCertificate.addCertificateForDomains(certificateResource, domain) transaction, object, err := a.store.Begin()
if err != nil {
log.Errorf("Error creating ACME store transaction from domain %s: %s", domain, err.Error())
continue
}
account = object.(*Account)
_, err = account.DomainsCertificate.addCertificateForDomains(certificateResource, domain)
if err != nil { if err != nil {
log.Errorf("Error adding ACME certificate for domain %s: %s", domains, err.Error()) log.Errorf("Error adding ACME certificate for domain %s: %s", domains, err.Error())
continue continue
} }
if err = a.saveAccount(); err != nil {
log.Errorf("Error Saving ACME account %+v: %s", a.account, err.Error()) if err = transaction.Commit(account); err != nil {
log.Errorf("Error Saving ACME account %+v: %s", account, err.Error())
continue continue
} }
} }
@ -385,12 +350,18 @@ func (a *ACME) retrieveCertificates(client *acme.Client) {
log.Infof("Retrieved ACME certificates") log.Infof("Retrieved ACME certificates")
} }
func (a *ACME) renewCertificates(client *acme.Client) error { func (a *ACME) renewCertificates() error {
log.Debugf("Testing certificate renew...") log.Debugf("Testing certificate renew...")
for _, certificateResource := range a.account.DomainsCertificate.Certs { account := a.store.Get().(*Account)
for _, certificateResource := range account.DomainsCertificate.Certs {
if certificateResource.needRenew() { if certificateResource.needRenew() {
transaction, object, err := a.store.Begin()
if err != nil {
return err
}
account = object.(*Account)
log.Debugf("Renewing certificate %+v", certificateResource.Domains) log.Debugf("Renewing certificate %+v", certificateResource.Domains)
renewedCert, err := client.RenewCertificate(acme.CertificateResource{ renewedCert, err := a.client.RenewCertificate(acme.CertificateResource{
Domain: certificateResource.Certificate.Domain, Domain: certificateResource.Certificate.Domain,
CertURL: certificateResource.Certificate.CertURL, CertURL: certificateResource.Certificate.CertURL,
CertStableURL: certificateResource.Certificate.CertStableURL, CertStableURL: certificateResource.Certificate.CertStableURL,
@ -409,13 +380,14 @@ func (a *ACME) renewCertificates(client *acme.Client) error {
PrivateKey: renewedCert.PrivateKey, PrivateKey: renewedCert.PrivateKey,
Certificate: renewedCert.Certificate, Certificate: renewedCert.Certificate,
} }
err = a.account.DomainsCertificate.renewCertificates(renewedACMECert, certificateResource.Domains) err = account.DomainsCertificate.renewCertificates(renewedACMECert, certificateResource.Domains)
if err != nil { if err != nil {
log.Errorf("Error renewing certificate: %v", err) log.Errorf("Error renewing certificate: %v", err)
continue continue
} }
if err = a.saveAccount(); err != nil {
log.Errorf("Error saving ACME account: %v", err) if err = transaction.Commit(account); err != nil {
log.Errorf("Error Saving ACME account %+v: %s", account, err.Error())
continue continue
} }
} }
@ -423,33 +395,45 @@ func (a *ACME) renewCertificates(client *acme.Client) error {
return nil return nil
} }
func (a *ACME) buildACMEClient() (*acme.Client, error) { func (a *ACME) buildACMEClient(account *Account) (*acme.Client, error) {
log.Debugf("Building ACME client...")
caServer := "https://acme-v01.api.letsencrypt.org/directory" caServer := "https://acme-v01.api.letsencrypt.org/directory"
if len(a.CAServer) > 0 { if len(a.CAServer) > 0 {
caServer = a.CAServer caServer = a.CAServer
} }
client, err := acme.NewClient(caServer, a.account, acme.RSA4096) client, err := acme.NewClient(caServer, account, acme.RSA4096)
if err != nil {
return nil, err
}
client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01})
err = client.SetChallengeProvider(acme.TLSSNI01, a.challengeProvider)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return client, nil return client, nil
} }
func (a *ACME) loadCertificateOnDemand(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { func (a *ACME) loadCertificateOnDemand(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
if certificateResource, ok := a.account.DomainsCertificate.getCertificateForDomain(clientHello.ServerName); ok { account := a.store.Get().(*Account)
if certificateResource, ok := account.DomainsCertificate.getCertificateForDomain(clientHello.ServerName); ok {
return certificateResource.tlsCert, nil return certificateResource.tlsCert, nil
} }
certificate, err := a.getDomainsCertificates(a.client, []string{clientHello.ServerName}) certificate, err := a.getDomainsCertificates([]string{clientHello.ServerName})
if err != nil { if err != nil {
return nil, err return nil, err
} }
log.Debugf("Got certificate on demand for domain %s", clientHello.ServerName) log.Debugf("Got certificate on demand for domain %s", clientHello.ServerName)
cert, err := a.account.DomainsCertificate.addCertificateForDomains(certificate, Domain{Main: clientHello.ServerName})
transaction, object, err := a.store.Begin()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err = a.saveAccount(); err != nil { account = object.(*Account)
cert, err := account.DomainsCertificate.addCertificateForDomains(certificate, Domain{Main: clientHello.ServerName})
if err != nil {
return nil, err
}
if err = transaction.Commit(account); err != nil {
return nil, err return nil, err
} }
return cert.tlsCert, nil return cert.tlsCert, nil
@ -458,6 +442,7 @@ func (a *ACME) loadCertificateOnDemand(clientHello *tls.ClientHelloInfo) (*tls.C
// LoadCertificateForDomains loads certificates from ACME for given domains // LoadCertificateForDomains loads certificates from ACME for given domains
func (a *ACME) LoadCertificateForDomains(domains []string) { func (a *ACME) LoadCertificateForDomains(domains []string) {
safe.Go(func() { safe.Go(func() {
account := a.store.Get().(*Account)
var domain Domain var domain Domain
if len(domains) == 0 { if len(domains) == 0 {
// no domain // no domain
@ -468,64 +453,39 @@ func (a *ACME) LoadCertificateForDomains(domains []string) {
} else { } else {
domain = Domain{Main: domains[0]} domain = Domain{Main: domains[0]}
} }
if _, exists := a.account.DomainsCertificate.exists(domain); exists { if _, exists := account.DomainsCertificate.exists(domain); exists {
// domain already exists // domain already exists
return return
} }
certificate, err := a.getDomainsCertificates(a.client, domains) certificate, err := a.getDomainsCertificates(domains)
if err != nil { if err != nil {
log.Errorf("Error getting ACME certificates %+v : %v", domains, err) log.Errorf("Error getting ACME certificates %+v : %v", domains, err)
return return
} }
log.Debugf("Got certificate for domains %+v", domains) log.Debugf("Got certificate for domains %+v", domains)
_, err = a.account.DomainsCertificate.addCertificateForDomains(certificate, domain) transaction, object, err := a.store.Begin()
if err != nil {
log.Errorf("Error creating transaction %+v : %v", domains, err)
return
}
account = object.(*Account)
_, err = account.DomainsCertificate.addCertificateForDomains(certificate, domain)
if err != nil { if err != nil {
log.Errorf("Error adding ACME certificates %+v : %v", domains, err) log.Errorf("Error adding ACME certificates %+v : %v", domains, err)
return return
} }
if err = a.saveAccount(); err != nil { if err = transaction.Commit(account); err != nil {
log.Errorf("Error Saving ACME account %+v: %v", a.account, err) log.Errorf("Error Saving ACME account %+v: %v", account, err)
return return
} }
}) })
} }
func (a *ACME) loadAccount(acmeConfig *ACME) (*Account, error) { func (a *ACME) getDomainsCertificates(domains []string) (*Certificate, error) {
a.storageLock.RLock()
defer a.storageLock.RUnlock()
account := Account{
DomainsCertificate: DomainsCertificates{},
}
file, err := ioutil.ReadFile(acmeConfig.Store)
if err != nil {
return nil, err
}
if err := json.Unmarshal(file, &account); err != nil {
return nil, err
}
err = account.DomainsCertificate.init()
if err != nil {
return nil, err
}
log.Infof("Loaded ACME config from store %s", acmeConfig.Store)
return &account, nil
}
func (a *ACME) saveAccount() error {
a.storageLock.Lock()
defer a.storageLock.Unlock()
// write account to file
data, err := json.MarshalIndent(a.account, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(a.Store, data, 0600)
}
func (a *ACME) getDomainsCertificates(client *acme.Client, domains []string) (*Certificate, error) {
log.Debugf("Loading ACME certificates %s...", domains) log.Debugf("Loading ACME certificates %s...", domains)
bundle := true bundle := true
certificate, failures := client.ObtainCertificate(domains, bundle, nil) certificate, failures := a.client.ObtainCertificate(domains, bundle, nil)
if len(failures) > 0 { if len(failures) > 0 {
log.Error(failures) log.Error(failures)
return nil, fmt.Errorf("Cannot obtain certificates %s+v", failures) return nil, fmt.Errorf("Cannot obtain certificates %s+v", failures)

View file

@ -4,33 +4,59 @@ import (
"crypto/tls" "crypto/tls"
"sync" "sync"
"bytes"
"crypto/rsa"
"crypto/x509" "crypto/x509"
"encoding/gob"
"github.com/containous/traefik/cluster"
"github.com/containous/traefik/log"
"github.com/xenolf/lego/acme" "github.com/xenolf/lego/acme"
"time"
) )
var _ acme.ChallengeProvider = (*inMemoryChallengeProvider)(nil) func init() {
gob.Register(rsa.PrivateKey{})
gob.Register(rsa.PublicKey{})
}
type inMemoryChallengeProvider struct { var _ acme.ChallengeProviderTimeout = (*challengeProvider)(nil)
challengeCerts map[string]*tls.Certificate
type challengeProvider struct {
store cluster.Store
lock sync.RWMutex lock sync.RWMutex
} }
func newWrapperChallengeProvider() *inMemoryChallengeProvider { func newMemoryChallengeProvider(store cluster.Store) *challengeProvider {
return &inMemoryChallengeProvider{ return &challengeProvider{
challengeCerts: map[string]*tls.Certificate{}, store: store,
} }
} }
func (c *inMemoryChallengeProvider) getCertificate(domain string) (cert *tls.Certificate, exists bool) { func (c *challengeProvider) getCertificate(domain string) (cert *tls.Certificate, exists bool) {
log.Debugf("Challenge GetCertificate %s", domain)
c.lock.RLock() c.lock.RLock()
defer c.lock.RUnlock() defer c.lock.RUnlock()
if cert, ok := c.challengeCerts[domain]; ok { account := c.store.Get().(*Account)
if account.ChallengeCerts == nil {
return nil, false
}
if certBinary, ok := account.ChallengeCerts[domain]; ok {
cert := &tls.Certificate{}
var buffer bytes.Buffer
buffer.Write(certBinary)
dec := gob.NewDecoder(&buffer)
err := dec.Decode(cert)
if err != nil {
log.Errorf("Error unmarshaling challenge cert %s", err.Error())
return nil, false
}
return cert, true return cert, true
} }
return nil, false return nil, false
} }
func (c *inMemoryChallengeProvider) Present(domain, token, keyAuth string) error { func (c *challengeProvider) Present(domain, token, keyAuth string) error {
log.Debugf("Challenge Present %s", domain)
cert, _, err := acme.TLSSNI01ChallengeCert(keyAuth) cert, _, err := acme.TLSSNI01ChallengeCert(keyAuth)
if err != nil { if err != nil {
return err return err
@ -42,16 +68,40 @@ func (c *inMemoryChallengeProvider) Present(domain, token, keyAuth string) error
c.lock.Lock() c.lock.Lock()
defer c.lock.Unlock() defer c.lock.Unlock()
for i := range cert.Leaf.DNSNames { transaction, object, err := c.store.Begin()
c.challengeCerts[cert.Leaf.DNSNames[i]] = &cert if err != nil {
return err
} }
account := object.(*Account)
return nil if account.ChallengeCerts == nil {
account.ChallengeCerts = map[string][]byte{}
}
for i := range cert.Leaf.DNSNames {
var buffer bytes.Buffer
enc := gob.NewEncoder(&buffer)
err := enc.Encode(cert)
if err != nil {
return err
}
account.ChallengeCerts[cert.Leaf.DNSNames[i]] = buffer.Bytes()
log.Debugf("Challenge Present cert: %s", cert.Leaf.DNSNames[i])
}
return transaction.Commit(account)
} }
func (c *inMemoryChallengeProvider) CleanUp(domain, token, keyAuth string) error { func (c *challengeProvider) CleanUp(domain, token, keyAuth string) error {
log.Debugf("Challenge CleanUp %s", domain)
c.lock.Lock() c.lock.Lock()
defer c.lock.Unlock() defer c.lock.Unlock()
delete(c.challengeCerts, domain) transaction, object, err := c.store.Begin()
return nil if err != nil {
return err
}
account := object.(*Account)
delete(account.ChallengeCerts, domain)
return transaction.Commit(account)
}
func (c *challengeProvider) Timeout() (timeout, interval time.Duration) {
return 60 * time.Second, 5 * time.Second
} }

97
acme/localStore.go Normal file
View file

@ -0,0 +1,97 @@
package acme
import (
"encoding/json"
"fmt"
"github.com/containous/traefik/cluster"
"github.com/containous/traefik/log"
"io/ioutil"
"sync"
)
var _ cluster.Store = (*LocalStore)(nil)
// LocalStore is a store using a file as storage
type LocalStore struct {
file string
storageLock sync.RWMutex
account *Account
}
// NewLocalStore create a LocalStore
func NewLocalStore(file string) *LocalStore {
return &LocalStore{
file: file,
storageLock: sync.RWMutex{},
}
}
// Get atomically a struct from the file storage
func (s *LocalStore) Get() cluster.Object {
s.storageLock.RLock()
defer s.storageLock.RUnlock()
return s.account
}
// Load loads file into store
func (s *LocalStore) Load() (cluster.Object, error) {
s.storageLock.Lock()
defer s.storageLock.Unlock()
account := &Account{}
file, err := ioutil.ReadFile(s.file)
if err != nil {
return nil, err
}
if err := json.Unmarshal(file, &account); err != nil {
return nil, err
}
account.Init()
s.account = account
log.Infof("Loaded ACME config from store %s", s.file)
return account, nil
}
// func (s *LocalStore) saveAccount(account *Account) error {
// s.storageLock.Lock()
// defer s.storageLock.Unlock()
// // write account to file
// data, err := json.MarshalIndent(account, "", " ")
// if err != nil {
// return err
// }
// return ioutil.WriteFile(s.file, data, 0644)
// }
// Begin creates a transaction with the KV store.
func (s *LocalStore) Begin() (cluster.Transaction, cluster.Object, error) {
s.storageLock.Lock()
return &localTransaction{LocalStore: s}, s.account, nil
}
var _ cluster.Transaction = (*localTransaction)(nil)
type localTransaction struct {
*LocalStore
dirty bool
}
// Commit allows to set an object in the file storage
func (t *localTransaction) Commit(object cluster.Object) error {
t.LocalStore.account = object.(*Account)
defer t.storageLock.Unlock()
if t.dirty {
return fmt.Errorf("Transaction already used. Please begin a new one.")
}
// write account to file
data, err := json.MarshalIndent(object, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(t.file, data, 0644)
if err != nil {
return err
}
t.dirty = true
return nil
}

View file

@ -6,7 +6,7 @@ package main
import ( import (
"net/http" "net/http"
log "github.com/Sirupsen/logrus" "github.com/containous/traefik/log"
) )
// OxyLogger implements oxy Logger interface with logrus. // OxyLogger implements oxy Logger interface with logrus.

View file

@ -1,44 +1,62 @@
package cluster package cluster
import ( import (
"encoding/json"
"fmt" "fmt"
log "github.com/Sirupsen/logrus"
"github.com/cenkalti/backoff"
"github.com/containous/staert" "github.com/containous/staert"
"github.com/containous/traefik/log"
"github.com/docker/libkv/store" "github.com/docker/libkv/store"
"github.com/emilevauge/backoff"
"github.com/satori/go.uuid" "github.com/satori/go.uuid"
"golang.org/x/net/context" "golang.org/x/net/context"
"sync" "sync"
"time" "time"
) )
// Object is the struct to store
type Object interface{}
// Metadata stores Object plus metadata // Metadata stores Object plus metadata
type Metadata struct { type Metadata struct {
object Object
Object []byte
Lock string Lock string
} }
func (m *Metadata) marshall() error {
var err error
m.Object, err = json.Marshal(m.object)
return err
}
func (m *Metadata) unmarshall() error {
if len(m.Object) == 0 {
return nil
}
return json.Unmarshal(m.Object, m.object)
}
// Listener is called when Object has been changed in KV store
type Listener func(Object) error
var _ Store = (*Datastore)(nil)
// Datastore holds a struct synced in a KV store // Datastore holds a struct synced in a KV store
type Datastore struct { type Datastore struct {
kv *staert.KvSource kv staert.KvSource
ctx context.Context ctx context.Context
localLock *sync.RWMutex localLock *sync.RWMutex
object Object
meta *Metadata meta *Metadata
lockKey string lockKey string
listener Listener
} }
// NewDataStore creates a Datastore // NewDataStore creates a Datastore
func NewDataStore(kvSource *staert.KvSource, ctx context.Context, object Object) (*Datastore, error) { func NewDataStore(kvSource staert.KvSource, ctx context.Context, object Object, listener Listener) (*Datastore, error) {
datastore := Datastore{ datastore := Datastore{
kv: kvSource, kv: kvSource,
ctx: ctx, ctx: ctx,
meta: &Metadata{}, meta: &Metadata{object: object},
object: object,
lockKey: kvSource.Prefix + "/lock", lockKey: kvSource.Prefix + "/lock",
localLock: &sync.RWMutex{}, localLock: &sync.RWMutex{},
listener: listener,
} }
err := datastore.watchChanges() err := datastore.watchChanges()
if err != nil { if err != nil {
@ -67,17 +85,24 @@ func (d *Datastore) watchChanges() error {
return err return err
} }
d.localLock.Lock() d.localLock.Lock()
err := d.kv.LoadConfig(d.object) err := d.kv.LoadConfig(d.meta)
if err != nil { if err != nil {
d.localLock.Unlock() d.localLock.Unlock()
return err return err
} }
err = d.kv.LoadConfig(d.meta) err = d.meta.unmarshall()
if err != nil { if err != nil {
d.localLock.Unlock() d.localLock.Unlock()
return err return err
} }
d.localLock.Unlock() d.localLock.Unlock()
// log.Debugf("Datastore object change received: %+v", d.object)
if d.listener != nil {
err := d.listener(d.meta.object)
if err != nil {
log.Errorf("Error calling datastore listener: %s", err)
}
}
} }
} }
} }
@ -93,11 +118,12 @@ func (d *Datastore) watchChanges() error {
} }
// Begin creates a transaction with the KV store. // Begin creates a transaction with the KV store.
func (d *Datastore) Begin() (*Transaction, error) { func (d *Datastore) Begin() (Transaction, Object, error) {
id := uuid.NewV4().String() id := uuid.NewV4().String()
log.Debugf("Transaction %s begins", id)
remoteLock, err := d.kv.NewLock(d.lockKey, &store.LockOptions{TTL: 20 * time.Second, Value: []byte(id)}) remoteLock, err := d.kv.NewLock(d.lockKey, &store.LockOptions{TTL: 20 * time.Second, Value: []byte(id)})
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
stopCh := make(chan struct{}) stopCh := make(chan struct{})
ctx, cancel := context.WithCancel(d.ctx) ctx, cancel := context.WithCancel(d.ctx)
@ -109,11 +135,11 @@ func (d *Datastore) Begin() (*Transaction, error) {
select { select {
case <-ctx.Done(): case <-ctx.Done():
if errLock != nil { if errLock != nil {
return nil, errLock return nil, nil, errLock
} }
case <-d.ctx.Done(): case <-d.ctx.Done():
stopCh <- struct{}{} stopCh <- struct{}{}
return nil, d.ctx.Err() return nil, nil, d.ctx.Err()
} }
// we got the lock! Now make sure we are synced with KV store // we got the lock! Now make sure we are synced with KV store
@ -131,14 +157,15 @@ func (d *Datastore) Begin() (*Transaction, error) {
ebo.MaxElapsedTime = 60 * time.Second ebo.MaxElapsedTime = 60 * time.Second
err = backoff.RetryNotify(operation, ebo, notify) err = backoff.RetryNotify(operation, ebo, notify)
if err != nil { if err != nil {
return nil, fmt.Errorf("Datastore cannot sync: %v", err) return nil, nil, fmt.Errorf("Datastore cannot sync: %v", err)
} }
// we synced with KV store, we can now return Setter // we synced with KV store, we can now return Setter
return &Transaction{ return &datastoreTransaction{
Datastore: d, Datastore: d,
remoteLock: remoteLock, remoteLock: remoteLock,
}, nil id: id,
}, d.meta.object, nil
} }
func (d *Datastore) get() *Metadata { func (d *Datastore) get() *Metadata {
@ -147,28 +174,50 @@ func (d *Datastore) get() *Metadata {
return d.meta return d.meta
} }
// Load load atomically a struct from the KV store
func (d *Datastore) Load() (Object, error) {
d.localLock.Lock()
defer d.localLock.Unlock()
err := d.kv.LoadConfig(d.meta)
if err != nil {
return nil, err
}
err = d.meta.unmarshall()
if err != nil {
return nil, err
}
return d.meta.object, nil
}
// Get atomically a struct from the KV store // Get atomically a struct from the KV store
func (d *Datastore) Get() Object { func (d *Datastore) Get() Object {
d.localLock.RLock() d.localLock.RLock()
defer d.localLock.RUnlock() defer d.localLock.RUnlock()
return d.object return d.meta.object
} }
// Transaction allows to set a struct in the KV store var _ Transaction = (*datastoreTransaction)(nil)
type Transaction struct {
type datastoreTransaction struct {
*Datastore *Datastore
remoteLock store.Locker remoteLock store.Locker
dirty bool dirty bool
id string
} }
// Commit allows to set an object in the KV store // Commit allows to set an object in the KV store
func (s *Transaction) Commit(object Object) error { func (s *datastoreTransaction) Commit(object Object) error {
s.localLock.Lock() s.localLock.Lock()
defer s.localLock.Unlock() defer s.localLock.Unlock()
if s.dirty { if s.dirty {
return fmt.Errorf("Transaction already used. Please begin a new one.") return fmt.Errorf("Transaction already used. Please begin a new one.")
} }
err := s.kv.StoreConfig(object) s.Datastore.meta.object = object
err := s.Datastore.meta.marshall()
if err != nil {
return err
}
err = s.kv.StoreConfig(s.Datastore.meta)
if err != nil { if err != nil {
return err return err
} }
@ -178,7 +227,8 @@ func (s *Transaction) Commit(object Object) error {
return err return err
} }
s.Datastore.object = object
s.dirty = true s.dirty = true
// log.Debugf("Datastore object saved: %+v", s.object)
log.Debugf("Transaction commited %s", s.id)
return nil return nil
} }

View file

@ -1,11 +1,11 @@
package cluster package cluster
import ( import (
log "github.com/Sirupsen/logrus" "github.com/containous/traefik/log"
"github.com/cenkalti/backoff"
"github.com/containous/traefik/safe" "github.com/containous/traefik/safe"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
"github.com/docker/leadership" "github.com/docker/leadership"
"github.com/emilevauge/backoff"
"golang.org/x/net/context" "golang.org/x/net/context"
"time" "time"
) )
@ -15,6 +15,8 @@ type Leadership struct {
*safe.Pool *safe.Pool
*types.Cluster *types.Cluster
candidate *leadership.Candidate candidate *leadership.Candidate
leader safe.Safe
listeners []LeaderListener
} }
// NewLeadership creates a leadership // NewLeadership creates a leadership
@ -23,9 +25,13 @@ func NewLeadership(ctx context.Context, cluster *types.Cluster) *Leadership {
Pool: safe.NewPool(ctx), Pool: safe.NewPool(ctx),
Cluster: cluster, Cluster: cluster,
candidate: leadership.NewCandidate(cluster.Store, cluster.Store.Prefix+"/leader", cluster.Node, 20*time.Second), candidate: leadership.NewCandidate(cluster.Store, cluster.Store.Prefix+"/leader", cluster.Node, 20*time.Second),
listeners: []LeaderListener{},
} }
} }
// LeaderListener is called when leadership has changed
type LeaderListener func(elected bool) error
// Participate tries to be a leader // Participate tries to be a leader
func (l *Leadership) Participate(pool *safe.Pool) { func (l *Leadership) Participate(pool *safe.Pool) {
pool.GoCtx(func(ctx context.Context) { pool.GoCtx(func(ctx context.Context) {
@ -46,10 +52,15 @@ func (l *Leadership) Participate(pool *safe.Pool) {
}) })
} }
// AddListener adds a leadership listerner
func (l *Leadership) AddListener(listener LeaderListener) {
l.listeners = append(l.listeners, listener)
}
// Resign resigns from being a leader // Resign resigns from being a leader
func (l *Leadership) Resign() { func (l *Leadership) Resign() {
l.candidate.Resign() l.candidate.Resign()
log.Infof("Node %s resined", l.Cluster.Node) log.Infof("Node %s resigned", l.Cluster.Node)
} }
func (l *Leadership) run(candidate *leadership.Candidate, ctx context.Context) error { func (l *Leadership) run(candidate *leadership.Candidate, ctx context.Context) error {
@ -70,9 +81,22 @@ func (l *Leadership) run(candidate *leadership.Candidate, ctx context.Context) e
func (l *Leadership) onElection(elected bool) { func (l *Leadership) onElection(elected bool) {
if elected { if elected {
log.Infof("Node %s elected leader ♚", l.Cluster.Node) log.Infof("Node %s elected leader ♚", l.Cluster.Node)
l.leader.Set(true)
l.Start() l.Start()
} else { } else {
log.Infof("Node %s elected slave ♝", l.Cluster.Node) log.Infof("Node %s elected slave ♝", l.Cluster.Node)
l.leader.Set(false)
l.Stop() l.Stop()
} }
for _, listener := range l.listeners {
err := listener(elected)
if err != nil {
log.Errorf("Error calling Leadership listener: %s", err)
}
}
}
// IsLeader returns true if current node is leader
func (l *Leadership) IsLeader() bool {
return l.leader.Get().(bool)
} }

16
cluster/store.go Normal file
View file

@ -0,0 +1,16 @@
package cluster
// Object is the struct to store
type Object interface{}
// Store is a generic interface to represents a storage
type Store interface {
Load() (Object, error)
Get() Object
Begin() (Transaction, Object, error)
}
// Transaction allows to set a struct in the KV store
type Transaction interface {
Commit(object Object) error
}

View file

@ -29,8 +29,8 @@ type GlobalConfiguration struct {
TraefikLogsFile string `description:"Traefik logs file"` TraefikLogsFile string `description:"Traefik logs file"`
LogLevel string `short:"l" description:"Log level"` LogLevel string `short:"l" description:"Log level"`
EntryPoints EntryPoints `description:"Entrypoints definition using format: --entryPoints='Name:http Address::8000 Redirect.EntryPoint:https' --entryPoints='Name:https Address::4442 TLS:tests/traefik.crt,tests/traefik.key'"` EntryPoints EntryPoints `description:"Entrypoints definition using format: --entryPoints='Name:http Address::8000 Redirect.EntryPoint:https' --entryPoints='Name:https Address::4442 TLS:tests/traefik.crt,tests/traefik.key'"`
Cluster *types.Cluster Cluster *types.Cluster `description:"Enable clustering"`
Constraints types.Constraints `description:"Filter services by constraint, matching with service tags."` Constraints types.Constraints `description:"Filter services by constraint, matching with service tags"`
ACME *acme.ACME `description:"Enable ACME (Let's Encrypt): automatic SSL"` ACME *acme.ACME `description:"Enable ACME (Let's Encrypt): automatic SSL"`
DefaultEntryPoints DefaultEntryPoints `description:"Entrypoints to be used by frontends that do not specify any entrypoint"` DefaultEntryPoints DefaultEntryPoints `description:"Entrypoints to be used by frontends that do not specify any entrypoint"`
ProvidersThrottleDuration time.Duration `description:"Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time."` ProvidersThrottleDuration time.Duration `description:"Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time."`

10
glide.lock generated
View file

@ -1,3 +1,4 @@
<<<<<<< 2fbcca003e6454c848801c859d8563da94ea8aaf
<<<<<<< a13549cc28273ba5c15a739fa4aaeb3e0f7216a4 <<<<<<< a13549cc28273ba5c15a739fa4aaeb3e0f7216a4
hash: c0ac205a859d78847e21d3cd63f427ffba985755c6ae84373e4a20364ba39b05 hash: c0ac205a859d78847e21d3cd63f427ffba985755c6ae84373e4a20364ba39b05
<<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab <<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab
@ -8,7 +9,14 @@ updated: 2016-09-28T16:50:04.352639437+01:00
hash: 809b3fa812ca88940fdc15530804a4bcd881708e4819fed5aa45c42c871ba5cf hash: 809b3fa812ca88940fdc15530804a4bcd881708e4819fed5aa45c42c871ba5cf
updated: 2016-09-20T14:50:04.029710103+02:00 updated: 2016-09-20T14:50:04.029710103+02:00
>>>>>>> Add KV datastore >>>>>>> Add KV datastore
<<<<<<< bea5ad3f132bae27b6c1a83adf00154058b484b5
>>>>>>> Add KV datastore >>>>>>> Add KV datastore
=======
=======
hash: 49c7bd0e32b2764248183bda52f168fe22d69e2db5e17c1dbeebbe71be9929b1
updated: 2016-08-11T14:33:42.826534934+02:00
>>>>>>> Add ACME store
>>>>>>> Add ACME store
imports: imports:
- name: github.com/abbot/go-http-auth - name: github.com/abbot/go-http-auth
version: cb4372376e1e00e9f6ab9ec142e029302c9e7140 version: cb4372376e1e00e9f6ab9ec142e029302c9e7140
@ -33,7 +41,7 @@ imports:
- name: github.com/containous/mux - name: github.com/containous/mux
version: a819b77bba13f0c0cbe36e437bc2e948411b3996 version: a819b77bba13f0c0cbe36e437bc2e948411b3996
- name: github.com/containous/staert - name: github.com/containous/staert
version: 044bdfee6c8f5e8fb71f70d5ba1cf4cb11a94e97 version: 56058c7d4152831a641764d10ec91132adf061ea
- name: github.com/coreos/etcd - name: github.com/coreos/etcd
version: 1c9e0a0e33051fed6c05c141e6fcbfe5c7f2a899 version: 1c9e0a0e33051fed6c05c141e6fcbfe5c7f2a899
subpackages: subpackages:

View file

@ -21,7 +21,7 @@ import:
- stream - stream
- utils - utils
- package: github.com/containous/staert - package: github.com/containous/staert
version: 044bdfee6c8f5e8fb71f70d5ba1cf4cb11a94e97 version: 56058c7d4152831a641764d10ec91132adf061ea
- package: github.com/docker/engine-api - package: github.com/docker/engine-api
version: 62043eb79d581a32ea849645277023c550732e52 version: 62043eb79d581a32ea849645277023c550732e52
subpackages: subpackages:

View file

@ -2,7 +2,7 @@ package middlewares
import ( import (
"fmt" "fmt"
log "github.com/Sirupsen/logrus" "github.com/containous/traefik/log"
"github.com/abbot/go-http-auth" "github.com/abbot/go-http-auth"
"github.com/codegangsta/negroni" "github.com/codegangsta/negroni"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"

View file

@ -12,7 +12,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
log "github.com/Sirupsen/logrus" "github.com/containous/traefik/log"
"github.com/streamrail/concurrent-map" "github.com/streamrail/concurrent-map"
) )

View file

@ -3,7 +3,7 @@ package middlewares
import ( import (
"bufio" "bufio"
"bytes" "bytes"
log "github.com/Sirupsen/logrus" "github.com/containous/traefik/log"
"github.com/vulcand/oxy/utils" "github.com/vulcand/oxy/utils"
"net" "net"
"net/http" "net/http"

View file

@ -1,7 +1,7 @@
package middlewares package middlewares
import ( import (
log "github.com/Sirupsen/logrus" "github.com/containous/traefik/log"
"github.com/vulcand/vulcand/plugin/rewrite" "github.com/vulcand/vulcand/plugin/rewrite"
"net/http" "net/http"
) )

View file

@ -9,7 +9,8 @@ import (
"time" "time"
"github.com/BurntSushi/ty/fun" "github.com/BurntSushi/ty/fun"
log "github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/containous/traefik/log"
"github.com/cenk/backoff" "github.com/cenk/backoff"
"github.com/containous/traefik/job" "github.com/containous/traefik/job"
"github.com/containous/traefik/safe" "github.com/containous/traefik/safe"
@ -270,7 +271,7 @@ func (provider *ConsulCatalog) getNodes(index map[string][]string) ([]catalogUpd
name := strings.ToLower(service) name := strings.ToLower(service)
if !strings.Contains(name, " ") && !visited[name] { if !strings.Contains(name, " ") && !visited[name] {
visited[name] = true visited[name] = true
log.WithFields(log.Fields{ log.WithFields(logrus.Fields{
"service": name, "service": name,
}).Debug("Fetching service") }).Debug("Fetching service")
healthy, err := provider.healthyNodes(name) healthy, err := provider.healthyNodes(name)

View file

@ -13,7 +13,7 @@ import (
"golang.org/x/net/context" "golang.org/x/net/context"
"github.com/BurntSushi/ty/fun" "github.com/BurntSushi/ty/fun"
log "github.com/Sirupsen/logrus" "github.com/containous/traefik/log"
"github.com/cenk/backoff" "github.com/cenk/backoff"
"github.com/containous/traefik/job" "github.com/containous/traefik/job"
"github.com/containous/traefik/safe" "github.com/containous/traefik/safe"

View file

@ -6,7 +6,7 @@ import (
"strings" "strings"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
log "github.com/Sirupsen/logrus" "github.com/containous/traefik/log"
"github.com/containous/traefik/safe" "github.com/containous/traefik/safe"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
"gopkg.in/fsnotify.v1" "gopkg.in/fsnotify.v1"

View file

@ -5,7 +5,7 @@ import (
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"fmt" "fmt"
log "github.com/Sirupsen/logrus" "github.com/containous/traefik/log"
"github.com/parnurzeal/gorequest" "github.com/parnurzeal/gorequest"
"net/http" "net/http"
"net/url" "net/url"

View file

@ -2,6 +2,10 @@ package provider
import ( import (
"fmt" "fmt"
"github.com/containous/traefik/log"
"github.com/containous/traefik/provider/k8s"
"github.com/containous/traefik/safe"
"github.com/containous/traefik/types"
"io/ioutil" "io/ioutil"
"os" "os"
"reflect" "reflect"
@ -10,7 +14,6 @@ import (
"text/template" "text/template"
"time" "time"
log "github.com/Sirupsen/logrus"
"github.com/cenk/backoff" "github.com/cenk/backoff"
"github.com/containous/traefik/job" "github.com/containous/traefik/job"
"github.com/containous/traefik/provider/k8s" "github.com/containous/traefik/provider/k8s"

View file

@ -9,7 +9,7 @@ import (
"errors" "errors"
"github.com/BurntSushi/ty/fun" "github.com/BurntSushi/ty/fun"
log "github.com/Sirupsen/logrus" "github.com/containous/traefik/log"
"github.com/cenk/backoff" "github.com/cenk/backoff"
"github.com/containous/traefik/job" "github.com/containous/traefik/job"
"github.com/containous/traefik/safe" "github.com/containous/traefik/safe"

View file

@ -13,7 +13,7 @@ import (
"time" "time"
"github.com/BurntSushi/ty/fun" "github.com/BurntSushi/ty/fun"
log "github.com/Sirupsen/logrus" "github.com/containous/traefik/log"
"github.com/cenk/backoff" "github.com/cenk/backoff"
"github.com/containous/traefik/job" "github.com/containous/traefik/job"
"github.com/containous/traefik/safe" "github.com/containous/traefik/safe"

View file

@ -8,7 +8,7 @@ import (
"fmt" "fmt"
"github.com/BurntSushi/ty/fun" "github.com/BurntSushi/ty/fun"
log "github.com/Sirupsen/logrus" "github.com/containous/traefik/log"
"github.com/cenk/backoff" "github.com/cenk/backoff"
"github.com/containous/traefik/job" "github.com/containous/traefik/job"
"github.com/containous/traefik/safe" "github.com/containous/traefik/safe"

View file

@ -1,7 +1,7 @@
package provider package provider
import ( import (
log "github.com/Sirupsen/logrus" "github.com/containous/traefik/log"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
"github.com/mesosphere/mesos-dns/records/state" "github.com/mesosphere/mesos-dns/records/state"
"reflect" "reflect"

View file

@ -13,7 +13,7 @@ import (
"os" "os"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
log "github.com/Sirupsen/logrus" "github.com/containous/traefik/log"
"github.com/containous/traefik/autogen" "github.com/containous/traefik/autogen"
"github.com/containous/traefik/safe" "github.com/containous/traefik/safe"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"

View file

@ -1,8 +1,8 @@
package safe package safe
import ( import (
"github.com/containous/traefik/log"
"golang.org/x/net/context" "golang.org/x/net/context"
"log"
"runtime/debug" "runtime/debug"
"sync" "sync"
) )
@ -26,18 +26,26 @@ type Pool struct {
} }
// NewPool creates a Pool // NewPool creates a Pool
func NewPool(baseCtx context.Context) *Pool { func NewPool(parentCtx context.Context) *Pool {
baseCtx, _ := context.WithCancel(parentCtx)
ctx, cancel := context.WithCancel(baseCtx) ctx, cancel := context.WithCancel(baseCtx)
return &Pool{ return &Pool{
baseCtx: baseCtx,
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
baseCtx: baseCtx,
} }
} }
// Ctx returns main context // Ctx returns main context
func (p *Pool) Ctx() context.Context { func (p *Pool) Ctx() context.Context {
return p.ctx return p.baseCtx
}
//AddGoCtx adds a recoverable goroutine with a context without starting it
func (p *Pool) AddGoCtx(goroutine routineCtx) {
p.lock.Lock()
p.routinesCtx = append(p.routinesCtx, goroutine)
p.lock.Unlock()
} }
//GoCtx starts a recoverable goroutine with a context //GoCtx starts a recoverable goroutine with a context
@ -71,6 +79,7 @@ func (p *Pool) Go(goroutine func(stop chan bool)) {
// Stop stops all started routines, waiting for their termination // Stop stops all started routines, waiting for their termination
func (p *Pool) Stop() { func (p *Pool) Stop() {
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock()
p.cancel() p.cancel()
for _, routine := range p.routines { for _, routine := range p.routines {
routine.stop <- true routine.stop <- true
@ -79,12 +88,12 @@ func (p *Pool) Stop() {
for _, routine := range p.routines { for _, routine := range p.routines {
close(routine.stop) close(routine.stop)
} }
p.lock.Unlock()
} }
// Start starts all stoped routines // Start starts all stopped routines
func (p *Pool) Start() { func (p *Pool) Start() {
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock()
p.ctx, p.cancel = context.WithCancel(p.baseCtx) p.ctx, p.cancel = context.WithCancel(p.baseCtx)
for _, routine := range p.routines { for _, routine := range p.routines {
p.waitGroup.Add(1) p.waitGroup.Add(1)
@ -102,7 +111,6 @@ func (p *Pool) Start() {
p.waitGroup.Done() p.waitGroup.Done()
}) })
} }
p.lock.Unlock()
} }
// Go starts a recoverable goroutine // Go starts a recoverable goroutine
@ -123,6 +131,6 @@ func GoWithRecover(goroutine func(), customRecover func(err interface{})) {
} }
func defaultRecoverGoroutine(err interface{}) { func defaultRecoverGoroutine(err interface{}) {
log.Println(err) log.Errorf("Error in Go routine: %s", err)
debug.PrintStack() debug.PrintStack()
} }

View file

@ -21,10 +21,10 @@ import (
"golang.org/x/net/context" "golang.org/x/net/context"
log "github.com/Sirupsen/logrus"
"github.com/codegangsta/negroni" "github.com/codegangsta/negroni"
"github.com/containous/mux" "github.com/containous/mux"
"github.com/containous/traefik/cluster" "github.com/containous/traefik/cluster"
"github.com/containous/traefik/log"
"github.com/containous/traefik/middlewares" "github.com/containous/traefik/middlewares"
"github.com/containous/traefik/provider" "github.com/containous/traefik/provider"
"github.com/containous/traefik/safe" "github.com/containous/traefik/safe"
@ -93,8 +93,8 @@ func NewServer(globalConfiguration GlobalConfiguration) *Server {
// Start starts the server and blocks until server is shutted down. // Start starts the server and blocks until server is shutted down.
func (server *Server) Start() { func (server *Server) Start() {
server.startLeadership()
server.startHTTPServers() server.startHTTPServers()
server.startLeadership()
server.routinesPool.Go(func(stop chan bool) { server.routinesPool.Go(func(stop chan bool) {
server.listenProviders(stop) server.listenProviders(stop)
}) })
@ -129,7 +129,7 @@ func (server *Server) Close() {
if ctx.Err() == context.Canceled { if ctx.Err() == context.Canceled {
return return
} else if ctx.Err() == context.DeadlineExceeded { } else if ctx.Err() == context.DeadlineExceeded {
log.Debugf("I love you all :'( ✝") log.Warnf("Timeout while stopping traefik, killing instance ✝")
os.Exit(1) os.Exit(1)
} }
}(ctx) }(ctx)
@ -147,17 +147,17 @@ func (server *Server) Close() {
func (server *Server) startLeadership() { func (server *Server) startLeadership() {
if server.leadership != nil { if server.leadership != nil {
server.leadership.Participate(server.routinesPool) server.leadership.Participate(server.routinesPool)
server.leadership.GoCtx(func(ctx context.Context) { // server.leadership.AddGoCtx(func(ctx context.Context) {
log.Debugf("Started test routine") // log.Debugf("Started test routine")
<-ctx.Done() // <-ctx.Done()
log.Debugf("Stopped test routine") // log.Debugf("Stopped test routine")
}) // })
} }
} }
func (server *Server) stopLeadership() { func (server *Server) stopLeadership() {
if server.leadership != nil { if server.leadership != nil {
server.leadership.Resign() server.leadership.Stop()
} }
} }
@ -283,7 +283,13 @@ func (server *Server) listenConfigurations(stop chan bool) {
} }
func (server *Server) postLoadConfig() { func (server *Server) postLoadConfig() {
if server.globalConfiguration.ACME != nil && server.globalConfiguration.ACME.OnHostRule { if server.globalConfiguration.ACME == nil {
return
}
if server.leadership != nil && !server.leadership.IsLeader() {
return
}
if server.globalConfiguration.ACME.OnHostRule {
currentConfigurations := server.currentConfigurations.Get().(configs) currentConfigurations := server.currentConfigurations.Get().(configs)
for _, configuration := range currentConfigurations { for _, configuration := range currentConfigurations {
for _, frontend := range configuration.Frontends { for _, frontend := range configuration.Frontends {
@ -401,10 +407,17 @@ func (server *Server) createTLSConfig(entryPointName string, tlsOption *TLS, rou
} }
return false return false
} }
if server.leadership == nil {
err := server.globalConfiguration.ACME.CreateLocalConfig(config, checkOnDemandDomain) err := server.globalConfiguration.ACME.CreateLocalConfig(config, checkOnDemandDomain)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else {
err := server.globalConfiguration.ACME.CreateClusterConfig(server.leadership, config, checkOnDemandDomain)
if err != nil {
return nil, err
}
}
} }
} else { } else {
return nil, errors.New("Unknown entrypoint " + server.globalConfiguration.ACME.EntryPoint + " for ACME configuration") return nil, errors.New("Unknown entrypoint " + server.globalConfiguration.ACME.EntryPoint + " for ACME configuration")

View file

@ -12,10 +12,11 @@ import (
"strings" "strings"
"text/template" "text/template"
log "github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/containous/flaeg" "github.com/containous/flaeg"
"github.com/containous/staert" "github.com/containous/staert"
"github.com/containous/traefik/acme" "github.com/containous/traefik/acme"
"github.com/containous/traefik/log"
"github.com/containous/traefik/middlewares" "github.com/containous/traefik/middlewares"
"github.com/containous/traefik/provider" "github.com/containous/traefik/provider"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
@ -208,7 +209,7 @@ func run(traefikConfiguration *TraefikConfiguration) {
} }
// logging // logging
level, err := log.ParseLevel(strings.ToLower(globalConfiguration.LogLevel)) level, err := logrus.ParseLevel(strings.ToLower(globalConfiguration.LogLevel))
if err != nil { if err != nil {
log.Error("Error getting level", err) log.Error("Error getting level", err)
} }
@ -224,10 +225,10 @@ func run(traefikConfiguration *TraefikConfiguration) {
log.Error("Error opening file", err) log.Error("Error opening file", err)
} else { } else {
log.SetOutput(fi) log.SetOutput(fi)
log.SetFormatter(&log.TextFormatter{DisableColors: true, FullTimestamp: true, DisableSorting: true}) log.SetFormatter(&logrus.TextFormatter{DisableColors: true, FullTimestamp: true, DisableSorting: true})
} }
} else { } else {
log.SetFormatter(&log.TextFormatter{FullTimestamp: true, DisableSorting: true}) log.SetFormatter(&logrus.TextFormatter{FullTimestamp: true, DisableSorting: true})
} }
jsonConf, _ := json.Marshal(globalConfiguration) jsonConf, _ := json.Marshal(globalConfiguration)
log.Infof("Traefik version %s built on %s", version.Version, version.BuildDate) log.Infof("Traefik version %s built on %s", version.Version, version.BuildDate)

View file

@ -201,7 +201,7 @@ type Store struct {
// Cluster holds cluster config // Cluster holds cluster config
type Cluster struct { type Cluster struct {
Node string Node string `description:"Node name"`
Store *Store Store *Store
} }

2
web.go
View file

@ -8,7 +8,7 @@ import (
"net/http" "net/http"
"runtime" "runtime"
log "github.com/Sirupsen/logrus" "github.com/containous/traefik/log"
"github.com/codegangsta/negroni" "github.com/codegangsta/negroni"
"github.com/containous/mux" "github.com/containous/mux"
"github.com/containous/traefik/autogen" "github.com/containous/traefik/autogen"