Merge branch 'v1.3' into merge-back-1_3_0-rc3

This commit is contained in:
Fernandez Ludovic 2017-05-24 20:39:38 +02:00
commit 2833d68f15
17 changed files with 357 additions and 195 deletions

View file

@ -1,5 +1,24 @@
# Change Log # Change Log
## [v1.3.0-rc3](https://github.com/containous/traefik/tree/v1.3.0-rc3) (2017-05-24)
[All Commits](https://github.com/containous/traefik/compare/v1.3.0-rc2...v1.3.0-rc3)
**Enhancements:**
- [#1658](https://api.github.com/repos/containous/traefik/issues/1658) Get testify/require dependency. ([timoreimann](https://api.github.com/users/timoreimann))
**Bug fixes:**
- [#1507](https://api.github.com/repos/containous/traefik/issues/1507) Create log folder if not present ([tanyadegurechaff](https://api.github.com/users/tanyadegurechaff))
- [#1604](https://api.github.com/repos/containous/traefik/issues/1604) [k8s] Ignore Ingresses with empty Endpoint subsets. ([timoreimann](https://api.github.com/users/timoreimann))
- [#1630](https://api.github.com/repos/containous/traefik/issues/1630) [k8s] Remove rule type path list. ([timoreimann](https://api.github.com/users/timoreimann))
- [#1635](https://api.github.com/repos/containous/traefik/issues/1635) Upgrade go-marathon to 15ea23e. ([timoreimann](https://api.github.com/users/timoreimann))
- [#1638](https://api.github.com/repos/containous/traefik/issues/1638) Fix behavior for PathPrefixStrip ([seryl](https://api.github.com/users/seryl))
- [#1654](https://api.github.com/repos/containous/traefik/issues/1654) fix: Empty Rancher Service Labels. ([ldez](https://api.github.com/users/ldez))
**Documentation:**
- [#1578](https://api.github.com/repos/containous/traefik/issues/1578) Add Marathon guide. ([Stibbons](https://api.github.com/users/Stibbons))
- [#1602](https://api.github.com/repos/containous/traefik/issues/1602) Re Orginise k8s docs to make 1.6 usage easier ([errm](https://api.github.com/users/errm))
- [#1642](https://api.github.com/repos/containous/traefik/issues/1642) Update changelog ([ldez](https://api.github.com/users/ldez))
## [v1.3.0-rc2](https://github.com/containous/traefik/tree/v1.3.0-rc2) (2017-05-16) ## [v1.3.0-rc2](https://github.com/containous/traefik/tree/v1.3.0-rc2) (2017-05-16)
[All Commits](https://github.com/containous/traefik/compare/v1.3.0-rc1...v1.3.0-rc2) [All Commits](https://github.com/containous/traefik/compare/v1.3.0-rc1...v1.3.0-rc2)

4
glide.lock generated
View file

@ -1,5 +1,5 @@
hash: e59e8244152a823cd3633fb09cdd583c4e5be78d7b50fb7047ba6b6a9ed5e8ec hash: e59e8244152a823cd3633fb09cdd583c4e5be78d7b50fb7047ba6b6a9ed5e8ec
updated: 2017-05-02T11:46:23.91434995-04:00 updated: 2017-05-19T23:30:19.890844996+02:00
imports: imports:
- name: cloud.google.com/go - name: cloud.google.com/go
version: 2e6a95edb1071d750f6d7db777bf66cd2997af6c version: 2e6a95edb1071d750f6d7db777bf66cd2997af6c
@ -201,7 +201,7 @@ imports:
- name: github.com/fatih/color - name: github.com/fatih/color
version: 9131ab34cf20d2f6d83fdc67168a5430d1c7dc23 version: 9131ab34cf20d2f6d83fdc67168a5430d1c7dc23
- name: github.com/gambol99/go-marathon - name: github.com/gambol99/go-marathon
version: d672c6fbb499596869d95146a26e7d0746c06c54 version: 15ea23e360abb8b25071e677aed344f31838e403
- 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

@ -17,17 +17,29 @@ type StripPrefix struct {
func (s *StripPrefix) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *StripPrefix) ServeHTTP(w http.ResponseWriter, r *http.Request) {
for _, prefix := range s.Prefixes { for _, prefix := range s.Prefixes {
if p := strings.TrimPrefix(r.URL.Path, strings.TrimSpace(prefix)); len(p) < len(r.URL.Path) { origPrefix := strings.TrimSpace(prefix)
r.URL.Path = p if origPrefix == r.URL.Path {
r.Header[forwardedPrefixHeader] = []string{prefix} r.URL.Path = "/"
r.RequestURI = r.URL.RequestURI() s.serveRequest(w, r, origPrefix)
s.Handler.ServeHTTP(w, r) return
}
prefix = strings.TrimSuffix(origPrefix, "/") + "/"
if p := strings.TrimPrefix(r.URL.Path, prefix); len(p) < len(r.URL.Path) {
r.URL.Path = "/" + strings.TrimPrefix(p, "/")
s.serveRequest(w, r, origPrefix)
return return
} }
} }
http.NotFound(w, r) http.NotFound(w, r)
} }
func (s *StripPrefix) serveRequest(w http.ResponseWriter, r *http.Request, prefix string) {
r.Header[forwardedPrefixHeader] = []string{prefix}
r.RequestURI = r.URL.RequestURI()
s.Handler.ServeHTTP(w, r)
}
// SetHandler sets handler // SetHandler sets handler
func (s *StripPrefix) SetHandler(Handler http.Handler) { func (s *StripPrefix) SetHandler(Handler http.Handler) {
s.Handler = Handler s.Handler = Handler

View file

@ -0,0 +1,103 @@
package middlewares
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStripPrefix(t *testing.T) {
tests := []struct {
desc string
prefixes []string
path string
expectedStatusCode int
expectedPath string
}{
{
desc: "no prefixes configured",
prefixes: []string{},
path: "/noprefixes",
expectedStatusCode: http.StatusNotFound,
},
{
desc: "wildcard (.*) requests",
prefixes: []string{"/"},
path: "/",
expectedStatusCode: http.StatusOK,
expectedPath: "/",
},
{
desc: "prefix and path matching",
prefixes: []string{"/stat"},
path: "/stat",
expectedStatusCode: http.StatusOK,
expectedPath: "/",
},
{
desc: "path prefix on exactly matching path",
prefixes: []string{"/stat/"},
path: "/stat/",
expectedStatusCode: http.StatusOK,
expectedPath: "/",
},
{
desc: "path prefix on matching longer path",
prefixes: []string{"/stat/"},
path: "/stat/us",
expectedStatusCode: http.StatusOK,
expectedPath: "/us",
},
{
desc: "path prefix on mismatching path",
prefixes: []string{"/stat/"},
path: "/status",
expectedStatusCode: http.StatusNotFound,
},
{
desc: "general prefix on matching path",
prefixes: []string{"/stat"},
path: "/stat/",
expectedStatusCode: http.StatusOK,
expectedPath: "/",
},
{
desc: "earlier prefix matching",
prefixes: []string{"/stat", "/stat/us"},
path: "/stat/us",
expectedStatusCode: http.StatusOK,
expectedPath: "/us",
},
{
desc: "later prefix matching",
prefixes: []string{"/mismatch", "/stat"},
path: "/stat",
expectedStatusCode: http.StatusOK,
expectedPath: "/",
},
}
for _, test := range tests {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var gotPath string
server := httptest.NewServer(&StripPrefix{
Prefixes: test.prefixes,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
}),
})
defer server.Close()
resp, err := http.Get(server.URL + test.path)
require.NoError(t, err, "Failed to send GET request")
assert.Equal(t, test.expectedStatusCode, resp.StatusCode, "Unexpected status code")
assert.Equal(t, test.expectedPath, gotPath, "Unexpected path")
})
}
}

View file

@ -27,9 +27,6 @@ var _ provider.Provider = (*Provider)(nil)
const ( const (
annotationFrontendRuleType = "traefik.frontend.rule.type" annotationFrontendRuleType = "traefik.frontend.rule.type"
ruleTypePathPrefixStrip = "PathPrefixStrip"
ruleTypePathStrip = "PathStrip"
ruleTypePath = "Path"
ruleTypePathPrefix = "PathPrefix" ruleTypePathPrefix = "PathPrefix"
annotationKubernetesWhitelistSourceRange = "ingress.kubernetes.io/whitelist-source-range" annotationKubernetesWhitelistSourceRange = "ingress.kubernetes.io/whitelist-source-range"
@ -205,12 +202,8 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
} }
if len(pa.Path) > 0 { if len(pa.Path) > 0 {
ruleType, unknown := getRuleTypeFromAnnotation(i.Annotations) ruleType := i.Annotations[annotationFrontendRuleType]
switch { if ruleType == "" {
case unknown:
log.Warnf("Unknown RuleType '%s' for Ingress %s/%s, falling back to PathPrefix", ruleType, i.ObjectMeta.Namespace, i.ObjectMeta.Name)
fallthrough
case ruleType == "":
ruleType = ruleTypePathPrefix ruleType = ruleTypePathPrefix
} }
@ -265,28 +258,25 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
} }
if !exists { if !exists {
log.Errorf("Endpoints not found for %s/%s", service.ObjectMeta.Namespace, service.ObjectMeta.Name) log.Warnf("Endpoints not found for %s/%s", service.ObjectMeta.Namespace, service.ObjectMeta.Name)
continue break
} }
if len(endpoints.Subsets) == 0 { if len(endpoints.Subsets) == 0 {
log.Warnf("Service endpoints not found for %s/%s, falling back to Service ClusterIP", service.ObjectMeta.Namespace, service.ObjectMeta.Name) log.Warnf("Endpoints not available for %s/%s", service.ObjectMeta.Namespace, service.ObjectMeta.Name)
templateObjects.Backends[r.Host+pa.Path].Servers[string(service.UID)] = types.Server{ break
URL: protocol + "://" + service.Spec.ClusterIP + ":" + strconv.Itoa(int(port.Port)), }
Weight: 1,
} for _, subset := range endpoints.Subsets {
} else { for _, address := range subset.Addresses {
for _, subset := range endpoints.Subsets { url := protocol + "://" + address.IP + ":" + strconv.Itoa(endpointPortNumber(port, subset.Ports))
for _, address := range subset.Addresses { name := url
url := protocol + "://" + address.IP + ":" + strconv.Itoa(endpointPortNumber(port, subset.Ports)) if address.TargetRef != nil && address.TargetRef.Name != "" {
name := url name = address.TargetRef.Name
if address.TargetRef != nil && address.TargetRef.Name != "" { }
name = address.TargetRef.Name templateObjects.Backends[r.Host+pa.Path].Servers[name] = types.Server{
} URL: url,
templateObjects.Backends[r.Host+pa.Path].Servers[name] = types.Server{ Weight: 1,
URL: url,
Weight: 1,
}
} }
} }
} }
@ -398,24 +388,3 @@ func (p *Provider) loadConfig(templateObjects types.Configuration) *types.Config
} }
return configuration return configuration
} }
func getRuleTypeFromAnnotation(annotations map[string]string) (ruleType string, unknown bool) {
ruleType = annotations[annotationFrontendRuleType]
for _, knownRuleType := range []string{
ruleTypePathPrefixStrip,
ruleTypePathStrip,
ruleTypePath,
ruleTypePathPrefix,
} {
if strings.ToLower(ruleType) == strings.ToLower(knownRuleType) {
return knownRuleType, false
}
}
if ruleType != "" {
// Annotation is set but does not match anything we know.
unknown = true
}
return ruleType, unknown
}

View file

@ -5,7 +5,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"reflect" "reflect"
"strings"
"testing" "testing"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
@ -333,19 +332,24 @@ func TestRuleType(t *testing.T) {
frontendRuleType string frontendRuleType string
}{ }{
{ {
desc: "implicit default", desc: "rule type annotation missing",
ingressRuleType: "", ingressRuleType: "",
frontendRuleType: ruleTypePathPrefix, frontendRuleType: ruleTypePathPrefix,
}, },
{ {
desc: "unknown ingress / explicit default", desc: "Path rule type annotation set",
ingressRuleType: "unknown", ingressRuleType: "Path",
frontendRuleType: ruleTypePathPrefix, frontendRuleType: "Path",
}, },
{ {
desc: "explicit ingress", desc: "PathStrip rule type annotation set",
ingressRuleType: ruleTypePath, ingressRuleType: "PathStrip",
frontendRuleType: ruleTypePath, frontendRuleType: "PathStrip",
},
{
desc: "PathStripPrefix rule type annotation set",
ingressRuleType: "PathStripPrefix",
frontendRuleType: "PathStripPrefix",
}, },
} }
@ -1832,65 +1836,6 @@ func TestInvalidPassHostHeaderValue(t *testing.T) {
} }
} }
func TestGetRuleTypeFromAnnotation(t *testing.T) {
tests := []struct {
in string
wantedUnknown bool
}{
{
in: ruleTypePathPrefixStrip,
wantedUnknown: false,
},
{
in: ruleTypePathStrip,
wantedUnknown: false,
},
{
in: ruleTypePath,
wantedUnknown: false,
},
{
in: ruleTypePathPrefix,
wantedUnknown: false,
},
{
wantedUnknown: false,
},
{
in: "Unknown",
wantedUnknown: true,
},
}
for _, test := range tests {
test := test
inputs := []string{test.in, strings.ToLower(test.in)}
if inputs[0] == inputs[1] {
// Lower-casing makes no difference -- truncating to single case.
inputs = inputs[:1]
}
for _, input := range inputs {
t.Run(fmt.Sprintf("in='%s'", input), func(t *testing.T) {
t.Parallel()
annotations := map[string]string{}
if test.in != "" {
annotations[annotationFrontendRuleType] = test.in
}
gotRuleType, gotUnknown := getRuleTypeFromAnnotation(annotations)
if gotUnknown != test.wantedUnknown {
t.Errorf("got unknown '%t', wanted '%t'", gotUnknown, test.wantedUnknown)
}
if gotRuleType != test.in {
t.Errorf("got rule type '%s', wanted '%s'", gotRuleType, test.in)
}
})
}
}
}
func TestKubeAPIErrors(t *testing.T) { func TestKubeAPIErrors(t *testing.T) {
ingresses := []*v1beta1.Ingress{{ ingresses := []*v1beta1.Ingress{{
ObjectMeta: v1.ObjectMeta{ ObjectMeta: v1.ObjectMeta{
@ -2027,6 +1972,21 @@ func TestMissingResources(t *testing.T) {
}, },
}, },
}, },
{
Host: "missing_endpoint_subsets",
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{
{
Backend: v1beta1.IngressBackend{
ServiceName: "missing_endpoint_subsets_service",
ServicePort: intstr.FromInt(80),
},
},
},
},
},
},
}, },
}, },
}} }}
@ -2061,6 +2021,21 @@ func TestMissingResources(t *testing.T) {
}, },
}, },
}, },
{
ObjectMeta: v1.ObjectMeta{
Name: "missing_endpoint_subsets_service",
UID: "4",
Namespace: "testing",
},
Spec: v1.ServiceSpec{
ClusterIP: "10.0.0.4",
Ports: []v1.ServicePort{
{
Port: 80,
},
},
},
},
} }
endpoints := []*v1.Endpoints{ endpoints := []*v1.Endpoints{
{ {
@ -2084,6 +2059,14 @@ func TestMissingResources(t *testing.T) {
}, },
}, },
}, },
{
ObjectMeta: v1.ObjectMeta{
Name: "missing_endpoint_subsets_service",
UID: "4",
Namespace: "testing",
},
Subsets: []v1.EndpointSubset{},
},
} }
watchChan := make(chan interface{}) watchChan := make(chan interface{})
@ -2130,6 +2113,14 @@ func TestMissingResources(t *testing.T) {
Sticky: false, Sticky: false,
}, },
}, },
"missing_endpoint_subsets": {
Servers: map[string]types.Server{},
CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{
Method: "wrr",
Sticky: false,
},
},
}, },
Frontends: map[string]*types.Frontend{ Frontends: map[string]*types.Frontend{
"fully_working": { "fully_working": {
@ -2150,6 +2141,15 @@ func TestMissingResources(t *testing.T) {
}, },
}, },
}, },
"missing_endpoint_subsets": {
Backend: "missing_endpoint_subsets",
PassHostHeader: true,
Routes: map[string]types.Route{
"missing_endpoint_subsets": {
Rule: "Host:missing_endpoint_subsets",
},
},
},
}, },
} }

View file

@ -388,8 +388,12 @@ func parseRancherData(environments []*rancher.Environment, services []*rancher.S
Containers: []string{}, Containers: []string{},
} }
for key, value := range service.LaunchConfig.Labels { if service.LaunchConfig == nil || service.LaunchConfig.Labels == nil {
rancherData.Labels[key] = value.(string) log.Warnf("Rancher Service Labels are missing. Environment: %s, service: %s", environment.Name, service.Name)
} else {
for key, value := range service.LaunchConfig.Labels {
rancherData.Labels[key] = value.(string)
}
} }
for _, container := range containers { for _, container := range containers {

View file

@ -1,4 +1,5 @@
[backends]{{range $backendName, $backend := .Backends}} [backends]{{range $backendName, $backend := .Backends}}
[backends."{{$backendName}}"]
{{if $backend.CircuitBreaker}} {{if $backend.CircuitBreaker}}
[backends."{{$backendName}}".circuitbreaker] [backends."{{$backendName}}".circuitbreaker]
expression = "{{$backend.CircuitBreaker.Expression}}" expression = "{{$backend.CircuitBreaker.Expression}}"

View file

@ -89,6 +89,8 @@ type Application struct {
TaskStats map[string]TaskStats `json:"taskStats,omitempty"` TaskStats map[string]TaskStats `json:"taskStats,omitempty"`
User string `json:"user,omitempty"` User string `json:"user,omitempty"`
UpgradeStrategy *UpgradeStrategy `json:"upgradeStrategy,omitempty"` UpgradeStrategy *UpgradeStrategy `json:"upgradeStrategy,omitempty"`
UnreachableStrategy *UnreachableStrategy `json:"unreachableStrategy,omitempty"`
KillSelection string `json:"killSelection,omitempty"`
Uris *[]string `json:"uris,omitempty"` Uris *[]string `json:"uris,omitempty"`
Version string `json:"version,omitempty"` Version string `json:"version,omitempty"`
VersionInfo *VersionInfo `json:"versionInfo,omitempty"` VersionInfo *VersionInfo `json:"versionInfo,omitempty"`
@ -453,7 +455,7 @@ func (r *Application) DeploymentIDs() []*DeploymentID {
// CheckHTTP adds a HTTP check to an application // CheckHTTP adds a HTTP check to an application
// port: the port the check should be checking // port: the port the check should be checking
// interval: the interval in seconds the check should be performed // interval: the interval in seconds the check should be performed
func (r *Application) CheckHTTP(uri string, port, interval int) (*Application, error) { func (r *Application) CheckHTTP(path string, port, interval int) (*Application, error) {
if r.Container == nil || r.Container.Docker == nil { if r.Container == nil || r.Container.Docker == nil {
return nil, ErrNoApplicationContainer return nil, ErrNoApplicationContainer
} }
@ -464,7 +466,7 @@ func (r *Application) CheckHTTP(uri string, port, interval int) (*Application, e
} }
health := NewDefaultHealthCheck() health := NewDefaultHealthCheck()
health.IntervalSeconds = interval health.IntervalSeconds = interval
*health.Path = uri *health.Path = path
*health.PortIndex = portIndex *health.PortIndex = portIndex
// step: add to the checks // step: add to the checks
r.AddHealthCheck(*health) r.AddHealthCheck(*health)
@ -555,6 +557,20 @@ func (r *Application) EmptyUpgradeStrategy() *Application {
return r return r
} }
// SetUnreachableStrategy sets the unreachable strategy.
func (r *Application) SetUnreachableStrategy(us UnreachableStrategy) *Application {
r.UnreachableStrategy = &us
return r
}
// EmptyUnreachableStrategy explicitly empties the unreachable strategy -- use this if
// you need to empty the unreachable strategy of an application that already has
// the unreachable strategy set (setting it to nil will keep the current value).
func (r *Application) EmptyUnreachableStrategy() *Application {
r.UnreachableStrategy = &UnreachableStrategy{}
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, "", " ")
@ -611,9 +627,9 @@ func (r *marathonClient) HasApplicationVersion(name, version string) (bool, erro
// ApplicationVersions is a list of versions which has been deployed with marathon for a specific application // ApplicationVersions is a list of versions which has been deployed with marathon for a specific application
// name: the id used to identify the application // name: the id used to identify the application
func (r *marathonClient) ApplicationVersions(name string) (*ApplicationVersions, error) { func (r *marathonClient) ApplicationVersions(name string) (*ApplicationVersions, error) {
uri := fmt.Sprintf("%s/versions", buildURI(name)) path := fmt.Sprintf("%s/versions", buildPath(name))
versions := new(ApplicationVersions) versions := new(ApplicationVersions)
if err := r.apiGet(uri, nil, versions); err != nil { if err := r.apiGet(path, nil, versions); err != nil {
return nil, err return nil, err
} }
return versions, nil return versions, nil
@ -623,9 +639,9 @@ 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) {
uri := fmt.Sprintf(buildURI(name)) path := fmt.Sprintf(buildPath(name))
deploymentID := new(DeploymentID) deploymentID := new(DeploymentID)
if err := r.apiPut(uri, version, deploymentID); err != nil { if err := r.apiPut(path, version, deploymentID); err != nil {
return nil, err return nil, err
} }
@ -639,7 +655,7 @@ func (r *marathonClient) Application(name string) (*Application, error) {
Application *Application `json:"app"` Application *Application `json:"app"`
} }
if err := r.apiGet(buildURI(name), nil, &wrapper); err != nil { if err := r.apiGet(buildPath(name), nil, &wrapper); err != nil {
return nil, err return nil, err
} }
@ -650,7 +666,7 @@ func (r *marathonClient) Application(name string) (*Application, error) {
// name: the id used to identify the application // name: the id used to identify the application
// opts: GetAppOpts request payload // opts: GetAppOpts request payload
func (r *marathonClient) ApplicationBy(name string, opts *GetAppOpts) (*Application, error) { func (r *marathonClient) ApplicationBy(name string, opts *GetAppOpts) (*Application, error) {
u, err := addOptions(buildURI(name), opts) path, err := addOptions(buildPath(name), opts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -658,7 +674,7 @@ func (r *marathonClient) ApplicationBy(name string, opts *GetAppOpts) (*Applicat
Application *Application `json:"app"` Application *Application `json:"app"`
} }
if err := r.apiGet(u, nil, &wrapper); err != nil { if err := r.apiGet(path, nil, &wrapper); err != nil {
return nil, err return nil, err
} }
@ -671,8 +687,8 @@ func (r *marathonClient) ApplicationBy(name string, opts *GetAppOpts) (*Applicat
func (r *marathonClient) ApplicationByVersion(name, version string) (*Application, error) { func (r *marathonClient) ApplicationByVersion(name, version string) (*Application, error) {
app := new(Application) app := new(Application)
uri := fmt.Sprintf("%s/versions/%s", buildURI(name), version) path := fmt.Sprintf("%s/versions/%s", buildPath(name), version)
if err := r.apiGet(uri, nil, app); err != nil { if err := r.apiGet(path, nil, app); err != nil {
return nil, err return nil, err
} }
@ -779,10 +795,10 @@ func (r *marathonClient) appExistAndRunning(name string) bool {
// name: the id used to identify the application // name: the id used to identify the application
// force: used to force the delete operation in case of blocked deployment // force: used to force the delete operation in case of blocked deployment
func (r *marathonClient) DeleteApplication(name string, force bool) (*DeploymentID, error) { func (r *marathonClient) DeleteApplication(name string, force bool) (*DeploymentID, error) {
uri := buildURIWithForceParam(name, force) path := buildPathWithForceParam(name, force)
// step: check of the application already exists // step: check of the application already exists
deployID := new(DeploymentID) deployID := new(DeploymentID)
if err := r.apiDelete(uri, nil, deployID); err != nil { if err := r.apiDelete(path, nil, deployID); err != nil {
return nil, err return nil, err
} }
@ -794,8 +810,8 @@ func (r *marathonClient) DeleteApplication(name string, force bool) (*Deployment
func (r *marathonClient) RestartApplication(name string, force bool) (*DeploymentID, error) { func (r *marathonClient) RestartApplication(name string, force bool) (*DeploymentID, error) {
deployment := new(DeploymentID) deployment := new(DeploymentID)
var options struct{} var options struct{}
uri := buildURIWithForceParam(fmt.Sprintf("%s/restart", name), force) path := buildPathWithForceParam(fmt.Sprintf("%s/restart", name), force)
if err := r.apiPost(uri, &options, deployment); err != nil { if err := r.apiPost(path, &options, deployment); err != nil {
return nil, err return nil, err
} }
@ -810,9 +826,9 @@ func (r *marathonClient) ScaleApplicationInstances(name string, instances int, f
changes := new(Application) changes := new(Application)
changes.ID = validateID(name) changes.ID = validateID(name)
changes.Instances = &instances changes.Instances = &instances
uri := buildURIWithForceParam(name, force) path := buildPathWithForceParam(name, force)
deployID := new(DeploymentID) deployID := new(DeploymentID)
if err := r.apiPut(uri, changes, deployID); err != nil { if err := r.apiPut(path, changes, deployID); err != nil {
return nil, err return nil, err
} }
@ -823,22 +839,22 @@ func (r *marathonClient) ScaleApplicationInstances(name string, instances int, f
// application: the structure holding the application configuration // application: the structure holding the application configuration
func (r *marathonClient) UpdateApplication(application *Application, force bool) (*DeploymentID, error) { func (r *marathonClient) UpdateApplication(application *Application, force bool) (*DeploymentID, error) {
result := new(DeploymentID) result := new(DeploymentID)
uri := buildURIWithForceParam(application.ID, force) path := buildPathWithForceParam(application.ID, force)
if err := r.apiPut(uri, application, result); err != nil { if err := r.apiPut(path, application, result); err != nil {
return nil, err return nil, err
} }
return result, nil return result, nil
} }
func buildURIWithForceParam(path string, force bool) string { func buildPathWithForceParam(rootPath string, force bool) string {
uri := buildURI(path) path := buildPath(rootPath)
if force { if force {
uri += "?force=true" path += "?force=true"
} }
return uri return path
} }
func buildURI(path string) string { func buildPath(path string) string {
return fmt.Sprintf("%s/%s", marathonAPIApps, trimRootPath(path)) return fmt.Sprintf("%s/%s", marathonAPIApps, trimRootPath(path))
} }

View file

@ -27,6 +27,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"regexp" "regexp"
"strings"
"sync" "sync"
"time" "time"
) )
@ -238,23 +239,23 @@ func (r *marathonClient) Ping() (bool, error) {
return true, nil return true, nil
} }
func (r *marathonClient) apiGet(uri string, post, result interface{}) error { func (r *marathonClient) apiGet(path string, post, result interface{}) error {
return r.apiCall("GET", uri, post, result) return r.apiCall("GET", path, post, result)
} }
func (r *marathonClient) apiPut(uri string, post, result interface{}) error { func (r *marathonClient) apiPut(path string, post, result interface{}) error {
return r.apiCall("PUT", uri, post, result) return r.apiCall("PUT", path, post, result)
} }
func (r *marathonClient) apiPost(uri string, post, result interface{}) error { func (r *marathonClient) apiPost(path string, post, result interface{}) error {
return r.apiCall("POST", uri, post, result) return r.apiCall("POST", path, post, result)
} }
func (r *marathonClient) apiDelete(uri string, post, result interface{}) error { func (r *marathonClient) apiDelete(path string, post, result interface{}) error {
return r.apiCall("DELETE", uri, post, result) return r.apiCall("DELETE", path, post, result)
} }
func (r *marathonClient) apiCall(method, url string, body, result interface{}) error { func (r *marathonClient) apiCall(method, path string, body, result interface{}) error {
for { for {
// step: marshall the request to json // step: marshall the request to json
var requestBody []byte var requestBody []byte
@ -266,7 +267,7 @@ func (r *marathonClient) apiCall(method, url string, body, result interface{}) e
} }
// step: create the API request // step: create the API request
request, member, err := r.buildAPIRequest(method, url, bytes.NewReader(requestBody)) request, member, err := r.buildAPIRequest(method, path, bytes.NewReader(requestBody))
if err != nil { if err != nil {
return err return err
} }
@ -317,7 +318,7 @@ func (r *marathonClient) apiCall(method, url string, body, result interface{}) e
} }
// buildAPIRequest creates a default API request // buildAPIRequest creates a default API request
func (r *marathonClient) buildAPIRequest(method, uri string, reader io.Reader) (request *http.Request, member string, err error) { func (r *marathonClient) buildAPIRequest(method, path string, reader io.Reader) (request *http.Request, member string, err error) {
// Grab a member from the cluster // Grab a member from the cluster
member, err = r.hosts.getMember() member, err = r.hosts.getMember()
if err != nil { if err != nil {
@ -325,16 +326,22 @@ func (r *marathonClient) buildAPIRequest(method, uri string, reader io.Reader) (
} }
// Build the HTTP request to Marathon // Build the HTTP request to Marathon
request, err = r.client.buildMarathonRequest(method, member, uri, reader) request, err = r.client.buildMarathonRequest(method, member, path, reader)
if err != nil { if err != nil {
return nil, member, err return nil, member, err
} }
return request, member, nil return request, member, nil
} }
func (rc *httpClient) buildMarathonRequest(method string, member string, uri string, reader io.Reader) (request *http.Request, err error) { // 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) {
if strings.HasPrefix(path, "/") {
panic(fmt.Sprintf("Path '%s' must not start with a leading slash", path))
}
// Create the endpoint URL // Create the endpoint URL
url := fmt.Sprintf("%s/%s", member, uri) url := fmt.Sprintf("%s/%s", member, path)
// Instantiate an HTTP request // Instantiate an HTTP request
request, err = http.NewRequest(method, url, reader) request, err = http.NewRequest(method, url, reader)

View file

@ -131,7 +131,7 @@ func (c *cluster) markDown(endpoint string) {
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 { for {
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 {

View file

@ -106,12 +106,12 @@ func (r *marathonClient) Group(name string) (*Group, error) {
// GroupsBy retrieves a list of all the groups from marathon by embed options // GroupsBy retrieves a list of all the groups from marathon by embed options
// opts: GetGroupOpts request payload // opts: GetGroupOpts request payload
func (r *marathonClient) GroupsBy(opts *GetGroupOpts) (*Groups, error) { func (r *marathonClient) GroupsBy(opts *GetGroupOpts) (*Groups, error) {
u, err := addOptions(marathonAPIGroups, opts) path, err := addOptions(marathonAPIGroups, opts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
groups := new(Groups) groups := new(Groups)
if err := r.apiGet(u, "", groups); err != nil { if err := r.apiGet(path, "", groups); err != nil {
return nil, err return nil, err
} }
return groups, nil return groups, nil
@ -121,12 +121,12 @@ func (r *marathonClient) GroupsBy(opts *GetGroupOpts) (*Groups, error) {
// name: the identifier for the group // name: the identifier for the group
// opts: GetGroupOpts request payload // opts: GetGroupOpts request payload
func (r *marathonClient) GroupBy(name string, opts *GetGroupOpts) (*Group, error) { func (r *marathonClient) GroupBy(name string, opts *GetGroupOpts) (*Group, error) {
u, err := addOptions(fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)), opts) path, err := addOptions(fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)), opts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
group := new(Group) group := new(Group)
if err := r.apiGet(u, nil, group); err != nil { if err := r.apiGet(path, nil, group); err != nil {
return nil, err return nil, err
} }
return group, nil return group, nil
@ -135,8 +135,8 @@ func (r *marathonClient) GroupBy(name string, opts *GetGroupOpts) (*Group, error
// HasGroup checks if the group exists in marathon // HasGroup checks if the group exists in marathon
// 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) {
uri := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)) path := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name))
err := r.apiCall("GET", uri, "", nil) err := r.apiCall("GET", 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
@ -208,11 +208,9 @@ func (r *marathonClient) WaitOnGroup(name string, timeout time.Duration) error {
// force: used to force the delete operation in case of blocked deployment // force: used to force the delete operation in case of blocked deployment
func (r *marathonClient) DeleteGroup(name string, force bool) (*DeploymentID, error) { func (r *marathonClient) DeleteGroup(name string, force bool) (*DeploymentID, error) {
version := new(DeploymentID) version := new(DeploymentID)
uri := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)) path := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name))
if force { path = buildPathWithForceParam(path, force)
uri = uri + "?force=true" if err := r.apiDelete(path, nil, version); err != nil {
}
if err := r.apiDelete(uri, nil, version); err != nil {
return nil, err return nil, err
} }
@ -225,11 +223,9 @@ func (r *marathonClient) DeleteGroup(name string, force bool) (*DeploymentID, er
// force: used to force the update operation in case of blocked deployment // force: used to force the update operation in case of blocked deployment
func (r *marathonClient) UpdateGroup(name string, group *Group, force bool) (*DeploymentID, error) { func (r *marathonClient) UpdateGroup(name string, group *Group, force bool) (*DeploymentID, error) {
deploymentID := new(DeploymentID) deploymentID := new(DeploymentID)
uri := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)) path := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name))
if force { path = buildPathWithForceParam(path, force)
uri = uri + "?force=true" if err := r.apiPut(path, group, deploymentID); err != nil {
}
if err := r.apiPut(uri, group, deploymentID); err != nil {
return nil, err return nil, err
} }

View file

@ -51,8 +51,8 @@ func (r *marathonClient) Queue() (*Queue, error) {
// DeleteQueueDelay resets task launch delay of the specific application // DeleteQueueDelay resets task launch delay of the specific application
// 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 {
uri := fmt.Sprintf("%s/%s/delay", marathonAPIQueue, trimRootPath(appID)) path := fmt.Sprintf("%s/%s/delay", marathonAPIQueue, trimRootPath(appID))
err := r.apiDelete(uri, nil, nil) err := r.apiDelete(path, nil, nil)
if err != nil { if err != nil {
return err return err
} }

View file

@ -201,8 +201,8 @@ func (r *marathonClient) registerSSESubscription() error {
// Subscribe adds a URL to Marathon's callback facility // Subscribe adds a URL to Marathon's callback facility
// callback : the URL you wish to subscribe // callback : the URL you wish to subscribe
func (r *marathonClient) Subscribe(callback string) error { func (r *marathonClient) Subscribe(callback string) error {
uri := fmt.Sprintf("%s?callbackUrl=%s", marathonAPISubscription, callback) path := fmt.Sprintf("%s?callbackUrl=%s", marathonAPISubscription, callback)
return r.apiPost(uri, "", nil) return r.apiPost(path, "", nil)
} }

View file

@ -79,13 +79,13 @@ func (r *Task) HasHealthCheckResults() bool {
// AllTasks lists tasks of all applications. // AllTasks lists tasks of all applications.
// opts: AllTasksOpts request payload // opts: AllTasksOpts request payload
func (r *marathonClient) AllTasks(opts *AllTasksOpts) (*Tasks, error) { func (r *marathonClient) AllTasks(opts *AllTasksOpts) (*Tasks, error) {
u, err := addOptions(marathonAPITasks, opts) path, err := addOptions(marathonAPITasks, opts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tasks := new(Tasks) tasks := new(Tasks)
if err := r.apiGet(u, nil, tasks); err != nil { if err := r.apiGet(path, nil, tasks); err != nil {
return nil, err return nil, err
} }
@ -107,14 +107,14 @@ func (r *marathonClient) Tasks(id string) (*Tasks, error) {
// id: the id of the application // id: the id of the application
// opts: KillApplicationTasksOpts request payload // opts: KillApplicationTasksOpts request payload
func (r *marathonClient) KillApplicationTasks(id string, opts *KillApplicationTasksOpts) (*Tasks, error) { func (r *marathonClient) KillApplicationTasks(id string, opts *KillApplicationTasksOpts) (*Tasks, error) {
u := fmt.Sprintf("%s/%s/tasks", marathonAPIApps, trimRootPath(id)) path := fmt.Sprintf("%s/%s/tasks", marathonAPIApps, trimRootPath(id))
u, err := addOptions(u, opts) path, err := addOptions(path, opts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tasks := new(Tasks) tasks := new(Tasks)
if err := r.apiDelete(u, nil, tasks); err != nil { if err := r.apiDelete(path, nil, tasks); err != nil {
return nil, err return nil, err
} }
@ -129,8 +129,8 @@ func (r *marathonClient) KillTask(taskID string, opts *KillTaskOpts) (*Task, err
appName = strings.Replace(appName, "_", "/", -1) appName = strings.Replace(appName, "_", "/", -1)
taskID = strings.Replace(taskID, "/", "_", -1) taskID = strings.Replace(taskID, "/", "_", -1)
u := fmt.Sprintf("%s/%s/tasks/%s", marathonAPIApps, appName, taskID) path := fmt.Sprintf("%s/%s/tasks/%s", marathonAPIApps, appName, taskID)
u, err := addOptions(u, opts) path, err := addOptions(path, opts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -139,7 +139,7 @@ func (r *marathonClient) KillTask(taskID string, opts *KillTaskOpts) (*Task, err
Task Task `json:"task"` Task Task `json:"task"`
}) })
if err := r.apiDelete(u, nil, wrappedTask); err != nil { if err := r.apiDelete(path, nil, wrappedTask); err != nil {
return nil, err return nil, err
} }
@ -150,8 +150,8 @@ func (r *marathonClient) KillTask(taskID string, opts *KillTaskOpts) (*Task, err
// tasks: the array of task ids // tasks: the array of task ids
// opts: KillTaskOpts request payload // opts: KillTaskOpts request payload
func (r *marathonClient) KillTasks(tasks []string, opts *KillTaskOpts) error { func (r *marathonClient) KillTasks(tasks []string, opts *KillTaskOpts) error {
u := fmt.Sprintf("%s/delete", marathonAPITasks) path := fmt.Sprintf("%s/delete", marathonAPITasks)
u, err := addOptions(u, opts) path, err := addOptions(path, opts)
if err != nil { if err != nil {
return nil return nil
} }
@ -161,7 +161,7 @@ func (r *marathonClient) KillTasks(tasks []string, opts *KillTaskOpts) error {
} }
post.IDs = tasks post.IDs = tasks
return r.apiPost(u, &post, nil) return r.apiPost(path, &post, nil)
} }
// TaskEndpoints gets the endpoints i.e. HOST_IP:DYNAMIC_PORT for a specific application service // TaskEndpoints gets the endpoints i.e. HOST_IP:DYNAMIC_PORT for a specific application service

View file

@ -0,0 +1,35 @@
/*
Copyright 2017 Rohith 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
// UnreachableStrategy is the unreachable strategy applied to an application.
type UnreachableStrategy struct {
InactiveAfterSeconds *float64 `json:"inactiveAfterSeconds,omitempty"`
ExpungeAfterSeconds *float64 `json:"expungeAfterSeconds,omitempty"`
}
// SetInactiveAfterSeconds sets the period after which instance will be marked as inactive.
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 {
us.ExpungeAfterSeconds = &cap
return us
}