Merge branch 'v1.5' into master

This commit is contained in:
Fernandez Ludovic 2017-12-20 15:47:15 +01:00
commit 4a7297d05c
43 changed files with 667 additions and 198 deletions

View file

@ -1,5 +1,32 @@
# Change Log # 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) ## [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) [All Commits](https://github.com/containous/traefik/compare/v1.5.0-rc1...v1.5.0-rc2)

View file

@ -516,6 +516,9 @@ var _templatesKubernetesTmpl = []byte(`[backends]{{range $backendName, $backend
backend = "{{$frontend.Backend}}" backend = "{{$frontend.Backend}}"
priority = {{$frontend.Priority}} priority = {{$frontend.Priority}}
passHostHeader = {{$frontend.PassHostHeader}} passHostHeader = {{$frontend.PassHostHeader}}
entryPoints = [{{range $frontend.EntryPoints}}
"{{.}}",
{{end}}]
basicAuth = [{{range $frontend.BasicAuth}} basicAuth = [{{range $frontend.BasicAuth}}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
@ -620,7 +623,7 @@ var _templatesKvTmpl = []byte(`{{$frontends := List .Prefix "/frontends/" }}
sticky = {{ getSticky . }} sticky = {{ getSticky . }}
{{if hasStickinessLabel $backend}} {{if hasStickinessLabel $backend}}
[backends."{{$backendName}}".loadBalancer.stickiness] [backends."{{$backendName}}".loadBalancer.stickiness]
cookieName = {{getStickinessCookieName $backend}} cookieName = "{{getStickinessCookieName $backend}}"
{{end}} {{end}}
{{end}} {{end}}

View file

@ -112,7 +112,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
var defaultZookeeper zk.Provider var defaultZookeeper zk.Provider
defaultZookeeper.Watch = true defaultZookeeper.Watch = true
defaultZookeeper.Endpoint = "127.0.0.1:2181" defaultZookeeper.Endpoint = "127.0.0.1:2181"
defaultZookeeper.Prefix = "/traefik" defaultZookeeper.Prefix = "traefik"
defaultZookeeper.Constraints = types.Constraints{} defaultZookeeper.Constraints = types.Constraints{}
//default Boltdb //default Boltdb

View file

@ -67,12 +67,21 @@ func runStoreConfig(kv *staert.KvSource, traefikConfiguration *TraefikConfigurat
return err return err
} }
} }
if traefikConfiguration.GlobalConfiguration.ACME != nil && len(traefikConfiguration.GlobalConfiguration.ACME.StorageFile) > 0 { if traefikConfiguration.GlobalConfiguration.ACME != nil {
// convert ACME json file to KV store var object cluster.Object
localStore := acme.NewLocalStore(traefikConfiguration.GlobalConfiguration.ACME.StorageFile) if len(traefikConfiguration.GlobalConfiguration.ACME.StorageFile) > 0 {
object, err := localStore.Load() // convert ACME json file to KV store
if err != nil { localStore := acme.NewLocalStore(traefikConfiguration.GlobalConfiguration.ACME.StorageFile)
return err 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) meta := cluster.NewMetadata(object)
@ -89,6 +98,11 @@ func runStoreConfig(kv *staert.KvSource, traefikConfiguration *TraefikConfigurat
if err != nil { if err != nil {
return err return err
} }
// Force to delete storagefile
err = kv.Delete(kv.Prefix + "/acme/storagefile")
if err != nil {
return err
}
} }
return nil return nil
} }

View file

@ -321,35 +321,6 @@ In this example, traffic routed through the first frontend will have the `X-Fram
!!! note !!! note
The detailed documentation for those security headers can be found in [unrolled/secure](https://github.com/unrolled/secure#available-options). 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 ### Backends
A backend is responsible to load-balance the traffic coming from one or more frontends to a set of http servers. A backend is responsible to load-balance the traffic coming from one or more frontends to a set of http servers.

View file

@ -20,6 +20,12 @@ See also [Let's Encrypt examples](/user-guide/examples/#lets-encrypt-support) an
# #
email = "test@traefik.io" email = "test@traefik.io"
# File used for certificates storage.
#
# Optional (Deprecated)
#
#storageFile = "acme.json"
# File or key used for certificates storage. # File or key used for certificates storage.
# #
# Required # Required
@ -55,7 +61,7 @@ entryPoint = "https"
# #
# acmeLogging = true # acmeLogging = true
# Enable on demand certificate. # Enable on demand certificate. (Deprecated)
# #
# Optional # Optional
# #
@ -89,6 +95,10 @@ entryPoint = "https"
# main = "local4.com" # 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` ### `storage`
```toml ```toml
@ -100,7 +110,7 @@ storage = "acme.json"
File or key used for certificates storage. File or key used for certificates storage.
**WARNING** If you use Traefik in Docker, you have 2 options: **WARNING** If you use Træfik in Docker, you have 2 options:
- create a file on your host and mount it as a volume: - create a file on your host and mount it as a volume:
```toml ```toml
@ -118,6 +128,14 @@ storage = "/etc/traefik/acme/acme.json"
docker run -v "/my/host/acme:/etc/traefik/acme" traefik 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` ### `dnsProvider`
```toml ```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` | | [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` | | [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` | | [Linode](https://www.linode.com) | `linode` | `LINODE_API_KEY` |
| manual | - | none, but run Traefik interactively & turn on `acmeLogging` to see instructions & press <kbd>Enter</kbd>. | | manual | - | none, but run Træfik interactively & turn on `acmeLogging` to see instructions & press <kbd>Enter</kbd>. |
| [Namecheap](https://www.namecheap.com) | `namecheap` | `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY` | | [Namecheap](https://www.namecheap.com) | `namecheap` | `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY` |
| [Ns1](https://ns1.com/) | `ns1` | `NS1_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` | | [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. Useful if internal networks block external DNS queries.
### `onDemand` ### `onDemand` (Deprecated)
```toml ```toml
[acme] [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. TLS handshakes will be slow when requesting a hostname certificate for the first time, this can lead to DoS attacks.
!!! warning !!! 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` ### `onHostRule`
@ -238,7 +259,7 @@ main = "local4.com"
``` ```
You can provide SANs (alternative domains) to each main domain. 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 !!! 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).

View file

@ -27,9 +27,9 @@ watch = true
# Prefix used for KV store. # Prefix used for KV store.
# #
# Optional # Optional
# Default: "/traefik" # Default: "traefik"
# #
prefix = "/traefik" prefix = "traefik"
# Override default configuration template. # Override default configuration template.
# For advanced users :) # For advanced users :)

View file

@ -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. 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 ## Retry Configuration
```toml ```toml

View file

@ -38,16 +38,7 @@ services:
etcdctl-ping: etcdctl-ping:
image: tenstartups/etcdctl image: tenstartups/etcdctl
command: --endpoints=[10.0.1.12:2379] get "traefik/acme/storagefile" command: --endpoints=[10.0.1.12:2379] get "traefik/acme/storage"
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"
environment: environment:
ETCDCTL_DIAL_: "TIMEOUT 10s" ETCDCTL_DIAL_: "TIMEOUT 10s"
ETCDCTL_API : "3" ETCDCTL_API : "3"
@ -129,7 +120,6 @@ services:
image: containous/traefik image: containous/traefik
volumes: volumes:
- "./traefik.toml:/traefik.toml:ro" - "./traefik.toml:/traefik.toml:ro"
- "./acme.json:/acme.json:ro"
command: storeconfig --debug command: storeconfig --debug
networks: networks:
- net - net

View file

@ -32,15 +32,6 @@ delete_services() {
return 0 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() { start_consul() {
up_environment consul up_environment consul
waiting_counter=12 waiting_counter=12
@ -76,7 +67,6 @@ start_etcd3() {
} }
start_storeconfig_consul() { start_storeconfig_consul() {
init_acme_json
# Create traefik.toml with consul provider # Create traefik.toml with consul provider
cp $basedir/traefik.toml.tmpl $basedir/traefik.toml cp $basedir/traefik.toml.tmpl $basedir/traefik.toml
echo ' echo '
@ -85,29 +75,13 @@ start_storeconfig_consul() {
watch = true watch = true
prefix = "traefik"' >> $basedir/traefik.toml prefix = "traefik"' >> $basedir/traefik.toml
up_environment traefik-storeconfig up_environment traefik-storeconfig
rm -f $basedir/traefik.toml && rm -f $basedir/acme.json rm -f $basedir/traefik.toml
# Delete acme-storage-file key
waiting_counter=5 waiting_counter=5
# Not start Traefik store config if consul is not started delete_services traefik-storeconfig
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
} }
start_storeconfig_etcd3() { start_storeconfig_etcd3() {
init_acme_json
# Create traefik.toml with consul provider # Create traefik.toml with consul provider
cp $basedir/traefik.toml.tmpl $basedir/traefik.toml cp $basedir/traefik.toml.tmpl $basedir/traefik.toml
echo ' echo '
@ -117,20 +91,15 @@ start_storeconfig_etcd3() {
prefix = "/traefik" prefix = "/traefik"
useAPIV3 = true' >> $basedir/traefik.toml useAPIV3 = true' >> $basedir/traefik.toml
up_environment traefik-storeconfig up_environment traefik-storeconfig
rm -f $basedir/traefik.toml && rm -f $basedir/acme.json rm -f $basedir/traefik.toml
# Delete acme-storage-file key
waiting_counter=5 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..." 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 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 sleep 5
let waiting_counter-=1 let waiting_counter-=1
done done
# Not start Traefik store config if consul is not started delete_services traefik-storeconfig etcdctl-ping
echo "Delete storage file key from ETCD3..."
up_environment etcdctl-rm && \
delete_services etcdctl-rm traefik-storeconfig etcdctl-ping
} }
start_traefik() { start_traefik() {

View file

@ -12,7 +12,6 @@ defaultEntryPoints = ["http", "https"]
[acme] [acme]
email = "test@traefik.io" email = "test@traefik.io"
storage = "traefik/acme/account" storage = "traefik/acme/account"
storageFile = "/acme.json"
entryPoint = "https" entryPoint = "https"
OnHostRule = true OnHostRule = true
caServer = "http://traefik.boulder.com:4000/directory" caServer = "http://traefik.boulder.com:4000/directory"

6
glide.lock generated
View file

@ -1,4 +1,4 @@
hash: f0d5ef854a4c115306c63c15320b595c29f715950eaf5f18418149886ecda400 hash: d7f811ac4a011308c6e1f73b618215dee90dae6cace9511f66d4b63d916a337a
updated: 2017-12-15T10:34:41.246378337+01:00 updated: 2017-12-15T10:34:41.246378337+01:00
imports: imports:
- name: cloud.google.com/go - name: cloud.google.com/go
@ -94,7 +94,7 @@ imports:
- name: github.com/containous/staert - name: github.com/containous/staert
version: af517d5b70db9c4b0505e0144fcc62b054057d2a version: af517d5b70db9c4b0505e0144fcc62b054057d2a
- name: github.com/containous/traefik-extra-service-fabric - name: github.com/containous/traefik-extra-service-fabric
version: c01c1ef60ed612c5e42c1ceae0c6f92e67619cc3 version: ca1fb57108293caad285b1c366b763f6c6ab71c9
- name: github.com/coreos/bbolt - name: github.com/coreos/bbolt
version: 3c6cbfb299c11444eb2f8c9d48f0d2ce09157423 version: 3c6cbfb299c11444eb2f8c9d48f0d2ce09157423
- name: github.com/coreos/etcd - name: github.com/coreos/etcd
@ -261,7 +261,7 @@ imports:
- name: github.com/fatih/color - name: github.com/fatih/color
version: 62e9147c64a1ed519147b62a56a14e83e2be02c1 version: 62e9147c64a1ed519147b62a56a14e83e2be02c1
- name: github.com/gambol99/go-marathon - name: github.com/gambol99/go-marathon
version: dd6cbd4c2d71294a19fb89158f2a00d427f174ab version: 03b46169666c53b9cc953b875ac5714e5103e064
- name: github.com/ghodss/yaml - name: github.com/ghodss/yaml
version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee
- name: github.com/go-ini/ini - name: github.com/go-ini/ini

View file

@ -12,7 +12,7 @@ import:
- package: github.com/cenk/backoff - package: github.com/cenk/backoff
- package: github.com/containous/flaeg - package: github.com/containous/flaeg
- package: github.com/containous/traefik-extra-service-fabric - package: github.com/containous/traefik-extra-service-fabric
version: v1.0.4 version: v1.0.5
- package: github.com/vulcand/oxy - package: github.com/vulcand/oxy
version: 7b6e758ab449705195df638765c4ca472248908a version: 7b6e758ab449705195df638765c4ca472248908a
repo: https://github.com/containous/oxy.git repo: https://github.com/containous/oxy.git

View file

@ -81,7 +81,7 @@ func taskRecords(st state.State) []state.Task {
for _, task := range f.Tasks { for _, task := range f.Tasks {
for _, slave := range st.Slaves { for _, slave := range st.Slaves {
if task.SlaveID == slave.ID { if task.SlaveID == slave.ID {
task.SlaveIP = slave.Hostname task.SlaveIP = slave.PID.Host
} }
} }

View file

@ -7,6 +7,7 @@ import (
"github.com/containous/traefik/provider/label" "github.com/containous/traefik/provider/label"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
"github.com/mesos/mesos-go/upid"
"github.com/mesosphere/mesos-dns/records/state" "github.com/mesosphere/mesos-dns/records/state"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -262,6 +263,9 @@ func TestTaskRecords(t *testing.T) {
ID: "s_id", ID: "s_id",
Hostname: "127.0.0.1", Hostname: "127.0.0.1",
} }
slave.PID.UPID = &upid.UPID{}
slave.PID.Host = slave.Hostname
var taskState = state.State{ var taskState = state.State{
Slaves: []state.Slave{slave}, Slaves: []state.Slave{slave},
Frameworks: []state.Framework{framework}, Frameworks: []state.Framework{framework},

View file

@ -54,11 +54,32 @@ func (r *Rules) path(paths ...string) *mux.Route {
func (r *Rules) pathPrefix(paths ...string) *mux.Route { func (r *Rules) pathPrefix(paths ...string) *mux.Route {
router := r.route.route.Subrouter() router := r.route.route.Subrouter()
for _, path := range paths { for _, path := range paths {
router.PathPrefix(strings.TrimSpace(path)) buildPath(path, router)
} }
return r.route.route 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 type bySize []string
func (a bySize) Len() int { return len(a) } func (a bySize) Len() int { return len(a) }
@ -111,7 +132,7 @@ func (r *Rules) pathPrefixStrip(paths ...string) *mux.Route {
r.route.stripPrefixes = paths r.route.stripPrefixes = paths
router := r.route.route.Subrouter() router := r.route.route.Subrouter()
for _, path := range paths { for _, path := range paths {
router.PathPrefix(strings.TrimSpace(path)) buildPath(path, router)
} }
return r.route.route return r.route.route
} }

View file

@ -192,3 +192,67 @@ type fakeHandler struct {
} }
func (h *fakeHandler) ServeHTTP(http.ResponseWriter, *http.Request) {} 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)
}
}
})
}
}

View file

@ -25,6 +25,9 @@
backend = "{{$frontend.Backend}}" backend = "{{$frontend.Backend}}"
priority = {{$frontend.Priority}} priority = {{$frontend.Priority}}
passHostHeader = {{$frontend.PassHostHeader}} passHostHeader = {{$frontend.PassHostHeader}}
entryPoints = [{{range $frontend.EntryPoints}}
"{{.}}",
{{end}}]
basicAuth = [{{range $frontend.BasicAuth}} basicAuth = [{{range $frontend.BasicAuth}}
"{{.}}", "{{.}}",
{{end}}] {{end}}]

View file

@ -20,7 +20,7 @@
sticky = {{ getSticky . }} sticky = {{ getSticky . }}
{{if hasStickinessLabel $backend}} {{if hasStickinessLabel $backend}}
[backends."{{$backendName}}".loadBalancer.stickiness] [backends."{{$backendName}}".loadBalancer.stickiness]
cookieName = {{getStickinessCookieName $backend}} cookieName = "{{getStickinessCookieName $backend}}"
{{end}} {{end}}
{{end}} {{end}}

View file

@ -303,7 +303,7 @@ func isPrimary(instance replicaInstance) bool {
} }
func isHealthy(instanceData *sf.ReplicaItemBase) 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 { func hasHTTPEndpoint(instanceData *sf.ReplicaItemBase) bool {

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 // Application is the definition for an application in marathon
type Application struct { type Application struct {
ID string `json:"id,omitempty"` ID string `json:"id,omitempty"`
Cmd *string `json:"cmd,omitempty"` Cmd *string `json:"cmd,omitempty"`
Args *[]string `json:"args,omitempty"` Args *[]string `json:"args,omitempty"`
Constraints *[][]string `json:"constraints,omitempty"` Constraints *[][]string `json:"constraints,omitempty"`
Container *Container `json:"container,omitempty"` Container *Container `json:"container,omitempty"`
CPUs float64 `json:"cpus,omitempty"` CPUs float64 `json:"cpus,omitempty"`
GPUs *float64 `json:"gpus,omitempty"` GPUs *float64 `json:"gpus,omitempty"`
Disk *float64 `json:"disk,omitempty"` Disk *float64 `json:"disk,omitempty"`
Env *map[string]string `json:"env,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"` Executor *string `json:"executor,omitempty"`
HealthChecks *[]HealthCheck `json:"healthChecks,omitempty"` HealthChecks *[]HealthCheck `json:"healthChecks,omitempty"`
ReadinessChecks *[]ReadinessCheck `json:"readinessChecks,omitempty"` ReadinessChecks *[]ReadinessCheck `json:"readinessChecks,omitempty"`
@ -99,6 +100,8 @@ type Application struct {
LastTaskFailure *LastTaskFailure `json:"lastTaskFailure,omitempty"` LastTaskFailure *LastTaskFailure `json:"lastTaskFailure,omitempty"`
Fetch *[]Fetch `json:"fetch,omitempty"` Fetch *[]Fetch `json:"fetch,omitempty"`
IPAddressPerTask *IPAddressPerTask `json:"ipAddress,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 // 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"` 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. // 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 // This configuration is not allowed to be used with Port or PortDefinitions. Thus, the implementation
// clears both. // clears both.
@ -355,8 +366,8 @@ func (r *Application) EmptyLabels() *Application {
} }
// AddEnv adds an environment variable to the application // AddEnv adds an environment variable to the application
// name: the name of the variable // name: the name of the variable
// value: go figure, the value associated to the above // value: go figure, the value associated to the above
func (r *Application) AddEnv(name, value string) *Application { func (r *Application) AddEnv(name, value string) *Application {
if r.Env == nil { if r.Env == nil {
r.EmptyEnvs() r.EmptyEnvs()
@ -375,6 +386,28 @@ func (r *Application) EmptyEnvs() *Application {
return r 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 // SetExecutor sets the executor
func (r *Application) SetExecutor(executor string) *Application { func (r *Application) SetExecutor(executor string) *Application {
r.Executor = &executor r.Executor = &executor
@ -571,6 +604,23 @@ func (r *Application) EmptyUnreachableStrategy() *Application {
return r 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 // String returns the json representation of this application
func (r *Application) String() string { func (r *Application) String() string {
s, err := json.MarshalIndent(r, "", " ") s, err := json.MarshalIndent(r, "", " ")
@ -639,7 +689,7 @@ func (r *marathonClient) ApplicationVersions(name string) (*ApplicationVersions,
// name: the id used to identify the application // name: the id used to identify the application
// version: the version (normally a timestamp) you wish to change to // version: the version (normally a timestamp) you wish to change to
func (r *marathonClient) SetApplicationVersion(name string, version *ApplicationVersion) (*DeploymentID, error) { func (r *marathonClient) SetApplicationVersion(name string, version *ApplicationVersion) (*DeploymentID, error) {
path := fmt.Sprintf(buildPath(name)) path := buildPath(name)
deploymentID := new(DeploymentID) deploymentID := new(DeploymentID)
if err := r.apiPut(path, version, deploymentID); err != nil { if err := r.apiPut(path, version, deploymentID); err != nil {
return nil, err return nil, err

View file

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

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -24,6 +24,7 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"net"
"net/http" "net/http"
"net/url" "net/url"
"regexp" "regexp"
@ -154,6 +155,24 @@ var (
ErrMarathonDown = errors.New("all the Marathon hosts are presently down") ErrMarathonDown = errors.New("all the Marathon hosts are presently down")
// ErrTimeoutError is thrown when the operation has timed out // ErrTimeoutError is thrown when the operation has timed out
ErrTimeoutError = errors.New("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. // EventsChannelContext holds contextual data for an EventsChannel.
@ -177,8 +196,8 @@ type marathonClient struct {
hosts *cluster hosts *cluster
// a map of service you wish to listen to // a map of service you wish to listen to
listeners map[EventsChannel]EventsChannelContext listeners map[EventsChannel]EventsChannelContext
// a custom logger for debug log messages // a custom log function for debug messages
debugLog *log.Logger debugLog func(format string, v ...interface{})
// the marathon HTTP client to ensure consistency in requests // the marathon HTTP client to ensure consistency in requests
client *httpClient client *httpClient
} }
@ -196,9 +215,18 @@ type newRequestError struct {
// NewClient creates a new marathon client // NewClient creates a new marathon client
// config: the configuration to use // config: the configuration to use
func NewClient(config Config) (Marathon, error) { 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 { if config.HTTPClient == nil {
config.HTTPClient = http.DefaultClient config.HTTPClient = defaultHTTPClient
} }
// step: if no polling wait time is set, default to 500 milliseconds. // 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 return nil, err
} }
debugLogOutput := config.LogOutput debugLog := func(string, ...interface{}) {}
if debugLogOutput == nil { if config.LogOutput != nil {
debugLogOutput = ioutil.Discard logger := log.New(config.LogOutput, "", 0)
debugLog = func(format string, v ...interface{}) {
logger.Printf(format, v...)
}
} }
return &marathonClient{ return &marathonClient{
config: config, config: config,
listeners: make(map[EventsChannel]EventsChannelContext), listeners: make(map[EventsChannel]EventsChannelContext),
hosts: hosts, hosts: hosts,
debugLog: log.New(debugLogOutput, "", 0), debugLog: debugLog,
client: client, client: client,
}, nil }, nil
} }
@ -280,7 +311,7 @@ func (r *marathonClient) apiCall(method, path string, body, result interface{})
if err != nil { if err != nil {
r.hosts.markDown(member) r.hosts.markDown(member)
// step: attempt the request on another 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 continue
} }
defer response.Body.Close() defer response.Body.Close()
@ -292,9 +323,9 @@ func (r *marathonClient) apiCall(method, path string, body, result interface{})
} }
if len(requestBody) > 0 { 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 { } 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 // 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 { if response.StatusCode >= 500 && response.StatusCode <= 599 {
// step: mark the host as down // step: mark the host as down
r.hosts.markDown(member) 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 continue
} }
@ -329,16 +360,28 @@ func (r *marathonClient) buildAPIRequest(method, path string, reader io.Reader)
} }
// Build the HTTP request to Marathon // 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 { if err != nil {
return nil, member, newRequestError{err} return nil, member, newRequestError{err}
} }
return request, member, nil 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. // 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. // 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, "/") { if strings.HasPrefix(path, "/") {
panic(fmt.Sprintf("Path '%s' must not start with a leading slash", 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("Authorization", "token="+rc.config.DCOSToken)
} }
request.Header.Add("Content-Type", "application/json")
request.Header.Add("Accept", "application/json")
return request, nil return request, nil
} }

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -39,6 +39,9 @@ type cluster struct {
members []*member members []*member
// the marathon HTTP client to ensure consistency in requests // the marathon HTTP client to ensure consistency in requests
client *httpClient client *httpClient
// healthCheckInterval is the interval by which we probe down nodes for
// availability again.
healthCheckInterval time.Duration
} }
// member represents an individual endpoint // member represents an individual endpoint
@ -94,8 +97,9 @@ func newCluster(client *httpClient, marathonURL string, isDCOS bool) (*cluster,
} }
return &cluster{ return &cluster{
client: client, client: client,
members: members, members: members,
healthCheckInterval: 5 * time.Second,
}, nil }, 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 // healthCheckNode performs a health check on the node and when active updates the status
func (c *cluster) healthCheckNode(node *member) { func (c *cluster) healthCheckNode(node *member) {
// step: wait for the node to become active ... we are assuming a /ping is enough here // 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) req, err := c.client.buildMarathonRequest("GET", node.endpoint, "ping", nil)
if err == nil { if err == nil {
res, err := c.client.Do(req) res, err := c.client.Do(req)
if err == nil && res.StatusCode == 200 { if err == nil && res.StatusCode == 200 {
// step: mark the node as active again
c.Lock()
node.status = memberStatusUp
c.Unlock()
break 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 // activeMembers returns a list of active members

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -50,8 +50,10 @@ type Config struct {
DCOSToken string DCOSToken string
// LogOutput the output for debug log messages // LogOutput the output for debug log messages
LogOutput io.Writer LogOutput io.Writer
// HTTPClient is the http client // HTTPClient is the HTTP client
HTTPClient *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 // wait time (in milliseconds) between repetitive requests to the API during polling
PollingWaitTime time.Duration PollingWaitTime time.Duration
} }

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 // Volume is the docker volume details associated to the container
type Volume struct { type Volume struct {
ContainerPath string `json:"containerPath,omitempty"` ContainerPath string `json:"containerPath,omitempty"`
HostPath string `json:"hostPath,omitempty"` HostPath string `json:"hostPath,omitempty"`
External *ExternalVolume `json:"external,omitempty"` External *ExternalVolume `json:"external,omitempty"`
Mode string `json:"mode,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 // ExternalVolume is an external volume definition
@ -98,6 +159,19 @@ func (container *Container) EmptyVolumes() *Container {
return 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 // SetExternalVolume define external elements for a volume
// name: the name of the volume // name: the name of the volume
// provider: the provider of the volume (e.g. dvdi) // provider: the provider of the volume (e.g. dvdi)

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 // name: the identifier for the group
func (r *marathonClient) HasGroup(name string) (bool, error) { func (r *marathonClient) HasGroup(name string) (bool, error) {
path := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)) path := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name))
err := r.apiCall("GET", path, "", nil) err := r.apiGet(path, "", nil)
if err != nil { if err != nil {
if apiErr, ok := err.(*APIError); ok && apiErr.ErrCode == ErrCodeNotFound { if apiErr, ok := err.(*APIError); ok && apiErr.ErrCode == ErrCodeNotFound {
return false, nil return false, nil

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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. // 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 h.Command = &c
return h return h
} }
// SetPortIndex sets the given port index on the health check. // 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 h.PortIndex = &i
return h return h
} }
// SetPort sets the given port on the health check. // 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 h.Port = &i
return h return h
} }
// SetPath sets the given path on the health check. // 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 h.Path = &p
return h return h
} }
// SetMaxConsecutiveFailures sets the maximum consecutive failures on the health check. // 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 h.MaxConsecutiveFailures = &i
return h return h
} }
// SetIgnoreHTTP1xx sets ignore http 1xx on the health check. // 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 h.IgnoreHTTP1xx = &ignore
return h return h
} }

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View file

@ -1,4 +1,5 @@
/* /*
Copyright 2015 The go-marathon Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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"` AppID string `json:"appId,omitempty"`
Host string `json:"host,omitempty"` Host string `json:"host,omitempty"`
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
SlaveID string `json:"slaveId,omitempty"`
State string `json:"state,omitempty"` State string `json:"state,omitempty"`
TaskID string `json:"taskId,omitempty"` TaskID string `json:"taskId,omitempty"`
Timestamp string `json:"timestamp,omitempty"` Timestamp string `json:"timestamp,omitempty"`

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 // 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 p.Port = &port
return p 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 // AddLabel adds a label to the PortDefinition
// name: the name of the label // name: the name of the label
// value: value for this 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 { if p.Labels == nil {
p.EmptyLabels() p.EmptyLabels()
} }

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 // appID: the ID of the application
func (r *marathonClient) DeleteQueueDelay(appID string) error { func (r *marathonClient) DeleteQueueDelay(appID string) error {
path := fmt.Sprintf("%s/%s/delay", marathonAPIQueue, trimRootPath(appID)) path := fmt.Sprintf("%s/%s/delay", marathonAPIQueue, trimRootPath(appID))
err := r.apiDelete(path, nil, nil) return r.apiDelete(path, nil, nil)
if err != nil {
return err
}
return nil
} }

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

48
vendor/github.com/gambol99/go-marathon/residency.go generated vendored Normal file
View file

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

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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: case EventsTransportCallback:
return r.registerCallbackSubscription() return r.registerCallbackSubscription()
case EventsTransportSSE: case EventsTransportSSE:
r.registerSSESubscription() return r.registerSSESubscription()
return nil
default: default:
return fmt.Errorf("the events transport: %d is not supported", r.config.EventsTransport) 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 // 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 // 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. // 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 { 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() { go func() {
for { for {
stream, err := r.connectToSSE() stream, err := r.connectToSSE()
if err != nil { 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) <-time.After(5 * time.Second)
continue continue
} }
err = r.listenToSSE(stream) err = r.listenToSSE(stream)
stream.Close() stream.Close()
r.debugLog.Printf("Error on SSE subscription: %s", err) r.debugLog("Error on SSE subscription: %s", err)
} }
}() }()
r.subscribedToSSE = true r.subscribedToSSE = true
return nil
} }
// connectToSSE tries to establish an *eventsource.Stream to any of the Marathon cluster members, marking the // 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 // its underlying fields for performance reasons. See note that at least the Transport
// should be reused here: https://golang.org/pkg/net/http/#Client // should be reused here: https://golang.org/pkg/net/http/#Client
httpClient := &http.Client{ httpClient := &http.Client{
Transport: r.config.HTTPClient.Transport, Transport: r.config.HTTPSSEClient.Transport,
CheckRedirect: r.config.HTTPClient.CheckRedirect, CheckRedirect: r.config.HTTPSSEClient.CheckRedirect,
Jar: r.config.HTTPClient.Jar, Jar: r.config.HTTPSSEClient.Jar,
Timeout: r.config.HTTPClient.Timeout, Timeout: r.config.HTTPSSEClient.Timeout,
} }
stream, err := eventsource.SubscribeWith("", httpClient, request) stream, err := eventsource.SubscribeWith("", httpClient, request)
if err != nil { 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) r.hosts.markDown(member)
continue continue
} }
@ -231,7 +237,7 @@ func (r *marathonClient) listenToSSE(stream *eventsource.Stream) error {
select { select {
case ev := <-stream.Events: case ev := <-stream.Events:
if err := r.handleEvent(ev.Data()); err != nil { 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: case err := <-stream.Errors:
return err return err
@ -319,12 +325,12 @@ func (r *marathonClient) handleCallbackEvent(writer http.ResponseWriter, request
body, err := ioutil.ReadAll(request.Body) body, err := ioutil.ReadAll(request.Body)
if err != nil { if err != nil {
// TODO should this return a 500? // 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 return
} }
if err := r.handleEvent(string(body[:])); err != nil { if err := r.handleEvent(string(body[:])); err != nil {
// TODO should this return a 500? // 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)
} }
} }

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 // step: check the health results then
for _, check := range r.HealthCheckResults { for _, check := range r.HealthCheckResults {
if check.Alive == false { if !check.Alive {
return false return false
} }
} }

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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. // 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 us.InactiveAfterSeconds = &cap
return us return us
} }
// SetExpungeAfterSeconds sets the period after which instance will be expunged. // 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 us.ExpungeAfterSeconds = &cap
return us return us
} }

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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. // SetMinimumHealthCapacity sets the minimum health capacity.
func (us UpgradeStrategy) SetMinimumHealthCapacity(cap float64) UpgradeStrategy { func (us *UpgradeStrategy) SetMinimumHealthCapacity(cap float64) *UpgradeStrategy {
us.MinimumHealthCapacity = &cap us.MinimumHealthCapacity = &cap
return us return us
} }
// SetMaximumOverCapacity sets the maximum over capacity. // SetMaximumOverCapacity sets the maximum over capacity.
func (us UpgradeStrategy) SetMaximumOverCapacity(cap float64) UpgradeStrategy { func (us *UpgradeStrategy) SetMaximumOverCapacity(cap float64) *UpgradeStrategy {
us.MaximumOverCapacity = &cap us.MaximumOverCapacity = &cap
return us return us
} }

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.