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
This commit is contained in:
Martin Baillie 2017-05-08 11:20:38 +10:00 committed by Ludovic Fernandez
parent 984ea1040f
commit 9cb07d026f
14 changed files with 1006 additions and 272 deletions

View file

@ -20,6 +20,7 @@ import (
"github.com/containous/traefik/cluster" "github.com/containous/traefik/cluster"
"github.com/containous/traefik/log" "github.com/containous/traefik/log"
"github.com/containous/traefik/provider/kubernetes" "github.com/containous/traefik/provider/kubernetes"
"github.com/containous/traefik/provider/rancher"
"github.com/containous/traefik/safe" "github.com/containous/traefik/safe"
"github.com/containous/traefik/server" "github.com/containous/traefik/server"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
@ -193,6 +194,28 @@ func run(traefikConfiguration *server.TraefikConfiguration) {
globalConfiguration.DefaultEntryPoints = []string{"http"} 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 { if globalConfiguration.Debug {
globalConfiguration.LogLevel = "DEBUG" globalConfiguration.LogLevel = "DEBUG"
} }

6
glide.lock generated
View file

@ -1,4 +1,4 @@
hash: 34ceb7bd979d43efdbf721ccb9d983061c06db527148f90f1784db89f6d089f0 hash: 088194c8357ca08e27476866b9007adfa7711500fe0c78650ecb397c4f70075a
updated: 2017-05-19T23:30:19.890844996+02:00 updated: 2017-05-19T23:30:19.890844996+02:00
imports: imports:
- name: cloud.google.com/go - name: cloud.google.com/go
@ -364,6 +364,10 @@ imports:
version: 5b8f6cc26b355ba03d7611fce3844155b7baf05b version: 5b8f6cc26b355ba03d7611fce3844155b7baf05b
subpackages: subpackages:
- client - client
- name: github.com/rancher/go-rancher-metadata
version: 95d4962a8f0420be24fb49c2cb4f5491284c62f1
subpackages:
- metadata
- name: github.com/ryanuber/go-glob - name: github.com/ryanuber/go-glob
version: 256dc444b735e061061cf46c809487313d5b0065 version: 256dc444b735e061061cf46c809487313d5b0065
- name: github.com/samuel/go-zookeeper - name: github.com/samuel/go-zookeeper

View file

@ -163,6 +163,8 @@ import:
version: 5b8f6cc26b355ba03d7611fce3844155b7baf05b version: 5b8f6cc26b355ba03d7611fce3844155b7baf05b
- package: golang.org/x/oauth2 - package: golang.org/x/oauth2
version: 7fdf09982454086d5570c7db3e11f360194830ca version: 7fdf09982454086d5570c7db3e11f360194830ca
- package: github.com/rancher/go-rancher-metadata
version: 95d4962a8f0420be24fb49c2cb4f5491284c62f1
subpackages: subpackages:
- google - google
- package: github.com/googleapis/gax-go - package: github.com/googleapis/gax-go

234
provider/rancher/api.go Normal file
View file

@ -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
}

View file

@ -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
}

View file

@ -1,28 +1,17 @@
package rancher package rancher
import ( import (
"context"
"errors"
"fmt" "fmt"
"math" "math"
"os"
"strconv" "strconv"
"strings" "strings"
"text/template" "text/template"
"time"
"github.com/BurntSushi/ty/fun" "github.com/BurntSushi/ty/fun"
"github.com/cenk/backoff"
"github.com/containous/traefik/job"
"github.com/containous/traefik/log" "github.com/containous/traefik/log"
"github.com/containous/traefik/provider" "github.com/containous/traefik/provider"
"github.com/containous/traefik/safe" "github.com/containous/traefik/safe"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
rancher "github.com/rancher/go-rancher/client"
)
var (
withoutPagination *rancher.ListOpts
) )
var _ provider.Provider = (*Provider)(nil) var _ provider.Provider = (*Provider)(nil)
@ -30,13 +19,13 @@ var _ provider.Provider = (*Provider)(nil)
// Provider holds configurations of the provider. // Provider holds configurations of the provider.
type Provider struct { type Provider struct {
provider.BaseProvider `mapstructure:",squash"` provider.BaseProvider `mapstructure:",squash"`
Endpoint string `description:"Rancher server HTTP(S) endpoint."` APIConfiguration `mapstructure:",squash"` // Provide backwards compatibility
AccessKey string `description:"Rancher server access key."` API *APIConfiguration `description:"Enable the Rancher API provider"`
SecretKey string `description:"Rancher server Secret Key."` Metadata *MetadataConfiguration `description:"Enable the Rancher metadata service provider"`
ExposedByDefault bool `description:"Expose Services by default"`
Domain string `description:"Default domain used"` Domain string `description:"Default domain used"`
RefreshSeconds int `description:"Polling interval (in seconds)"` RefreshSeconds int `description:"Polling interval (in seconds)"`
EnableServiceHealthFilter bool `description:"Filter services with unhealthy states and health states."` ExposedByDefault bool `description:"Expose services by default"`
EnableServiceHealthFilter bool `description:"Filter services with unhealthy states and inactive states"`
} }
type rancherData struct { type rancherData struct {
@ -47,12 +36,6 @@ type rancherData struct {
State string State string
} }
func init() {
withoutPagination = &rancher.ListOpts{
Filters: map[string]interface{}{"limit": 0},
}
}
func (r rancherData) String() string { 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) 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 value, nil
} }
} }
return "", errors.New("Label not found:" + label) return "", fmt.Errorf("label not found: %s", label)
} }
func (p *Provider) createClient() (*rancher.RancherClient, error) { // Provide allows either the Rancher API or metadata service provider to
// seed configuration into Traefik using the given configuration channel.
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.
func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error { func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error {
p.Constraints = append(p.Constraints, constraints...) if p.Metadata == nil {
return p.apiProvide(configurationChan, pool, 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
} }
return p.metadataProvide(configurationChan, pool, constraints)
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)
}
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
} }
func (p *Provider) loadRancherConfig(services []rancherData) *types.Configuration { 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 { func containerFilter(name, healthState, state string) bool {
if container.HealthState != "" && container.HealthState != "healthy" && container.HealthState != "updating-healthy" { if healthState != "" && healthState != "healthy" && healthState != "updating-healthy" {
log.Debugf("Filtering container %s with healthState of %s", container.Name, container.HealthState) log.Debugf("Filtering container %s with healthState of %s", name, healthState)
return false return false
} }
if container.State != "" && container.State != "running" && container.State != "updating-running" { if state != "" && state != "running" && state != "updating-running" {
log.Debugf("Filtering container %s with state of %s", container.Name, container.State) log.Debugf("Filtering container %s with state of %s", name, state)
return false return false
} }

View file

@ -6,7 +6,6 @@ import (
"testing" "testing"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
rancher "github.com/rancher/go-rancher/client"
) )
func TestRancherServiceFilter(t *testing.T) { func TestRancherServiceFilter(t *testing.T) {
@ -114,49 +113,41 @@ func TestRancherServiceFilter(t *testing.T) {
func TestRancherContainerFilter(t *testing.T) { func TestRancherContainerFilter(t *testing.T) {
containers := []struct { containers := []struct {
container *rancher.Container name string
healthState string
state string
expected bool expected bool
}{ }{
{ {
container: &rancher.Container{ healthState: "unhealthy",
HealthState: "unhealthy", state: "running",
State: "running",
},
expected: false, expected: false,
}, },
{ {
container: &rancher.Container{ healthState: "healthy",
HealthState: "healthy", state: "stopped",
State: "stopped",
},
expected: false, expected: false,
}, },
{ {
container: &rancher.Container{ state: "stopped",
State: "stopped",
},
expected: false, expected: false,
}, },
{ {
container: &rancher.Container{ healthState: "healthy",
HealthState: "healthy", state: "running",
State: "running",
},
expected: true, expected: true,
}, },
{ {
container: &rancher.Container{ healthState: "updating-healthy",
HealthState: "updating-healthy", state: "updating-running",
State: "updating-running",
},
expected: true, expected: true,
}, },
} }
for _, e := range containers { for _, container := range containers {
actual := containerFilter(e.container) actual := containerFilter(container.name, container.healthState, container.state)
if actual != e.expected { if actual != container.expected {
t.Fatalf("expected %t, got %t", e.expected, actual) t.Fatalf("expected %t, got %t", container.expected, actual)
} }
} }
} }
@ -506,7 +497,7 @@ func TestRancherGetLabel(t *testing.T) {
service: rancherData{ service: rancherData{
Name: "test-service", Name: "test-service",
}, },
expected: "Label not found", expected: "label not found",
}, },
{ {
service: rancherData{ service: rancherData{
@ -593,9 +584,7 @@ func TestRancherLoadRancherConfig(t *testing.T) {
for _, c := range cases { for _, c := range cases {
var rancherDataList []rancherData var rancherDataList []rancherData
for _, service := range c.services { rancherDataList = append(rancherDataList, c.services...)
rancherDataList = append(rancherDataList, service)
}
actualConfig := provider.loadRancherConfig(rancherDataList) actualConfig := provider.loadRancherConfig(rancherDataList)

View file

@ -450,7 +450,6 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
defaultRancher.Watch = true defaultRancher.Watch = true
defaultRancher.ExposedByDefault = true defaultRancher.ExposedByDefault = true
defaultRancher.RefreshSeconds = 15 defaultRancher.RefreshSeconds = 15
defaultRancher.EnableServiceHealthFilter = false
// default DynamoDB // default DynamoDB
var defaultDynamoDB dynamodb.Provider var defaultDynamoDB dynamodb.Provider
@ -485,9 +484,6 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
AccessLog: &defaultAccessLog, AccessLog: &defaultAccessLog,
} }
//default Rancher
//@TODO: ADD
return &TraefikConfiguration{ return &TraefikConfiguration{
GlobalConfiguration: defaultConfiguration, GlobalConfiguration: defaultConfiguration,
} }

View file

@ -1098,28 +1098,59 @@
# #
# ExposedByDefault = false # ExposedByDefault = false
# Filter services with unhealthy states and health states # Filter services with unhealthy states and inactive states
# #
# Optional # Optional
# Default: false # 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 # Required
# Endpoint = "http://rancherserver.example.com/v1" # Endpoint = "http://rancherserver.example.com/v1"
# AccessKey to use when connecting to Rancher # AccessKey to use when connecting to the Rancher API
# #
# Required # Required
# AccessKey = "XXXXXXXXXXXXXXXXXXXX" # AccessKey = "XXXXXXXXXXXXXXXXXXXX"
# SecretKey to use when connecting to Rancher # SecretKey to use when connecting to the Rancher API
# #
# Required # Required
# SecretKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # 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 # Constraints
# #
# Optional # Optional

31
vendor/github.com/rancher/go-rancher-metadata/main.go generated vendored Normal file
View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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"`
}

View file

@ -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
}