From 9e0b2b180048064cccbe6393c581c407c00522ea Mon Sep 17 00:00:00 2001 From: Tomasz Janiszewski Date: Mon, 7 Dec 2015 17:39:29 +0100 Subject: [PATCH] Add tests for marathon.go and refactor --- apps/app.go | 19 ++ apps/app.json | 106 +++++++ apps/app_test.go | 88 ++++++ apps/apps.json | 104 +++++++ config/config.go | 1 + marathon/config.go | 9 +- marathon/marathon.go | 196 ++++-------- marathon/marathon_stub_test.go | 37 +++ marathon/marathon_test.go | 529 ++++++++++++++++++--------------- tasks/task.go | 14 +- tasks/task_test.go | 52 +++- tasks/tasks.json | 32 ++ 12 files changed, 795 insertions(+), 392 deletions(-) create mode 100644 apps/app.json create mode 100644 apps/app_test.go create mode 100644 apps/apps.json create mode 100644 marathon/marathon_stub_test.go create mode 100644 tasks/tasks.json diff --git a/apps/app.go b/apps/app.go index 1451010..1beb2ac 100644 --- a/apps/app.go +++ b/apps/app.go @@ -1,6 +1,7 @@ package apps import ( + "encoding/json" "github.com/allegro/marathon-consul/tasks" ) @@ -18,9 +19,27 @@ type AppWrapper struct { App App `json:"app"` } +type AppsResponse struct { + Apps []*App `json:"apps"` +} + type App struct { Labels map[string]string `json:"labels"` HealthChecks []HealthCheck `json:"healthChecks"` ID string `json:"id"` Tasks []tasks.Task `json:"tasks"` } + +func ParseApps(jsonBlob []byte) ([]*App, error) { + apps := &AppsResponse{} + err := json.Unmarshal(jsonBlob, apps) + + return apps.Apps, err +} + +func ParseApp(jsonBlob []byte) (*App, error) { + wrapper := &AppWrapper{} + err := json.Unmarshal(jsonBlob, wrapper) + + return &wrapper.App, err +} diff --git a/apps/app.json b/apps/app.json new file mode 100644 index 0000000..5468478 --- /dev/null +++ b/apps/app.json @@ -0,0 +1,106 @@ +{ + "app": { + "id": "/myapp", + "cmd": "env && python -m SimpleHTTPServer $PORT0", + "args": null, + "user": null, + "env": {}, + "instances": 2, + "cpus": 0.1, + "mem": 32.0, + "disk": 0.0, + "executor": "", + "constraints": [], + "uris": [], + "storeUrls": [], + "ports": [ + 10002, + 1, + 2, + 3 + ], + "requirePorts": false, + "backoffSeconds": 1, + "backoffFactor": 1.15, + "maxLaunchDelaySeconds": 3600, + "container": null, + "healthChecks": [ + { + "path": "/", + "protocol": "HTTP", + "portIndex": 0, + "gracePeriodSeconds": 10, + "intervalSeconds": 5, + "timeoutSeconds": 10, + "maxConsecutiveFailures": 3, + "ignoreHttp1xx": false + } + ], + "dependencies": [], + "upgradeStrategy": { + "minimumHealthCapacity": 1.0, + "maximumOverCapacity": 1.0 + }, + "labels": { + "consul": "true", + "public": "tag" + }, + "version": "2015-12-01T10:03:32.003Z", + "tasksStaged": 0, + "tasksRunning": 2, + "tasksHealthy": 2, + "tasksUnhealthy": 0, + "deployments": [], + "tasks": [ + { + "id": "myapp.cc49ccc1-9812-11e5-a06e-56847afe9799", + "host": "10.141.141.10", + "ports": [ + 31678, + 31679, + 31680, + 31681 + ], + "startedAt": "2015-12-01T10:03:40.966Z", + "stagedAt": "2015-12-01T10:03:40.890Z", + "version": "2015-12-01T10:03:32.003Z", + "appId": "/myapp", + "healthCheckResults": [ + { + "alive": true, + "consecutiveFailures": 0, + "firstSuccess": "2015-12-01T10:03:42.324Z", + "lastFailure": null, + "lastSuccess": "2015-12-01T10:03:42.324Z", + "taskId": "myapp.cc49ccc1-9812-11e5-a06e-56847afe9799" + } + ] + }, + { + "id": "myapp.c8b449f0-9812-11e5-a06e-56847afe9799", + "host": "10.141.141.10", + "ports": [ + 31307, + 31308, + 31309, + 31310 + ], + "startedAt": "2015-12-01T10:03:34.945Z", + "stagedAt": "2015-12-01T10:03:34.877Z", + "version": "2015-12-01T10:03:32.003Z", + "appId": "/myapp", + "healthCheckResults": [ + { + "alive": true, + "consecutiveFailures": 0, + "firstSuccess": "2015-12-01T10:03:37.313Z", + "lastFailure": null, + "lastSuccess": "2015-12-01T10:03:42.337Z", + "taskId": "myapp.c8b449f0-9812-11e5-a06e-56847afe9799" + } + ] + } + ], + "lastTaskFailure": null + } +} \ No newline at end of file diff --git a/apps/app_test.go b/apps/app_test.go new file mode 100644 index 0000000..23d8436 --- /dev/null +++ b/apps/app_test.go @@ -0,0 +1,88 @@ +package apps + +import ( + "github.com/allegro/marathon-consul/tasks" + "github.com/stretchr/testify/assert" + "io/ioutil" + "testing" +) + +func TestParseApps(t *testing.T) { + t.Parallel() + + appBlob, _ := ioutil.ReadFile("apps.json") + + expected := []*App{ + &App{ + HealthChecks: []HealthCheck{ + HealthCheck{ + Path: "/", + PortIndex: 0, + Protocol: "HTTP", + GracePeriodSeconds: 5, + IntervalSeconds: 20, + TimeoutSeconds: 20, + MaxConsecutiveFailures: 3, + }, + }, + ID: "/bridged-webapp", + Tasks: []tasks.Task{ + tasks.Task{ + ID: "test.47de43bd-1a81-11e5-bdb6-e6cb6734eaf8", + AppID: "/test", + Host: "192.168.2.114", + Ports: []int{31315}, + HealthCheckResults: []tasks.HealthCheckResult{tasks.HealthCheckResult{Alive: true}}, + }, + tasks.Task{ + ID: "test.4453212c-1a81-11e5-bdb6-e6cb6734eaf8", + AppID: "/test", + Host: "192.168.2.114", + Ports: []int{31797}, + }, + }, + }, + } + apps, err := ParseApps(appBlob) + assert.NoError(t, err) + assert.Len(t, apps, 1) + assert.Equal(t, expected, apps) +} + +func TestParseApp(t *testing.T) { + t.Parallel() + + appBlob, _ := ioutil.ReadFile("app.json") + + expected := &App{Labels: map[string]string{"consul": "true", "public": "tag"}, + HealthChecks: []HealthCheck{HealthCheck{Path: "/", + PortIndex: 0, + Protocol: "HTTP", + GracePeriodSeconds: 10, + IntervalSeconds: 5, + TimeoutSeconds: 10, + MaxConsecutiveFailures: 3}}, + ID: "/myapp", + Tasks: []tasks.Task{tasks.Task{ + ID: "myapp.cc49ccc1-9812-11e5-a06e-56847afe9799", + AppID: "/myapp", + Host: "10.141.141.10", + Ports: []int{31678, + 31679, + 31680, + 31681}, + HealthCheckResults: []tasks.HealthCheckResult{tasks.HealthCheckResult{Alive: true}}}, + tasks.Task{ + ID: "myapp.c8b449f0-9812-11e5-a06e-56847afe9799", + AppID: "/myapp", + Host: "10.141.141.10", + Ports: []int{31307, + 31308, + 31309, + 31310}, + HealthCheckResults: []tasks.HealthCheckResult{tasks.HealthCheckResult{Alive: true}}}}} + + app, err := ParseApp(appBlob) + assert.NoError(t, err) + assert.Equal(t, expected, app) +} diff --git a/apps/apps.json b/apps/apps.json new file mode 100644 index 0000000..94b61f7 --- /dev/null +++ b/apps/apps.json @@ -0,0 +1,104 @@ +{ + "apps": [ + { + "args": null, + "backoffFactor": 1.15, + "backoffSeconds": 1, + "maxLaunchDelaySeconds": 3600, + "cmd": "python3 -m http.server 8080", + "constraints": [], + "container": { + "docker": { + "image": "python:3", + "network": "BRIDGE", + "portMappings": [ + { + "containerPort": 8080, + "hostPort": 0, + "servicePort": 9000, + "protocol": "tcp" + }, + { + "containerPort": 161, + "hostPort": 0, + "protocol": "udp" + } + ] + }, + "type": "DOCKER", + "volumes": [] + }, + "cpus": 0.5, + "dependencies": [], + "deployments": [], + "disk": 0.0, + "env": {}, + "executor": "", + "healthChecks": [ + { + "command": null, + "gracePeriodSeconds": 5, + "intervalSeconds": 20, + "maxConsecutiveFailures": 3, + "path": "/", + "portIndex": 0, + "protocol": "HTTP", + "timeoutSeconds": 20 + } + ], + "id": "/bridged-webapp", + "instances": 2, + "mem": 64.0, + "ports": [ + 10000, + 10001 + ], + "requirePorts": false, + "storeUrls": [], + "tasksRunning": 2, + "tasksHealthy": 2, + "tasksUnhealthy": 0, + "tasksStaged": 0, + "upgradeStrategy": { + "minimumHealthCapacity": 1.0 + }, + "uris": [], + "user": null, + "version": "2014-09-25T02:26:59.256Z", + "tasks": [ + { + "appId": "/test", + "host": "192.168.2.114", + "id": "test.47de43bd-1a81-11e5-bdb6-e6cb6734eaf8", + "ports": [ + 31315 + ], + "stagedAt": "2015-06-24T14:57:06.353Z", + "startedAt": "2015-06-24T14:57:06.466Z", + "version": "2015-06-24T14:56:57.466Z", + "healthCheckResults": [ + { + "alive": true, + "consecutiveFailures": 0, + "firstSuccess": "2015-11-28T18:21:11.957Z", + "lastFailure": null, + "lastSuccess": "2015-11-30T10:08:19.477Z", + "taskId": "bridged-webapp.a9b051fb-95fc-11e5-9571-02818b42970e" + } + ] + }, + { + "appId": "/test", + "host": "192.168.2.114", + "id": "test.4453212c-1a81-11e5-bdb6-e6cb6734eaf8", + "ports": [ + 31797 + ], + "stagedAt": "2015-06-24T14:57:00.474Z", + "startedAt": "2015-06-24T14:57:00.611Z", + "version": "2015-06-24T14:56:57.466Z" + } + ] + } + ] +} \ No newline at end of file diff --git a/config/config.go b/config/config.go index ba3c17c..c26893c 100644 --- a/config/config.go +++ b/config/config.go @@ -55,6 +55,7 @@ func (config *Config) parseFlags() { flag.StringVar(&config.Marathon.Protocol, "marathon-protocol", "http", "Marathon protocol (http or https)") flag.StringVar(&config.Marathon.Username, "marathon-username", "", "Marathon username for basic auth") flag.StringVar(&config.Marathon.Password, "marathon-password", "", "Marathon password for basic auth") + flag.BoolVar(&config.Marathon.VerifySsl, "marathon-ssl-verify", true, "Verify certificates when connecting via SSL") // Metrics flag.StringVar(&config.Metrics.Target, "metrics-target", "stdout", "Metrics destination stdout or graphite") diff --git a/marathon/config.go b/marathon/config.go index 609c209..5557990 100644 --- a/marathon/config.go +++ b/marathon/config.go @@ -1,8 +1,9 @@ package marathon type Config struct { - Location string - Protocol string - Username string - Password string + Location string + Protocol string + Username string + Password string + VerifySsl bool } diff --git a/marathon/marathon.go b/marathon/marathon.go index 293ce6a..f34943c 100644 --- a/marathon/marathon.go +++ b/marathon/marathon.go @@ -2,7 +2,6 @@ package marathon import ( "crypto/tls" - "encoding/json" "fmt" log "github.com/Sirupsen/logrus" "github.com/allegro/marathon-consul/apps" @@ -11,6 +10,7 @@ import ( "io/ioutil" "net/http" "net/url" + "strings" ) type Marathoner interface { @@ -20,132 +20,51 @@ type Marathoner interface { } type Marathon struct { - Location string - Protocol string - Auth *url.Userinfo - NoVerifySsl bool + Location string + Protocol string + Auth *url.Userinfo + transport http.RoundTripper } func New(config Config) (Marathon, error) { - return Marathon{ - Location: config.Location, - Protocol: config.Protocol, - Auth: url.UserPassword(config.Username, config.Password), - NoVerifySsl: false, - }, nil -} - -func NewMarathon(location, protocol string, auth *url.Userinfo) (Marathon, error) { - return Marathon{location, protocol, auth, false}, nil -} - -func (m Marathon) Url(path string) string { - return m.UrlWithQuery(path, "") -} - -func (m Marathon) UrlWithQuery(path string, query string) string { - marathon := url.URL{ - Scheme: m.Protocol, - User: m.Auth, - Host: m.Location, - Path: path, - RawQuery: query, + var auth *url.Userinfo + if len(config.Username) == 0 && len(config.Password) == 0 { + auth = nil + } else { + auth = url.UserPassword(config.Username, config.Password) } - - return marathon.String() -} - -func (m Marathon) getClient() *pester.Client { - client := pester.New() - client.Transport = &http.Transport{ - Proxy: http.ProxyFromEnvironment, - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: m.NoVerifySsl, + return Marathon{ + Location: config.Location, + Protocol: config.Protocol, + Auth: auth, + transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: !config.VerifySsl, + }, }, - } - - return client + }, nil } func (m Marathon) App(appId string) (*apps.App, error) { log.WithField("location", m.Location).Debug("asking Marathon for " + appId) - client := m.getClient() - - request, err := http.NewRequest("GET", m.UrlWithQuery("/v2/apps/"+appId, "embed=apps.tasks"), nil) - if err != nil { - log.Error(err.Error()) - return nil, err - } - request.Header.Add("Accept", "application/json") - - appsResponse, err := client.Do(request) - if err != nil || (appsResponse.StatusCode != 200) { - m.logHTTPError(appsResponse, err) - return nil, err - } - - body, err := ioutil.ReadAll(appsResponse.Body) - if err != nil { - m.logHTTPError(appsResponse, err) - return nil, err - } - app, err := m.ParseApp(body) + body, err := m.get(m.urlWithQuery("/v2/apps/"+appId, "embed=apps.tasks")) if err != nil { - m.logHTTPError(appsResponse, err) return nil, err } - return app, err + return apps.ParseApp(body) } func (m Marathon) Apps() ([]*apps.App, error) { log.WithField("location", m.Location).Debug("asking Marathon for apps") - client := m.getClient() - - request, err := http.NewRequest("GET", m.UrlWithQuery("/v2/apps", "embed=apps.tasks"), nil) - if err != nil { - log.Error(err.Error()) - return nil, err - } - request.Header.Add("Accept", "application/json") - - appsResponse, err := client.Do(request) - if err != nil || (appsResponse.StatusCode != 200) { - m.logHTTPError(appsResponse, err) - return nil, err - } - - body, err := ioutil.ReadAll(appsResponse.Body) + body, err := m.get(m.urlWithQuery("/v2/apps", "embed=apps.tasks")) if err != nil { - m.logHTTPError(appsResponse, err) return nil, err } - appList, err := m.ParseApps(body) - if err != nil { - m.logHTTPError(appsResponse, err) - } - - return appList, err -} - -type AppsResponse struct { - Apps []*apps.App `json:"apps"` -} - -func (m Marathon) ParseApps(jsonBlob []byte) ([]*apps.App, error) { - apps := &AppsResponse{} - err := json.Unmarshal(jsonBlob, apps) - - return apps.Apps, err -} - -func (m Marathon) ParseApp(jsonBlob []byte) (*apps.App, error) { - wrapper := &apps.AppWrapper{} - err := json.Unmarshal(jsonBlob, wrapper) - - return &wrapper.App, err + return apps.ParseApps(body) } func (m Marathon) Tasks(app string) ([]*tasks.Task, error) { @@ -153,48 +72,37 @@ func (m Marathon) Tasks(app string) ([]*tasks.Task, error) { "location": m.Location, "app": app, }).Debug("asking Marathon for tasks") - client := m.getClient() - if app[0] == '/' { - app = app[1:] + app = strings.Trim(app, "/") + body, err := m.get(m.url(fmt.Sprintf("/v2/apps/%s/tasks", app))) + if err != nil { + return nil, err } - request, err := http.NewRequest("GET", m.Url(fmt.Sprintf("/v2/apps/%s/tasks", app)), nil) + return tasks.ParseTasks(body) +} + +func (m Marathon) get(url string) ([]byte, error) { + client := m.getClient() + request, err := http.NewRequest("GET", url, nil) if err != nil { log.Error(err.Error()) return nil, err } request.Header.Add("Accept", "application/json") - tasksResponse, err := client.Do(request) - if err != nil || (tasksResponse.StatusCode != 200) { - m.logHTTPError(tasksResponse, err) - return nil, err - } - - body, err := ioutil.ReadAll(tasksResponse.Body) + response, err := client.Do(request) if err != nil { - m.logHTTPError(tasksResponse, err) + m.logHTTPError(response, err) return nil, err } - - taskList, err := m.ParseTasks(body) - if err != nil { - m.logHTTPError(tasksResponse, err) + if response.StatusCode != 200 { + err = fmt.Errorf("Expected 200 but got %d for %s", response.StatusCode, response.Request.URL) + m.logHTTPError(response, err) + return nil, err } - return taskList, err -} - -type TasksResponse struct { - Tasks []*tasks.Task `json:"tasks"` -} - -func (m Marathon) ParseTasks(jsonBlob []byte) ([]*tasks.Task, error) { - tasks := &TasksResponse{} - err := json.Unmarshal(jsonBlob, tasks) - - return tasks.Tasks, err + return ioutil.ReadAll(response.Body) } func (m Marathon) logHTTPError(resp *http.Response, err error) { @@ -209,3 +117,25 @@ func (m Marathon) logHTTPError(resp *http.Response, err error) { "statusCode": statusCode, }).Error(err) } + +func (m Marathon) url(path string) string { + return m.urlWithQuery(path, "") +} + +func (m Marathon) urlWithQuery(path string, query string) string { + marathon := url.URL{ + Scheme: m.Protocol, + User: m.Auth, + Host: m.Location, + Path: path, + RawQuery: query, + } + + return marathon.String() +} + +func (m Marathon) getClient() *pester.Client { + client := pester.New() + client.Transport = m.transport + return client +} diff --git a/marathon/marathon_stub_test.go b/marathon/marathon_stub_test.go new file mode 100644 index 0000000..c351a3d --- /dev/null +++ b/marathon/marathon_stub_test.go @@ -0,0 +1,37 @@ +package marathon_test + +import ( + "github.com/allegro/marathon-consul/marathon" + "github.com/allegro/marathon-consul/utils" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestMarathonStub(t *testing.T) { + t.Parallel() + // given + m := marathon.MarathonerStubForApps(utils.ConsulApp("/test/app", 3)) + // when + apps, _ := m.Apps() + // then + assert.Len(t, apps, 1) + // when + existingApp, _ := m.App("/test/app") + // then + assert.NotNil(t, existingApp) + //when + notExistingApp, errOnNotExistingApp := m.App("/not/existing/app") + // then + assert.Error(t, errOnNotExistingApp) + assert.Nil(t, notExistingApp) + // when + existingTasks, _ := m.Tasks("/test/app") + // then + assert.Len(t, existingTasks, 3) + // when + notExistingTasks, errOnNotExistingTasks := m.Tasks("/not/existing/app") + // then + assert.Error(t, errOnNotExistingTasks) + assert.Nil(t, notExistingTasks) + +} diff --git a/marathon/marathon_test.go b/marathon/marathon_test.go index dbc8687..3be6a7d 100644 --- a/marathon/marathon_test.go +++ b/marathon/marathon_test.go @@ -1,246 +1,303 @@ package marathon import ( + "fmt" "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "net/url" "testing" ) -func TestUrl(t *testing.T) { - t.Parallel() - - m, _ := NewMarathon("localhost:8080", "http", nil) - url := m.Url("/v2/apps") - - assert.Equal(t, url, "http://localhost:8080/v2/apps") -} - -func TestParseApps(t *testing.T) { - t.Parallel() - - appBlob := []byte(`{ - "apps": [{ - "args": null, - "backoffFactor": 1.15, - "backoffSeconds": 1, - "maxLaunchDelaySeconds": 3600, - "cmd": "python3 -m http.server 8080", - "constraints": [], - "container": { - "docker": { - "image": "python:3", - "network": "BRIDGE", - "portMappings": [ - {"containerPort": 8080, "hostPort": 0, "servicePort": 9000, "protocol": "tcp"}, - {"containerPort": 161, "hostPort": 0, "protocol": "udp"} - ] - }, - "type": "DOCKER", - "volumes": [] - }, - "cpus": 0.5, - "dependencies": [], - "deployments": [], - "disk": 0.0, - "env": {}, - "executor": "", - "healthChecks": [{ - "command": null, - "gracePeriodSeconds": 5, - "intervalSeconds": 20, - "maxConsecutiveFailures": 3, - "path": "/", - "portIndex": 0, - "protocol": "HTTP", - "timeoutSeconds": 20 - }], - "id": "/bridged-webapp", - "instances": 2, - "mem": 64.0, - "ports": [10000, 10001], - "requirePorts": false, - "storeUrls": [], - "tasksRunning": 2, - "tasksHealthy": 2, - "tasksUnhealthy": 0, - "tasksStaged": 0, - "upgradeStrategy": {"minimumHealthCapacity": 1.0}, - "uris": [], - "user": null, - "version": "2014-09-25T02:26:59.256Z", - "tasks": [ - { - "appId": "/test", - "host": "192.168.2.114", - "id": "test.47de43bd-1a81-11e5-bdb6-e6cb6734eaf8", - "ports": [31315], - "stagedAt": "2015-06-24T14:57:06.353Z", - "startedAt": "2015-06-24T14:57:06.466Z", - "version": "2015-06-24T14:56:57.466Z", - "healthCheckResults":[ - { - "alive":true, - "consecutiveFailures":0, - "firstSuccess":"2015-11-28T18:21:11.957Z", - "lastFailure":null, - "lastSuccess":"2015-11-30T10:08:19.477Z", - "taskId":"bridged-webapp.a9b051fb-95fc-11e5-9571-02818b42970e" - } - ] - }, - { - "appId": "/test", - "host": "192.168.2.114", - "id": "test.4453212c-1a81-11e5-bdb6-e6cb6734eaf8", - "ports": [31797], - "stagedAt": "2015-06-24T14:57:00.474Z", - "startedAt": "2015-06-24T14:57:00.611Z", - "version": "2015-06-24T14:56:57.466Z" - } - ] - } -]} -`) - - m, _ := NewMarathon("localhost:8080", "http", nil) - apps, err := m.ParseApps(appBlob) - assert.Nil(t, err) - assert.Equal(t, len(apps), 1) -} - -func TestParseApp(t *testing.T) { - t.Parallel() - - appBlob := []byte(`{ - "app": { - "id": "/myapp", - "cmd": "env && python -m SimpleHTTPServer $PORT0", - "args": null, - "user": null, - "env": {}, - "instances": 2, - "cpus": 0.1, - "mem": 32.0, - "disk": 0.0, - "executor": "", - "constraints": [], - "uris": [], - "storeUrls": [], - "ports": [10002, 1, 2, 3], - "requirePorts": false, - "backoffSeconds": 1, - "backoffFactor": 1.15, - "maxLaunchDelaySeconds": 3600, - "container": null, - "healthChecks": [{ - "path": "/", - "protocol": "HTTP", - "portIndex": 0, - "gracePeriodSeconds": 10, - "intervalSeconds": 5, - "timeoutSeconds": 10, - "maxConsecutiveFailures": 3, - "ignoreHttp1xx": false - }], - "dependencies": [], - "upgradeStrategy": { - "minimumHealthCapacity": 1.0, - "maximumOverCapacity": 1.0 - }, - "labels": { - "consul": "true", - "public": "tag" +func TestMarathon_AppsWhenMarathonReturnEmptyList(t *testing.T) { + t.Parallel() + // given + server, transport := stubServer("/v2/apps?embed=apps.tasks", `{"apps": []}`) + defer server.Close() + + url, _ := url.Parse(server.URL) + m, _ := New(Config{Location: url.Host, Protocol: "HTTP"}) + m.transport = transport + // when + apps, err := m.Apps() + //then + assert.NoError(t, err) + assert.Empty(t, apps) +} + +func TestMarathon_AppsWhenConfigIsWrong(t *testing.T) { + t.Parallel() + // given + m, _ := New(Config{Location: "not::valid/location", Protocol: "HTTP"}) + // when + apps, err := m.Apps() + //then + assert.Error(t, err) + assert.Nil(t, apps) +} + +func TestMarathon_AppsWhenServerIsNotResponding(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + t.Parallel() + // given + m, _ := New(Config{Location: "unknown:22", Protocol: "HTTP"}) + // when + apps, err := m.Apps() + //then + assert.Error(t, err) + assert.Nil(t, apps) +} + +func TestMarathon_AppsWhenMarathonConnectionFailedShouldRetry(t *testing.T) { + t.Parallel() + // given + calls := 0 + server, transport := mockServer(func(w http.ResponseWriter, r *http.Request) { + calls++ + w.WriteHeader(500) + }) + defer server.Close() + + url, _ := url.Parse(server.URL) + m, _ := New(Config{Location: url.Host, Protocol: "HTTP"}) + m.transport = transport + // when + apps, err := m.Apps() + //then + assert.Error(t, err) + assert.Empty(t, apps) + assert.Equal(t, 3, calls) +} + +func TestMarathon_TasksWhenMarathonConnectionFailedShouldRetry(t *testing.T) { + t.Parallel() + // given + calls := 0 + server, transport := mockServer(func(w http.ResponseWriter, r *http.Request) { + calls++ + w.WriteHeader(500) + }) + defer server.Close() + + url, _ := url.Parse(server.URL) + m, _ := New(Config{Location: url.Host, Protocol: "HTTP"}) + m.transport = transport + // when + tasks, err := m.Tasks("/app/id") + //then + assert.Error(t, err) + assert.Empty(t, tasks) + assert.Equal(t, 3, calls) +} + +func TestMarathon_AppWhenMarathonConnectionFailedShouldRetry(t *testing.T) { + t.Parallel() + // given + calls := 0 + server, transport := mockServer(func(w http.ResponseWriter, r *http.Request) { + calls++ + w.WriteHeader(500) + }) + defer server.Close() + + url, _ := url.Parse(server.URL) + m, _ := New(Config{Location: url.Host, Protocol: "HTTP"}) + m.transport = transport + // when + app, err := m.App("/app/id") + //then + assert.Error(t, err) + assert.Nil(t, app) + assert.Equal(t, 3, calls) +} + +func TestMarathon_AppsWhenMarathonReturnEmptyResponse(t *testing.T) { + t.Parallel() + // given + server, transport := stubServer("/v2/apps?embed=apps.tasks", ``) + defer server.Close() + + url, _ := url.Parse(server.URL) + m, _ := New(Config{Location: url.Host, Protocol: "HTTP"}) + m.transport = transport + // when + apps, err := m.Apps() + //then + assert.Nil(t, apps) + assert.Error(t, err) +} + +func TestMarathon_AppsWhenMarathonReturnMalformedJsonResponse(t *testing.T) { + t.Parallel() + // given + server, transport := stubServer("/v2/apps?embed=apps.tasks", `{"apps":}`) + defer server.Close() + + url, _ := url.Parse(server.URL) + m, _ := New(Config{Location: url.Host, Protocol: "HTTP"}) + m.transport = transport + // when + app, err := m.App("/test/app") + //then + assert.Nil(t, app) + assert.Error(t, err) +} + +func TestMarathon_AppWhenMarathonReturnEmptyApp(t *testing.T) { + t.Parallel() + // given + server, transport := stubServer("/v2/apps//test/app?embed=apps.tasks", `{"app": {}}`) + defer server.Close() + + url, _ := url.Parse(server.URL) + m, _ := New(Config{Location: url.Host, Protocol: "HTTP"}) + m.transport = transport + // when + app, err := m.App("/test/app") + //then + assert.NoError(t, err) + assert.NotNil(t, app) +} + +func TestMarathon_AppWhenMarathonReturnEmptyResponse(t *testing.T) { + t.Parallel() + // given + server, transport := stubServer("/v2/apps//test/app?embed=apps.tasks", ``) + defer server.Close() + + url, _ := url.Parse(server.URL) + m, _ := New(Config{Location: url.Host, Protocol: "HTTP"}) + m.transport = transport + // when + app, err := m.App("/test/app") + //then + assert.NotNil(t, app) + assert.Error(t, err) +} + +func TestMarathon_AppWhenMarathonReturnMalformedJsonResponse(t *testing.T) { + t.Parallel() + // given + server, transport := stubServer("/v2/apps//test/app?embed=apps.tasks", `{apps:}`) + defer server.Close() + + url, _ := url.Parse(server.URL) + m, _ := New(Config{Location: url.Host, Protocol: "HTTP"}) + m.transport = transport + // when + apps, err := m.Apps() + //then + assert.Nil(t, apps) + assert.Error(t, err) +} + +func TestMarathon_TasksWhenMarathonReturnEmptyList(t *testing.T) { + t.Parallel() + // given + server, transport := stubServer("/v2/apps/test/app/tasks", ` + {"tasks": [{ + "appId": "/test", + "host": "192.168.2.114", + "id": "test.47de43bd-1a81-11e5-bdb6-e6cb6734eaf8", + "ports": [31315], + "healthCheckResults":[{ "alive":true }] + }]}`) + defer server.Close() + url, _ := url.Parse(server.URL) + m, _ := New(Config{Location: url.Host, Protocol: "HTTP"}) + m.transport = transport + // when + tasks, err := m.Tasks("//test/app") + //then + assert.NoError(t, err) + assert.NotNil(t, tasks) +} + +func TestMarathon_TasksWhenMarathonReturnEmptyResponse(t *testing.T) { + t.Parallel() + // given + server, transport := stubServer("/v2/apps/test/app/tasks", ``) + defer server.Close() + url, _ := url.Parse(server.URL) + m, _ := New(Config{Location: url.Host, Protocol: "HTTP"}) + m.transport = transport + // when + tasks, err := m.Tasks("/test/app") + //then + assert.Nil(t, tasks) + assert.Error(t, err) +} + +func TestMarathon_TasksWhenMarathonReturnMalformedJsonResponse(t *testing.T) { + t.Parallel() + // given + server, transport := stubServer("/v2/apps/test/app/tasks", ``) + defer server.Close() + url, _ := url.Parse(server.URL) + m, _ := New(Config{Location: url.Host, Protocol: "HTTP"}) + m.transport = transport + // when + tasks, err := m.Tasks("/test/app") + //then + assert.Nil(t, tasks) + assert.Error(t, err) +} + +func TestConfig_transport(t *testing.T) { + t.Parallel() + // given + config := Config{VerifySsl: false} + // when + marathon, _ := New(config) + // then + transport, ok := marathon.transport.(*http.Transport) + assert.True(t, ok) + assert.True(t, transport.TLSClientConfig.InsecureSkipVerify) +} + +func TestUrl_WithoutAuth(t *testing.T) { + t.Parallel() + // given + config := Config{Location: "localhost:8080", Protocol: "http"} + // when + m, _ := New(config) + // then + assert.Equal(t, "http://localhost:8080/v2/apps", m.url("/v2/apps")) +} + +func TestUrl_WithAuth(t *testing.T) { + t.Parallel() + // given + config := Config{Location: "localhost:8080", Protocol: "http", Username: "peter", Password: "parker"} + // when + m, _ := New(config) + // then + assert.Equal(t, "http://peter:parker@localhost:8080/v2/apps", m.url("/v2/apps")) +} + +// http://keighl.com/post/mocking-http-responses-in-golang/ +func stubServer(uri string, body string) (*httptest.Server, *http.Transport) { + return mockServer(func(w http.ResponseWriter, r *http.Request) { + if r.URL.RequestURI() == uri { + w.WriteHeader(200) + w.Header().Set("Content-Type", "application/json") + fmt.Fprintln(w, body) + } else { + w.WriteHeader(404) + } + }) +} + +func mockServer(handle func(w http.ResponseWriter, r *http.Request)) (*httptest.Server, *http.Transport) { + server := httptest.NewServer(http.HandlerFunc(handle)) + + transport := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse(server.URL) }, - "version": "2015-12-01T10:03:32.003Z", - "tasksStaged": 0, - "tasksRunning": 2, - "tasksHealthy": 2, - "tasksUnhealthy": 0, - "deployments": [], - "tasks": [{ - "id": "myapp.cc49ccc1-9812-11e5-a06e-56847afe9799", - "host": "10.141.141.10", - "ports": [31678, 31679, 31680, 31681], - "startedAt": "2015-12-01T10:03:40.966Z", - "stagedAt": "2015-12-01T10:03:40.890Z", - "version": "2015-12-01T10:03:32.003Z", - "appId": "/myapp", - "healthCheckResults": [{ - "alive": true, - "consecutiveFailures": 0, - "firstSuccess": "2015-12-01T10:03:42.324Z", - "lastFailure": null, - "lastSuccess": "2015-12-01T10:03:42.324Z", - "taskId": "myapp.cc49ccc1-9812-11e5-a06e-56847afe9799" - }] - }, { - "id": "myapp.c8b449f0-9812-11e5-a06e-56847afe9799", - "host": "10.141.141.10", - "ports": [31307, 31308, 31309, 31310], - "startedAt": "2015-12-01T10:03:34.945Z", - "stagedAt": "2015-12-01T10:03:34.877Z", - "version": "2015-12-01T10:03:32.003Z", - "appId": "/myapp", - "healthCheckResults": [{ - "alive": true, - "consecutiveFailures": 0, - "firstSuccess": "2015-12-01T10:03:37.313Z", - "lastFailure": null, - "lastSuccess": "2015-12-01T10:03:42.337Z", - "taskId": "myapp.c8b449f0-9812-11e5-a06e-56847afe9799" - }] - }], - "lastTaskFailure": null } -}`) - - m, _ := NewMarathon("localhost:8080", "http", nil) - app, err := m.ParseApp(appBlob) - assert.Nil(t, err) - assert.Equal(t, len(app.Tasks), 2) - assert.Equal(t, len(app.HealthChecks), 1) - assert.Equal(t, "true", app.Labels["consul"]) - assert.Equal(t, "tag", app.Labels["public"]) -} - -func TestParseTasks(t *testing.T) { - t.Parallel() - - tasksBlob := []byte(`{ - "tasks": [ - { - "appId": "/test", - "host": "192.168.2.114", - "id": "test.47de43bd-1a81-11e5-bdb6-e6cb6734eaf8", - "ports": [31315], - "stagedAt": "2015-06-24T14:57:06.353Z", - "startedAt": "2015-06-24T14:57:06.466Z", - "version": "2015-06-24T14:56:57.466Z", - "healthCheckResults":[ - { - "alive":true, - "consecutiveFailures":0, - "firstSuccess":"2015-11-28T18:21:11.957Z", - "lastFailure":null, - "lastSuccess":"2015-11-30T10:08:19.477Z", - "taskId":"bridged-webapp.a9b051fb-95fc-11e5-9571-02818b42970e" - } - ] - }, - { - "appId": "/test", - "host": "192.168.2.114", - "id": "test.4453212c-1a81-11e5-bdb6-e6cb6734eaf8", - "ports": [31797], - "stagedAt": "2015-06-24T14:57:00.474Z", - "startedAt": "2015-06-24T14:57:00.611Z", - "version": "2015-06-24T14:56:57.466Z" - } - ] -} -`) - - m, _ := NewMarathon("localhost:8080", "http", nil) - tasks, err := m.ParseTasks(tasksBlob) - assert.Nil(t, err) - assert.Equal(t, len(tasks), 2) + + return server, transport } diff --git a/tasks/task.go b/tasks/task.go index a24223f..c700afd 100644 --- a/tasks/task.go +++ b/tasks/task.go @@ -5,14 +5,11 @@ import ( ) type Task struct { - Timestamp string `json:"timestamp"` - SlaveID string `json:"slaveId"` ID string `json:"id"` TaskStatus string `json:"taskStatus"` AppID string `json:"appId"` Host string `json:"host"` Ports []int `json:"ports"` - Version string `json:"version"` HealthCheckResults []HealthCheckResult `json:"healthCheckResults"` } @@ -20,6 +17,17 @@ type HealthCheckResult struct { Alive bool `json:"alive"` } +type TasksResponse struct { + Tasks []*Task `json:"tasks"` +} + +func ParseTasks(jsonBlob []byte) ([]*Task, error) { + tasks := &TasksResponse{} + err := json.Unmarshal(jsonBlob, tasks) + + return tasks.Tasks, err +} + func ParseTask(event []byte) (*Task, error) { task := &Task{} err := json.Unmarshal(event, task) diff --git a/tasks/task_test.go b/tasks/task_test.go index 8733b51..8b56d83 100644 --- a/tasks/task_test.go +++ b/tasks/task_test.go @@ -3,37 +3,57 @@ package tasks import ( "encoding/json" "github.com/stretchr/testify/assert" + "io/ioutil" "testing" ) -var testTask = &Task{ - Timestamp: "2014-03-01T23:29:30.158Z", - SlaveID: "20140909-054127-177048842-5050-1494-0", - ID: "my-app_0-1396592784349", - TaskStatus: "TASK_RUNNING", - AppID: "/my-app", - Host: "slave-1234.acme.org", - Ports: []int{31372}, - Version: "2014-04-04T06:26:23.051Z", - HealthCheckResults: []HealthCheckResult{HealthCheckResult{Alive: true}}, -} - func TestParseTask(t *testing.T) { t.Parallel() + testTask := &Task{ + ID: "my-app_0-1396592784349", + AppID: "/my-app", + Host: "slave-1234.acme.org", + Ports: []int{31372}, + HealthCheckResults: []HealthCheckResult{HealthCheckResult{Alive: true}}, + } + jsonified, err := json.Marshal(testTask) assert.Nil(t, err) service, err := ParseTask(jsonified) assert.Nil(t, err) - assert.Equal(t, testTask.Timestamp, service.Timestamp) - assert.Equal(t, testTask.SlaveID, service.SlaveID) assert.Equal(t, testTask.ID, service.ID) - assert.Equal(t, testTask.TaskStatus, service.TaskStatus) assert.Equal(t, testTask.AppID, service.AppID) assert.Equal(t, testTask.Host, service.Host) assert.Equal(t, testTask.Ports, service.Ports) - assert.Equal(t, testTask.Version, service.Version) assert.Equal(t, testTask.HealthCheckResults[0].Alive, service.HealthCheckResults[0].Alive) } + +func TestParseTasks(t *testing.T) { + t.Parallel() + + tasksBlob, _ := ioutil.ReadFile("tasks.json") + + expectedTasks := []*Task{ + &Task{ + ID: "test.47de43bd-1a81-11e5-bdb6-e6cb6734eaf8", + AppID: "/test", + Host: "192.168.2.114", + Ports: []int{31315}, + HealthCheckResults: []HealthCheckResult{HealthCheckResult{Alive: true}}, + }, + &Task{ + ID: "test.4453212c-1a81-11e5-bdb6-e6cb6734eaf8", + AppID: "/test", + Host: "192.168.2.114", + Ports: []int{31797}, + }, + } + + tasks, err := ParseTasks(tasksBlob) + assert.Nil(t, err) + assert.Len(t, tasks, 2) + assert.Equal(t, expectedTasks, tasks) +} diff --git a/tasks/tasks.json b/tasks/tasks.json new file mode 100644 index 0000000..f5edfee --- /dev/null +++ b/tasks/tasks.json @@ -0,0 +1,32 @@ +{ + "tasks": [ + { + "appId": "/test", + "host": "192.168.2.114", + "id": "test.47de43bd-1a81-11e5-bdb6-e6cb6734eaf8", + "ports": [31315], + "stagedAt": "2015-06-24T14:57:06.353Z", + "startedAt": "2015-06-24T14:57:06.466Z", + "version": "2015-06-24T14:56:57.466Z", + "healthCheckResults":[ + { + "alive":true, + "consecutiveFailures":0, + "firstSuccess":"2015-11-28T18:21:11.957Z", + "lastFailure":null, + "lastSuccess":"2015-11-30T10:08:19.477Z", + "taskId":"bridged-webapp.a9b051fb-95fc-11e5-9571-02818b42970e" + } + ] + }, + { + "appId": "/test", + "host": "192.168.2.114", + "id": "test.4453212c-1a81-11e5-bdb6-e6cb6734eaf8", + "ports": [31797], + "stagedAt": "2015-06-24T14:57:00.474Z", + "startedAt": "2015-06-24T14:57:00.611Z", + "version": "2015-06-24T14:56:57.466Z" + } + ] +} \ No newline at end of file