From 9cb07d026faad243d91494398e8bf2b1fe60ead2 Mon Sep 17 00:00:00 2001 From: Martin Baillie Date: Mon, 8 May 2017 11:20:38 +1000 Subject: [PATCH] Refactor into dual Rancher API/Metadata providers Introduces Rancher's metadata service as an optional provider source for Traefik, enabled by setting `rancher.MetadataService`. The provider uses a long polling technique to watch the metadata service and obtain near instantaneous updates. Alternatively it can be configured to poll the metadata service every `rancher.RefreshSeconds` by setting `rancher.MetadataPoll`. The refactor splits API and metadata service code into separate source files respectively, and specific configuration is deferred to sub-structs. Incorporates bugfix #1414 --- cmd/traefik/traefik.go | 23 ++ glide.lock | 6 +- glide.yaml | 2 + provider/rancher/api.go | 234 ++++++++++++++++ provider/rancher/metadata.go | 136 +++++++++ provider/rancher/rancher.go | 242 ++-------------- provider/rancher/rancher_test.go | 65 ++--- server/configuration.go | 4 - traefik.sample.toml | 41 ++- .../rancher/go-rancher-metadata/main.go | 31 +++ .../go-rancher-metadata/metadata/change.go | 64 +++++ .../go-rancher-metadata/metadata/metadata.go | 262 ++++++++++++++++++ .../go-rancher-metadata/metadata/types.go | 149 ++++++++++ .../go-rancher-metadata/metadata/utils.go | 19 ++ 14 files changed, 1006 insertions(+), 272 deletions(-) create mode 100644 provider/rancher/api.go create mode 100644 provider/rancher/metadata.go create mode 100644 vendor/github.com/rancher/go-rancher-metadata/main.go create mode 100644 vendor/github.com/rancher/go-rancher-metadata/metadata/change.go create mode 100644 vendor/github.com/rancher/go-rancher-metadata/metadata/metadata.go create mode 100644 vendor/github.com/rancher/go-rancher-metadata/metadata/types.go create mode 100644 vendor/github.com/rancher/go-rancher-metadata/metadata/utils.go diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index b8564dff8..dceabc0d8 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -20,6 +20,7 @@ import ( "github.com/containous/traefik/cluster" "github.com/containous/traefik/log" "github.com/containous/traefik/provider/kubernetes" + "github.com/containous/traefik/provider/rancher" "github.com/containous/traefik/safe" "github.com/containous/traefik/server" "github.com/containous/traefik/types" @@ -193,6 +194,28 @@ func run(traefikConfiguration *server.TraefikConfiguration) { globalConfiguration.DefaultEntryPoints = []string{"http"} } + if globalConfiguration.Rancher != nil { + // Ensure backwards compatibility for now + if len(globalConfiguration.Rancher.AccessKey) > 0 || + len(globalConfiguration.Rancher.Endpoint) > 0 || + len(globalConfiguration.Rancher.SecretKey) > 0 { + + if globalConfiguration.Rancher.API == nil { + globalConfiguration.Rancher.API = &rancher.APIConfiguration{ + AccessKey: globalConfiguration.Rancher.AccessKey, + SecretKey: globalConfiguration.Rancher.SecretKey, + Endpoint: globalConfiguration.Rancher.Endpoint, + } + } + log.Warn("Deprecated configuration found: rancher.[accesskey|secretkey|endpoint]. " + + "Please use rancher.api.[accesskey|secretkey|endpoint] instead.") + } + + if globalConfiguration.Rancher.Metadata != nil && len(globalConfiguration.Rancher.Metadata.Prefix) == 0 { + globalConfiguration.Rancher.Metadata.Prefix = "latest" + } + } + if globalConfiguration.Debug { globalConfiguration.LogLevel = "DEBUG" } diff --git a/glide.lock b/glide.lock index a56adf5b2..6ae926ca3 100644 --- a/glide.lock +++ b/glide.lock @@ -1,4 +1,4 @@ -hash: 34ceb7bd979d43efdbf721ccb9d983061c06db527148f90f1784db89f6d089f0 +hash: 088194c8357ca08e27476866b9007adfa7711500fe0c78650ecb397c4f70075a updated: 2017-05-19T23:30:19.890844996+02:00 imports: - name: cloud.google.com/go @@ -364,6 +364,10 @@ imports: version: 5b8f6cc26b355ba03d7611fce3844155b7baf05b subpackages: - client +- name: github.com/rancher/go-rancher-metadata + version: 95d4962a8f0420be24fb49c2cb4f5491284c62f1 + subpackages: + - metadata - name: github.com/ryanuber/go-glob version: 256dc444b735e061061cf46c809487313d5b0065 - name: github.com/samuel/go-zookeeper diff --git a/glide.yaml b/glide.yaml index dff9fe4d5..64fe24a5c 100644 --- a/glide.yaml +++ b/glide.yaml @@ -163,6 +163,8 @@ import: version: 5b8f6cc26b355ba03d7611fce3844155b7baf05b - package: golang.org/x/oauth2 version: 7fdf09982454086d5570c7db3e11f360194830ca +- package: github.com/rancher/go-rancher-metadata + version: 95d4962a8f0420be24fb49c2cb4f5491284c62f1 subpackages: - google - package: github.com/googleapis/gax-go diff --git a/provider/rancher/api.go b/provider/rancher/api.go new file mode 100644 index 000000000..b6fa7f418 --- /dev/null +++ b/provider/rancher/api.go @@ -0,0 +1,234 @@ +package rancher + +import ( + "context" + "os" + "time" + + "github.com/cenk/backoff" + "github.com/containous/traefik/job" + "github.com/containous/traefik/log" + "github.com/containous/traefik/safe" + "github.com/containous/traefik/types" + + rancher "github.com/rancher/go-rancher/client" +) + +var ( + withoutPagination *rancher.ListOpts +) + +// APIConfiguration contains configuration properties specific to the Rancher +// API provider. +type APIConfiguration struct { + Endpoint string `description:"Rancher server API HTTP(S) endpoint"` + AccessKey string `description:"Rancher server API access key"` + SecretKey string `description:"Rancher server API secret key"` +} + +func init() { + withoutPagination = &rancher.ListOpts{ + Filters: map[string]interface{}{"limit": 0}, + } +} + +func (p *Provider) createClient() (*rancher.RancherClient, error) { + rancherURL := getenv("CATTLE_URL", p.API.Endpoint) + accessKey := getenv("CATTLE_ACCESS_KEY", p.API.AccessKey) + secretKey := getenv("CATTLE_SECRET_KEY", p.API.SecretKey) + + return rancher.NewRancherClient(&rancher.ClientOpts{ + Url: rancherURL, + AccessKey: accessKey, + SecretKey: secretKey, + }) +} + +func getenv(key, fallback string) string { + value := os.Getenv(key) + if len(value) == 0 { + return fallback + } + return value +} + +func (p *Provider) apiProvide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error { + p.Constraints = append(p.Constraints, constraints...) + + if p.API == nil { + p.API = &APIConfiguration{} + } + + safe.Go(func() { + operation := func() error { + rancherClient, err := p.createClient() + + if err != nil { + log.Errorf("Failed to create a client for rancher, error: %s", err) + return err + } + + ctx := context.Background() + var environments = listRancherEnvironments(rancherClient) + var services = listRancherServices(rancherClient) + var container = listRancherContainer(rancherClient) + + var rancherData = parseAPISourcedRancherData(environments, services, container) + + configuration := p.loadRancherConfig(rancherData) + configurationChan <- types.ConfigMessage{ + ProviderName: "rancher", + Configuration: configuration, + } + + if p.Watch { + _, cancel := context.WithCancel(ctx) + ticker := time.NewTicker(time.Second * time.Duration(p.RefreshSeconds)) + pool.Go(func(stop chan bool) { + for { + select { + case <-ticker.C: + + log.Debugf("Refreshing new Data from Provider API") + var environments = listRancherEnvironments(rancherClient) + var services = listRancherServices(rancherClient) + var container = listRancherContainer(rancherClient) + + rancherData := parseAPISourcedRancherData(environments, services, container) + + configuration := p.loadRancherConfig(rancherData) + if configuration != nil { + configurationChan <- types.ConfigMessage{ + ProviderName: "rancher", + Configuration: configuration, + } + } + case <-stop: + ticker.Stop() + cancel() + return + } + } + }) + } + + return nil + } + notify := func(err error, time time.Duration) { + log.Errorf("Provider connection error %+v, retrying in %s", err, time) + } + err := backoff.RetryNotify(operation, job.NewBackOff(backoff.NewExponentialBackOff()), notify) + if err != nil { + log.Errorf("Cannot connect to Provider Endpoint %+v", err) + } + }) + + return nil +} + +func listRancherEnvironments(client *rancher.RancherClient) []*rancher.Project { + + // Rancher Environment in frontend UI is actually project in API + // https://forums.rancher.com/t/api-key-for-all-environments/279/9 + + var environmentList = []*rancher.Project{} + + environments, err := client.Project.List(nil) + + if err != nil { + log.Errorf("Cannot get Rancher Environments %+v", err) + } + + for k := range environments.Data { + environmentList = append(environmentList, &environments.Data[k]) + } + + return environmentList +} + +func listRancherServices(client *rancher.RancherClient) []*rancher.Service { + + var servicesList = []*rancher.Service{} + + services, err := client.Service.List(withoutPagination) + + if err != nil { + log.Errorf("Cannot get Provider Services %+v", err) + } + + for k := range services.Data { + servicesList = append(servicesList, &services.Data[k]) + } + + return servicesList +} + +func listRancherContainer(client *rancher.RancherClient) []*rancher.Container { + + containerList := []*rancher.Container{} + + container, err := client.Container.List(withoutPagination) + + if err != nil { + log.Errorf("Cannot get Provider Services %+v", err) + } + + valid := true + + for valid { + for k := range container.Data { + containerList = append(containerList, &container.Data[k]) + } + + container, err = container.Next() + + if err != nil { + break + } + + if container == nil || len(container.Data) == 0 { + valid = false + } + } + + return containerList +} + +func parseAPISourcedRancherData(environments []*rancher.Project, services []*rancher.Service, containers []*rancher.Container) []rancherData { + var rancherDataList []rancherData + + for _, environment := range environments { + + for _, service := range services { + if service.EnvironmentId != environment.Id { + continue + } + + rancherData := rancherData{ + Name: environment.Name + "/" + service.Name, + Health: service.HealthState, + State: service.State, + Labels: make(map[string]string), + Containers: []string{}, + } + + if service.LaunchConfig == nil || service.LaunchConfig.Labels == nil { + log.Warnf("Rancher Service Labels are missing. Environment: %s, service: %s", environment.Name, service.Name) + } else { + for key, value := range service.LaunchConfig.Labels { + rancherData.Labels[key] = value.(string) + } + } + + for _, container := range containers { + if container.Labels["io.rancher.stack_service.name"] == rancherData.Name && + containerFilter(container.Name, container.HealthState, container.State) { + rancherData.Containers = append(rancherData.Containers, container.PrimaryIpAddress) + } + } + rancherDataList = append(rancherDataList, rancherData) + } + } + + return rancherDataList +} diff --git a/provider/rancher/metadata.go b/provider/rancher/metadata.go new file mode 100644 index 000000000..606d0321a --- /dev/null +++ b/provider/rancher/metadata.go @@ -0,0 +1,136 @@ +package rancher + +import ( + "context" + "fmt" + "time" + + "github.com/Sirupsen/logrus" + "github.com/cenk/backoff" + "github.com/containous/traefik/job" + "github.com/containous/traefik/log" + "github.com/containous/traefik/safe" + "github.com/containous/traefik/types" + + rancher "github.com/rancher/go-rancher-metadata/metadata" +) + +// MetadataConfiguration contains configuration properties specific to +// the Rancher metadata service provider. +type MetadataConfiguration struct { + IntervalPoll bool `description:"Poll the Rancher metadata service every 'rancher.refreshseconds' (less accurate)"` + Prefix string `description:"Prefix used for accessing the Rancher metadata service"` +} + +func (p *Provider) metadataProvide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error { + p.Constraints = append(p.Constraints, constraints...) + + metadataServiceURL := fmt.Sprintf("http://rancher-metadata.rancher.internal/%s", p.Metadata.Prefix) + + safe.Go(func() { + operation := func() error { + client, err := rancher.NewClientAndWait(metadataServiceURL) + if err != nil { + log.Errorln("Failed to create Rancher metadata service client: %s", err) + return err + } + + updateConfiguration := func(version string) { + log.WithField("metadata_version", version).Debugln("Refreshing configuration from Rancher metadata service") + + services, err := client.GetServices() + if err != nil { + log.Errorf("Failed to query Rancher metadata service: %s", err) + return + } + + rancherData := parseMetadataSourcedRancherData(services) + configuration := p.loadRancherConfig(rancherData) + configurationChan <- types.ConfigMessage{ + ProviderName: "rancher", + Configuration: configuration, + } + } + updateConfiguration("init") + + if p.Watch { + pool.Go(func(stop chan bool) { + switch { + case p.Metadata.IntervalPoll: + p.intervalPoll(client, updateConfiguration, stop) + default: + p.longPoll(client, updateConfiguration, stop) + } + }) + } + return nil + } + + notify := func(err error, time time.Duration) { + log.WithFields(logrus.Fields{ + "error": err, + "retry_in": time, + }).Errorln("Rancher metadata service connection error") + } + + if err := backoff.RetryNotify(operation, job.NewBackOff(backoff.NewExponentialBackOff()), notify); err != nil { + log.WithField("endpoint", metadataServiceURL).Errorln("Cannot connect to Rancher metadata service") + } + }) + + return nil +} + +func (p *Provider) intervalPoll(client rancher.Client, updateConfiguration func(string), stop chan bool) { + _, cancel := context.WithCancel(context.Background()) + defer cancel() + + ticker := time.NewTicker(time.Duration(p.RefreshSeconds)) + defer ticker.Stop() + + var version string + for { + select { + case <-ticker.C: + newVersion, err := client.GetVersion() + if err != nil { + log.WithField("error", err).Errorln("Failed to read Rancher metadata service version") + } else if version != newVersion { + version = newVersion + updateConfiguration(version) + } + case <-stop: + return + } + } +} + +func (p *Provider) longPoll(client rancher.Client, updateConfiguration func(string), stop chan bool) { + _, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Holds the connection until there is either a change in the metadata + // repository or `p.RefreshSeconds` has elapsed. Long polling should be + // favoured for the most accurate configuration updates. + go client.OnChange(p.RefreshSeconds, updateConfiguration) + <-stop +} + +func parseMetadataSourcedRancherData(services []rancher.Service) (rancherDataList []rancherData) { + for _, service := range services { + var containerIPAddresses []string + for _, container := range service.Containers { + if containerFilter(container.Name, container.HealthState, container.State) { + containerIPAddresses = append(containerIPAddresses, container.PrimaryIp) + } + } + + rancherDataList = append(rancherDataList, rancherData{ + Name: service.Name, + State: service.State, + Labels: service.Labels, + Containers: containerIPAddresses, + }) + } + return rancherDataList +} diff --git a/provider/rancher/rancher.go b/provider/rancher/rancher.go index 22233d9f1..e7c5c9f7a 100644 --- a/provider/rancher/rancher.go +++ b/provider/rancher/rancher.go @@ -1,28 +1,17 @@ package rancher import ( - "context" - "errors" "fmt" "math" - "os" "strconv" "strings" "text/template" - "time" "github.com/BurntSushi/ty/fun" - "github.com/cenk/backoff" - "github.com/containous/traefik/job" "github.com/containous/traefik/log" "github.com/containous/traefik/provider" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" - rancher "github.com/rancher/go-rancher/client" -) - -var ( - withoutPagination *rancher.ListOpts ) var _ provider.Provider = (*Provider)(nil) @@ -30,13 +19,13 @@ var _ provider.Provider = (*Provider)(nil) // Provider holds configurations of the provider. type Provider struct { provider.BaseProvider `mapstructure:",squash"` - Endpoint string `description:"Rancher server HTTP(S) endpoint."` - AccessKey string `description:"Rancher server access key."` - SecretKey string `description:"Rancher server Secret Key."` - ExposedByDefault bool `description:"Expose Services by default"` - Domain string `description:"Default domain used"` - RefreshSeconds int `description:"Polling interval (in seconds)"` - EnableServiceHealthFilter bool `description:"Filter services with unhealthy states and health states."` + APIConfiguration `mapstructure:",squash"` // Provide backwards compatibility + API *APIConfiguration `description:"Enable the Rancher API provider"` + Metadata *MetadataConfiguration `description:"Enable the Rancher metadata service provider"` + Domain string `description:"Default domain used"` + RefreshSeconds int `description:"Polling interval (in seconds)"` + ExposedByDefault bool `description:"Expose services by default"` + EnableServiceHealthFilter bool `description:"Filter services with unhealthy states and inactive states"` } type rancherData struct { @@ -47,12 +36,6 @@ type rancherData struct { State string } -func init() { - withoutPagination = &rancher.ListOpts{ - Filters: map[string]interface{}{"limit": 0}, - } -} - func (r rancherData) String() string { return fmt.Sprintf("{name:%s, labels:%v, containers: %v, health: %s, state: %s}", r.Name, r.Labels, r.Containers, r.Health, r.State) } @@ -207,205 +190,16 @@ func getServiceLabel(service rancherData, label string) (string, error) { return value, nil } } - return "", errors.New("Label not found:" + label) + return "", fmt.Errorf("label not found: %s", label) } -func (p *Provider) createClient() (*rancher.RancherClient, error) { - - rancherURL := getenv("CATTLE_URL", p.Endpoint) - accessKey := getenv("CATTLE_ACCESS_KEY", p.AccessKey) - secretKey := getenv("CATTLE_SECRET_KEY", p.SecretKey) - - return rancher.NewRancherClient(&rancher.ClientOpts{ - Url: rancherURL, - AccessKey: accessKey, - SecretKey: secretKey, - }) -} - -func getenv(key, fallback string) string { - value := os.Getenv(key) - if len(value) == 0 { - return fallback - } - return value -} - -// Provide allows the rancher provider to provide configurations to traefik -// using the given configuration channel. +// Provide allows either the Rancher API or metadata service provider to +// seed configuration into Traefik using the given configuration channel. func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error { - p.Constraints = append(p.Constraints, constraints...) - - safe.Go(func() { - operation := func() error { - rancherClient, err := p.createClient() - - if err != nil { - log.Errorf("Failed to create a client for rancher, error: %s", err) - return err - } - - ctx := context.Background() - var environments = listRancherEnvironments(rancherClient) - var services = listRancherServices(rancherClient) - var container = listRancherContainer(rancherClient) - - var rancherData = parseRancherData(environments, services, container) - - configuration := p.loadRancherConfig(rancherData) - configurationChan <- types.ConfigMessage{ - ProviderName: "rancher", - Configuration: configuration, - } - - if p.Watch { - _, cancel := context.WithCancel(ctx) - ticker := time.NewTicker(time.Second * time.Duration(p.RefreshSeconds)) - pool.Go(func(stop chan bool) { - for { - select { - case <-ticker.C: - - log.Debugf("Refreshing new Data from Provider API") - var environments = listRancherEnvironments(rancherClient) - var services = listRancherServices(rancherClient) - var container = listRancherContainer(rancherClient) - - rancherData := parseRancherData(environments, services, container) - - configuration := p.loadRancherConfig(rancherData) - if configuration != nil { - configurationChan <- types.ConfigMessage{ - ProviderName: "rancher", - Configuration: configuration, - } - } - case <-stop: - ticker.Stop() - cancel() - return - } - } - }) - } - - return nil - } - notify := func(err error, time time.Duration) { - log.Errorf("Provider connection error %+v, retrying in %s", err, time) - } - err := backoff.RetryNotify(operation, job.NewBackOff(backoff.NewExponentialBackOff()), notify) - if err != nil { - log.Errorf("Cannot connect to Provider Endpoint %+v", err) - } - }) - - return nil -} - -func listRancherEnvironments(client *rancher.RancherClient) []*rancher.Environment { - - var environmentList = []*rancher.Environment{} - - environments, err := client.Environment.List(withoutPagination) - - if err != nil { - log.Errorf("Cannot get Provider Environments %+v", err) + if p.Metadata == nil { + return p.apiProvide(configurationChan, pool, constraints) } - - for k := range environments.Data { - environmentList = append(environmentList, &environments.Data[k]) - } - - return environmentList -} - -func listRancherServices(client *rancher.RancherClient) []*rancher.Service { - - var servicesList = []*rancher.Service{} - - services, err := client.Service.List(withoutPagination) - - if err != nil { - log.Errorf("Cannot get Provider Services %+v", err) - } - - for k := range services.Data { - servicesList = append(servicesList, &services.Data[k]) - } - - return servicesList -} - -func listRancherContainer(client *rancher.RancherClient) []*rancher.Container { - - containerList := []*rancher.Container{} - - container, err := client.Container.List(withoutPagination) - - log.Debugf("first container len: %i", len(container.Data)) - - if err != nil { - log.Errorf("Cannot get Provider Services %+v", err) - } - - valid := true - - for valid { - for k := range container.Data { - containerList = append(containerList, &container.Data[k]) - } - - container, err = container.Next() - - if err != nil { - break - } - - if container == nil || len(container.Data) == 0 { - valid = false - } - } - - return containerList -} - -func parseRancherData(environments []*rancher.Environment, services []*rancher.Service, containers []*rancher.Container) []rancherData { - var rancherDataList []rancherData - - for _, environment := range environments { - - for _, service := range services { - if service.EnvironmentId != environment.Id { - continue - } - - rancherData := rancherData{ - Name: environment.Name + "/" + service.Name, - Health: service.HealthState, - State: service.State, - Labels: make(map[string]string), - Containers: []string{}, - } - - if service.LaunchConfig == nil || service.LaunchConfig.Labels == nil { - log.Warnf("Rancher Service Labels are missing. Environment: %s, service: %s", environment.Name, service.Name) - } else { - for key, value := range service.LaunchConfig.Labels { - rancherData.Labels[key] = value.(string) - } - } - - for _, container := range containers { - if container.Labels["io.rancher.stack_service.name"] == rancherData.Name && containerFilter(container) { - rancherData.Containers = append(rancherData.Containers, container.PrimaryIpAddress) - } - } - rancherDataList = append(rancherDataList, rancherData) - } - } - - return rancherDataList + return p.metadataProvide(configurationChan, pool, constraints) } func (p *Provider) loadRancherConfig(services []rancherData) *types.Configuration { @@ -464,14 +258,14 @@ func (p *Provider) loadRancherConfig(services []rancherData) *types.Configuratio } -func containerFilter(container *rancher.Container) bool { - if container.HealthState != "" && container.HealthState != "healthy" && container.HealthState != "updating-healthy" { - log.Debugf("Filtering container %s with healthState of %s", container.Name, container.HealthState) +func containerFilter(name, healthState, state string) bool { + if healthState != "" && healthState != "healthy" && healthState != "updating-healthy" { + log.Debugf("Filtering container %s with healthState of %s", name, healthState) return false } - if container.State != "" && container.State != "running" && container.State != "updating-running" { - log.Debugf("Filtering container %s with state of %s", container.Name, container.State) + if state != "" && state != "running" && state != "updating-running" { + log.Debugf("Filtering container %s with state of %s", name, state) return false } diff --git a/provider/rancher/rancher_test.go b/provider/rancher/rancher_test.go index a2eee64d8..0e8b07386 100644 --- a/provider/rancher/rancher_test.go +++ b/provider/rancher/rancher_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/containous/traefik/types" - rancher "github.com/rancher/go-rancher/client" ) func TestRancherServiceFilter(t *testing.T) { @@ -114,49 +113,41 @@ func TestRancherServiceFilter(t *testing.T) { func TestRancherContainerFilter(t *testing.T) { containers := []struct { - container *rancher.Container - expected bool + name string + healthState string + state string + expected bool }{ { - container: &rancher.Container{ - HealthState: "unhealthy", - State: "running", - }, + healthState: "unhealthy", + state: "running", + expected: false, + }, + { + healthState: "healthy", + state: "stopped", + expected: false, + }, + { + state: "stopped", expected: false, }, { - container: &rancher.Container{ - HealthState: "healthy", - State: "stopped", - }, - expected: false, + healthState: "healthy", + state: "running", + expected: true, }, { - container: &rancher.Container{ - State: "stopped", - }, - expected: false, - }, - { - container: &rancher.Container{ - HealthState: "healthy", - State: "running", - }, - expected: true, - }, - { - container: &rancher.Container{ - HealthState: "updating-healthy", - State: "updating-running", - }, - expected: true, + healthState: "updating-healthy", + state: "updating-running", + expected: true, }, } - for _, e := range containers { - actual := containerFilter(e.container) - if actual != e.expected { - t.Fatalf("expected %t, got %t", e.expected, actual) + for _, container := range containers { + actual := containerFilter(container.name, container.healthState, container.state) + if actual != container.expected { + t.Fatalf("expected %t, got %t", container.expected, actual) } } } @@ -506,7 +497,7 @@ func TestRancherGetLabel(t *testing.T) { service: rancherData{ Name: "test-service", }, - expected: "Label not found", + expected: "label not found", }, { service: rancherData{ @@ -593,9 +584,7 @@ func TestRancherLoadRancherConfig(t *testing.T) { for _, c := range cases { var rancherDataList []rancherData - for _, service := range c.services { - rancherDataList = append(rancherDataList, service) - } + rancherDataList = append(rancherDataList, c.services...) actualConfig := provider.loadRancherConfig(rancherDataList) diff --git a/server/configuration.go b/server/configuration.go index a3cb33289..2fc81428e 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -450,7 +450,6 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { defaultRancher.Watch = true defaultRancher.ExposedByDefault = true defaultRancher.RefreshSeconds = 15 - defaultRancher.EnableServiceHealthFilter = false // default DynamoDB var defaultDynamoDB dynamodb.Provider @@ -485,9 +484,6 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { AccessLog: &defaultAccessLog, } - //default Rancher - //@TODO: ADD - return &TraefikConfiguration{ GlobalConfiguration: defaultConfiguration, } diff --git a/traefik.sample.toml b/traefik.sample.toml index 9dff28a06..1275b9ce7 100644 --- a/traefik.sample.toml +++ b/traefik.sample.toml @@ -1098,28 +1098,59 @@ # # ExposedByDefault = false -# Filter services with unhealthy states and health states +# Filter services with unhealthy states and inactive states # # Optional # Default: false # -# EnableServiceHealthFilter = false +# EnableServiceHealthFilter = true -# Endpoint to use when connecting to Rancher +# Enable Rancher API configuration backend +# +# Optional +# Default: true +# +# [rancher.api] + +# Endpoint to use when connecting to the Rancher API # # Required # Endpoint = "http://rancherserver.example.com/v1" -# AccessKey to use when connecting to Rancher +# AccessKey to use when connecting to the Rancher API # # Required # AccessKey = "XXXXXXXXXXXXXXXXXXXX" -# SecretKey to use when connecting to Rancher +# SecretKey to use when connecting to the Rancher API # # Required # SecretKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +# Enable Rancher metadata service configuration backend instead of the API +# configuration backend +# +# Optional +# Default: false +# +# [rancher.metadataservice] + +# Poll the Rancher metadata service for changes every `rancher.RefreshSeconds` +# NOTE: this is less accurate than the default long polling technique which +# will provide near instantaneous updates to Traefik +# +# Optional +# Default: false +# +# IntervalPoll = true + +# Prefix used for accessing the Rancher metadata service +# +# Optional +# Default: "/latest" +# +# Prefix = "/2016-07-29" + # Constraints # # Optional diff --git a/vendor/github.com/rancher/go-rancher-metadata/main.go b/vendor/github.com/rancher/go-rancher-metadata/main.go new file mode 100644 index 000000000..fe3037eb2 --- /dev/null +++ b/vendor/github.com/rancher/go-rancher-metadata/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "time" + + "github.com/Sirupsen/logrus" + "github.com/rancher/go-rancher-metadata/metadata" +) + +const ( + metadataUrl = "http://rancher-metadata/2015-12-19" +) + +func main() { + m := metadata.NewClient(metadataUrl) + + version := "init" + + for { + newVersion, err := m.GetVersion() + if err != nil { + logrus.Errorf("Error reading metadata version: %v", err) + } else if version == newVersion { + logrus.Debug("No changes in metadata version") + } else { + logrus.Debugf("Metadata version has changed, oldVersion=[%s], newVersion=[%s]", version, newVersion) + version = newVersion + } + time.Sleep(5 * time.Second) + } +} diff --git a/vendor/github.com/rancher/go-rancher-metadata/metadata/change.go b/vendor/github.com/rancher/go-rancher-metadata/metadata/change.go new file mode 100644 index 000000000..8d6c17557 --- /dev/null +++ b/vendor/github.com/rancher/go-rancher-metadata/metadata/change.go @@ -0,0 +1,64 @@ +package metadata + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/Sirupsen/logrus" +) + +func (m *client) OnChangeWithError(intervalSeconds int, do func(string)) error { + return m.onChangeFromVersionWithError("init", intervalSeconds, do) +} + +func (m *client) onChangeFromVersionWithError(version string, intervalSeconds int, do func(string)) error { + for { + newVersion, err := m.waitVersion(intervalSeconds, version) + if err != nil { + return err + } else if version == newVersion { + logrus.Debug("No changes in metadata version") + } else { + logrus.Debugf("Metadata Version has been changed. Old version: %s. New version: %s.", version, newVersion) + version = newVersion + do(newVersion) + } + } + + return nil +} + +func (m *client) OnChange(intervalSeconds int, do func(string)) { + version := "init" + updateVersionAndDo := func(v string) { + version = v + do(version) + } + interval := time.Duration(intervalSeconds) + for { + if err := m.onChangeFromVersionWithError(version, intervalSeconds, updateVersionAndDo); err != nil { + logrus.Errorf("Error reading metadata version: %v", err) + } + time.Sleep(interval * time.Second) + } +} + +type timeout interface { + Timeout() bool +} + +func (m *client) waitVersion(maxWait int, version string) (string, error) { + for { + resp, err := m.SendRequest(fmt.Sprintf("/version?wait=true&value=%s&maxWait=%d", version, maxWait)) + if err != nil { + t, ok := err.(timeout) + if ok && t.Timeout() { + continue + } + return "", err + } + err = json.Unmarshal(resp, &version) + return version, err + } +} diff --git a/vendor/github.com/rancher/go-rancher-metadata/metadata/metadata.go b/vendor/github.com/rancher/go-rancher-metadata/metadata/metadata.go new file mode 100644 index 000000000..f06b181aa --- /dev/null +++ b/vendor/github.com/rancher/go-rancher-metadata/metadata/metadata.go @@ -0,0 +1,262 @@ +package metadata + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" +) + +type Client interface { + OnChangeWithError(int, func(string)) error + OnChange(int, func(string)) + SendRequest(string) ([]byte, error) + GetVersion() (string, error) + GetSelfHost() (Host, error) + GetSelfContainer() (Container, error) + GetSelfServiceByName(string) (Service, error) + GetSelfService() (Service, error) + GetSelfStack() (Stack, error) + GetServices() ([]Service, error) + GetStacks() ([]Stack, error) + GetContainers() ([]Container, error) + GetServiceContainers(string, string) ([]Container, error) + GetHosts() ([]Host, error) + GetHost(string) (Host, error) + GetNetworks() ([]Network, error) +} + +type client struct { + url string + ip string + client *http.Client +} + +func newClient(url, ip string) *client { + return &client{url, ip, &http.Client{Timeout: 10 * time.Second}} +} + +func NewClient(url string) Client { + ip := "" + return newClient(url, ip) +} + +func NewClientWithIPAndWait(url, ip string) (Client, error) { + client := newClient(url, ip) + + if err := testConnection(client); err != nil { + return nil, err + } + + return client, nil +} + +func NewClientAndWait(url string) (Client, error) { + ip := "" + client := newClient(url, ip) + + if err := testConnection(client); err != nil { + return nil, err + } + + return client, nil +} + +func (m *client) SendRequest(path string) ([]byte, error) { + req, err := http.NewRequest("GET", m.url+path, nil) + req.Header.Add("Accept", "application/json") + if m.ip != "" { + req.Header.Add("X-Forwarded-For", m.ip) + } + resp, err := m.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("Error %v accessing %v path", resp.StatusCode, path) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return body, nil +} + +func (m *client) GetVersion() (string, error) { + resp, err := m.SendRequest("/version") + if err != nil { + return "", err + } + return string(resp[:]), nil +} + +func (m *client) GetSelfHost() (Host, error) { + resp, err := m.SendRequest("/self/host") + var host Host + if err != nil { + return host, err + } + + if err = json.Unmarshal(resp, &host); err != nil { + return host, err + } + + return host, nil +} + +func (m *client) GetSelfContainer() (Container, error) { + resp, err := m.SendRequest("/self/container") + var container Container + if err != nil { + return container, err + } + + if err = json.Unmarshal(resp, &container); err != nil { + return container, err + } + + return container, nil +} + +func (m *client) GetSelfServiceByName(name string) (Service, error) { + resp, err := m.SendRequest("/self/stack/services/" + name) + var service Service + if err != nil { + return service, err + } + + if err = json.Unmarshal(resp, &service); err != nil { + return service, err + } + + return service, nil +} + +func (m *client) GetSelfService() (Service, error) { + resp, err := m.SendRequest("/self/service") + var service Service + if err != nil { + return service, err + } + + if err = json.Unmarshal(resp, &service); err != nil { + return service, err + } + + return service, nil +} + +func (m *client) GetSelfStack() (Stack, error) { + resp, err := m.SendRequest("/self/stack") + var stack Stack + if err != nil { + return stack, err + } + + if err = json.Unmarshal(resp, &stack); err != nil { + return stack, err + } + + return stack, nil +} + +func (m *client) GetServices() ([]Service, error) { + resp, err := m.SendRequest("/services") + var services []Service + if err != nil { + return services, err + } + + if err = json.Unmarshal(resp, &services); err != nil { + return services, err + } + return services, nil +} + +func (m *client) GetStacks() ([]Stack, error) { + resp, err := m.SendRequest("/stacks") + var stacks []Stack + if err != nil { + return stacks, err + } + + if err = json.Unmarshal(resp, &stacks); err != nil { + return stacks, err + } + return stacks, nil +} + +func (m *client) GetContainers() ([]Container, error) { + resp, err := m.SendRequest("/containers") + var containers []Container + if err != nil { + return containers, err + } + + if err = json.Unmarshal(resp, &containers); err != nil { + return containers, err + } + return containers, nil +} + +func (m *client) GetServiceContainers(serviceName string, stackName string) ([]Container, error) { + var serviceContainers = []Container{} + containers, err := m.GetContainers() + if err != nil { + return serviceContainers, err + } + + for _, container := range containers { + if container.StackName == stackName && container.ServiceName == serviceName { + serviceContainers = append(serviceContainers, container) + } + } + + return serviceContainers, nil +} + +func (m *client) GetHosts() ([]Host, error) { + resp, err := m.SendRequest("/hosts") + var hosts []Host + if err != nil { + return hosts, err + } + + if err = json.Unmarshal(resp, &hosts); err != nil { + return hosts, err + } + return hosts, nil +} + +func (m *client) GetHost(UUID string) (Host, error) { + var host Host + hosts, err := m.GetHosts() + if err != nil { + return host, err + } + for _, host := range hosts { + if host.UUID == UUID { + return host, nil + } + } + + return host, fmt.Errorf("could not find host by UUID %v", UUID) +} + +func (m *client) GetNetworks() ([]Network, error) { + resp, err := m.SendRequest("/networks") + var networks []Network + if err != nil { + return networks, err + } + + if err = json.Unmarshal(resp, &networks); err != nil { + return networks, err + } + + return networks, nil +} diff --git a/vendor/github.com/rancher/go-rancher-metadata/metadata/types.go b/vendor/github.com/rancher/go-rancher-metadata/metadata/types.go new file mode 100644 index 000000000..f4b938e92 --- /dev/null +++ b/vendor/github.com/rancher/go-rancher-metadata/metadata/types.go @@ -0,0 +1,149 @@ +package metadata + +type Stack struct { + EnvironmentName string `json:"environment_name"` + EnvironmentUUID string `json:"environment_uuid"` + Name string `json:"name"` + UUID string `json:"uuid"` + Services []Service `json:"services"` + System bool `json:"system"` +} + +type HealthCheck struct { + HealthyThreshold int `json:"healthy_threshold"` + Interval int `json:"interval"` + Port int `json:"port"` + RequestLine string `json:"request_line"` + ResponseTimeout int `json:"response_timeout"` + UnhealthyThreshold int `json:"unhealthy_threshold"` +} + +type Service struct { + Scale int `json:"scale"` + Name string `json:"name"` + StackName string `json:"stack_name"` + StackUUID string `json:"stack_uuid"` + Kind string `json:"kind"` + Hostname string `json:"hostname"` + Vip string `json:"vip"` + CreateIndex int `json:"create_index"` + UUID string `json:"uuid"` + ExternalIps []string `json:"external_ips"` + Sidekicks []string `json:"sidekicks"` + Containers []Container `json:"containers"` + Ports []string `json:"ports"` + Labels map[string]string `json:"labels"` + Links map[string]string `json:"links"` + Metadata map[string]interface{} `json:"metadata"` + Token string `json:"token"` + Fqdn string `json:"fqdn"` + HealthCheck HealthCheck `json:"health_check"` + PrimaryServiceName string `json:"primary_service_name"` + LBConfig LBConfig `json:"lb_config"` + EnvironmentUUID string `json:"environment_uuid"` + State string `json:"state"` + System bool `json:"system"` +} + +type Container struct { + Name string `json:"name"` + PrimaryIp string `json:"primary_ip"` + PrimaryMacAddress string `json:"primary_mac_address"` + Ips []string `json:"ips"` + Ports []string `json:"ports"` + ServiceName string `json:"service_name"` + ServiceIndex string `json:"service_index"` + StackName string `json:"stack_name"` + Labels map[string]string `json:"labels"` + CreateIndex int `json:"create_index"` + HostUUID string `json:"host_uuid"` + UUID string `json:"uuid"` + State string `json:"state"` + HealthState string `json:"health_state"` + ExternalId string `json:"external_id"` + StartCount int `json:"start_count"` + MemoryReservation int64 `json:"memory_reservation"` + MilliCPUReservation int64 `json:"milli_cpu_reservation"` + Dns []string `json:"dns"` + DnsSearch []string `json:"dns_search"` + HealthCheckHosts []string `json:"health_check_hosts"` + NetworkFromContainerUUID string `json:"network_from_container_uuid"` + NetworkUUID string `json:"network_uuid"` + Links map[string]string `json:"links"` + System bool `json:"system"` + EnvironmentUUID string `json:"environment_uuid"` + HealthCheck HealthCheck `json:"health_check"` +} + +type Network struct { + Name string `json:"name"` + UUID string `json:"uuid"` + EnvironmentUUID string `json:"environment_uuid"` + Metadata map[string]interface{} `json:"metadata"` + HostPorts bool `json:"host_ports"` + Default bool `json:"is_default"` + Policy []NetworkPolicyRule `json:"policy,omitempty"` + DefaultPolicyAction string `json:"default_policy_action"` +} + +type Host struct { + Name string `json:"name"` + AgentIP string `json:"agent_ip"` + HostId int `json:"host_id"` + Labels map[string]string `json:"labels"` + UUID string `json:"uuid"` + Hostname string `json:"hostname"` + Memory int64 `json:"memory"` + MilliCPU int64 `json:"milli_cpu"` + LocalStorageMb int64 `json:"local_storage_mb"` + EnvironmentUUID string `json:"environment_uuid"` +} + +type PortRule struct { + SourcePort int `json:"source_port"` + Protocol string `json:"protocol"` + Path string `json:"path"` + Hostname string `json:"hostname"` + Service string `json:"service"` + TargetPort int `json:"target_port"` + Priority int `json:"priority"` + BackendName string `json:"backend_name"` + Selector string `json:"selector"` + Container string `json:"container"` +} + +type LBConfig struct { + Certs []string `json:"certs"` + DefaultCert string `json:"default_cert"` + PortRules []PortRule `json:"port_rules"` + Config string `json:"config"` + StickinessPolicy LBStickinessPolicy `json:"stickiness_policy"` +} + +type LBStickinessPolicy struct { + Name string `json:"name"` + Cookie string `json:"cookie"` + Domain string `json:"domain"` + Indirect bool `json:"indirect"` + Nocache bool `json:"nocache"` + Postonly bool `json:"postonly"` + Mode string `json:"mode"` +} + +type NetworkPolicyRuleBetween struct { + Selector string `yaml:"selector,omitempty"` + GroupBy string `yaml:"groupBy,omitempty"` +} + +type NetworkPolicyRuleMember struct { + Selector string `yaml:"selector,omitempty"` +} + +type NetworkPolicyRule struct { + From *NetworkPolicyRuleMember `yaml:"from"` + To *NetworkPolicyRuleMember `yaml:"to"` + Ports []string `yaml:"ports"` + Within string `yaml:"within"` + Between *NetworkPolicyRuleBetween `yaml:"between"` + Action string `yaml:"action"` +} diff --git a/vendor/github.com/rancher/go-rancher-metadata/metadata/utils.go b/vendor/github.com/rancher/go-rancher-metadata/metadata/utils.go new file mode 100644 index 000000000..509a53016 --- /dev/null +++ b/vendor/github.com/rancher/go-rancher-metadata/metadata/utils.go @@ -0,0 +1,19 @@ +package metadata + +import ( + "time" +) + +func testConnection(mdClient Client) error { + var err error + maxTime := 20 * time.Second + + for i := 1 * time.Second; i < maxTime; i *= time.Duration(2) { + if _, err = mdClient.GetVersion(); err != nil { + time.Sleep(i) + } else { + return nil + } + } + return err +}