From a42845502e9b6e3b9985c56ad99d28c1357287b2 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Thu, 18 Aug 2016 14:20:11 +0200 Subject: [PATCH] Add ACME store Signed-off-by: Emile Vauge --- acme/account.go | 176 ++++++++++++ acme/acme.go | 508 ++++++++++++++++------------------- acme/challengeProvider.go | 86 ++++-- acme/localStore.go | 97 +++++++ adapters.go | 2 +- cluster/datastore.go | 102 +++++-- cluster/leadership.go | 30 ++- cluster/store.go | 16 ++ configuration.go | 16 +- glide.lock | 10 +- glide.yaml | 2 +- middlewares/authenticator.go | 2 +- middlewares/logger.go | 2 +- middlewares/retry.go | 2 +- middlewares/rewrite.go | 2 +- provider/consul_catalog.go | 5 +- provider/docker.go | 2 +- provider/file.go | 2 +- provider/k8s/client.go | 2 +- provider/kubernetes.go | 5 +- provider/kv.go | 2 +- provider/marathon.go | 2 +- provider/mesos.go | 2 +- provider/mesos_test.go | 2 +- provider/provider.go | 2 +- safe/routine.go | 24 +- server.go | 39 ++- traefik.go | 9 +- types/types.go | 2 +- web.go | 2 +- 30 files changed, 781 insertions(+), 374 deletions(-) create mode 100644 acme/account.go create mode 100644 acme/localStore.go create mode 100644 cluster/store.go diff --git a/acme/account.go b/acme/account.go new file mode 100644 index 000000000..55ac44672 --- /dev/null +++ b/acme/account.go @@ -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 +} diff --git a/acme/acme.go b/acme/acme.go index 0de565eb4..575e7b2ac 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -1,179 +1,36 @@ package acme import ( - "crypto" - "crypto/rand" - "crypto/rsa" "crypto/tls" - "crypto/x509" - "encoding/json" "errors" "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" fmtlog "log" "os" - "reflect" "strings" - "sync" "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 type ACME struct { - 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'"` - StorageFile string `description:"File 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."` - OnHostRule bool `description:"Enable certificate generation on frontends Host rules."` - CAServer string `description:"CA server to use."` - EntryPoint string `description:"Entrypoint to proxy acme challenge to."` - storageLock sync.RWMutex - client *acme.Client - account *Account - defaultCertificate *tls.Certificate + 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'"` + 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."` + OnHostRule bool `description:"Enable certificate generation on frontends Host rules."` + CAServer string `description:"CA server to use."` + EntryPoint string `description:"Entrypoint to proxy acme challenge to."` + client *acme.Client + defaultCertificate *tls.Certificate + store cluster.Store + challengeProvider *challengeProvider + checkOnDemandDomain func(domain string) bool } //Domains parse []Domain @@ -218,11 +75,7 @@ type Domain struct { } func (a *ACME) init() error { - if len(a.Store) == 0 { - a.Store = a.StorageFile - } acme.Logger = fmtlog.New(ioutil.Discard, "", 0) - log.Debugf("Generating default certificate...") // no certificates in TLS config, so we add a default one cert, err := generateDefaultCertificate() if err != nil { @@ -232,78 +85,174 @@ func (a *ACME) init() error { return nil } -// CreateClusterConfig creates a tls.config from using ACME configuration -func (a *ACME) CreateClusterConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(domain string) bool) error { +// CreateClusterConfig creates a tls.config using ACME configuration in cluster mode +func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tls.Config, checkOnDemandDomain func(domain string) bool) error { err := a.init() if err != nil { return err } - if len(a.Store) == 0 { - return errors.New("Empty Store, please provide a filename/key for certs storage") + if len(a.Storage) == 0 { + return errors.New("Empty Store, please provide a key for certs storage") } + a.checkOnDemandDomain = checkOnDemandDomain 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 } -// CreateLocalConfig creates a tls.config from using ACME configuration -func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(domain string) bool) error { +// CreateLocalConfig creates a tls.config using local ACME configuration +func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, checkOnDemandDomain func(domain string) bool) error { err := a.init() if err != nil { return err } - if len(a.Store) == 0 { - return errors.New("Empty Store, please provide a filename/key for certs storage") + if len(a.Storage) == 0 { + return errors.New("Empty Store, please provide a filename for certs storage") } + a.checkOnDemandDomain = checkOnDemandDomain 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 err error + var account *Account - // if certificates in storage, load them - if fileInfo, fileErr := os.Stat(a.Store); fileErr == nil && fileInfo.Size() != 0 { - log.Infof("Loading ACME certificates...") + if fileInfo, fileErr := os.Stat(a.Storage); fileErr == nil && fileInfo.Size() != 0 { + log.Infof("Loading ACME Account...") // load account - a.account, err = a.loadAccount(a) + object, err := localStore.Load() if err != nil { return err } + account = object.(*Account) } else { log.Infof("Generating ACME Account...") - // Create a user. New accounts need an email and private key to start - privateKey, err := rsa.GenerateKey(rand.Reader, 4096) + account, err = NewAccount(a.Email) if err != nil { return err } - a.account = &Account{ - Email: a.Email, - PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey), - } - a.account.DomainsCertificate = DomainsCertificates{Certs: []*DomainsCertificate{}, lock: &sync.RWMutex{}} needRegister = true } - a.client, err = a.buildACMEClient() - if err != nil { - return err - } - a.client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01}) - wrapperChallengeProvider := newWrapperChallengeProvider() - err = client.SetChallengeProvider(acme.TLSSNI01, wrapperChallengeProvider) + log.Infof("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.Infof("Register...") reg, err := a.client.Register() if err != nil { return err } - a.account.Registration = reg + 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.Infof("AgreeToTOS...") err = a.client.AgreeToTOS() if err != nil { // Let's Encrypt Subscriber Agreement renew ? @@ -311,45 +260,33 @@ func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, CheckOnDemandDomain func if err != nil { return err } - a.account.Registration = reg + account.Registration = reg err = a.client.AgreeToTOS() 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 - err = a.saveAccount() + transaction, _, err := a.store.Begin() + if err != nil { + return err + } + err = transaction.Commit(account) if err != nil { return err } safe.Go(func() { - a.retrieveCertificates(a.client) - if err := a.renewCertificates(a.client); err != nil { - log.Errorf("Error renewing ACME certificate %+v: %s", a.account, err.Error()) + a.retrieveCertificates() + if err := a.renewCertificates(); err != nil { + 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) safe.Go(func() { 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()) } } @@ -358,26 +295,54 @@ func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, CheckOnDemandDomain func 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...") for _, domain := range a.Domains { // 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 = append(domains, domain.Main) domains = append(domains, domain.SANs...) - certificateResource, err := a.getDomainsCertificates(client, domains) + certificateResource, err := a.getDomainsCertificates(domains) if err != nil { log.Errorf("Error getting ACME certificate for domain %s: %s", domains, err.Error()) 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 { log.Errorf("Error adding ACME certificate for domain %s: %s", domains, err.Error()) 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 } } @@ -385,12 +350,18 @@ func (a *ACME) retrieveCertificates(client *acme.Client) { log.Infof("Retrieved ACME certificates") } -func (a *ACME) renewCertificates(client *acme.Client) error { +func (a *ACME) renewCertificates() error { 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() { + transaction, object, err := a.store.Begin() + if err != nil { + return err + } + account = object.(*Account) log.Debugf("Renewing certificate %+v", certificateResource.Domains) - renewedCert, err := client.RenewCertificate(acme.CertificateResource{ + renewedCert, err := a.client.RenewCertificate(acme.CertificateResource{ Domain: certificateResource.Certificate.Domain, CertURL: certificateResource.Certificate.CertURL, CertStableURL: certificateResource.Certificate.CertStableURL, @@ -409,13 +380,14 @@ func (a *ACME) renewCertificates(client *acme.Client) error { PrivateKey: renewedCert.PrivateKey, Certificate: renewedCert.Certificate, } - err = a.account.DomainsCertificate.renewCertificates(renewedACMECert, certificateResource.Domains) + err = account.DomainsCertificate.renewCertificates(renewedACMECert, certificateResource.Domains) if err != nil { log.Errorf("Error renewing certificate: %v", err) 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 } } @@ -423,33 +395,45 @@ func (a *ACME) renewCertificates(client *acme.Client) error { 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" if len(a.CAServer) > 0 { 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 { return nil, err } - return client, nil } 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 } - certificate, err := a.getDomainsCertificates(a.client, []string{clientHello.ServerName}) + certificate, err := a.getDomainsCertificates([]string{clientHello.ServerName}) if err != nil { return nil, err } 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 { 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 cert.tlsCert, nil @@ -458,6 +442,7 @@ func (a *ACME) loadCertificateOnDemand(clientHello *tls.ClientHelloInfo) (*tls.C // LoadCertificateForDomains loads certificates from ACME for given domains func (a *ACME) LoadCertificateForDomains(domains []string) { safe.Go(func() { + account := a.store.Get().(*Account) var domain Domain if len(domains) == 0 { // no domain @@ -468,64 +453,39 @@ func (a *ACME) LoadCertificateForDomains(domains []string) { } else { domain = Domain{Main: domains[0]} } - if _, exists := a.account.DomainsCertificate.exists(domain); exists { + if _, exists := account.DomainsCertificate.exists(domain); exists { // domain already exists return } - certificate, err := a.getDomainsCertificates(a.client, domains) + certificate, err := a.getDomainsCertificates(domains) if err != nil { log.Errorf("Error getting ACME certificates %+v : %v", domains, err) return } 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 { log.Errorf("Error adding ACME certificates %+v : %v", domains, err) return } - if err = a.saveAccount(); err != nil { - log.Errorf("Error Saving ACME account %+v: %v", a.account, err) + if err = transaction.Commit(account); err != nil { + log.Errorf("Error Saving ACME account %+v: %v", account, err) return } }) } -func (a *ACME) loadAccount(acmeConfig *ACME) (*Account, 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) { +func (a *ACME) getDomainsCertificates(domains []string) (*Certificate, error) { log.Debugf("Loading ACME certificates %s...", domains) bundle := true - certificate, failures := client.ObtainCertificate(domains, bundle, nil) + certificate, failures := a.client.ObtainCertificate(domains, bundle, nil) if len(failures) > 0 { log.Error(failures) return nil, fmt.Errorf("Cannot obtain certificates %s+v", failures) diff --git a/acme/challengeProvider.go b/acme/challengeProvider.go index 1083b5f83..1ed096b94 100644 --- a/acme/challengeProvider.go +++ b/acme/challengeProvider.go @@ -4,33 +4,59 @@ import ( "crypto/tls" "sync" + "bytes" + "crypto/rsa" "crypto/x509" + "encoding/gob" + "github.com/containous/traefik/cluster" + "github.com/containous/traefik/log" "github.com/xenolf/lego/acme" + "time" ) -var _ acme.ChallengeProvider = (*inMemoryChallengeProvider)(nil) - -type inMemoryChallengeProvider struct { - challengeCerts map[string]*tls.Certificate - lock sync.RWMutex +func init() { + gob.Register(rsa.PrivateKey{}) + gob.Register(rsa.PublicKey{}) } -func newWrapperChallengeProvider() *inMemoryChallengeProvider { - return &inMemoryChallengeProvider{ - challengeCerts: map[string]*tls.Certificate{}, +var _ acme.ChallengeProviderTimeout = (*challengeProvider)(nil) + +type challengeProvider struct { + store cluster.Store + lock sync.RWMutex +} + +func newMemoryChallengeProvider(store cluster.Store) *challengeProvider { + return &challengeProvider{ + 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() 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 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) if err != nil { return err @@ -42,16 +68,40 @@ func (c *inMemoryChallengeProvider) Present(domain, token, keyAuth string) error c.lock.Lock() defer c.lock.Unlock() - for i := range cert.Leaf.DNSNames { - c.challengeCerts[cert.Leaf.DNSNames[i]] = &cert + transaction, object, err := c.store.Begin() + if err != nil { + return err } - - return nil + account := object.(*Account) + 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() defer c.lock.Unlock() - delete(c.challengeCerts, domain) - return nil + transaction, object, err := c.store.Begin() + 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 } diff --git a/acme/localStore.go b/acme/localStore.go new file mode 100644 index 000000000..c39322091 --- /dev/null +++ b/acme/localStore.go @@ -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 +} diff --git a/adapters.go b/adapters.go index ac1c7835b..edf745e85 100644 --- a/adapters.go +++ b/adapters.go @@ -6,7 +6,7 @@ package main import ( "net/http" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" ) // OxyLogger implements oxy Logger interface with logrus. diff --git a/cluster/datastore.go b/cluster/datastore.go index 5bad330a8..f2bcf9c61 100644 --- a/cluster/datastore.go +++ b/cluster/datastore.go @@ -1,44 +1,62 @@ package cluster import ( + "encoding/json" "fmt" - log "github.com/Sirupsen/logrus" - "github.com/cenkalti/backoff" "github.com/containous/staert" + "github.com/containous/traefik/log" "github.com/docker/libkv/store" + "github.com/emilevauge/backoff" "github.com/satori/go.uuid" "golang.org/x/net/context" "sync" "time" ) -// Object is the struct to store -type Object interface{} - // Metadata stores Object plus metadata type Metadata struct { - Lock string + object Object + Object []byte + 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 type Datastore struct { - kv *staert.KvSource + kv staert.KvSource ctx context.Context localLock *sync.RWMutex - object Object meta *Metadata lockKey string + listener Listener } // 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{ kv: kvSource, ctx: ctx, - meta: &Metadata{}, - object: object, + meta: &Metadata{object: object}, lockKey: kvSource.Prefix + "/lock", localLock: &sync.RWMutex{}, + listener: listener, } err := datastore.watchChanges() if err != nil { @@ -67,17 +85,24 @@ func (d *Datastore) watchChanges() error { return err } d.localLock.Lock() - err := d.kv.LoadConfig(d.object) + err := d.kv.LoadConfig(d.meta) if err != nil { d.localLock.Unlock() return err } - err = d.kv.LoadConfig(d.meta) + err = d.meta.unmarshall() if err != nil { d.localLock.Unlock() return err } 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. -func (d *Datastore) Begin() (*Transaction, error) { +func (d *Datastore) Begin() (Transaction, Object, error) { 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)}) if err != nil { - return nil, err + return nil, nil, err } stopCh := make(chan struct{}) ctx, cancel := context.WithCancel(d.ctx) @@ -109,11 +135,11 @@ func (d *Datastore) Begin() (*Transaction, error) { select { case <-ctx.Done(): if errLock != nil { - return nil, errLock + return nil, nil, errLock } case <-d.ctx.Done(): 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 @@ -131,14 +157,15 @@ func (d *Datastore) Begin() (*Transaction, error) { ebo.MaxElapsedTime = 60 * time.Second err = backoff.RetryNotify(operation, ebo, notify) 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 - return &Transaction{ + return &datastoreTransaction{ Datastore: d, remoteLock: remoteLock, - }, nil + id: id, + }, d.meta.object, nil } func (d *Datastore) get() *Metadata { @@ -147,28 +174,50 @@ func (d *Datastore) get() *Metadata { 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 func (d *Datastore) Get() Object { d.localLock.RLock() defer d.localLock.RUnlock() - return d.object + return d.meta.object } -// Transaction allows to set a struct in the KV store -type Transaction struct { +var _ Transaction = (*datastoreTransaction)(nil) + +type datastoreTransaction struct { *Datastore remoteLock store.Locker dirty bool + id string } // 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() defer s.localLock.Unlock() if s.dirty { 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 { return err } @@ -178,7 +227,8 @@ func (s *Transaction) Commit(object Object) error { return err } - s.Datastore.object = object s.dirty = true + // log.Debugf("Datastore object saved: %+v", s.object) + log.Debugf("Transaction commited %s", s.id) return nil } diff --git a/cluster/leadership.go b/cluster/leadership.go index 60d67e427..2143ee6d9 100644 --- a/cluster/leadership.go +++ b/cluster/leadership.go @@ -1,11 +1,11 @@ package cluster import ( - log "github.com/Sirupsen/logrus" - "github.com/cenkalti/backoff" + "github.com/containous/traefik/log" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" "github.com/docker/leadership" + "github.com/emilevauge/backoff" "golang.org/x/net/context" "time" ) @@ -15,6 +15,8 @@ type Leadership struct { *safe.Pool *types.Cluster candidate *leadership.Candidate + leader safe.Safe + listeners []LeaderListener } // NewLeadership creates a leadership @@ -23,9 +25,13 @@ func NewLeadership(ctx context.Context, cluster *types.Cluster) *Leadership { Pool: safe.NewPool(ctx), Cluster: cluster, 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 func (l *Leadership) Participate(pool *safe.Pool) { 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 func (l *Leadership) 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 { @@ -70,9 +81,22 @@ func (l *Leadership) run(candidate *leadership.Candidate, ctx context.Context) e func (l *Leadership) onElection(elected bool) { if elected { log.Infof("Node %s elected leader ♚", l.Cluster.Node) + l.leader.Set(true) l.Start() } else { log.Infof("Node %s elected slave ♝", l.Cluster.Node) + l.leader.Set(false) 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) } diff --git a/cluster/store.go b/cluster/store.go new file mode 100644 index 000000000..c8e9207be --- /dev/null +++ b/cluster/store.go @@ -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 +} diff --git a/configuration.go b/configuration.go index 558ce2932..20546e8b3 100644 --- a/configuration.go +++ b/configuration.go @@ -23,14 +23,14 @@ type TraefikConfiguration struct { // GlobalConfiguration holds global configuration (with providers, etc.). // It's populated from the traefik configuration file passed as an argument to the binary. type GlobalConfiguration struct { - GraceTimeOut int64 `short:"g" description:"Duration to give active requests a chance to finish during hot-reload"` - Debug bool `short:"d" description:"Enable debug mode"` - AccessLogsFile string `description:"Access logs file"` - TraefikLogsFile string `description:"Traefik logs file"` - 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'"` - Cluster *types.Cluster - Constraints types.Constraints `description:"Filter services by constraint, matching with service tags."` + GraceTimeOut int64 `short:"g" description:"Duration to give active requests a chance to finish during hot-reload"` + Debug bool `short:"d" description:"Enable debug mode"` + AccessLogsFile string `description:"Access logs file"` + TraefikLogsFile string `description:"Traefik logs file"` + 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'"` + Cluster *types.Cluster `description:"Enable clustering"` + Constraints types.Constraints `description:"Filter services by constraint, matching with service tags"` 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"` 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."` diff --git a/glide.lock b/glide.lock index f447712ec..26c72cbbc 100644 --- a/glide.lock +++ b/glide.lock @@ -1,3 +1,4 @@ +<<<<<<< 2fbcca003e6454c848801c859d8563da94ea8aaf <<<<<<< a13549cc28273ba5c15a739fa4aaeb3e0f7216a4 hash: c0ac205a859d78847e21d3cd63f427ffba985755c6ae84373e4a20364ba39b05 <<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab @@ -8,7 +9,14 @@ updated: 2016-09-28T16:50:04.352639437+01:00 hash: 809b3fa812ca88940fdc15530804a4bcd881708e4819fed5aa45c42c871ba5cf updated: 2016-09-20T14:50:04.029710103+02:00 >>>>>>> Add KV datastore +<<<<<<< bea5ad3f132bae27b6c1a83adf00154058b484b5 >>>>>>> Add KV datastore +======= +======= +hash: 49c7bd0e32b2764248183bda52f168fe22d69e2db5e17c1dbeebbe71be9929b1 +updated: 2016-08-11T14:33:42.826534934+02:00 +>>>>>>> Add ACME store +>>>>>>> Add ACME store imports: - name: github.com/abbot/go-http-auth version: cb4372376e1e00e9f6ab9ec142e029302c9e7140 @@ -33,7 +41,7 @@ imports: - name: github.com/containous/mux version: a819b77bba13f0c0cbe36e437bc2e948411b3996 - name: github.com/containous/staert - version: 044bdfee6c8f5e8fb71f70d5ba1cf4cb11a94e97 + version: 56058c7d4152831a641764d10ec91132adf061ea - name: github.com/coreos/etcd version: 1c9e0a0e33051fed6c05c141e6fcbfe5c7f2a899 subpackages: diff --git a/glide.yaml b/glide.yaml index 9f1c7b4e6..15b1b8be0 100644 --- a/glide.yaml +++ b/glide.yaml @@ -21,7 +21,7 @@ import: - stream - utils - package: github.com/containous/staert - version: 044bdfee6c8f5e8fb71f70d5ba1cf4cb11a94e97 + version: 56058c7d4152831a641764d10ec91132adf061ea - package: github.com/docker/engine-api version: 62043eb79d581a32ea849645277023c550732e52 subpackages: diff --git a/middlewares/authenticator.go b/middlewares/authenticator.go index b5e6b0eb1..33fb77ce9 100644 --- a/middlewares/authenticator.go +++ b/middlewares/authenticator.go @@ -2,7 +2,7 @@ package middlewares import ( "fmt" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/abbot/go-http-auth" "github.com/codegangsta/negroni" "github.com/containous/traefik/types" diff --git a/middlewares/logger.go b/middlewares/logger.go index 74617da54..d65cbbf3f 100644 --- a/middlewares/logger.go +++ b/middlewares/logger.go @@ -12,7 +12,7 @@ import ( "sync/atomic" "time" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/streamrail/concurrent-map" ) diff --git a/middlewares/retry.go b/middlewares/retry.go index e40b75968..8577a1685 100644 --- a/middlewares/retry.go +++ b/middlewares/retry.go @@ -3,7 +3,7 @@ package middlewares import ( "bufio" "bytes" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/vulcand/oxy/utils" "net" "net/http" diff --git a/middlewares/rewrite.go b/middlewares/rewrite.go index eaaff6bec..d3bc4d635 100644 --- a/middlewares/rewrite.go +++ b/middlewares/rewrite.go @@ -1,7 +1,7 @@ package middlewares import ( - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/vulcand/vulcand/plugin/rewrite" "net/http" ) diff --git a/provider/consul_catalog.go b/provider/consul_catalog.go index 1d1d4c928..947368a68 100644 --- a/provider/consul_catalog.go +++ b/provider/consul_catalog.go @@ -9,7 +9,8 @@ import ( "time" "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/containous/traefik/job" "github.com/containous/traefik/safe" @@ -270,7 +271,7 @@ func (provider *ConsulCatalog) getNodes(index map[string][]string) ([]catalogUpd name := strings.ToLower(service) if !strings.Contains(name, " ") && !visited[name] { visited[name] = true - log.WithFields(log.Fields{ + log.WithFields(logrus.Fields{ "service": name, }).Debug("Fetching service") healthy, err := provider.healthyNodes(name) diff --git a/provider/docker.go b/provider/docker.go index 73f897244..175cd3ebb 100644 --- a/provider/docker.go +++ b/provider/docker.go @@ -13,7 +13,7 @@ import ( "golang.org/x/net/context" "github.com/BurntSushi/ty/fun" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/cenk/backoff" "github.com/containous/traefik/job" "github.com/containous/traefik/safe" diff --git a/provider/file.go b/provider/file.go index dc42b26d5..aab244a89 100644 --- a/provider/file.go +++ b/provider/file.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/BurntSushi/toml" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" "gopkg.in/fsnotify.v1" diff --git a/provider/k8s/client.go b/provider/k8s/client.go index d7ac398e6..ad9679cae 100644 --- a/provider/k8s/client.go +++ b/provider/k8s/client.go @@ -5,7 +5,7 @@ import ( "crypto/x509" "encoding/json" "fmt" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/parnurzeal/gorequest" "net/http" "net/url" diff --git a/provider/kubernetes.go b/provider/kubernetes.go index a5c8654f6..22b9fd18e 100644 --- a/provider/kubernetes.go +++ b/provider/kubernetes.go @@ -2,6 +2,10 @@ package provider import ( "fmt" + "github.com/containous/traefik/log" + "github.com/containous/traefik/provider/k8s" + "github.com/containous/traefik/safe" + "github.com/containous/traefik/types" "io/ioutil" "os" "reflect" @@ -10,7 +14,6 @@ import ( "text/template" "time" - log "github.com/Sirupsen/logrus" "github.com/cenk/backoff" "github.com/containous/traefik/job" "github.com/containous/traefik/provider/k8s" diff --git a/provider/kv.go b/provider/kv.go index 86c97d1b7..27b5469b2 100644 --- a/provider/kv.go +++ b/provider/kv.go @@ -9,7 +9,7 @@ import ( "errors" "github.com/BurntSushi/ty/fun" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/cenk/backoff" "github.com/containous/traefik/job" "github.com/containous/traefik/safe" diff --git a/provider/marathon.go b/provider/marathon.go index c06b49309..ddeefe923 100644 --- a/provider/marathon.go +++ b/provider/marathon.go @@ -13,7 +13,7 @@ import ( "time" "github.com/BurntSushi/ty/fun" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/cenk/backoff" "github.com/containous/traefik/job" "github.com/containous/traefik/safe" diff --git a/provider/mesos.go b/provider/mesos.go index 2396cf018..0943a3db6 100644 --- a/provider/mesos.go +++ b/provider/mesos.go @@ -8,7 +8,7 @@ import ( "fmt" "github.com/BurntSushi/ty/fun" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/cenk/backoff" "github.com/containous/traefik/job" "github.com/containous/traefik/safe" diff --git a/provider/mesos_test.go b/provider/mesos_test.go index 1c1eb2376..5968db8e9 100644 --- a/provider/mesos_test.go +++ b/provider/mesos_test.go @@ -1,7 +1,7 @@ package provider import ( - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/containous/traefik/types" "github.com/mesosphere/mesos-dns/records/state" "reflect" diff --git a/provider/provider.go b/provider/provider.go index 441861025..72610c7af 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -13,7 +13,7 @@ import ( "os" "github.com/BurntSushi/toml" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/containous/traefik/autogen" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" diff --git a/safe/routine.go b/safe/routine.go index 75e2358b7..a7f171968 100644 --- a/safe/routine.go +++ b/safe/routine.go @@ -1,8 +1,8 @@ package safe import ( + "github.com/containous/traefik/log" "golang.org/x/net/context" - "log" "runtime/debug" "sync" ) @@ -26,18 +26,26 @@ type Pool struct { } // 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) return &Pool{ + baseCtx: baseCtx, ctx: ctx, cancel: cancel, - baseCtx: baseCtx, } } // Ctx returns main 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 @@ -71,6 +79,7 @@ func (p *Pool) Go(goroutine func(stop chan bool)) { // Stop stops all started routines, waiting for their termination func (p *Pool) Stop() { p.lock.Lock() + defer p.lock.Unlock() p.cancel() for _, routine := range p.routines { routine.stop <- true @@ -79,12 +88,12 @@ func (p *Pool) Stop() { for _, routine := range p.routines { close(routine.stop) } - p.lock.Unlock() } -// Start starts all stoped routines +// Start starts all stopped routines func (p *Pool) Start() { p.lock.Lock() + defer p.lock.Unlock() p.ctx, p.cancel = context.WithCancel(p.baseCtx) for _, routine := range p.routines { p.waitGroup.Add(1) @@ -102,7 +111,6 @@ func (p *Pool) Start() { p.waitGroup.Done() }) } - p.lock.Unlock() } // Go starts a recoverable goroutine @@ -123,6 +131,6 @@ func GoWithRecover(goroutine func(), customRecover func(err interface{})) { } func defaultRecoverGoroutine(err interface{}) { - log.Println(err) + log.Errorf("Error in Go routine: %s", err) debug.PrintStack() } diff --git a/server.go b/server.go index a9222bf2a..5ace528aa 100644 --- a/server.go +++ b/server.go @@ -21,10 +21,10 @@ import ( "golang.org/x/net/context" - log "github.com/Sirupsen/logrus" "github.com/codegangsta/negroni" "github.com/containous/mux" "github.com/containous/traefik/cluster" + "github.com/containous/traefik/log" "github.com/containous/traefik/middlewares" "github.com/containous/traefik/provider" "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. func (server *Server) Start() { - server.startLeadership() server.startHTTPServers() + server.startLeadership() server.routinesPool.Go(func(stop chan bool) { server.listenProviders(stop) }) @@ -129,7 +129,7 @@ func (server *Server) Close() { if ctx.Err() == context.Canceled { return } else if ctx.Err() == context.DeadlineExceeded { - log.Debugf("I love you all :'( ✝") + log.Warnf("Timeout while stopping traefik, killing instance ✝") os.Exit(1) } }(ctx) @@ -147,17 +147,17 @@ func (server *Server) Close() { func (server *Server) startLeadership() { if server.leadership != nil { server.leadership.Participate(server.routinesPool) - server.leadership.GoCtx(func(ctx context.Context) { - log.Debugf("Started test routine") - <-ctx.Done() - log.Debugf("Stopped test routine") - }) + // server.leadership.AddGoCtx(func(ctx context.Context) { + // log.Debugf("Started test routine") + // <-ctx.Done() + // log.Debugf("Stopped test routine") + // }) } } func (server *Server) stopLeadership() { 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() { - 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) for _, configuration := range currentConfigurations { for _, frontend := range configuration.Frontends { @@ -401,9 +407,16 @@ func (server *Server) createTLSConfig(entryPointName string, tlsOption *TLS, rou } return false } - err := server.globalConfiguration.ACME.CreateLocalConfig(config, checkOnDemandDomain) - if err != nil { - return nil, err + if server.leadership == nil { + err := server.globalConfiguration.ACME.CreateLocalConfig(config, checkOnDemandDomain) + if err != nil { + return nil, err + } + } else { + err := server.globalConfiguration.ACME.CreateClusterConfig(server.leadership, config, checkOnDemandDomain) + if err != nil { + return nil, err + } } } } else { diff --git a/traefik.go b/traefik.go index e5f65468a..397133819 100644 --- a/traefik.go +++ b/traefik.go @@ -12,10 +12,11 @@ import ( "strings" "text/template" - log "github.com/Sirupsen/logrus" + "github.com/Sirupsen/logrus" "github.com/containous/flaeg" "github.com/containous/staert" "github.com/containous/traefik/acme" + "github.com/containous/traefik/log" "github.com/containous/traefik/middlewares" "github.com/containous/traefik/provider" "github.com/containous/traefik/types" @@ -208,7 +209,7 @@ func run(traefikConfiguration *TraefikConfiguration) { } // logging - level, err := log.ParseLevel(strings.ToLower(globalConfiguration.LogLevel)) + level, err := logrus.ParseLevel(strings.ToLower(globalConfiguration.LogLevel)) if err != nil { log.Error("Error getting level", err) } @@ -224,10 +225,10 @@ func run(traefikConfiguration *TraefikConfiguration) { log.Error("Error opening file", err) } else { log.SetOutput(fi) - log.SetFormatter(&log.TextFormatter{DisableColors: true, FullTimestamp: true, DisableSorting: true}) + log.SetFormatter(&logrus.TextFormatter{DisableColors: true, FullTimestamp: true, DisableSorting: true}) } } else { - log.SetFormatter(&log.TextFormatter{FullTimestamp: true, DisableSorting: true}) + log.SetFormatter(&logrus.TextFormatter{FullTimestamp: true, DisableSorting: true}) } jsonConf, _ := json.Marshal(globalConfiguration) log.Infof("Traefik version %s built on %s", version.Version, version.BuildDate) diff --git a/types/types.go b/types/types.go index e2208180b..b9d8298f8 100644 --- a/types/types.go +++ b/types/types.go @@ -201,7 +201,7 @@ type Store struct { // Cluster holds cluster config type Cluster struct { - Node string + Node string `description:"Node name"` Store *Store } diff --git a/web.go b/web.go index 21b2bd130..974c2fbf8 100644 --- a/web.go +++ b/web.go @@ -8,7 +8,7 @@ import ( "net/http" "runtime" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/codegangsta/negroni" "github.com/containous/mux" "github.com/containous/traefik/autogen"