Merge pull request #625 from containous/add-ha-acme-support
HA acme support
This commit is contained in:
commit
d4da14cf18
46 changed files with 1660 additions and 391 deletions
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
glide.lock binary
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,7 +2,6 @@
|
||||||
gen.go
|
gen.go
|
||||||
.idea
|
.idea
|
||||||
.intellij
|
.intellij
|
||||||
log
|
|
||||||
*.iml
|
*.iml
|
||||||
traefik
|
traefik
|
||||||
traefik.toml
|
traefik.toml
|
||||||
|
|
202
acme/account.go
Normal file
202
acme/account.go
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
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]*ChallengeCert
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChallengeCert stores a challenge certificate
|
||||||
|
type ChallengeCert struct {
|
||||||
|
Certificate []byte
|
||||||
|
PrivateKey []byte
|
||||||
|
certificate *tls.Certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init inits acccount struct
|
||||||
|
func (a *Account) Init() error {
|
||||||
|
err := a.DomainsCertificate.Init()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cert := range a.ChallengeCerts {
|
||||||
|
if cert.certificate == nil {
|
||||||
|
certificate, err := tls.X509KeyPair(cert.Certificate, cert.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cert.certificate = &certificate
|
||||||
|
}
|
||||||
|
if cert.certificate.Leaf == nil {
|
||||||
|
leaf, err := x509.ParseCertificate(cert.certificate.Certificate[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cert.certificate.Leaf = leaf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAccount creates an account
|
||||||
|
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: DomainsCertificates{Certs: domainsCerts.Certs},
|
||||||
|
ChallengeCerts: map[string]*ChallengeCert{}}, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init inits DomainsCertificates
|
||||||
|
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 * 30 * time.Hour))) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
600
acme/acme.go
600
acme/acme.go
|
@ -1,178 +1,38 @@
|
||||||
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/cenk/backoff"
|
||||||
|
"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."`
|
StorageFile string // deprecated
|
||||||
OnHostRule bool `description:"Enable certificate generation on frontends Host rules."`
|
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."`
|
||||||
CAServer string `description:"CA server to use."`
|
OnHostRule bool `description:"Enable certificate generation on frontends Host rules."`
|
||||||
EntryPoint string `description:"Entrypoint to proxy acme challenge to."`
|
CAServer string `description:"CA server to use."`
|
||||||
storageLock sync.RWMutex
|
EntryPoint string `description:"Entrypoint to proxy acme challenge to."`
|
||||||
client *acme.Client
|
client *acme.Client
|
||||||
account *Account
|
defaultCertificate *tls.Certificate
|
||||||
|
store cluster.Store
|
||||||
|
challengeProvider *challengeProvider
|
||||||
|
checkOnDemandDomain func(domain string) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
//Domains parse []Domain
|
//Domains parse []Domain
|
||||||
|
@ -216,68 +76,198 @@ type Domain struct {
|
||||||
SANs []string
|
SANs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateConfig creates a tls.config from using ACME configuration
|
func (a *ACME) init() error {
|
||||||
func (a *ACME) CreateConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(domain string) bool) error {
|
|
||||||
acme.Logger = fmtlog.New(ioutil.Discard, "", 0)
|
acme.Logger = fmtlog.New(ioutil.Discard, "", 0)
|
||||||
|
// no certificates in TLS config, so we add a default one
|
||||||
if len(a.StorageFile) == 0 {
|
cert, err := generateDefaultCertificate()
|
||||||
return errors.New("Empty StorageFile, please provide a filename for certs storage")
|
if err != nil {
|
||||||
}
|
return err
|
||||||
|
}
|
||||||
log.Debugf("Generating default certificate...")
|
a.defaultCertificate = cert
|
||||||
if len(tlsConfig.Certificates) == 0 {
|
// TODO: to remove in the futurs
|
||||||
// no certificates in TLS config, so we add a default one
|
if len(a.StorageFile) > 0 && len(a.Storage) == 0 {
|
||||||
cert, err := generateDefaultCertificate()
|
log.Warnf("ACME.StorageFile is deprecated, use ACME.Storage instead")
|
||||||
if err != nil {
|
a.Storage = a.StorageFile
|
||||||
return err
|
}
|
||||||
}
|
return nil
|
||||||
tlsConfig.Certificates = append(tlsConfig.Certificates, *cert)
|
}
|
||||||
}
|
|
||||||
var needRegister bool
|
// CreateClusterConfig creates a tls.config using ACME configuration in cluster mode
|
||||||
var err error
|
func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tls.Config, checkOnDemandDomain func(domain string) bool) error {
|
||||||
|
err := a.init()
|
||||||
// if certificates in storage, load them
|
if err != nil {
|
||||||
if fileInfo, err := os.Stat(a.StorageFile); err == nil && fileInfo.Size() != 0 {
|
return err
|
||||||
log.Infof("Loading ACME certificates...")
|
}
|
||||||
// load account
|
if len(a.Storage) == 0 {
|
||||||
a.account, err = a.loadAccount(a)
|
return errors.New("Empty Store, please provide a key for certs storage")
|
||||||
if err != nil {
|
}
|
||||||
return err
|
a.checkOnDemandDomain = checkOnDemandDomain
|
||||||
}
|
tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate)
|
||||||
} else {
|
tlsConfig.GetCertificate = a.getCertificate
|
||||||
log.Infof("Generating ACME Account...")
|
listener := func(object cluster.Object) error {
|
||||||
// Create a user. New accounts need an email and private key to start
|
account := object.(*Account)
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
account.Init()
|
||||||
if err != nil {
|
if !leadership.IsLeader() {
|
||||||
return err
|
a.client, err = a.buildACMEClient(account)
|
||||||
}
|
if err != nil {
|
||||||
a.account = &Account{
|
log.Errorf("Error building ACME client %+v: %s", object, err.Error())
|
||||||
Email: a.Email,
|
}
|
||||||
PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey),
|
}
|
||||||
}
|
return nil
|
||||||
a.account.DomainsCertificate = DomainsCertificates{Certs: []*DomainsCertificate{}, lock: &sync.RWMutex{}}
|
}
|
||||||
needRegister = true
|
|
||||||
}
|
datastore, err := cluster.NewDataStore(
|
||||||
|
staert.KvSource{
|
||||||
a.client, err = a.buildACMEClient()
|
Store: leadership.Store,
|
||||||
|
Prefix: a.Storage,
|
||||||
|
},
|
||||||
|
leadership.Pool.Ctx(), &Account{},
|
||||||
|
listener)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.store = datastore
|
||||||
|
a.challengeProvider = &challengeProvider{store: 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
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
// Let's Encrypt Subscriber Agreement renew ?
|
||||||
|
reg, err := a.client.QueryRegistration()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
account.Registration = reg
|
||||||
|
err = a.client.AgreeToTOS()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error sending ACME agreement to TOS: %+v: %s", account, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 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.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 = &challengeProvider{store: a.store}
|
||||||
|
|
||||||
|
var needRegister bool
|
||||||
|
var account *Account
|
||||||
|
|
||||||
|
if fileInfo, fileErr := os.Stat(a.Storage); fileErr == nil && fileInfo.Size() != 0 {
|
||||||
|
log.Infof("Loading ACME Account...")
|
||||||
|
// load account
|
||||||
|
object, err := localStore.Load()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
account = object.(*Account)
|
||||||
|
} else {
|
||||||
|
log.Infof("Generating ACME Account...")
|
||||||
|
account, err = NewAccount(a.Email)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
needRegister = true
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("buildACMEClient...")
|
||||||
|
a.client, err = a.buildACMEClient(account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
a.client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01})
|
|
||||||
wrapperChallengeProvider := newWrapperChallengeProvider()
|
|
||||||
a.client.SetChallengeProvider(acme.TLSSNI01, wrapperChallengeProvider)
|
|
||||||
|
|
||||||
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 ?
|
||||||
|
@ -285,49 +275,34 @@ func (a *ACME) CreateConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(doma
|
||||||
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 {
|
for range ticker.C {
|
||||||
select {
|
if err := a.renewCertificates(); err != nil {
|
||||||
case <-ticker.C:
|
log.Errorf("Error renewing ACME certificate %+v: %s", account, err.Error())
|
||||||
if err := a.renewCertificates(a.client); err != nil {
|
|
||||||
log.Errorf("Error renewing ACME certificate %+v: %s", a.account, err.Error())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -335,26 +310,54 @@ func (a *ACME) CreateConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(doma
|
||||||
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 domain cert %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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -362,12 +365,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,
|
||||||
|
@ -386,13 +395,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -400,33 +410,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
|
||||||
|
@ -435,6 +457,23 @@ 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() {
|
||||||
|
operation := func() error {
|
||||||
|
if a.client == nil {
|
||||||
|
return fmt.Errorf("ACME client still not built")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
notify := func(err error, time time.Duration) {
|
||||||
|
log.Errorf("Error getting ACME client: %v, retrying in %s", err, time)
|
||||||
|
}
|
||||||
|
ebo := backoff.NewExponentialBackOff()
|
||||||
|
ebo.MaxElapsedTime = 30 * time.Second
|
||||||
|
err := backoff.RetryNotify(operation, ebo, notify)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error getting ACME client: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
account := a.store.Get().(*Account)
|
||||||
var domain Domain
|
var domain Domain
|
||||||
if len(domains) == 0 {
|
if len(domains) == 0 {
|
||||||
// no domain
|
// no domain
|
||||||
|
@ -445,64 +484,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.StorageFile)
|
|
||||||
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 storage %s", acmeConfig.StorageFile)
|
|
||||||
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.StorageFile, 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)
|
||||||
|
|
|
@ -63,7 +63,7 @@ func TestDomainsSetAppend(t *testing.T) {
|
||||||
|
|
||||||
func TestCertificatesRenew(t *testing.T) {
|
func TestCertificatesRenew(t *testing.T) {
|
||||||
domainsCertificates := DomainsCertificates{
|
domainsCertificates := DomainsCertificates{
|
||||||
lock: &sync.RWMutex{},
|
lock: sync.RWMutex{},
|
||||||
Certs: []*DomainsCertificate{
|
Certs: []*DomainsCertificate{
|
||||||
{
|
{
|
||||||
Domains: Domain{
|
Domains: Domain{
|
||||||
|
|
|
@ -2,55 +2,95 @@ package acme
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"crypto/x509"
|
"fmt"
|
||||||
|
"github.com/cenk/backoff"
|
||||||
|
"github.com/containous/traefik/cluster"
|
||||||
|
"github.com/containous/traefik/log"
|
||||||
"github.com/xenolf/lego/acme"
|
"github.com/xenolf/lego/acme"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type wrapperChallengeProvider struct {
|
var _ acme.ChallengeProviderTimeout = (*challengeProvider)(nil)
|
||||||
challengeCerts map[string]*tls.Certificate
|
|
||||||
lock sync.RWMutex
|
type challengeProvider struct {
|
||||||
|
store cluster.Store
|
||||||
|
lock sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func newWrapperChallengeProvider() *wrapperChallengeProvider {
|
func (c *challengeProvider) getCertificate(domain string) (cert *tls.Certificate, exists bool) {
|
||||||
return &wrapperChallengeProvider{
|
log.Debugf("Challenge GetCertificate %s", domain)
|
||||||
challengeCerts: map[string]*tls.Certificate{},
|
if !strings.HasSuffix(domain, ".acme.invalid") {
|
||||||
|
return nil, false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (c *wrapperChallengeProvider) getCertificate(domain string) (cert *tls.Certificate, exists bool) {
|
|
||||||
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)
|
||||||
return cert, true
|
if account.ChallengeCerts == nil {
|
||||||
|
return nil, false
|
||||||
}
|
}
|
||||||
return nil, false
|
account.Init()
|
||||||
|
var result *tls.Certificate
|
||||||
|
operation := func() error {
|
||||||
|
for _, cert := range account.ChallengeCerts {
|
||||||
|
for _, dns := range cert.certificate.Leaf.DNSNames {
|
||||||
|
if domain == dns {
|
||||||
|
result = cert.certificate
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Cannot find challenge cert for domain %s", domain)
|
||||||
|
}
|
||||||
|
notify := func(err error, time time.Duration) {
|
||||||
|
log.Errorf("Error getting cert: %v, retrying in %s", err, time)
|
||||||
|
}
|
||||||
|
ebo := backoff.NewExponentialBackOff()
|
||||||
|
ebo.MaxElapsedTime = 60 * time.Second
|
||||||
|
err := backoff.RetryNotify(operation, ebo, notify)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error getting cert: %v", err)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return result, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *wrapperChallengeProvider) Present(domain, token, keyAuth string) error {
|
func (c *challengeProvider) Present(domain, token, keyAuth string) error {
|
||||||
cert, _, err := acme.TLSSNI01ChallengeCert(keyAuth)
|
log.Debugf("Challenge Present %s", domain)
|
||||||
if err != nil {
|
cert, _, err := TLSSNI01ChallengeCert(keyAuth)
|
||||||
return err
|
|
||||||
}
|
|
||||||
cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
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]*ChallengeCert{}
|
||||||
|
}
|
||||||
|
account.ChallengeCerts[domain] = &cert
|
||||||
|
return transaction.Commit(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *wrapperChallengeProvider) 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package acme
|
package acme
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
@ -76,3 +78,48 @@ func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain strin
|
||||||
|
|
||||||
return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
|
return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TLSSNI01ChallengeCert returns a certificate and target domain for the `tls-sni-01` challenge
|
||||||
|
func TLSSNI01ChallengeCert(keyAuth string) (ChallengeCert, string, error) {
|
||||||
|
// generate a new RSA key for the certificates
|
||||||
|
var tempPrivKey crypto.PrivateKey
|
||||||
|
tempPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
return ChallengeCert{}, "", err
|
||||||
|
}
|
||||||
|
rsaPrivKey := tempPrivKey.(*rsa.PrivateKey)
|
||||||
|
rsaPrivPEM := pemEncode(rsaPrivKey)
|
||||||
|
|
||||||
|
zBytes := sha256.Sum256([]byte(keyAuth))
|
||||||
|
z := hex.EncodeToString(zBytes[:sha256.Size])
|
||||||
|
domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:])
|
||||||
|
tempCertPEM, err := generatePemCert(rsaPrivKey, domain)
|
||||||
|
if err != nil {
|
||||||
|
return ChallengeCert{}, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM)
|
||||||
|
if err != nil {
|
||||||
|
return ChallengeCert{}, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ChallengeCert{Certificate: tempCertPEM, PrivateKey: rsaPrivPEM, certificate: &certificate}, domain, nil
|
||||||
|
}
|
||||||
|
func pemEncode(data interface{}) []byte {
|
||||||
|
var pemBlock *pem.Block
|
||||||
|
switch key := data.(type) {
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
keyBytes, _ := x509.MarshalECPrivateKey(key)
|
||||||
|
pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
|
||||||
|
break
|
||||||
|
case *x509.CertificateRequest:
|
||||||
|
pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw}
|
||||||
|
break
|
||||||
|
case []byte:
|
||||||
|
pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.([]byte))}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pem.EncodeToMemory(pemBlock)
|
||||||
|
}
|
||||||
|
|
85
acme/localStore.go
Normal file
85
acme/localStore.go
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile(t.file, data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.dirty = true
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
253
cluster/datastore.go
Normal file
253
cluster/datastore.go
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/cenk/backoff"
|
||||||
|
"github.com/containous/staert"
|
||||||
|
"github.com/containous/traefik/job"
|
||||||
|
"github.com/containous/traefik/log"
|
||||||
|
"github.com/docker/libkv/store"
|
||||||
|
"github.com/satori/go.uuid"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Metadata stores Object plus metadata
|
||||||
|
type Metadata struct {
|
||||||
|
object Object
|
||||||
|
Object []byte
|
||||||
|
Lock string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMetadata returns new Metadata
|
||||||
|
func NewMetadata(object Object) *Metadata {
|
||||||
|
return &Metadata{object: object}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshall marshalls object
|
||||||
|
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
|
||||||
|
ctx context.Context
|
||||||
|
localLock *sync.RWMutex
|
||||||
|
meta *Metadata
|
||||||
|
lockKey string
|
||||||
|
listener Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDataStore creates a Datastore
|
||||||
|
func NewDataStore(kvSource staert.KvSource, ctx context.Context, object Object, listener Listener) (*Datastore, error) {
|
||||||
|
datastore := Datastore{
|
||||||
|
kv: kvSource,
|
||||||
|
ctx: ctx,
|
||||||
|
meta: &Metadata{object: object},
|
||||||
|
lockKey: kvSource.Prefix + "/lock",
|
||||||
|
localLock: &sync.RWMutex{},
|
||||||
|
listener: listener,
|
||||||
|
}
|
||||||
|
err := datastore.watchChanges()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &datastore, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Datastore) watchChanges() error {
|
||||||
|
stopCh := make(chan struct{})
|
||||||
|
kvCh, err := d.kv.Watch(d.lockKey, stopCh)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
ctx, cancel := context.WithCancel(d.ctx)
|
||||||
|
operation := func() error {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
stopCh <- struct{}{}
|
||||||
|
return nil
|
||||||
|
case _, ok := <-kvCh:
|
||||||
|
if !ok {
|
||||||
|
cancel()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = d.reload()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// log.Debugf("Datastore object change received: %+v", d.meta)
|
||||||
|
if d.listener != nil {
|
||||||
|
err := d.listener(d.meta.object)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error calling datastore listener: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notify := func(err error, time time.Duration) {
|
||||||
|
log.Errorf("Error in watch datastore: %+v, retrying in %s", err, time)
|
||||||
|
}
|
||||||
|
err := backoff.RetryNotify(operation, job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error in watch datastore: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Datastore) reload() error {
|
||||||
|
log.Debugf("Datastore reload")
|
||||||
|
d.localLock.Lock()
|
||||||
|
err := d.kv.LoadConfig(d.meta)
|
||||||
|
if err != nil {
|
||||||
|
d.localLock.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = d.meta.unmarshall()
|
||||||
|
if err != nil {
|
||||||
|
d.localLock.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.localLock.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin creates a transaction with the KV store.
|
||||||
|
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, nil, err
|
||||||
|
}
|
||||||
|
stopCh := make(chan struct{})
|
||||||
|
ctx, cancel := context.WithCancel(d.ctx)
|
||||||
|
var errLock error
|
||||||
|
go func() {
|
||||||
|
_, errLock = remoteLock.Lock(stopCh)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
if errLock != nil {
|
||||||
|
return nil, nil, errLock
|
||||||
|
}
|
||||||
|
case <-d.ctx.Done():
|
||||||
|
stopCh <- struct{}{}
|
||||||
|
return nil, nil, d.ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// we got the lock! Now make sure we are synced with KV store
|
||||||
|
operation := func() error {
|
||||||
|
meta := d.get()
|
||||||
|
if meta.Lock != id {
|
||||||
|
return fmt.Errorf("Object lock value: expected %s, got %s", id, meta.Lock)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
notify := func(err error, time time.Duration) {
|
||||||
|
log.Errorf("Datastore sync error: %v, retrying in %s", err, time)
|
||||||
|
err = d.reload()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error reloading: %+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ebo := backoff.NewExponentialBackOff()
|
||||||
|
ebo.MaxElapsedTime = 60 * time.Second
|
||||||
|
err = backoff.RetryNotify(operation, ebo, notify)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("Datastore cannot sync: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we synced with KV store, we can now return Setter
|
||||||
|
return &datastoreTransaction{
|
||||||
|
Datastore: d,
|
||||||
|
remoteLock: remoteLock,
|
||||||
|
id: id,
|
||||||
|
}, d.meta.object, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Datastore) get() *Metadata {
|
||||||
|
d.localLock.RLock()
|
||||||
|
defer d.localLock.RUnlock()
|
||||||
|
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.meta.object
|
||||||
|
}
|
||||||
|
|
||||||
|
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 *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.")
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.remoteLock.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.dirty = true
|
||||||
|
log.Debugf("Transaction commited %s", s.id)
|
||||||
|
return nil
|
||||||
|
}
|
102
cluster/leadership.go
Normal file
102
cluster/leadership.go
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/cenk/backoff"
|
||||||
|
"github.com/containous/traefik/log"
|
||||||
|
"github.com/containous/traefik/safe"
|
||||||
|
"github.com/containous/traefik/types"
|
||||||
|
"github.com/docker/leadership"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Leadership allows leadership election using a KV store
|
||||||
|
type Leadership struct {
|
||||||
|
*safe.Pool
|
||||||
|
*types.Cluster
|
||||||
|
candidate *leadership.Candidate
|
||||||
|
leader safe.Safe
|
||||||
|
listeners []LeaderListener
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLeadership creates a leadership
|
||||||
|
func NewLeadership(ctx context.Context, cluster *types.Cluster) *Leadership {
|
||||||
|
return &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) {
|
||||||
|
log.Debugf("Node %s running for election", l.Cluster.Node)
|
||||||
|
defer log.Debugf("Node %s no more running for election", l.Cluster.Node)
|
||||||
|
backOff := backoff.NewExponentialBackOff()
|
||||||
|
operation := func() error {
|
||||||
|
return l.run(l.candidate, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
notify := func(err error, time time.Duration) {
|
||||||
|
log.Errorf("Leadership election error %+v, retrying in %s", err, time)
|
||||||
|
}
|
||||||
|
err := backoff.RetryNotify(operation, backOff, notify)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Cannot elect leadership %+v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 resigned", l.Cluster.Node)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Leadership) run(candidate *leadership.Candidate, ctx context.Context) error {
|
||||||
|
electedCh, errCh := candidate.RunForElection()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case elected := <-electedCh:
|
||||||
|
l.onElection(elected)
|
||||||
|
case err := <-errCh:
|
||||||
|
return err
|
||||||
|
case <-ctx.Done():
|
||||||
|
l.candidate.Resign()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
16
cluster/store.go
Normal file
16
cluster/store.go
Normal 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
|
||||||
|
}
|
|
@ -29,7 +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'"`
|
||||||
Constraints types.Constraints `description:"Filter services by constraint, matching with service tags."`
|
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"`
|
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."`
|
||||||
|
@ -73,7 +74,9 @@ func (dep *DefaultEntryPoints) Set(value string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get return the EntryPoints map
|
// Get return the EntryPoints map
|
||||||
func (dep *DefaultEntryPoints) Get() interface{} { return DefaultEntryPoints(*dep) }
|
func (dep *DefaultEntryPoints) Get() interface{} {
|
||||||
|
return DefaultEntryPoints(*dep)
|
||||||
|
}
|
||||||
|
|
||||||
// SetValue sets the EntryPoints map with val
|
// SetValue sets the EntryPoints map with val
|
||||||
func (dep *DefaultEntryPoints) SetValue(val interface{}) {
|
func (dep *DefaultEntryPoints) SetValue(val interface{}) {
|
||||||
|
@ -153,7 +156,9 @@ func (ep *EntryPoints) Set(value string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get return the EntryPoints map
|
// Get return the EntryPoints map
|
||||||
func (ep *EntryPoints) Get() interface{} { return EntryPoints(*ep) }
|
func (ep *EntryPoints) Get() interface{} {
|
||||||
|
return EntryPoints(*ep)
|
||||||
|
}
|
||||||
|
|
||||||
// SetValue sets the EntryPoints map with val
|
// SetValue sets the EntryPoints map with val
|
||||||
func (ep *EntryPoints) SetValue(val interface{}) {
|
func (ep *EntryPoints) SetValue(val interface{}) {
|
||||||
|
|
|
@ -247,7 +247,7 @@ Supported filters:
|
||||||
#
|
#
|
||||||
email = "test@traefik.io"
|
email = "test@traefik.io"
|
||||||
|
|
||||||
# File used for certificates storage.
|
# File or key used for certificates storage.
|
||||||
# WARNING, if you use Traefik in Docker, you have 2 options:
|
# WARNING, if you use Traefik in Docker, you have 2 options:
|
||||||
# - create a file on your host and mount it as a volume
|
# - create a file on your host and mount it as a volume
|
||||||
# storageFile = "acme.json"
|
# storageFile = "acme.json"
|
||||||
|
@ -258,7 +258,7 @@ email = "test@traefik.io"
|
||||||
#
|
#
|
||||||
# Required
|
# Required
|
||||||
#
|
#
|
||||||
storageFile = "acme.json"
|
storage = "acme.json" # or "traefik/acme/account" if using KV store
|
||||||
|
|
||||||
# Entrypoint to proxy acme challenge to.
|
# Entrypoint to proxy acme challenge to.
|
||||||
# WARNING, must point to an entrypoint on port 443
|
# WARNING, must point to an entrypoint on port 443
|
||||||
|
|
19
docs/user-guide/cluster.md
Normal file
19
docs/user-guide/cluster.md
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# Clustering / High Availability
|
||||||
|
|
||||||
|
This guide explains how tu use Træfɪk in high availability mode.
|
||||||
|
In order to deploy and configure multiple Træfɪk instances, without copying the same configuration file on each instance, we will use a distributed Key-Value store.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
You will need a working KV store cluster.
|
||||||
|
|
||||||
|
## File configuration to KV store migration
|
||||||
|
|
||||||
|
We created a special Træfɪk command to help configuring your Key Value store from a Træfɪk TOML configuration file.
|
||||||
|
Please refer to [this section](/user-guide/kv-config/#store-configuration-in-key-value-store) to get more details.
|
||||||
|
|
||||||
|
## Deploy a Træfɪk cluster
|
||||||
|
|
||||||
|
Once your Træfɪk configuration is uploaded on your KV store, you can start each Træfɪk instance.
|
||||||
|
A Træfɪk cluster is based on a master/slave model. When starting, Træfɪk will elect a master. If this instance fails, another master will be automatically elected.
|
||||||
|
|
|
@ -302,6 +302,7 @@ Further, if the `/traefik/alias` key is set, all other configuration with `/trae
|
||||||
|
|
||||||
# Store configuration in Key-value store
|
# Store configuration in Key-value store
|
||||||
|
|
||||||
|
Don't forget to [setup the connection between Træfɪk and Key-value store](/user-guide/kv-config/#launch-trfk).
|
||||||
The static Træfɪk configuration in a key-value store can be automatically created and updated, using the [`storeconfig` subcommand](/basics/#commands).
|
The static Træfɪk configuration in a key-value store can be automatically created and updated, using the [`storeconfig` subcommand](/basics/#commands).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -309,6 +310,19 @@ $ traefik storeconfig [flags] ...
|
||||||
```
|
```
|
||||||
This command is here only to automate the [process which upload the configuration into the Key-value store](/user-guide/kv-config/#upload-the-configuration-in-the-key-value-store).
|
This command is here only to automate the [process which upload the configuration into the Key-value store](/user-guide/kv-config/#upload-the-configuration-in-the-key-value-store).
|
||||||
Træfɪk will not start but the [static configuration](/basics/#static-trfk-configuration) will be uploaded into the Key-value store.
|
Træfɪk will not start but the [static configuration](/basics/#static-trfk-configuration) will be uploaded into the Key-value store.
|
||||||
|
If you configured ACME (Let's Encrypt), your registration account and your certificates will also be uploaded.
|
||||||
|
|
||||||
|
To upload your ACME certificates to the KV store, get your traefik TOML file and add the new `storage` option in the `acme` section:
|
||||||
|
|
||||||
|
```
|
||||||
|
[acme]
|
||||||
|
email = "test@traefik.io"
|
||||||
|
storage = "traefik/acme/account" # the key where to store your certificates in the KV store
|
||||||
|
storageFile = "acme.json" # your old certificates store
|
||||||
|
```
|
||||||
|
|
||||||
|
Call `traefik storeconfig` to upload your config in the KV store.
|
||||||
|
Then remove the line `storageFile = "acme.json"` from your TOML config file.
|
||||||
|
That's it!
|
||||||
|
|
||||||
Don't forget to [setup the connection between Træfɪk and Key-value store](/user-guide/kv-config/#launch-trfk).
|
|
||||||
|
|
||||||
|
|
10
glide.lock
generated
10
glide.lock
generated
|
@ -1,5 +1,5 @@
|
||||||
hash: c0ac205a859d78847e21d3cd63f427ffba985755c6ae84373e4a20364ba39b05
|
hash: 39ff28cc1d13d5915a870b14491ece1849c4eaf5a56cecd50a7676ecee6c6143
|
||||||
updated: 2016-09-30T10:57:42.336729457+02:00
|
updated: 2016-09-30T11:27:29.529525636+02:00
|
||||||
imports:
|
imports:
|
||||||
- name: github.com/abbot/go-http-auth
|
- name: github.com/abbot/go-http-auth
|
||||||
version: cb4372376e1e00e9f6ab9ec142e029302c9e7140
|
version: cb4372376e1e00e9f6ab9ec142e029302c9e7140
|
||||||
|
@ -24,7 +24,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: 92329254783dc01174f03302d51d7cf2c9ff84cf
|
||||||
- name: github.com/coreos/etcd
|
- name: github.com/coreos/etcd
|
||||||
version: 1c9e0a0e33051fed6c05c141e6fcbfe5c7f2a899
|
version: 1c9e0a0e33051fed6c05c141e6fcbfe5c7f2a899
|
||||||
subpackages:
|
subpackages:
|
||||||
|
@ -123,6 +123,8 @@ imports:
|
||||||
- tlsconfig
|
- tlsconfig
|
||||||
- name: github.com/docker/go-units
|
- name: github.com/docker/go-units
|
||||||
version: f2d77a61e3c169b43402a0a1e84f06daf29b8190
|
version: f2d77a61e3c169b43402a0a1e84f06daf29b8190
|
||||||
|
- name: github.com/docker/leadership
|
||||||
|
version: bfc7753dd48af19513b29deec23c364bf0f274eb
|
||||||
- name: github.com/docker/libcompose
|
- name: github.com/docker/libcompose
|
||||||
version: d1876c1d68527a49c0aac22a0b161acc7296b740
|
version: d1876c1d68527a49c0aac22a0b161acc7296b740
|
||||||
subpackages:
|
subpackages:
|
||||||
|
@ -243,6 +245,8 @@ imports:
|
||||||
version: e64db453f3512cade908163702045e0f31137843
|
version: e64db453f3512cade908163702045e0f31137843
|
||||||
subpackages:
|
subpackages:
|
||||||
- zk
|
- zk
|
||||||
|
- name: github.com/satori/go.uuid
|
||||||
|
version: 879c5887cd475cd7864858769793b2ceb0d44feb
|
||||||
- name: github.com/Sirupsen/logrus
|
- name: github.com/Sirupsen/logrus
|
||||||
version: a283a10442df8dc09befd873fab202bf8a253d6a
|
version: a283a10442df8dc09befd873fab202bf8a253d6a
|
||||||
- name: github.com/streamrail/concurrent-map
|
- name: github.com/streamrail/concurrent-map
|
||||||
|
|
|
@ -21,7 +21,7 @@ import:
|
||||||
- stream
|
- stream
|
||||||
- utils
|
- utils
|
||||||
- package: github.com/containous/staert
|
- package: github.com/containous/staert
|
||||||
version: 044bdfee6c8f5e8fb71f70d5ba1cf4cb11a94e97
|
version: 92329254783dc01174f03302d51d7cf2c9ff84cf
|
||||||
- package: github.com/docker/engine-api
|
- package: github.com/docker/engine-api
|
||||||
version: 62043eb79d581a32ea849645277023c550732e52
|
version: 62043eb79d581a32ea849645277023c550732e52
|
||||||
subpackages:
|
subpackages:
|
||||||
|
@ -99,3 +99,6 @@ import:
|
||||||
- package: github.com/miekg/dns
|
- package: github.com/miekg/dns
|
||||||
version: 5d001d020961ae1c184f9f8152fdc73810481677
|
version: 5d001d020961ae1c184f9f8152fdc73810481677
|
||||||
- package: github.com/NYTimes/gziphandler
|
- package: github.com/NYTimes/gziphandler
|
||||||
|
- package: github.com/docker/leadership
|
||||||
|
- package: github.com/satori/go.uuid
|
||||||
|
version: ^1.1.0
|
||||||
|
|
|
@ -5,18 +5,22 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/containous/staert"
|
||||||
"github.com/docker/libkv"
|
"github.com/docker/libkv"
|
||||||
"github.com/docker/libkv/store"
|
"github.com/docker/libkv/store"
|
||||||
"github.com/docker/libkv/store/consul"
|
"github.com/docker/libkv/store/consul"
|
||||||
"github.com/go-check/check"
|
"github.com/go-check/check"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/containous/traefik/cluster"
|
||||||
"github.com/containous/traefik/integration/utils"
|
"github.com/containous/traefik/integration/utils"
|
||||||
"github.com/containous/traefik/provider"
|
"github.com/containous/traefik/provider"
|
||||||
checker "github.com/vdemeester/shakers"
|
checker "github.com/vdemeester/shakers"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Consul test suites (using libcompose)
|
// Consul test suites (using libcompose)
|
||||||
|
@ -427,3 +431,93 @@ func (s *ConsulSuite) TestCommandStoreConfig(c *check.C) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TestStruct struct {
|
||||||
|
String string
|
||||||
|
Int int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConsulSuite) TestDatastore(c *check.C) {
|
||||||
|
s.setupConsul(c)
|
||||||
|
consulHost := s.composeProject.Container(c, "consul").NetworkSettings.IPAddress
|
||||||
|
kvSource, err := staert.NewKvSource(store.CONSUL, []string{consulHost + ":8500"}, &store.Config{
|
||||||
|
ConnectionTimeout: 10 * time.Second,
|
||||||
|
}, "traefik")
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
datastore1, err := cluster.NewDataStore(*kvSource, ctx, &TestStruct{}, nil)
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
datastore2, err := cluster.NewDataStore(*kvSource, ctx, &TestStruct{}, nil)
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
|
setter1, _, err := datastore1.Begin()
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
err = setter1.Commit(&TestStruct{
|
||||||
|
String: "foo",
|
||||||
|
Int: 1,
|
||||||
|
})
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
test1 := datastore1.Get().(*TestStruct)
|
||||||
|
c.Assert(test1.String, checker.Equals, "foo")
|
||||||
|
|
||||||
|
test2 := datastore2.Get().(*TestStruct)
|
||||||
|
c.Assert(test2.String, checker.Equals, "foo")
|
||||||
|
|
||||||
|
setter2, _, err := datastore2.Begin()
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
err = setter2.Commit(&TestStruct{
|
||||||
|
String: "bar",
|
||||||
|
Int: 2,
|
||||||
|
})
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
test1 = datastore1.Get().(*TestStruct)
|
||||||
|
c.Assert(test1.String, checker.Equals, "bar")
|
||||||
|
|
||||||
|
test2 = datastore2.Get().(*TestStruct)
|
||||||
|
c.Assert(test2.String, checker.Equals, "bar")
|
||||||
|
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
wg.Add(4)
|
||||||
|
go func() {
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
setter1, _, err := datastore1.Begin()
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
err = setter1.Commit(&TestStruct{
|
||||||
|
String: "datastore1",
|
||||||
|
Int: i,
|
||||||
|
})
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
setter2, _, err := datastore2.Begin()
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
err = setter2.Commit(&TestStruct{
|
||||||
|
String: "datastore2",
|
||||||
|
Int: i,
|
||||||
|
})
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
test1 := datastore1.Get().(*TestStruct)
|
||||||
|
c.Assert(test1, checker.NotNil)
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
test2 := datastore2.Get().(*TestStruct)
|
||||||
|
c.Assert(test2, checker.NotNil)
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
188
log/logger.go
Normal file
188
log/logger.go
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logger *logrus.Entry
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
logger = logrus.StandardLogger().WithFields(logrus.Fields{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context sets the Context of the logger
|
||||||
|
func Context(context interface{}) *logrus.Entry {
|
||||||
|
return logger.WithField("context", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOutput sets the standard logger output.
|
||||||
|
func SetOutput(out io.Writer) {
|
||||||
|
logrus.SetOutput(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFormatter sets the standard logger formatter.
|
||||||
|
func SetFormatter(formatter logrus.Formatter) {
|
||||||
|
logrus.SetFormatter(formatter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLevel sets the standard logger level.
|
||||||
|
func SetLevel(level logrus.Level) {
|
||||||
|
logrus.SetLevel(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLevel returns the standard logger level.
|
||||||
|
func GetLevel() logrus.Level {
|
||||||
|
return logrus.GetLevel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddHook adds a hook to the standard logger hooks.
|
||||||
|
func AddHook(hook logrus.Hook) {
|
||||||
|
logrus.AddHook(hook)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithError creates an entry from the standard logger and adds an error to it, using the value defined in ErrorKey as key.
|
||||||
|
func WithError(err error) *logrus.Entry {
|
||||||
|
return logger.WithError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithField creates an entry from the standard logger and adds a field to
|
||||||
|
// it. If you want multiple fields, use `WithFields`.
|
||||||
|
//
|
||||||
|
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
|
||||||
|
// or Panic on the Entry it returns.
|
||||||
|
func WithField(key string, value interface{}) *logrus.Entry {
|
||||||
|
return logger.WithField(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFields creates an entry from the standard logger and adds multiple
|
||||||
|
// fields to it. This is simply a helper for `WithField`, invoking it
|
||||||
|
// once for each field.
|
||||||
|
//
|
||||||
|
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
|
||||||
|
// or Panic on the Entry it returns.
|
||||||
|
func WithFields(fields logrus.Fields) *logrus.Entry {
|
||||||
|
return logger.WithFields(fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logs a message at level Debug on the standard logger.
|
||||||
|
func Debug(args ...interface{}) {
|
||||||
|
logger.Debug(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print logs a message at level Info on the standard logger.
|
||||||
|
func Print(args ...interface{}) {
|
||||||
|
logger.Print(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info logs a message at level Info on the standard logger.
|
||||||
|
func Info(args ...interface{}) {
|
||||||
|
logger.Info(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn logs a message at level Warn on the standard logger.
|
||||||
|
func Warn(args ...interface{}) {
|
||||||
|
logger.Warn(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warning logs a message at level Warn on the standard logger.
|
||||||
|
func Warning(args ...interface{}) {
|
||||||
|
logger.Warning(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error logs a message at level Error on the standard logger.
|
||||||
|
func Error(args ...interface{}) {
|
||||||
|
logger.Error(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panic logs a message at level Panic on the standard logger.
|
||||||
|
func Panic(args ...interface{}) {
|
||||||
|
logger.Panic(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatal logs a message at level Fatal on the standard logger.
|
||||||
|
func Fatal(args ...interface{}) {
|
||||||
|
logger.Fatal(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugf logs a message at level Debug on the standard logger.
|
||||||
|
func Debugf(format string, args ...interface{}) {
|
||||||
|
logger.Debugf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printf logs a message at level Info on the standard logger.
|
||||||
|
func Printf(format string, args ...interface{}) {
|
||||||
|
logger.Printf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infof logs a message at level Info on the standard logger.
|
||||||
|
func Infof(format string, args ...interface{}) {
|
||||||
|
logger.Infof(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warnf logs a message at level Warn on the standard logger.
|
||||||
|
func Warnf(format string, args ...interface{}) {
|
||||||
|
logger.Warnf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warningf logs a message at level Warn on the standard logger.
|
||||||
|
func Warningf(format string, args ...interface{}) {
|
||||||
|
logger.Warningf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf logs a message at level Error on the standard logger.
|
||||||
|
func Errorf(format string, args ...interface{}) {
|
||||||
|
logger.Errorf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panicf logs a message at level Panic on the standard logger.
|
||||||
|
func Panicf(format string, args ...interface{}) {
|
||||||
|
logger.Panicf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatalf logs a message at level Fatal on the standard logger.
|
||||||
|
func Fatalf(format string, args ...interface{}) {
|
||||||
|
logger.Fatalf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugln logs a message at level Debug on the standard logger.
|
||||||
|
func Debugln(args ...interface{}) {
|
||||||
|
logger.Debugln(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Println logs a message at level Info on the standard logger.
|
||||||
|
func Println(args ...interface{}) {
|
||||||
|
logger.Println(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infoln logs a message at level Info on the standard logger.
|
||||||
|
func Infoln(args ...interface{}) {
|
||||||
|
logger.Infoln(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warnln logs a message at level Warn on the standard logger.
|
||||||
|
func Warnln(args ...interface{}) {
|
||||||
|
logger.Warnln(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warningln logs a message at level Warn on the standard logger.
|
||||||
|
func Warningln(args ...interface{}) {
|
||||||
|
logger.Warningln(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorln logs a message at level Error on the standard logger.
|
||||||
|
func Errorln(args ...interface{}) {
|
||||||
|
logger.Errorln(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panicln logs a message at level Panic on the standard logger.
|
||||||
|
func Panicln(args ...interface{}) {
|
||||||
|
logger.Panicln(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatalln logs a message at level Fatal on the standard logger.
|
||||||
|
func Fatalln(args ...interface{}) {
|
||||||
|
logger.Fatalln(args...)
|
||||||
|
}
|
|
@ -2,9 +2,9 @@ package middlewares
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
log "github.com/Sirupsen/logrus"
|
|
||||||
"github.com/abbot/go-http-auth"
|
"github.com/abbot/go-http-auth"
|
||||||
"github.com/codegangsta/negroni"
|
"github.com/codegangsta/negroni"
|
||||||
|
"github.com/containous/traefik/log"
|
||||||
"github.com/containous/traefik/types"
|
"github.com/containous/traefik/types"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -51,3 +51,4 @@ pages:
|
||||||
- 'Swarm cluster': 'user-guide/swarm.md'
|
- 'Swarm cluster': 'user-guide/swarm.md'
|
||||||
- 'Kubernetes': 'user-guide/kubernetes.md'
|
- 'Kubernetes': 'user-guide/kubernetes.md'
|
||||||
- 'Key-value store configuration': 'user-guide/kv-config.md'
|
- 'Key-value store configuration': 'user-guide/kv-config.md'
|
||||||
|
- 'Clustering/HA': 'user-guide/cluster.md'
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"github.com/docker/libkv/store/boltdb"
|
"github.com/docker/libkv/store/boltdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ Provider = (*BoltDb)(nil)
|
||||||
|
|
||||||
// BoltDb holds configurations of the BoltDb provider.
|
// BoltDb holds configurations of the BoltDb provider.
|
||||||
type BoltDb struct {
|
type BoltDb struct {
|
||||||
Kv `mapstructure:",squash"`
|
Kv `mapstructure:",squash"`
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"github.com/docker/libkv/store/consul"
|
"github.com/docker/libkv/store/consul"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ Provider = (*Consul)(nil)
|
||||||
|
|
||||||
// Consul holds configurations of the Consul provider.
|
// Consul holds configurations of the Consul provider.
|
||||||
type Consul struct {
|
type Consul struct {
|
||||||
Kv `mapstructure:",squash"`
|
Kv `mapstructure:",squash"`
|
||||||
|
|
|
@ -9,9 +9,10 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/BurntSushi/ty/fun"
|
"github.com/BurntSushi/ty/fun"
|
||||||
log "github.com/Sirupsen/logrus"
|
"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/log"
|
||||||
"github.com/containous/traefik/safe"
|
"github.com/containous/traefik/safe"
|
||||||
"github.com/containous/traefik/types"
|
"github.com/containous/traefik/types"
|
||||||
"github.com/hashicorp/consul/api"
|
"github.com/hashicorp/consul/api"
|
||||||
|
@ -24,6 +25,8 @@ const (
|
||||||
DefaultConsulCatalogTagPrefix = "traefik"
|
DefaultConsulCatalogTagPrefix = "traefik"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ Provider = (*ConsulCatalog)(nil)
|
||||||
|
|
||||||
// ConsulCatalog holds configurations of the Consul catalog provider.
|
// ConsulCatalog holds configurations of the Consul catalog provider.
|
||||||
type ConsulCatalog struct {
|
type ConsulCatalog struct {
|
||||||
BaseProvider `mapstructure:",squash"`
|
BaseProvider `mapstructure:",squash"`
|
||||||
|
@ -268,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)
|
||||||
|
|
|
@ -13,9 +13,9 @@ 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/cenk/backoff"
|
"github.com/cenk/backoff"
|
||||||
"github.com/containous/traefik/job"
|
"github.com/containous/traefik/job"
|
||||||
|
"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"
|
||||||
"github.com/containous/traefik/version"
|
"github.com/containous/traefik/version"
|
||||||
|
@ -40,6 +40,8 @@ const (
|
||||||
SwarmDefaultWatchTime = 15 * time.Second
|
SwarmDefaultWatchTime = 15 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ Provider = (*Docker)(nil)
|
||||||
|
|
||||||
// Docker holds configurations of the Docker provider.
|
// Docker holds configurations of the Docker provider.
|
||||||
type Docker struct {
|
type Docker struct {
|
||||||
BaseProvider `mapstructure:",squash"`
|
BaseProvider `mapstructure:",squash"`
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"github.com/docker/libkv/store/etcd"
|
"github.com/docker/libkv/store/etcd"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ Provider = (*Etcd)(nil)
|
||||||
|
|
||||||
// Etcd holds configurations of the Etcd provider.
|
// Etcd holds configurations of the Etcd provider.
|
||||||
type Etcd struct {
|
type Etcd struct {
|
||||||
Kv `mapstructure:",squash"`
|
Kv `mapstructure:",squash"`
|
||||||
|
|
|
@ -6,12 +6,14 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ Provider = (*File)(nil)
|
||||||
|
|
||||||
// File holds configurations of the File provider.
|
// File holds configurations of the File provider.
|
||||||
type File struct {
|
type File struct {
|
||||||
BaseProvider `mapstructure:",squash"`
|
BaseProvider `mapstructure:",squash"`
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,12 +14,8 @@ 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/safe"
|
|
||||||
"github.com/containous/traefik/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -50,6 +50,8 @@ func (ns *Namespaces) SetValue(val interface{}) {
|
||||||
*ns = Namespaces(val.(Namespaces))
|
*ns = Namespaces(val.(Namespaces))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ Provider = (*Kubernetes)(nil)
|
||||||
|
|
||||||
// Kubernetes holds configurations of the Kubernetes provider.
|
// Kubernetes holds configurations of the Kubernetes provider.
|
||||||
type Kubernetes struct {
|
type Kubernetes struct {
|
||||||
BaseProvider `mapstructure:",squash"`
|
BaseProvider `mapstructure:",squash"`
|
||||||
|
|
|
@ -9,9 +9,9 @@ import (
|
||||||
|
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/BurntSushi/ty/fun"
|
"github.com/BurntSushi/ty/fun"
|
||||||
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/log"
|
||||||
"github.com/containous/traefik/safe"
|
"github.com/containous/traefik/safe"
|
||||||
"github.com/containous/traefik/types"
|
"github.com/containous/traefik/types"
|
||||||
"github.com/docker/libkv"
|
"github.com/docker/libkv"
|
||||||
|
@ -148,7 +148,7 @@ func (provider *Kv) list(keys ...string) []string {
|
||||||
joinedKeys := strings.Join(keys, "")
|
joinedKeys := strings.Join(keys, "")
|
||||||
keysPairs, err := provider.kvclient.List(joinedKeys)
|
keysPairs, err := provider.kvclient.List(joinedKeys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Error getting keys %s %s ", joinedKeys, err)
|
log.Debugf("Cannot get keys %s %s ", joinedKeys, err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
directoryKeys := make(map[string]string)
|
directoryKeys := make(map[string]string)
|
||||||
|
@ -170,10 +170,10 @@ func (provider *Kv) get(defaultValue string, keys ...string) string {
|
||||||
joinedKeys := strings.Join(keys, "")
|
joinedKeys := strings.Join(keys, "")
|
||||||
keyPair, err := provider.kvclient.Get(strings.TrimPrefix(joinedKeys, "/"))
|
keyPair, err := provider.kvclient.Get(strings.TrimPrefix(joinedKeys, "/"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Error getting key %s %s, setting default %s", joinedKeys, err, defaultValue)
|
log.Debugf("Cannot get key %s %s, setting default %s", joinedKeys, err, defaultValue)
|
||||||
return defaultValue
|
return defaultValue
|
||||||
} else if keyPair == nil {
|
} else if keyPair == nil {
|
||||||
log.Warnf("Error getting key %s, setting default %s", joinedKeys, defaultValue)
|
log.Debugf("Cannot get key %s, setting default %s", joinedKeys, defaultValue)
|
||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
return string(keyPair.Value)
|
return string(keyPair.Value)
|
||||||
|
@ -183,10 +183,10 @@ func (provider *Kv) splitGet(keys ...string) []string {
|
||||||
joinedKeys := strings.Join(keys, "")
|
joinedKeys := strings.Join(keys, "")
|
||||||
keyPair, err := provider.kvclient.Get(joinedKeys)
|
keyPair, err := provider.kvclient.Get(joinedKeys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Error getting key %s %s, setting default empty", joinedKeys, err)
|
log.Debugf("Cannot get key %s %s, setting default empty", joinedKeys, err)
|
||||||
return []string{}
|
return []string{}
|
||||||
} else if keyPair == nil {
|
} else if keyPair == nil {
|
||||||
log.Warnf("Error getting key %s, setting default %empty", joinedKeys)
|
log.Debugf("Cannot get key %s, setting default %empty", joinedKeys)
|
||||||
return []string{}
|
return []string{}
|
||||||
}
|
}
|
||||||
return strings.Split(string(keyPair.Value), ",")
|
return strings.Split(string(keyPair.Value), ",")
|
||||||
|
|
|
@ -13,14 +13,16 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/BurntSushi/ty/fun"
|
"github.com/BurntSushi/ty/fun"
|
||||||
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/log"
|
||||||
"github.com/containous/traefik/safe"
|
"github.com/containous/traefik/safe"
|
||||||
"github.com/containous/traefik/types"
|
"github.com/containous/traefik/types"
|
||||||
"github.com/gambol99/go-marathon"
|
"github.com/gambol99/go-marathon"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ Provider = (*Marathon)(nil)
|
||||||
|
|
||||||
// Marathon holds configuration of the Marathon provider.
|
// Marathon holds configuration of the Marathon provider.
|
||||||
type Marathon struct {
|
type Marathon struct {
|
||||||
BaseProvider
|
BaseProvider
|
||||||
|
|
|
@ -8,9 +8,9 @@ import (
|
||||||
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/BurntSushi/ty/fun"
|
"github.com/BurntSushi/ty/fun"
|
||||||
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/log"
|
||||||
"github.com/containous/traefik/safe"
|
"github.com/containous/traefik/safe"
|
||||||
"github.com/containous/traefik/types"
|
"github.com/containous/traefik/types"
|
||||||
"github.com/mesos/mesos-go/detector"
|
"github.com/mesos/mesos-go/detector"
|
||||||
|
@ -24,6 +24,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ Provider = (*Mesos)(nil)
|
||||||
|
|
||||||
//Mesos holds configuration of the mesos provider.
|
//Mesos holds configuration of the mesos provider.
|
||||||
type Mesos struct {
|
type Mesos struct {
|
||||||
BaseProvider
|
BaseProvider
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -13,8 +13,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
"github.com/BurntSushi/toml"
|
||||||
log "github.com/Sirupsen/logrus"
|
|
||||||
"github.com/containous/traefik/autogen"
|
"github.com/containous/traefik/autogen"
|
||||||
|
"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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"github.com/docker/libkv/store/zookeeper"
|
"github.com/docker/libkv/store/zookeeper"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ Provider = (*Zookepper)(nil)
|
||||||
|
|
||||||
// Zookepper holds configurations of the Zookepper provider.
|
// Zookepper holds configurations of the Zookepper provider.
|
||||||
type Zookepper struct {
|
type Zookepper struct {
|
||||||
Kv `mapstructure:",squash"`
|
Kv `mapstructure:",squash"`
|
||||||
|
|
6
rules.go
6
rules.go
|
@ -149,7 +149,7 @@ func (r *Rules) parseRules(expression string, onRule func(functionName string, f
|
||||||
|
|
||||||
err := onRule(functionName, parsedFunction, parsedArgs)
|
err := onRule(functionName, parsedFunction, parsedArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Parsing error on rule:", err)
|
return fmt.Errorf("Parsing error on rule: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -180,7 +180,7 @@ func (r *Rules) Parse(expression string) (*mux.Route, error) {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error parsing rule:", err)
|
return nil, fmt.Errorf("Error parsing rule: %v", err)
|
||||||
}
|
}
|
||||||
return resultRoute, nil
|
return resultRoute, nil
|
||||||
}
|
}
|
||||||
|
@ -195,7 +195,7 @@ func (r *Rules) ParseDomains(expression string) ([]string, error) {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error parsing domains:", err)
|
return nil, fmt.Errorf("Error parsing domains: %v", err)
|
||||||
}
|
}
|
||||||
return domains, nil
|
return domains, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
package safe
|
package safe
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"github.com/containous/traefik/log"
|
||||||
|
"golang.org/x/net/context"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
@ -11,11 +12,52 @@ type routine struct {
|
||||||
stop chan bool
|
stop chan bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pool creates a pool of go routines
|
type routineCtx func(ctx context.Context)
|
||||||
|
|
||||||
|
// Pool is a pool of go routines
|
||||||
type Pool struct {
|
type Pool struct {
|
||||||
routines []routine
|
routines []routine
|
||||||
waitGroup sync.WaitGroup
|
routinesCtx []routineCtx
|
||||||
lock sync.Mutex
|
waitGroup sync.WaitGroup
|
||||||
|
lock sync.Mutex
|
||||||
|
baseCtx context.Context
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPool creates a Pool
|
||||||
|
func NewPool(parentCtx context.Context) *Pool {
|
||||||
|
baseCtx, _ := context.WithCancel(parentCtx)
|
||||||
|
ctx, cancel := context.WithCancel(baseCtx)
|
||||||
|
return &Pool{
|
||||||
|
baseCtx: baseCtx,
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctx returns main context
|
||||||
|
func (p *Pool) Ctx() context.Context {
|
||||||
|
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
|
||||||
|
func (p *Pool) GoCtx(goroutine routineCtx) {
|
||||||
|
p.lock.Lock()
|
||||||
|
p.routinesCtx = append(p.routinesCtx, goroutine)
|
||||||
|
p.waitGroup.Add(1)
|
||||||
|
Go(func() {
|
||||||
|
goroutine(p.ctx)
|
||||||
|
p.waitGroup.Done()
|
||||||
|
})
|
||||||
|
p.lock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go starts a recoverable goroutine, and can be stopped with stop chan
|
// Go starts a recoverable goroutine, and can be stopped with stop chan
|
||||||
|
@ -37,6 +79,8 @@ 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()
|
||||||
for _, routine := range p.routines {
|
for _, routine := range p.routines {
|
||||||
routine.stop <- true
|
routine.stop <- true
|
||||||
}
|
}
|
||||||
|
@ -44,7 +88,29 @@ 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 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)
|
||||||
|
routine.stop = make(chan bool, 1)
|
||||||
|
Go(func() {
|
||||||
|
routine.goroutine(routine.stop)
|
||||||
|
p.waitGroup.Done()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, routine := range p.routinesCtx {
|
||||||
|
p.waitGroup.Add(1)
|
||||||
|
Go(func() {
|
||||||
|
routine(p.ctx)
|
||||||
|
p.waitGroup.Done()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go starts a recoverable goroutine
|
// Go starts a recoverable goroutine
|
||||||
|
@ -65,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()
|
||||||
}
|
}
|
||||||
|
|
57
server.go
57
server.go
|
@ -21,9 +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/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"
|
||||||
|
@ -50,7 +51,8 @@ type Server struct {
|
||||||
currentConfigurations safe.Safe
|
currentConfigurations safe.Safe
|
||||||
globalConfiguration GlobalConfiguration
|
globalConfiguration GlobalConfiguration
|
||||||
loggerMiddleware *middlewares.Logger
|
loggerMiddleware *middlewares.Logger
|
||||||
routinesPool safe.Pool
|
routinesPool *safe.Pool
|
||||||
|
leadership *cluster.Leadership
|
||||||
}
|
}
|
||||||
|
|
||||||
type serverEntryPoints map[string]*serverEntryPoint
|
type serverEntryPoints map[string]*serverEntryPoint
|
||||||
|
@ -80,6 +82,11 @@ func NewServer(globalConfiguration GlobalConfiguration) *Server {
|
||||||
server.currentConfigurations.Set(currentConfigurations)
|
server.currentConfigurations.Set(currentConfigurations)
|
||||||
server.globalConfiguration = globalConfiguration
|
server.globalConfiguration = globalConfiguration
|
||||||
server.loggerMiddleware = middlewares.NewLogger(globalConfiguration.AccessLogsFile)
|
server.loggerMiddleware = middlewares.NewLogger(globalConfiguration.AccessLogsFile)
|
||||||
|
server.routinesPool = safe.NewPool(context.Background())
|
||||||
|
if globalConfiguration.Cluster != nil {
|
||||||
|
// leadership creation if cluster mode
|
||||||
|
server.leadership = cluster.NewLeadership(server.routinesPool.Ctx(), globalConfiguration.Cluster)
|
||||||
|
}
|
||||||
|
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
@ -87,6 +94,7 @@ 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.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)
|
||||||
})
|
})
|
||||||
|
@ -121,10 +129,11 @@ 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)
|
||||||
|
server.stopLeadership()
|
||||||
server.routinesPool.Stop()
|
server.routinesPool.Stop()
|
||||||
close(server.configurationChan)
|
close(server.configurationChan)
|
||||||
close(server.configurationValidatedChan)
|
close(server.configurationValidatedChan)
|
||||||
|
@ -135,6 +144,23 @@ func (server *Server) Close() {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (server *Server) startLeadership() {
|
||||||
|
if server.leadership != nil {
|
||||||
|
server.leadership.Participate(server.routinesPool)
|
||||||
|
// 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.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (server *Server) startHTTPServers() {
|
func (server *Server) startHTTPServers() {
|
||||||
server.serverEntryPoints = server.buildEntryPoints(server.globalConfiguration)
|
server.serverEntryPoints = server.buildEntryPoints(server.globalConfiguration)
|
||||||
for newServerEntryPointName, newServerEntryPoint := range server.serverEntryPoints {
|
for newServerEntryPointName, newServerEntryPoint := range server.serverEntryPoints {
|
||||||
|
@ -217,7 +243,7 @@ func (server *Server) defaultConfigurationValues(configuration *types.Configurat
|
||||||
for backendName, backend := range configuration.Backends {
|
for backendName, backend := range configuration.Backends {
|
||||||
_, err := types.NewLoadBalancerMethod(backend.LoadBalancer)
|
_, err := types.NewLoadBalancerMethod(backend.LoadBalancer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("Error loading load balancer method '%+v' for backend %s: %v. Using default wrr.", backend.LoadBalancer, backendName, err)
|
log.Debugf("Load balancer method '%+v' for backend %s: %v. Using default wrr.", backend.LoadBalancer, backendName, err)
|
||||||
backend.LoadBalancer = &types.LoadBalancer{Method: "wrr"}
|
backend.LoadBalancer = &types.LoadBalancer{Method: "wrr"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -257,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 {
|
||||||
|
@ -321,7 +353,7 @@ func (server *Server) startProviders() {
|
||||||
log.Infof("Starting provider %v %s", reflect.TypeOf(provider), jsonConf)
|
log.Infof("Starting provider %v %s", reflect.TypeOf(provider), jsonConf)
|
||||||
currentProvider := provider
|
currentProvider := provider
|
||||||
safe.Go(func() {
|
safe.Go(func() {
|
||||||
err := currentProvider.Provide(server.configurationChan, &server.routinesPool, server.globalConfiguration.Constraints)
|
err := currentProvider.Provide(server.configurationChan, server.routinesPool, server.globalConfiguration.Constraints)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Error starting provider %s", err)
|
log.Errorf("Error starting provider %s", err)
|
||||||
}
|
}
|
||||||
|
@ -375,9 +407,16 @@ func (server *Server) createTLSConfig(entryPointName string, tlsOption *TLS, rou
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
err := server.globalConfiguration.ACME.CreateConfig(config, checkOnDemandDomain)
|
if server.leadership == nil {
|
||||||
if err != nil {
|
err := server.globalConfiguration.ACME.CreateLocalConfig(config, checkOnDemandDomain)
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := server.globalConfiguration.ACME.CreateClusterConfig(server.leadership, config, checkOnDemandDomain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
59
traefik.go
59
traefik.go
|
@ -12,15 +12,18 @@ 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/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/types"
|
"github.com/containous/traefik/types"
|
||||||
"github.com/containous/traefik/version"
|
"github.com/containous/traefik/version"
|
||||||
"github.com/docker/libkv/store"
|
"github.com/docker/libkv/store"
|
||||||
|
"github.com/satori/go.uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
var versionTemplate = `Version: {{.Version}}
|
var versionTemplate = `Version: {{.Version}}
|
||||||
|
@ -98,9 +101,37 @@ Complete documentation is available at https://traefik.io`,
|
||||||
if kv == nil {
|
if kv == nil {
|
||||||
return fmt.Errorf("Error using command storeconfig, no Key-value store defined")
|
return fmt.Errorf("Error using command storeconfig, no Key-value store defined")
|
||||||
}
|
}
|
||||||
jsonConf, _ := json.Marshal(traefikConfiguration.GlobalConfiguration)
|
jsonConf, err := json.Marshal(traefikConfiguration.GlobalConfiguration)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
fmtlog.Printf("Storing configuration: %s\n", jsonConf)
|
fmtlog.Printf("Storing configuration: %s\n", jsonConf)
|
||||||
return kv.StoreConfig(traefikConfiguration.GlobalConfiguration)
|
err = kv.StoreConfig(traefikConfiguration.GlobalConfiguration)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if traefikConfiguration.GlobalConfiguration.ACME != nil && len(traefikConfiguration.GlobalConfiguration.ACME.StorageFile) > 0 {
|
||||||
|
// convert ACME json file to KV store
|
||||||
|
store := acme.NewLocalStore(traefikConfiguration.GlobalConfiguration.ACME.StorageFile)
|
||||||
|
object, err := store.Load()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
meta := cluster.NewMetadata(object)
|
||||||
|
err = meta.Marshall()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
source := staert.KvSource{
|
||||||
|
Store: kv,
|
||||||
|
Prefix: traefikConfiguration.GlobalConfiguration.ACME.Storage,
|
||||||
|
}
|
||||||
|
err = source.StoreConfig(meta)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
Metadata: map[string]string{
|
Metadata: map[string]string{
|
||||||
"parseAllSources": "true",
|
"parseAllSources": "true",
|
||||||
|
@ -127,7 +158,7 @@ Complete documentation is available at https://traefik.io`,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := f.Parse(usedCmd); err != nil {
|
if _, err := f.Parse(usedCmd); err != nil {
|
||||||
fmtlog.Println(err)
|
fmtlog.Printf("Error parsing command: %s\n", err)
|
||||||
os.Exit(-1)
|
os.Exit(-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,21 +179,27 @@ Complete documentation is available at https://traefik.io`,
|
||||||
|
|
||||||
kv, err = CreateKvSource(traefikConfiguration)
|
kv, err = CreateKvSource(traefikConfiguration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmtlog.Println(err)
|
fmtlog.Printf("Error creating kv store: %s\n", err)
|
||||||
os.Exit(-1)
|
os.Exit(-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IF a KV Store is enable and no sub-command called in args
|
// IF a KV Store is enable and no sub-command called in args
|
||||||
if kv != nil && usedCmd == traefikCmd {
|
if kv != nil && usedCmd == traefikCmd {
|
||||||
|
if traefikConfiguration.Cluster == nil {
|
||||||
|
traefikConfiguration.Cluster = &types.Cluster{Node: uuid.NewV4().String()}
|
||||||
|
}
|
||||||
|
if traefikConfiguration.Cluster.Store == nil {
|
||||||
|
traefikConfiguration.Cluster.Store = &types.Store{Prefix: kv.Prefix, Store: kv.Store}
|
||||||
|
}
|
||||||
s.AddSource(kv)
|
s.AddSource(kv)
|
||||||
if _, err := s.LoadConfig(); err != nil {
|
if _, err := s.LoadConfig(); err != nil {
|
||||||
fmtlog.Println(err)
|
fmtlog.Printf("Error loading configuration: %s\n", err)
|
||||||
os.Exit(-1)
|
os.Exit(-1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Run(); err != nil {
|
if err := s.Run(); err != nil {
|
||||||
fmtlog.Println(err)
|
fmtlog.Printf("Error running traefik: %s\n", err)
|
||||||
os.Exit(-1)
|
os.Exit(-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,7 +238,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)
|
||||||
}
|
}
|
||||||
|
@ -217,10 +254,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)
|
||||||
|
@ -235,7 +272,7 @@ func run(traefikConfiguration *TraefikConfiguration) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateKvSource creates KvSource
|
// CreateKvSource creates KvSource
|
||||||
// TLS support is enable for Consul and ects backends
|
// TLS support is enable for Consul and Etcd backends
|
||||||
func CreateKvSource(traefikConfiguration *TraefikConfiguration) (*staert.KvSource, error) {
|
func CreateKvSource(traefikConfiguration *TraefikConfiguration) (*staert.KvSource, error) {
|
||||||
var kv *staert.KvSource
|
var kv *staert.KvSource
|
||||||
var store store.Store
|
var store store.Store
|
||||||
|
|
|
@ -100,12 +100,18 @@
|
||||||
#
|
#
|
||||||
# email = "test@traefik.io"
|
# email = "test@traefik.io"
|
||||||
|
|
||||||
# File used for certificates storage.
|
# File or key used for certificates storage.
|
||||||
# WARNING, if you use Traefik in Docker, don't forget to mount this file as a volume.
|
# WARNING, if you use Traefik in Docker, you have 2 options:
|
||||||
|
# - create a file on your host and mount it as a volume
|
||||||
|
# storageFile = "acme.json"
|
||||||
|
# $ docker run -v "/my/host/acme.json:acme.json" traefik
|
||||||
|
# - mount the folder containing the file as a volume
|
||||||
|
# storageFile = "/etc/traefik/acme/acme.json"
|
||||||
|
# $ docker run -v "/my/host/acme:/etc/traefik/acme" traefik
|
||||||
#
|
#
|
||||||
# Required
|
# Required
|
||||||
#
|
#
|
||||||
# storageFile = "acme.json"
|
# storage = "acme.json" # or "traefik/acme/account" if using KV store
|
||||||
|
|
||||||
# Entrypoint to proxy acme challenge to.
|
# Entrypoint to proxy acme challenge to.
|
||||||
# WARNING, must point to an entrypoint on port 443
|
# WARNING, must point to an entrypoint on port 443
|
||||||
|
|
|
@ -3,6 +3,7 @@ package types
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/docker/libkv/store"
|
||||||
"github.com/ryanuber/go-glob"
|
"github.com/ryanuber/go-glob"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
@ -192,6 +193,18 @@ func (cs *Constraints) Type() string {
|
||||||
return fmt.Sprint("constraint")
|
return fmt.Sprint("constraint")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store holds KV store cluster config
|
||||||
|
type Store struct {
|
||||||
|
store.Store
|
||||||
|
Prefix string // like this "prefix" (without the /)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cluster holds cluster config
|
||||||
|
type Cluster struct {
|
||||||
|
Node string `description:"Node name"`
|
||||||
|
Store *Store
|
||||||
|
}
|
||||||
|
|
||||||
// Auth holds authentication configuration (BASIC, DIGEST, users)
|
// Auth holds authentication configuration (BASIC, DIGEST, users)
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
Basic *Basic
|
Basic *Basic
|
||||||
|
|
4
web.go
4
web.go
|
@ -8,10 +8,10 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
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/autogen"
|
"github.com/containous/traefik/autogen"
|
||||||
|
"github.com/containous/traefik/log"
|
||||||
"github.com/containous/traefik/middlewares"
|
"github.com/containous/traefik/middlewares"
|
||||||
"github.com/containous/traefik/safe"
|
"github.com/containous/traefik/safe"
|
||||||
"github.com/containous/traefik/types"
|
"github.com/containous/traefik/types"
|
||||||
|
@ -79,7 +79,7 @@ func (provider *WebProvider) Provide(configurationChan chan<- types.ConfigMessag
|
||||||
body, _ := ioutil.ReadAll(request.Body)
|
body, _ := ioutil.ReadAll(request.Body)
|
||||||
err := json.Unmarshal(body, configuration)
|
err := json.Unmarshal(body, configuration)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
configurationChan <- types.ConfigMessage{"web", configuration}
|
configurationChan <- types.ConfigMessage{ProviderName: "web", Configuration: configuration}
|
||||||
provider.getConfigHandler(response, request)
|
provider.getConfigHandler(response, request)
|
||||||
} else {
|
} else {
|
||||||
log.Errorf("Error parsing configuration %+v", err)
|
log.Errorf("Error parsing configuration %+v", err)
|
||||||
|
|
Loading…
Reference in a new issue