From daf3023b02a7420a9229c43e9d8a4033c0a5ab67 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 18 Dec 2017 09:22:03 +0100 Subject: [PATCH 01/10] Change Zookeeper default prefix. --- cmd/traefik/configuration.go | 2 +- docs/configuration/backends/zookeeper.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/traefik/configuration.go b/cmd/traefik/configuration.go index 692a956c4..dec67e328 100644 --- a/cmd/traefik/configuration.go +++ b/cmd/traefik/configuration.go @@ -112,7 +112,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { var defaultZookeeper zk.Provider defaultZookeeper.Watch = true defaultZookeeper.Endpoint = "127.0.0.1:2181" - defaultZookeeper.Prefix = "/traefik" + defaultZookeeper.Prefix = "traefik" defaultZookeeper.Constraints = types.Constraints{} //default Boltdb diff --git a/docs/configuration/backends/zookeeper.md b/docs/configuration/backends/zookeeper.md index 55b6cb88a..5d31ae3bc 100644 --- a/docs/configuration/backends/zookeeper.md +++ b/docs/configuration/backends/zookeeper.md @@ -27,9 +27,9 @@ watch = true # Prefix used for KV store. # # Optional -# Default: "/traefik" +# Default: "traefik" # -prefix = "/traefik" +prefix = "traefik" # Override default configuration template. # For advanced users :) From 35b5ca4c6326ad415ca1c36ac71a54e266c4e6a5 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 18 Dec 2017 10:30:08 +0100 Subject: [PATCH 02/10] fix isHealthy logic. --- glide.lock | 4 ++-- glide.yaml | 2 +- .../containous/traefik-extra-service-fabric/servicefabric.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/glide.lock b/glide.lock index f35752694..edd61a69a 100644 --- a/glide.lock +++ b/glide.lock @@ -1,4 +1,4 @@ -hash: 03cd7f5ecab087e73cc395cb61b58b82cefef55969aa368f93fc17095b92815f +hash: 2ca4d2b4f55342c6a722f70e0ef2e85ac2a38d8395dc206ad3f71a785b9f050f updated: 2017-12-15T10:34:41.246378337+01:00 imports: - name: cloud.google.com/go @@ -94,7 +94,7 @@ imports: - name: github.com/containous/staert version: af517d5b70db9c4b0505e0144fcc62b054057d2a - name: github.com/containous/traefik-extra-service-fabric - version: c01c1ef60ed612c5e42c1ceae0c6f92e67619cc3 + version: ca1fb57108293caad285b1c366b763f6c6ab71c9 - name: github.com/coreos/bbolt version: 3c6cbfb299c11444eb2f8c9d48f0d2ce09157423 - name: github.com/coreos/etcd diff --git a/glide.yaml b/glide.yaml index f582a9187..b394d797d 100644 --- a/glide.yaml +++ b/glide.yaml @@ -12,7 +12,7 @@ import: - package: github.com/cenk/backoff - package: github.com/containous/flaeg - package: github.com/containous/traefik-extra-service-fabric - version: v1.0.4 + version: v1.0.5 - package: github.com/vulcand/oxy version: 7b6e758ab449705195df638765c4ca472248908a repo: https://github.com/containous/oxy.git diff --git a/vendor/github.com/containous/traefik-extra-service-fabric/servicefabric.go b/vendor/github.com/containous/traefik-extra-service-fabric/servicefabric.go index 8183d9480..c23153487 100644 --- a/vendor/github.com/containous/traefik-extra-service-fabric/servicefabric.go +++ b/vendor/github.com/containous/traefik-extra-service-fabric/servicefabric.go @@ -303,7 +303,7 @@ func isPrimary(instance replicaInstance) bool { } func isHealthy(instanceData *sf.ReplicaItemBase) bool { - return instanceData != nil && (instanceData.ReplicaStatus == "Ready" || instanceData.HealthState != "Error") + return instanceData != nil && (instanceData.ReplicaStatus == "Ready" && instanceData.HealthState != "Error") } func hasHTTPEndpoint(instanceData *sf.ReplicaItemBase) bool { From b4dc96527da4b7a730cc22652eb376fb7bb0c839 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 19 Dec 2017 09:48:03 +0100 Subject: [PATCH 03/10] Move rate limit documentation. --- docs/basics.md | 29 ----------------------------- docs/configuration/commons.md | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/docs/basics.md b/docs/basics.md index 15d3f977b..9a84281a4 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -321,35 +321,6 @@ In this example, traffic routed through the first frontend will have the `X-Fram !!! note The detailed documentation for those security headers can be found in [unrolled/secure](https://github.com/unrolled/secure#available-options). -#### Rate limiting - -Rate limiting can be configured per frontend. -Multiple sets of rates can be added to each frontend, but the time periods must be unique. - -```toml -[frontends] - [frontends.frontend1] - passHostHeader = true - entrypoints = ["http"] - backend = "backend1" - [frontends.frontend1.routes.test_1] - rule = "Path:/" - [frontends.frontend1.ratelimit] - extractorfunc = "client.ip" - [frontends.frontend1.ratelimit.rateset.rateset1] - period = "10s" - average = 100 - burst = 200 - [frontends.frontend1.ratelimit.rateset.rateset2] - period = "3s" - average = 5 - burst = 10 -``` - -In the above example, frontend1 is configured to limit requests by the client's ip address. -An average of 5 requests every 3 seconds is allowed and an average of 100 requests every 10 seconds. -These can "burst" up to 10 and 200 in each period respectively. - ### Backends A backend is responsible to load-balance the traffic coming from one or more frontends to a set of http servers. diff --git a/docs/configuration/commons.md b/docs/configuration/commons.md index 69e167e86..0f97cc355 100644 --- a/docs/configuration/commons.md +++ b/docs/configuration/commons.md @@ -277,6 +277,36 @@ Custom error pages are easiest to implement using the file provider. For dynamic providers, the corresponding template file needs to be customized accordingly and referenced in the Traefik configuration. +## Rate limiting + +Rate limiting can be configured per frontend. +Multiple sets of rates can be added to each frontend, but the time periods must be unique. + +```toml +[frontends] + [frontends.frontend1] + passHostHeader = true + entrypoints = ["http"] + backend = "backend1" + [frontends.frontend1.routes.test_1] + rule = "Path:/" + [frontends.frontend1.ratelimit] + extractorfunc = "client.ip" + [frontends.frontend1.ratelimit.rateset.rateset1] + period = "10s" + average = 100 + burst = 200 + [frontends.frontend1.ratelimit.rateset.rateset2] + period = "3s" + average = 5 + burst = 10 +``` + +In the above example, frontend1 is configured to limit requests by the client's ip address. +An average of 5 requests every 3 seconds is allowed and an average of 100 requests every 10 seconds. +These can "burst" up to 10 and 200 in each period respectively. + + ## Retry Configuration ```toml From 3142a4f4b3af4444d3270c648707489f66a98049 Mon Sep 17 00:00:00 2001 From: lishaoxiong Date: Tue, 19 Dec 2017 21:08:03 +0800 Subject: [PATCH 04/10] Fix stickiness bug due to template syntax error --- autogen/gentemplates/gen.go | 2 +- templates/kv.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/autogen/gentemplates/gen.go b/autogen/gentemplates/gen.go index 35c00fae6..a8e76b5a3 100644 --- a/autogen/gentemplates/gen.go +++ b/autogen/gentemplates/gen.go @@ -532,7 +532,7 @@ var _templatesKvTmpl = []byte(`{{$frontends := List .Prefix "/frontends/" }} sticky = {{ getSticky . }} {{if hasStickinessLabel $backend}} [backends."{{$backendName}}".loadBalancer.stickiness] - cookieName = {{getStickinessCookieName $backend}} + cookieName = "{{getStickinessCookieName $backend}}" {{end}} {{end}} diff --git a/templates/kv.tmpl b/templates/kv.tmpl index 09b04b683..08d353016 100644 --- a/templates/kv.tmpl +++ b/templates/kv.tmpl @@ -20,7 +20,7 @@ sticky = {{ getSticky . }} {{if hasStickinessLabel $backend}} [backends."{{$backendName}}".loadBalancer.stickiness] - cookieName = {{getStickinessCookieName $backend}} + cookieName = "{{getStickinessCookieName $backend}}" {{end}} {{end}} From 877770f7cf9526c739ab5749bd1bb0206edb6e66 Mon Sep 17 00:00:00 2001 From: Timo Reimann Date: Tue, 19 Dec 2017 16:00:09 +0100 Subject: [PATCH 05/10] Update go-marathon --- glide.lock | 2 +- .../gambol99/go-marathon/application.go | 76 ++++++++++--- .../go-marathon/application_marshalling.go | 106 ++++++++++++++++++ .../github.com/gambol99/go-marathon/client.go | 76 ++++++++++--- .../gambol99/go-marathon/cluster.go | 23 ++-- .../github.com/gambol99/go-marathon/config.go | 6 +- .../github.com/gambol99/go-marathon/const.go | 2 +- .../gambol99/go-marathon/deployment.go | 2 +- .../github.com/gambol99/go-marathon/docker.go | 84 +++++++++++++- .../github.com/gambol99/go-marathon/error.go | 2 +- .../github.com/gambol99/go-marathon/events.go | 2 +- .../github.com/gambol99/go-marathon/group.go | 4 +- .../github.com/gambol99/go-marathon/health.go | 14 +-- .../github.com/gambol99/go-marathon/info.go | 2 +- .../gambol99/go-marathon/last_task_failure.go | 2 + .../gambol99/go-marathon/port_definition.go | 30 ++++- .../github.com/gambol99/go-marathon/queue.go | 8 +- .../gambol99/go-marathon/readiness.go | 2 +- .../gambol99/go-marathon/residency.go | 48 ++++++++ .../gambol99/go-marathon/subscription.go | 38 ++++--- .../github.com/gambol99/go-marathon/task.go | 4 +- .../go-marathon/unreachable_strategy.go | 6 +- .../gambol99/go-marathon/upgrade_strategy.go | 6 +- .../github.com/gambol99/go-marathon/utils.go | 2 +- 24 files changed, 450 insertions(+), 97 deletions(-) create mode 100644 vendor/github.com/gambol99/go-marathon/application_marshalling.go create mode 100644 vendor/github.com/gambol99/go-marathon/residency.go diff --git a/glide.lock b/glide.lock index edd61a69a..7f49501d5 100644 --- a/glide.lock +++ b/glide.lock @@ -261,7 +261,7 @@ imports: - name: github.com/fatih/color version: 62e9147c64a1ed519147b62a56a14e83e2be02c1 - name: github.com/gambol99/go-marathon - version: dd6cbd4c2d71294a19fb89158f2a00d427f174ab + version: 03b46169666c53b9cc953b875ac5714e5103e064 - name: github.com/ghodss/yaml version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee - name: github.com/go-ini/ini diff --git a/vendor/github.com/gambol99/go-marathon/application.go b/vendor/github.com/gambol99/go-marathon/application.go index aba8dc77c..fbb6dc1e6 100644 --- a/vendor/github.com/gambol99/go-marathon/application.go +++ b/vendor/github.com/gambol99/go-marathon/application.go @@ -1,5 +1,5 @@ /* -Copyright 2014 Rohith All rights reserved. +Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -56,15 +56,16 @@ type Port struct { // Application is the definition for an application in marathon type Application struct { - ID string `json:"id,omitempty"` - Cmd *string `json:"cmd,omitempty"` - Args *[]string `json:"args,omitempty"` - Constraints *[][]string `json:"constraints,omitempty"` - Container *Container `json:"container,omitempty"` - CPUs float64 `json:"cpus,omitempty"` - GPUs *float64 `json:"gpus,omitempty"` - Disk *float64 `json:"disk,omitempty"` - Env *map[string]string `json:"env,omitempty"` + ID string `json:"id,omitempty"` + Cmd *string `json:"cmd,omitempty"` + Args *[]string `json:"args,omitempty"` + Constraints *[][]string `json:"constraints,omitempty"` + Container *Container `json:"container,omitempty"` + CPUs float64 `json:"cpus,omitempty"` + GPUs *float64 `json:"gpus,omitempty"` + Disk *float64 `json:"disk,omitempty"` + // Contains non-secret environment variables. Secrets environment variables are part of the Secrets map. + Env *map[string]string `json:"-"` Executor *string `json:"executor,omitempty"` HealthChecks *[]HealthCheck `json:"healthChecks,omitempty"` ReadinessChecks *[]ReadinessCheck `json:"readinessChecks,omitempty"` @@ -99,6 +100,8 @@ type Application struct { LastTaskFailure *LastTaskFailure `json:"lastTaskFailure,omitempty"` Fetch *[]Fetch `json:"fetch,omitempty"` IPAddressPerTask *IPAddressPerTask `json:"ipAddress,omitempty"` + Residency *Residency `json:"residency,omitempty"` + Secrets *map[string]Secret `json:"-"` } // ApplicationVersions is a collection of application versions for a specific app in marathon @@ -149,6 +152,14 @@ type Stats struct { LifeTime map[string]float64 `json:"lifeTime"` } +// Secret is the environment variable and secret store path associated with a secret. +// The value for EnvVar is populated from the env field, and Source is populated from +// the secrets field of the application json. +type Secret struct { + EnvVar string + Source string +} + // SetIPAddressPerTask defines that the application will have a IP address defines by a external agent. // This configuration is not allowed to be used with Port or PortDefinitions. Thus, the implementation // clears both. @@ -355,8 +366,8 @@ func (r *Application) EmptyLabels() *Application { } // AddEnv adds an environment variable to the application -// name: the name of the variable -// value: go figure, the value associated to the above +// name: the name of the variable +// value: go figure, the value associated to the above func (r *Application) AddEnv(name, value string) *Application { if r.Env == nil { r.EmptyEnvs() @@ -375,6 +386,28 @@ func (r *Application) EmptyEnvs() *Application { return r } +// AddSecret adds a secret declaration +// envVar: the name of the environment variable +// name: the name of the secret +// source: the source ID of the secret +func (r *Application) AddSecret(envVar, name, source string) *Application { + if r.Secrets == nil { + r.EmptySecrets() + } + (*r.Secrets)[name] = Secret{EnvVar: envVar, Source: source} + + return r +} + +// EmptySecrets explicitly empties the secrets -- use this if you need to empty +// the secrets of an application that already has secrets set (setting secrets to nil will +// keep the current value) +func (r *Application) EmptySecrets() *Application { + r.Secrets = &map[string]Secret{} + + return r +} + // SetExecutor sets the executor func (r *Application) SetExecutor(executor string) *Application { r.Executor = &executor @@ -571,6 +604,23 @@ func (r *Application) EmptyUnreachableStrategy() *Application { return r } +// SetResidency sets behavior for resident applications, an application is resident when +// it has local persistent volumes set +func (r *Application) SetResidency(whenLost TaskLostBehaviorType) *Application { + r.Residency = &Residency{ + TaskLostBehavior: whenLost, + } + return r +} + +// EmptyResidency explicitly empties the residency -- use this if +// you need to empty the residency of an application that already has +// the residency set (setting it to nil will keep the current value). +func (r *Application) EmptyResidency() *Application { + r.Residency = &Residency{} + return r +} + // String returns the json representation of this application func (r *Application) String() string { s, err := json.MarshalIndent(r, "", " ") @@ -639,7 +689,7 @@ func (r *marathonClient) ApplicationVersions(name string) (*ApplicationVersions, // name: the id used to identify the application // version: the version (normally a timestamp) you wish to change to func (r *marathonClient) SetApplicationVersion(name string, version *ApplicationVersion) (*DeploymentID, error) { - path := fmt.Sprintf(buildPath(name)) + path := buildPath(name) deploymentID := new(DeploymentID) if err := r.apiPut(path, version, deploymentID); err != nil { return nil, err diff --git a/vendor/github.com/gambol99/go-marathon/application_marshalling.go b/vendor/github.com/gambol99/go-marathon/application_marshalling.go new file mode 100644 index 000000000..c92b9ca01 --- /dev/null +++ b/vendor/github.com/gambol99/go-marathon/application_marshalling.go @@ -0,0 +1,106 @@ +/* +Copyright 2017 The go-marathon Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package marathon + +import ( + "encoding/json" + "fmt" +) + +// Alias aliases the Application struct so that it will be marshaled/unmarshaled automatically +type Alias Application + +// TmpEnvSecret holds the secret values deserialized from the environment variables field +type TmpEnvSecret struct { + Secret string `json:"secret,omitempty"` +} + +// TmpSecret holds the deserialized secrets field in a Marathon application configuration +type TmpSecret struct { + Source string `json:"source,omitempty"` +} + +// UnmarshalJSON unmarshals the given Application JSON as expected except for environment variables and secrets. +// Environment varialbes are stored in the Env field. Secrets, including the environment variable part, +// are stored in the Secrets field. +func (app *Application) UnmarshalJSON(b []byte) error { + aux := &struct { + *Alias + Env map[string]interface{} `json:"env"` + Secrets map[string]TmpSecret `json:"secrets"` + }{ + Alias: (*Alias)(app), + } + if err := json.Unmarshal(b, aux); err != nil { + return fmt.Errorf("malformed application definition %v", err) + } + env := &map[string]string{} + secrets := &map[string]Secret{} + + for envName, genericEnvValue := range aux.Env { + switch envValOrSecret := genericEnvValue.(type) { + case string: + (*env)[envName] = envValOrSecret + case map[string]interface{}: + for secret, secretStore := range envValOrSecret { + if secStore, ok := secretStore.(string); ok && secret == "secret" { + (*secrets)[secStore] = Secret{EnvVar: envName} + break + } + return fmt.Errorf("unexpected secret field %v or value type %T", secret, envValOrSecret[secret]) + } + default: + return fmt.Errorf("unexpected environment variable type %T", envValOrSecret) + } + } + app.Env = env + for k, v := range aux.Secrets { + tmp := (*secrets)[k] + tmp.Source = v.Source + (*secrets)[k] = tmp + } + app.Secrets = secrets + return nil +} + +// MarshalJSON marshals the given Application as expected except for environment variables and secrets, +// which are marshaled from specialized structs. The environment variable piece of the secrets and other +// normal environment variables are combined and marshaled to the env field. The secrets and the related +// source are marshaled into the secrets field. +func (app *Application) MarshalJSON() ([]byte, error) { + env := make(map[string]interface{}) + secrets := make(map[string]TmpSecret) + + if app.Env != nil { + for k, v := range *app.Env { + env[string(k)] = string(v) + } + } + if app.Secrets != nil { + for k, v := range *app.Secrets { + env[v.EnvVar] = TmpEnvSecret{Secret: k} + secrets[k] = TmpSecret{v.Source} + } + } + aux := &struct { + *Alias + Env map[string]interface{} `json:"env,omitempty"` + Secrets map[string]TmpSecret `json:"secrets,omitempty"` + }{Alias: (*Alias)(app), Env: env, Secrets: secrets} + + return json.Marshal(aux) +} diff --git a/vendor/github.com/gambol99/go-marathon/client.go b/vendor/github.com/gambol99/go-marathon/client.go index a042c560f..cc75c3d3e 100644 --- a/vendor/github.com/gambol99/go-marathon/client.go +++ b/vendor/github.com/gambol99/go-marathon/client.go @@ -1,5 +1,5 @@ /* -Copyright 2014 Rohith All rights reserved. +Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import ( "io" "io/ioutil" "log" + "net" "net/http" "net/url" "regexp" @@ -154,6 +155,24 @@ var ( ErrMarathonDown = errors.New("all the Marathon hosts are presently down") // ErrTimeoutError is thrown when the operation has timed out ErrTimeoutError = errors.New("the operation has timed out") + + // Default HTTP client used for SSE subscription requests + // It is invalid to set client.Timeout because it includes time to read response so + // set dial, tls handshake and response header timeouts instead + defaultHTTPSSEClient = &http.Client{ + Transport: &http.Transport{ + Dial: (&net.Dialer{ + Timeout: 5 * time.Second, + }).Dial, + ResponseHeaderTimeout: 10 * time.Second, + TLSHandshakeTimeout: 5 * time.Second, + }, + } + + // Default HTTP client used for non SSE requests + defaultHTTPClient = &http.Client{ + Timeout: 10 * time.Second, + } ) // EventsChannelContext holds contextual data for an EventsChannel. @@ -177,8 +196,8 @@ type marathonClient struct { hosts *cluster // a map of service you wish to listen to listeners map[EventsChannel]EventsChannelContext - // a custom logger for debug log messages - debugLog *log.Logger + // a custom log function for debug messages + debugLog func(format string, v ...interface{}) // the marathon HTTP client to ensure consistency in requests client *httpClient } @@ -196,9 +215,18 @@ type newRequestError struct { // NewClient creates a new marathon client // config: the configuration to use func NewClient(config Config) (Marathon, error) { - // step: if no http client, set to default + // step: if the SSE HTTP client is missing, prefer a configured regular + // client, and otherwise use the default SSE HTTP client. + if config.HTTPSSEClient == nil { + config.HTTPSSEClient = defaultHTTPSSEClient + if config.HTTPClient != nil { + config.HTTPSSEClient = config.HTTPClient + } + } + + // step: if a regular HTTP client is missing, use the default one. if config.HTTPClient == nil { - config.HTTPClient = http.DefaultClient + config.HTTPClient = defaultHTTPClient } // step: if no polling wait time is set, default to 500 milliseconds. @@ -215,16 +243,19 @@ func NewClient(config Config) (Marathon, error) { return nil, err } - debugLogOutput := config.LogOutput - if debugLogOutput == nil { - debugLogOutput = ioutil.Discard + debugLog := func(string, ...interface{}) {} + if config.LogOutput != nil { + logger := log.New(config.LogOutput, "", 0) + debugLog = func(format string, v ...interface{}) { + logger.Printf(format, v...) + } } return &marathonClient{ config: config, listeners: make(map[EventsChannel]EventsChannelContext), hosts: hosts, - debugLog: log.New(debugLogOutput, "", 0), + debugLog: debugLog, client: client, }, nil } @@ -280,7 +311,7 @@ func (r *marathonClient) apiCall(method, path string, body, result interface{}) if err != nil { r.hosts.markDown(member) // step: attempt the request on another member - r.debugLog.Printf("apiCall(): request failed on host: %s, error: %s, trying another\n", member, err) + r.debugLog("apiCall(): request failed on host: %s, error: %s, trying another", member, err) continue } defer response.Body.Close() @@ -292,9 +323,9 @@ func (r *marathonClient) apiCall(method, path string, body, result interface{}) } if len(requestBody) > 0 { - r.debugLog.Printf("apiCall(): %v %v %s returned %v %s\n", request.Method, request.URL.String(), requestBody, response.Status, oneLogLine(respBody)) + r.debugLog("apiCall(): %v %v %s returned %v %s", request.Method, request.URL.String(), requestBody, response.Status, oneLogLine(respBody)) } else { - r.debugLog.Printf("apiCall(): %v %v returned %v %s\n", request.Method, request.URL.String(), response.Status, oneLogLine(respBody)) + r.debugLog("apiCall(): %v %v returned %v %s", request.Method, request.URL.String(), response.Status, oneLogLine(respBody)) } // step: check for a successfull response @@ -311,7 +342,7 @@ func (r *marathonClient) apiCall(method, path string, body, result interface{}) if response.StatusCode >= 500 && response.StatusCode <= 599 { // step: mark the host as down r.hosts.markDown(member) - r.debugLog.Printf("apiCall(): request failed, host: %s, status: %d, trying another\n", member, response.StatusCode) + r.debugLog("apiCall(): request failed, host: %s, status: %d, trying another", member, response.StatusCode) continue } @@ -329,16 +360,28 @@ func (r *marathonClient) buildAPIRequest(method, path string, reader io.Reader) } // Build the HTTP request to Marathon - request, err = r.client.buildMarathonRequest(method, member, path, reader) + request, err = r.client.buildMarathonJSONRequest(method, member, path, reader) if err != nil { return nil, member, newRequestError{err} } return request, member, nil } +// buildMarathonJSONRequest is like buildMarathonRequest but sets the +// Content-Type and Accept headers to application/json. +func (rc *httpClient) buildMarathonJSONRequest(method, member, path string, reader io.Reader) (request *http.Request, err error) { + req, err := rc.buildMarathonRequest(method, member, path, reader) + if err == nil { + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + } + + return req, err +} + // buildMarathonRequest creates a new HTTP request and configures it according to the *httpClient configuration. // The path must not contain a leading "/", otherwise buildMarathonRequest will panic. -func (rc *httpClient) buildMarathonRequest(method string, member string, path string, reader io.Reader) (request *http.Request, err error) { +func (rc *httpClient) buildMarathonRequest(method, member, path string, reader io.Reader) (request *http.Request, err error) { if strings.HasPrefix(path, "/") { panic(fmt.Sprintf("Path '%s' must not start with a leading slash", path)) } @@ -361,9 +404,6 @@ func (rc *httpClient) buildMarathonRequest(method string, member string, path st request.Header.Add("Authorization", "token="+rc.config.DCOSToken) } - request.Header.Add("Content-Type", "application/json") - request.Header.Add("Accept", "application/json") - return request, nil } diff --git a/vendor/github.com/gambol99/go-marathon/cluster.go b/vendor/github.com/gambol99/go-marathon/cluster.go index a97a22c53..3ca99b2c4 100644 --- a/vendor/github.com/gambol99/go-marathon/cluster.go +++ b/vendor/github.com/gambol99/go-marathon/cluster.go @@ -1,5 +1,5 @@ /* -Copyright 2014 Rohith All rights reserved. +Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -39,6 +39,9 @@ type cluster struct { members []*member // the marathon HTTP client to ensure consistency in requests client *httpClient + // healthCheckInterval is the interval by which we probe down nodes for + // availability again. + healthCheckInterval time.Duration } // member represents an individual endpoint @@ -94,8 +97,9 @@ func newCluster(client *httpClient, marathonURL string, isDCOS bool) (*cluster, } return &cluster{ - client: client, - members: members, + client: client, + members: members, + healthCheckInterval: 5 * time.Second, }, nil } @@ -130,20 +134,21 @@ func (c *cluster) markDown(endpoint string) { // healthCheckNode performs a health check on the node and when active updates the status func (c *cluster) healthCheckNode(node *member) { // step: wait for the node to become active ... we are assuming a /ping is enough here - for { + ticker := time.NewTicker(c.healthCheckInterval) + defer ticker.Stop() + for range ticker.C { req, err := c.client.buildMarathonRequest("GET", node.endpoint, "ping", nil) if err == nil { res, err := c.client.Do(req) if err == nil && res.StatusCode == 200 { + // step: mark the node as active again + c.Lock() + node.status = memberStatusUp + c.Unlock() break } } - <-time.After(time.Duration(5 * time.Second)) } - // step: mark the node as active again - c.Lock() - defer c.Unlock() - node.status = memberStatusUp } // activeMembers returns a list of active members diff --git a/vendor/github.com/gambol99/go-marathon/config.go b/vendor/github.com/gambol99/go-marathon/config.go index 67bba0982..2e110cc81 100644 --- a/vendor/github.com/gambol99/go-marathon/config.go +++ b/vendor/github.com/gambol99/go-marathon/config.go @@ -1,5 +1,5 @@ /* -Copyright 2014 Rohith All rights reserved. +Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -50,8 +50,10 @@ type Config struct { DCOSToken string // LogOutput the output for debug log messages LogOutput io.Writer - // HTTPClient is the http client + // HTTPClient is the HTTP client HTTPClient *http.Client + // HTTPSSEClient is the HTTP client used for SSE subscriptions, can't have client.Timeout set + HTTPSSEClient *http.Client // wait time (in milliseconds) between repetitive requests to the API during polling PollingWaitTime time.Duration } diff --git a/vendor/github.com/gambol99/go-marathon/const.go b/vendor/github.com/gambol99/go-marathon/const.go index 43b1d46a9..8b70c5acb 100644 --- a/vendor/github.com/gambol99/go-marathon/const.go +++ b/vendor/github.com/gambol99/go-marathon/const.go @@ -1,5 +1,5 @@ /* -Copyright 2014 Rohith All rights reserved. +Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vendor/github.com/gambol99/go-marathon/deployment.go b/vendor/github.com/gambol99/go-marathon/deployment.go index 7d57f1758..f83821903 100644 --- a/vendor/github.com/gambol99/go-marathon/deployment.go +++ b/vendor/github.com/gambol99/go-marathon/deployment.go @@ -1,5 +1,5 @@ /* -Copyright 2014 Rohith All rights reserved. +Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vendor/github.com/gambol99/go-marathon/docker.go b/vendor/github.com/gambol99/go-marathon/docker.go index 550409a3c..217d3bbbe 100644 --- a/vendor/github.com/gambol99/go-marathon/docker.go +++ b/vendor/github.com/gambol99/go-marathon/docker.go @@ -1,5 +1,5 @@ /* -Copyright 2014 Rohith All rights reserved. +Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -46,10 +46,71 @@ type Parameters struct { // Volume is the docker volume details associated to the container type Volume struct { - ContainerPath string `json:"containerPath,omitempty"` - HostPath string `json:"hostPath,omitempty"` - External *ExternalVolume `json:"external,omitempty"` - Mode string `json:"mode,omitempty"` + ContainerPath string `json:"containerPath,omitempty"` + HostPath string `json:"hostPath,omitempty"` + External *ExternalVolume `json:"external,omitempty"` + Mode string `json:"mode,omitempty"` + Persistent *PersistentVolume `json:"persistent,omitempty"` +} + +type PersistentVolumeType string + +const ( + PersistentVolumeTypeRoot PersistentVolumeType = "root" + PersistentVolumeTypePath PersistentVolumeType = "path" + PersistentVolumeTypeMount PersistentVolumeType = "mount" +) + +// PersistentVolume declares a Volume to be Persistent, and sets +// the size (in MiB) and optional type, max size (MiB) and constraints for the Volume. +type PersistentVolume struct { + Type PersistentVolumeType `json:"type,omitempty"` + Size int `json:"size"` + MaxSize int `json:"maxSize,omitempty"` + Constraints *[][]string `json:"constraints,omitempty"` +} + +// SetType sets the type of mesos disk resource to use +// type: PersistentVolumeType enum +func (p *PersistentVolume) SetType(tp PersistentVolumeType) *PersistentVolume { + p.Type = tp + return p +} + +// SetSize sets size of the persistent volume +// size: size in MiB +func (p *PersistentVolume) SetSize(size int) *PersistentVolume { + p.Size = size + return p +} + +// SetMaxSize sets maximum size of an exclusive mount-disk resource to consider; +// does not apply to root or path disk resource types +// maxSize: size in MiB +func (p *PersistentVolume) SetMaxSize(maxSize int) *PersistentVolume { + p.MaxSize = maxSize + return p +} + +// AddConstraint adds a new constraint +// constraints: the constraint definition, one constraint per array element +func (p *PersistentVolume) AddConstraint(constraints ...string) *PersistentVolume { + if p.Constraints == nil { + p.EmptyConstraints() + } + + c := *p.Constraints + c = append(c, constraints) + p.Constraints = &c + return p +} + +// EmptyConstraints explicitly empties constraints -- use this if you need to empty +// constraints of an application that already has constraints set (setting constraints to nil will +// keep the current value) +func (p *PersistentVolume) EmptyConstraints() *PersistentVolume { + p.Constraints = &[][]string{} + return p } // ExternalVolume is an external volume definition @@ -98,6 +159,19 @@ func (container *Container) EmptyVolumes() *Container { return container } +// SetPersistentVolume defines persistent properties for volume +func (v *Volume) SetPersistentVolume() *PersistentVolume { + ev := &PersistentVolume{} + v.Persistent = ev + return ev +} + +// EmptyPersistentVolume empties the persistent volume definition +func (v *Volume) EmptyPersistentVolume() *Volume { + v.Persistent = &PersistentVolume{} + return v +} + // SetExternalVolume define external elements for a volume // name: the name of the volume // provider: the provider of the volume (e.g. dvdi) diff --git a/vendor/github.com/gambol99/go-marathon/error.go b/vendor/github.com/gambol99/go-marathon/error.go index 21e731146..09d7dae49 100644 --- a/vendor/github.com/gambol99/go-marathon/error.go +++ b/vendor/github.com/gambol99/go-marathon/error.go @@ -1,5 +1,5 @@ /* -Copyright 2015 Rohith All rights reserved. +Copyright 2015 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vendor/github.com/gambol99/go-marathon/events.go b/vendor/github.com/gambol99/go-marathon/events.go index f97df9084..5814cad29 100644 --- a/vendor/github.com/gambol99/go-marathon/events.go +++ b/vendor/github.com/gambol99/go-marathon/events.go @@ -1,5 +1,5 @@ /* -Copyright 2014 Rohith All rights reserved. +Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vendor/github.com/gambol99/go-marathon/group.go b/vendor/github.com/gambol99/go-marathon/group.go index 401916e3f..63ce310bb 100644 --- a/vendor/github.com/gambol99/go-marathon/group.go +++ b/vendor/github.com/gambol99/go-marathon/group.go @@ -1,5 +1,5 @@ /* -Copyright 2014 Rohith All rights reserved. +Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -136,7 +136,7 @@ func (r *marathonClient) GroupBy(name string, opts *GetGroupOpts) (*Group, error // name: the identifier for the group func (r *marathonClient) HasGroup(name string) (bool, error) { path := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)) - err := r.apiCall("GET", path, "", nil) + err := r.apiGet(path, "", nil) if err != nil { if apiErr, ok := err.(*APIError); ok && apiErr.ErrCode == ErrCodeNotFound { return false, nil diff --git a/vendor/github.com/gambol99/go-marathon/health.go b/vendor/github.com/gambol99/go-marathon/health.go index 11c68e64d..b46d94aad 100644 --- a/vendor/github.com/gambol99/go-marathon/health.go +++ b/vendor/github.com/gambol99/go-marathon/health.go @@ -1,5 +1,5 @@ /* -Copyright 2014 Rohith All rights reserved. +Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -31,37 +31,37 @@ type HealthCheck struct { } // SetCommand sets the given command on the health check. -func (h HealthCheck) SetCommand(c Command) HealthCheck { +func (h *HealthCheck) SetCommand(c Command) *HealthCheck { h.Command = &c return h } // SetPortIndex sets the given port index on the health check. -func (h HealthCheck) SetPortIndex(i int) HealthCheck { +func (h *HealthCheck) SetPortIndex(i int) *HealthCheck { h.PortIndex = &i return h } // SetPort sets the given port on the health check. -func (h HealthCheck) SetPort(i int) HealthCheck { +func (h *HealthCheck) SetPort(i int) *HealthCheck { h.Port = &i return h } // SetPath sets the given path on the health check. -func (h HealthCheck) SetPath(p string) HealthCheck { +func (h *HealthCheck) SetPath(p string) *HealthCheck { h.Path = &p return h } // SetMaxConsecutiveFailures sets the maximum consecutive failures on the health check. -func (h HealthCheck) SetMaxConsecutiveFailures(i int) HealthCheck { +func (h *HealthCheck) SetMaxConsecutiveFailures(i int) *HealthCheck { h.MaxConsecutiveFailures = &i return h } // SetIgnoreHTTP1xx sets ignore http 1xx on the health check. -func (h HealthCheck) SetIgnoreHTTP1xx(ignore bool) HealthCheck { +func (h *HealthCheck) SetIgnoreHTTP1xx(ignore bool) *HealthCheck { h.IgnoreHTTP1xx = &ignore return h } diff --git a/vendor/github.com/gambol99/go-marathon/info.go b/vendor/github.com/gambol99/go-marathon/info.go index e38cc9ef8..45f5d6807 100644 --- a/vendor/github.com/gambol99/go-marathon/info.go +++ b/vendor/github.com/gambol99/go-marathon/info.go @@ -1,5 +1,5 @@ /* -Copyright 2014 Rohith All rights reserved. +Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vendor/github.com/gambol99/go-marathon/last_task_failure.go b/vendor/github.com/gambol99/go-marathon/last_task_failure.go index 1870f2868..357deee52 100644 --- a/vendor/github.com/gambol99/go-marathon/last_task_failure.go +++ b/vendor/github.com/gambol99/go-marathon/last_task_failure.go @@ -1,4 +1,5 @@ /* +Copyright 2015 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,6 +21,7 @@ type LastTaskFailure struct { AppID string `json:"appId,omitempty"` Host string `json:"host,omitempty"` Message string `json:"message,omitempty"` + SlaveID string `json:"slaveId,omitempty"` State string `json:"state,omitempty"` TaskID string `json:"taskId,omitempty"` Timestamp string `json:"timestamp,omitempty"` diff --git a/vendor/github.com/gambol99/go-marathon/port_definition.go b/vendor/github.com/gambol99/go-marathon/port_definition.go index 4cf3c1fb7..6a5dc6d95 100644 --- a/vendor/github.com/gambol99/go-marathon/port_definition.go +++ b/vendor/github.com/gambol99/go-marathon/port_definition.go @@ -1,5 +1,5 @@ /* -Copyright 2016 Rohith All rights reserved. +Copyright 2016 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -27,15 +27,39 @@ type PortDefinition struct { } // SetPort sets the given port for the PortDefinition -func (p PortDefinition) SetPort(port int) PortDefinition { +func (p *PortDefinition) SetPort(port int) *PortDefinition { + if p.Port == nil { + p.EmptyPort() + } p.Port = &port return p } +// EmptyPort sets the port to 0 for the PortDefinition +func (p *PortDefinition) EmptyPort() *PortDefinition { + port := 0 + p.Port = &port + return p +} + +// SetProtocol sets the protocol for the PortDefinition +// protocol: the protocol as a string +func (p *PortDefinition) SetProtocol(protocol string) *PortDefinition { + p.Protocol = protocol + return p +} + +// SetName sets the name for the PortDefinition +// name: the name of the PortDefinition +func (p *PortDefinition) SetName(name string) *PortDefinition { + p.Name = name + return p +} + // AddLabel adds a label to the PortDefinition // name: the name of the label // value: value for this label -func (p PortDefinition) AddLabel(name, value string) PortDefinition { +func (p *PortDefinition) AddLabel(name, value string) *PortDefinition { if p.Labels == nil { p.EmptyLabels() } diff --git a/vendor/github.com/gambol99/go-marathon/queue.go b/vendor/github.com/gambol99/go-marathon/queue.go index 436489377..2eaede34f 100644 --- a/vendor/github.com/gambol99/go-marathon/queue.go +++ b/vendor/github.com/gambol99/go-marathon/queue.go @@ -1,5 +1,5 @@ /* -Copyright 2016 Rohith All rights reserved. +Copyright 2016 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -52,9 +52,5 @@ func (r *marathonClient) Queue() (*Queue, error) { // appID: the ID of the application func (r *marathonClient) DeleteQueueDelay(appID string) error { path := fmt.Sprintf("%s/%s/delay", marathonAPIQueue, trimRootPath(appID)) - err := r.apiDelete(path, nil, nil) - if err != nil { - return err - } - return nil + return r.apiDelete(path, nil, nil) } diff --git a/vendor/github.com/gambol99/go-marathon/readiness.go b/vendor/github.com/gambol99/go-marathon/readiness.go index c1887c3c3..ffb0aa149 100644 --- a/vendor/github.com/gambol99/go-marathon/readiness.go +++ b/vendor/github.com/gambol99/go-marathon/readiness.go @@ -1,5 +1,5 @@ /* -Copyright 2017 Rohith All rights reserved. +Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vendor/github.com/gambol99/go-marathon/residency.go b/vendor/github.com/gambol99/go-marathon/residency.go new file mode 100644 index 000000000..ea9d72d6c --- /dev/null +++ b/vendor/github.com/gambol99/go-marathon/residency.go @@ -0,0 +1,48 @@ +/* +Copyright 2017 The go-marathon Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package marathon + +import "time" + +// TaskLostBehaviorType sets action taken when the resident task is lost +type TaskLostBehaviorType string + +const ( + // TaskLostBehaviorTypeWaitForever indicates to not take any action when the resident task is lost + TaskLostBehaviorTypeWaitForever TaskLostBehaviorType = "WAIT_FOREVER" + // TaskLostBehaviorTypeWaitForever indicates to try relaunching the lost resident task on + // another node after the relaunch escalation timeout has elapsed + TaskLostBehaviorTypeRelaunchAfterTimeout TaskLostBehaviorType = "RELAUNCH_AFTER_TIMEOUT" +) + +// Residency defines how terminal states of tasks with local persistent volumes are handled +type Residency struct { + TaskLostBehavior TaskLostBehaviorType `json:"taskLostBehavior,omitempty"` + RelaunchEscalationTimeoutSeconds int `json:"relaunchEscalationTimeoutSeconds,omitempty"` +} + +// SetTaskLostBehavior sets the residency behavior +func (r *Residency) SetTaskLostBehavior(behavior TaskLostBehaviorType) *Residency { + r.TaskLostBehavior = behavior + return r +} + +// SetRelaunchEscalationTimeout sets the residency relaunch escalation timeout with seconds precision +func (r *Residency) SetRelaunchEscalationTimeout(timeout time.Duration) *Residency { + r.RelaunchEscalationTimeoutSeconds = int(timeout.Seconds()) + return r +} diff --git a/vendor/github.com/gambol99/go-marathon/subscription.go b/vendor/github.com/gambol99/go-marathon/subscription.go index fa70b1488..a9f75c664 100644 --- a/vendor/github.com/gambol99/go-marathon/subscription.go +++ b/vendor/github.com/gambol99/go-marathon/subscription.go @@ -1,5 +1,5 @@ /* -Copyright 2014 Rohith All rights reserved. +Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -103,8 +103,7 @@ func (r *marathonClient) registerSubscription() error { case EventsTransportCallback: return r.registerCallbackSubscription() case EventsTransportSSE: - r.registerSSESubscription() - return nil + return r.registerSSESubscription() default: return fmt.Errorf("the events transport: %d is not supported", r.config.EventsTransport) } @@ -167,27 +166,34 @@ func (r *marathonClient) registerCallbackSubscription() error { // connect to the SSE stream and to process the received events. To establish // the connection it tries the active cluster members until no more member is // active. When this happens it will retry to get a connection every 5 seconds. -func (r *marathonClient) registerSSESubscription() { +func (r *marathonClient) registerSSESubscription() error { if r.subscribedToSSE { - return + return nil + } + + if r.config.HTTPSSEClient.Timeout != 0 { + return fmt.Errorf( + "global timeout must not be set for SSE connections (found %s) -- remove global timeout from HTTP client or provide separate SSE HTTP client without global timeout", + r.config.HTTPSSEClient.Timeout, + ) } go func() { for { stream, err := r.connectToSSE() if err != nil { - r.debugLog.Printf("Error connecting SSE subscription: %s", err) + r.debugLog("Error connecting SSE subscription: %s", err) <-time.After(5 * time.Second) continue } - err = r.listenToSSE(stream) stream.Close() - r.debugLog.Printf("Error on SSE subscription: %s", err) + r.debugLog("Error on SSE subscription: %s", err) } }() r.subscribedToSSE = true + return nil } // connectToSSE tries to establish an *eventsource.Stream to any of the Marathon cluster members, marking the @@ -209,15 +215,15 @@ func (r *marathonClient) connectToSSE() (*eventsource.Stream, error) { // its underlying fields for performance reasons. See note that at least the Transport // should be reused here: https://golang.org/pkg/net/http/#Client httpClient := &http.Client{ - Transport: r.config.HTTPClient.Transport, - CheckRedirect: r.config.HTTPClient.CheckRedirect, - Jar: r.config.HTTPClient.Jar, - Timeout: r.config.HTTPClient.Timeout, + Transport: r.config.HTTPSSEClient.Transport, + CheckRedirect: r.config.HTTPSSEClient.CheckRedirect, + Jar: r.config.HTTPSSEClient.Jar, + Timeout: r.config.HTTPSSEClient.Timeout, } stream, err := eventsource.SubscribeWith("", httpClient, request) if err != nil { - r.debugLog.Printf("Error subscribing to Marathon event stream: %s", err) + r.debugLog("Error subscribing to Marathon event stream: %s", err) r.hosts.markDown(member) continue } @@ -231,7 +237,7 @@ func (r *marathonClient) listenToSSE(stream *eventsource.Stream) error { select { case ev := <-stream.Events: if err := r.handleEvent(ev.Data()); err != nil { - r.debugLog.Printf("listenToSSE(): failed to handle event: %v", err) + r.debugLog("listenToSSE(): failed to handle event: %v", err) } case err := <-stream.Errors: return err @@ -319,12 +325,12 @@ func (r *marathonClient) handleCallbackEvent(writer http.ResponseWriter, request body, err := ioutil.ReadAll(request.Body) if err != nil { // TODO should this return a 500? - r.debugLog.Printf("handleCallbackEvent(): failed to read request body, error: %s\n", err) + r.debugLog("handleCallbackEvent(): failed to read request body, error: %s", err) return } if err := r.handleEvent(string(body[:])); err != nil { // TODO should this return a 500? - r.debugLog.Printf("handleCallbackEvent(): failed to handle event: %v\n", err) + r.debugLog("handleCallbackEvent(): failed to handle event: %v", err) } } diff --git a/vendor/github.com/gambol99/go-marathon/task.go b/vendor/github.com/gambol99/go-marathon/task.go index cb7afed35..d923692d7 100644 --- a/vendor/github.com/gambol99/go-marathon/task.go +++ b/vendor/github.com/gambol99/go-marathon/task.go @@ -1,5 +1,5 @@ /* -Copyright 2014 Rohith All rights reserved. +Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -217,7 +217,7 @@ func (r *Task) allHealthChecksAlive() bool { } // step: check the health results then for _, check := range r.HealthCheckResults { - if check.Alive == false { + if !check.Alive { return false } } diff --git a/vendor/github.com/gambol99/go-marathon/unreachable_strategy.go b/vendor/github.com/gambol99/go-marathon/unreachable_strategy.go index a77ff6936..9ed02df9f 100644 --- a/vendor/github.com/gambol99/go-marathon/unreachable_strategy.go +++ b/vendor/github.com/gambol99/go-marathon/unreachable_strategy.go @@ -1,5 +1,5 @@ /* -Copyright 2017 Rohith All rights reserved. +Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -65,13 +65,13 @@ func (us *UnreachableStrategy) MarshalJSON() ([]byte, error) { } // SetInactiveAfterSeconds sets the period after which instance will be marked as inactive. -func (us UnreachableStrategy) SetInactiveAfterSeconds(cap float64) UnreachableStrategy { +func (us *UnreachableStrategy) SetInactiveAfterSeconds(cap float64) *UnreachableStrategy { us.InactiveAfterSeconds = &cap return us } // SetExpungeAfterSeconds sets the period after which instance will be expunged. -func (us UnreachableStrategy) SetExpungeAfterSeconds(cap float64) UnreachableStrategy { +func (us *UnreachableStrategy) SetExpungeAfterSeconds(cap float64) *UnreachableStrategy { us.ExpungeAfterSeconds = &cap return us } diff --git a/vendor/github.com/gambol99/go-marathon/upgrade_strategy.go b/vendor/github.com/gambol99/go-marathon/upgrade_strategy.go index f964f08b3..d4d7598a6 100644 --- a/vendor/github.com/gambol99/go-marathon/upgrade_strategy.go +++ b/vendor/github.com/gambol99/go-marathon/upgrade_strategy.go @@ -1,5 +1,5 @@ /* -Copyright 2014 Rohith All rights reserved. +Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,13 +23,13 @@ type UpgradeStrategy struct { } // SetMinimumHealthCapacity sets the minimum health capacity. -func (us UpgradeStrategy) SetMinimumHealthCapacity(cap float64) UpgradeStrategy { +func (us *UpgradeStrategy) SetMinimumHealthCapacity(cap float64) *UpgradeStrategy { us.MinimumHealthCapacity = &cap return us } // SetMaximumOverCapacity sets the maximum over capacity. -func (us UpgradeStrategy) SetMaximumOverCapacity(cap float64) UpgradeStrategy { +func (us *UpgradeStrategy) SetMaximumOverCapacity(cap float64) *UpgradeStrategy { us.MaximumOverCapacity = &cap return us } diff --git a/vendor/github.com/gambol99/go-marathon/utils.go b/vendor/github.com/gambol99/go-marathon/utils.go index 278f49943..718d57bb4 100644 --- a/vendor/github.com/gambol99/go-marathon/utils.go +++ b/vendor/github.com/gambol99/go-marathon/utils.go @@ -1,5 +1,5 @@ /* -Copyright 2014 Rohith All rights reserved. +Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From b23b2611b3fa35803bc00dfe9359ec7bd22b9080 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Tue, 19 Dec 2017 17:00:12 +0100 Subject: [PATCH 06/10] Add non regex pathPrefix --- server/rules.go | 25 +++++++++++++++-- server/rules_test.go | 64 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/server/rules.go b/server/rules.go index 661b80224..be7f2b033 100644 --- a/server/rules.go +++ b/server/rules.go @@ -54,11 +54,32 @@ func (r *Rules) path(paths ...string) *mux.Route { func (r *Rules) pathPrefix(paths ...string) *mux.Route { router := r.route.route.Subrouter() for _, path := range paths { - router.PathPrefix(strings.TrimSpace(path)) + buildPath(path, router) } return r.route.route } +func buildPath(path string, router *mux.Router) { + cleanPath := strings.TrimSpace(path) + // {} are used to define a regex pattern in http://www.gorillatoolkit.org/pkg/mux. + // if we find a { in the path, that means we use regex, then the gorilla/mux implementation is chosen + // otherwise, we use a lightweight implementation + if strings.Contains(cleanPath, "{") { + router.PathPrefix(cleanPath) + } else { + m := &prefixMatcher{prefix: cleanPath} + router.NewRoute().MatcherFunc(m.Match) + } +} + +type prefixMatcher struct { + prefix string +} + +func (m *prefixMatcher) Match(r *http.Request, _ *mux.RouteMatch) bool { + return strings.HasPrefix(r.URL.Path, m.prefix) || strings.HasPrefix(r.URL.Path, m.prefix+"/") +} + type bySize []string func (a bySize) Len() int { return len(a) } @@ -111,7 +132,7 @@ func (r *Rules) pathPrefixStrip(paths ...string) *mux.Route { r.route.stripPrefixes = paths router := r.route.route.Subrouter() for _, path := range paths { - router.PathPrefix(strings.TrimSpace(path)) + buildPath(path, router) } return r.route.route } diff --git a/server/rules_test.go b/server/rules_test.go index f0ad65edd..7d3712289 100644 --- a/server/rules_test.go +++ b/server/rules_test.go @@ -192,3 +192,67 @@ type fakeHandler struct { } func (h *fakeHandler) ServeHTTP(http.ResponseWriter, *http.Request) {} + +func TestPathPrefix(t *testing.T) { + testCases := []struct { + desc string + path string + urls map[string]bool + }{ + { + desc: "leading slash", + path: "/bar", + urls: map[string]bool{ + "http://foo.com/bar": true, + "http://foo.com/bar/": true, + }, + }, + { + desc: "leading trailing slash", + path: "/bar/", + urls: map[string]bool{ + "http://foo.com/bar": false, + "http://foo.com/bar/": true, + }, + }, + { + desc: "no slash", + path: "bar", + urls: map[string]bool{ + "http://foo.com/bar": false, + "http://foo.com/bar/": false, + }, + }, + { + desc: "trailing slash", + path: "bar/", + urls: map[string]bool{ + "http://foo.com/bar": false, + "http://foo.com/bar/": false, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + rls := &Rules{ + route: &serverRoute{ + route: &mux.Route{}, + }, + } + + rt := rls.pathPrefix(test.path) + + for testURL, expectedMatch := range test.urls { + req := testhelpers.MustNewRequest(http.MethodGet, testURL, nil) + match := rt.Match(req, &mux.RouteMatch{}) + if match != expectedMatch { + t.Errorf("Error matching %s with %s, got %v expected %v", test.path, testURL, match, expectedMatch) + } + } + }) + } +} From cd1b3904dab5cae50da91eec4b107dc608af2a0e Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 20 Dec 2017 10:26:03 +0100 Subject: [PATCH 07/10] Add missing entrypoints template. --- autogen/gentemplates/gen.go | 3 +++ templates/kubernetes.tmpl | 3 +++ 2 files changed, 6 insertions(+) diff --git a/autogen/gentemplates/gen.go b/autogen/gentemplates/gen.go index a8e76b5a3..cbe9b72a0 100644 --- a/autogen/gentemplates/gen.go +++ b/autogen/gentemplates/gen.go @@ -428,6 +428,9 @@ var _templatesKubernetesTmpl = []byte(`[backends]{{range $backendName, $backend backend = "{{$frontend.Backend}}" priority = {{$frontend.Priority}} passHostHeader = {{$frontend.PassHostHeader}} + entryPoints = [{{range $frontend.EntryPoints}} + "{{.}}", + {{end}}] basicAuth = [{{range $frontend.BasicAuth}} "{{.}}", {{end}}] diff --git a/templates/kubernetes.tmpl b/templates/kubernetes.tmpl index 305fd4f72..741088953 100644 --- a/templates/kubernetes.tmpl +++ b/templates/kubernetes.tmpl @@ -25,6 +25,9 @@ backend = "{{$frontend.Backend}}" priority = {{$frontend.Priority}} passHostHeader = {{$frontend.PassHostHeader}} + entryPoints = [{{range $frontend.EntryPoints}} + "{{.}}", + {{end}}] basicAuth = [{{range $frontend.BasicAuth}} "{{.}}", {{end}}] From 3c7c6c4d9f21483817f956face6cc205dae5cf70 Mon Sep 17 00:00:00 2001 From: Nimi Wariboko Jr Date: Wed, 20 Dec 2017 03:12:03 -0800 Subject: [PATCH 08/10] Mesos: Use slave.PID.Host as task SlaveIP. --- provider/mesos/mesos.go | 2 +- provider/mesos/mesos_test.go | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/provider/mesos/mesos.go b/provider/mesos/mesos.go index 46f7d33f7..69a9bff2f 100644 --- a/provider/mesos/mesos.go +++ b/provider/mesos/mesos.go @@ -408,7 +408,7 @@ func (p *Provider) taskRecords(sj state.State) []state.Task { for _, task := range f.Tasks { for _, slave := range sj.Slaves { if task.SlaveID == slave.ID { - task.SlaveIP = slave.Hostname + task.SlaveIP = slave.PID.Host } } diff --git a/provider/mesos/mesos_test.go b/provider/mesos/mesos_test.go index 95bb8a3e6..34f7f8fc2 100644 --- a/provider/mesos/mesos_test.go +++ b/provider/mesos/mesos_test.go @@ -6,6 +6,7 @@ import ( "github.com/containous/traefik/log" "github.com/containous/traefik/types" + "github.com/mesos/mesos-go/upid" "github.com/mesosphere/mesos-dns/records/state" ) @@ -194,6 +195,9 @@ func TestTaskRecords(t *testing.T) { ID: "s_id", Hostname: "127.0.0.1", } + + slave.PID.UPID = &upid.UPID{} + slave.PID.Host = slave.Hostname var state = state.State{ Slaves: []state.Slave{slave}, Frameworks: []state.Framework{framework}, From 9e41485ff10ff9a357a7164376b845883d58f01b Mon Sep 17 00:00:00 2001 From: NicoMen Date: Wed, 20 Dec 2017 14:40:07 +0100 Subject: [PATCH 09/10] Modify ACME configuration migration into KV store --- cmd/traefik/storeconfig.go | 26 +++++++++--- docs/configuration/acme.md | 33 ++++++++++++--- examples/cluster/docker-compose.yml | 12 +----- .../manage_cluster_docker_environment.sh | 41 +++---------------- examples/cluster/traefik.toml.tmpl | 1 - 5 files changed, 53 insertions(+), 60 deletions(-) diff --git a/cmd/traefik/storeconfig.go b/cmd/traefik/storeconfig.go index 534a0682d..296e30608 100644 --- a/cmd/traefik/storeconfig.go +++ b/cmd/traefik/storeconfig.go @@ -67,12 +67,21 @@ func runStoreConfig(kv *staert.KvSource, traefikConfiguration *TraefikConfigurat return err } } - if traefikConfiguration.GlobalConfiguration.ACME != nil && len(traefikConfiguration.GlobalConfiguration.ACME.StorageFile) > 0 { - // convert ACME json file to KV store - localStore := acme.NewLocalStore(traefikConfiguration.GlobalConfiguration.ACME.StorageFile) - object, err := localStore.Load() - if err != nil { - return err + if traefikConfiguration.GlobalConfiguration.ACME != nil { + var object cluster.Object + if len(traefikConfiguration.GlobalConfiguration.ACME.StorageFile) > 0 { + // convert ACME json file to KV store + localStore := acme.NewLocalStore(traefikConfiguration.GlobalConfiguration.ACME.StorageFile) + object, err = localStore.Load() + if err != nil { + return err + } + + } else { + // Create an empty account to create all the keys into the KV store + account := &acme.Account{} + account.Init() + object = account } meta := cluster.NewMetadata(object) @@ -89,6 +98,11 @@ func runStoreConfig(kv *staert.KvSource, traefikConfiguration *TraefikConfigurat if err != nil { return err } + // Force to delete storagefile + err = kv.Delete(kv.Prefix + "/acme/storagefile") + if err != nil { + return err + } } return nil } diff --git a/docs/configuration/acme.md b/docs/configuration/acme.md index 8308e7d41..05a105c1c 100644 --- a/docs/configuration/acme.md +++ b/docs/configuration/acme.md @@ -20,6 +20,12 @@ See also [Let's Encrypt examples](/user-guide/examples/#lets-encrypt-support) an # email = "test@traefik.io" +# File used for certificates storage. +# +# Optional (Deprecated) +# +#storageFile = "acme.json" + # File or key used for certificates storage. # # Required @@ -55,7 +61,7 @@ entryPoint = "https" # # acmeLogging = true -# Enable on demand certificate. +# Enable on demand certificate. (Deprecated) # # Optional # @@ -89,6 +95,10 @@ entryPoint = "https" # main = "local4.com" ``` +!!! note + ACME entryPoint has to be relied to the port 443, otherwise ACME Challenges can not be done. + It's a Let's Encrypt limitation as described on the [community forum](https://community.letsencrypt.org/t/support-for-ports-other-than-80-and-443/3419/72). + ### `storage` ```toml @@ -100,7 +110,7 @@ storage = "acme.json" File or key used for certificates storage. -**WARNING** If you use Traefik in Docker, you have 2 options: +**WARNING** If you use Træfik in Docker, you have 2 options: - create a file on your host and mount it as a volume: ```toml @@ -118,6 +128,14 @@ storage = "/etc/traefik/acme/acme.json" docker run -v "/my/host/acme:/etc/traefik/acme" traefik ``` +!!! note + `storage` replaces `storageFile` which is deprecated. + +!!! note + During Træfik configuration migration from a configuration file to a KV store (thanks to `storeconfig` subcommand as described [here](/user-guide/kv-config/#store-configuration-in-key-value-store)), if ACME certificates have to be migrated too, use both `storageFile` and `storage`. + `storageFile` will contain the path to the `acme.json` file to migrate. + `storage` will contain the key where the certificates will be stored. + ### `dnsProvider` ```toml @@ -146,7 +164,7 @@ Select the provider that matches the DNS domain that will host the challenge TXT | [GoDaddy](https://godaddy.com/domains) | `godaddy` | `GODADDY_API_KEY`, `GODADDY_API_SECRET` | | [Google Cloud DNS](https://cloud.google.com/dns/docs/) | `gcloud` | `GCE_PROJECT`, `GCE_SERVICE_ACCOUNT_FILE` | | [Linode](https://www.linode.com) | `linode` | `LINODE_API_KEY` | -| manual | - | none, but run Traefik interactively & turn on `acmeLogging` to see instructions & press Enter. | +| manual | - | none, but run Træfik interactively & turn on `acmeLogging` to see instructions & press Enter. | | [Namecheap](https://www.namecheap.com) | `namecheap` | `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY` | | [Ns1](https://ns1.com/) | `ns1` | `NS1_API_KEY` | | [Open Telekom Cloud](https://cloud.telekom.de/en/) | `otc` | `OTC_DOMAIN_NAME`, `OTC_USER_NAME`, `OTC_PASSWORD`, `OTC_PROJECT_NAME`, `OTC_IDENTITY_ENDPOINT` | @@ -171,7 +189,7 @@ If `delayDontCheckDNS` is greater than zero, avoid this & instead just wait so m Useful if internal networks block external DNS queries. -### `onDemand` +### `onDemand` (Deprecated) ```toml [acme] @@ -188,7 +206,10 @@ This will request a certificate from Let's Encrypt during the first TLS handshak TLS handshakes will be slow when requesting a hostname certificate for the first time, this can lead to DoS attacks. !!! warning - Take note that Let's Encrypt have [rate limiting](https://letsencrypt.org/docs/rate-limits) + Take note that Let's Encrypt have [rate limiting](https://letsencrypt.org/docs/rate-limits). + +!!! warning + This option is deprecated. ### `onHostRule` @@ -238,7 +259,7 @@ main = "local4.com" ``` You can provide SANs (alternative domains) to each main domain. -All domains must have A/AAAA records pointing to Traefik. +All domains must have A/AAAA records pointing to Træfik. !!! warning Take note that Let's Encrypt have [rate limiting](https://letsencrypt.org/docs/rate-limits). diff --git a/examples/cluster/docker-compose.yml b/examples/cluster/docker-compose.yml index 2dd1b0b46..c45102874 100644 --- a/examples/cluster/docker-compose.yml +++ b/examples/cluster/docker-compose.yml @@ -38,16 +38,7 @@ services: etcdctl-ping: image: tenstartups/etcdctl - command: --endpoints=[10.0.1.12:2379] get "traefik/acme/storagefile" - environment: - ETCDCTL_DIAL_: "TIMEOUT 10s" - ETCDCTL_API : "3" - networks: - - net - - etcdctl-rm: - image: tenstartups/etcdctl - command: --endpoints=[10.0.1.12:2379] del "/traefik/acme/storagefile" + command: --endpoints=[10.0.1.12:2379] get "traefik/acme/storage" environment: ETCDCTL_DIAL_: "TIMEOUT 10s" ETCDCTL_API : "3" @@ -129,7 +120,6 @@ services: image: containous/traefik volumes: - "./traefik.toml:/traefik.toml:ro" - - "./acme.json:/acme.json:ro" command: storeconfig --debug networks: - net diff --git a/examples/cluster/manage_cluster_docker_environment.sh b/examples/cluster/manage_cluster_docker_environment.sh index 9252d2a5e..7f1196157 100755 --- a/examples/cluster/manage_cluster_docker_environment.sh +++ b/examples/cluster/manage_cluster_docker_environment.sh @@ -32,15 +32,6 @@ delete_services() { return 0 } -# Init the environment : get IP address and create needed files -init_acme_json() { - echo "CREATE empty acme.json file" - rm -f $basedir/acme.json && \ - touch $basedir/acme.json && \ - echo "{}" > $basedir/acme.json && \ - chmod 600 $basedir/acme.json # Needed for ACME -} - start_consul() { up_environment consul waiting_counter=12 @@ -76,7 +67,6 @@ start_etcd3() { } start_storeconfig_consul() { - init_acme_json # Create traefik.toml with consul provider cp $basedir/traefik.toml.tmpl $basedir/traefik.toml echo ' @@ -85,29 +75,13 @@ start_storeconfig_consul() { watch = true prefix = "traefik"' >> $basedir/traefik.toml up_environment traefik-storeconfig - rm -f $basedir/traefik.toml && rm -f $basedir/acme.json - # Delete acme-storage-file key + rm -f $basedir/traefik.toml waiting_counter=5 - # Not start Traefik store config if consul is not started - echo "Delete storage file key..." - while [[ -z $(curl -s http://10.0.1.2:8500/v1/kv/traefik/acme/storagefile) && $waiting_counter -gt 0 ]]; do - sleep 5 - let waiting_counter-=1 - done - if [[ $waiting_counter -eq 0 ]]; then - echo "[WARN] Unable to get storagefile key in consul" - else - curl -s --request DELETE http://10.0.1.2:8500/v1/kv/traefik/acme/storagefile - ret=$1 - if [[ $ret -ne 0 ]]; then - echo "[ERROR] Unable to delete storagefile key from consul kv." - fi - fi + delete_services traefik-storeconfig } start_storeconfig_etcd3() { - init_acme_json # Create traefik.toml with consul provider cp $basedir/traefik.toml.tmpl $basedir/traefik.toml echo ' @@ -117,20 +91,15 @@ start_storeconfig_etcd3() { prefix = "/traefik" useAPIV3 = true' >> $basedir/traefik.toml up_environment traefik-storeconfig - rm -f $basedir/traefik.toml && rm -f $basedir/acme.json - # Delete acme-storage-file key + rm -f $basedir/traefik.toml waiting_counter=5 - # Not start Traefik store config if consul is not started + # Don't start Traefik store config if ETCD3 is not started echo "Delete storage file key..." while [[ $(docker-compose -f $doc_file up --exit-code-from etcdctl-ping etcdctl-ping &>/dev/null) -ne 0 && $waiting_counter -gt 0 ]]; do sleep 5 let waiting_counter-=1 done - # Not start Traefik store config if consul is not started - echo "Delete storage file key from ETCD3..." - - up_environment etcdctl-rm && \ - delete_services etcdctl-rm traefik-storeconfig etcdctl-ping + delete_services traefik-storeconfig etcdctl-ping } start_traefik() { diff --git a/examples/cluster/traefik.toml.tmpl b/examples/cluster/traefik.toml.tmpl index bfa09538c..2569e66d8 100644 --- a/examples/cluster/traefik.toml.tmpl +++ b/examples/cluster/traefik.toml.tmpl @@ -12,7 +12,6 @@ defaultEntryPoints = ["http", "https"] [acme] email = "test@traefik.io" storage = "traefik/acme/account" -storageFile = "/acme.json" entryPoint = "https" OnHostRule = true caServer = "http://traefik.boulder.com:4000/directory" From 89a79d0f1b01015ce9dffe152dba4de3b42aaedf Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 20 Dec 2017 15:10:06 +0100 Subject: [PATCH 10/10] Prepare release 1.5.0-rc3 --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d438cdb0..5741a5a2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Change Log +## [v1.5.0-rc3](https://github.com/containous/traefik/tree/v1.5.0-rc3) (2017-12-20) +[All Commits](https://github.com/containous/traefik/compare/v1.5.0-rc2...v1.5.0-rc3) + +**Enhancements:** +- **[docker,k8s,rancher]** Support regex redirect by frontend ([#2570](https://github.com/containous/traefik/pull/2570) by [ldez](https://github.com/ldez)) + +**Bug fixes:** +- **[acme,docker]** Modify ACME configuration migration into KV store ([#2598](https://github.com/containous/traefik/pull/2598) by [nmengin](https://github.com/nmengin)) +- **[consulcatalog]** Reload configuration when port change for one service ([#2574](https://github.com/containous/traefik/pull/2574) by [mmatur](https://github.com/mmatur)) +- **[consulcatalog]** Fix bad Træfik update on Consul Catalog ([#2573](https://github.com/containous/traefik/pull/2573) by [mmatur](https://github.com/mmatur)) +- **[k8s]** Add missing entrypoints template. ([#2594](https://github.com/containous/traefik/pull/2594) by [ldez](https://github.com/ldez)) +- **[kv]** Fix stickiness bug due to template syntax error ([#2591](https://github.com/containous/traefik/pull/2591) by [dahefanteng](https://github.com/dahefanteng)) +- **[marathon]** Update go-marathon ([#2585](https://github.com/containous/traefik/pull/2585) by [timoreimann](https://github.com/timoreimann)) +- **[mesos]** Mesos: Use slave.PID.Host as task SlaveIP. ([#2590](https://github.com/containous/traefik/pull/2590) by [nemosupremo](https://github.com/nemosupremo)) +- **[middleware]** Fix RawPath handling in addPrefix ([#2560](https://github.com/containous/traefik/pull/2560) by [risdenk](https://github.com/risdenk)) +- **[rules]** Add non regex pathPrefix ([#2592](https://github.com/containous/traefik/pull/2592) by [emilevauge](https://github.com/emilevauge)) +- **[servicefabric]** Fix backend name for Stateful services. (Service Fabric) ([#2559](https://github.com/containous/traefik/pull/2559) by [ldez](https://github.com/ldez)) +- **[servicefabric]** Fix isHealthy logic. ([#2577](https://github.com/containous/traefik/pull/2577) by [ldez](https://github.com/ldez)) +- **[zk]** Change Zookeeper default prefix. ([#2580](https://github.com/containous/traefik/pull/2580) by [ldez](https://github.com/ldez)) +- Fix frontend redirect ([#2544](https://github.com/containous/traefik/pull/2544) by [ldez](https://github.com/ldez)) + +**Documentation:** +- **[acme]** Improve documentation for Cloudflare API key ([#2558](https://github.com/containous/traefik/pull/2558) by [mmatur](https://github.com/mmatur)) +- Move rate limit documentation. ([#2588](https://github.com/containous/traefik/pull/2588) by [ldez](https://github.com/ldez)) +- Grammar ([#2562](https://github.com/containous/traefik/pull/2562) by [geraldcroes](https://github.com/geraldcroes)) +- Fix broken links and improve ResponseCodeRatio() description ([#2538](https://github.com/containous/traefik/pull/2538) by [mvasin](https://github.com/mvasin)) + ## [v1.5.0-rc2](https://github.com/containous/traefik/tree/v1.5.0-rc2) (2017-12-06) [All Commits](https://github.com/containous/traefik/compare/v1.5.0-rc1...v1.5.0-rc2)