From 5a0440d6f81b0ea2a2d93634fe9e7fa36793a256 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Tue, 16 Aug 2016 19:13:18 +0200 Subject: [PATCH 1/6] Add KV datastore Signed-off-by: Emile Vauge --- acme/acme.go | 81 ++++++++++------ acme/challengeProvider.go | 15 +-- cluster/datastore.go | 184 +++++++++++++++++++++++++++++++++++++ cluster/leadership.go | 56 +++++++++++ configuration.go | 21 +++-- glide.lock | 49 ++++++++++ glide.yaml | 3 + integration/consul_test.go | 94 +++++++++++++++++++ provider/boltdb.go | 2 + provider/consul.go | 2 + provider/consul_catalog.go | 2 + provider/docker.go | 2 + provider/etcd.go | 2 + provider/file.go | 2 + provider/kubernetes.go | 2 + provider/marathon.go | 2 + provider/mesos.go | 2 + provider/zk.go | 2 + server.go | 2 +- traefik.go | 6 +- types/types.go | 13 +++ 21 files changed, 498 insertions(+), 46 deletions(-) create mode 100644 cluster/datastore.go create mode 100644 cluster/leadership.go diff --git a/acme/acme.go b/acme/acme.go index 8763cc235..0de565eb4 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -173,6 +173,7 @@ type ACME struct { storageLock sync.RWMutex client *acme.Client account *Account + defaultCertificate *tls.Certificate } //Domains parse []Domain @@ -216,28 +217,50 @@ type Domain struct { SANs []string } -// CreateConfig creates a tls.config from using ACME configuration -func (a *ACME) CreateConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(domain string) bool) error { +func (a *ACME) init() error { + if len(a.Store) == 0 { + a.Store = a.StorageFile + } acme.Logger = fmtlog.New(ioutil.Discard, "", 0) - - if len(a.StorageFile) == 0 { - return errors.New("Empty StorageFile, please provide a filename for certs storage") - } - log.Debugf("Generating default certificate...") - if len(tlsConfig.Certificates) == 0 { - // no certificates in TLS config, so we add a default one - cert, err := generateDefaultCertificate() - if err != nil { - return err - } - tlsConfig.Certificates = append(tlsConfig.Certificates, *cert) + // no certificates in TLS config, so we add a default one + cert, err := generateDefaultCertificate() + if err != nil { + return err } + a.defaultCertificate = cert + return nil +} + +// CreateClusterConfig creates a tls.config from using ACME configuration +func (a *ACME) CreateClusterConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(domain string) bool) error { + err := a.init() + if err != nil { + return err + } + if len(a.Store) == 0 { + return errors.New("Empty Store, please provide a filename/key for certs storage") + } + tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate) + return nil +} + +// CreateLocalConfig creates a tls.config from using ACME configuration +func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(domain string) bool) error { + err := a.init() + if err != nil { + return err + } + if len(a.Store) == 0 { + return errors.New("Empty Store, please provide a filename/key for certs storage") + } + tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate) + var needRegister bool var err error // if certificates in storage, load them - if fileInfo, err := os.Stat(a.StorageFile); err == nil && fileInfo.Size() != 0 { + if fileInfo, fileErr := os.Stat(a.Store); fileErr == nil && fileInfo.Size() != 0 { log.Infof("Loading ACME certificates...") // load account a.account, err = a.loadAccount(a) @@ -265,7 +288,10 @@ func (a *ACME) CreateConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(doma } a.client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01}) wrapperChallengeProvider := newWrapperChallengeProvider() - a.client.SetChallengeProvider(acme.TLSSNI01, wrapperChallengeProvider) + err = client.SetChallengeProvider(acme.TLSSNI01, wrapperChallengeProvider) + if err != nil { + return err + } if needRegister { // New users will need to register; be sure to save it @@ -322,12 +348,9 @@ func (a *ACME) CreateConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(doma ticker := time.NewTicker(24 * time.Hour) safe.Go(func() { - for { - select { - case <-ticker.C: - if err := a.renewCertificates(a.client); err != nil { - log.Errorf("Error renewing ACME certificate %+v: %s", a.account, err.Error()) - } + for range ticker.C { + if err := a.renewCertificates(client, account); err != nil { + log.Errorf("Error renewing ACME certificate %+v: %s", account, err.Error()) } } @@ -470,22 +493,22 @@ func (a *ACME) LoadCertificateForDomains(domains []string) { func (a *ACME) loadAccount(acmeConfig *ACME) (*Account, error) { a.storageLock.RLock() defer a.storageLock.RUnlock() - Account := Account{ + account := Account{ DomainsCertificate: DomainsCertificates{}, } - file, err := ioutil.ReadFile(acmeConfig.StorageFile) + file, err := ioutil.ReadFile(acmeConfig.Store) if err != nil { return nil, err } - if err := json.Unmarshal(file, &Account); err != nil { + if err := json.Unmarshal(file, &account); err != nil { return nil, err } - err = Account.DomainsCertificate.init() + err = account.DomainsCertificate.init() if err != nil { return nil, err } - log.Infof("Loaded ACME config from storage %s", acmeConfig.StorageFile) - return &Account, nil + log.Infof("Loaded ACME config from store %s", acmeConfig.Store) + return &account, nil } func (a *ACME) saveAccount() error { @@ -496,7 +519,7 @@ func (a *ACME) saveAccount() error { if err != nil { return err } - return ioutil.WriteFile(a.StorageFile, data, 0600) + return ioutil.WriteFile(a.Store, data, 0600) } func (a *ACME) getDomainsCertificates(client *acme.Client, domains []string) (*Certificate, error) { diff --git a/acme/challengeProvider.go b/acme/challengeProvider.go index d9bccf204..1083b5f83 100644 --- a/acme/challengeProvider.go +++ b/acme/challengeProvider.go @@ -8,18 +8,20 @@ import ( "github.com/xenolf/lego/acme" ) -type wrapperChallengeProvider struct { +var _ acme.ChallengeProvider = (*inMemoryChallengeProvider)(nil) + +type inMemoryChallengeProvider struct { challengeCerts map[string]*tls.Certificate lock sync.RWMutex } -func newWrapperChallengeProvider() *wrapperChallengeProvider { - return &wrapperChallengeProvider{ +func newWrapperChallengeProvider() *inMemoryChallengeProvider { + return &inMemoryChallengeProvider{ challengeCerts: map[string]*tls.Certificate{}, } } -func (c *wrapperChallengeProvider) getCertificate(domain string) (cert *tls.Certificate, exists bool) { +func (c *inMemoryChallengeProvider) getCertificate(domain string) (cert *tls.Certificate, exists bool) { c.lock.RLock() defer c.lock.RUnlock() if cert, ok := c.challengeCerts[domain]; ok { @@ -28,7 +30,7 @@ func (c *wrapperChallengeProvider) getCertificate(domain string) (cert *tls.Cert return nil, false } -func (c *wrapperChallengeProvider) Present(domain, token, keyAuth string) error { +func (c *inMemoryChallengeProvider) Present(domain, token, keyAuth string) error { cert, _, err := acme.TLSSNI01ChallengeCert(keyAuth) if err != nil { return err @@ -45,10 +47,9 @@ func (c *wrapperChallengeProvider) Present(domain, token, keyAuth string) error } return nil - } -func (c *wrapperChallengeProvider) CleanUp(domain, token, keyAuth string) error { +func (c *inMemoryChallengeProvider) CleanUp(domain, token, keyAuth string) error { c.lock.Lock() defer c.lock.Unlock() delete(c.challengeCerts, domain) diff --git a/cluster/datastore.go b/cluster/datastore.go new file mode 100644 index 000000000..5f3896772 --- /dev/null +++ b/cluster/datastore.go @@ -0,0 +1,184 @@ +package cluster + +import ( + "fmt" + log "github.com/Sirupsen/logrus" + "github.com/cenkalti/backoff" + "github.com/containous/staert" + "github.com/docker/libkv/store" + "github.com/satori/go.uuid" + "golang.org/x/net/context" + "sync" + "time" +) + +// Object is the struct to store +type Object interface{} + +// Metadata stores Object plus metadata +type Metadata struct { + Lock string +} + +// Datastore holds a struct synced in a KV store +type Datastore struct { + kv *staert.KvSource + ctx context.Context + localLock *sync.RWMutex + object Object + meta *Metadata + lockKey string +} + +// NewDataStore creates a Datastore +func NewDataStore(kvSource *staert.KvSource, ctx context.Context, object Object) (*Datastore, error) { + datastore := Datastore{ + kv: kvSource, + ctx: ctx, + meta: &Metadata{}, + object: object, + lockKey: kvSource.Prefix + "/lock", + localLock: &sync.RWMutex{}, + } + 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 + } + d.localLock.Lock() + err := d.kv.LoadConfig(d.object) + if err != nil { + d.localLock.Unlock() + return err + } + err = d.kv.LoadConfig(d.meta) + if err != nil { + d.localLock.Unlock() + return err + } + d.localLock.Unlock() + } + } + } + notify := func(err error, time time.Duration) { + log.Errorf("Error in watch datastore: %+v, retrying in %s", err, time) + } + err := backoff.RetryNotify(operation, backoff.NewExponentialBackOff(), notify) + if err != nil { + log.Errorf("Error in watch datastore: %v", err) + } + }() + return nil +} + +// Begin creates a transaction with the KV store. +func (d *Datastore) Begin() (*Transaction, error) { + value := uuid.NewV4().String() + remoteLock, err := d.kv.NewLock(d.lockKey, &store.LockOptions{TTL: 20 * time.Second, Value: []byte(value)}) + if err != nil { + return 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, errLock + } + case <-d.ctx.Done(): + stopCh <- struct{}{} + return 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 != value { + return fmt.Errorf("Object lock value: expected %s, got %s", value, meta.Lock) + } + return nil + } + notify := func(err error, time time.Duration) { + log.Errorf("Datastore sync error: %v, retrying in %s", err, time) + } + ebo := backoff.NewExponentialBackOff() + ebo.MaxElapsedTime = 60 * time.Second + err = backoff.RetryNotify(operation, ebo, notify) + if err != nil { + return nil, fmt.Errorf("Datastore cannot sync: %v", err) + } + + // we synced with KV store, we can now return Setter + return &Transaction{ + Datastore: d, + remoteLock: remoteLock, + }, nil +} + +func (d *Datastore) get() *Metadata { + d.localLock.RLock() + defer d.localLock.RUnlock() + return d.meta +} + +// Get atomically a struct from the KV store +func (d *Datastore) Get() Object { + d.localLock.RLock() + defer d.localLock.RUnlock() + return d.object +} + +// Transaction allows to set a struct in the KV store +type Transaction struct { + *Datastore + remoteLock store.Locker + dirty bool +} + +// Commit allows to set an object in the KV store +func (s *Transaction) Commit(object Object) error { + s.localLock.Lock() + defer s.localLock.Unlock() + if s.dirty { + return fmt.Errorf("Transaction already used. Please begin a new one.") + } + err := s.kv.StoreConfig(object) + if err != nil { + return err + } + + err = s.remoteLock.Unlock() + if err != nil { + return err + } + + s.Datastore.object = object + s.dirty = true + return nil +} diff --git a/cluster/leadership.go b/cluster/leadership.go new file mode 100644 index 000000000..851e90010 --- /dev/null +++ b/cluster/leadership.go @@ -0,0 +1,56 @@ +package cluster + +import ( + log "github.com/Sirupsen/logrus" + "github.com/cenkalti/backoff" + "github.com/containous/traefik/safe" + "github.com/containous/traefik/types" + "github.com/docker/leadership" + "time" +) + +// Leadership allows leadership election using a KV store +type Leadership struct { + types.Cluster + candidate *leadership.Candidate +} + +// Participate tries to be a leader +func (l *Leadership) Participate(pool *safe.Pool, isElected func(bool)) { + pool.Go(func(stop chan bool) { + l.candidate = leadership.NewCandidate(l.Store, l.Store.Prefix+"/leader", l.Node, 30*time.Second) + backOff := backoff.NewExponentialBackOff() + operation := func() error { + return l.run(l.candidate, stop, isElected) + } + + 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) + } + }) +} + +// Resign resigns from being a leader +func (l *Leadership) Resign() { + if l.candidate != nil { + l.candidate.Resign() + } +} + +func (l *Leadership) run(candidate *leadership.Candidate, stop chan bool, isElected func(bool)) error { + electedCh, errCh := candidate.RunForElection() + for { + select { + case elected := <-electedCh: + isElected(elected) + case err := <-errCh: + return err + case <-stop: + return nil + } + } +} diff --git a/configuration.go b/configuration.go index 1f98bce9e..558ce2932 100644 --- a/configuration.go +++ b/configuration.go @@ -23,12 +23,13 @@ type TraefikConfiguration struct { // GlobalConfiguration holds global configuration (with providers, etc.). // It's populated from the traefik configuration file passed as an argument to the binary. type GlobalConfiguration struct { - GraceTimeOut int64 `short:"g" description:"Duration to give active requests a chance to finish during hot-reload"` - Debug bool `short:"d" description:"Enable debug mode"` - AccessLogsFile string `description:"Access logs file"` - TraefikLogsFile string `description:"Traefik logs file"` - LogLevel string `short:"l" description:"Log level"` - EntryPoints EntryPoints `description:"Entrypoints definition using format: --entryPoints='Name:http Address::8000 Redirect.EntryPoint:https' --entryPoints='Name:https Address::4442 TLS:tests/traefik.crt,tests/traefik.key'"` + GraceTimeOut int64 `short:"g" description:"Duration to give active requests a chance to finish during hot-reload"` + Debug bool `short:"d" description:"Enable debug mode"` + AccessLogsFile string `description:"Access logs file"` + TraefikLogsFile string `description:"Traefik logs file"` + LogLevel string `short:"l" description:"Log level"` + EntryPoints EntryPoints `description:"Entrypoints definition using format: --entryPoints='Name:http Address::8000 Redirect.EntryPoint:https' --entryPoints='Name:https Address::4442 TLS:tests/traefik.crt,tests/traefik.key'"` + Cluster *types.Cluster Constraints types.Constraints `description:"Filter services by constraint, matching with service tags."` 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"` @@ -73,7 +74,9 @@ func (dep *DefaultEntryPoints) Set(value string) error { } // 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 func (dep *DefaultEntryPoints) SetValue(val interface{}) { @@ -153,7 +156,9 @@ func (ep *EntryPoints) Set(value string) error { } // 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 func (ep *EntryPoints) SetValue(val interface{}) { diff --git a/glide.lock b/glide.lock index 4097907e3..f447712ec 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,14 @@ +<<<<<<< a13549cc28273ba5c15a739fa4aaeb3e0f7216a4 hash: c0ac205a859d78847e21d3cd63f427ffba985755c6ae84373e4a20364ba39b05 +<<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab updated: 2016-09-30T10:57:42.336729457+02:00 +======= +updated: 2016-09-28T16:50:04.352639437+01:00 +======= +hash: 809b3fa812ca88940fdc15530804a4bcd881708e4819fed5aa45c42c871ba5cf +updated: 2016-09-20T14:50:04.029710103+02:00 +>>>>>>> Add KV datastore +>>>>>>> Add KV datastore imports: - name: github.com/abbot/go-http-auth version: cb4372376e1e00e9f6ab9ec142e029302c9e7140 @@ -36,7 +45,15 @@ imports: subpackages: - spew - name: github.com/docker/distribution +<<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab version: 87917f30529e6a7fca8eaff2932424915fb11225 +======= +<<<<<<< a13549cc28273ba5c15a739fa4aaeb3e0f7216a4 + version: 99cb7c0946d2f5a38015443e515dc916295064d7 +======= + version: 857d0f15c0a4d8037175642e0ca3660829551cb5 +>>>>>>> Add KV datastore +>>>>>>> Add KV datastore subpackages: - context - digest @@ -122,7 +139,13 @@ imports: - sockets - tlsconfig - name: github.com/docker/go-units +<<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab version: f2d77a61e3c169b43402a0a1e84f06daf29b8190 +======= + version: f2145db703495b2e525c59662db69a7344b00bb8 +- name: github.com/docker/leadership + version: bfc7753dd48af19513b29deec23c364bf0f274eb +>>>>>>> Add KV datastore - name: github.com/docker/libcompose version: d1876c1d68527a49c0aac22a0b161acc7296b740 subpackages: @@ -141,7 +164,11 @@ imports: - version - yaml - name: github.com/docker/libkv +<<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab version: 35d3e2084c650109e7bcc7282655b1bc8ba924ff +======= + version: aabc039ad04deb721e234f99cd1b4aa28ac71a40 +>>>>>>> Add KV datastore subpackages: - store - store/boltdb @@ -157,7 +184,15 @@ imports: - name: github.com/go-check/check version: 4f90aeace3a26ad7021961c297b22c42160c7b25 - name: github.com/gogo/protobuf +<<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab version: e33835a643a970c11ac74f6333f5f6866387a101 +======= +<<<<<<< a13549cc28273ba5c15a739fa4aaeb3e0f7216a4 + version: 89f1976ff373a3e549675d2f212c10f98b6c6316 +======= + version: e57a569e1882958f6b188cb42231d6db87701f2a +>>>>>>> Add KV datastore +>>>>>>> Add KV datastore subpackages: - proto - name: github.com/golang/glog @@ -220,7 +255,11 @@ imports: - name: github.com/miekg/dns version: 5d001d020961ae1c184f9f8152fdc73810481677 - name: github.com/mitchellh/mapstructure +<<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab version: d2dd0262208475919e1a362f675cfc0e7c10e905 +======= + version: 21a35fb16463dfb7c8eee579c65d995d95e64d1e +>>>>>>> Add KV datastore - name: github.com/moul/http2curl version: b1479103caacaa39319f75e7f57fc545287fca0d - name: github.com/NYTimes/gziphandler @@ -228,7 +267,15 @@ imports: - name: github.com/ogier/pflag version: 45c278ab3607870051a2ea9040bb85fcb8557481 - name: github.com/opencontainers/runc +<<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab version: 1a81e9ab1f138c091fe5c86d0883f87716088527 +======= +<<<<<<< a13549cc28273ba5c15a739fa4aaeb3e0f7216a4 + version: d9fec4c63b089ddfc267194ecb6cda58a13f072c +======= + version: ff88baa42fa5b2a1568a3a14665142fb4bdb3a2a +>>>>>>> Add KV datastore +>>>>>>> Add KV datastore subpackages: - libcontainer/user - name: github.com/parnurzeal/gorequest @@ -243,6 +290,8 @@ imports: version: e64db453f3512cade908163702045e0f31137843 subpackages: - zk +- name: github.com/satori/go.uuid + version: 879c5887cd475cd7864858769793b2ceb0d44feb - name: github.com/Sirupsen/logrus version: a283a10442df8dc09befd873fab202bf8a253d6a - name: github.com/streamrail/concurrent-map diff --git a/glide.yaml b/glide.yaml index 7f0c63588..9f1c7b4e6 100644 --- a/glide.yaml +++ b/glide.yaml @@ -99,3 +99,6 @@ import: - package: github.com/miekg/dns version: 5d001d020961ae1c184f9f8152fdc73810481677 - package: github.com/NYTimes/gziphandler +- package: github.com/docker/leadership +- package: github.com/satori/go.uuid + version: ^1.1.0 diff --git a/integration/consul_test.go b/integration/consul_test.go index e51479047..ce60c38f9 100644 --- a/integration/consul_test.go +++ b/integration/consul_test.go @@ -5,18 +5,22 @@ import ( "os/exec" "time" + "github.com/containous/staert" "github.com/docker/libkv" "github.com/docker/libkv/store" "github.com/docker/libkv/store/consul" "github.com/go-check/check" + "golang.org/x/net/context" "errors" + "github.com/containous/traefik/cluster" "github.com/containous/traefik/integration/utils" "github.com/containous/traefik/provider" checker "github.com/vdemeester/shakers" "io/ioutil" "os" "strings" + "sync" ) // 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{}) + c.Assert(err, checker.IsNil) + datastore2, err := cluster.NewDataStore(kvSource, ctx, &TestStruct{}) + 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() +} diff --git a/provider/boltdb.go b/provider/boltdb.go index 5220fca98..3347fe340 100644 --- a/provider/boltdb.go +++ b/provider/boltdb.go @@ -8,6 +8,8 @@ import ( "github.com/docker/libkv/store/boltdb" ) +var _ Provider = (*BoltDb)(nil) + // BoltDb holds configurations of the BoltDb provider. type BoltDb struct { Kv `mapstructure:",squash"` diff --git a/provider/consul.go b/provider/consul.go index 4cc8b2851..1b628dd4f 100644 --- a/provider/consul.go +++ b/provider/consul.go @@ -8,6 +8,8 @@ import ( "github.com/docker/libkv/store/consul" ) +var _ Provider = (*Consul)(nil) + // Consul holds configurations of the Consul provider. type Consul struct { Kv `mapstructure:",squash"` diff --git a/provider/consul_catalog.go b/provider/consul_catalog.go index 328a686df..1d1d4c928 100644 --- a/provider/consul_catalog.go +++ b/provider/consul_catalog.go @@ -24,6 +24,8 @@ const ( DefaultConsulCatalogTagPrefix = "traefik" ) +var _ Provider = (*ConsulCatalog)(nil) + // ConsulCatalog holds configurations of the Consul catalog provider. type ConsulCatalog struct { BaseProvider `mapstructure:",squash"` diff --git a/provider/docker.go b/provider/docker.go index f816b3130..73f897244 100644 --- a/provider/docker.go +++ b/provider/docker.go @@ -40,6 +40,8 @@ const ( SwarmDefaultWatchTime = 15 * time.Second ) +var _ Provider = (*Docker)(nil) + // Docker holds configurations of the Docker provider. type Docker struct { BaseProvider `mapstructure:",squash"` diff --git a/provider/etcd.go b/provider/etcd.go index 0165bf3c1..9c7726baa 100644 --- a/provider/etcd.go +++ b/provider/etcd.go @@ -8,6 +8,8 @@ import ( "github.com/docker/libkv/store/etcd" ) +var _ Provider = (*Etcd)(nil) + // Etcd holds configurations of the Etcd provider. type Etcd struct { Kv `mapstructure:",squash"` diff --git a/provider/file.go b/provider/file.go index 05cb89eee..dc42b26d5 100644 --- a/provider/file.go +++ b/provider/file.go @@ -12,6 +12,8 @@ import ( "gopkg.in/fsnotify.v1" ) +var _ Provider = (*File)(nil) + // File holds configurations of the File provider. type File struct { BaseProvider `mapstructure:",squash"` diff --git a/provider/kubernetes.go b/provider/kubernetes.go index e1509ac4a..a5c8654f6 100644 --- a/provider/kubernetes.go +++ b/provider/kubernetes.go @@ -50,6 +50,8 @@ func (ns *Namespaces) SetValue(val interface{}) { *ns = Namespaces(val.(Namespaces)) } +var _ Provider = (*Kubernetes)(nil) + // Kubernetes holds configurations of the Kubernetes provider. type Kubernetes struct { BaseProvider `mapstructure:",squash"` diff --git a/provider/marathon.go b/provider/marathon.go index 10b99f999..c06b49309 100644 --- a/provider/marathon.go +++ b/provider/marathon.go @@ -21,6 +21,8 @@ import ( "github.com/gambol99/go-marathon" ) +var _ Provider = (*Marathon)(nil) + // Marathon holds configuration of the Marathon provider. type Marathon struct { BaseProvider diff --git a/provider/mesos.go b/provider/mesos.go index 623fe1acc..2396cf018 100644 --- a/provider/mesos.go +++ b/provider/mesos.go @@ -24,6 +24,8 @@ import ( "time" ) +var _ Provider = (*Mesos)(nil) + //Mesos holds configuration of the mesos provider. type Mesos struct { BaseProvider diff --git a/provider/zk.go b/provider/zk.go index 467f59e2f..19f3654e5 100644 --- a/provider/zk.go +++ b/provider/zk.go @@ -8,6 +8,8 @@ import ( "github.com/docker/libkv/store/zookeeper" ) +var _ Provider = (*Zookepper)(nil) + // Zookepper holds configurations of the Zookepper provider. type Zookepper struct { Kv `mapstructure:",squash"` diff --git a/server.go b/server.go index 9587ba872..48c8c3ffb 100644 --- a/server.go +++ b/server.go @@ -375,7 +375,7 @@ func (server *Server) createTLSConfig(entryPointName string, tlsOption *TLS, rou } return false } - err := server.globalConfiguration.ACME.CreateConfig(config, checkOnDemandDomain) + err := server.globalConfiguration.ACME.CreateLocalConfig(config, checkOnDemandDomain) if err != nil { return nil, err } diff --git a/traefik.go b/traefik.go index e20e11456..f29ee8591 100644 --- a/traefik.go +++ b/traefik.go @@ -154,6 +154,10 @@ Complete documentation is available at https://traefik.io`, // IF a KV Store is enable and no sub-command called in args if kv != nil && usedCmd == traefikCmd { + if traefikConfiguration.Cluster == nil { + hostname, _ := os.Hostname() + traefikConfiguration.Cluster = &types.Cluster{Store: types.Store{Prefix: kv.Prefix, Store: kv.Store}, Node: hostname} + } s.AddSource(kv) if _, err := s.LoadConfig(); err != nil { fmtlog.Println(err) @@ -235,7 +239,7 @@ func run(traefikConfiguration *TraefikConfiguration) { } // 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) { var kv *staert.KvSource var store store.Store diff --git a/types/types.go b/types/types.go index d5a0f0ead..d24f5a036 100644 --- a/types/types.go +++ b/types/types.go @@ -3,6 +3,7 @@ package types import ( "errors" "fmt" + "github.com/docker/libkv/store" "github.com/ryanuber/go-glob" "strings" ) @@ -192,6 +193,18 @@ func (cs *Constraints) Type() string { 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 + Store Store +} + // Auth holds authentication configuration (BASIC, DIGEST, users) type Auth struct { Basic *Basic From bea5ad3f132bae27b6c1a83adf00154058b484b5 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Thu, 18 Aug 2016 13:03:10 +0200 Subject: [PATCH 2/6] Add leadership election Signed-off-by: Emile Vauge --- cluster/datastore.go | 8 +++--- cluster/leadership.go | 44 +++++++++++++++++++++-------- provider/kv.go | 10 +++---- safe/routine.go | 66 ++++++++++++++++++++++++++++++++++++++++--- server.go | 30 ++++++++++++++++++-- traefik.go | 7 +++-- types/types.go | 2 +- 7 files changed, 138 insertions(+), 29 deletions(-) diff --git a/cluster/datastore.go b/cluster/datastore.go index 5f3896772..5bad330a8 100644 --- a/cluster/datastore.go +++ b/cluster/datastore.go @@ -94,8 +94,8 @@ func (d *Datastore) watchChanges() error { // Begin creates a transaction with the KV store. func (d *Datastore) Begin() (*Transaction, error) { - value := uuid.NewV4().String() - remoteLock, err := d.kv.NewLock(d.lockKey, &store.LockOptions{TTL: 20 * time.Second, Value: []byte(value)}) + id := uuid.NewV4().String() + remoteLock, err := d.kv.NewLock(d.lockKey, &store.LockOptions{TTL: 20 * time.Second, Value: []byte(id)}) if err != nil { return nil, err } @@ -119,8 +119,8 @@ func (d *Datastore) Begin() (*Transaction, error) { // we got the lock! Now make sure we are synced with KV store operation := func() error { meta := d.get() - if meta.Lock != value { - return fmt.Errorf("Object lock value: expected %s, got %s", value, meta.Lock) + if meta.Lock != id { + return fmt.Errorf("Object lock value: expected %s, got %s", id, meta.Lock) } return nil } diff --git a/cluster/leadership.go b/cluster/leadership.go index 851e90010..60d67e427 100644 --- a/cluster/leadership.go +++ b/cluster/leadership.go @@ -6,22 +6,34 @@ import ( "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 { - types.Cluster + *safe.Pool + *types.Cluster candidate *leadership.Candidate } +// 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), + } +} + // Participate tries to be a leader -func (l *Leadership) Participate(pool *safe.Pool, isElected func(bool)) { - pool.Go(func(stop chan bool) { - l.candidate = leadership.NewCandidate(l.Store, l.Store.Prefix+"/leader", l.Node, 30*time.Second) +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, stop, isElected) + return l.run(l.candidate, ctx) } notify := func(err error, time time.Duration) { @@ -36,21 +48,31 @@ func (l *Leadership) Participate(pool *safe.Pool, isElected func(bool)) { // Resign resigns from being a leader func (l *Leadership) Resign() { - if l.candidate != nil { - l.candidate.Resign() - } + l.candidate.Resign() + log.Infof("Node %s resined", l.Cluster.Node) } -func (l *Leadership) run(candidate *leadership.Candidate, stop chan bool, isElected func(bool)) error { +func (l *Leadership) run(candidate *leadership.Candidate, ctx context.Context) error { electedCh, errCh := candidate.RunForElection() for { select { case elected := <-electedCh: - isElected(elected) + l.onElection(elected) case err := <-errCh: return err - case <-stop: + 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.Start() + } else { + log.Infof("Node %s elected slave ♝", l.Cluster.Node) + l.Stop() + } +} diff --git a/provider/kv.go b/provider/kv.go index bc60f80cd..86c97d1b7 100644 --- a/provider/kv.go +++ b/provider/kv.go @@ -148,7 +148,7 @@ func (provider *Kv) list(keys ...string) []string { joinedKeys := strings.Join(keys, "") keysPairs, err := provider.kvclient.List(joinedKeys) if err != nil { - log.Errorf("Error getting keys %s %s ", joinedKeys, err) + log.Warnf("Cannot get keys %s %s ", joinedKeys, err) return nil } directoryKeys := make(map[string]string) @@ -170,10 +170,10 @@ func (provider *Kv) get(defaultValue string, keys ...string) string { joinedKeys := strings.Join(keys, "") keyPair, err := provider.kvclient.Get(strings.TrimPrefix(joinedKeys, "/")) if err != nil { - log.Warnf("Error getting key %s %s, setting default %s", joinedKeys, err, defaultValue) + log.Warnf("Cannot get key %s %s, setting default %s", joinedKeys, err, defaultValue) return defaultValue } else if keyPair == nil { - log.Warnf("Error getting key %s, setting default %s", joinedKeys, defaultValue) + log.Warnf("Cannot get key %s, setting default %s", joinedKeys, defaultValue) return defaultValue } return string(keyPair.Value) @@ -183,10 +183,10 @@ func (provider *Kv) splitGet(keys ...string) []string { joinedKeys := strings.Join(keys, "") keyPair, err := provider.kvclient.Get(joinedKeys) if err != nil { - log.Warnf("Error getting key %s %s, setting default empty", joinedKeys, err) + log.Warnf("Cannot get key %s %s, setting default empty", joinedKeys, err) return []string{} } else if keyPair == nil { - log.Warnf("Error getting key %s, setting default %empty", joinedKeys) + log.Warnf("Cannot get key %s, setting default %empty", joinedKeys) return []string{} } return strings.Split(string(keyPair.Value), ",") diff --git a/safe/routine.go b/safe/routine.go index 7fe9498dc..75e2358b7 100644 --- a/safe/routine.go +++ b/safe/routine.go @@ -1,6 +1,7 @@ package safe import ( + "golang.org/x/net/context" "log" "runtime/debug" "sync" @@ -11,11 +12,44 @@ type routine struct { 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 { - routines []routine - waitGroup sync.WaitGroup - lock sync.Mutex + routines []routine + routinesCtx []routineCtx + waitGroup sync.WaitGroup + lock sync.Mutex + baseCtx context.Context + ctx context.Context + cancel context.CancelFunc +} + +// NewPool creates a Pool +func NewPool(baseCtx context.Context) *Pool { + ctx, cancel := context.WithCancel(baseCtx) + return &Pool{ + ctx: ctx, + cancel: cancel, + baseCtx: baseCtx, + } +} + +// Ctx returns main context +func (p *Pool) Ctx() context.Context { + return p.ctx +} + +//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 @@ -37,6 +71,7 @@ func (p *Pool) Go(goroutine func(stop chan bool)) { // Stop stops all started routines, waiting for their termination func (p *Pool) Stop() { p.lock.Lock() + p.cancel() for _, routine := range p.routines { routine.stop <- true } @@ -47,6 +82,29 @@ func (p *Pool) Stop() { p.lock.Unlock() } +// Start starts all stoped routines +func (p *Pool) Start() { + p.lock.Lock() + 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() + }) + } + p.lock.Unlock() +} + // Go starts a recoverable goroutine func Go(goroutine func()) { GoWithRecover(goroutine, defaultRecoverGoroutine) diff --git a/server.go b/server.go index 48c8c3ffb..a9222bf2a 100644 --- a/server.go +++ b/server.go @@ -24,6 +24,7 @@ import ( log "github.com/Sirupsen/logrus" "github.com/codegangsta/negroni" "github.com/containous/mux" + "github.com/containous/traefik/cluster" "github.com/containous/traefik/middlewares" "github.com/containous/traefik/provider" "github.com/containous/traefik/safe" @@ -50,7 +51,8 @@ type Server struct { currentConfigurations safe.Safe globalConfiguration GlobalConfiguration loggerMiddleware *middlewares.Logger - routinesPool safe.Pool + routinesPool *safe.Pool + leadership *cluster.Leadership } type serverEntryPoints map[string]*serverEntryPoint @@ -80,12 +82,18 @@ func NewServer(globalConfiguration GlobalConfiguration) *Server { server.currentConfigurations.Set(currentConfigurations) server.globalConfiguration = globalConfiguration 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 } // Start starts the server and blocks until server is shutted down. func (server *Server) Start() { + server.startLeadership() server.startHTTPServers() server.routinesPool.Go(func(stop chan bool) { server.listenProviders(stop) @@ -125,6 +133,7 @@ func (server *Server) Close() { os.Exit(1) } }(ctx) + server.stopLeadership() server.routinesPool.Stop() close(server.configurationChan) close(server.configurationValidatedChan) @@ -135,6 +144,23 @@ func (server *Server) Close() { cancel() } +func (server *Server) startLeadership() { + if server.leadership != nil { + server.leadership.Participate(server.routinesPool) + server.leadership.GoCtx(func(ctx context.Context) { + log.Debugf("Started test routine") + <-ctx.Done() + log.Debugf("Stopped test routine") + }) + } +} + +func (server *Server) stopLeadership() { + if server.leadership != nil { + server.leadership.Resign() + } +} + func (server *Server) startHTTPServers() { server.serverEntryPoints = server.buildEntryPoints(server.globalConfiguration) for newServerEntryPointName, newServerEntryPoint := range server.serverEntryPoints { @@ -321,7 +347,7 @@ func (server *Server) startProviders() { log.Infof("Starting provider %v %s", reflect.TypeOf(provider), jsonConf) currentProvider := provider 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 { log.Errorf("Error starting provider %s", err) } diff --git a/traefik.go b/traefik.go index f29ee8591..e5f65468a 100644 --- a/traefik.go +++ b/traefik.go @@ -21,6 +21,7 @@ import ( "github.com/containous/traefik/types" "github.com/containous/traefik/version" "github.com/docker/libkv/store" + "github.com/satori/go.uuid" ) var versionTemplate = `Version: {{.Version}} @@ -155,8 +156,10 @@ Complete documentation is available at https://traefik.io`, // IF a KV Store is enable and no sub-command called in args if kv != nil && usedCmd == traefikCmd { if traefikConfiguration.Cluster == nil { - hostname, _ := os.Hostname() - traefikConfiguration.Cluster = &types.Cluster{Store: types.Store{Prefix: kv.Prefix, Store: kv.Store}, Node: hostname} + 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) if _, err := s.LoadConfig(); err != nil { diff --git a/types/types.go b/types/types.go index d24f5a036..e2208180b 100644 --- a/types/types.go +++ b/types/types.go @@ -202,7 +202,7 @@ type Store struct { // Cluster holds cluster config type Cluster struct { Node string - Store Store + Store *Store } // Auth holds authentication configuration (BASIC, DIGEST, users) From a42845502e9b6e3b9985c56ad99d28c1357287b2 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Thu, 18 Aug 2016 14:20:11 +0200 Subject: [PATCH 3/6] Add ACME store Signed-off-by: Emile Vauge --- acme/account.go | 176 ++++++++++++ acme/acme.go | 508 ++++++++++++++++------------------- acme/challengeProvider.go | 86 ++++-- acme/localStore.go | 97 +++++++ adapters.go | 2 +- cluster/datastore.go | 102 +++++-- cluster/leadership.go | 30 ++- cluster/store.go | 16 ++ configuration.go | 16 +- glide.lock | 10 +- glide.yaml | 2 +- middlewares/authenticator.go | 2 +- middlewares/logger.go | 2 +- middlewares/retry.go | 2 +- middlewares/rewrite.go | 2 +- provider/consul_catalog.go | 5 +- provider/docker.go | 2 +- provider/file.go | 2 +- provider/k8s/client.go | 2 +- provider/kubernetes.go | 5 +- provider/kv.go | 2 +- provider/marathon.go | 2 +- provider/mesos.go | 2 +- provider/mesos_test.go | 2 +- provider/provider.go | 2 +- safe/routine.go | 24 +- server.go | 39 ++- traefik.go | 9 +- types/types.go | 2 +- web.go | 2 +- 30 files changed, 781 insertions(+), 374 deletions(-) create mode 100644 acme/account.go create mode 100644 acme/localStore.go create mode 100644 cluster/store.go diff --git a/acme/account.go b/acme/account.go new file mode 100644 index 000000000..55ac44672 --- /dev/null +++ b/acme/account.go @@ -0,0 +1,176 @@ +package acme + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "errors" + "github.com/containous/traefik/log" + "github.com/xenolf/lego/acme" + "reflect" + "sync" + "time" +) + +// Account is used to store lets encrypt registration info +type Account struct { + Email string + Registration *acme.RegistrationResource + PrivateKey []byte + DomainsCertificate DomainsCertificates + ChallengeCerts map[string][]byte +} + +// Init inits acccount struct +func (a *Account) Init() error { + err := a.DomainsCertificate.Init() + if err != nil { + return err + } + return nil +} + +func NewAccount(email string) (*Account, error) { + // Create a user. New accounts need an email and private key to start + privateKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, err + } + domainsCerts := DomainsCertificates{Certs: []*DomainsCertificate{}} + domainsCerts.Init() + return &Account{ + Email: email, + PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey), + DomainsCertificate: domainsCerts, + ChallengeCerts: map[string][]byte{}}, nil +} + +// GetEmail returns email +func (a Account) GetEmail() string { + return a.Email +} + +// GetRegistration returns lets encrypt registration resource +func (a Account) GetRegistration() *acme.RegistrationResource { + return a.Registration +} + +// GetPrivateKey returns private key +func (a Account) GetPrivateKey() crypto.PrivateKey { + if privateKey, err := x509.ParsePKCS1PrivateKey(a.PrivateKey); err == nil { + return privateKey + } + log.Errorf("Cannot unmarshall private key %+v", a.PrivateKey) + return nil +} + +// Certificate is used to store certificate info +type Certificate struct { + Domain string + CertURL string + CertStableURL string + PrivateKey []byte + Certificate []byte +} + +// DomainsCertificates stores a certificate for multiple domains +type DomainsCertificates struct { + Certs []*DomainsCertificate + lock sync.RWMutex +} + +func (dc *DomainsCertificates) Init() error { + dc.lock.Lock() + defer dc.lock.Unlock() + for _, domainsCertificate := range dc.Certs { + tlsCert, err := tls.X509KeyPair(domainsCertificate.Certificate.Certificate, domainsCertificate.Certificate.PrivateKey) + if err != nil { + return err + } + domainsCertificate.tlsCert = &tlsCert + } + return nil +} + +func (dc *DomainsCertificates) renewCertificates(acmeCert *Certificate, domain Domain) error { + dc.lock.Lock() + defer dc.lock.Unlock() + + for _, domainsCertificate := range dc.Certs { + if reflect.DeepEqual(domain, domainsCertificate.Domains) { + tlsCert, err := tls.X509KeyPair(acmeCert.Certificate, acmeCert.PrivateKey) + if err != nil { + return err + } + domainsCertificate.Certificate = acmeCert + domainsCertificate.tlsCert = &tlsCert + return nil + } + } + return errors.New("Certificate to renew not found for domain " + domain.Main) +} + +func (dc *DomainsCertificates) addCertificateForDomains(acmeCert *Certificate, domain Domain) (*DomainsCertificate, error) { + dc.lock.Lock() + defer dc.lock.Unlock() + + tlsCert, err := tls.X509KeyPair(acmeCert.Certificate, acmeCert.PrivateKey) + if err != nil { + return nil, err + } + cert := DomainsCertificate{Domains: domain, Certificate: acmeCert, tlsCert: &tlsCert} + dc.Certs = append(dc.Certs, &cert) + return &cert, nil +} + +func (dc *DomainsCertificates) getCertificateForDomain(domainToFind string) (*DomainsCertificate, bool) { + dc.lock.RLock() + defer dc.lock.RUnlock() + for _, domainsCertificate := range dc.Certs { + domains := []string{} + domains = append(domains, domainsCertificate.Domains.Main) + domains = append(domains, domainsCertificate.Domains.SANs...) + for _, domain := range domains { + if domain == domainToFind { + return domainsCertificate, true + } + } + } + return nil, false +} + +func (dc *DomainsCertificates) exists(domainToFind Domain) (*DomainsCertificate, bool) { + dc.lock.RLock() + defer dc.lock.RUnlock() + for _, domainsCertificate := range dc.Certs { + if reflect.DeepEqual(domainToFind, domainsCertificate.Domains) { + return domainsCertificate, true + } + } + return nil, false +} + +// DomainsCertificate contains a certificate for multiple domains +type DomainsCertificate struct { + Domains Domain + Certificate *Certificate + tlsCert *tls.Certificate +} + +func (dc *DomainsCertificate) needRenew() bool { + for _, c := range dc.tlsCert.Certificate { + crt, err := x509.ParseCertificate(c) + if err != nil { + // If there's an error, we assume the cert is broken, and needs update + return true + } + // <= 7 days left, renew certificate + if crt.NotAfter.Before(time.Now().Add(time.Duration(24 * 7 * time.Hour))) { + return true + } + } + + return false +} diff --git a/acme/acme.go b/acme/acme.go index 0de565eb4..575e7b2ac 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -1,179 +1,36 @@ package acme import ( - "crypto" - "crypto/rand" - "crypto/rsa" "crypto/tls" - "crypto/x509" - "encoding/json" "errors" "fmt" + "github.com/containous/staert" + "github.com/containous/traefik/cluster" + "github.com/containous/traefik/log" + "github.com/containous/traefik/safe" + "github.com/xenolf/lego/acme" + "golang.org/x/net/context" "io/ioutil" fmtlog "log" "os" - "reflect" "strings" - "sync" "time" - - log "github.com/Sirupsen/logrus" - "github.com/containous/traefik/safe" - "github.com/xenolf/lego/acme" ) -// Account is used to store lets encrypt registration info -type Account struct { - Email string - Registration *acme.RegistrationResource - PrivateKey []byte - DomainsCertificate DomainsCertificates -} - -// GetEmail returns email -func (a Account) GetEmail() string { - return a.Email -} - -// GetRegistration returns lets encrypt registration resource -func (a Account) GetRegistration() *acme.RegistrationResource { - return a.Registration -} - -// GetPrivateKey returns private key -func (a Account) GetPrivateKey() crypto.PrivateKey { - if privateKey, err := x509.ParsePKCS1PrivateKey(a.PrivateKey); err == nil { - return privateKey - } - log.Errorf("Cannot unmarshall private key %+v", a.PrivateKey) - return nil -} - -// Certificate is used to store certificate info -type Certificate struct { - Domain string - CertURL string - CertStableURL string - PrivateKey []byte - Certificate []byte -} - -// DomainsCertificates stores a certificate for multiple domains -type DomainsCertificates struct { - Certs []*DomainsCertificate - lock *sync.RWMutex -} - -func (dc *DomainsCertificates) init() error { - if dc.lock == nil { - dc.lock = &sync.RWMutex{} - } - dc.lock.Lock() - defer dc.lock.Unlock() - for _, domainsCertificate := range dc.Certs { - tlsCert, err := tls.X509KeyPair(domainsCertificate.Certificate.Certificate, domainsCertificate.Certificate.PrivateKey) - if err != nil { - return err - } - domainsCertificate.tlsCert = &tlsCert - } - return nil -} - -func (dc *DomainsCertificates) renewCertificates(acmeCert *Certificate, domain Domain) error { - dc.lock.Lock() - defer dc.lock.Unlock() - - for _, domainsCertificate := range dc.Certs { - if reflect.DeepEqual(domain, domainsCertificate.Domains) { - tlsCert, err := tls.X509KeyPair(acmeCert.Certificate, acmeCert.PrivateKey) - if err != nil { - return err - } - domainsCertificate.Certificate = acmeCert - domainsCertificate.tlsCert = &tlsCert - return nil - } - } - return errors.New("Certificate to renew not found for domain " + domain.Main) -} - -func (dc *DomainsCertificates) addCertificateForDomains(acmeCert *Certificate, domain Domain) (*DomainsCertificate, error) { - dc.lock.Lock() - defer dc.lock.Unlock() - - tlsCert, err := tls.X509KeyPair(acmeCert.Certificate, acmeCert.PrivateKey) - if err != nil { - return nil, err - } - cert := DomainsCertificate{Domains: domain, Certificate: acmeCert, tlsCert: &tlsCert} - dc.Certs = append(dc.Certs, &cert) - return &cert, nil -} - -func (dc *DomainsCertificates) getCertificateForDomain(domainToFind string) (*DomainsCertificate, bool) { - dc.lock.RLock() - defer dc.lock.RUnlock() - for _, domainsCertificate := range dc.Certs { - domains := []string{} - domains = append(domains, domainsCertificate.Domains.Main) - domains = append(domains, domainsCertificate.Domains.SANs...) - for _, domain := range domains { - if domain == domainToFind { - return domainsCertificate, true - } - } - } - return nil, false -} - -func (dc *DomainsCertificates) exists(domainToFind Domain) (*DomainsCertificate, bool) { - dc.lock.RLock() - defer dc.lock.RUnlock() - for _, domainsCertificate := range dc.Certs { - if reflect.DeepEqual(domainToFind, domainsCertificate.Domains) { - return domainsCertificate, true - } - } - return nil, false -} - -// DomainsCertificate contains a certificate for multiple domains -type DomainsCertificate struct { - Domains Domain - Certificate *Certificate - tlsCert *tls.Certificate -} - -func (dc *DomainsCertificate) needRenew() bool { - for _, c := range dc.tlsCert.Certificate { - crt, err := x509.ParseCertificate(c) - if err != nil { - // If there's an error, we assume the cert is broken, and needs update - return true - } - // <= 30 days left, renew certificate - if crt.NotAfter.Before(time.Now().Add(time.Duration(24 * 30 * time.Hour))) { - return true - } - } - - return false -} - // ACME allows to connect to lets encrypt and retrieve certs type ACME struct { - Email string `description:"Email address used for registration"` - Domains []Domain `description:"SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='main.net,san1.net,san2.net'"` - StorageFile string `description:"File used for certificates storage."` - OnDemand bool `description:"Enable on demand certificate. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."` - OnHostRule bool `description:"Enable certificate generation on frontends Host rules."` - CAServer string `description:"CA server to use."` - EntryPoint string `description:"Entrypoint to proxy acme challenge to."` - storageLock sync.RWMutex - client *acme.Client - account *Account - defaultCertificate *tls.Certificate + Email string `description:"Email address used for registration"` + Domains []Domain `description:"SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='main.net,san1.net,san2.net'"` + Storage string `description:"File or key used for certificates storage."` + OnDemand bool `description:"Enable on demand certificate. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."` + OnHostRule bool `description:"Enable certificate generation on frontends Host rules."` + CAServer string `description:"CA server to use."` + EntryPoint string `description:"Entrypoint to proxy acme challenge to."` + client *acme.Client + defaultCertificate *tls.Certificate + store cluster.Store + challengeProvider *challengeProvider + checkOnDemandDomain func(domain string) bool } //Domains parse []Domain @@ -218,11 +75,7 @@ type Domain struct { } func (a *ACME) init() error { - if len(a.Store) == 0 { - a.Store = a.StorageFile - } acme.Logger = fmtlog.New(ioutil.Discard, "", 0) - log.Debugf("Generating default certificate...") // no certificates in TLS config, so we add a default one cert, err := generateDefaultCertificate() if err != nil { @@ -232,78 +85,174 @@ func (a *ACME) init() error { return nil } -// CreateClusterConfig creates a tls.config from using ACME configuration -func (a *ACME) CreateClusterConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(domain string) bool) error { +// CreateClusterConfig creates a tls.config using ACME configuration in cluster mode +func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tls.Config, checkOnDemandDomain func(domain string) bool) error { err := a.init() if err != nil { return err } - if len(a.Store) == 0 { - return errors.New("Empty Store, please provide a filename/key for certs storage") + if len(a.Storage) == 0 { + return errors.New("Empty Store, please provide a key for certs storage") } + a.checkOnDemandDomain = checkOnDemandDomain tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate) + tlsConfig.GetCertificate = a.getCertificate + + datastore, err := cluster.NewDataStore( + staert.KvSource{ + Store: leadership.Store, + Prefix: leadership.Store.Prefix + "/acme/account", + }, + leadership.Pool.Ctx(), &Account{}, + func(object cluster.Object) error { + account := object.(*Account) + account.Init() + if !leadership.IsLeader() { + a.client, err = a.buildACMEClient(account) + if err != nil { + log.Errorf("Error building ACME client %+v: %s", object, err.Error()) + } + } + + return nil + }) + if err != nil { + return err + } + + a.store = datastore + a.challengeProvider = newMemoryChallengeProvider(a.store) + + ticker := time.NewTicker(24 * time.Hour) + leadership.Pool.AddGoCtx(func(ctx context.Context) { + log.Infof("Starting ACME renew job...") + defer log.Infof("Stopped ACME renew job...") + select { + case <-ctx.Done(): + return + case <-ticker.C: + if err := a.renewCertificates(); err != nil { + log.Errorf("Error renewing ACME certificate: %s", err.Error()) + } + } + }) + + leadership.AddListener(func(elected bool) error { + if elected { + object, err := a.store.Load() + if err != nil { + return err + } + transaction, object, err := a.store.Begin() + if err != nil { + return err + } + account := object.(*Account) + account.Init() + var needRegister bool + if account == nil || len(account.Email) == 0 { + account, err = NewAccount(a.Email) + if err != nil { + return err + } + needRegister = true + } + if err != nil { + return err + } + log.Debugf("buildACMEClient...") + a.client, err = a.buildACMEClient(account) + if err != nil { + return err + } + if needRegister { + // New users will need to register; be sure to save it + log.Debugf("Register...") + reg, err := a.client.Register() + if err != nil { + return err + } + account.Registration = reg + } + // The client has a URL to the current Let's Encrypt Subscriber + // Agreement. The user will need to agree to it. + log.Debugf("AgreeToTOS...") + err = a.client.AgreeToTOS() + if err != nil { + return err + } + err = transaction.Commit(account) + if err != nil { + return err + } + safe.Go(func() { + a.retrieveCertificates() + if err := a.renewCertificates(); err != nil { + log.Errorf("Error renewing ACME certificate %+v: %s", account, err.Error()) + } + }) + } + return nil + }) return nil } -// CreateLocalConfig creates a tls.config from using ACME configuration -func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(domain string) bool) error { +// CreateLocalConfig creates a tls.config using local ACME configuration +func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, checkOnDemandDomain func(domain string) bool) error { err := a.init() if err != nil { return err } - if len(a.Store) == 0 { - return errors.New("Empty Store, please provide a filename/key for certs storage") + if len(a.Storage) == 0 { + return errors.New("Empty Store, please provide a filename for certs storage") } + a.checkOnDemandDomain = checkOnDemandDomain tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate) + tlsConfig.GetCertificate = a.getCertificate + + localStore := NewLocalStore(a.Storage) + a.store = localStore + a.challengeProvider = newMemoryChallengeProvider(a.store) var needRegister bool - var err error + var account *Account - // if certificates in storage, load them - if fileInfo, fileErr := os.Stat(a.Store); fileErr == nil && fileInfo.Size() != 0 { - log.Infof("Loading ACME certificates...") + if fileInfo, fileErr := os.Stat(a.Storage); fileErr == nil && fileInfo.Size() != 0 { + log.Infof("Loading ACME Account...") // load account - a.account, err = a.loadAccount(a) + object, err := localStore.Load() if err != nil { return err } + account = object.(*Account) } else { log.Infof("Generating ACME Account...") - // Create a user. New accounts need an email and private key to start - privateKey, err := rsa.GenerateKey(rand.Reader, 4096) + account, err = NewAccount(a.Email) if err != nil { return err } - a.account = &Account{ - Email: a.Email, - PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey), - } - a.account.DomainsCertificate = DomainsCertificates{Certs: []*DomainsCertificate{}, lock: &sync.RWMutex{}} needRegister = true } - a.client, err = a.buildACMEClient() - if err != nil { - return err - } - a.client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01}) - wrapperChallengeProvider := newWrapperChallengeProvider() - err = client.SetChallengeProvider(acme.TLSSNI01, wrapperChallengeProvider) + log.Infof("buildACMEClient...") + a.client, err = a.buildACMEClient(account) if err != nil { return err } if needRegister { // New users will need to register; be sure to save it + log.Infof("Register...") reg, err := a.client.Register() if err != nil { return err } - a.account.Registration = reg + account.Registration = reg } // The client has a URL to the current Let's Encrypt Subscriber // Agreement. The user will need to agree to it. + log.Infof("AgreeToTOS...") err = a.client.AgreeToTOS() if err != nil { // Let's Encrypt Subscriber Agreement renew ? @@ -311,45 +260,33 @@ func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, CheckOnDemandDomain func if err != nil { return err } - a.account.Registration = reg + account.Registration = reg err = a.client.AgreeToTOS() if err != nil { - log.Errorf("Error sending ACME agreement to TOS: %+v: %s", a.account, err.Error()) + log.Errorf("Error sending ACME agreement to TOS: %+v: %s", account, err.Error()) } } // save account - err = a.saveAccount() + transaction, _, err := a.store.Begin() + if err != nil { + return err + } + err = transaction.Commit(account) if err != nil { return err } safe.Go(func() { - a.retrieveCertificates(a.client) - if err := a.renewCertificates(a.client); err != nil { - log.Errorf("Error renewing ACME certificate %+v: %s", a.account, err.Error()) + a.retrieveCertificates() + if err := a.renewCertificates(); err != nil { + log.Errorf("Error renewing ACME certificate %+v: %s", account, err.Error()) } }) - tlsConfig.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { - if challengeCert, ok := wrapperChallengeProvider.getCertificate(clientHello.ServerName); ok { - return challengeCert, nil - } - if domainCert, ok := a.account.DomainsCertificate.getCertificateForDomain(clientHello.ServerName); ok { - return domainCert.tlsCert, nil - } - if a.OnDemand { - if CheckOnDemandDomain != nil && !CheckOnDemandDomain(clientHello.ServerName) { - return nil, nil - } - return a.loadCertificateOnDemand(clientHello) - } - return nil, nil - } - ticker := time.NewTicker(24 * time.Hour) safe.Go(func() { for range ticker.C { - if err := a.renewCertificates(client, account); err != nil { + if err := a.renewCertificates(); err != nil { log.Errorf("Error renewing ACME certificate %+v: %s", account, err.Error()) } } @@ -358,26 +295,54 @@ func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, CheckOnDemandDomain func return nil } -func (a *ACME) retrieveCertificates(client *acme.Client) { +func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { + account := a.store.Get().(*Account) + if challengeCert, ok := a.challengeProvider.getCertificate(clientHello.ServerName); ok { + log.Debugf("ACME got challenge %s", clientHello.ServerName) + return challengeCert, nil + } + if domainCert, ok := account.DomainsCertificate.getCertificateForDomain(clientHello.ServerName); ok { + log.Debugf("ACME got domaincert %s", clientHello.ServerName) + return domainCert.tlsCert, nil + } + if a.OnDemand { + if a.checkOnDemandDomain != nil && !a.checkOnDemandDomain(clientHello.ServerName) { + return nil, nil + } + return a.loadCertificateOnDemand(clientHello) + } + log.Debugf("ACME got nothing %s", clientHello.ServerName) + return nil, nil +} + +func (a *ACME) retrieveCertificates() { log.Infof("Retrieving ACME certificates...") for _, domain := range a.Domains { // check if cert isn't already loaded - if _, exists := a.account.DomainsCertificate.exists(domain); !exists { + account := a.store.Get().(*Account) + if _, exists := account.DomainsCertificate.exists(domain); !exists { domains := []string{} domains = append(domains, domain.Main) domains = append(domains, domain.SANs...) - certificateResource, err := a.getDomainsCertificates(client, domains) + certificateResource, err := a.getDomainsCertificates(domains) if err != nil { log.Errorf("Error getting ACME certificate for domain %s: %s", domains, err.Error()) continue } - _, err = a.account.DomainsCertificate.addCertificateForDomains(certificateResource, domain) + transaction, object, err := a.store.Begin() + if err != nil { + log.Errorf("Error creating ACME store transaction from domain %s: %s", domain, err.Error()) + continue + } + account = object.(*Account) + _, err = account.DomainsCertificate.addCertificateForDomains(certificateResource, domain) if err != nil { log.Errorf("Error adding ACME certificate for domain %s: %s", domains, err.Error()) continue } - if err = a.saveAccount(); err != nil { - log.Errorf("Error Saving ACME account %+v: %s", a.account, err.Error()) + + if err = transaction.Commit(account); err != nil { + log.Errorf("Error Saving ACME account %+v: %s", account, err.Error()) continue } } @@ -385,12 +350,18 @@ func (a *ACME) retrieveCertificates(client *acme.Client) { log.Infof("Retrieved ACME certificates") } -func (a *ACME) renewCertificates(client *acme.Client) error { +func (a *ACME) renewCertificates() error { log.Debugf("Testing certificate renew...") - for _, certificateResource := range a.account.DomainsCertificate.Certs { + account := a.store.Get().(*Account) + for _, certificateResource := range account.DomainsCertificate.Certs { if certificateResource.needRenew() { + transaction, object, err := a.store.Begin() + if err != nil { + return err + } + account = object.(*Account) log.Debugf("Renewing certificate %+v", certificateResource.Domains) - renewedCert, err := client.RenewCertificate(acme.CertificateResource{ + renewedCert, err := a.client.RenewCertificate(acme.CertificateResource{ Domain: certificateResource.Certificate.Domain, CertURL: certificateResource.Certificate.CertURL, CertStableURL: certificateResource.Certificate.CertStableURL, @@ -409,13 +380,14 @@ func (a *ACME) renewCertificates(client *acme.Client) error { PrivateKey: renewedCert.PrivateKey, Certificate: renewedCert.Certificate, } - err = a.account.DomainsCertificate.renewCertificates(renewedACMECert, certificateResource.Domains) + err = account.DomainsCertificate.renewCertificates(renewedACMECert, certificateResource.Domains) if err != nil { log.Errorf("Error renewing certificate: %v", err) continue } - if err = a.saveAccount(); err != nil { - log.Errorf("Error saving ACME account: %v", err) + + if err = transaction.Commit(account); err != nil { + log.Errorf("Error Saving ACME account %+v: %s", account, err.Error()) continue } } @@ -423,33 +395,45 @@ func (a *ACME) renewCertificates(client *acme.Client) error { return nil } -func (a *ACME) buildACMEClient() (*acme.Client, error) { +func (a *ACME) buildACMEClient(account *Account) (*acme.Client, error) { + log.Debugf("Building ACME client...") caServer := "https://acme-v01.api.letsencrypt.org/directory" if len(a.CAServer) > 0 { caServer = a.CAServer } - client, err := acme.NewClient(caServer, a.account, acme.RSA4096) + client, err := acme.NewClient(caServer, account, acme.RSA4096) + if err != nil { + return nil, err + } + client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01}) + err = client.SetChallengeProvider(acme.TLSSNI01, a.challengeProvider) if err != nil { return nil, err } - return client, nil } func (a *ACME) loadCertificateOnDemand(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { - if certificateResource, ok := a.account.DomainsCertificate.getCertificateForDomain(clientHello.ServerName); ok { + account := a.store.Get().(*Account) + if certificateResource, ok := account.DomainsCertificate.getCertificateForDomain(clientHello.ServerName); ok { return certificateResource.tlsCert, nil } - certificate, err := a.getDomainsCertificates(a.client, []string{clientHello.ServerName}) + certificate, err := a.getDomainsCertificates([]string{clientHello.ServerName}) if err != nil { return nil, err } log.Debugf("Got certificate on demand for domain %s", clientHello.ServerName) - cert, err := a.account.DomainsCertificate.addCertificateForDomains(certificate, Domain{Main: clientHello.ServerName}) + + transaction, object, err := a.store.Begin() if err != nil { return nil, err } - if err = a.saveAccount(); err != nil { + account = object.(*Account) + cert, err := account.DomainsCertificate.addCertificateForDomains(certificate, Domain{Main: clientHello.ServerName}) + if err != nil { + return nil, err + } + if err = transaction.Commit(account); err != nil { return nil, err } return cert.tlsCert, nil @@ -458,6 +442,7 @@ func (a *ACME) loadCertificateOnDemand(clientHello *tls.ClientHelloInfo) (*tls.C // LoadCertificateForDomains loads certificates from ACME for given domains func (a *ACME) LoadCertificateForDomains(domains []string) { safe.Go(func() { + account := a.store.Get().(*Account) var domain Domain if len(domains) == 0 { // no domain @@ -468,64 +453,39 @@ func (a *ACME) LoadCertificateForDomains(domains []string) { } else { domain = Domain{Main: domains[0]} } - if _, exists := a.account.DomainsCertificate.exists(domain); exists { + if _, exists := account.DomainsCertificate.exists(domain); exists { // domain already exists return } - certificate, err := a.getDomainsCertificates(a.client, domains) + certificate, err := a.getDomainsCertificates(domains) if err != nil { log.Errorf("Error getting ACME certificates %+v : %v", domains, err) return } log.Debugf("Got certificate for domains %+v", domains) - _, err = a.account.DomainsCertificate.addCertificateForDomains(certificate, domain) + transaction, object, err := a.store.Begin() + + if err != nil { + log.Errorf("Error creating transaction %+v : %v", domains, err) + return + } + account = object.(*Account) + _, err = account.DomainsCertificate.addCertificateForDomains(certificate, domain) if err != nil { log.Errorf("Error adding ACME certificates %+v : %v", domains, err) return } - if err = a.saveAccount(); err != nil { - log.Errorf("Error Saving ACME account %+v: %v", a.account, err) + if err = transaction.Commit(account); err != nil { + log.Errorf("Error Saving ACME account %+v: %v", account, err) return } }) } -func (a *ACME) loadAccount(acmeConfig *ACME) (*Account, error) { - a.storageLock.RLock() - defer a.storageLock.RUnlock() - account := Account{ - DomainsCertificate: DomainsCertificates{}, - } - file, err := ioutil.ReadFile(acmeConfig.Store) - if err != nil { - return nil, err - } - if err := json.Unmarshal(file, &account); err != nil { - return nil, err - } - err = account.DomainsCertificate.init() - if err != nil { - return nil, err - } - log.Infof("Loaded ACME config from store %s", acmeConfig.Store) - return &account, nil -} - -func (a *ACME) saveAccount() error { - a.storageLock.Lock() - defer a.storageLock.Unlock() - // write account to file - data, err := json.MarshalIndent(a.account, "", " ") - if err != nil { - return err - } - return ioutil.WriteFile(a.Store, data, 0600) -} - -func (a *ACME) getDomainsCertificates(client *acme.Client, domains []string) (*Certificate, error) { +func (a *ACME) getDomainsCertificates(domains []string) (*Certificate, error) { log.Debugf("Loading ACME certificates %s...", domains) bundle := true - certificate, failures := client.ObtainCertificate(domains, bundle, nil) + certificate, failures := a.client.ObtainCertificate(domains, bundle, nil) if len(failures) > 0 { log.Error(failures) return nil, fmt.Errorf("Cannot obtain certificates %s+v", failures) diff --git a/acme/challengeProvider.go b/acme/challengeProvider.go index 1083b5f83..1ed096b94 100644 --- a/acme/challengeProvider.go +++ b/acme/challengeProvider.go @@ -4,33 +4,59 @@ import ( "crypto/tls" "sync" + "bytes" + "crypto/rsa" "crypto/x509" + "encoding/gob" + "github.com/containous/traefik/cluster" + "github.com/containous/traefik/log" "github.com/xenolf/lego/acme" + "time" ) -var _ acme.ChallengeProvider = (*inMemoryChallengeProvider)(nil) - -type inMemoryChallengeProvider struct { - challengeCerts map[string]*tls.Certificate - lock sync.RWMutex +func init() { + gob.Register(rsa.PrivateKey{}) + gob.Register(rsa.PublicKey{}) } -func newWrapperChallengeProvider() *inMemoryChallengeProvider { - return &inMemoryChallengeProvider{ - challengeCerts: map[string]*tls.Certificate{}, +var _ acme.ChallengeProviderTimeout = (*challengeProvider)(nil) + +type challengeProvider struct { + store cluster.Store + lock sync.RWMutex +} + +func newMemoryChallengeProvider(store cluster.Store) *challengeProvider { + return &challengeProvider{ + store: store, } } -func (c *inMemoryChallengeProvider) getCertificate(domain string) (cert *tls.Certificate, exists bool) { +func (c *challengeProvider) getCertificate(domain string) (cert *tls.Certificate, exists bool) { + log.Debugf("Challenge GetCertificate %s", domain) c.lock.RLock() defer c.lock.RUnlock() - if cert, ok := c.challengeCerts[domain]; ok { + account := c.store.Get().(*Account) + if account.ChallengeCerts == nil { + return nil, false + } + if certBinary, ok := account.ChallengeCerts[domain]; ok { + cert := &tls.Certificate{} + var buffer bytes.Buffer + buffer.Write(certBinary) + dec := gob.NewDecoder(&buffer) + err := dec.Decode(cert) + if err != nil { + log.Errorf("Error unmarshaling challenge cert %s", err.Error()) + return nil, false + } return cert, true } return nil, false } -func (c *inMemoryChallengeProvider) Present(domain, token, keyAuth string) error { +func (c *challengeProvider) Present(domain, token, keyAuth string) error { + log.Debugf("Challenge Present %s", domain) cert, _, err := acme.TLSSNI01ChallengeCert(keyAuth) if err != nil { return err @@ -42,16 +68,40 @@ func (c *inMemoryChallengeProvider) Present(domain, token, keyAuth string) error c.lock.Lock() defer c.lock.Unlock() - for i := range cert.Leaf.DNSNames { - c.challengeCerts[cert.Leaf.DNSNames[i]] = &cert + transaction, object, err := c.store.Begin() + if err != nil { + return err } - - return nil + account := object.(*Account) + if account.ChallengeCerts == nil { + account.ChallengeCerts = map[string][]byte{} + } + for i := range cert.Leaf.DNSNames { + var buffer bytes.Buffer + enc := gob.NewEncoder(&buffer) + err := enc.Encode(cert) + if err != nil { + return err + } + account.ChallengeCerts[cert.Leaf.DNSNames[i]] = buffer.Bytes() + log.Debugf("Challenge Present cert: %s", cert.Leaf.DNSNames[i]) + } + return transaction.Commit(account) } -func (c *inMemoryChallengeProvider) CleanUp(domain, token, keyAuth string) error { +func (c *challengeProvider) CleanUp(domain, token, keyAuth string) error { + log.Debugf("Challenge CleanUp %s", domain) c.lock.Lock() defer c.lock.Unlock() - delete(c.challengeCerts, domain) - return nil + transaction, object, err := c.store.Begin() + if err != nil { + return err + } + account := object.(*Account) + delete(account.ChallengeCerts, domain) + return transaction.Commit(account) +} + +func (c *challengeProvider) Timeout() (timeout, interval time.Duration) { + return 60 * time.Second, 5 * time.Second } diff --git a/acme/localStore.go b/acme/localStore.go new file mode 100644 index 000000000..c39322091 --- /dev/null +++ b/acme/localStore.go @@ -0,0 +1,97 @@ +package acme + +import ( + "encoding/json" + "fmt" + "github.com/containous/traefik/cluster" + "github.com/containous/traefik/log" + "io/ioutil" + "sync" +) + +var _ cluster.Store = (*LocalStore)(nil) + +// LocalStore is a store using a file as storage +type LocalStore struct { + file string + storageLock sync.RWMutex + account *Account +} + +// NewLocalStore create a LocalStore +func NewLocalStore(file string) *LocalStore { + return &LocalStore{ + file: file, + storageLock: sync.RWMutex{}, + } +} + +// Get atomically a struct from the file storage +func (s *LocalStore) Get() cluster.Object { + s.storageLock.RLock() + defer s.storageLock.RUnlock() + return s.account +} + +// Load loads file into store +func (s *LocalStore) Load() (cluster.Object, error) { + s.storageLock.Lock() + defer s.storageLock.Unlock() + account := &Account{} + file, err := ioutil.ReadFile(s.file) + if err != nil { + return nil, err + } + if err := json.Unmarshal(file, &account); err != nil { + return nil, err + } + account.Init() + s.account = account + log.Infof("Loaded ACME config from store %s", s.file) + return account, nil +} + +// func (s *LocalStore) saveAccount(account *Account) error { +// s.storageLock.Lock() +// defer s.storageLock.Unlock() +// // write account to file +// data, err := json.MarshalIndent(account, "", " ") +// if err != nil { +// return err +// } +// return ioutil.WriteFile(s.file, data, 0644) +// } + +// Begin creates a transaction with the KV store. +func (s *LocalStore) Begin() (cluster.Transaction, cluster.Object, error) { + s.storageLock.Lock() + return &localTransaction{LocalStore: s}, s.account, nil +} + +var _ cluster.Transaction = (*localTransaction)(nil) + +type localTransaction struct { + *LocalStore + dirty bool +} + +// Commit allows to set an object in the file storage +func (t *localTransaction) Commit(object cluster.Object) error { + t.LocalStore.account = object.(*Account) + defer t.storageLock.Unlock() + if t.dirty { + return fmt.Errorf("Transaction already used. Please begin a new one.") + } + + // write account to file + data, err := json.MarshalIndent(object, "", " ") + if err != nil { + return err + } + return ioutil.WriteFile(t.file, data, 0644) + if err != nil { + return err + } + t.dirty = true + return nil +} diff --git a/adapters.go b/adapters.go index ac1c7835b..edf745e85 100644 --- a/adapters.go +++ b/adapters.go @@ -6,7 +6,7 @@ package main import ( "net/http" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" ) // OxyLogger implements oxy Logger interface with logrus. diff --git a/cluster/datastore.go b/cluster/datastore.go index 5bad330a8..f2bcf9c61 100644 --- a/cluster/datastore.go +++ b/cluster/datastore.go @@ -1,44 +1,62 @@ package cluster import ( + "encoding/json" "fmt" - log "github.com/Sirupsen/logrus" - "github.com/cenkalti/backoff" "github.com/containous/staert" + "github.com/containous/traefik/log" "github.com/docker/libkv/store" + "github.com/emilevauge/backoff" "github.com/satori/go.uuid" "golang.org/x/net/context" "sync" "time" ) -// Object is the struct to store -type Object interface{} - // Metadata stores Object plus metadata type Metadata struct { - Lock string + object Object + Object []byte + Lock string } +func (m *Metadata) marshall() error { + var err error + m.Object, err = json.Marshal(m.object) + return err +} + +func (m *Metadata) unmarshall() error { + if len(m.Object) == 0 { + return nil + } + return json.Unmarshal(m.Object, m.object) +} + +// Listener is called when Object has been changed in KV store +type Listener func(Object) error + +var _ Store = (*Datastore)(nil) + // Datastore holds a struct synced in a KV store type Datastore struct { - kv *staert.KvSource + kv staert.KvSource ctx context.Context localLock *sync.RWMutex - object Object meta *Metadata lockKey string + listener Listener } // NewDataStore creates a Datastore -func NewDataStore(kvSource *staert.KvSource, ctx context.Context, object Object) (*Datastore, error) { +func NewDataStore(kvSource staert.KvSource, ctx context.Context, object Object, listener Listener) (*Datastore, error) { datastore := Datastore{ kv: kvSource, ctx: ctx, - meta: &Metadata{}, - object: object, + meta: &Metadata{object: object}, lockKey: kvSource.Prefix + "/lock", localLock: &sync.RWMutex{}, + listener: listener, } err := datastore.watchChanges() if err != nil { @@ -67,17 +85,24 @@ func (d *Datastore) watchChanges() error { return err } d.localLock.Lock() - err := d.kv.LoadConfig(d.object) + err := d.kv.LoadConfig(d.meta) if err != nil { d.localLock.Unlock() return err } - err = d.kv.LoadConfig(d.meta) + err = d.meta.unmarshall() if err != nil { d.localLock.Unlock() return err } d.localLock.Unlock() + // log.Debugf("Datastore object change received: %+v", d.object) + if d.listener != nil { + err := d.listener(d.meta.object) + if err != nil { + log.Errorf("Error calling datastore listener: %s", err) + } + } } } } @@ -93,11 +118,12 @@ func (d *Datastore) watchChanges() error { } // Begin creates a transaction with the KV store. -func (d *Datastore) Begin() (*Transaction, error) { +func (d *Datastore) Begin() (Transaction, Object, error) { id := uuid.NewV4().String() + log.Debugf("Transaction %s begins", id) remoteLock, err := d.kv.NewLock(d.lockKey, &store.LockOptions{TTL: 20 * time.Second, Value: []byte(id)}) if err != nil { - return nil, err + return nil, nil, err } stopCh := make(chan struct{}) ctx, cancel := context.WithCancel(d.ctx) @@ -109,11 +135,11 @@ func (d *Datastore) Begin() (*Transaction, error) { select { case <-ctx.Done(): if errLock != nil { - return nil, errLock + return nil, nil, errLock } case <-d.ctx.Done(): stopCh <- struct{}{} - return nil, d.ctx.Err() + return nil, nil, d.ctx.Err() } // we got the lock! Now make sure we are synced with KV store @@ -131,14 +157,15 @@ func (d *Datastore) Begin() (*Transaction, error) { ebo.MaxElapsedTime = 60 * time.Second err = backoff.RetryNotify(operation, ebo, notify) if err != nil { - return nil, fmt.Errorf("Datastore cannot sync: %v", err) + return nil, nil, fmt.Errorf("Datastore cannot sync: %v", err) } // we synced with KV store, we can now return Setter - return &Transaction{ + return &datastoreTransaction{ Datastore: d, remoteLock: remoteLock, - }, nil + id: id, + }, d.meta.object, nil } func (d *Datastore) get() *Metadata { @@ -147,28 +174,50 @@ func (d *Datastore) get() *Metadata { return d.meta } +// Load load atomically a struct from the KV store +func (d *Datastore) Load() (Object, error) { + d.localLock.Lock() + defer d.localLock.Unlock() + err := d.kv.LoadConfig(d.meta) + if err != nil { + return nil, err + } + err = d.meta.unmarshall() + if err != nil { + return nil, err + } + return d.meta.object, nil +} + // Get atomically a struct from the KV store func (d *Datastore) Get() Object { d.localLock.RLock() defer d.localLock.RUnlock() - return d.object + return d.meta.object } -// Transaction allows to set a struct in the KV store -type Transaction struct { +var _ Transaction = (*datastoreTransaction)(nil) + +type datastoreTransaction struct { *Datastore remoteLock store.Locker dirty bool + id string } // Commit allows to set an object in the KV store -func (s *Transaction) Commit(object Object) error { +func (s *datastoreTransaction) Commit(object Object) error { s.localLock.Lock() defer s.localLock.Unlock() if s.dirty { return fmt.Errorf("Transaction already used. Please begin a new one.") } - err := s.kv.StoreConfig(object) + s.Datastore.meta.object = object + err := s.Datastore.meta.marshall() + if err != nil { + return err + } + err = s.kv.StoreConfig(s.Datastore.meta) if err != nil { return err } @@ -178,7 +227,8 @@ func (s *Transaction) Commit(object Object) error { return err } - s.Datastore.object = object s.dirty = true + // log.Debugf("Datastore object saved: %+v", s.object) + log.Debugf("Transaction commited %s", s.id) return nil } diff --git a/cluster/leadership.go b/cluster/leadership.go index 60d67e427..2143ee6d9 100644 --- a/cluster/leadership.go +++ b/cluster/leadership.go @@ -1,11 +1,11 @@ package cluster import ( - log "github.com/Sirupsen/logrus" - "github.com/cenkalti/backoff" + "github.com/containous/traefik/log" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" "github.com/docker/leadership" + "github.com/emilevauge/backoff" "golang.org/x/net/context" "time" ) @@ -15,6 +15,8 @@ type Leadership struct { *safe.Pool *types.Cluster candidate *leadership.Candidate + leader safe.Safe + listeners []LeaderListener } // NewLeadership creates a leadership @@ -23,9 +25,13 @@ func NewLeadership(ctx context.Context, cluster *types.Cluster) *Leadership { Pool: safe.NewPool(ctx), Cluster: cluster, candidate: leadership.NewCandidate(cluster.Store, cluster.Store.Prefix+"/leader", cluster.Node, 20*time.Second), + listeners: []LeaderListener{}, } } +// LeaderListener is called when leadership has changed +type LeaderListener func(elected bool) error + // Participate tries to be a leader func (l *Leadership) Participate(pool *safe.Pool) { pool.GoCtx(func(ctx context.Context) { @@ -46,10 +52,15 @@ func (l *Leadership) Participate(pool *safe.Pool) { }) } +// AddListener adds a leadership listerner +func (l *Leadership) AddListener(listener LeaderListener) { + l.listeners = append(l.listeners, listener) +} + // Resign resigns from being a leader func (l *Leadership) Resign() { l.candidate.Resign() - log.Infof("Node %s resined", l.Cluster.Node) + log.Infof("Node %s resigned", l.Cluster.Node) } func (l *Leadership) run(candidate *leadership.Candidate, ctx context.Context) error { @@ -70,9 +81,22 @@ func (l *Leadership) run(candidate *leadership.Candidate, ctx context.Context) e func (l *Leadership) onElection(elected bool) { if elected { log.Infof("Node %s elected leader ♚", l.Cluster.Node) + l.leader.Set(true) l.Start() } else { log.Infof("Node %s elected slave ♝", l.Cluster.Node) + l.leader.Set(false) l.Stop() } + for _, listener := range l.listeners { + err := listener(elected) + if err != nil { + log.Errorf("Error calling Leadership listener: %s", err) + } + } +} + +// IsLeader returns true if current node is leader +func (l *Leadership) IsLeader() bool { + return l.leader.Get().(bool) } diff --git a/cluster/store.go b/cluster/store.go new file mode 100644 index 000000000..c8e9207be --- /dev/null +++ b/cluster/store.go @@ -0,0 +1,16 @@ +package cluster + +// Object is the struct to store +type Object interface{} + +// Store is a generic interface to represents a storage +type Store interface { + Load() (Object, error) + Get() Object + Begin() (Transaction, Object, error) +} + +// Transaction allows to set a struct in the KV store +type Transaction interface { + Commit(object Object) error +} diff --git a/configuration.go b/configuration.go index 558ce2932..20546e8b3 100644 --- a/configuration.go +++ b/configuration.go @@ -23,14 +23,14 @@ type TraefikConfiguration struct { // GlobalConfiguration holds global configuration (with providers, etc.). // It's populated from the traefik configuration file passed as an argument to the binary. type GlobalConfiguration struct { - GraceTimeOut int64 `short:"g" description:"Duration to give active requests a chance to finish during hot-reload"` - Debug bool `short:"d" description:"Enable debug mode"` - AccessLogsFile string `description:"Access logs file"` - TraefikLogsFile string `description:"Traefik logs file"` - LogLevel string `short:"l" description:"Log level"` - EntryPoints EntryPoints `description:"Entrypoints definition using format: --entryPoints='Name:http Address::8000 Redirect.EntryPoint:https' --entryPoints='Name:https Address::4442 TLS:tests/traefik.crt,tests/traefik.key'"` - Cluster *types.Cluster - Constraints types.Constraints `description:"Filter services by constraint, matching with service tags."` + GraceTimeOut int64 `short:"g" description:"Duration to give active requests a chance to finish during hot-reload"` + Debug bool `short:"d" description:"Enable debug mode"` + AccessLogsFile string `description:"Access logs file"` + TraefikLogsFile string `description:"Traefik logs file"` + LogLevel string `short:"l" description:"Log level"` + EntryPoints EntryPoints `description:"Entrypoints definition using format: --entryPoints='Name:http Address::8000 Redirect.EntryPoint:https' --entryPoints='Name:https Address::4442 TLS:tests/traefik.crt,tests/traefik.key'"` + Cluster *types.Cluster `description:"Enable clustering"` + Constraints types.Constraints `description:"Filter services by constraint, matching with service tags"` ACME *acme.ACME `description:"Enable ACME (Let's Encrypt): automatic SSL"` DefaultEntryPoints DefaultEntryPoints `description:"Entrypoints to be used by frontends that do not specify any entrypoint"` ProvidersThrottleDuration time.Duration `description:"Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time."` diff --git a/glide.lock b/glide.lock index f447712ec..26c72cbbc 100644 --- a/glide.lock +++ b/glide.lock @@ -1,3 +1,4 @@ +<<<<<<< 2fbcca003e6454c848801c859d8563da94ea8aaf <<<<<<< a13549cc28273ba5c15a739fa4aaeb3e0f7216a4 hash: c0ac205a859d78847e21d3cd63f427ffba985755c6ae84373e4a20364ba39b05 <<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab @@ -8,7 +9,14 @@ updated: 2016-09-28T16:50:04.352639437+01:00 hash: 809b3fa812ca88940fdc15530804a4bcd881708e4819fed5aa45c42c871ba5cf updated: 2016-09-20T14:50:04.029710103+02:00 >>>>>>> Add KV datastore +<<<<<<< bea5ad3f132bae27b6c1a83adf00154058b484b5 >>>>>>> Add KV datastore +======= +======= +hash: 49c7bd0e32b2764248183bda52f168fe22d69e2db5e17c1dbeebbe71be9929b1 +updated: 2016-08-11T14:33:42.826534934+02:00 +>>>>>>> Add ACME store +>>>>>>> Add ACME store imports: - name: github.com/abbot/go-http-auth version: cb4372376e1e00e9f6ab9ec142e029302c9e7140 @@ -33,7 +41,7 @@ imports: - name: github.com/containous/mux version: a819b77bba13f0c0cbe36e437bc2e948411b3996 - name: github.com/containous/staert - version: 044bdfee6c8f5e8fb71f70d5ba1cf4cb11a94e97 + version: 56058c7d4152831a641764d10ec91132adf061ea - name: github.com/coreos/etcd version: 1c9e0a0e33051fed6c05c141e6fcbfe5c7f2a899 subpackages: diff --git a/glide.yaml b/glide.yaml index 9f1c7b4e6..15b1b8be0 100644 --- a/glide.yaml +++ b/glide.yaml @@ -21,7 +21,7 @@ import: - stream - utils - package: github.com/containous/staert - version: 044bdfee6c8f5e8fb71f70d5ba1cf4cb11a94e97 + version: 56058c7d4152831a641764d10ec91132adf061ea - package: github.com/docker/engine-api version: 62043eb79d581a32ea849645277023c550732e52 subpackages: diff --git a/middlewares/authenticator.go b/middlewares/authenticator.go index b5e6b0eb1..33fb77ce9 100644 --- a/middlewares/authenticator.go +++ b/middlewares/authenticator.go @@ -2,7 +2,7 @@ package middlewares import ( "fmt" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/abbot/go-http-auth" "github.com/codegangsta/negroni" "github.com/containous/traefik/types" diff --git a/middlewares/logger.go b/middlewares/logger.go index 74617da54..d65cbbf3f 100644 --- a/middlewares/logger.go +++ b/middlewares/logger.go @@ -12,7 +12,7 @@ import ( "sync/atomic" "time" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/streamrail/concurrent-map" ) diff --git a/middlewares/retry.go b/middlewares/retry.go index e40b75968..8577a1685 100644 --- a/middlewares/retry.go +++ b/middlewares/retry.go @@ -3,7 +3,7 @@ package middlewares import ( "bufio" "bytes" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/vulcand/oxy/utils" "net" "net/http" diff --git a/middlewares/rewrite.go b/middlewares/rewrite.go index eaaff6bec..d3bc4d635 100644 --- a/middlewares/rewrite.go +++ b/middlewares/rewrite.go @@ -1,7 +1,7 @@ package middlewares import ( - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/vulcand/vulcand/plugin/rewrite" "net/http" ) diff --git a/provider/consul_catalog.go b/provider/consul_catalog.go index 1d1d4c928..947368a68 100644 --- a/provider/consul_catalog.go +++ b/provider/consul_catalog.go @@ -9,7 +9,8 @@ import ( "time" "github.com/BurntSushi/ty/fun" - log "github.com/Sirupsen/logrus" + "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/cenk/backoff" "github.com/containous/traefik/job" "github.com/containous/traefik/safe" @@ -270,7 +271,7 @@ func (provider *ConsulCatalog) getNodes(index map[string][]string) ([]catalogUpd name := strings.ToLower(service) if !strings.Contains(name, " ") && !visited[name] { visited[name] = true - log.WithFields(log.Fields{ + log.WithFields(logrus.Fields{ "service": name, }).Debug("Fetching service") healthy, err := provider.healthyNodes(name) diff --git a/provider/docker.go b/provider/docker.go index 73f897244..175cd3ebb 100644 --- a/provider/docker.go +++ b/provider/docker.go @@ -13,7 +13,7 @@ import ( "golang.org/x/net/context" "github.com/BurntSushi/ty/fun" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/cenk/backoff" "github.com/containous/traefik/job" "github.com/containous/traefik/safe" diff --git a/provider/file.go b/provider/file.go index dc42b26d5..aab244a89 100644 --- a/provider/file.go +++ b/provider/file.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/BurntSushi/toml" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" "gopkg.in/fsnotify.v1" diff --git a/provider/k8s/client.go b/provider/k8s/client.go index d7ac398e6..ad9679cae 100644 --- a/provider/k8s/client.go +++ b/provider/k8s/client.go @@ -5,7 +5,7 @@ import ( "crypto/x509" "encoding/json" "fmt" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/parnurzeal/gorequest" "net/http" "net/url" diff --git a/provider/kubernetes.go b/provider/kubernetes.go index a5c8654f6..22b9fd18e 100644 --- a/provider/kubernetes.go +++ b/provider/kubernetes.go @@ -2,6 +2,10 @@ package provider import ( "fmt" + "github.com/containous/traefik/log" + "github.com/containous/traefik/provider/k8s" + "github.com/containous/traefik/safe" + "github.com/containous/traefik/types" "io/ioutil" "os" "reflect" @@ -10,7 +14,6 @@ import ( "text/template" "time" - log "github.com/Sirupsen/logrus" "github.com/cenk/backoff" "github.com/containous/traefik/job" "github.com/containous/traefik/provider/k8s" diff --git a/provider/kv.go b/provider/kv.go index 86c97d1b7..27b5469b2 100644 --- a/provider/kv.go +++ b/provider/kv.go @@ -9,7 +9,7 @@ import ( "errors" "github.com/BurntSushi/ty/fun" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/cenk/backoff" "github.com/containous/traefik/job" "github.com/containous/traefik/safe" diff --git a/provider/marathon.go b/provider/marathon.go index c06b49309..ddeefe923 100644 --- a/provider/marathon.go +++ b/provider/marathon.go @@ -13,7 +13,7 @@ import ( "time" "github.com/BurntSushi/ty/fun" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/cenk/backoff" "github.com/containous/traefik/job" "github.com/containous/traefik/safe" diff --git a/provider/mesos.go b/provider/mesos.go index 2396cf018..0943a3db6 100644 --- a/provider/mesos.go +++ b/provider/mesos.go @@ -8,7 +8,7 @@ import ( "fmt" "github.com/BurntSushi/ty/fun" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/cenk/backoff" "github.com/containous/traefik/job" "github.com/containous/traefik/safe" diff --git a/provider/mesos_test.go b/provider/mesos_test.go index 1c1eb2376..5968db8e9 100644 --- a/provider/mesos_test.go +++ b/provider/mesos_test.go @@ -1,7 +1,7 @@ package provider import ( - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/containous/traefik/types" "github.com/mesosphere/mesos-dns/records/state" "reflect" diff --git a/provider/provider.go b/provider/provider.go index 441861025..72610c7af 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -13,7 +13,7 @@ import ( "os" "github.com/BurntSushi/toml" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/containous/traefik/autogen" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" diff --git a/safe/routine.go b/safe/routine.go index 75e2358b7..a7f171968 100644 --- a/safe/routine.go +++ b/safe/routine.go @@ -1,8 +1,8 @@ package safe import ( + "github.com/containous/traefik/log" "golang.org/x/net/context" - "log" "runtime/debug" "sync" ) @@ -26,18 +26,26 @@ type Pool struct { } // NewPool creates a Pool -func NewPool(baseCtx context.Context) *Pool { +func NewPool(parentCtx context.Context) *Pool { + baseCtx, _ := context.WithCancel(parentCtx) ctx, cancel := context.WithCancel(baseCtx) return &Pool{ + baseCtx: baseCtx, ctx: ctx, cancel: cancel, - baseCtx: baseCtx, } } // Ctx returns main context func (p *Pool) Ctx() context.Context { - return p.ctx + return p.baseCtx +} + +//AddGoCtx adds a recoverable goroutine with a context without starting it +func (p *Pool) AddGoCtx(goroutine routineCtx) { + p.lock.Lock() + p.routinesCtx = append(p.routinesCtx, goroutine) + p.lock.Unlock() } //GoCtx starts a recoverable goroutine with a context @@ -71,6 +79,7 @@ func (p *Pool) Go(goroutine func(stop chan bool)) { // Stop stops all started routines, waiting for their termination func (p *Pool) Stop() { p.lock.Lock() + defer p.lock.Unlock() p.cancel() for _, routine := range p.routines { routine.stop <- true @@ -79,12 +88,12 @@ func (p *Pool) Stop() { for _, routine := range p.routines { close(routine.stop) } - p.lock.Unlock() } -// Start starts all stoped routines +// Start starts all stopped routines func (p *Pool) Start() { p.lock.Lock() + defer p.lock.Unlock() p.ctx, p.cancel = context.WithCancel(p.baseCtx) for _, routine := range p.routines { p.waitGroup.Add(1) @@ -102,7 +111,6 @@ func (p *Pool) Start() { p.waitGroup.Done() }) } - p.lock.Unlock() } // Go starts a recoverable goroutine @@ -123,6 +131,6 @@ func GoWithRecover(goroutine func(), customRecover func(err interface{})) { } func defaultRecoverGoroutine(err interface{}) { - log.Println(err) + log.Errorf("Error in Go routine: %s", err) debug.PrintStack() } diff --git a/server.go b/server.go index a9222bf2a..5ace528aa 100644 --- a/server.go +++ b/server.go @@ -21,10 +21,10 @@ import ( "golang.org/x/net/context" - log "github.com/Sirupsen/logrus" "github.com/codegangsta/negroni" "github.com/containous/mux" "github.com/containous/traefik/cluster" + "github.com/containous/traefik/log" "github.com/containous/traefik/middlewares" "github.com/containous/traefik/provider" "github.com/containous/traefik/safe" @@ -93,8 +93,8 @@ func NewServer(globalConfiguration GlobalConfiguration) *Server { // Start starts the server and blocks until server is shutted down. func (server *Server) Start() { - server.startLeadership() server.startHTTPServers() + server.startLeadership() server.routinesPool.Go(func(stop chan bool) { server.listenProviders(stop) }) @@ -129,7 +129,7 @@ func (server *Server) Close() { if ctx.Err() == context.Canceled { return } else if ctx.Err() == context.DeadlineExceeded { - log.Debugf("I love you all :'( ✝") + log.Warnf("Timeout while stopping traefik, killing instance ✝") os.Exit(1) } }(ctx) @@ -147,17 +147,17 @@ func (server *Server) Close() { func (server *Server) startLeadership() { if server.leadership != nil { server.leadership.Participate(server.routinesPool) - server.leadership.GoCtx(func(ctx context.Context) { - log.Debugf("Started test routine") - <-ctx.Done() - log.Debugf("Stopped test routine") - }) + // server.leadership.AddGoCtx(func(ctx context.Context) { + // log.Debugf("Started test routine") + // <-ctx.Done() + // log.Debugf("Stopped test routine") + // }) } } func (server *Server) stopLeadership() { if server.leadership != nil { - server.leadership.Resign() + server.leadership.Stop() } } @@ -283,7 +283,13 @@ func (server *Server) listenConfigurations(stop chan bool) { } func (server *Server) postLoadConfig() { - if server.globalConfiguration.ACME != nil && server.globalConfiguration.ACME.OnHostRule { + if server.globalConfiguration.ACME == nil { + return + } + if server.leadership != nil && !server.leadership.IsLeader() { + return + } + if server.globalConfiguration.ACME.OnHostRule { currentConfigurations := server.currentConfigurations.Get().(configs) for _, configuration := range currentConfigurations { for _, frontend := range configuration.Frontends { @@ -401,9 +407,16 @@ func (server *Server) createTLSConfig(entryPointName string, tlsOption *TLS, rou } return false } - err := server.globalConfiguration.ACME.CreateLocalConfig(config, checkOnDemandDomain) - if err != nil { - return nil, err + if server.leadership == nil { + err := server.globalConfiguration.ACME.CreateLocalConfig(config, checkOnDemandDomain) + if err != nil { + return nil, err + } + } else { + err := server.globalConfiguration.ACME.CreateClusterConfig(server.leadership, config, checkOnDemandDomain) + if err != nil { + return nil, err + } } } } else { diff --git a/traefik.go b/traefik.go index e5f65468a..397133819 100644 --- a/traefik.go +++ b/traefik.go @@ -12,10 +12,11 @@ import ( "strings" "text/template" - log "github.com/Sirupsen/logrus" + "github.com/Sirupsen/logrus" "github.com/containous/flaeg" "github.com/containous/staert" "github.com/containous/traefik/acme" + "github.com/containous/traefik/log" "github.com/containous/traefik/middlewares" "github.com/containous/traefik/provider" "github.com/containous/traefik/types" @@ -208,7 +209,7 @@ func run(traefikConfiguration *TraefikConfiguration) { } // logging - level, err := log.ParseLevel(strings.ToLower(globalConfiguration.LogLevel)) + level, err := logrus.ParseLevel(strings.ToLower(globalConfiguration.LogLevel)) if err != nil { log.Error("Error getting level", err) } @@ -224,10 +225,10 @@ func run(traefikConfiguration *TraefikConfiguration) { log.Error("Error opening file", err) } else { log.SetOutput(fi) - log.SetFormatter(&log.TextFormatter{DisableColors: true, FullTimestamp: true, DisableSorting: true}) + log.SetFormatter(&logrus.TextFormatter{DisableColors: true, FullTimestamp: true, DisableSorting: true}) } } else { - log.SetFormatter(&log.TextFormatter{FullTimestamp: true, DisableSorting: true}) + log.SetFormatter(&logrus.TextFormatter{FullTimestamp: true, DisableSorting: true}) } jsonConf, _ := json.Marshal(globalConfiguration) log.Infof("Traefik version %s built on %s", version.Version, version.BuildDate) diff --git a/types/types.go b/types/types.go index e2208180b..b9d8298f8 100644 --- a/types/types.go +++ b/types/types.go @@ -201,7 +201,7 @@ type Store struct { // Cluster holds cluster config type Cluster struct { - Node string + Node string `description:"Node name"` Store *Store } diff --git a/web.go b/web.go index 21b2bd130..974c2fbf8 100644 --- a/web.go +++ b/web.go @@ -8,7 +8,7 @@ import ( "net/http" "runtime" - log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/log" "github.com/codegangsta/negroni" "github.com/containous/mux" "github.com/containous/traefik/autogen" From e72e65858fbbab16dffb0d14b854026e224a3875 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Fri, 23 Sep 2016 18:27:01 +0200 Subject: [PATCH 4/6] Challenge certs PEM encoding Signed-off-by: Emile Vauge --- .gitignore | 1 - acme/account.go | 40 ++++++-- acme/acme.go | 37 ++++++- acme/acme_test.go | 2 +- acme/challengeProvider.go | 67 ++++++------- acme/crypto.go | 47 +++++++++ acme/localStore.go | 13 +-- cluster/datastore.go | 39 +++++--- cluster/leadership.go | 2 +- glide.lock | 100 ++++++++++++++++++- glide.yaml | 2 +- integration/consul_test.go | 12 +-- log/logger.go | 188 +++++++++++++++++++++++++++++++++++ middlewares/authenticator.go | 2 +- provider/consul_catalog.go | 2 +- provider/docker.go | 2 +- provider/kubernetes.go | 3 - provider/kv.go | 12 +-- provider/marathon.go | 2 +- provider/mesos.go | 2 +- provider/provider.go | 2 +- rules.go | 6 +- server.go | 2 +- traefik.go | 8 +- web.go | 4 +- 25 files changed, 490 insertions(+), 107 deletions(-) create mode 100644 log/logger.go diff --git a/.gitignore b/.gitignore index 5caf38ec1..9a57474fa 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ gen.go .idea .intellij -log *.iml traefik traefik.toml diff --git a/acme/account.go b/acme/account.go index 55ac44672..52897db02 100644 --- a/acme/account.go +++ b/acme/account.go @@ -20,7 +20,14 @@ type Account struct { Registration *acme.RegistrationResource PrivateKey []byte DomainsCertificate DomainsCertificates - ChallengeCerts map[string][]byte + ChallengeCerts map[string]*ChallengeCert +} + +// ChallengeCert stores a challenge certificate +type ChallengeCert struct { + Certificate []byte + PrivateKey []byte + certificate *tls.Certificate } // Init inits acccount struct @@ -29,9 +36,27 @@ func (a *Account) Init() error { 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) @@ -43,22 +68,22 @@ func NewAccount(email string) (*Account, error) { return &Account{ Email: email, PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey), - DomainsCertificate: domainsCerts, - ChallengeCerts: map[string][]byte{}}, nil + DomainsCertificate: DomainsCertificates{Certs: domainsCerts.Certs}, + ChallengeCerts: map[string]*ChallengeCert{}}, nil } // GetEmail returns email -func (a Account) GetEmail() string { +func (a *Account) GetEmail() string { return a.Email } // GetRegistration returns lets encrypt registration resource -func (a Account) GetRegistration() *acme.RegistrationResource { +func (a *Account) GetRegistration() *acme.RegistrationResource { return a.Registration } // GetPrivateKey returns private key -func (a Account) GetPrivateKey() crypto.PrivateKey { +func (a *Account) GetPrivateKey() crypto.PrivateKey { if privateKey, err := x509.ParsePKCS1PrivateKey(a.PrivateKey); err == nil { return privateKey } @@ -81,6 +106,7 @@ type DomainsCertificates struct { lock sync.RWMutex } +// Init inits DomainsCertificates func (dc *DomainsCertificates) Init() error { dc.lock.Lock() defer dc.lock.Unlock() @@ -167,7 +193,7 @@ func (dc *DomainsCertificate) needRenew() bool { return true } // <= 7 days left, renew certificate - if crt.NotAfter.Before(time.Now().Add(time.Duration(24 * 7 * time.Hour))) { + if crt.NotAfter.Before(time.Now().Add(time.Duration(24 * 30 * time.Hour))) { return true } } diff --git a/acme/acme.go b/acme/acme.go index 575e7b2ac..045ff5572 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "errors" "fmt" + "github.com/cenk/backoff" "github.com/containous/staert" "github.com/containous/traefik/cluster" "github.com/containous/traefik/log" @@ -22,6 +23,7 @@ type ACME struct { Email string `description:"Email address used for registration"` Domains []Domain `description:"SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='main.net,san1.net,san2.net'"` Storage string `description:"File or key used for certificates storage."` + StorageFile string // deprecated OnDemand bool `description:"Enable on demand certificate. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."` OnHostRule bool `description:"Enable certificate generation on frontends Host rules."` CAServer string `description:"CA server to use."` @@ -82,6 +84,11 @@ func (a *ACME) init() error { return err } a.defaultCertificate = cert + // TODO: to remove in the futurs + if len(a.StorageFile) > 0 && len(a.Storage) == 0 { + log.Warnf("ACME.StorageFile is deprecated, use ACME.Storage instead") + a.Storage = a.StorageFile + } return nil } @@ -160,7 +167,6 @@ func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tl if err != nil { return err } - log.Debugf("buildACMEClient...") a.client, err = a.buildACMEClient(account) if err != nil { return err @@ -179,7 +185,16 @@ func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tl log.Debugf("AgreeToTOS...") err = a.client.AgreeToTOS() if err != nil { - return err + // 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 { @@ -302,7 +317,7 @@ func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificat return challengeCert, nil } if domainCert, ok := account.DomainsCertificate.getCertificateForDomain(clientHello.ServerName); ok { - log.Debugf("ACME got domaincert %s", clientHello.ServerName) + log.Debugf("ACME got domain cert %s", clientHello.ServerName) return domainCert.tlsCert, nil } if a.OnDemand { @@ -442,6 +457,22 @@ func (a *ACME) loadCertificateOnDemand(clientHello *tls.ClientHelloInfo) (*tls.C // LoadCertificateForDomains loads certificates from ACME for given domains func (a *ACME) LoadCertificateForDomains(domains []string) { safe.Go(func() { + 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 if len(domains) == 0 { diff --git a/acme/acme_test.go b/acme/acme_test.go index 399592022..ba7e7509d 100644 --- a/acme/acme_test.go +++ b/acme/acme_test.go @@ -63,7 +63,7 @@ func TestDomainsSetAppend(t *testing.T) { func TestCertificatesRenew(t *testing.T) { domainsCertificates := DomainsCertificates{ - lock: &sync.RWMutex{}, + lock: sync.RWMutex{}, Certs: []*DomainsCertificate{ { Domains: Domain{ diff --git a/acme/challengeProvider.go b/acme/challengeProvider.go index 1ed096b94..e66469258 100644 --- a/acme/challengeProvider.go +++ b/acme/challengeProvider.go @@ -2,23 +2,17 @@ package acme import ( "crypto/tls" + "strings" "sync" - "bytes" - "crypto/rsa" - "crypto/x509" - "encoding/gob" + "fmt" + "github.com/cenk/backoff" "github.com/containous/traefik/cluster" "github.com/containous/traefik/log" "github.com/xenolf/lego/acme" "time" ) -func init() { - gob.Register(rsa.PrivateKey{}) - gob.Register(rsa.PublicKey{}) -} - var _ acme.ChallengeProviderTimeout = (*challengeProvider)(nil) type challengeProvider struct { @@ -34,34 +28,44 @@ func newMemoryChallengeProvider(store cluster.Store) *challengeProvider { func (c *challengeProvider) getCertificate(domain string) (cert *tls.Certificate, exists bool) { log.Debugf("Challenge GetCertificate %s", domain) + if !strings.HasSuffix(domain, ".acme.invalid") { + return nil, false + } c.lock.RLock() defer c.lock.RUnlock() account := c.store.Get().(*Account) if account.ChallengeCerts == nil { return nil, false } - if certBinary, ok := account.ChallengeCerts[domain]; ok { - cert := &tls.Certificate{} - var buffer bytes.Buffer - buffer.Write(certBinary) - dec := gob.NewDecoder(&buffer) - err := dec.Decode(cert) - if err != nil { - log.Errorf("Error unmarshaling challenge cert %s", err.Error()) - return nil, false + 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 cert, true + return fmt.Errorf("Cannot find challenge cert for domain %s", domain) } - return nil, false + 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 *challengeProvider) Present(domain, token, keyAuth string) error { log.Debugf("Challenge Present %s", domain) - cert, _, err := acme.TLSSNI01ChallengeCert(keyAuth) - if err != nil { - return err - } - cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) + cert, _, err := TLSSNI01ChallengeCert(keyAuth) if err != nil { return err } @@ -74,18 +78,9 @@ func (c *challengeProvider) Present(domain, token, keyAuth string) error { } account := object.(*Account) if account.ChallengeCerts == nil { - account.ChallengeCerts = map[string][]byte{} - } - for i := range cert.Leaf.DNSNames { - var buffer bytes.Buffer - enc := gob.NewEncoder(&buffer) - err := enc.Encode(cert) - if err != nil { - return err - } - account.ChallengeCerts[cert.Leaf.DNSNames[i]] = buffer.Bytes() - log.Debugf("Challenge Present cert: %s", cert.Leaf.DNSNames[i]) + account.ChallengeCerts = map[string]*ChallengeCert{} } + account.ChallengeCerts[domain] = &cert return transaction.Commit(account) } diff --git a/acme/crypto.go b/acme/crypto.go index 6fa544b70..c90f3d126 100644 --- a/acme/crypto.go +++ b/acme/crypto.go @@ -1,6 +1,8 @@ package acme import ( + "crypto" + "crypto/ecdsa" "crypto/rand" "crypto/rsa" "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) } + +// 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) +} diff --git a/acme/localStore.go b/acme/localStore.go index c39322091..86d62867c 100644 --- a/acme/localStore.go +++ b/acme/localStore.go @@ -51,17 +51,6 @@ func (s *LocalStore) Load() (cluster.Object, error) { return account, nil } -// func (s *LocalStore) saveAccount(account *Account) error { -// s.storageLock.Lock() -// defer s.storageLock.Unlock() -// // write account to file -// data, err := json.MarshalIndent(account, "", " ") -// if err != nil { -// return err -// } -// return ioutil.WriteFile(s.file, data, 0644) -// } - // Begin creates a transaction with the KV store. func (s *LocalStore) Begin() (cluster.Transaction, cluster.Object, error) { s.storageLock.Lock() @@ -88,7 +77,7 @@ func (t *localTransaction) Commit(object cluster.Object) error { if err != nil { return err } - return ioutil.WriteFile(t.file, data, 0644) + err = ioutil.WriteFile(t.file, data, 0644) if err != nil { return err } diff --git a/cluster/datastore.go b/cluster/datastore.go index f2bcf9c61..a0c9599ad 100644 --- a/cluster/datastore.go +++ b/cluster/datastore.go @@ -3,10 +3,11 @@ 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/emilevauge/backoff" "github.com/satori/go.uuid" "golang.org/x/net/context" "sync" @@ -84,19 +85,11 @@ func (d *Datastore) watchChanges() error { cancel() return err } - d.localLock.Lock() - err := d.kv.LoadConfig(d.meta) + err = d.reload() if err != nil { - d.localLock.Unlock() return err } - err = d.meta.unmarshall() - if err != nil { - d.localLock.Unlock() - return err - } - d.localLock.Unlock() - // log.Debugf("Datastore object change received: %+v", d.object) + // log.Debugf("Datastore object change received: %+v", d.meta) if d.listener != nil { err := d.listener(d.meta.object) if err != nil { @@ -109,7 +102,7 @@ func (d *Datastore) watchChanges() error { notify := func(err error, time time.Duration) { log.Errorf("Error in watch datastore: %+v, retrying in %s", err, time) } - err := backoff.RetryNotify(operation, backoff.NewExponentialBackOff(), notify) + err := backoff.RetryNotify(operation, job.NewBackOff(backoff.NewExponentialBackOff()), notify) if err != nil { log.Errorf("Error in watch datastore: %v", err) } @@ -117,6 +110,23 @@ func (d *Datastore) watchChanges() error { 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() @@ -152,6 +162,10 @@ func (d *Datastore) Begin() (Transaction, Object, error) { } 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 @@ -228,7 +242,6 @@ func (s *datastoreTransaction) Commit(object Object) error { } s.dirty = true - // log.Debugf("Datastore object saved: %+v", s.object) log.Debugf("Transaction commited %s", s.id) return nil } diff --git a/cluster/leadership.go b/cluster/leadership.go index 2143ee6d9..bcdff6a1d 100644 --- a/cluster/leadership.go +++ b/cluster/leadership.go @@ -1,11 +1,11 @@ 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" - "github.com/emilevauge/backoff" "golang.org/x/net/context" "time" ) diff --git a/glide.lock b/glide.lock index 26c72cbbc..31570349c 100644 --- a/glide.lock +++ b/glide.lock @@ -1,3 +1,4 @@ +<<<<<<< 9fb29a2d5ae0ade0aa8cb65df5c726a944e4a829 <<<<<<< 2fbcca003e6454c848801c859d8563da94ea8aaf <<<<<<< a13549cc28273ba5c15a739fa4aaeb3e0f7216a4 hash: c0ac205a859d78847e21d3cd63f427ffba985755c6ae84373e4a20364ba39b05 @@ -16,7 +17,14 @@ updated: 2016-09-20T14:50:04.029710103+02:00 hash: 49c7bd0e32b2764248183bda52f168fe22d69e2db5e17c1dbeebbe71be9929b1 updated: 2016-08-11T14:33:42.826534934+02:00 >>>>>>> Add ACME store +<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 >>>>>>> Add ACME store +======= +======= +hash: af34f34bc4f9f3cc6c988caa4cc273a7f32f91b24c77bdf5cadd9bcb48883a53 +updated: 2016-09-28T11:40:41.311876377+02:00 +>>>>>>> Challenge certs PEM encoding +>>>>>>> Challenge certs PEM encoding imports: - name: github.com/abbot/go-http-auth version: cb4372376e1e00e9f6ab9ec142e029302c9e7140 @@ -41,7 +49,7 @@ imports: - name: github.com/containous/mux version: a819b77bba13f0c0cbe36e437bc2e948411b3996 - name: github.com/containous/staert - version: 56058c7d4152831a641764d10ec91132adf061ea + version: 92329254783dc01174f03302d51d7cf2c9ff84cf - name: github.com/coreos/etcd version: 1c9e0a0e33051fed6c05c141e6fcbfe5c7f2a899 subpackages: @@ -53,15 +61,25 @@ imports: subpackages: - spew - name: github.com/docker/distribution +<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 <<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab version: 87917f30529e6a7fca8eaff2932424915fb11225 ======= +======= +<<<<<<< 9fb29a2d5ae0ade0aa8cb65df5c726a944e4a829 +>>>>>>> Challenge certs PEM encoding <<<<<<< a13549cc28273ba5c15a739fa4aaeb3e0f7216a4 version: 99cb7c0946d2f5a38015443e515dc916295064d7 ======= version: 857d0f15c0a4d8037175642e0ca3660829551cb5 >>>>>>> Add KV datastore +<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 >>>>>>> Add KV datastore +======= +======= + version: 87917f30529e6a7fca8eaff2932424915fb11225 +>>>>>>> Challenge certs PEM encoding +>>>>>>> Challenge certs PEM encoding subpackages: - context - digest @@ -147,10 +165,14 @@ imports: - sockets - tlsconfig - name: github.com/docker/go-units +<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 <<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab version: f2d77a61e3c169b43402a0a1e84f06daf29b8190 ======= version: f2145db703495b2e525c59662db69a7344b00bb8 +======= + version: f2d77a61e3c169b43402a0a1e84f06daf29b8190 +>>>>>>> Challenge certs PEM encoding - name: github.com/docker/leadership version: bfc7753dd48af19513b29deec23c364bf0f274eb >>>>>>> Add KV datastore @@ -172,11 +194,15 @@ imports: - version - yaml - name: github.com/docker/libkv +<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 <<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab version: 35d3e2084c650109e7bcc7282655b1bc8ba924ff ======= version: aabc039ad04deb721e234f99cd1b4aa28ac71a40 >>>>>>> Add KV datastore +======= + version: 35d3e2084c650109e7bcc7282655b1bc8ba924ff +>>>>>>> Challenge certs PEM encoding subpackages: - store - store/boltdb @@ -192,15 +218,25 @@ imports: - name: github.com/go-check/check version: 4f90aeace3a26ad7021961c297b22c42160c7b25 - name: github.com/gogo/protobuf +<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 <<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab version: e33835a643a970c11ac74f6333f5f6866387a101 ======= +======= +<<<<<<< 9fb29a2d5ae0ade0aa8cb65df5c726a944e4a829 +>>>>>>> Challenge certs PEM encoding <<<<<<< a13549cc28273ba5c15a739fa4aaeb3e0f7216a4 version: 89f1976ff373a3e549675d2f212c10f98b6c6316 ======= version: e57a569e1882958f6b188cb42231d6db87701f2a >>>>>>> Add KV datastore +<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 >>>>>>> Add KV datastore +======= +======= + version: e33835a643a970c11ac74f6333f5f6866387a101 +>>>>>>> Challenge certs PEM encoding +>>>>>>> Challenge certs PEM encoding subpackages: - proto - name: github.com/golang/glog @@ -212,13 +248,29 @@ imports: - name: github.com/gorilla/context version: aed02d124ae4a0e94fea4541c8effd05bf0c8296 - name: github.com/hashicorp/consul +<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 version: fce7d75609a04eeb9d4bf41c8dc592aac18fc97d +======= +<<<<<<< 9fb29a2d5ae0ade0aa8cb65df5c726a944e4a829 + version: d5b7530ec593f1ec2a8f8a7c145bcadafa88b572 +======= + version: fce7d75609a04eeb9d4bf41c8dc592aac18fc97d +>>>>>>> Challenge certs PEM encoding +>>>>>>> Challenge certs PEM encoding subpackages: - api - name: github.com/hashicorp/go-cleanhttp version: 875fb671b3ddc66f8e2f0acc33829c8cb989a38d - name: github.com/hashicorp/serf +<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 version: 6c4672d66fc6312ddde18399262943e21175d831 +======= +<<<<<<< 9fb29a2d5ae0ade0aa8cb65df5c726a944e4a829 + version: b7a120a5fc494f6dd5e858f42fd0fd4022d6320f +======= + version: 6c4672d66fc6312ddde18399262943e21175d831 +>>>>>>> Challenge certs PEM encoding +>>>>>>> Challenge certs PEM encoding subpackages: - coordinate - serf @@ -263,11 +315,15 @@ imports: - name: github.com/miekg/dns version: 5d001d020961ae1c184f9f8152fdc73810481677 - name: github.com/mitchellh/mapstructure +<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 <<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab version: d2dd0262208475919e1a362f675cfc0e7c10e905 ======= version: 21a35fb16463dfb7c8eee579c65d995d95e64d1e >>>>>>> Add KV datastore +======= + version: d2dd0262208475919e1a362f675cfc0e7c10e905 +>>>>>>> Challenge certs PEM encoding - name: github.com/moul/http2curl version: b1479103caacaa39319f75e7f57fc545287fca0d - name: github.com/NYTimes/gziphandler @@ -275,15 +331,25 @@ imports: - name: github.com/ogier/pflag version: 45c278ab3607870051a2ea9040bb85fcb8557481 - name: github.com/opencontainers/runc +<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 <<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab version: 1a81e9ab1f138c091fe5c86d0883f87716088527 ======= +======= +<<<<<<< 9fb29a2d5ae0ade0aa8cb65df5c726a944e4a829 +>>>>>>> Challenge certs PEM encoding <<<<<<< a13549cc28273ba5c15a739fa4aaeb3e0f7216a4 version: d9fec4c63b089ddfc267194ecb6cda58a13f072c ======= version: ff88baa42fa5b2a1568a3a14665142fb4bdb3a2a >>>>>>> Add KV datastore +<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 >>>>>>> Add KV datastore +======= +======= + version: 1a81e9ab1f138c091fe5c86d0883f87716088527 +>>>>>>> Challenge certs PEM encoding +>>>>>>> Challenge certs PEM encoding subpackages: - libcontainer/user - name: github.com/parnurzeal/gorequest @@ -342,7 +408,15 @@ imports: - name: github.com/vulcand/route version: cb89d787ddbb1c5849a7ac9f79004c1fd12a4a32 - name: github.com/vulcand/vulcand +<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 version: 28a4e5c0892167589737b95ceecbcef00295be50 +======= +<<<<<<< 9fb29a2d5ae0ade0aa8cb65df5c726a944e4a829 + version: 643ca8acff8386e3b276f6feb8ba9b5893dbc4a2 +======= + version: 28a4e5c0892167589737b95ceecbcef00295be50 +>>>>>>> Challenge certs PEM encoding +>>>>>>> Challenge certs PEM encoding subpackages: - conntracker - plugin @@ -372,11 +446,27 @@ imports: - name: gopkg.in/fsnotify.v1 version: a8a77c9133d2d6fd8334f3260d06f60e8d80a5fb - name: gopkg.in/mgo.v2 +<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 version: 29cc868a5ca65f401ff318143f9408d02f4799cc subpackages: - bson - name: gopkg.in/square/go-jose.v1 version: e3f973b66b91445ec816dd7411ad1b6495a5a2fc +======= +<<<<<<< 9fb29a2d5ae0ade0aa8cb65df5c726a944e4a829 + version: 22287bab4379e1fbf6002fb4eb769888f3fb224c + subpackages: + - bson +- name: gopkg.in/square/go-jose.v1 + version: aa2e30fdd1fe9dd3394119af66451ae790d50e0d +======= + version: 29cc868a5ca65f401ff318143f9408d02f4799cc + subpackages: + - bson +- name: gopkg.in/square/go-jose.v1 + version: e3f973b66b91445ec816dd7411ad1b6495a5a2fc +>>>>>>> Challenge certs PEM encoding +>>>>>>> Challenge certs PEM encoding subpackages: - cipher - json @@ -394,7 +484,15 @@ testImports: - name: github.com/libkermit/docker-check version: cbe0ef03b3d23070eac4d00ba8828f2cc7f7e5a3 - name: github.com/spf13/pflag +<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 version: 5644820622454e71517561946e3d94b9f9db6842 +======= +<<<<<<< 9fb29a2d5ae0ade0aa8cb65df5c726a944e4a829 + version: 08b1a584251b5b62f458943640fc8ebd4d50aaa5 +======= + version: 5644820622454e71517561946e3d94b9f9db6842 +>>>>>>> Challenge certs PEM encoding +>>>>>>> Challenge certs PEM encoding - name: github.com/vbatts/tar-split version: 6810cedb21b2c3d0b9bb8f9af12ff2dc7a2f14df subpackages: diff --git a/glide.yaml b/glide.yaml index 15b1b8be0..6230825c3 100644 --- a/glide.yaml +++ b/glide.yaml @@ -21,7 +21,7 @@ import: - stream - utils - package: github.com/containous/staert - version: 56058c7d4152831a641764d10ec91132adf061ea + version: 92329254783dc01174f03302d51d7cf2c9ff84cf - package: github.com/docker/engine-api version: 62043eb79d581a32ea849645277023c550732e52 subpackages: diff --git a/integration/consul_test.go b/integration/consul_test.go index ce60c38f9..d9b7b8218 100644 --- a/integration/consul_test.go +++ b/integration/consul_test.go @@ -446,12 +446,12 @@ func (s *ConsulSuite) TestDatastore(c *check.C) { c.Assert(err, checker.IsNil) ctx := context.Background() - datastore1, err := cluster.NewDataStore(kvSource, ctx, &TestStruct{}) + datastore1, err := cluster.NewDataStore(*kvSource, ctx, &TestStruct{}, nil) c.Assert(err, checker.IsNil) - datastore2, err := cluster.NewDataStore(kvSource, ctx, &TestStruct{}) + datastore2, err := cluster.NewDataStore(*kvSource, ctx, &TestStruct{}, nil) c.Assert(err, checker.IsNil) - setter1, err := datastore1.Begin() + setter1, _, err := datastore1.Begin() c.Assert(err, checker.IsNil) err = setter1.Commit(&TestStruct{ String: "foo", @@ -465,7 +465,7 @@ func (s *ConsulSuite) TestDatastore(c *check.C) { test2 := datastore2.Get().(*TestStruct) c.Assert(test2.String, checker.Equals, "foo") - setter2, err := datastore2.Begin() + setter2, _, err := datastore2.Begin() c.Assert(err, checker.IsNil) err = setter2.Commit(&TestStruct{ String: "bar", @@ -483,7 +483,7 @@ func (s *ConsulSuite) TestDatastore(c *check.C) { wg.Add(4) go func() { for i := 0; i < 100; i++ { - setter1, err := datastore1.Begin() + setter1, _, err := datastore1.Begin() c.Assert(err, checker.IsNil) err = setter1.Commit(&TestStruct{ String: "datastore1", @@ -495,7 +495,7 @@ func (s *ConsulSuite) TestDatastore(c *check.C) { }() go func() { for i := 0; i < 100; i++ { - setter2, err := datastore2.Begin() + setter2, _, err := datastore2.Begin() c.Assert(err, checker.IsNil) err = setter2.Commit(&TestStruct{ String: "datastore2", diff --git a/log/logger.go b/log/logger.go new file mode 100644 index 000000000..9ef60fc27 --- /dev/null +++ b/log/logger.go @@ -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...) +} diff --git a/middlewares/authenticator.go b/middlewares/authenticator.go index 33fb77ce9..380287e3f 100644 --- a/middlewares/authenticator.go +++ b/middlewares/authenticator.go @@ -2,9 +2,9 @@ package middlewares import ( "fmt" - "github.com/containous/traefik/log" "github.com/abbot/go-http-auth" "github.com/codegangsta/negroni" + "github.com/containous/traefik/log" "github.com/containous/traefik/types" "net/http" "strings" diff --git a/provider/consul_catalog.go b/provider/consul_catalog.go index 947368a68..aad2e8ae5 100644 --- a/provider/consul_catalog.go +++ b/provider/consul_catalog.go @@ -10,9 +10,9 @@ import ( "github.com/BurntSushi/ty/fun" "github.com/Sirupsen/logrus" - "github.com/containous/traefik/log" "github.com/cenk/backoff" "github.com/containous/traefik/job" + "github.com/containous/traefik/log" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" "github.com/hashicorp/consul/api" diff --git a/provider/docker.go b/provider/docker.go index 175cd3ebb..411f18696 100644 --- a/provider/docker.go +++ b/provider/docker.go @@ -13,9 +13,9 @@ import ( "golang.org/x/net/context" "github.com/BurntSushi/ty/fun" - "github.com/containous/traefik/log" "github.com/cenk/backoff" "github.com/containous/traefik/job" + "github.com/containous/traefik/log" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" "github.com/containous/traefik/version" diff --git a/provider/kubernetes.go b/provider/kubernetes.go index 22b9fd18e..3ca6cd681 100644 --- a/provider/kubernetes.go +++ b/provider/kubernetes.go @@ -16,9 +16,6 @@ import ( "github.com/cenk/backoff" "github.com/containous/traefik/job" - "github.com/containous/traefik/provider/k8s" - "github.com/containous/traefik/safe" - "github.com/containous/traefik/types" ) const ( diff --git a/provider/kv.go b/provider/kv.go index 27b5469b2..db8bac553 100644 --- a/provider/kv.go +++ b/provider/kv.go @@ -9,9 +9,9 @@ import ( "errors" "github.com/BurntSushi/ty/fun" - "github.com/containous/traefik/log" "github.com/cenk/backoff" "github.com/containous/traefik/job" + "github.com/containous/traefik/log" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" "github.com/docker/libkv" @@ -148,7 +148,7 @@ func (provider *Kv) list(keys ...string) []string { joinedKeys := strings.Join(keys, "") keysPairs, err := provider.kvclient.List(joinedKeys) if err != nil { - log.Warnf("Cannot get keys %s %s ", joinedKeys, err) + log.Debugf("Cannot get keys %s %s ", joinedKeys, err) return nil } directoryKeys := make(map[string]string) @@ -170,10 +170,10 @@ func (provider *Kv) get(defaultValue string, keys ...string) string { joinedKeys := strings.Join(keys, "") keyPair, err := provider.kvclient.Get(strings.TrimPrefix(joinedKeys, "/")) if err != nil { - log.Warnf("Cannot get key %s %s, setting default %s", joinedKeys, err, defaultValue) + log.Debugf("Cannot get key %s %s, setting default %s", joinedKeys, err, defaultValue) return defaultValue } else if keyPair == nil { - log.Warnf("Cannot get key %s, setting default %s", joinedKeys, defaultValue) + log.Debugf("Cannot get key %s, setting default %s", joinedKeys, defaultValue) return defaultValue } return string(keyPair.Value) @@ -183,10 +183,10 @@ func (provider *Kv) splitGet(keys ...string) []string { joinedKeys := strings.Join(keys, "") keyPair, err := provider.kvclient.Get(joinedKeys) if err != nil { - log.Warnf("Cannot get key %s %s, setting default empty", joinedKeys, err) + log.Debugf("Cannot get key %s %s, setting default empty", joinedKeys, err) return []string{} } else if keyPair == nil { - log.Warnf("Cannot get key %s, setting default %empty", joinedKeys) + log.Debugf("Cannot get key %s, setting default %empty", joinedKeys) return []string{} } return strings.Split(string(keyPair.Value), ",") diff --git a/provider/marathon.go b/provider/marathon.go index ddeefe923..39934ec3e 100644 --- a/provider/marathon.go +++ b/provider/marathon.go @@ -13,9 +13,9 @@ import ( "time" "github.com/BurntSushi/ty/fun" - "github.com/containous/traefik/log" "github.com/cenk/backoff" "github.com/containous/traefik/job" + "github.com/containous/traefik/log" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" "github.com/gambol99/go-marathon" diff --git a/provider/mesos.go b/provider/mesos.go index 0943a3db6..5d8378412 100644 --- a/provider/mesos.go +++ b/provider/mesos.go @@ -8,9 +8,9 @@ import ( "fmt" "github.com/BurntSushi/ty/fun" - "github.com/containous/traefik/log" "github.com/cenk/backoff" "github.com/containous/traefik/job" + "github.com/containous/traefik/log" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" "github.com/mesos/mesos-go/detector" diff --git a/provider/provider.go b/provider/provider.go index 72610c7af..b2e7d5d09 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -13,8 +13,8 @@ import ( "os" "github.com/BurntSushi/toml" - "github.com/containous/traefik/log" "github.com/containous/traefik/autogen" + "github.com/containous/traefik/log" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" ) diff --git a/rules.go b/rules.go index 6527c7c0e..c091e0fa6 100644 --- a/rules.go +++ b/rules.go @@ -149,7 +149,7 @@ func (r *Rules) parseRules(expression string, onRule func(functionName string, f err := onRule(functionName, parsedFunction, parsedArgs) if err != nil { - return fmt.Errorf("Parsing error on rule:", err) + return fmt.Errorf("Parsing error on rule: %v", err) } } return nil @@ -180,7 +180,7 @@ func (r *Rules) Parse(expression string) (*mux.Route, error) { return nil }) if err != nil { - return nil, fmt.Errorf("Error parsing rule:", err) + return nil, fmt.Errorf("Error parsing rule: %v", err) } return resultRoute, nil } @@ -195,7 +195,7 @@ func (r *Rules) ParseDomains(expression string) ([]string, error) { return nil }) if err != nil { - return nil, fmt.Errorf("Error parsing domains:", err) + return nil, fmt.Errorf("Error parsing domains: %v", err) } return domains, nil } diff --git a/server.go b/server.go index 5ace528aa..4846e1885 100644 --- a/server.go +++ b/server.go @@ -243,7 +243,7 @@ func (server *Server) defaultConfigurationValues(configuration *types.Configurat for backendName, backend := range configuration.Backends { _, err := types.NewLoadBalancerMethod(backend.LoadBalancer) 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"} } } diff --git a/traefik.go b/traefik.go index 397133819..cfd5d11e6 100644 --- a/traefik.go +++ b/traefik.go @@ -129,7 +129,7 @@ Complete documentation is available at https://traefik.io`, } if _, err := f.Parse(usedCmd); err != nil { - fmtlog.Println(err) + fmtlog.Printf("Error parsing command: %s\n", err) os.Exit(-1) } @@ -150,7 +150,7 @@ Complete documentation is available at https://traefik.io`, kv, err = CreateKvSource(traefikConfiguration) if err != nil { - fmtlog.Println(err) + fmtlog.Printf("Error creating kv store: %s\n", err) os.Exit(-1) } @@ -164,13 +164,13 @@ Complete documentation is available at https://traefik.io`, } s.AddSource(kv) if _, err := s.LoadConfig(); err != nil { - fmtlog.Println(err) + fmtlog.Printf("Error loading configuration: %s\n", err) os.Exit(-1) } } if err := s.Run(); err != nil { - fmtlog.Println(err) + fmtlog.Printf("Error running traefik: %s\n", err) os.Exit(-1) } diff --git a/web.go b/web.go index 974c2fbf8..97b31ff17 100644 --- a/web.go +++ b/web.go @@ -8,10 +8,10 @@ import ( "net/http" "runtime" - "github.com/containous/traefik/log" "github.com/codegangsta/negroni" "github.com/containous/mux" "github.com/containous/traefik/autogen" + "github.com/containous/traefik/log" "github.com/containous/traefik/middlewares" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" @@ -79,7 +79,7 @@ func (provider *WebProvider) Provide(configurationChan chan<- types.ConfigMessag body, _ := ioutil.ReadAll(request.Body) err := json.Unmarshal(body, configuration) if err == nil { - configurationChan <- types.ConfigMessage{"web", configuration} + configurationChan <- types.ConfigMessage{ProviderName: "web", Configuration: configuration} provider.getConfigHandler(response, request) } else { log.Errorf("Error parsing configuration %+v", err) From bb29d9c8cafa1d6f65ee7d3c34ff2d1ecb1b3b4d Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Thu, 29 Sep 2016 13:49:12 +0200 Subject: [PATCH 5/6] Add documentation Signed-off-by: Emile Vauge --- docs/toml.md | 4 ++-- docs/user-guide/cluster.md | 19 +++++++++++++++++++ docs/user-guide/kv-config.md | 16 +++++++++++++++- mkdocs.yml | 1 + traefik.sample.toml | 12 +++++++++--- 5 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 docs/user-guide/cluster.md diff --git a/docs/toml.md b/docs/toml.md index 95feaa327..6c72c88ea 100644 --- a/docs/toml.md +++ b/docs/toml.md @@ -247,7 +247,7 @@ Supported filters: # 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: # - create a file on your host and mount it as a volume # storageFile = "acme.json" @@ -258,7 +258,7 @@ email = "test@traefik.io" # # Required # -storageFile = "acme.json" +storage = "acme.json" # or "traefik/acme/account" if using KV store # Entrypoint to proxy acme challenge to. # WARNING, must point to an entrypoint on port 443 diff --git a/docs/user-guide/cluster.md b/docs/user-guide/cluster.md new file mode 100644 index 000000000..e39795f2f --- /dev/null +++ b/docs/user-guide/cluster.md @@ -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. + \ No newline at end of file diff --git a/docs/user-guide/kv-config.md b/docs/user-guide/kv-config.md index 368bb7a87..749f77093 100644 --- a/docs/user-guide/kv-config.md +++ b/docs/user-guide/kv-config.md @@ -302,6 +302,7 @@ Further, if the `/traefik/alias` key is set, all other configuration with `/trae # 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). ```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). 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). diff --git a/mkdocs.yml b/mkdocs.yml index be6298810..fbb1642d8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -51,3 +51,4 @@ pages: - 'Swarm cluster': 'user-guide/swarm.md' - 'Kubernetes': 'user-guide/kubernetes.md' - 'Key-value store configuration': 'user-guide/kv-config.md' + - 'Clustering/HA': 'user-guide/cluster.md' diff --git a/traefik.sample.toml b/traefik.sample.toml index 17b798780..989395a73 100644 --- a/traefik.sample.toml +++ b/traefik.sample.toml @@ -100,12 +100,18 @@ # # email = "test@traefik.io" -# File used for certificates storage. -# WARNING, if you use Traefik in Docker, don't forget to mount this file as a volume. +# File or key used for certificates storage. +# 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 # -# storageFile = "acme.json" +# storage = "acme.json" # or "traefik/acme/account" if using KV store # Entrypoint to proxy acme challenge to. # WARNING, must point to an entrypoint on port 443 From 4ad4b8e0b83923c31b702873cc043d941842b1ec Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Thu, 29 Sep 2016 15:36:52 +0200 Subject: [PATCH 6/6] Add ACME account to storeconfig command Signed-off-by: Emile Vauge --- .gitattributes | 1 + acme/acme.go | 30 ++++---- acme/challengeProvider.go | 6 -- acme/localStore.go | 3 +- cluster/datastore.go | 10 ++- glide.lock | 155 +------------------------------------- traefik.go | 33 +++++++- 7 files changed, 58 insertions(+), 180 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..e8a4935c2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +glide.lock binary \ No newline at end of file diff --git a/acme/acme.go b/acme/acme.go index 045ff5572..32bebf37a 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -104,31 +104,31 @@ func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tl a.checkOnDemandDomain = checkOnDemandDomain tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate) tlsConfig.GetCertificate = a.getCertificate + listener := func(object cluster.Object) error { + account := object.(*Account) + account.Init() + if !leadership.IsLeader() { + a.client, err = a.buildACMEClient(account) + if err != nil { + log.Errorf("Error building ACME client %+v: %s", object, err.Error()) + } + } + return nil + } datastore, err := cluster.NewDataStore( staert.KvSource{ Store: leadership.Store, - Prefix: leadership.Store.Prefix + "/acme/account", + Prefix: a.Storage, }, leadership.Pool.Ctx(), &Account{}, - func(object cluster.Object) error { - account := object.(*Account) - account.Init() - if !leadership.IsLeader() { - a.client, err = a.buildACMEClient(account) - if err != nil { - log.Errorf("Error building ACME client %+v: %s", object, err.Error()) - } - } - - return nil - }) + listener) if err != nil { return err } a.store = datastore - a.challengeProvider = newMemoryChallengeProvider(a.store) + a.challengeProvider = &challengeProvider{store: a.store} ticker := time.NewTicker(24 * time.Hour) leadership.Pool.AddGoCtx(func(ctx context.Context) { @@ -227,7 +227,7 @@ func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, checkOnDemandDomain func localStore := NewLocalStore(a.Storage) a.store = localStore - a.challengeProvider = newMemoryChallengeProvider(a.store) + a.challengeProvider = &challengeProvider{store: a.store} var needRegister bool var account *Account diff --git a/acme/challengeProvider.go b/acme/challengeProvider.go index e66469258..2d5a8daae 100644 --- a/acme/challengeProvider.go +++ b/acme/challengeProvider.go @@ -20,12 +20,6 @@ type challengeProvider struct { lock sync.RWMutex } -func newMemoryChallengeProvider(store cluster.Store) *challengeProvider { - return &challengeProvider{ - store: store, - } -} - func (c *challengeProvider) getCertificate(domain string) (cert *tls.Certificate, exists bool) { log.Debugf("Challenge GetCertificate %s", domain) if !strings.HasSuffix(domain, ".acme.invalid") { diff --git a/acme/localStore.go b/acme/localStore.go index 86d62867c..f159a5acd 100644 --- a/acme/localStore.go +++ b/acme/localStore.go @@ -21,8 +21,7 @@ type LocalStore struct { // NewLocalStore create a LocalStore func NewLocalStore(file string) *LocalStore { return &LocalStore{ - file: file, - storageLock: sync.RWMutex{}, + file: file, } } diff --git a/cluster/datastore.go b/cluster/datastore.go index a0c9599ad..ff81215d9 100644 --- a/cluster/datastore.go +++ b/cluster/datastore.go @@ -21,7 +21,13 @@ type Metadata struct { Lock string } -func (m *Metadata) marshall() error { +// 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 @@ -227,7 +233,7 @@ func (s *datastoreTransaction) Commit(object Object) error { return fmt.Errorf("Transaction already used. Please begin a new one.") } s.Datastore.meta.object = object - err := s.Datastore.meta.marshall() + err := s.Datastore.meta.Marshall() if err != nil { return err } diff --git a/glide.lock b/glide.lock index 31570349c..0ec63fe12 100644 --- a/glide.lock +++ b/glide.lock @@ -1,30 +1,5 @@ -<<<<<<< 9fb29a2d5ae0ade0aa8cb65df5c726a944e4a829 -<<<<<<< 2fbcca003e6454c848801c859d8563da94ea8aaf -<<<<<<< a13549cc28273ba5c15a739fa4aaeb3e0f7216a4 -hash: c0ac205a859d78847e21d3cd63f427ffba985755c6ae84373e4a20364ba39b05 -<<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab -updated: 2016-09-30T10:57:42.336729457+02:00 -======= -updated: 2016-09-28T16:50:04.352639437+01:00 -======= -hash: 809b3fa812ca88940fdc15530804a4bcd881708e4819fed5aa45c42c871ba5cf -updated: 2016-09-20T14:50:04.029710103+02:00 ->>>>>>> Add KV datastore -<<<<<<< bea5ad3f132bae27b6c1a83adf00154058b484b5 ->>>>>>> Add KV datastore -======= -======= -hash: 49c7bd0e32b2764248183bda52f168fe22d69e2db5e17c1dbeebbe71be9929b1 -updated: 2016-08-11T14:33:42.826534934+02:00 ->>>>>>> Add ACME store -<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 ->>>>>>> Add ACME store -======= -======= -hash: af34f34bc4f9f3cc6c988caa4cc273a7f32f91b24c77bdf5cadd9bcb48883a53 -updated: 2016-09-28T11:40:41.311876377+02:00 ->>>>>>> Challenge certs PEM encoding ->>>>>>> Challenge certs PEM encoding +hash: 39ff28cc1d13d5915a870b14491ece1849c4eaf5a56cecd50a7676ecee6c6143 +updated: 2016-09-30T11:27:29.529525636+02:00 imports: - name: github.com/abbot/go-http-auth version: cb4372376e1e00e9f6ab9ec142e029302c9e7140 @@ -61,25 +36,7 @@ imports: subpackages: - spew - name: github.com/docker/distribution -<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 -<<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab version: 87917f30529e6a7fca8eaff2932424915fb11225 -======= -======= -<<<<<<< 9fb29a2d5ae0ade0aa8cb65df5c726a944e4a829 ->>>>>>> Challenge certs PEM encoding -<<<<<<< a13549cc28273ba5c15a739fa4aaeb3e0f7216a4 - version: 99cb7c0946d2f5a38015443e515dc916295064d7 -======= - version: 857d0f15c0a4d8037175642e0ca3660829551cb5 ->>>>>>> Add KV datastore -<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 ->>>>>>> Add KV datastore -======= -======= - version: 87917f30529e6a7fca8eaff2932424915fb11225 ->>>>>>> Challenge certs PEM encoding ->>>>>>> Challenge certs PEM encoding subpackages: - context - digest @@ -165,17 +122,9 @@ imports: - sockets - tlsconfig - name: github.com/docker/go-units -<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 -<<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab version: f2d77a61e3c169b43402a0a1e84f06daf29b8190 -======= - version: f2145db703495b2e525c59662db69a7344b00bb8 -======= - version: f2d77a61e3c169b43402a0a1e84f06daf29b8190 ->>>>>>> Challenge certs PEM encoding - name: github.com/docker/leadership version: bfc7753dd48af19513b29deec23c364bf0f274eb ->>>>>>> Add KV datastore - name: github.com/docker/libcompose version: d1876c1d68527a49c0aac22a0b161acc7296b740 subpackages: @@ -194,15 +143,7 @@ imports: - version - yaml - name: github.com/docker/libkv -<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 -<<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab version: 35d3e2084c650109e7bcc7282655b1bc8ba924ff -======= - version: aabc039ad04deb721e234f99cd1b4aa28ac71a40 ->>>>>>> Add KV datastore -======= - version: 35d3e2084c650109e7bcc7282655b1bc8ba924ff ->>>>>>> Challenge certs PEM encoding subpackages: - store - store/boltdb @@ -218,25 +159,7 @@ imports: - name: github.com/go-check/check version: 4f90aeace3a26ad7021961c297b22c42160c7b25 - name: github.com/gogo/protobuf -<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 -<<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab version: e33835a643a970c11ac74f6333f5f6866387a101 -======= -======= -<<<<<<< 9fb29a2d5ae0ade0aa8cb65df5c726a944e4a829 ->>>>>>> Challenge certs PEM encoding -<<<<<<< a13549cc28273ba5c15a739fa4aaeb3e0f7216a4 - version: 89f1976ff373a3e549675d2f212c10f98b6c6316 -======= - version: e57a569e1882958f6b188cb42231d6db87701f2a ->>>>>>> Add KV datastore -<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 ->>>>>>> Add KV datastore -======= -======= - version: e33835a643a970c11ac74f6333f5f6866387a101 ->>>>>>> Challenge certs PEM encoding ->>>>>>> Challenge certs PEM encoding subpackages: - proto - name: github.com/golang/glog @@ -248,29 +171,13 @@ imports: - name: github.com/gorilla/context version: aed02d124ae4a0e94fea4541c8effd05bf0c8296 - name: github.com/hashicorp/consul -<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 version: fce7d75609a04eeb9d4bf41c8dc592aac18fc97d -======= -<<<<<<< 9fb29a2d5ae0ade0aa8cb65df5c726a944e4a829 - version: d5b7530ec593f1ec2a8f8a7c145bcadafa88b572 -======= - version: fce7d75609a04eeb9d4bf41c8dc592aac18fc97d ->>>>>>> Challenge certs PEM encoding ->>>>>>> Challenge certs PEM encoding subpackages: - api - name: github.com/hashicorp/go-cleanhttp version: 875fb671b3ddc66f8e2f0acc33829c8cb989a38d - name: github.com/hashicorp/serf -<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 version: 6c4672d66fc6312ddde18399262943e21175d831 -======= -<<<<<<< 9fb29a2d5ae0ade0aa8cb65df5c726a944e4a829 - version: b7a120a5fc494f6dd5e858f42fd0fd4022d6320f -======= - version: 6c4672d66fc6312ddde18399262943e21175d831 ->>>>>>> Challenge certs PEM encoding ->>>>>>> Challenge certs PEM encoding subpackages: - coordinate - serf @@ -315,15 +222,7 @@ imports: - name: github.com/miekg/dns version: 5d001d020961ae1c184f9f8152fdc73810481677 - name: github.com/mitchellh/mapstructure -<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 -<<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab version: d2dd0262208475919e1a362f675cfc0e7c10e905 -======= - version: 21a35fb16463dfb7c8eee579c65d995d95e64d1e ->>>>>>> Add KV datastore -======= - version: d2dd0262208475919e1a362f675cfc0e7c10e905 ->>>>>>> Challenge certs PEM encoding - name: github.com/moul/http2curl version: b1479103caacaa39319f75e7f57fc545287fca0d - name: github.com/NYTimes/gziphandler @@ -331,25 +230,7 @@ imports: - name: github.com/ogier/pflag version: 45c278ab3607870051a2ea9040bb85fcb8557481 - name: github.com/opencontainers/runc -<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 -<<<<<<< 38b62d4ae311e2d5247065cbc2c09421a2bb81ab version: 1a81e9ab1f138c091fe5c86d0883f87716088527 -======= -======= -<<<<<<< 9fb29a2d5ae0ade0aa8cb65df5c726a944e4a829 ->>>>>>> Challenge certs PEM encoding -<<<<<<< a13549cc28273ba5c15a739fa4aaeb3e0f7216a4 - version: d9fec4c63b089ddfc267194ecb6cda58a13f072c -======= - version: ff88baa42fa5b2a1568a3a14665142fb4bdb3a2a ->>>>>>> Add KV datastore -<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 ->>>>>>> Add KV datastore -======= -======= - version: 1a81e9ab1f138c091fe5c86d0883f87716088527 ->>>>>>> Challenge certs PEM encoding ->>>>>>> Challenge certs PEM encoding subpackages: - libcontainer/user - name: github.com/parnurzeal/gorequest @@ -408,15 +289,7 @@ imports: - name: github.com/vulcand/route version: cb89d787ddbb1c5849a7ac9f79004c1fd12a4a32 - name: github.com/vulcand/vulcand -<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 version: 28a4e5c0892167589737b95ceecbcef00295be50 -======= -<<<<<<< 9fb29a2d5ae0ade0aa8cb65df5c726a944e4a829 - version: 643ca8acff8386e3b276f6feb8ba9b5893dbc4a2 -======= - version: 28a4e5c0892167589737b95ceecbcef00295be50 ->>>>>>> Challenge certs PEM encoding ->>>>>>> Challenge certs PEM encoding subpackages: - conntracker - plugin @@ -446,27 +319,11 @@ imports: - name: gopkg.in/fsnotify.v1 version: a8a77c9133d2d6fd8334f3260d06f60e8d80a5fb - name: gopkg.in/mgo.v2 -<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 version: 29cc868a5ca65f401ff318143f9408d02f4799cc subpackages: - bson - name: gopkg.in/square/go-jose.v1 version: e3f973b66b91445ec816dd7411ad1b6495a5a2fc -======= -<<<<<<< 9fb29a2d5ae0ade0aa8cb65df5c726a944e4a829 - version: 22287bab4379e1fbf6002fb4eb769888f3fb224c - subpackages: - - bson -- name: gopkg.in/square/go-jose.v1 - version: aa2e30fdd1fe9dd3394119af66451ae790d50e0d -======= - version: 29cc868a5ca65f401ff318143f9408d02f4799cc - subpackages: - - bson -- name: gopkg.in/square/go-jose.v1 - version: e3f973b66b91445ec816dd7411ad1b6495a5a2fc ->>>>>>> Challenge certs PEM encoding ->>>>>>> Challenge certs PEM encoding subpackages: - cipher - json @@ -484,15 +341,7 @@ testImports: - name: github.com/libkermit/docker-check version: cbe0ef03b3d23070eac4d00ba8828f2cc7f7e5a3 - name: github.com/spf13/pflag -<<<<<<< a42845502e9b6e3b9985c56ad99d28c1357287b2 version: 5644820622454e71517561946e3d94b9f9db6842 -======= -<<<<<<< 9fb29a2d5ae0ade0aa8cb65df5c726a944e4a829 - version: 08b1a584251b5b62f458943640fc8ebd4d50aaa5 -======= - version: 5644820622454e71517561946e3d94b9f9db6842 ->>>>>>> Challenge certs PEM encoding ->>>>>>> Challenge certs PEM encoding - name: github.com/vbatts/tar-split version: 6810cedb21b2c3d0b9bb8f9af12ff2dc7a2f14df subpackages: diff --git a/traefik.go b/traefik.go index cfd5d11e6..5cdef2497 100644 --- a/traefik.go +++ b/traefik.go @@ -16,6 +16,7 @@ import ( "github.com/containous/flaeg" "github.com/containous/staert" "github.com/containous/traefik/acme" + "github.com/containous/traefik/cluster" "github.com/containous/traefik/log" "github.com/containous/traefik/middlewares" "github.com/containous/traefik/provider" @@ -100,9 +101,37 @@ Complete documentation is available at https://traefik.io`, if kv == nil { 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) - 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{ "parseAllSources": "true",