├── examples ├── Makefile ├── docker-compose.yml ├── multiple_endpoints │ └── main.go ├── glog │ └── main.go ├── tasks │ └── main.go ├── queue │ └── main.go ├── events_sse_transport │ └── main.go ├── events_callback_transport │ └── main.go ├── applications │ └── main.go ├── groups │ └── main.go └── pods │ └── main.go ├── .travis.yml ├── .gitignore ├── tests └── app-definitions │ ├── TestApplicationString-output.json │ └── TestApplicationString-1.5-output.json ├── CONTRIBUTING.md ├── last_task_failure.go ├── resources.go ├── upgrade_strategy.go ├── queue_test.go ├── residency_test.go ├── pod_instance_test.go ├── info_test.go ├── readiness_test.go ├── const.go ├── health_test.go ├── Makefile ├── offer.go ├── volume.go ├── pod_status_test.go ├── residency.go ├── pod_container_image.go ├── config.go ├── port_definition.go ├── unreachable_strategy.go ├── deployment_test.go ├── queue.go ├── group_test.go ├── pod_container_marshalling.go ├── utils_test.go ├── application_marshalling_test.go ├── utils.go ├── pod_marshalling.go ├── readiness.go ├── task_test.go ├── application_marshalling.go ├── pod_instance.go ├── info.go ├── pod_instance_status.go ├── network.go ├── pod_status.go ├── unreachable_strategy_test.go ├── pod_marshalling_test.go ├── pod_test.go ├── cluster_test.go ├── cluster.go ├── pod_scheduling.go ├── deployment.go ├── CHANGELOG.md ├── pod_container.go ├── error_test.go ├── error.go ├── docker_test.go ├── task.go ├── group.go └── health.go /examples/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | find * -type d -exec bash -exc "cd {}; go build . || kill $${PPID}" \; 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | global: 3 | secure: YiSCbBUz0VMONSBZ6TfRaSM9bFBuT5xvaknt9WxWczPSiSgiY8+dGYlsOaX2jzI26J4zA8KxIyxOihN1UE28tkkGoXRkRovoQuOl9YUYp+VCtZdaeksZ7tJ/j/b6aYGpGN3GRRfxkuIhXw1ghZLgqdCVtqfmD3GODlmeuFE01ug= 4 | language: go 5 | go: 6 | - 1.6 7 | - 1.7 8 | - 1.8 9 | - 1.9 10 | - "1.10" 11 | - "1.11" 12 | install: 13 | - go get github.com/mattn/goveralls 14 | script: 15 | - make test examples 16 | - if ([[ ${TRAVIS_BRANCH} == "master" ]] && [[ ${TRAVIS_EVENT_TYPE} == "push" ]]); then 17 | make coverage; 18 | goveralls -coverprofile=coverage -service=travis-ci; 19 | fi 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | go-marathon.iml 6 | .idea/ 7 | Gemfile.lock 8 | thin.* 9 | examples/applications/applications 10 | examples/events_callback_transport/events_callback_transport 11 | examples/events_sse_transport/events_sse_transport 12 | examples/glog/glog 13 | examples/groups/groups 14 | examples/multiple_endpoints/multiple_endpoints 15 | examples/pods/pods 16 | examples/queue/queue 17 | examples/tasks/tasks 18 | coverage 19 | 20 | # Folders 21 | _obj 22 | _test 23 | 24 | # Architecture specific extensions/prefixes 25 | *.[568vq] 26 | [568vq].out 27 | 28 | *.cgo1.go 29 | *.cgo2.c 30 | _cgo_defun.c 31 | _cgo_gotypes.go 32 | _cgo_export.* 33 | 34 | _testmain.go 35 | 36 | *.exe 37 | *.test 38 | *.prof 39 | 40 | tests/rest-api/rest-api 41 | -------------------------------------------------------------------------------- /tests/app-definitions/TestApplicationString-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "/my-app", 3 | "args": [ 4 | "/usr/sbin/apache2ctl", 5 | "-D", 6 | "FOREGROUND" 7 | ], 8 | "container": { 9 | "type": "DOCKER", 10 | "docker": { 11 | "image": "quay.io/gambol99/apache-php:latest", 12 | "network": "BRIDGE", 13 | "portMappings": [ 14 | { 15 | "containerPort": 80, 16 | "hostPort": 0, 17 | "protocol": "tcp" 18 | }, 19 | { 20 | "containerPort": 443, 21 | "hostPort": 0, 22 | "protocol": "tcp" 23 | } 24 | ] 25 | } 26 | }, 27 | "cpus": 0.1, 28 | "disk": 0, 29 | "healthChecks": [ 30 | { 31 | "portIndex": 0, 32 | "path": "/health", 33 | "maxConsecutiveFailures": 3, 34 | "protocol": "HTTP", 35 | "gracePeriodSeconds": 30, 36 | "intervalSeconds": 5, 37 | "timeoutSeconds": 5 38 | } 39 | ], 40 | "instances": 2, 41 | "mem": 64, 42 | "ports": null, 43 | "dependencies": null, 44 | "env": { 45 | "NAME": "frontend_http", 46 | "SERVICE_80_NAME": "test_http" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | ## Pre-Development 4 | - Look for an existing Github issue describing the bug you have found/feature request you would like to see getting implemented. 5 | - If no issue exists and there is reason to believe that your (non-trivial) contribution might be subject to an up-front design discussion, file an issue first and propose your idea. 6 | 7 | ## Development 8 | - Fork the repository. 9 | - Create a feature branch (`git checkout -b my-new-feature master`). 10 | - Commit your changes, preferring one commit per logical unit of work. Often times, this simply means having a single commit. 11 | - If applicable, update the documentation in the [README file](README.md). 12 | - In the vast majority of cases, you should add/amend a (regression) test for your bug fix/feature. 13 | - Push your branch (`git push origin my-new-feature`). 14 | - Create a new pull request. 15 | - Address any comments your reviewer raises, pushing additional commits onto your branch along the way. In particular, refrain from amending/force-pushing until you receive an LGTM (Looks Good To Me) from your reviewer. This will allow for a better review experience. 16 | -------------------------------------------------------------------------------- /tests/app-definitions/TestApplicationString-1.5-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "/my-app", 3 | "args": [ 4 | "/usr/sbin/apache2ctl", 5 | "-D", 6 | "FOREGROUND" 7 | ], 8 | "container": { 9 | "type": "DOCKER", 10 | "docker": { 11 | "image": "quay.io/gambol99/apache-php:latest" 12 | }, 13 | "portMappings": [ 14 | { 15 | "containerPort": 80, 16 | "hostPort": 0, 17 | "protocol": "tcp" 18 | }, 19 | { 20 | "containerPort": 443, 21 | "hostPort": 0, 22 | "protocol": "tcp" 23 | } 24 | ] 25 | }, 26 | "cpus": 0.1, 27 | "disk": 0, 28 | "networks": [ 29 | { 30 | "mode": "container/bridge" 31 | } 32 | ], 33 | "healthChecks": [ 34 | { 35 | "portIndex": 0, 36 | "path": "/health", 37 | "maxConsecutiveFailures": 3, 38 | "protocol": "HTTP", 39 | "gracePeriodSeconds": 30, 40 | "intervalSeconds": 5, 41 | "timeoutSeconds": 5 42 | } 43 | ], 44 | "instances": 2, 45 | "mem": 64, 46 | "ports": null, 47 | "dependencies": null, 48 | "env": { 49 | "NAME": "frontend_http", 50 | "SERVICE_80_NAME": "test_http" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /last_task_failure.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | // LastTaskFailure provides details on the last error experienced by an application 20 | type LastTaskFailure struct { 21 | AppID string `json:"appId,omitempty"` 22 | Host string `json:"host,omitempty"` 23 | Message string `json:"message,omitempty"` 24 | SlaveID string `json:"slaveId,omitempty"` 25 | State string `json:"state,omitempty"` 26 | TaskID string `json:"taskId,omitempty"` 27 | Timestamp string `json:"timestamp,omitempty"` 28 | Version string `json:"version,omitempty"` 29 | } 30 | -------------------------------------------------------------------------------- /resources.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | // ExecutorResources are the resources supported by an executor (a task running a pod) 20 | type ExecutorResources struct { 21 | Cpus float64 `json:"cpus,omitempty"` 22 | Mem float64 `json:"mem,omitempty"` 23 | Disk float64 `json:"disk,omitempty"` 24 | } 25 | 26 | // Resources are the full set of resources for a task 27 | type Resources struct { 28 | Cpus float64 `json:"cpus"` 29 | Mem float64 `json:"mem"` 30 | Disk float64 `json:"disk,omitempty"` 31 | Gpus int32 `json:"gpus,omitempty"` 32 | } 33 | 34 | // NewResources creates an empty Resources 35 | func NewResources() *Resources { 36 | return &Resources{} 37 | } 38 | -------------------------------------------------------------------------------- /upgrade_strategy.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | // UpgradeStrategy is the upgrade strategy applied to an application. 20 | type UpgradeStrategy struct { 21 | MinimumHealthCapacity *float64 `json:"minimumHealthCapacity,omitempty"` 22 | MaximumOverCapacity *float64 `json:"maximumOverCapacity,omitempty"` 23 | } 24 | 25 | // SetMinimumHealthCapacity sets the minimum health capacity. 26 | func (us *UpgradeStrategy) SetMinimumHealthCapacity(cap float64) *UpgradeStrategy { 27 | us.MinimumHealthCapacity = &cap 28 | return us 29 | } 30 | 31 | // SetMaximumOverCapacity sets the maximum over capacity. 32 | func (us *UpgradeStrategy) SetMaximumOverCapacity(cap float64) *UpgradeStrategy { 33 | us.MaximumOverCapacity = &cap 34 | return us 35 | } 36 | -------------------------------------------------------------------------------- /queue_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | func TestQueue(t *testing.T) { 26 | endpoint := newFakeMarathonEndpoint(t, nil) 27 | defer endpoint.Close() 28 | 29 | queue, err := endpoint.Client.Queue() 30 | assert.NoError(t, err) 31 | assert.NotNil(t, queue) 32 | 33 | assert.Len(t, queue.Items, 1) 34 | item := queue.Items[0] 35 | assert.Equal(t, item.Count, 10) 36 | assert.Equal(t, item.Delay.Overdue, true) 37 | assert.Equal(t, item.Delay.TimeLeftSeconds, 784) 38 | assert.NotEmpty(t, item.Application.ID) 39 | } 40 | 41 | func TestDeleteQueueDelay(t *testing.T) { 42 | endpoint := newFakeMarathonEndpoint(t, nil) 43 | defer endpoint.Close() 44 | 45 | err := endpoint.Client.DeleteQueueDelay(fakeAppName) 46 | assert.NoError(t, err) 47 | } 48 | -------------------------------------------------------------------------------- /examples/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/meltwater/docker-mesos 2 | 3 | zookeeper: 4 | image: mesoscloud/zookeeper:3.4.6-centos-7 5 | ports: 6 | - "2181:2181" 7 | - "2888:2888" 8 | - "3888:3888" 9 | environment: 10 | SERVERS: server.1=127.0.0.1 11 | MYID: 1 12 | 13 | mesosmaster: 14 | image: mesoscloud/mesos-master:0.24.1-centos-7 15 | net: host 16 | environment: 17 | MESOS_ZK: zk://localhost:2181/mesos 18 | MESOS_QUORUM: 1 19 | MESOS_CLUSTER: local 20 | MESOS_HOSTNAME: localhost 21 | 22 | mesosslave: 23 | image: mesoscloud/mesos-slave:0.24.1-centos-7 24 | net: host 25 | privileged: true 26 | volumes: 27 | - /sys:/sys 28 | # /cgroup is needed on some older Linux versions 29 | # - /cgroup:/cgroup 30 | # /usr/bin/docker is needed if you're running an older docker version 31 | # - /usr/local/bin/docker:/usr/bin/docker:r 32 | - /var/run/docker.sock:/var/run/docker.sock:rw 33 | environment: 34 | MESOS_MASTER: zk://localhost:2181/mesos 35 | MESOS_EXECUTOR_SHUTDOWN_GRACE_PERIOD: 90secs 36 | MESOS_DOCKER_STOP_TIMEOUT: 60secs 37 | # If your workstation doesn't have a resolvable hostname/FQDN then $MESOS_HOSTNAME needs to be set to its IP-address 38 | # MESOS_HOSTNAME: 192.168.178.39 39 | 40 | marathon: 41 | image: mesoscloud/marathon:0.11.0-centos-7 42 | net: host 43 | environment: 44 | MARATHON_ZK: zk://localhost:2181/marathon 45 | MARATHON_MASTER: zk://localhost:2181/mesos 46 | MARATHON_EVENT_SUBSCRIBER: http_callback 47 | MARATHON_TASK_LAUNCH_TIMEOUT: 300000 48 | -------------------------------------------------------------------------------- /residency_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "testing" 21 | "time" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestResidency(t *testing.T) { 27 | app := NewDockerApplication() 28 | 29 | app = app.SetResidency(TaskLostBehaviorTypeWaitForever) 30 | 31 | if assert.NotNil(t, app.Residency) { 32 | res := app.Residency 33 | 34 | assert.Equal(t, res.TaskLostBehavior, TaskLostBehaviorTypeWaitForever) 35 | 36 | res.SetRelaunchEscalationTimeout(2525 * time.Millisecond) 37 | // should be trimmed to seconds precision 38 | assert.Equal(t, app.Residency.RelaunchEscalationTimeoutSeconds, 2) 39 | 40 | res.SetTaskLostBehavior(TaskLostBehaviorTypeRelaunchAfterTimeout) 41 | assert.Equal(t, res.TaskLostBehavior, TaskLostBehaviorTypeRelaunchAfterTimeout) 42 | } 43 | 44 | app = app.EmptyResidency() 45 | 46 | if assert.NotNil(t, app.Residency) { 47 | assert.Equal(t, app.Residency, &Residency{}) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pod_instance_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | "github.com/stretchr/testify/require" 24 | ) 25 | 26 | const fakePodInstanceName = "fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003" 27 | 28 | func TestDeletePodInstance(t *testing.T) { 29 | endpoint := newFakeMarathonEndpoint(t, nil) 30 | defer endpoint.Close() 31 | 32 | podInstance, err := endpoint.Client.DeletePodInstance(fakePodName, fakePodInstanceName) 33 | require.NoError(t, err) 34 | assert.Equal(t, podInstance.InstanceID.ID, fakePodInstanceName) 35 | } 36 | 37 | func TestDeletePodInstances(t *testing.T) { 38 | endpoint := newFakeMarathonEndpoint(t, nil) 39 | defer endpoint.Close() 40 | 41 | instances := []string{fakePodInstanceName} 42 | podInstances, err := endpoint.Client.DeletePodInstances(fakePodName, instances) 43 | require.NoError(t, err) 44 | assert.Equal(t, podInstances[0].InstanceID.ID, fakePodInstanceName) 45 | } 46 | -------------------------------------------------------------------------------- /examples/multiple_endpoints/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "log" 22 | "time" 23 | 24 | marathon "github.com/gambol99/go-marathon" 25 | ) 26 | 27 | const waitTime = 5 * time.Second 28 | 29 | var marathonURL string 30 | 31 | func init() { 32 | flag.StringVar(&marathonURL, "url", "http://127.0.0.1:8080,127.0.0.1:8080", "the url for the marathon endpoint") 33 | } 34 | 35 | func main() { 36 | flag.Parse() 37 | config := marathon.NewDefaultConfig() 38 | config.URL = marathonURL 39 | client, err := marathon.NewClient(config) 40 | if err != nil { 41 | log.Fatalf("Failed to create a client for marathon, error: %s", err) 42 | } 43 | for { 44 | if application, err := client.Applications(nil); err != nil { 45 | log.Fatalf("Failed to retrieve a list of applications, error: %s", err) 46 | } else { 47 | log.Printf("Retrieved a list of applications, %v", application) 48 | } 49 | log.Printf("Going to sleep for %s\n", waitTime) 50 | time.Sleep(waitTime) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /examples/glog/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // go run main.go -logtostderr 18 | 19 | package main 20 | 21 | import ( 22 | "flag" 23 | 24 | marathon "github.com/gambol99/go-marathon" 25 | "github.com/golang/glog" 26 | ) 27 | 28 | var marathonURL string 29 | 30 | type logBridge struct{} 31 | 32 | func (l *logBridge) Write(b []byte) (n int, err error) { 33 | glog.InfoDepth(3, "go-marathon: "+string(b)) 34 | return len(b), nil 35 | } 36 | 37 | func init() { 38 | flag.StringVar(&marathonURL, "url", "http://127.0.0.1:8080", "the url for the marathon endpoint") 39 | } 40 | 41 | func main() { 42 | flag.Parse() 43 | config := marathon.NewDefaultConfig() 44 | config.URL = marathonURL 45 | config.LogOutput = new(logBridge) 46 | client, err := marathon.NewClient(config) 47 | if err != nil { 48 | glog.Exitln(err) 49 | } 50 | 51 | applications, err := client.Applications(nil) 52 | if err != nil { 53 | glog.Exitln(err) 54 | } 55 | 56 | for _, a := range applications.Apps { 57 | glog.Infof("App ID: %v\n", a.ID) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /info_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | func TestInfo(t *testing.T) { 26 | endpoint := newFakeMarathonEndpoint(t, nil) 27 | defer endpoint.Close() 28 | 29 | info, err := endpoint.Client.Info() 30 | assert.NoError(t, err) 31 | assert.Equal(t, info.FrameworkID, "20140730-222531-1863654316-5050-10422-0000") 32 | assert.Equal(t, info.Leader, "127.0.0.1:8080") 33 | assert.Equal(t, info.Version, "0.7.0-SNAPSHOT") 34 | } 35 | 36 | func TestLeader(t *testing.T) { 37 | endpoint := newFakeMarathonEndpoint(t, nil) 38 | defer endpoint.Close() 39 | 40 | leader, err := endpoint.Client.Leader() 41 | assert.NoError(t, err) 42 | assert.Equal(t, leader, "127.0.0.1:8080") 43 | } 44 | 45 | func TestAbdicateLeader(t *testing.T) { 46 | endpoint := newFakeMarathonEndpoint(t, nil) 47 | defer endpoint.Close() 48 | 49 | message, err := endpoint.Client.AbdicateLeader() 50 | assert.NoError(t, err) 51 | assert.Equal(t, message, "Leadership abdicted") 52 | } 53 | -------------------------------------------------------------------------------- /readiness_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "testing" 21 | "time" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestReadinessCheck(t *testing.T) { 27 | rc := ReadinessCheck{} 28 | rc.SetName("readiness"). 29 | SetProtocol("HTTP"). 30 | SetPath("/ready"). 31 | SetPortName("http"). 32 | SetInterval(3 * time.Second). 33 | SetTimeout(5 * time.Second). 34 | SetHTTPStatusCodesForReady([]int{200, 201}). 35 | SetPreserveLastResponse(true) 36 | 37 | if assert.NotNil(t, rc.Name) { 38 | assert.Equal(t, "readiness", *rc.Name) 39 | } 40 | assert.Equal(t, rc.Protocol, "HTTP") 41 | assert.Equal(t, rc.Path, "/ready") 42 | assert.Equal(t, rc.PortName, "http") 43 | assert.Equal(t, rc.IntervalSeconds, 3) 44 | assert.Equal(t, rc.TimeoutSeconds, 5) 45 | if assert.NotNil(t, rc.HTTPStatusCodesForReady) { 46 | assert.Equal(t, *rc.HTTPStatusCodesForReady, []int{200, 201}) 47 | } 48 | if assert.NotNil(t, rc.PreserveLastResponse) { 49 | assert.True(t, *rc.PreserveLastResponse) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /const.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | const ( 20 | defaultEventsURL = "/event" 21 | 22 | /* --- api related constants --- */ 23 | marathonAPIVersion = "v2" 24 | marathonAPIEventStream = marathonAPIVersion + "/events" 25 | marathonAPISubscription = marathonAPIVersion + "/eventSubscriptions" 26 | marathonAPIApps = marathonAPIVersion + "/apps" 27 | marathonAPIPods = marathonAPIVersion + "/pods" 28 | marathonAPITasks = marathonAPIVersion + "/tasks" 29 | marathonAPIDeployments = marathonAPIVersion + "/deployments" 30 | marathonAPIGroups = marathonAPIVersion + "/groups" 31 | marathonAPIQueue = marathonAPIVersion + "/queue" 32 | marathonAPIInfo = marathonAPIVersion + "/info" 33 | marathonAPILeader = marathonAPIVersion + "/leader" 34 | marathonAPIPing = "ping" 35 | ) 36 | 37 | const ( 38 | // EventsTransportCallback activates callback events transport 39 | EventsTransportCallback EventsTransport = 1 << iota 40 | 41 | // EventsTransportSSE activates stream events transport 42 | EventsTransportSSE 43 | ) 44 | -------------------------------------------------------------------------------- /health_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | func TestCommand(t *testing.T) { 26 | hc := new(HealthCheck) 27 | command := Command{"curl localhost:8080"} 28 | hc.SetCommand(command) 29 | assert.Equal(t, command, (*hc.Command)) 30 | } 31 | 32 | func TestPortIndex(t *testing.T) { 33 | hc := new(HealthCheck) 34 | hc.SetPortIndex(0) 35 | assert.Equal(t, 0, (*hc.PortIndex)) 36 | } 37 | 38 | func TestPort(t *testing.T) { 39 | hc := new(HealthCheck) 40 | hc.SetPort(8000) 41 | assert.Equal(t, 8000, (*hc.Port)) 42 | } 43 | 44 | func TestPath(t *testing.T) { 45 | hc := new(HealthCheck) 46 | hc.SetPath("/path") 47 | assert.Equal(t, "/path", (*hc.Path)) 48 | } 49 | 50 | func TestMaxConsecutiveFailures(t *testing.T) { 51 | hc := new(HealthCheck) 52 | hc.SetMaxConsecutiveFailures(3) 53 | assert.Equal(t, 3, (*hc.MaxConsecutiveFailures)) 54 | } 55 | 56 | func TestIgnoreHTTP1xx(t *testing.T) { 57 | hc := new(HealthCheck) 58 | hc.SetIgnoreHTTP1xx(true) 59 | assert.True(t, (*hc.IgnoreHTTP1xx)) 60 | } 61 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Author: Rohith (gambol99@gmail.com) 3 | # Date: 2015-02-10 15:35:14 +0000 (Tue, 10 Feb 2015) 4 | # 5 | # vim:ts=2:sw=2:et 6 | # 7 | HARDWARE=$(shell uname -m) 8 | VERSION=$(shell awk '/const Version/ { print $$4 }' version.go | sed 's/"//g') 9 | DEPS=$(shell go list -f '{{range .TestImports}}{{.}} {{end}}' ./...) 10 | PACKAGES=$(shell go list ./...) 11 | VETARGS?=-asmdecl -atomic -bool -buildtags -copylocks -methods -nilfunc -printf -rangeloops -shift -structtags -unsafeptr 12 | 13 | .PHONY: test examples changelog check-format coverage cover 14 | 15 | build: 16 | go build 17 | 18 | deps: 19 | @echo "--> Installing build dependencies" 20 | @go get -d -v ./... $(DEPS) 21 | 22 | lint: 23 | @echo "--> Running golint" 24 | @which golint 2>/dev/null ; if [ $$? -eq 1 ]; then \ 25 | go get -u github.com/golang/lint/golint; \ 26 | fi 27 | @golint . 28 | 29 | vet: 30 | @echo "--> Running go tool vet $(VETARGS) ." 31 | @go tool vet 2>/dev/null ; if [ $$? -eq 3 ]; then \ 32 | go get golang.org/x/tools/cmd/vet; \ 33 | fi 34 | @go tool vet $(VETARGS) . 35 | 36 | cover: 37 | @echo "--> Running go test --cover" 38 | @go test --cover 39 | 40 | coverage: 41 | @echo "--> Running go coverage" 42 | @go test -covermode=count -coverprofile=coverage 43 | 44 | format: 45 | @echo "--> Running go fmt" 46 | @go fmt $(PACKAGES) 47 | 48 | check-format: 49 | @echo "--> Checking format" 50 | @if gofmt -l . 2>&1 | grep -q '.go'; then \ 51 | echo "found unformatted files:"; \ 52 | echo; \ 53 | gofmt -l .; \ 54 | exit 1; \ 55 | fi 56 | 57 | test: deps vet 58 | @echo "--> Running go tests" 59 | @go test -race -v 60 | @$(MAKE) cover 61 | 62 | examples: 63 | make -C examples all 64 | 65 | changelog: release 66 | git log $(shell git tag | tail -n1)..HEAD --no-merges --format=%B > changelog 67 | -------------------------------------------------------------------------------- /examples/tasks/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "time" 22 | 23 | marathon "github.com/gambol99/go-marathon" 24 | ) 25 | 26 | const marathonURL = "http://127.0.0.1:8080" 27 | 28 | func main() { 29 | config := marathon.NewDefaultConfig() 30 | config.URL = marathonURL 31 | client, err := marathon.NewClient(config) 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | app := marathon.Application{} 37 | app.ID = "tasks-test" 38 | app.Command("sleep 60") 39 | app.Count(3) 40 | fmt.Println("Creating app.") 41 | // Update application will either create or update the app. 42 | _, err = client.UpdateApplication(&app, false) 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | // wait until marathon will launch tasks 48 | client.WaitOnApplication(app.ID, 10*time.Second) 49 | fmt.Println("Tasks were deployed.") 50 | 51 | tasks, err := client.Tasks(app.ID) 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | host := tasks.Tasks[0].Host 57 | fmt.Printf("Killing tasks on the host: %s\n", host) 58 | 59 | _, err = client.KillApplicationTasks(app.ID, &marathon.KillApplicationTasksOpts{Scale: true, Host: host}) 60 | if err != nil { 61 | panic(err) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /offer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package marathon 17 | 18 | // based on https://github.com/mesosphere/marathon/blob/e7b1456ad0cfba23c9fdfa3c5d638a4b9aeb60d0/docs/docs/rest-api/public/api/v2/types/offer.raml 19 | 20 | // Offer describes a Mesos offer to a framework 21 | type Offer struct { 22 | ID string `json:"id"` 23 | Hostname string `json:"hostname"` 24 | AgentID string `json:"agentId"` 25 | Resources []OfferResource `json:"resources"` 26 | Attributes []AgentAttribute `json:"attributes"` 27 | } 28 | 29 | // OfferResource describes a resource that is part of an offer 30 | type OfferResource struct { 31 | Name string `json:"name"` 32 | Role string `json:"role"` 33 | Scalar *float64 `json:"scalar,omitempty"` 34 | Ranges []NumberRange `json:"ranges,omitempty"` 35 | Set []string `json:"set,omitempty"` 36 | } 37 | 38 | // NumberRange is a range of numbers 39 | type NumberRange struct { 40 | Begin int64 `json:"begin"` 41 | End int64 `json:"end"` 42 | } 43 | 44 | // AgentAttribute describes an attribute of an agent node 45 | type AgentAttribute struct { 46 | Name string `json:"name"` 47 | Text *string `json:"text,omitempty"` 48 | Scalar *float64 `json:"scalar,omitempty"` 49 | Ranges []NumberRange `json:"ranges,omitempty"` 50 | Set []string `json:"set,omitempty"` 51 | } 52 | -------------------------------------------------------------------------------- /volume.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | // PodVolume describes a volume on the host 20 | type PodVolume struct { 21 | Name string `json:"name,omitempty"` 22 | Host string `json:"host,omitempty"` 23 | Secret string `json:"secret,omitempty"` 24 | Persistent *PersistentVolume `json:"persistent,omitempty"` 25 | } 26 | 27 | // PodVolumeMount describes how to mount a volume into a task 28 | type PodVolumeMount struct { 29 | Name string `json:"name,omitempty"` 30 | MountPath string `json:"mountPath,omitempty"` 31 | ReadOnly *bool `json:"readOnly,omitempty"` 32 | } 33 | 34 | // NewPodVolume creates a new PodVolume 35 | func NewPodVolume(name, path string) *PodVolume { 36 | return &PodVolume{ 37 | Name: name, 38 | Host: path, 39 | } 40 | } 41 | 42 | // NewPodVolume creates a new PodVolume for file based secrets 43 | func NewPodVolumeSecret(name, secretPath string) *PodVolume { 44 | return &PodVolume{ 45 | Name: name, 46 | Secret: secretPath, 47 | } 48 | } 49 | 50 | // NewPodVolumeMount creates a new PodVolumeMount 51 | func NewPodVolumeMount(name, mount string) *PodVolumeMount { 52 | return &PodVolumeMount{ 53 | Name: name, 54 | MountPath: mount, 55 | } 56 | } 57 | 58 | // SetPersistentVolume sets the persistence settings of a PodVolume 59 | func (pv *PodVolume) SetPersistentVolume(p *PersistentVolume) *PodVolume { 60 | pv.Persistent = p 61 | return pv 62 | } 63 | -------------------------------------------------------------------------------- /pod_status_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "testing" 21 | "time" 22 | 23 | "github.com/stretchr/testify/assert" 24 | "github.com/stretchr/testify/require" 25 | ) 26 | 27 | func TestGetPodStatus(t *testing.T) { 28 | endpoint := newFakeMarathonEndpoint(t, nil) 29 | defer endpoint.Close() 30 | 31 | podStatus, err := endpoint.Client.PodStatus(fakePodName) 32 | require.NoError(t, err) 33 | 34 | if assert.NotNil(t, podStatus) { 35 | assert.Equal(t, podStatus.Spec.ID, fakePodName) 36 | } 37 | } 38 | 39 | func TestGetAllPodStatus(t *testing.T) { 40 | endpoint := newFakeMarathonEndpoint(t, nil) 41 | defer endpoint.Close() 42 | 43 | podStatuses, err := endpoint.Client.PodStatuses() 44 | require.NoError(t, err) 45 | assert.Equal(t, podStatuses[0].Spec.ID, fakePodName) 46 | } 47 | 48 | func TestWaitOnPod(t *testing.T) { 49 | endpoint := newFakeMarathonEndpoint(t, nil) 50 | defer endpoint.Close() 51 | 52 | err := endpoint.Client.WaitOnPod(fakePodName, 1*time.Microsecond) 53 | require.NoError(t, err) 54 | } 55 | 56 | func TestPodIsRunning(t *testing.T) { 57 | endpoint := newFakeMarathonEndpoint(t, nil) 58 | defer endpoint.Close() 59 | 60 | exists := endpoint.Client.PodIsRunning(fakePodName) 61 | assert.True(t, exists) 62 | 63 | exists = endpoint.Client.PodIsRunning("not_existing") 64 | assert.False(t, exists) 65 | 66 | exists = endpoint.Client.PodIsRunning(secondFakePodName) 67 | assert.False(t, exists) 68 | } 69 | -------------------------------------------------------------------------------- /residency.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import "time" 20 | 21 | // TaskLostBehaviorType sets action taken when the resident task is lost 22 | type TaskLostBehaviorType string 23 | 24 | const ( 25 | // TaskLostBehaviorTypeWaitForever indicates to not take any action when the resident task is lost 26 | TaskLostBehaviorTypeWaitForever TaskLostBehaviorType = "WAIT_FOREVER" 27 | // TaskLostBehaviorTypeRelaunchAfterTimeout indicates to try relaunching the lost resident task on 28 | // another node after the relaunch escalation timeout has elapsed 29 | TaskLostBehaviorTypeRelaunchAfterTimeout TaskLostBehaviorType = "RELAUNCH_AFTER_TIMEOUT" 30 | ) 31 | 32 | // Residency defines how terminal states of tasks with local persistent volumes are handled 33 | type Residency struct { 34 | TaskLostBehavior TaskLostBehaviorType `json:"taskLostBehavior,omitempty"` 35 | RelaunchEscalationTimeoutSeconds int `json:"relaunchEscalationTimeoutSeconds,omitempty"` 36 | } 37 | 38 | // SetTaskLostBehavior sets the residency behavior 39 | func (r *Residency) SetTaskLostBehavior(behavior TaskLostBehaviorType) *Residency { 40 | r.TaskLostBehavior = behavior 41 | return r 42 | } 43 | 44 | // SetRelaunchEscalationTimeout sets the residency relaunch escalation timeout with seconds precision 45 | func (r *Residency) SetRelaunchEscalationTimeout(timeout time.Duration) *Residency { 46 | r.RelaunchEscalationTimeoutSeconds = int(timeout.Seconds()) 47 | return r 48 | } 49 | -------------------------------------------------------------------------------- /pod_container_image.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | // ImageType represents the image format type 20 | type ImageType string 21 | 22 | const ( 23 | // ImageTypeDocker is the docker format 24 | ImageTypeDocker ImageType = "DOCKER" 25 | 26 | // ImageTypeAppC is the appc format 27 | ImageTypeAppC ImageType = "APPC" 28 | ) 29 | 30 | // PodContainerImage describes how to retrieve the container image 31 | type PodContainerImage struct { 32 | Kind ImageType `json:"kind,omitempty"` 33 | ID string `json:"id,omitempty"` 34 | ForcePull *bool `json:"forcePull,omitempty"` 35 | PullConfig *PullConfig `json:"pullConfig,omitempty"` 36 | } 37 | 38 | // NewPodContainerImage creates an empty PodContainerImage 39 | func NewPodContainerImage() *PodContainerImage { 40 | return &PodContainerImage{} 41 | } 42 | 43 | // SetKind sets the Kind of the image 44 | func (i *PodContainerImage) SetKind(typ ImageType) *PodContainerImage { 45 | i.Kind = typ 46 | return i 47 | } 48 | 49 | // SetID sets the ID of the image 50 | func (i *PodContainerImage) SetID(id string) *PodContainerImage { 51 | i.ID = id 52 | return i 53 | } 54 | 55 | // SetPullConfig adds *PullConfig to PodContainerImage 56 | func (i *PodContainerImage) SetPullConfig(pullConfig *PullConfig) *PodContainerImage { 57 | i.PullConfig = pullConfig 58 | 59 | return i 60 | } 61 | 62 | // NewDockerPodContainerImage creates a docker PodContainerImage 63 | func NewDockerPodContainerImage() *PodContainerImage { 64 | return NewPodContainerImage().SetKind(ImageTypeDocker) 65 | } 66 | -------------------------------------------------------------------------------- /examples/queue/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "fmt" 22 | "log" 23 | "time" 24 | 25 | marathon "github.com/gambol99/go-marathon" 26 | ) 27 | 28 | var marathonURL string 29 | 30 | func init() { 31 | flag.StringVar(&marathonURL, "url", "http://127.0.0.1:8080", "the url for the marathon endpoint") 32 | } 33 | 34 | func main() { 35 | config := marathon.NewDefaultConfig() 36 | config.URL = marathonURL 37 | client, err := marathon.NewClient(config) 38 | if err != nil { 39 | log.Fatalf("Make new marathon client error: %v", err) 40 | } 41 | 42 | app := marathon.Application{} 43 | app.ID = "queue-test" 44 | app.Command("sleep 5") 45 | app.Count(1) 46 | app.Memory(32) 47 | fmt.Println("Creating/updating app.") 48 | // Update application will either create or update the app. 49 | _, err = client.UpdateApplication(&app, false) 50 | if err != nil { 51 | log.Fatalf("Update application error: %v", err) 52 | } 53 | // wait until marathon will launch tasks 54 | err = client.WaitOnApplication(app.ID, 10*time.Second) 55 | if err != nil { 56 | log.Fatalln("Application deploy failure, timeout.") 57 | } 58 | fmt.Println("Application was deployed.") 59 | 60 | // get marathon queue by chance 61 | for i := 0; i < 30; i++ { 62 | // Avoid shadowing err from outer scope. 63 | var queue *marathon.Queue 64 | queue, err = client.Queue() 65 | if err != nil { 66 | log.Fatalf("Get queue error: %v\n", err) 67 | } 68 | if len(queue.Items) > 0 { 69 | fmt.Println(queue) 70 | break 71 | } 72 | fmt.Printf("Queue is blank now, retry(%d)...\n", 30-i) 73 | time.Sleep(time.Second) 74 | } 75 | 76 | // delete marathon queue delay 77 | err = client.DeleteQueueDelay(app.ID) 78 | if err != nil { 79 | log.Fatalf("Delete queue delay error: %v\n", err) 80 | } 81 | fmt.Println("Queue delay deleted.") 82 | 83 | return 84 | } 85 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "io" 21 | "io/ioutil" 22 | "net/http" 23 | "time" 24 | ) 25 | 26 | const defaultPollingWaitTime = 500 * time.Millisecond 27 | 28 | const defaultDCOSPath = "marathon" 29 | 30 | // EventsTransport describes which transport should be used to deliver Marathon events 31 | type EventsTransport int 32 | 33 | // Config holds the settings and options for the client 34 | type Config struct { 35 | // URL is the url for marathon 36 | URL string 37 | // EventsTransport is the events transport: EventsTransportCallback or EventsTransportSSE 38 | EventsTransport EventsTransport 39 | // EventsPort is the event handler port 40 | EventsPort int 41 | // the interface we should be listening on for events 42 | EventsInterface string 43 | // HTTPBasicAuthUser is the http basic auth 44 | HTTPBasicAuthUser string 45 | // HTTPBasicPassword is the http basic password 46 | HTTPBasicPassword string 47 | // CallbackURL custom callback url 48 | CallbackURL string 49 | // DCOSToken for DCOS environment, This will override the Authorization header 50 | DCOSToken string 51 | // LogOutput the output for debug log messages 52 | LogOutput io.Writer 53 | // HTTPClient is the HTTP client 54 | HTTPClient *http.Client 55 | // HTTPSSEClient is the HTTP client used for SSE subscriptions, can't have client.Timeout set 56 | HTTPSSEClient *http.Client 57 | // wait time (in milliseconds) between repetitive requests to the API during polling 58 | PollingWaitTime time.Duration 59 | } 60 | 61 | // NewDefaultConfig create a default client config 62 | func NewDefaultConfig() Config { 63 | return Config{ 64 | URL: "http://127.0.0.1:8080", 65 | EventsTransport: EventsTransportCallback, 66 | EventsPort: 10001, 67 | EventsInterface: "eth0", 68 | LogOutput: ioutil.Discard, 69 | PollingWaitTime: defaultPollingWaitTime, 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /examples/events_sse_transport/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "log" 22 | "time" 23 | 24 | marathon "github.com/gambol99/go-marathon" 25 | ) 26 | 27 | var marathonURL string 28 | var timeout int 29 | 30 | func init() { 31 | flag.StringVar(&marathonURL, "url", "http://127.0.0.1:8080", "the url for the Marathon endpoint") 32 | flag.IntVar(&timeout, "timeout", 60, "listen to events for x seconds") 33 | } 34 | 35 | func assert(err error) { 36 | if err != nil { 37 | log.Fatalf("Failed, error: %s", err) 38 | } 39 | } 40 | 41 | func main() { 42 | flag.Parse() 43 | config := marathon.NewDefaultConfig() 44 | config.URL = marathonURL 45 | config.EventsTransport = marathon.EventsTransportSSE 46 | log.Printf("Creating a client, Marathon: %s", marathonURL) 47 | 48 | client, err := marathon.NewClient(config) 49 | assert(err) 50 | 51 | // Register for events 52 | events, err := client.AddEventsListener(marathon.EventIDApplications) 53 | assert(err) 54 | deployments, err := client.AddEventsListener(marathon.EventIDDeploymentStepSuccess) 55 | assert(err) 56 | 57 | // Listen for x seconds and then split 58 | timer := time.After(time.Duration(timeout) * time.Second) 59 | done := false 60 | for { 61 | if done { 62 | break 63 | } 64 | select { 65 | case <-timer: 66 | log.Printf("Exiting the loop") 67 | done = true 68 | case event := <-events: 69 | log.Printf("Received application event: %s", event) 70 | case event := <-deployments: 71 | log.Printf("Received deployment event: %v", event) 72 | var deployment *marathon.EventDeploymentStepSuccess 73 | deployment = event.Event.(*marathon.EventDeploymentStepSuccess) 74 | log.Printf("deployment step: %v", deployment.CurrentStep) 75 | } 76 | } 77 | 78 | log.Printf("Removing our subscription") 79 | client.RemoveEventsListener(events) 80 | client.RemoveEventsListener(deployments) 81 | } 82 | -------------------------------------------------------------------------------- /port_definition.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | // PortDefinition is a definition of a port that should be considered 20 | // part of a resource. Port definitions are necessary when you are 21 | // using HOST networking and no port mappings are specified. 22 | type PortDefinition struct { 23 | Port *int `json:"port,omitempty"` 24 | Protocol string `json:"protocol,omitempty"` 25 | Name string `json:"name,omitempty"` 26 | Labels *map[string]string `json:"labels,omitempty"` 27 | } 28 | 29 | // SetPort sets the given port for the PortDefinition 30 | func (p *PortDefinition) SetPort(port int) *PortDefinition { 31 | if p.Port == nil { 32 | p.EmptyPort() 33 | } 34 | p.Port = &port 35 | return p 36 | } 37 | 38 | // EmptyPort sets the port to 0 for the PortDefinition 39 | func (p *PortDefinition) EmptyPort() *PortDefinition { 40 | port := 0 41 | p.Port = &port 42 | return p 43 | } 44 | 45 | // SetProtocol sets the protocol for the PortDefinition 46 | // protocol: the protocol as a string 47 | func (p *PortDefinition) SetProtocol(protocol string) *PortDefinition { 48 | p.Protocol = protocol 49 | return p 50 | } 51 | 52 | // SetName sets the name for the PortDefinition 53 | // name: the name of the PortDefinition 54 | func (p *PortDefinition) SetName(name string) *PortDefinition { 55 | p.Name = name 56 | return p 57 | } 58 | 59 | // AddLabel adds a label to the PortDefinition 60 | // name: the name of the label 61 | // value: value for this label 62 | func (p *PortDefinition) AddLabel(name, value string) *PortDefinition { 63 | if p.Labels == nil { 64 | p.EmptyLabels() 65 | } 66 | (*p.Labels)[name] = value 67 | 68 | return p 69 | } 70 | 71 | // EmptyLabels explicitly empties the labels -- use this if you need to empty 72 | // the labels of a PortDefinition that already has labels set 73 | // (setting labels to nill will keep the current value) 74 | func (p *PortDefinition) EmptyLabels() *PortDefinition { 75 | p.Labels = &map[string]string{} 76 | 77 | return p 78 | } 79 | -------------------------------------------------------------------------------- /examples/events_callback_transport/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "log" 22 | "time" 23 | 24 | marathon "github.com/gambol99/go-marathon" 25 | ) 26 | 27 | var marathonURL string 28 | var marathonInterface string 29 | var marathonPort int 30 | var timeout int 31 | 32 | func init() { 33 | flag.StringVar(&marathonURL, "url", "http://127.0.0.1:8080", "the url for the Marathon endpoint") 34 | flag.StringVar(&marathonInterface, "interface", "eth0", "the interface we should use for events") 35 | flag.IntVar(&marathonPort, "port", 19999, "the port the events service should run on") 36 | flag.IntVar(&timeout, "timeout", 60, "listen to events for x seconds") 37 | } 38 | 39 | func assert(err error) { 40 | if err != nil { 41 | log.Fatalf("Failed, error: %s", err) 42 | } 43 | } 44 | 45 | func main() { 46 | flag.Parse() 47 | config := marathon.NewDefaultConfig() 48 | config.URL = marathonURL 49 | config.EventsInterface = marathonInterface 50 | config.EventsPort = marathonPort 51 | log.Printf("Creating a client, Marathon: %s", marathonURL) 52 | 53 | client, err := marathon.NewClient(config) 54 | assert(err) 55 | 56 | // Register for events 57 | events, err := client.AddEventsListener(marathon.EventIDApplications) 58 | assert(err) 59 | deployments, err := client.AddEventsListener(marathon.EventIDDeploymentStepSuccess) 60 | assert(err) 61 | 62 | // Listen for x seconds and then split 63 | timer := time.After(time.Duration(timeout) * time.Second) 64 | done := false 65 | for { 66 | if done { 67 | break 68 | } 69 | select { 70 | case <-timer: 71 | log.Printf("Exiting the loop") 72 | done = true 73 | case event := <-events: 74 | log.Printf("Received application event: %s", event) 75 | case event := <-deployments: 76 | log.Printf("Received deployment event: %v", event) 77 | var deployment *marathon.EventDeploymentStepSuccess 78 | deployment = event.Event.(*marathon.EventDeploymentStepSuccess) 79 | log.Printf("deployment step: %v", deployment.CurrentStep) 80 | } 81 | } 82 | 83 | log.Printf("Removing our subscription") 84 | client.RemoveEventsListener(events) 85 | client.RemoveEventsListener(deployments) 86 | } 87 | -------------------------------------------------------------------------------- /unreachable_strategy.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | ) 23 | 24 | // UnreachableStrategyAbsenceReasonDisabled signifies the reason of disabled unreachable strategy 25 | const UnreachableStrategyAbsenceReasonDisabled = "disabled" 26 | 27 | // UnreachableStrategy is the unreachable strategy applied to an application. 28 | type UnreachableStrategy struct { 29 | EnabledUnreachableStrategy 30 | AbsenceReason string 31 | } 32 | 33 | // EnabledUnreachableStrategy covers parameters pertaining to present unreachable strategies. 34 | type EnabledUnreachableStrategy struct { 35 | InactiveAfterSeconds *float64 `json:"inactiveAfterSeconds,omitempty"` 36 | ExpungeAfterSeconds *float64 `json:"expungeAfterSeconds,omitempty"` 37 | } 38 | 39 | type unreachableStrategy UnreachableStrategy 40 | 41 | // UnmarshalJSON unmarshals the given JSON into an UnreachableStrategy. It 42 | // populates parameters for present strategies, and otherwise only sets the 43 | // absence reason. 44 | func (us *UnreachableStrategy) UnmarshalJSON(b []byte) error { 45 | var u unreachableStrategy 46 | var errEnabledUS, errNonEnabledUS error 47 | if errEnabledUS = json.Unmarshal(b, &u); errEnabledUS == nil { 48 | *us = UnreachableStrategy(u) 49 | return nil 50 | } 51 | 52 | if errNonEnabledUS = json.Unmarshal(b, &us.AbsenceReason); errNonEnabledUS == nil { 53 | return nil 54 | } 55 | 56 | return fmt.Errorf("failed to unmarshal unreachable strategy: unmarshaling into enabled returned error '%s'; unmarshaling into non-enabled returned error '%s'", errEnabledUS, errNonEnabledUS) 57 | } 58 | 59 | // MarshalJSON marshals the unreachable strategy. 60 | func (us *UnreachableStrategy) MarshalJSON() ([]byte, error) { 61 | if us.AbsenceReason == "" { 62 | return json.Marshal(us.EnabledUnreachableStrategy) 63 | } 64 | 65 | return json.Marshal(us.AbsenceReason) 66 | } 67 | 68 | // SetInactiveAfterSeconds sets the period after which instance will be marked as inactive. 69 | func (us *UnreachableStrategy) SetInactiveAfterSeconds(cap float64) *UnreachableStrategy { 70 | us.InactiveAfterSeconds = &cap 71 | return us 72 | } 73 | 74 | // SetExpungeAfterSeconds sets the period after which instance will be expunged. 75 | func (us *UnreachableStrategy) SetExpungeAfterSeconds(cap float64) *UnreachableStrategy { 76 | us.ExpungeAfterSeconds = &cap 77 | return us 78 | } 79 | -------------------------------------------------------------------------------- /deployment_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | "github.com/stretchr/testify/require" 24 | ) 25 | 26 | func TestDeployments(t *testing.T) { 27 | endpoint := newFakeMarathonEndpoint(t, nil) 28 | defer endpoint.Close() 29 | 30 | deployments, err := endpoint.Client.Deployments() 31 | require.NoError(t, err) 32 | require.NotNil(t, deployments) 33 | require.Equal(t, len(deployments), 1) 34 | deployment := deployments[0] 35 | require.NotNil(t, deployment) 36 | assert.Equal(t, deployment.ID, "867ed450-f6a8-4d33-9b0e-e11c5513990b") 37 | require.NotNil(t, deployment.Steps) 38 | assert.Equal(t, len(deployment.Steps), 1) 39 | } 40 | 41 | func TestDeploymentsV1(t *testing.T) { 42 | endpoint := newFakeMarathonEndpoint(t, &configContainer{ 43 | server: &serverConfig{ 44 | scope: "v1.1.1", 45 | }, 46 | }) 47 | defer endpoint.Close() 48 | deployments, err := endpoint.Client.Deployments() 49 | assert.NoError(t, err) 50 | assert.NotNil(t, deployments) 51 | assert.Equal(t, len(deployments), 1) 52 | deployment := deployments[0] 53 | assert.NotNil(t, deployment) 54 | assert.Equal(t, deployment.ID, "2620aa06-1001-4eea-8861-a51957d4fd80") 55 | assert.NotNil(t, deployment.Steps) 56 | assert.Equal(t, len(deployment.Steps), 2) 57 | 58 | require.Equal(t, len(deployment.CurrentActions), 1) 59 | curAction := deployment.CurrentActions[0] 60 | require.NotNil(t, curAction) 61 | require.NotNil(t, curAction.ReadinessCheckResults) 62 | require.True(t, len(*curAction.ReadinessCheckResults) > 0) 63 | actualRes := (*curAction.ReadinessCheckResults)[0] 64 | expectedRes := ReadinessCheckResult{ 65 | Name: "myReadyCheck", 66 | TaskID: "test_frontend_app1.c9de6033", 67 | Ready: false, 68 | LastResponse: ReadinessLastResponse{ 69 | Body: "{}", 70 | ContentType: "application/json", 71 | Status: 500, 72 | }, 73 | } 74 | assert.Equal(t, expectedRes, actualRes) 75 | } 76 | 77 | func TestDeleteDeployment(t *testing.T) { 78 | endpoint := newFakeMarathonEndpoint(t, nil) 79 | defer endpoint.Close() 80 | id, err := endpoint.Client.DeleteDeployment(fakeDeploymentID, false) 81 | require.NoError(t, err) 82 | assert.Equal(t, id.DeploymentID, "0b1467fc-d5cd-4bbc-bac2-2805351cee1e") 83 | assert.Equal(t, id.Version, "2014-08-26T08:20:26.171Z") 84 | } 85 | 86 | func TestDeleteDeploymentForce(t *testing.T) { 87 | endpoint := newFakeMarathonEndpoint(t, nil) 88 | defer endpoint.Close() 89 | resp, err := endpoint.Client.DeleteDeployment(fakeDeploymentID, true) 90 | require.NoError(t, err) 91 | assert.Nil(t, resp) 92 | } 93 | -------------------------------------------------------------------------------- /queue.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "fmt" 21 | ) 22 | 23 | // Queue is the definition of marathon queue 24 | type Queue struct { 25 | Items []Item `json:"queue"` 26 | } 27 | 28 | // Item is the definition of element in the queue 29 | type Item struct { 30 | Count int `json:"count"` 31 | Delay Delay `json:"delay"` 32 | Application *Application `json:"app"` 33 | Pod *Pod `json:"pod"` 34 | Role string `json:"role"` 35 | Since string `json:"since"` 36 | ProcessedOffersSummary ProcessedOffersSummary `json:"processedOffersSummary"` 37 | LastUnusedOffers []UnusedOffer `json:"lastUnusedOffers,omitempty"` 38 | } 39 | 40 | // Delay cotains the application postpone information 41 | type Delay struct { 42 | Overdue bool `json:"overdue"` 43 | TimeLeftSeconds int `json:"timeLeftSeconds"` 44 | } 45 | 46 | // ProcessedOffersSummary contains statistics for processed offers. 47 | type ProcessedOffersSummary struct { 48 | ProcessedOffersCount int32 `json:"processedOffersCount"` 49 | UnusedOffersCount int32 `json:"unusedOffersCount"` 50 | LastUnusedOfferAt *string `json:"lastUnusedOfferAt,omitempty"` 51 | LastUsedOfferAt *string `json:"lastUsedOfferAt,omitempty"` 52 | RejectSummaryLastOffers []DeclinedOfferStep `json:"rejectSummaryLastOffers,omitempty"` 53 | RejectSummaryLaunchAttempt []DeclinedOfferStep `json:"rejectSummaryLaunchAttempt,omitempty"` 54 | } 55 | 56 | // DeclinedOfferStep contains how often an offer was declined for a specific reason 57 | type DeclinedOfferStep struct { 58 | Reason string `json:"reason"` 59 | Declined int32 `json:"declined"` 60 | Processed int32 `json:"processed"` 61 | } 62 | 63 | // UnusedOffer contains which offers weren't used and why 64 | type UnusedOffer struct { 65 | Offer Offer `json:"offer"` 66 | Reason []string `json:"reason"` 67 | Timestamp string `json:"timestamp"` 68 | } 69 | 70 | // Queue retrieves content of the marathon launch queue 71 | func (r *marathonClient) Queue() (*Queue, error) { 72 | var queue *Queue 73 | err := r.apiGet(marathonAPIQueue, nil, &queue) 74 | if err != nil { 75 | return nil, err 76 | } 77 | return queue, nil 78 | } 79 | 80 | // DeleteQueueDelay resets task launch delay of the specific application 81 | // appID: the ID of the application 82 | func (r *marathonClient) DeleteQueueDelay(appID string) error { 83 | path := fmt.Sprintf("%s/%s/delay", marathonAPIQueue, trimRootPath(appID)) 84 | return r.apiDelete(path, nil, nil) 85 | } 86 | -------------------------------------------------------------------------------- /group_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | func TestGroups(t *testing.T) { 26 | endpoint := newFakeMarathonEndpoint(t, nil) 27 | defer endpoint.Close() 28 | 29 | groups, err := endpoint.Client.Groups() 30 | assert.NoError(t, err) 31 | assert.NotNil(t, groups) 32 | assert.Equal(t, 1, len(groups.Groups)) 33 | group := groups.Groups[0] 34 | assert.Equal(t, fakeGroupName, group.ID) 35 | } 36 | 37 | func TestNewGroup(t *testing.T) { 38 | endpoint := newFakeMarathonEndpoint(t, nil) 39 | defer endpoint.Close() 40 | 41 | group, err := endpoint.Client.Group(fakeGroupName) 42 | assert.NoError(t, err) 43 | assert.NotNil(t, group) 44 | assert.Equal(t, 1, len(group.Apps)) 45 | assert.Equal(t, fakeGroupName, group.ID) 46 | 47 | group, err = endpoint.Client.Group(fakeGroupName1) 48 | 49 | assert.NoError(t, err) 50 | assert.NotNil(t, group) 51 | assert.Equal(t, fakeGroupName1, group.ID) 52 | assert.NotNil(t, group.Groups) 53 | assert.Equal(t, 1, len(group.Groups)) 54 | 55 | frontend := group.Groups[0] 56 | assert.Equal(t, "frontend", frontend.ID) 57 | assert.Equal(t, 3, len(frontend.Apps)) 58 | for _, app := range frontend.Apps { 59 | assert.NotNil(t, app.Container) 60 | assert.NotNil(t, app.Container.Docker) 61 | for _, network := range *app.Networks { 62 | assert.Equal(t, BridgeNetworkMode, network.Mode) 63 | } 64 | if len(*app.Container.PortMappings) == 0 { 65 | t.Fail() 66 | } 67 | } 68 | } 69 | 70 | // TODO @kamsz: How to work with old and new endpoints from methods.yml? 71 | // func TestGroup(t *testing.T) { 72 | // endpoint := newFakeMarathonEndpoint(t, nil) 73 | // defer endpoint.Close() 74 | 75 | // group, err := endpoint.Client.Group(fakeGroupName) 76 | // assert.NoError(t, err) 77 | // assert.NotNil(t, group) 78 | // assert.Equal(t, 1, len(group.Apps)) 79 | // assert.Equal(t, fakeGroupName, group.ID) 80 | 81 | // group, err = endpoint.Client.Group(fakeGroupName1) 82 | 83 | // assert.NoError(t, err) 84 | // assert.NotNil(t, group) 85 | // assert.Equal(t, fakeGroupName1, group.ID) 86 | // assert.NotNil(t, group.Groups) 87 | // assert.Equal(t, 1, len(group.Groups)) 88 | 89 | // frontend := group.Groups[0] 90 | // assert.Equal(t, "frontend", frontend.ID) 91 | // assert.Equal(t, 3, len(frontend.Apps)) 92 | // for _, app := range frontend.Apps { 93 | // assert.NotNil(t, app.Container) 94 | // assert.NotNil(t, app.Container.Docker) 95 | // assert.Equal(t, "BRIDGE", app.Container.Docker.Network) 96 | // if len(*app.Container.Docker.PortMappings) == 0 { 97 | // t.Fail() 98 | // } 99 | // } 100 | // } 101 | -------------------------------------------------------------------------------- /pod_container_marshalling.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | ) 23 | 24 | // PodContainerAlias aliases the PodContainer struct so that it will be marshaled/unmarshaled automatically 25 | type PodContainerAlias PodContainer 26 | 27 | // UnmarshalJSON unmarshals the given PodContainer JSON as expected except for environment variables and secrets. 28 | // Environment variables are stored in the Env field. Secrets, including the environment variable part, 29 | // are stored in the Secrets field. 30 | func (p *PodContainer) UnmarshalJSON(b []byte) error { 31 | aux := &struct { 32 | *PodContainerAlias 33 | Env map[string]interface{} `json:"environment"` 34 | }{ 35 | PodContainerAlias: (*PodContainerAlias)(p), 36 | } 37 | if err := json.Unmarshal(b, aux); err != nil { 38 | return fmt.Errorf("malformed pod container definition %v", err) 39 | } 40 | env := map[string]string{} 41 | secrets := map[string]Secret{} 42 | 43 | for envName, genericEnvValue := range aux.Env { 44 | switch envValOrSecret := genericEnvValue.(type) { 45 | case string: 46 | env[envName] = envValOrSecret 47 | case map[string]interface{}: 48 | for secret, secretStore := range envValOrSecret { 49 | if secStore, ok := secretStore.(string); ok && secret == "secret" { 50 | secrets[secStore] = Secret{EnvVar: envName} 51 | break 52 | } 53 | return fmt.Errorf("unexpected secret field %v of value type %T", secret, envValOrSecret[secret]) 54 | } 55 | default: 56 | return fmt.Errorf("unexpected environment variable type %T", envValOrSecret) 57 | } 58 | } 59 | p.Env = env 60 | for k, v := range aux.Secrets { 61 | tmp := secrets[k] 62 | tmp.Source = v.Source 63 | secrets[k] = tmp 64 | } 65 | p.Secrets = secrets 66 | return nil 67 | } 68 | 69 | // MarshalJSON marshals the given PodContainer as expected except for environment variables and secrets, 70 | // which are marshaled from specialized structs. The environment variable piece of the secrets and other 71 | // normal environment variables are combined and marshaled to the env field. The secrets and the related 72 | // source are marshaled into the secrets field. 73 | func (p *PodContainer) MarshalJSON() ([]byte, error) { 74 | env := make(map[string]interface{}) 75 | secrets := make(map[string]TmpSecret) 76 | 77 | if p.Env != nil { 78 | for k, v := range p.Env { 79 | env[string(k)] = string(v) 80 | } 81 | } 82 | if p.Secrets != nil { 83 | for k, v := range p.Secrets { 84 | env[v.EnvVar] = TmpEnvSecret{Secret: k} 85 | secrets[k] = TmpSecret{v.Source} 86 | } 87 | } 88 | aux := &struct { 89 | *PodContainerAlias 90 | Env map[string]interface{} `json:"environment,omitempty"` 91 | }{PodContainerAlias: (*PodContainerAlias)(p), Env: env} 92 | 93 | return json.Marshal(aux) 94 | } 95 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "net" 21 | "testing" 22 | "time" 23 | 24 | "github.com/stretchr/testify/assert" 25 | ) 26 | 27 | type stubAddr struct { 28 | addr string 29 | } 30 | 31 | func (sa stubAddr) Network() string { 32 | return "network" 33 | } 34 | 35 | func (sa stubAddr) String() string { 36 | return sa.addr + "/8" 37 | } 38 | 39 | func TestUtilsAtomicIsSwitched(t *testing.T) { 40 | var sw atomicSwitch 41 | assert.False(t, sw.IsSwitched()) 42 | sw.SwitchOn() 43 | assert.True(t, sw.IsSwitched()) 44 | } 45 | 46 | func TestUtilsAtomicIsSwitchedOff(t *testing.T) { 47 | var sw atomicSwitch 48 | assert.False(t, sw.IsSwitched()) 49 | sw.SwitchOn() 50 | assert.True(t, sw.IsSwitched()) 51 | sw.SwitchedOff() 52 | assert.False(t, sw.IsSwitched()) 53 | } 54 | 55 | func TestUtilsDeadline(t *testing.T) { 56 | err := deadline(time.Duration(5)*time.Millisecond, func(chan bool) error { 57 | <-time.After(time.Duration(1) * time.Second) 58 | return nil 59 | }) 60 | assert.Error(t, err) 61 | assert.Equal(t, ErrTimeoutError, err) 62 | 63 | err = deadline(time.Duration(5)*time.Second, func(chan bool) error { 64 | <-time.After(time.Duration(5) * time.Millisecond) 65 | return nil 66 | }) 67 | 68 | assert.NoError(t, err) 69 | } 70 | 71 | func TestUtilsContains(t *testing.T) { 72 | list := []string{"1", "2", "3"} 73 | assert.True(t, contains(list, "2")) 74 | assert.False(t, contains(list, "12")) 75 | } 76 | 77 | func TestUtilsValidateID(t *testing.T) { 78 | path := "test/path" 79 | assert.Equal(t, validateID(path), "/test/path") 80 | path = "/test/path" 81 | assert.Equal(t, validateID(path), "/test/path") 82 | } 83 | 84 | func TestUtilsGetInterfaceAddress(t *testing.T) { 85 | // Find actual IP address we can test against. 86 | interfaces, err := net.Interfaces() 87 | assert.NoError(t, err) 88 | assert.NotEqual(t, 0, len(interfaces)) 89 | iface := interfaces[0] 90 | expectedName := iface.Name 91 | addresses, err := iface.Addrs() 92 | assert.NoError(t, err) 93 | expectedIPAddress := parseIPAddr(addresses[0]) 94 | 95 | // Execute test. 96 | address, err := getInterfaceAddress(expectedName) 97 | assert.NoError(t, err) 98 | assert.Equal(t, expectedIPAddress, address) 99 | address, err = getInterfaceAddress("nothing") 100 | assert.Error(t, err) 101 | assert.Equal(t, "", address) 102 | } 103 | 104 | func TestUtilsTrimRootPath(t *testing.T) { 105 | path := "/test/path" 106 | assert.Equal(t, trimRootPath(path), "test/path") 107 | path = "test/path" 108 | assert.Equal(t, trimRootPath(path), "test/path") 109 | } 110 | 111 | func TestParseIPAddr(t *testing.T) { 112 | ipAddr := "127.0.0.1" 113 | addr := stubAddr{ipAddr} 114 | assert.Equal(t, ipAddr, parseIPAddr(addr)) 115 | } 116 | -------------------------------------------------------------------------------- /application_marshalling_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "encoding/json" 21 | "strings" 22 | "testing" 23 | 24 | "github.com/stretchr/testify/assert" 25 | "github.com/stretchr/testify/require" 26 | ) 27 | 28 | func TestEnvironmentVariableUnmarshal(t *testing.T) { 29 | defaultConfig := NewDefaultConfig() 30 | configs := &configContainer{ 31 | client: &defaultConfig, 32 | server: &serverConfig{ 33 | scope: "environment-variables", 34 | }, 35 | } 36 | 37 | endpoint := newFakeMarathonEndpoint(t, configs) 38 | defer endpoint.Close() 39 | 40 | application, err := endpoint.Client.Application(fakeAppName) 41 | require.NoError(t, err) 42 | 43 | env := application.Env 44 | secrets := application.Secrets 45 | 46 | require.NotNil(t, env) 47 | assert.Equal(t, "bar", (*env)["FOO"]) 48 | assert.Equal(t, "TOP", (*secrets)["secret"].EnvVar) 49 | assert.Equal(t, "/path/to/secret", (*secrets)["secret"].Source) 50 | } 51 | 52 | func TestMalformedPayloadUnmarshal(t *testing.T) { 53 | var tests = []struct { 54 | expected string 55 | given []byte 56 | description string 57 | }{ 58 | { 59 | expected: "unexpected secret field", 60 | given: []byte(`{"env": {"FOO": "bar", "SECRET": {"not_secret": "secret1"}}, "secrets": {"secret1": {"source": "/path/to/secret"}}}`), 61 | description: "Field in environment secret not equal to secret.", 62 | }, 63 | { 64 | expected: "unexpected secret field", 65 | given: []byte(`{"env": {"FOO": "bar", "SECRET": {"secret": 1}}, "secrets": {"secret1": {"source": "/path/to/secret"}}}`), 66 | description: "Invalid value in environment secret.", 67 | }, 68 | { 69 | expected: "unexpected environment variable type", 70 | given: []byte(`{"env": {"FOO": 1, "SECRET": {"secret": "secret1"}}, "secrets": {"secret1": {"source": "/path/to/secret"}}}`), 71 | description: "Invalid environment variable type.", 72 | }, 73 | { 74 | expected: "malformed application definition", 75 | given: []byte(`{"env": "value"}`), 76 | description: "Bad application definition.", 77 | }, 78 | } 79 | 80 | for _, test := range tests { 81 | tmpApp := new(Application) 82 | 83 | err := json.Unmarshal(test.given, &tmpApp) 84 | if assert.Error(t, err, test.description) { 85 | assert.True(t, strings.HasPrefix(err.Error(), test.expected), test.description) 86 | } 87 | } 88 | } 89 | 90 | func TestEnvironmentVariableMarshal(t *testing.T) { 91 | testApp := new(Application) 92 | targetString := []byte(`{"ports":null,"dependencies":null,"env":{"FOO":"bar","TOP":{"secret":"secret1"}},"secrets":{"secret1":{"source":"/path/to/secret"}}}`) 93 | testApp.AddEnv("FOO", "bar") 94 | testApp.AddSecret("TOP", "secret1", "/path/to/secret") 95 | 96 | app, err := json.Marshal(testApp) 97 | if assert.NoError(t, err) { 98 | assert.Equal(t, targetString, app) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "net" 23 | "net/url" 24 | "reflect" 25 | "strings" 26 | "sync/atomic" 27 | "time" 28 | 29 | "github.com/google/go-querystring/query" 30 | ) 31 | 32 | type atomicSwitch int64 33 | 34 | func (r *atomicSwitch) IsSwitched() bool { 35 | return atomic.LoadInt64((*int64)(r)) != 0 36 | } 37 | 38 | func (r *atomicSwitch) SwitchOn() { 39 | atomic.StoreInt64((*int64)(r), 1) 40 | } 41 | 42 | func (r *atomicSwitch) SwitchedOff() { 43 | atomic.StoreInt64((*int64)(r), 0) 44 | } 45 | 46 | func validateID(id string) string { 47 | if !strings.HasPrefix(id, "/") { 48 | return fmt.Sprintf("/%s", id) 49 | } 50 | return id 51 | } 52 | 53 | func trimRootPath(id string) string { 54 | if strings.HasPrefix(id, "/") { 55 | return strings.TrimPrefix(id, "/") 56 | } 57 | return id 58 | } 59 | 60 | func deadline(timeout time.Duration, work func(chan bool) error) error { 61 | result := make(chan error) 62 | timer := time.After(timeout) 63 | stopChannel := make(chan bool, 1) 64 | 65 | // allow the method to attempt 66 | go func() { 67 | result <- work(stopChannel) 68 | }() 69 | for { 70 | select { 71 | case err := <-result: 72 | return err 73 | case <-timer: 74 | stopChannel <- true 75 | return ErrTimeoutError 76 | } 77 | } 78 | } 79 | 80 | func getInterfaceAddress(name string) (string, error) { 81 | interfaces, err := net.Interfaces() 82 | if err != nil { 83 | return "", err 84 | } 85 | for _, iface := range interfaces { 86 | // step: get only the interface we're interested in 87 | if iface.Name == name { 88 | addrs, err := iface.Addrs() 89 | if err != nil { 90 | return "", err 91 | } 92 | // step: return the first address 93 | if len(addrs) > 0 { 94 | return parseIPAddr(addrs[0]), nil 95 | } 96 | } 97 | } 98 | 99 | return "", errors.New("Unable to determine or find the interface") 100 | } 101 | 102 | func contains(elements []string, value string) bool { 103 | for _, element := range elements { 104 | if element == value { 105 | return true 106 | } 107 | } 108 | return false 109 | } 110 | 111 | func parseIPAddr(addr net.Addr) string { 112 | return strings.SplitN(addr.String(), "/", 2)[0] 113 | } 114 | 115 | // addOptions adds the parameters in opt as URL query parameters to s. 116 | // opt must be a struct whose fields may contain "url" tags. 117 | func addOptions(s string, opt interface{}) (string, error) { 118 | v := reflect.ValueOf(opt) 119 | if v.Kind() == reflect.Ptr && v.IsNil() { 120 | return s, nil 121 | } 122 | 123 | u, err := url.Parse(s) 124 | if err != nil { 125 | return s, err 126 | } 127 | 128 | qs, err := query.Values(opt) 129 | if err != nil { 130 | return s, err 131 | } 132 | 133 | u.RawQuery = qs.Encode() 134 | return u.String(), nil 135 | } 136 | 137 | // Bool returns a pointer to the passed in bool value 138 | func Bool(b bool) *bool { 139 | return &b 140 | } 141 | -------------------------------------------------------------------------------- /pod_marshalling.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | ) 23 | 24 | // PodAlias aliases the Pod struct so that it will be marshaled/unmarshaled automatically 25 | type PodAlias Pod 26 | 27 | // UnmarshalJSON unmarshals the given Pod JSON as expected except for environment variables and secrets. 28 | // Environment variables are stored in the Env field. Secrets, including the environment variable part, 29 | // are stored in the Secrets field. 30 | func (p *Pod) UnmarshalJSON(b []byte) error { 31 | aux := &struct { 32 | *PodAlias 33 | Env map[string]interface{} `json:"environment"` 34 | Secrets map[string]TmpSecret `json:"secrets"` 35 | }{ 36 | PodAlias: (*PodAlias)(p), 37 | } 38 | if err := json.Unmarshal(b, aux); err != nil { 39 | return fmt.Errorf("malformed pod definition %v", err) 40 | } 41 | env := map[string]string{} 42 | secrets := map[string]Secret{} 43 | 44 | for envName, genericEnvValue := range aux.Env { 45 | switch envValOrSecret := genericEnvValue.(type) { 46 | case string: 47 | env[envName] = envValOrSecret 48 | case map[string]interface{}: 49 | for secret, secretStore := range envValOrSecret { 50 | if secStore, ok := secretStore.(string); ok && secret == "secret" { 51 | secrets[secStore] = Secret{EnvVar: envName} 52 | break 53 | } 54 | return fmt.Errorf("unexpected secret field %v of value type %T", secret, envValOrSecret[secret]) 55 | } 56 | default: 57 | return fmt.Errorf("unexpected environment variable type %T", envValOrSecret) 58 | } 59 | } 60 | p.Env = env 61 | for k, v := range aux.Secrets { 62 | tmp := secrets[k] 63 | tmp.Source = v.Source 64 | secrets[k] = tmp 65 | } 66 | p.Secrets = secrets 67 | return nil 68 | } 69 | 70 | // MarshalJSON marshals the given Pod as expected except for environment variables and secrets, 71 | // which are marshaled from specialized structs. The environment variable piece of the secrets and other 72 | // normal environment variables are combined and marshaled to the env field. The secrets and the related 73 | // source are marshaled into the secrets field. 74 | func (p *Pod) MarshalJSON() ([]byte, error) { 75 | env := make(map[string]interface{}) 76 | secrets := make(map[string]TmpSecret) 77 | 78 | if p.Env != nil { 79 | for k, v := range p.Env { 80 | env[string(k)] = string(v) 81 | } 82 | } 83 | if p.Secrets != nil { 84 | for k, v := range p.Secrets { 85 | // Only add it to the root level pod environment if it's used 86 | // Otherwise it's likely in one of the container environments 87 | if v.EnvVar != "" { 88 | env[v.EnvVar] = TmpEnvSecret{Secret: k} 89 | } 90 | secrets[k] = TmpSecret{v.Source} 91 | } 92 | } 93 | aux := &struct { 94 | *PodAlias 95 | Env map[string]interface{} `json:"environment,omitempty"` 96 | Secrets map[string]TmpSecret `json:"secrets,omitempty"` 97 | }{PodAlias: (*PodAlias)(p), Env: env, Secrets: secrets} 98 | 99 | return json.Marshal(aux) 100 | } 101 | -------------------------------------------------------------------------------- /readiness.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import "time" 20 | 21 | // ReadinessCheck represents a readiness check. 22 | type ReadinessCheck struct { 23 | Name *string `json:"name,omitempty"` 24 | Protocol string `json:"protocol,omitempty"` 25 | Path string `json:"path,omitempty"` 26 | PortName string `json:"portName,omitempty"` 27 | IntervalSeconds int `json:"intervalSeconds,omitempty"` 28 | TimeoutSeconds int `json:"timeoutSeconds,omitempty"` 29 | HTTPStatusCodesForReady *[]int `json:"httpStatusCodesForReady,omitempty"` 30 | PreserveLastResponse *bool `json:"preserveLastResponse,omitempty"` 31 | } 32 | 33 | // SetName sets the name on the readiness check. 34 | func (rc *ReadinessCheck) SetName(name string) *ReadinessCheck { 35 | rc.Name = &name 36 | return rc 37 | } 38 | 39 | // SetProtocol sets the protocol on the readiness check. 40 | func (rc *ReadinessCheck) SetProtocol(proto string) *ReadinessCheck { 41 | rc.Protocol = proto 42 | return rc 43 | } 44 | 45 | // SetPath sets the path on the readiness check. 46 | func (rc *ReadinessCheck) SetPath(p string) *ReadinessCheck { 47 | rc.Path = p 48 | return rc 49 | } 50 | 51 | // SetPortName sets the port name on the readiness check. 52 | func (rc *ReadinessCheck) SetPortName(name string) *ReadinessCheck { 53 | rc.PortName = name 54 | return rc 55 | } 56 | 57 | // SetInterval sets the interval on the readiness check. 58 | func (rc *ReadinessCheck) SetInterval(interval time.Duration) *ReadinessCheck { 59 | secs := int(interval.Seconds()) 60 | rc.IntervalSeconds = secs 61 | return rc 62 | } 63 | 64 | // SetTimeout sets the timeout on the readiness check. 65 | func (rc *ReadinessCheck) SetTimeout(timeout time.Duration) *ReadinessCheck { 66 | secs := int(timeout.Seconds()) 67 | rc.TimeoutSeconds = secs 68 | return rc 69 | } 70 | 71 | // SetHTTPStatusCodesForReady sets the HTTP status codes for ready on the 72 | // readiness check. 73 | func (rc *ReadinessCheck) SetHTTPStatusCodesForReady(codes []int) *ReadinessCheck { 74 | rc.HTTPStatusCodesForReady = &codes 75 | return rc 76 | } 77 | 78 | // SetPreserveLastResponse sets the preserve last response flag on the 79 | // readiness check. 80 | func (rc *ReadinessCheck) SetPreserveLastResponse(preserve bool) *ReadinessCheck { 81 | rc.PreserveLastResponse = &preserve 82 | return rc 83 | } 84 | 85 | // ReadinessLastResponse holds the result of the last response embedded in a 86 | // readiness check result. 87 | type ReadinessLastResponse struct { 88 | Body string `json:"body"` 89 | ContentType string `json:"contentType"` 90 | Status int `json:"status"` 91 | } 92 | 93 | // ReadinessCheckResult is the result of a readiness check. 94 | type ReadinessCheckResult struct { 95 | Name string `json:"name"` 96 | TaskID string `json:"taskId"` 97 | Ready bool `json:"ready"` 98 | LastResponse ReadinessLastResponse `json:"lastResponse,omitempty"` 99 | } 100 | -------------------------------------------------------------------------------- /task_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | func TestHasHealthCheckResults(t *testing.T) { 26 | task := Task{} 27 | assert.False(t, task.HasHealthCheckResults()) 28 | task.HealthCheckResults = append(task.HealthCheckResults, &HealthCheckResult{}) 29 | assert.True(t, task.HasHealthCheckResults()) 30 | } 31 | 32 | func TestAllTasks(t *testing.T) { 33 | endpoint := newFakeMarathonEndpoint(t, nil) 34 | defer endpoint.Close() 35 | 36 | tasks, err := endpoint.Client.AllTasks(nil) 37 | assert.NoError(t, err) 38 | if assert.NotNil(t, tasks) { 39 | assert.Equal(t, len(tasks.Tasks), 2) 40 | } 41 | 42 | tasks, err = endpoint.Client.AllTasks(&AllTasksOpts{Status: "staging"}) 43 | assert.Nil(t, err) 44 | if assert.NotNil(t, tasks) { 45 | assert.Equal(t, len(tasks.Tasks), 0) 46 | } 47 | } 48 | 49 | func TestTasks(t *testing.T) { 50 | endpoint := newFakeMarathonEndpoint(t, nil) 51 | defer endpoint.Close() 52 | 53 | tasks, err := endpoint.Client.Tasks(fakeAppName) 54 | assert.NoError(t, err) 55 | if assert.NotNil(t, tasks) { 56 | assert.Equal(t, len(tasks.Tasks), 2) 57 | } 58 | } 59 | 60 | func TestKillApplicationTasks(t *testing.T) { 61 | endpoint := newFakeMarathonEndpoint(t, nil) 62 | defer endpoint.Close() 63 | 64 | tasks, err := endpoint.Client.KillApplicationTasks(fakeAppName, nil) 65 | assert.NoError(t, err) 66 | assert.NotNil(t, tasks) 67 | } 68 | 69 | func TestKillTask(t *testing.T) { 70 | cases := map[string]struct { 71 | TaskID string 72 | Result string 73 | }{ 74 | "CommonApp": {fakeTaskID, fakeTaskID}, 75 | "GroupApp": {"fake-group_fake-app.fake-task", "fake-group_fake-app.fake-task"}, 76 | "GroupAppWithSlashes": {"fake-group/fake-app.fake-task", "fake-group_fake-app.fake-task"}, 77 | } 78 | endpoint := newFakeMarathonEndpoint(t, nil) 79 | defer endpoint.Close() 80 | 81 | for k, tc := range cases { 82 | task, err := endpoint.Client.KillTask(tc.TaskID, nil) 83 | assert.NoError(t, err, "TestCase: %s", k) 84 | assert.Equal(t, tc.Result, task.ID, "TestCase: %s", k) 85 | } 86 | } 87 | 88 | func TestKillTasks(t *testing.T) { 89 | endpoint := newFakeMarathonEndpoint(t, nil) 90 | defer endpoint.Close() 91 | 92 | err := endpoint.Client.KillTasks([]string{fakeTaskID}, nil) 93 | assert.NoError(t, err) 94 | } 95 | 96 | func TestTaskEndpoints(t *testing.T) { 97 | endpoint := newFakeMarathonEndpoint(t, nil) 98 | defer endpoint.Close() 99 | 100 | endpoints, err := endpoint.Client.TaskEndpoints(fakeAppNameBroken, 8080, true) 101 | assert.NoError(t, err) 102 | assert.NotNil(t, endpoints) 103 | assert.Equal(t, len(endpoints), 1, t) 104 | assert.Equal(t, endpoints[0], "10.141.141.10:31045", t) 105 | 106 | endpoints, err = endpoint.Client.TaskEndpoints(fakeAppNameBroken, 8080, false) 107 | assert.NoError(t, err) 108 | assert.NotNil(t, endpoints) 109 | assert.Equal(t, len(endpoints), 2, t) 110 | assert.Equal(t, endpoints[0], "10.141.141.10:31045", t) 111 | assert.Equal(t, endpoints[1], "10.141.141.10:31234", t) 112 | 113 | _, err = endpoint.Client.TaskEndpoints(fakeAppNameBroken, 80, true) 114 | assert.Error(t, err) 115 | } 116 | -------------------------------------------------------------------------------- /application_marshalling.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | ) 23 | 24 | // Alias aliases the Application struct so that it will be marshaled/unmarshaled automatically 25 | type Alias Application 26 | 27 | // TmpEnvSecret holds the secret values deserialized from the environment variables field 28 | type TmpEnvSecret struct { 29 | Secret string `json:"secret,omitempty"` 30 | } 31 | 32 | // TmpSecret holds the deserialized secrets field in a Marathon application configuration 33 | type TmpSecret struct { 34 | Source string `json:"source,omitempty"` 35 | } 36 | 37 | // UnmarshalJSON unmarshals the given Application JSON as expected except for environment variables and secrets. 38 | // Environment varialbes are stored in the Env field. Secrets, including the environment variable part, 39 | // are stored in the Secrets field. 40 | func (app *Application) UnmarshalJSON(b []byte) error { 41 | aux := &struct { 42 | *Alias 43 | Env map[string]interface{} `json:"env"` 44 | Secrets map[string]TmpSecret `json:"secrets"` 45 | }{ 46 | Alias: (*Alias)(app), 47 | } 48 | if err := json.Unmarshal(b, aux); err != nil { 49 | return fmt.Errorf("malformed application definition %v", err) 50 | } 51 | env := &map[string]string{} 52 | secrets := &map[string]Secret{} 53 | 54 | for envName, genericEnvValue := range aux.Env { 55 | switch envValOrSecret := genericEnvValue.(type) { 56 | case string: 57 | (*env)[envName] = envValOrSecret 58 | case map[string]interface{}: 59 | for secret, secretStore := range envValOrSecret { 60 | if secStore, ok := secretStore.(string); ok && secret == "secret" { 61 | (*secrets)[secStore] = Secret{EnvVar: envName} 62 | break 63 | } 64 | return fmt.Errorf("unexpected secret field %v of value type %T", secret, envValOrSecret[secret]) 65 | } 66 | default: 67 | return fmt.Errorf("unexpected environment variable type %T", envValOrSecret) 68 | } 69 | } 70 | app.Env = env 71 | for k, v := range aux.Secrets { 72 | tmp := (*secrets)[k] 73 | tmp.Source = v.Source 74 | (*secrets)[k] = tmp 75 | } 76 | app.Secrets = secrets 77 | return nil 78 | } 79 | 80 | // MarshalJSON marshals the given Application as expected except for environment variables and secrets, 81 | // which are marshaled from specialized structs. The environment variable piece of the secrets and other 82 | // normal environment variables are combined and marshaled to the env field. The secrets and the related 83 | // source are marshaled into the secrets field. 84 | func (app *Application) MarshalJSON() ([]byte, error) { 85 | env := make(map[string]interface{}) 86 | secrets := make(map[string]TmpSecret) 87 | 88 | if app.Env != nil { 89 | for k, v := range *app.Env { 90 | env[string(k)] = string(v) 91 | } 92 | } 93 | if app.Secrets != nil { 94 | for k, v := range *app.Secrets { 95 | env[v.EnvVar] = TmpEnvSecret{Secret: k} 96 | secrets[k] = TmpSecret{v.Source} 97 | } 98 | } 99 | aux := &struct { 100 | *Alias 101 | Env map[string]interface{} `json:"env,omitempty"` 102 | Secrets map[string]TmpSecret `json:"secrets,omitempty"` 103 | }{Alias: (*Alias)(app), Env: env, Secrets: secrets} 104 | 105 | return json.Marshal(aux) 106 | } 107 | -------------------------------------------------------------------------------- /examples/applications/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "log" 22 | "time" 23 | 24 | marathon "github.com/gambol99/go-marathon" 25 | ) 26 | 27 | var marathonURL string 28 | 29 | func init() { 30 | flag.StringVar(&marathonURL, "url", "http://127.0.0.1:8080", "the url for the marathon endpoint") 31 | } 32 | 33 | func assert(err error) { 34 | if err != nil { 35 | log.Fatalf("Failed, error: %s", err) 36 | } 37 | } 38 | 39 | func waitOnDeployment(client marathon.Marathon, id *marathon.DeploymentID) { 40 | assert(client.WaitOnDeployment(id.DeploymentID, 0)) 41 | } 42 | 43 | func main() { 44 | flag.Parse() 45 | config := marathon.NewDefaultConfig() 46 | config.URL = marathonURL 47 | client, err := marathon.NewClient(config) 48 | assert(err) 49 | applications, err := client.Applications(nil) 50 | assert(err) 51 | 52 | log.Printf("Found %d application running", len(applications.Apps)) 53 | for _, application := range applications.Apps { 54 | log.Printf("Application: %v", application) 55 | details, err := client.Application(application.ID) 56 | assert(err) 57 | if details.Tasks != nil && len(details.Tasks) > 0 { 58 | for _, task := range details.Tasks { 59 | log.Printf("task: %v", task) 60 | } 61 | health, err := client.ApplicationOK(details.ID) 62 | assert(err) 63 | log.Printf("Application: %s, healthy: %t", details.ID, health) 64 | } 65 | } 66 | 67 | applicationName := "/my/product" 68 | 69 | if _, err := client.Application(applicationName); err == nil { 70 | deployID, err := client.DeleteApplication(applicationName, false) 71 | assert(err) 72 | waitOnDeployment(client, deployID) 73 | } 74 | 75 | log.Printf("Deploying a new application") 76 | application := marathon.NewDockerApplication(). 77 | Name(applicationName). 78 | CPU(0.1). 79 | Memory(64). 80 | Storage(0.0). 81 | Count(2). 82 | AddArgs("/usr/sbin/apache2ctl", "-D", "FOREGROUND"). 83 | AddEnv("NAME", "frontend_http"). 84 | AddEnv("SERVICE_80_NAME", "test_http") 85 | 86 | application. 87 | Container.Docker.Container("quay.io/gambol99/apache-php:latest"). 88 | Bridged(). 89 | Expose(80). 90 | Expose(443) 91 | 92 | *application.RequirePorts = true 93 | _, err = client.CreateApplication(application) 94 | assert(err) 95 | client.WaitOnApplication(application.ID, 30*time.Second) 96 | 97 | log.Printf("Scaling the application to 4 instances") 98 | deployID, err := client.ScaleApplicationInstances(application.ID, 4, false) 99 | assert(err) 100 | client.WaitOnApplication(application.ID, 30*time.Second) 101 | log.Printf("Successfully scaled the application, deployID: %s", deployID.DeploymentID) 102 | 103 | log.Printf("Deleting the application: %s", applicationName) 104 | deployID, err = client.DeleteApplication(application.ID, true) 105 | assert(err) 106 | waitOnDeployment(client, deployID) 107 | log.Printf("Successfully deleted the application") 108 | 109 | log.Printf("Starting the application again") 110 | _, err = client.CreateApplication(application) 111 | assert(err) 112 | log.Printf("Created the application: %s", application.ID) 113 | 114 | log.Printf("Delete all the tasks") 115 | _, err = client.KillApplicationTasks(application.ID, nil) 116 | assert(err) 117 | } 118 | -------------------------------------------------------------------------------- /pod_instance.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "fmt" 21 | "time" 22 | ) 23 | 24 | // PodInstance is the representation of an instance as returned by deleting an instance 25 | type PodInstance struct { 26 | InstanceID PodInstanceID `json:"instanceId"` 27 | AgentInfo PodAgentInfo `json:"agentInfo"` 28 | TasksMap map[string]PodTask `json:"tasksMap"` 29 | RunSpecVersion time.Time `json:"runSpecVersion"` 30 | State PodInstanceStateHistory `json:"state"` 31 | UnreachableStrategy EnabledUnreachableStrategy `json:"unreachableStrategy"` 32 | } 33 | 34 | // PodInstanceStateHistory is the pod instance's state 35 | type PodInstanceStateHistory struct { 36 | Condition PodTaskCondition `json:"condition"` 37 | Since time.Time `json:"since"` 38 | ActiveSince time.Time `json:"activeSince"` 39 | } 40 | 41 | // PodInstanceID contains the instance ID 42 | type PodInstanceID struct { 43 | ID string `json:"idString"` 44 | } 45 | 46 | // PodAgentInfo contains info about the agent the instance is running on 47 | type PodAgentInfo struct { 48 | Host string `json:"host"` 49 | AgentID string `json:"agentId"` 50 | Attributes []string `json:"attributes"` 51 | } 52 | 53 | // PodTask contains the info about the specific task within the instance 54 | type PodTask struct { 55 | TaskID string `json:"taskId"` 56 | RunSpecVersion time.Time `json:"runSpecVersion"` 57 | Status PodTaskStatus `json:"status"` 58 | } 59 | 60 | // PodTaskStatus is the current status of the task 61 | type PodTaskStatus struct { 62 | StagedAt time.Time `json:"stagedAt"` 63 | StartedAt time.Time `json:"startedAt"` 64 | MesosStatus string `json:"mesosStatus"` 65 | Condition PodTaskCondition `json:"condition"` 66 | NetworkInfo PodNetworkInfo `json:"networkInfo"` 67 | } 68 | 69 | // PodTaskCondition contains a string representation of the condition 70 | type PodTaskCondition struct { 71 | Str string `json:"str"` 72 | } 73 | 74 | // PodNetworkInfo contains the network info for a task 75 | type PodNetworkInfo struct { 76 | HostName string `json:"hostName"` 77 | HostPorts []int `json:"hostPorts"` 78 | IPAddresses []IPAddress `json:"ipAddresses"` 79 | } 80 | 81 | // DeletePodInstances deletes all instances of the named pod 82 | func (r *marathonClient) DeletePodInstances(name string, instances []string) ([]*PodInstance, error) { 83 | uri := buildPodInstancesURI(name) 84 | var result []*PodInstance 85 | if err := r.apiDelete(uri, instances, &result); err != nil { 86 | return nil, err 87 | } 88 | 89 | return result, nil 90 | } 91 | 92 | // DeletePodInstance deletes a specific instance of a pod 93 | func (r *marathonClient) DeletePodInstance(name, instance string) (*PodInstance, error) { 94 | uri := fmt.Sprintf("%s/%s", buildPodInstancesURI(name), instance) 95 | result := new(PodInstance) 96 | if err := r.apiDelete(uri, nil, result); err != nil { 97 | return nil, err 98 | } 99 | 100 | return result, nil 101 | } 102 | 103 | func buildPodInstancesURI(path string) string { 104 | return fmt.Sprintf("%s/%s::instances", marathonAPIPods, trimRootPath(path)) 105 | } 106 | -------------------------------------------------------------------------------- /info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | // Info is the detailed stats returned from marathon info 20 | type Info struct { 21 | EventSubscriber struct { 22 | HTTPEndpoints []string `json:"http_endpoints"` 23 | Type string `json:"type"` 24 | } `json:"event_subscriber"` 25 | FrameworkID string `json:"frameworkId"` 26 | HTTPConfig struct { 27 | AssetsPath interface{} `json:"assets_path"` 28 | HTTPPort float64 `json:"http_port"` 29 | HTTPSPort float64 `json:"https_port"` 30 | } `json:"http_config"` 31 | Leader string `json:"leader"` 32 | MarathonConfig struct { 33 | Checkpoint bool `json:"checkpoint"` 34 | Executor string `json:"executor"` 35 | FailoverTimeout float64 `json:"failover_timeout"` 36 | FrameworkName string `json:"framework_name"` 37 | Ha bool `json:"ha"` 38 | Hostname string `json:"hostname"` 39 | LeaderProxyConnectionTimeoutMs float64 `json:"leader_proxy_connection_timeout_ms"` 40 | LeaderProxyReadTimeoutMs float64 `json:"leader_proxy_read_timeout_ms"` 41 | LocalPortMax float64 `json:"local_port_max"` 42 | LocalPortMin float64 `json:"local_port_min"` 43 | Master string `json:"master"` 44 | MesosLeaderUIURL string `json:"mesos_leader_ui_url"` 45 | WebUIURL string `json:"webui_url"` 46 | MesosRole string `json:"mesos_role"` 47 | MesosUser string `json:"mesos_user"` 48 | ReconciliationInitialDelay float64 `json:"reconciliation_initial_delay"` 49 | ReconciliationInterval float64 `json:"reconciliation_interval"` 50 | TaskLaunchTimeout float64 `json:"task_launch_timeout"` 51 | TaskReservationTimeout float64 `json:"task_reservation_timeout"` 52 | } `json:"marathon_config"` 53 | Name string `json:"name"` 54 | Version string `json:"version"` 55 | ZookeeperConfig struct { 56 | Zk string `json:"zk"` 57 | ZkFutureTimeout struct { 58 | Duration float64 `json:"duration"` 59 | } `json:"zk_future_timeout"` 60 | ZkHosts string `json:"zk_hosts"` 61 | ZkPath string `json:"zk_path"` 62 | ZkState string `json:"zk_state"` 63 | ZkTimeout float64 `json:"zk_timeout"` 64 | } `json:"zookeeper_config"` 65 | } 66 | 67 | // Info retrieves the info stats from marathon 68 | func (r *marathonClient) Info() (*Info, error) { 69 | info := new(Info) 70 | if err := r.apiGet(marathonAPIInfo, nil, info); err != nil { 71 | return nil, err 72 | } 73 | 74 | return info, nil 75 | } 76 | 77 | // Leader retrieves the current marathon leader node 78 | func (r *marathonClient) Leader() (string, error) { 79 | var leader struct { 80 | Leader string `json:"leader"` 81 | } 82 | if err := r.apiGet(marathonAPILeader, nil, &leader); err != nil { 83 | return "", err 84 | } 85 | 86 | return leader.Leader, nil 87 | } 88 | 89 | // AbdicateLeader abdicates the marathon leadership 90 | func (r *marathonClient) AbdicateLeader() (string, error) { 91 | var message struct { 92 | Message string `json:"message"` 93 | } 94 | 95 | if err := r.apiDelete(marathonAPILeader, nil, &message); err != nil { 96 | return "", err 97 | } 98 | 99 | return message.Message, nil 100 | } 101 | -------------------------------------------------------------------------------- /pod_instance_status.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | // PodInstanceState is the state of a specific pod instance 20 | type PodInstanceState string 21 | 22 | const ( 23 | // PodInstanceStatePending is when an instance is pending scheduling 24 | PodInstanceStatePending PodInstanceState = "PENDING" 25 | 26 | // PodInstanceStateStaging is when an instance is staged to be scheduled 27 | PodInstanceStateStaging PodInstanceState = "STAGING" 28 | 29 | // PodInstanceStateStable is when an instance is stably running 30 | PodInstanceStateStable PodInstanceState = "STABLE" 31 | 32 | // PodInstanceStateDegraded is when an instance is degraded status 33 | PodInstanceStateDegraded PodInstanceState = "DEGRADED" 34 | 35 | // PodInstanceStateTerminal is when an instance is terminal 36 | PodInstanceStateTerminal PodInstanceState = "TERMINAL" 37 | ) 38 | 39 | // PodInstanceStatus is the status of a pod instance 40 | type PodInstanceStatus struct { 41 | AgentHostname string `json:"agentHostname,omitempty"` 42 | Conditions []*StatusCondition `json:"conditions,omitempty"` 43 | Containers []*ContainerStatus `json:"containers,omitempty"` 44 | ID string `json:"id,omitempty"` 45 | LastChanged string `json:"lastChanged,omitempty"` 46 | LastUpdated string `json:"lastUpdated,omitempty"` 47 | Message string `json:"message,omitempty"` 48 | Networks []*PodNetworkStatus `json:"networks,omitempty"` 49 | Resources *Resources `json:"resources,omitempty"` 50 | SpecReference string `json:"specReference,omitempty"` 51 | Status PodInstanceState `json:"status,omitempty"` 52 | StatusSince string `json:"statusSince,omitempty"` 53 | } 54 | 55 | // PodNetworkStatus is the networks attached to a pod instance 56 | type PodNetworkStatus struct { 57 | Addresses []string `json:"addresses,omitempty"` 58 | Name string `json:"name,omitempty"` 59 | } 60 | 61 | // StatusCondition describes info about a status change 62 | type StatusCondition struct { 63 | Name string `json:"name,omitempty"` 64 | Value string `json:"value,omitempty"` 65 | Reason string `json:"reason,omitempty"` 66 | LastChanged string `json:"lastChanged,omitempty"` 67 | LastUpdated string `json:"lastUpdated,omitempty"` 68 | } 69 | 70 | // ContainerStatus contains all status information for a container instance 71 | type ContainerStatus struct { 72 | Conditions []*StatusCondition `json:"conditions,omitempty"` 73 | ContainerID string `json:"containerId,omitempty"` 74 | Endpoints []*PodEndpoint `json:"endpoints,omitempty"` 75 | LastChanged string `json:"lastChanged,omitempty"` 76 | LastUpdated string `json:"lastUpdated,omitempty"` 77 | Message string `json:"message,omitempty"` 78 | Name string `json:"name,omitempty"` 79 | Resources *Resources `json:"resources,omitempty"` 80 | Status string `json:"status,omitempty"` 81 | StatusSince string `json:"statusSince,omitempty"` 82 | Termination *ContainerTerminationState `json:"termination,omitempty"` 83 | } 84 | 85 | // ContainerTerminationState describes why a container terminated 86 | type ContainerTerminationState struct { 87 | ExitCode int `json:"exitCode,omitempty"` 88 | Message string `json:"message,omitempty"` 89 | } 90 | -------------------------------------------------------------------------------- /network.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | // PodNetworkMode is the mode of a network descriptor 20 | type PodNetworkMode string 21 | 22 | const ( 23 | ContainerNetworkMode PodNetworkMode = "container" 24 | BridgeNetworkMode PodNetworkMode = "container/bridge" 25 | HostNetworkMode PodNetworkMode = "host" 26 | ) 27 | 28 | // PodNetwork contains network descriptors for a pod 29 | type PodNetwork struct { 30 | Name string `json:"name,omitempty"` 31 | Mode PodNetworkMode `json:"mode,omitempty"` 32 | Labels map[string]string `json:"labels,omitempty"` 33 | } 34 | 35 | // PodEndpoint describes an endpoint for a pod's container 36 | type PodEndpoint struct { 37 | Name string `json:"name,omitempty"` 38 | ContainerPort int `json:"containerPort,omitempty"` 39 | HostPort int `json:"hostPort,omitempty"` 40 | Protocol []string `json:"protocol,omitempty"` 41 | Labels map[string]string `json:"labels,omitempty"` 42 | } 43 | 44 | // NewPodNetwork creates an empty PodNetwork 45 | func NewPodNetwork(name string) *PodNetwork { 46 | return &PodNetwork{ 47 | Name: name, 48 | Labels: map[string]string{}, 49 | } 50 | } 51 | 52 | // NewPodEndpoint creates an empty PodEndpoint 53 | func NewPodEndpoint() *PodEndpoint { 54 | return &PodEndpoint{ 55 | Protocol: []string{}, 56 | Labels: map[string]string{}, 57 | } 58 | } 59 | 60 | // NewBridgePodNetwork creates a PodNetwork for a container in bridge mode 61 | func NewBridgePodNetwork() *PodNetwork { 62 | pn := NewPodNetwork("") 63 | return pn.SetMode(BridgeNetworkMode) 64 | } 65 | 66 | // NewContainerPodNetwork creates a PodNetwork for a container 67 | func NewContainerPodNetwork(name string) *PodNetwork { 68 | pn := NewPodNetwork(name) 69 | return pn.SetMode(ContainerNetworkMode) 70 | } 71 | 72 | // NewHostPodNetwork creates a PodNetwork for a container in host mode 73 | func NewHostPodNetwork() *PodNetwork { 74 | pn := NewPodNetwork("") 75 | return pn.SetMode(HostNetworkMode) 76 | } 77 | 78 | // SetName sets the name of a PodNetwork 79 | func (n *PodNetwork) SetName(name string) *PodNetwork { 80 | n.Name = name 81 | return n 82 | } 83 | 84 | // SetMode sets the mode of a PodNetwork 85 | func (n *PodNetwork) SetMode(mode PodNetworkMode) *PodNetwork { 86 | n.Mode = mode 87 | return n 88 | } 89 | 90 | // Label sets a label of a PodNetwork 91 | func (n *PodNetwork) Label(key, value string) *PodNetwork { 92 | n.Labels[key] = value 93 | return n 94 | } 95 | 96 | // SetName sets the name for a PodEndpoint 97 | func (e *PodEndpoint) SetName(name string) *PodEndpoint { 98 | e.Name = name 99 | return e 100 | } 101 | 102 | // SetContainerPort sets the container port for a PodEndpoint 103 | func (e *PodEndpoint) SetContainerPort(port int) *PodEndpoint { 104 | e.ContainerPort = port 105 | return e 106 | } 107 | 108 | // SetHostPort sets the host port for a PodEndpoint 109 | func (e *PodEndpoint) SetHostPort(port int) *PodEndpoint { 110 | e.HostPort = port 111 | return e 112 | } 113 | 114 | // AddProtocol appends a protocol for a PodEndpoint 115 | func (e *PodEndpoint) AddProtocol(protocol string) *PodEndpoint { 116 | e.Protocol = append(e.Protocol, protocol) 117 | return e 118 | } 119 | 120 | // Label sets a label for a PodEndpoint 121 | func (e *PodEndpoint) Label(key, value string) *PodEndpoint { 122 | e.Labels[key] = value 123 | return e 124 | } 125 | -------------------------------------------------------------------------------- /pod_status.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "fmt" 21 | "time" 22 | ) 23 | 24 | // PodState defines the state of a pod 25 | type PodState string 26 | 27 | const ( 28 | // PodStateDegraded is a degraded pod 29 | PodStateDegraded PodState = "DEGRADED" 30 | 31 | // PodStateStable is a stable pod 32 | PodStateStable PodState = "STABLE" 33 | 34 | // PodStateTerminal is a terminal pod 35 | PodStateTerminal PodState = "TERMINAL" 36 | ) 37 | 38 | // PodStatus describes the pod status 39 | type PodStatus struct { 40 | ID string `json:"id,omitempty"` 41 | Spec *Pod `json:"spec,omitempty"` 42 | Status PodState `json:"status,omitempty"` 43 | StatusSince string `json:"statusSince,omitempty"` 44 | Message string `json:"message,omitempty"` 45 | Instances []*PodInstanceStatus `json:"instances,omitempty"` 46 | TerminationHistory []*PodTerminationHistory `json:"terminationHistory,omitempty"` 47 | LastUpdated string `json:"lastUpdated,omitempty"` 48 | LastChanged string `json:"lastChanged,omitempty"` 49 | } 50 | 51 | // PodTerminationHistory is the termination history of the pod 52 | type PodTerminationHistory struct { 53 | InstanceID string `json:"instanceId,omitempty"` 54 | StartedAt string `json:"startedAt,omitempty"` 55 | TerminatedAt string `json:"terminatedAt,omitempty"` 56 | Message string `json:"message,omitempty"` 57 | Containers []*ContainerTerminationHistory `json:"containers,omitempty"` 58 | } 59 | 60 | // ContainerTerminationHistory is the termination history of a container in a pod 61 | type ContainerTerminationHistory struct { 62 | ContainerID string `json:"containerId,omitempty"` 63 | LastKnownState string `json:"lastKnownState,omitempty"` 64 | Termination *ContainerTerminationState `json:"termination,omitempty"` 65 | } 66 | 67 | // PodStatus retrieves the pod configuration from marathon 68 | func (r *marathonClient) PodStatus(name string) (*PodStatus, error) { 69 | var podStatus PodStatus 70 | 71 | if err := r.apiGet(buildPodStatusURI(name), nil, &podStatus); err != nil { 72 | return nil, err 73 | } 74 | 75 | return &podStatus, nil 76 | } 77 | 78 | // PodStatuses retrieves all pod configuration from marathon 79 | func (r *marathonClient) PodStatuses() ([]*PodStatus, error) { 80 | var podStatuses []*PodStatus 81 | 82 | if err := r.apiGet(buildPodStatusURI(""), nil, &podStatuses); err != nil { 83 | return nil, err 84 | } 85 | 86 | return podStatuses, nil 87 | } 88 | 89 | // WaitOnPod blocks until a pod to be deployed 90 | func (r *marathonClient) WaitOnPod(name string, timeout time.Duration) error { 91 | return r.wait(name, timeout, r.PodIsRunning) 92 | } 93 | 94 | // PodIsRunning returns whether the pod is stably running 95 | func (r *marathonClient) PodIsRunning(name string) bool { 96 | podStatus, err := r.PodStatus(name) 97 | if apiErr, ok := err.(*APIError); ok && apiErr.ErrCode == ErrCodeNotFound { 98 | return false 99 | } 100 | if err == nil && podStatus.Status == PodStateStable { 101 | return true 102 | } 103 | return false 104 | } 105 | 106 | func buildPodStatusURI(path string) string { 107 | return fmt.Sprintf("%s/%s::status", marathonAPIPods, trimRootPath(path)) 108 | } 109 | -------------------------------------------------------------------------------- /examples/groups/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "log" 22 | "time" 23 | 24 | marathon "github.com/gambol99/go-marathon" 25 | ) 26 | 27 | var marathonURL string 28 | 29 | func init() { 30 | flag.StringVar(&marathonURL, "url", "http://127.0.0.1:8080", "the url for the marathon endpoint") 31 | } 32 | 33 | func assert(err error) { 34 | if err != nil { 35 | log.Fatalf("Failed, error: %s", err) 36 | } 37 | } 38 | 39 | func main() { 40 | flag.Parse() 41 | config := marathon.NewDefaultConfig() 42 | config.URL = marathonURL 43 | client, err := marathon.NewClient(config) 44 | if err != nil { 45 | log.Fatalf("Failed to create a client for marathon, error: %s", err) 46 | } 47 | 48 | log.Printf("Retrieving a list of groups") 49 | if groups, err := client.Groups(); err != nil { 50 | log.Fatalf("Failed to retrieve the groups from maratho, error: %s", err) 51 | } else { 52 | for _, group := range groups.Groups { 53 | log.Printf("Found group: %s", group.ID) 54 | } 55 | } 56 | 57 | groupName := "/product/group" 58 | 59 | found, err := client.HasGroup(groupName) 60 | assert(err) 61 | if found { 62 | log.Printf("Deleting the group: %s, as it already exists", groupName) 63 | id, err := client.DeleteGroup(groupName, true) 64 | assert(err) 65 | err = client.WaitOnDeployment(id.DeploymentID, 0) 66 | assert(err) 67 | } 68 | 69 | /* step: the frontend app */ 70 | frontend := marathon.NewDockerApplication() 71 | frontend.Name("/product/group/frontend") 72 | frontend.CPU(0.1).Memory(64).Storage(0.0).Count(2) 73 | frontend.AddArgs("/usr/sbin/apache2ctl", "-D", "FOREGROUND") 74 | frontend.AddEnv("NAME", "frontend_http") 75 | frontend.AddEnv("SERVICE_80_NAME", "frontend_http") 76 | frontend.AddEnv("SERVICE_443_NAME", "frontend_https") 77 | frontend.AddEnv("BACKEND_MYSQL", "/product/group/mysql/3306;3306") 78 | frontend.AddEnv("BACKEND_CACHE", "/product/group/cache/6379;6379") 79 | frontend.DependsOn("/product/group/cache") 80 | frontend.DependsOn("/product/group/mysql") 81 | frontend.Container.Docker.Container("quay.io/gambol99/apache-php:latest").Expose(80).Expose(443) 82 | _, err = frontend.CheckHTTP("/hostname.php", 80, 10) 83 | assert(err) 84 | 85 | mysql := marathon.NewDockerApplication() 86 | mysql.Name("/product/group/mysql") 87 | mysql.CPU(0.1).Memory(128).Storage(0.0).Count(1) 88 | mysql.AddEnv("NAME", "group_cache") 89 | mysql.AddEnv("SERVICE_3306_NAME", "mysql") 90 | mysql.AddEnv("MYSQL_PASS", "mysql") 91 | mysql.Container.Docker.Container("tutum/mysql").Expose(3306) 92 | _, err = mysql.CheckTCP(3306, 10) 93 | assert(err) 94 | 95 | redis := marathon.NewDockerApplication() 96 | redis.Name("/product/group/cache") 97 | redis.CPU(0.1).Memory(64).Storage(0.0).Count(2) 98 | redis.AddEnv("NAME", "group_cache") 99 | redis.AddEnv("SERVICE_6379_NAME", "redis") 100 | redis.Container.Docker.Container("redis:latest").Expose(6379) 101 | _, err = redis.CheckTCP(6379, 10) 102 | assert(err) 103 | 104 | group := marathon.NewApplicationGroup(groupName) 105 | group.App(frontend).App(redis).App(mysql) 106 | 107 | assert(client.CreateGroup(group)) 108 | log.Printf("Successfully created the group: %s", group.ID) 109 | 110 | log.Printf("Updating the group parameters") 111 | frontend.Count(4) 112 | 113 | id, err := client.UpdateGroup(groupName, group, true) 114 | assert(err) 115 | log.Printf("Successfully updated the group: %s, version: %s", group.ID, id.DeploymentID) 116 | assert(client.WaitOnGroup(groupName, 500*time.Second)) 117 | } 118 | -------------------------------------------------------------------------------- /unreachable_strategy_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "fmt" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | "github.com/stretchr/testify/require" 25 | ) 26 | 27 | func TestUnreachableStrategyAPI(t *testing.T) { 28 | app := Application{} 29 | require.Nil(t, app.UnreachableStrategy) 30 | us := new(UnreachableStrategy) 31 | us.SetExpungeAfterSeconds(30.0).SetInactiveAfterSeconds(5.0) 32 | app.SetUnreachableStrategy(*us) 33 | testUs := app.UnreachableStrategy 34 | assert.Equal(t, 30.0, *testUs.ExpungeAfterSeconds) 35 | assert.Equal(t, 5.0, *testUs.InactiveAfterSeconds) 36 | 37 | app.EmptyUnreachableStrategy() 38 | us = app.UnreachableStrategy 39 | require.NotNil(t, us) 40 | assert.Nil(t, us.ExpungeAfterSeconds) 41 | assert.Nil(t, us.InactiveAfterSeconds) 42 | } 43 | 44 | func TestUnreachableStrategyUnmarshalEnabled(t *testing.T) { 45 | defaultConfig := NewDefaultConfig() 46 | configs := &configContainer{ 47 | client: &defaultConfig, 48 | server: &serverConfig{ 49 | scope: "unreachablestrategy-present", 50 | }, 51 | } 52 | 53 | endpoint := newFakeMarathonEndpoint(t, configs) 54 | defer endpoint.Close() 55 | 56 | application, err := endpoint.Client.Application(fakeAppName) 57 | require.NoError(t, err) 58 | 59 | us := application.UnreachableStrategy 60 | require.NotNil(t, us) 61 | assert.Empty(t, us.AbsenceReason) 62 | if assert.NotNil(t, us.InactiveAfterSeconds) { 63 | assert.Equal(t, 3.0, *us.InactiveAfterSeconds) 64 | } 65 | if assert.NotNil(t, us.ExpungeAfterSeconds) { 66 | assert.Equal(t, 4.0, *us.ExpungeAfterSeconds) 67 | } 68 | } 69 | 70 | func TestUnreachableStrategyUnmarshalNonEnabled(t *testing.T) { 71 | defaultConfig := NewDefaultConfig() 72 | configs := &configContainer{ 73 | client: &defaultConfig, 74 | server: &serverConfig{ 75 | scope: "unreachablestrategy-absent", 76 | }, 77 | } 78 | 79 | endpoint := newFakeMarathonEndpoint(t, configs) 80 | defer endpoint.Close() 81 | 82 | application, err := endpoint.Client.Application(fakeAppName) 83 | require.NoError(t, err) 84 | 85 | us := application.UnreachableStrategy 86 | require.NotNil(t, us) 87 | assert.Equal(t, UnreachableStrategyAbsenceReasonDisabled, us.AbsenceReason) 88 | } 89 | 90 | func TestUnreachableStrategyUnmarshalIllegal(t *testing.T) { 91 | j := []byte(`{false}`) 92 | us := UnreachableStrategy{} 93 | assert.Error(t, us.UnmarshalJSON(j)) 94 | } 95 | 96 | func TestUnreachableStrategyMarshal(t *testing.T) { 97 | tests := []struct { 98 | name string 99 | us UnreachableStrategy 100 | wantJSON string 101 | }{ 102 | { 103 | name: "present", 104 | us: UnreachableStrategy{ 105 | EnabledUnreachableStrategy: EnabledUnreachableStrategy{ 106 | InactiveAfterSeconds: float64p(3.5), 107 | ExpungeAfterSeconds: float64p(4.5), 108 | }, 109 | AbsenceReason: "", 110 | }, 111 | wantJSON: `{"inactiveAfterSeconds":3.5,"expungeAfterSeconds":4.5}`, 112 | }, 113 | { 114 | name: "absent", 115 | us: UnreachableStrategy{ 116 | AbsenceReason: UnreachableStrategyAbsenceReasonDisabled, 117 | }, 118 | wantJSON: fmt.Sprintf(`"%s"`, UnreachableStrategyAbsenceReasonDisabled), 119 | }, 120 | } 121 | 122 | for _, test := range tests { 123 | label := fmt.Sprintf("test: %s", test.name) 124 | j, err := test.us.MarshalJSON() 125 | if assert.NoError(t, err, label) { 126 | assert.Equal(t, test.wantJSON, string(j), label) 127 | } 128 | } 129 | } 130 | 131 | func float64p(f float64) *float64 { 132 | return &f 133 | } 134 | -------------------------------------------------------------------------------- /pod_marshalling_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "encoding/json" 21 | "strings" 22 | "testing" 23 | 24 | "github.com/stretchr/testify/assert" 25 | "github.com/stretchr/testify/require" 26 | ) 27 | 28 | func TestPodEnvironmentVariableUnmarshal(t *testing.T) { 29 | endpoint := newFakeMarathonEndpoint(t, nil) 30 | defer endpoint.Close() 31 | 32 | pod, err := endpoint.Client.Pod(fakePodName) 33 | require.NoError(t, err) 34 | 35 | env := pod.Env 36 | secrets := pod.Secrets 37 | 38 | require.NotNil(t, env) 39 | assert.Equal(t, "value", env["key1"]) 40 | assert.Equal(t, "key2", secrets["secret0"].EnvVar) 41 | assert.Equal(t, "source0", secrets["secret0"].Source) 42 | 43 | assert.Equal(t, "value3", pod.Containers[0].Env["key3"]) 44 | assert.Equal(t, "key4", pod.Containers[0].Secrets["secret1"].EnvVar) 45 | assert.Equal(t, "source1", secrets["secret1"].Source) 46 | } 47 | 48 | func TestPodMalformedPayloadUnmarshal(t *testing.T) { 49 | var tests = []struct { 50 | expected string 51 | given []byte 52 | description string 53 | }{ 54 | { 55 | expected: "unexpected secret field", 56 | given: []byte(`{"environment": {"FOO": "bar", "SECRET": {"not_secret": "secret1"}}, "secrets": {"secret1": {"source": "/path/to/secret"}}}`), 57 | description: "Field in environment secret not equal to secret.", 58 | }, 59 | { 60 | expected: "unexpected secret field", 61 | given: []byte(`{"environment": {"FOO": "bar", "SECRET": {"secret": 1}}, "secrets": {"secret1": {"source": "/path/to/secret"}}}`), 62 | description: "Invalid value in environment secret.", 63 | }, 64 | { 65 | expected: "unexpected environment variable type", 66 | given: []byte(`{"environment": {"FOO": 1, "SECRET": {"secret": "secret1"}}, "secrets": {"secret1": {"source": "/path/to/secret"}}}`), 67 | description: "Invalid environment variable type.", 68 | }, 69 | { 70 | expected: "malformed pod definition", 71 | given: []byte(`{"environment": "value"}`), 72 | description: "Bad pod definition.", 73 | }, 74 | } 75 | 76 | for _, test := range tests { 77 | tmpPod := new(Pod) 78 | 79 | err := json.Unmarshal(test.given, &tmpPod) 80 | if assert.Error(t, err, test.description) { 81 | assert.True(t, strings.HasPrefix(err.Error(), test.expected), test.description) 82 | } 83 | } 84 | } 85 | 86 | func TestPodEnvironmentVariableMarshal(t *testing.T) { 87 | testPod := new(Pod) 88 | targetString := []byte(`{"containers":[{"lifecycle":{},"environment":{"FOO2":"bar2","TOP2":"secret1"}}],"environment":{"FOO":"bar","TOP":{"secret":"secret1"}},"secrets":{"secret1":{"source":"/path/to/secret"}}}`) 89 | 90 | testPod.AddEnv("FOO", "bar") 91 | testPod.AddSecret("TOP", "secret1", "/path/to/secret") 92 | 93 | testContainer := new(PodContainer) 94 | testContainer.AddSecret("TOP2", "secret1") 95 | testContainer.AddEnv("FOO2", "bar2") 96 | testPod.AddContainer(testContainer) 97 | 98 | pod, err := json.Marshal(testPod) 99 | if assert.NoError(t, err) { 100 | assert.Equal(t, targetString, pod) 101 | } 102 | } 103 | 104 | func TestPodContainerArtifactBoolMarshal(t *testing.T) { 105 | targetString := `{"containers":[{"artifacts":[{"extract":false}],"lifecycle":{}}]}` 106 | 107 | testPod := new(Pod) 108 | testArtifact := new(PodArtifact) 109 | testArtifact.Extract = Bool(false) 110 | testContainer := new(PodContainer) 111 | testContainer.AddArtifact(testArtifact) 112 | testPod.AddContainer(testContainer) 113 | 114 | pod, err := json.Marshal(testPod) 115 | if assert.NoError(t, err) { 116 | assert.Equal(t, targetString, string(pod)) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /examples/pods/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "fmt" 22 | "log" 23 | "time" 24 | 25 | marathon "github.com/gambol99/go-marathon" 26 | ) 27 | 28 | var marathonURL string 29 | var dcosToken string 30 | 31 | func init() { 32 | flag.StringVar(&marathonURL, "url", "http://127.0.0.1:8080", "the url for the marathon endpoint") 33 | flag.StringVar(&dcosToken, "token", "", "DCOS token for auth") 34 | } 35 | 36 | func assert(err error) { 37 | if err != nil { 38 | log.Fatalf("Failed, error: %s", err) 39 | } 40 | } 41 | 42 | func waitOnDeployment(client marathon.Marathon, id *marathon.DeploymentID) { 43 | assert(client.WaitOnDeployment(id.DeploymentID, 0)) 44 | } 45 | 46 | func createRawPod() *marathon.Pod { 47 | var containers []*marathon.PodContainer 48 | for i := 0; i < 2; i++ { 49 | container := &marathon.PodContainer{ 50 | Name: fmt.Sprintf("container%d", i), 51 | Exec: &marathon.PodExec{ 52 | Command: marathon.PodCommand{ 53 | Shell: "echo Hello World && sleep 600", 54 | }, 55 | }, 56 | Image: &marathon.PodContainerImage{ 57 | Kind: "DOCKER", 58 | ID: "nginx", 59 | ForcePull: marathon.Bool(true), 60 | }, 61 | VolumeMounts: []*marathon.PodVolumeMount{ 62 | &marathon.PodVolumeMount{ 63 | Name: "sharedvolume", 64 | MountPath: "/peers", 65 | }, 66 | }, 67 | Resources: &marathon.Resources{ 68 | Cpus: 0.1, 69 | Mem: 128, 70 | }, 71 | Env: map[string]string{ 72 | "key": "value", 73 | }, 74 | } 75 | 76 | containers = append(containers, container) 77 | } 78 | 79 | pod := &marathon.Pod{ 80 | ID: "/mypod", 81 | Containers: containers, 82 | Scaling: &marathon.PodScalingPolicy{ 83 | Kind: "fixed", 84 | Instances: 2, 85 | }, 86 | Volumes: []*marathon.PodVolume{ 87 | &marathon.PodVolume{ 88 | Name: "sharedvolume", 89 | Host: "/tmp", 90 | }, 91 | }, 92 | } 93 | 94 | return pod 95 | } 96 | 97 | func createConveniencePod() *marathon.Pod { 98 | pod := marathon.NewPod() 99 | 100 | pod.Name("mypod"). 101 | Count(2). 102 | AddVolume(marathon.NewPodVolume("sharedvolume", "/tmp")) 103 | 104 | for i := 0; i < 2; i++ { 105 | image := marathon.NewDockerPodContainerImage(). 106 | SetID("nginx") 107 | 108 | container := marathon.NewPodContainer(). 109 | SetName(fmt.Sprintf("container%d", i)). 110 | CPUs(0.1). 111 | Memory(128). 112 | SetImage(image). 113 | AddEnv("key", "value"). 114 | AddVolumeMount(marathon.NewPodVolumeMount("sharedvolume", "/peers")). 115 | SetCommand("echo Hello World && sleep 600") 116 | 117 | pod.AddContainer(container) 118 | } 119 | 120 | return pod 121 | } 122 | 123 | func doPlayground(client marathon.Marathon, pod *marathon.Pod) { 124 | // Create a pod 125 | fmt.Println("Creating pod...") 126 | pod, err := client.CreatePod(pod) 127 | assert(err) 128 | 129 | // Check its status 130 | fmt.Println("Waiting on pod...") 131 | err = client.WaitOnPod(pod.ID, time.Minute*1) 132 | assert(err) 133 | 134 | // Scale it 135 | fmt.Println("Scaling pod...") 136 | pod.Count(5) 137 | pod, err = client.UpdatePod(pod, true) 138 | assert(err) 139 | 140 | // Get instances 141 | status, err := client.PodStatus(pod.ID) 142 | fmt.Printf("Pod status: %s\n", status.Status) 143 | assert(err) 144 | 145 | // Kill an instance 146 | fmt.Println("Deleting an instance...") 147 | _, err = client.DeletePodInstance(pod.ID, status.Instances[0].ID) 148 | assert(err) 149 | 150 | // Delete it 151 | fmt.Println("Deleting the pod") 152 | id, err := client.DeletePod(pod.ID, true) 153 | assert(err) 154 | 155 | waitOnDeployment(client, id) 156 | } 157 | 158 | func main() { 159 | flag.Parse() 160 | config := marathon.NewDefaultConfig() 161 | config.URL = marathonURL 162 | config.DCOSToken = dcosToken 163 | client, err := marathon.NewClient(config) 164 | assert(err) 165 | 166 | fmt.Println("Convenience Pods:") 167 | podC := createConveniencePod() 168 | doPlayground(client, podC) 169 | 170 | fmt.Println("Raw Pods:") 171 | podR := createRawPod() 172 | doPlayground(client, podR) 173 | } 174 | -------------------------------------------------------------------------------- /pod_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | "github.com/stretchr/testify/require" 24 | ) 25 | 26 | const key = "testKey" 27 | const val = "testValue" 28 | 29 | const fakePodName = "/fake-pod" 30 | const secondFakePodName = "/fake-pod2" 31 | 32 | func TestPodLabels(t *testing.T) { 33 | pod := NewPod() 34 | pod.AddLabel(key, val) 35 | if assert.Equal(t, len(pod.Labels), 1) { 36 | assert.Equal(t, pod.Labels[key], val) 37 | } 38 | 39 | pod.EmptyLabels() 40 | assert.Equal(t, len(pod.Labels), 0) 41 | } 42 | 43 | func TestPodEnvironmentVars(t *testing.T) { 44 | pod := NewPod() 45 | pod.AddEnv(key, val) 46 | 47 | newVal, ok := pod.Env[key] 48 | assert.Equal(t, newVal, val) 49 | assert.Equal(t, ok, true) 50 | 51 | badVal, ok := pod.Env["fakeKey"] 52 | assert.Equal(t, badVal, "") 53 | assert.Equal(t, ok, false) 54 | 55 | pod.EmptyEnvs() 56 | assert.Equal(t, len(pod.Env), 0) 57 | } 58 | 59 | func TestSecrets(t *testing.T) { 60 | pod := NewPod() 61 | pod.AddSecret("randomVar", key, val) 62 | 63 | newVal, err := pod.GetSecretSource(key) 64 | assert.Equal(t, newVal, val) 65 | assert.Equal(t, err, nil) 66 | 67 | badVal, err := pod.GetSecretSource("fakeKey") 68 | assert.Equal(t, badVal, "") 69 | assert.NotNil(t, err) 70 | 71 | pod.EmptySecrets() 72 | assert.Equal(t, len(pod.Env), 0) 73 | } 74 | 75 | func TestSupportsPod(t *testing.T) { 76 | endpoint := newFakeMarathonEndpoint(t, nil) 77 | 78 | supports, err := endpoint.Client.SupportsPods() 79 | if assert.Nil(t, err) { 80 | 81 | assert.Equal(t, supports, true) 82 | } 83 | 84 | // Manually closing to test lack of support 85 | endpoint.Close() 86 | 87 | supports, err = endpoint.Client.SupportsPods() 88 | assert.NotNil(t, err) 89 | assert.Equal(t, supports, false) 90 | } 91 | func TestGetPod(t *testing.T) { 92 | endpoint := newFakeMarathonEndpoint(t, nil) 93 | defer endpoint.Close() 94 | 95 | pod, err := endpoint.Client.Pod(fakePodName) 96 | require.NoError(t, err) 97 | if assert.NotNil(t, pod) { 98 | assert.Equal(t, pod.ID, fakePodName) 99 | } 100 | } 101 | 102 | func TestGetAllPods(t *testing.T) { 103 | endpoint := newFakeMarathonEndpoint(t, nil) 104 | defer endpoint.Close() 105 | 106 | pods, err := endpoint.Client.Pods() 107 | require.NoError(t, err) 108 | if assert.Equal(t, len(pods), 2) { 109 | assert.Equal(t, pods[0].ID, fakePodName) 110 | assert.Equal(t, pods[1].ID, secondFakePodName) 111 | } 112 | } 113 | 114 | func TestCreatePod(t *testing.T) { 115 | endpoint := newFakeMarathonEndpoint(t, nil) 116 | defer endpoint.Close() 117 | 118 | pod := NewPod().Name(fakePodName) 119 | pod, err := endpoint.Client.CreatePod(pod) 120 | require.NoError(t, err) 121 | if assert.NotNil(t, pod) { 122 | assert.Equal(t, pod.ID, fakePodName) 123 | } 124 | } 125 | 126 | func TestUpdatePod(t *testing.T) { 127 | endpoint := newFakeMarathonEndpoint(t, nil) 128 | defer endpoint.Close() 129 | 130 | pod := NewPod().Name(fakePodName) 131 | pod, err := endpoint.Client.CreatePod(pod) 132 | require.NoError(t, err) 133 | 134 | pod, err = endpoint.Client.UpdatePod(pod, true) 135 | require.NoError(t, err) 136 | 137 | if assert.NotNil(t, pod) { 138 | assert.Equal(t, pod.ID, fakePodName) 139 | assert.Equal(t, pod.Scaling.Instances, 2) 140 | } 141 | } 142 | 143 | func TestDeletePod(t *testing.T) { 144 | endpoint := newFakeMarathonEndpoint(t, nil) 145 | defer endpoint.Close() 146 | 147 | id, err := endpoint.Client.DeletePod(fakePodName, true) 148 | require.NoError(t, err) 149 | 150 | if assert.NotNil(t, id) { 151 | assert.Equal(t, id.DeploymentID, "c0e7434c-df47-4d23-99f1-78bd78662231") 152 | } 153 | } 154 | 155 | func TestVersions(t *testing.T) { 156 | endpoint := newFakeMarathonEndpoint(t, nil) 157 | defer endpoint.Close() 158 | 159 | versions, err := endpoint.Client.PodVersions(fakePodName) 160 | require.NoError(t, err) 161 | assert.Equal(t, versions[0], "2014-08-18T22:36:41.451Z") 162 | } 163 | 164 | func TestGetPodByVersion(t *testing.T) { 165 | endpoint := newFakeMarathonEndpoint(t, nil) 166 | defer endpoint.Close() 167 | 168 | pod, err := endpoint.Client.PodByVersion(fakePodName, "2014-08-18T22:36:41.451Z") 169 | require.NoError(t, err) 170 | assert.Equal(t, pod.ID, fakePodName) 171 | } 172 | 173 | func TestAddPodImagePullConfig(t *testing.T) { 174 | container := new(PodContainer) 175 | container.Image = new(PodContainerImage) 176 | pullConfig := NewPullConfig("pullConfig-secret") 177 | 178 | container.Image.SetPullConfig(pullConfig) 179 | 180 | if assert.NotNil(t, container.Image.PullConfig) { 181 | assert.Equal(t, "pullConfig-secret", container.Image.PullConfig.Secret) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /cluster_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "testing" 21 | "time" 22 | 23 | "github.com/stretchr/testify/assert" 24 | "github.com/stretchr/testify/require" 25 | ) 26 | 27 | func TestSize(t *testing.T) { 28 | cluster, err := newStandardCluster(fakeMarathonURL) 29 | assert.NoError(t, err) 30 | assert.Equal(t, cluster.size(), 3) 31 | } 32 | 33 | func TestActive(t *testing.T) { 34 | cluster, err := newStandardCluster(fakeMarathonURL) 35 | assert.NoError(t, err) 36 | assert.Equal(t, len(cluster.activeMembers()), 3) 37 | } 38 | 39 | func TestNonActive(t *testing.T) { 40 | cluster, err := newStandardCluster(fakeMarathonURL) 41 | assert.NoError(t, err) 42 | assert.Equal(t, len(cluster.nonActiveMembers()), 0) 43 | } 44 | 45 | func TestGetMember(t *testing.T) { 46 | cases := []struct { 47 | isDCOS bool 48 | MarathonURL string 49 | member string 50 | }{ 51 | { 52 | isDCOS: false, 53 | MarathonURL: fakeMarathonURL, 54 | member: "http://127.0.0.1:3000", 55 | }, 56 | { 57 | isDCOS: false, 58 | MarathonURL: fakeMarathonURLWithPath, 59 | member: "http://127.0.0.1:3000/path", 60 | }, 61 | { 62 | isDCOS: true, 63 | MarathonURL: fakeMarathonURL, 64 | member: "http://127.0.0.1:3000/marathon", 65 | }, 66 | { 67 | isDCOS: true, 68 | MarathonURL: fakeMarathonURLWithPath, 69 | member: "http://127.0.0.1:3000/path", 70 | }, 71 | } 72 | for _, x := range cases { 73 | cluster, err := newCluster(&httpClient{config: Config{HTTPClient: defaultHTTPClient}}, x.MarathonURL, x.isDCOS) 74 | assert.NoError(t, err) 75 | member, err := cluster.getMember() 76 | assert.NoError(t, err) 77 | assert.Equal(t, member, x.member) 78 | } 79 | } 80 | 81 | func TestMarkDown(t *testing.T) { 82 | endpoint := newFakeMarathonEndpoint(t, nil) 83 | defer endpoint.Close() 84 | cluster, err := newStandardCluster(endpoint.URL) 85 | require.NoError(t, err) 86 | require.Equal(t, len(cluster.activeMembers()), 3) 87 | cluster.healthCheckInterval = 2500 * time.Millisecond 88 | 89 | members := cluster.activeMembers() 90 | cluster.markDown(members[0]) 91 | cluster.markDown(members[1]) 92 | require.Equal(t, len(cluster.activeMembers()), 1) 93 | 94 | ticker := time.NewTicker(250 * time.Millisecond) 95 | defer ticker.Stop() 96 | timeout := time.NewTimer(5 * time.Second) 97 | defer timeout.Stop() 98 | var numFoundMembers int 99 | for { 100 | numFoundMembers = len(cluster.activeMembers()) 101 | if numFoundMembers == 3 { 102 | break 103 | } 104 | select { 105 | case <-ticker.C: 106 | continue 107 | case <-timeout.C: 108 | t.Fatalf("found %d active member(s), want 3", numFoundMembers) 109 | } 110 | } 111 | } 112 | 113 | func TestValidClusterHosts(t *testing.T) { 114 | cs := []struct { 115 | URL string 116 | Expect []string 117 | }{ 118 | { 119 | URL: "http://127.0.0.1", 120 | Expect: []string{"http://127.0.0.1"}, 121 | }, 122 | { 123 | URL: "http://127.0.0.1:8080", 124 | Expect: []string{"http://127.0.0.1:8080"}, 125 | }, 126 | { 127 | URL: "http://127.0.0.1:8080,http://127.0.0.2:8081", 128 | Expect: []string{"http://127.0.0.1:8080", "http://127.0.0.2:8081"}, 129 | }, 130 | { 131 | URL: "https://127.0.0.1:8080,http://127.0.0.2:8081", 132 | Expect: []string{"https://127.0.0.1:8080", "http://127.0.0.2:8081"}, 133 | }, 134 | { 135 | URL: "http://127.0.0.1:8080,127.0.0.2", 136 | Expect: []string{"http://127.0.0.1:8080", "http://127.0.0.2"}, 137 | }, 138 | { 139 | URL: "https://127.0.0.1:8080,127.0.0.2", 140 | Expect: []string{"https://127.0.0.1:8080", "https://127.0.0.2"}, 141 | }, 142 | { 143 | URL: "http://127.0.0.1:8080,127.0.0.2:8080", 144 | Expect: []string{"http://127.0.0.1:8080", "http://127.0.0.2:8080"}, 145 | }, 146 | { 147 | URL: "http://127.0.0.1:8080,https://127.0.0.2", 148 | Expect: []string{"http://127.0.0.1:8080", "https://127.0.0.2"}, 149 | }, 150 | { 151 | URL: "http://127.0.0.1:8080,https://127.0.0.2:8080", 152 | Expect: []string{"http://127.0.0.1:8080", "https://127.0.0.2:8080"}, 153 | }, 154 | { 155 | URL: "http://127.0.0.1:8080/path1,127.0.0.2/path2", 156 | Expect: []string{"http://127.0.0.1:8080/path1", "http://127.0.0.2/path2"}, 157 | }, 158 | } 159 | for _, x := range cs { 160 | c, err := newStandardCluster(x.URL) 161 | if !assert.NoError(t, err, "URL '%s' should not have thrown an error: %s", x.URL, err) { 162 | continue 163 | } 164 | assert.Equal(t, x.Expect, c.activeMembers(), "URL '%s', expected: %v, got: %s", x.URL, x.Expect, c.activeMembers()) 165 | } 166 | } 167 | 168 | func TestInvalidClusterHosts(t *testing.T) { 169 | for _, invalidHost := range []string{ 170 | "", 171 | "://", 172 | "http://", 173 | "http://,,", 174 | "http://%42", 175 | "http://,127.0.0.1:3000,127.0.0.1:3000", 176 | "http://127.0.0.1:3000,,127.0.0.1:3000", 177 | "http://127.0.0.1:3000,127.0.0.1:3000,", 178 | "foo://127.0.0.1:3000", 179 | } { 180 | _, err := newStandardCluster(invalidHost) 181 | if !assert.Error(t, err) { 182 | t.Errorf("undetected invalid host: %s", invalidHost) 183 | } 184 | } 185 | } 186 | 187 | func newStandardCluster(url string) (*cluster, error) { 188 | return newCluster(&httpClient{config: Config{HTTPClient: defaultHTTPClient}}, url, false) 189 | } 190 | -------------------------------------------------------------------------------- /cluster.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "fmt" 21 | "net/url" 22 | "strings" 23 | "sync" 24 | "time" 25 | ) 26 | 27 | const ( 28 | memberStatusUp = 0 29 | memberStatusDown = 1 30 | ) 31 | 32 | // the status of a member node 33 | type memberStatus int 34 | 35 | // cluster is a collection of marathon nodes 36 | type cluster struct { 37 | sync.RWMutex 38 | // a collection of nodes 39 | members []*member 40 | // the marathon HTTP client to ensure consistency in requests 41 | client *httpClient 42 | // healthCheckInterval is the interval by which we probe down nodes for 43 | // availability again. 44 | healthCheckInterval time.Duration 45 | } 46 | 47 | // member represents an individual endpoint 48 | type member struct { 49 | // the name / ip address of the host 50 | endpoint string 51 | // the status of the host 52 | status memberStatus 53 | } 54 | 55 | // newCluster returns a new marathon cluster 56 | func newCluster(client *httpClient, marathonURL string, isDCOS bool) (*cluster, error) { 57 | // step: extract and basic validate the endpoints 58 | var members []*member 59 | var defaultProto string 60 | 61 | for _, endpoint := range strings.Split(marathonURL, ",") { 62 | // step: check for nothing 63 | if endpoint == "" { 64 | return nil, newInvalidEndpointError("endpoint is blank") 65 | } 66 | // step: prepend scheme if missing on (non-initial) endpoint. 67 | if !strings.HasPrefix(endpoint, "http://") && !strings.HasPrefix(endpoint, "https://") { 68 | if defaultProto == "" { 69 | return nil, newInvalidEndpointError("missing scheme on (first) endpoint") 70 | } 71 | 72 | endpoint = fmt.Sprintf("%s://%s", defaultProto, endpoint) 73 | } 74 | // step: parse the url 75 | u, err := url.Parse(endpoint) 76 | if err != nil { 77 | return nil, newInvalidEndpointError("invalid endpoint '%s': %s", endpoint, err) 78 | } 79 | if defaultProto == "" { 80 | defaultProto = u.Scheme 81 | } 82 | 83 | // step: check for empty hosts 84 | if u.Host == "" { 85 | return nil, newInvalidEndpointError("endpoint: %s must have a host", endpoint) 86 | } 87 | 88 | // step: if DCOS is set and no path is given, set the default DCOS path. 89 | // done in order to maintain compatibility with automatic addition of the 90 | // default DCOS path. 91 | if isDCOS && strings.TrimLeft(u.Path, "/") == "" { 92 | u.Path = defaultDCOSPath 93 | } 94 | 95 | // step: create a new node for this endpoint 96 | members = append(members, &member{endpoint: u.String()}) 97 | } 98 | 99 | return &cluster{ 100 | client: client, 101 | members: members, 102 | healthCheckInterval: 5 * time.Second, 103 | }, nil 104 | } 105 | 106 | // retrieve the current member, i.e. the current endpoint in use 107 | func (c *cluster) getMember() (string, error) { 108 | c.RLock() 109 | defer c.RUnlock() 110 | for _, n := range c.members { 111 | if n.status == memberStatusUp { 112 | return n.endpoint, nil 113 | } 114 | } 115 | 116 | return "", ErrMarathonDown 117 | } 118 | 119 | // markDown marks down the current endpoint 120 | func (c *cluster) markDown(endpoint string) { 121 | c.Lock() 122 | defer c.Unlock() 123 | for _, n := range c.members { 124 | // step: check if this is the node and it's marked as up - The double checking on the 125 | // nodes status ensures the multiple calls don't create multiple checks 126 | if n.status == memberStatusUp && n.endpoint == endpoint { 127 | n.status = memberStatusDown 128 | go c.healthCheckNode(n) 129 | break 130 | } 131 | } 132 | } 133 | 134 | // healthCheckNode performs a health check on the node and when active updates the status 135 | func (c *cluster) healthCheckNode(node *member) { 136 | // step: wait for the node to become active ... we are assuming a /ping is enough here 137 | ticker := time.NewTicker(c.healthCheckInterval) 138 | defer ticker.Stop() 139 | for range ticker.C { 140 | req, err := c.client.buildMarathonRequest("GET", node.endpoint, "ping", nil) 141 | if err == nil { 142 | res, err := c.client.Do(req) 143 | if err == nil && res.StatusCode == 200 { 144 | // step: mark the node as active again 145 | c.Lock() 146 | node.status = memberStatusUp 147 | c.Unlock() 148 | break 149 | } 150 | } 151 | } 152 | } 153 | 154 | // activeMembers returns a list of active members 155 | func (c *cluster) activeMembers() []string { 156 | return c.membersList(memberStatusUp) 157 | } 158 | 159 | // nonActiveMembers returns a list of non-active members in the cluster 160 | func (c *cluster) nonActiveMembers() []string { 161 | return c.membersList(memberStatusDown) 162 | } 163 | 164 | // memberList returns a list of members of a specified status 165 | func (c *cluster) membersList(status memberStatus) []string { 166 | c.RLock() 167 | defer c.RUnlock() 168 | var list []string 169 | for _, m := range c.members { 170 | if m.status == status { 171 | list = append(list, m.endpoint) 172 | } 173 | } 174 | 175 | return list 176 | } 177 | 178 | // size returns the size of the cluster 179 | func (c *cluster) size() int { 180 | return len(c.members) 181 | } 182 | 183 | // String returns a string representation 184 | func (m member) String() string { 185 | status := "UP" 186 | if m.status == memberStatusDown { 187 | status = "DOWN" 188 | } 189 | 190 | return fmt.Sprintf("member: %s:%s", m.endpoint, status) 191 | } 192 | -------------------------------------------------------------------------------- /pod_scheduling.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | // PodBackoff describes the backoff for re-run attempts of a pod 20 | type PodBackoff struct { 21 | Backoff *float64 `json:"backoff,omitempty"` 22 | BackoffFactor *float64 `json:"backoffFactor,omitempty"` 23 | MaxLaunchDelay *float64 `json:"maxLaunchDelay,omitempty"` 24 | } 25 | 26 | // PodUpgrade describes the policy for upgrading a pod in-place 27 | type PodUpgrade struct { 28 | MinimumHealthCapacity *float64 `json:"minimumHealthCapacity,omitempty"` 29 | MaximumOverCapacity *float64 `json:"maximumOverCapacity,omitempty"` 30 | } 31 | 32 | // PodPlacement supports constraining which hosts a pod is placed on 33 | type PodPlacement struct { 34 | Constraints *[]Constraint `json:"constraints"` 35 | AcceptedResourceRoles []string `json:"acceptedResourceRoles,omitempty"` 36 | } 37 | 38 | // PodSchedulingPolicy is the overarching pod scheduling policy 39 | type PodSchedulingPolicy struct { 40 | Backoff *PodBackoff `json:"backoff,omitempty"` 41 | Upgrade *PodUpgrade `json:"upgrade,omitempty"` 42 | Placement *PodPlacement `json:"placement,omitempty"` 43 | UnreachableStrategy *UnreachableStrategy `json:"unreachableStrategy,omitempty"` 44 | KillSelection string `json:"killSelection,omitempty"` 45 | } 46 | 47 | // Constraint describes the constraint for pod placement 48 | type Constraint struct { 49 | FieldName string `json:"fieldName"` 50 | Operator string `json:"operator"` 51 | Value string `json:"value,omitempty"` 52 | } 53 | 54 | // NewPodPlacement creates an empty PodPlacement 55 | func NewPodPlacement() *PodPlacement { 56 | return &PodPlacement{ 57 | Constraints: &[]Constraint{}, 58 | AcceptedResourceRoles: []string{}, 59 | } 60 | } 61 | 62 | // AddConstraint adds a new constraint 63 | // constraints: the constraint definition, one constraint per array element 64 | func (p *PodPlacement) AddConstraint(constraint Constraint) *PodPlacement { 65 | c := *p.Constraints 66 | c = append(c, constraint) 67 | p.Constraints = &c 68 | 69 | return p 70 | } 71 | 72 | // NewPodSchedulingPolicy creates an empty PodSchedulingPolicy 73 | func NewPodSchedulingPolicy() *PodSchedulingPolicy { 74 | return &PodSchedulingPolicy{ 75 | Placement: NewPodPlacement(), 76 | } 77 | } 78 | 79 | // NewPodBackoff creates an empty PodBackoff 80 | func NewPodBackoff() *PodBackoff { 81 | return &PodBackoff{} 82 | } 83 | 84 | // NewPodUpgrade creates a new PodUpgrade 85 | func NewPodUpgrade() *PodUpgrade { 86 | return &PodUpgrade{} 87 | } 88 | 89 | // SetBackoff sets the base backoff interval for failed pod launches, in seconds 90 | func (p *PodBackoff) SetBackoff(backoffSeconds float64) *PodBackoff { 91 | p.Backoff = &backoffSeconds 92 | return p 93 | } 94 | 95 | // SetBackoffFactor sets the backoff interval growth factor for failed pod launches 96 | func (p *PodBackoff) SetBackoffFactor(backoffFactor float64) *PodBackoff { 97 | p.BackoffFactor = &backoffFactor 98 | return p 99 | } 100 | 101 | // SetMaxLaunchDelay sets the maximum backoff interval for failed pod launches, in seconds 102 | func (p *PodBackoff) SetMaxLaunchDelay(maxLaunchDelaySeconds float64) *PodBackoff { 103 | p.MaxLaunchDelay = &maxLaunchDelaySeconds 104 | return p 105 | } 106 | 107 | // SetMinimumHealthCapacity sets the minimum amount of pod instances for healthy operation, expressed as a fraction of instance count 108 | func (p *PodUpgrade) SetMinimumHealthCapacity(capacity float64) *PodUpgrade { 109 | p.MinimumHealthCapacity = &capacity 110 | return p 111 | } 112 | 113 | // SetMaximumOverCapacity sets the maximum amount of pod instances above the instance count, expressed as a fraction of instance count 114 | func (p *PodUpgrade) SetMaximumOverCapacity(capacity float64) *PodUpgrade { 115 | p.MaximumOverCapacity = &capacity 116 | return p 117 | } 118 | 119 | // SetBackoff sets the pod's backoff settings 120 | func (p *PodSchedulingPolicy) SetBackoff(backoff *PodBackoff) *PodSchedulingPolicy { 121 | p.Backoff = backoff 122 | return p 123 | } 124 | 125 | // SetUpgrade sets the pod's upgrade settings 126 | func (p *PodSchedulingPolicy) SetUpgrade(upgrade *PodUpgrade) *PodSchedulingPolicy { 127 | p.Upgrade = upgrade 128 | return p 129 | } 130 | 131 | // SetPlacement sets the pod's placement settings 132 | func (p *PodSchedulingPolicy) SetPlacement(placement *PodPlacement) *PodSchedulingPolicy { 133 | p.Placement = placement 134 | return p 135 | } 136 | 137 | // SetKillSelection sets the pod's kill selection criteria when terminating pod instances 138 | func (p *PodSchedulingPolicy) SetKillSelection(killSelection string) *PodSchedulingPolicy { 139 | p.KillSelection = killSelection 140 | return p 141 | } 142 | 143 | // SetUnreachableStrategy sets the pod's unreachable strategy for lost instances 144 | func (p *PodSchedulingPolicy) SetUnreachableStrategy(strategy EnabledUnreachableStrategy) *PodSchedulingPolicy { 145 | p.UnreachableStrategy = &UnreachableStrategy{ 146 | EnabledUnreachableStrategy: strategy, 147 | } 148 | return p 149 | } 150 | 151 | // SetUnreachableStrategyDisabled disables the pod's unreachable strategy 152 | func (p *PodSchedulingPolicy) SetUnreachableStrategyDisabled() *PodSchedulingPolicy { 153 | p.UnreachableStrategy = &UnreachableStrategy{ 154 | AbsenceReason: UnreachableStrategyAbsenceReasonDisabled, 155 | } 156 | return p 157 | } 158 | -------------------------------------------------------------------------------- /deployment.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "time" 23 | ) 24 | 25 | // Deployment is a marathon deployment definition 26 | type Deployment struct { 27 | ID string `json:"id"` 28 | Version string `json:"version"` 29 | CurrentStep int `json:"currentStep"` 30 | TotalSteps int `json:"totalSteps"` 31 | AffectedApps []string `json:"affectedApps"` 32 | AffectedPods []string `json:"affectedPods"` 33 | Steps [][]*DeploymentStep `json:"-"` 34 | XXStepsRaw json.RawMessage `json:"steps"` // Holds raw steps JSON to unmarshal later 35 | CurrentActions []*DeploymentStep `json:"currentActions"` 36 | } 37 | 38 | // DeploymentID is the identifier for a application deployment 39 | type DeploymentID struct { 40 | DeploymentID string `json:"deploymentId"` 41 | Version string `json:"version"` 42 | } 43 | 44 | // DeploymentStep is a step in the application deployment plan 45 | type DeploymentStep struct { 46 | Action string `json:"action"` 47 | App string `json:"app"` 48 | ReadinessCheckResults *[]ReadinessCheckResult `json:"readinessCheckResults,omitempty"` 49 | } 50 | 51 | // StepActions is a series of deployment steps 52 | type StepActions struct { 53 | Actions []struct { 54 | Action string `json:"action"` // 1.1.2 and after 55 | Type string `json:"type"` // 1.1.1 and before 56 | App string `json:"app"` 57 | } 58 | } 59 | 60 | // DeploymentPlan is a collection of steps for application deployment 61 | type DeploymentPlan struct { 62 | ID string `json:"id"` 63 | Version string `json:"version"` 64 | Original *Group `json:"original"` 65 | Target *Group `json:"target"` 66 | Steps []*StepActions `json:"steps"` 67 | } 68 | 69 | // Deployments retrieves a list of current deployments 70 | func (r *marathonClient) Deployments() ([]*Deployment, error) { 71 | var deployments []*Deployment 72 | err := r.apiGet(marathonAPIDeployments, nil, &deployments) 73 | if err != nil { 74 | return nil, err 75 | } 76 | // Allows loading of deployment steps from the Marathon v1.X API 77 | // Implements a fix for issue https://github.com/gambol99/go-marathon/issues/153 78 | for _, deployment := range deployments { 79 | // Unmarshal pre-v1.X step 80 | if err := json.Unmarshal(deployment.XXStepsRaw, &deployment.Steps); err != nil { 81 | deployment.Steps = make([][]*DeploymentStep, 0) 82 | var steps []*StepActions 83 | // Unmarshal v1.X Marathon step 84 | if err := json.Unmarshal(deployment.XXStepsRaw, &steps); err != nil { 85 | return nil, err 86 | } 87 | for stepIndex, step := range steps { 88 | deployment.Steps = append(deployment.Steps, make([]*DeploymentStep, len(step.Actions))) 89 | for actionIndex, action := range step.Actions { 90 | var stepAction string 91 | if action.Type != "" { 92 | stepAction = action.Type 93 | } else { 94 | stepAction = action.Action 95 | } 96 | deployment.Steps[stepIndex][actionIndex] = &DeploymentStep{ 97 | Action: stepAction, 98 | App: action.App, 99 | } 100 | } 101 | } 102 | } 103 | } 104 | return deployments, nil 105 | } 106 | 107 | // DeleteDeployment delete a current deployment from marathon 108 | // id: the deployment id you wish to delete 109 | // force: whether or not to force the deletion 110 | func (r *marathonClient) DeleteDeployment(id string, force bool) (*DeploymentID, error) { 111 | path := fmt.Sprintf("%s/%s", marathonAPIDeployments, id) 112 | 113 | // if force=true, no body is returned 114 | if force { 115 | path += "?force=true" 116 | return nil, r.apiDelete(path, nil, nil) 117 | } 118 | 119 | deployment := new(DeploymentID) 120 | err := r.apiDelete(path, nil, deployment) 121 | 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | return deployment, nil 127 | } 128 | 129 | // HasDeployment checks to see if a deployment exists 130 | // id: the deployment id you are looking for 131 | func (r *marathonClient) HasDeployment(id string) (bool, error) { 132 | deployments, err := r.Deployments() 133 | if err != nil { 134 | return false, err 135 | } 136 | for _, deployment := range deployments { 137 | if deployment.ID == id { 138 | return true, nil 139 | } 140 | } 141 | return false, nil 142 | } 143 | 144 | // WaitOnDeployment waits on a deployment to finish 145 | // version: the version of the application 146 | // timeout: the timeout to wait for the deployment to take, otherwise return an error 147 | func (r *marathonClient) WaitOnDeployment(id string, timeout time.Duration) error { 148 | if found, err := r.HasDeployment(id); err != nil { 149 | return err 150 | } else if !found { 151 | return nil 152 | } 153 | 154 | nowTime := time.Now() 155 | stopTime := nowTime.Add(timeout) 156 | if timeout <= 0 { 157 | stopTime = nowTime.Add(time.Duration(900) * time.Second) 158 | } 159 | 160 | // step: a somewhat naive implementation, but it will work 161 | for { 162 | if time.Now().After(stopTime) { 163 | return ErrTimeoutError 164 | } 165 | found, err := r.HasDeployment(id) 166 | if err != nil { 167 | return err 168 | } 169 | if !found { 170 | return nil 171 | } 172 | time.Sleep(r.config.PollingWaitTime) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [Unreleased] 8 | ### Added 9 | - [#273][PR273] Implement readiness checks. 10 | - [#267][PR267] Add DCOS path parameter for additional marathon instances. 11 | 12 | ## [0.7.1] - 2017-02-20 13 | ### Fixed 14 | - [#261][PR261] Fix URL parsing for Go 1.8. 15 | 16 | ## [0.7.0] - 2017-02-17 17 | ### Added 18 | - [#256][PR256] Expose task state. 19 | 20 | ### Changed 21 | - [#259][PR259] Add 'omitempty' to UpgradeStrategy properties. 22 | 23 | ## [0.6.0] - 2016-12-14 24 | ### Added 25 | - [#246][PR246] Add TaskKillGracePeriodSeconds support. 26 | - [#244][PR244] Add taskStats support. 27 | 28 | ### Changed 29 | - [#242][PR242] Pointerize IPAddressPerTask.Discovery. 30 | 31 | ## [0.5.1] - 2016-11-09 32 | ### Fixed 33 | - [#239][PR239] Fix scheme-less endpoint with port. 34 | 35 | ## [0.5.0] - 2016-11-07 36 | ### Fixed 37 | - [#231][PR231] Fixed Marathon cluster code 38 | - [#229][PR229] Add LastFailureCause field to HealthCheckResult. 39 | 40 | ## [0.4.0] - 2016-10-28 41 | ### Added 42 | - [#223][PR223] Add support for IP-per-task. 43 | - [#220][PR220] Add external volume definition to container. 44 | - [#211][PR211] Close event channel on event listener removal. 45 | 46 | ### Fixed 47 | - [#218][PR218] Remove TimeWaitPolling from marathonClient. 48 | - [#214][PR214] Remove extra pointer layers when passing to r.api*. 49 | 50 | ## [0.3.0] - 2016-09-28 51 | - [#201][PR201]: Subscribe method is now exposed on the client to allow subscription of callback URL's 52 | 53 | ### Fixed 54 | - [#205][PR205]: Fix memory leak by signalling goroutine termination on event listener removal. 55 | 56 | ### Changed 57 | - [#205][PR205]: Change AddEventsListener to return event channel instead of taking one. 58 | 59 | ## [0.2.0] - 2016-09-23 60 | ### Added 61 | - [#196][PR196]: Port definitions. 62 | - [#191][PR191]: name and labels to portMappings. 63 | 64 | ### Changed 65 | - [#191][PR191] ExposePort() now takes a portMapping instance. 66 | 67 | ### Fixed 68 | - [#202][PR202]: Timeout error in WaitOnApplication. 69 | 70 | ## [0.1.1] - 2016-09-07 71 | ### Fixed 72 | - Drop question mark-only query parameter in Applications(url.Values) manually 73 | due to changed behavior in Go 1.7's net/url.Parse. 74 | 75 | ## [0.1.0] - 2016-08-01 76 | ### Added 77 | - Field `message` to the EventStatusUpdate struct. 78 | - Method `Host()` to set host mode explicitly. 79 | - Field `port` to HealthCheck. 80 | - Support for launch queues. 81 | - Convenience method `AddFetchURIs()`. 82 | - Support for forced operations across all methods. 83 | - Filtering method variants (`*By`-suffixed). 84 | - Support for Marathon DCOS token. 85 | - Basic auth and HTTP client settings. 86 | - Marshalling of `Deployment.DeploymentStep` for Marathon v1.X. 87 | - Field `ipAddresses` to tasks and events. 88 | - Field `slaveId` to tasks. 89 | - Convenience methods to populate/clear pointerized values. 90 | - Method `ApplicationByVersion()` to retrieve version-specific apps. 91 | - Support for fetch URIs. 92 | - Parse API error responses on all error types for programmatic evaluation. 93 | 94 | ### Changed 95 | - Consider app as unhealthy in ApplicationOK if health check is missing. (Ensures result stability during all phases of deployment.) 96 | - Various identifiers violating golint rules. 97 | - Do not set "bridged" mode on Docker containers by default. 98 | 99 | ### Fixed 100 | - Flawed unmarshalling of `CurrentStep` in events. 101 | - Missing omitempty tag modifiers on `Application.Uris`. 102 | - Missing leading slash in path used by `Ping()`. 103 | - Flawed `KillTask()` in case of hierarchical app ID path. 104 | - Missing omitempty tag modifier on `PortMapping.Protocol`. 105 | - Nil dereference on empty debug log. 106 | - Various occasions where omitted and empty fields could not be distinguished. 107 | 108 | ## 0.0.1 - 2016-01-27 109 | ### Added 110 | - Initial SemVer release. 111 | 112 | [Unreleased]: https://github.com/gambol99/go-marathon/compare/v0.7.1...HEAD 113 | [0.7.1]: https://github.com/gambol99/go-marathon/compare/v0.7.0...v0.7.1 114 | [0.7.0]: https://github.com/gambol99/go-marathon/compare/v0.6.0...v0.7.0 115 | [0.6.0]: https://github.com/gambol99/go-marathon/compare/v0.5.1...v0.6.0 116 | [0.5.1]: https://github.com/gambol99/go-marathon/compare/v0.5.0...v0.5.1 117 | [0.5.0]: https://github.com/gambol99/go-marathon/compare/v0.4.0...v0.5.0 118 | [0.4.0]: https://github.com/gambol99/go-marathon/compare/v0.3.0...v0.4.0 119 | [0.3.0]: https://github.com/gambol99/go-marathon/compare/v0.2.0...v0.3.0 120 | [0.2.0]: https://github.com/gambol99/go-marathon/compare/v0.1.1...v0.2.0 121 | [0.1.1]: https://github.com/gambol99/go-marathon/compare/v0.1.0...v0.1.1 122 | [0.1.0]: https://github.com/gambol99/go-marathon/compare/v0.0.1...v0.1.0 123 | 124 | [PR273]: https://github.com/gambol99/go-marathon/pull/273 125 | [PR267]: https://github.com/gambol99/go-marathon/pull/267 126 | [PR261]: https://github.com/gambol99/go-marathon/pull/261 127 | [PR259]: https://github.com/gambol99/go-marathon/pull/259 128 | [PR256]: https://github.com/gambol99/go-marathon/pull/256 129 | [PR246]: https://github.com/gambol99/go-marathon/pull/246 130 | [PR244]: https://github.com/gambol99/go-marathon/pull/244 131 | [PR242]: https://github.com/gambol99/go-marathon/pull/242 132 | [PR239]: https://github.com/gambol99/go-marathon/pull/239 133 | [PR231]: https://github.com/gambol99/go-marathon/pull/231 134 | [PR229]: https://github.com/gambol99/go-marathon/pull/229 135 | [PR223]: https://github.com/gambol99/go-marathon/pull/223 136 | [PR220]: https://github.com/gambol99/go-marathon/pull/220 137 | [PR218]: https://github.com/gambol99/go-marathon/pull/218 138 | [PR214]: https://github.com/gambol99/go-marathon/pull/214 139 | [PR211]: https://github.com/gambol99/go-marathon/pull/211 140 | [PR205]: https://github.com/gambol99/go-marathon/pull/205 141 | [PR202]: https://github.com/gambol99/go-marathon/pull/202 142 | [PR201]: https://github.com/gambol99/go-marathon/pull/201 143 | [PR196]: https://github.com/gambol99/go-marathon/pull/196 144 | [PR191]: https://github.com/gambol99/go-marathon/pull/191 145 | -------------------------------------------------------------------------------- /pod_container.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | // PodContainer describes a container in a pod 20 | type PodContainer struct { 21 | Name string `json:"name,omitempty"` 22 | Exec *PodExec `json:"exec,omitempty"` 23 | Resources *Resources `json:"resources,omitempty"` 24 | Endpoints []*PodEndpoint `json:"endpoints,omitempty"` 25 | Image *PodContainerImage `json:"image,omitempty"` 26 | Env map[string]string `json:"-"` 27 | Secrets map[string]Secret `json:"-"` 28 | User string `json:"user,omitempty"` 29 | HealthCheck *PodHealthCheck `json:"healthCheck,omitempty"` 30 | VolumeMounts []*PodVolumeMount `json:"volumeMounts,omitempty"` 31 | Artifacts []*PodArtifact `json:"artifacts,omitempty"` 32 | Labels map[string]string `json:"labels,omitempty"` 33 | Lifecycle PodLifecycle `json:"lifecycle,omitempty"` 34 | } 35 | 36 | // PodLifecycle describes the lifecycle of a pod 37 | type PodLifecycle struct { 38 | KillGracePeriodSeconds *float64 `json:"killGracePeriodSeconds,omitempty"` 39 | } 40 | 41 | // PodCommand is the command to run as the entrypoint of the container 42 | type PodCommand struct { 43 | Shell string `json:"shell,omitempty"` 44 | } 45 | 46 | // PodExec contains the PodCommand 47 | type PodExec struct { 48 | Command PodCommand `json:"command,omitempty"` 49 | } 50 | 51 | // PodArtifact describes how to obtain a generic artifact for a pod 52 | type PodArtifact struct { 53 | URI string `json:"uri,omitempty"` 54 | Extract *bool `json:"extract,omitempty"` 55 | Executable *bool `json:"executable,omitempty"` 56 | Cache *bool `json:"cache,omitempty"` 57 | DestPath string `json:"destPath,omitempty"` 58 | } 59 | 60 | // NewPodContainer creates an empty PodContainer 61 | func NewPodContainer() *PodContainer { 62 | return &PodContainer{ 63 | Endpoints: []*PodEndpoint{}, 64 | Env: map[string]string{}, 65 | VolumeMounts: []*PodVolumeMount{}, 66 | Artifacts: []*PodArtifact{}, 67 | Labels: map[string]string{}, 68 | Resources: NewResources(), 69 | } 70 | } 71 | 72 | // SetName sets the name of a pod container 73 | func (p *PodContainer) SetName(name string) *PodContainer { 74 | p.Name = name 75 | return p 76 | } 77 | 78 | // SetCommand sets the shell command of a pod container 79 | func (p *PodContainer) SetCommand(name string) *PodContainer { 80 | p.Exec = &PodExec{ 81 | Command: PodCommand{ 82 | Shell: name, 83 | }, 84 | } 85 | return p 86 | } 87 | 88 | // CPUs sets the CPUs of a pod container 89 | func (p *PodContainer) CPUs(cpu float64) *PodContainer { 90 | p.Resources.Cpus = cpu 91 | return p 92 | } 93 | 94 | // Memory sets the memory of a pod container 95 | func (p *PodContainer) Memory(memory float64) *PodContainer { 96 | p.Resources.Mem = memory 97 | return p 98 | } 99 | 100 | // Storage sets the storage capacity of a pod container 101 | func (p *PodContainer) Storage(disk float64) *PodContainer { 102 | p.Resources.Disk = disk 103 | return p 104 | } 105 | 106 | // GPUs sets the GPU requirements of a pod container 107 | func (p *PodContainer) GPUs(gpu int32) *PodContainer { 108 | p.Resources.Gpus = gpu 109 | return p 110 | } 111 | 112 | // AddEndpoint appends an endpoint for a pod container 113 | func (p *PodContainer) AddEndpoint(endpoint *PodEndpoint) *PodContainer { 114 | p.Endpoints = append(p.Endpoints, endpoint) 115 | return p 116 | } 117 | 118 | // SetImage sets the image of a pod container 119 | func (p *PodContainer) SetImage(image *PodContainerImage) *PodContainer { 120 | p.Image = image 121 | return p 122 | } 123 | 124 | // EmptyEnvs initialized env to empty 125 | func (p *PodContainer) EmptyEnvs() *PodContainer { 126 | p.Env = make(map[string]string) 127 | return p 128 | } 129 | 130 | // AddEnv adds an environment variable for a pod container 131 | func (p *PodContainer) AddEnv(name, value string) *PodContainer { 132 | if p.Env == nil { 133 | p = p.EmptyEnvs() 134 | } 135 | p.Env[name] = value 136 | return p 137 | } 138 | 139 | // ExtendEnv extends the environment for a pod container 140 | func (p *PodContainer) ExtendEnv(env map[string]string) *PodContainer { 141 | if p.Env == nil { 142 | p = p.EmptyEnvs() 143 | } 144 | for k, v := range env { 145 | p.AddEnv(k, v) 146 | } 147 | return p 148 | } 149 | 150 | // AddSecret adds a secret to the environment for a pod container 151 | func (p *PodContainer) AddSecret(name, secretName string) *PodContainer { 152 | if p.Env == nil { 153 | p = p.EmptyEnvs() 154 | } 155 | p.Env[name] = secretName 156 | return p 157 | } 158 | 159 | // SetUser sets the user to run the pod as 160 | func (p *PodContainer) SetUser(user string) *PodContainer { 161 | p.User = user 162 | return p 163 | } 164 | 165 | // SetHealthCheck sets the health check of a pod container 166 | func (p *PodContainer) SetHealthCheck(healthcheck *PodHealthCheck) *PodContainer { 167 | p.HealthCheck = healthcheck 168 | return p 169 | } 170 | 171 | // AddVolumeMount appends a volume mount to a pod container 172 | func (p *PodContainer) AddVolumeMount(mount *PodVolumeMount) *PodContainer { 173 | p.VolumeMounts = append(p.VolumeMounts, mount) 174 | return p 175 | } 176 | 177 | // AddArtifact appends an artifact to a pod container 178 | func (p *PodContainer) AddArtifact(artifact *PodArtifact) *PodContainer { 179 | p.Artifacts = append(p.Artifacts, artifact) 180 | return p 181 | } 182 | 183 | // AddLabel adds a label to a pod container 184 | func (p *PodContainer) AddLabel(key, value string) *PodContainer { 185 | p.Labels[key] = value 186 | return p 187 | } 188 | 189 | // SetLifecycle sets the lifecycle of a pod container 190 | func (p *PodContainer) SetLifecycle(lifecycle PodLifecycle) *PodContainer { 191 | p.Lifecycle = lifecycle 192 | return p 193 | } 194 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "fmt" 21 | "net/http" 22 | "strings" 23 | "testing" 24 | 25 | "github.com/stretchr/testify/assert" 26 | ) 27 | 28 | func TestErrors(t *testing.T) { 29 | tests := []struct { 30 | httpCode int 31 | nameSuffix string 32 | errCode int 33 | errText string 34 | content string 35 | }{ 36 | // 400 37 | { 38 | httpCode: http.StatusBadRequest, 39 | errCode: ErrCodeBadRequest, 40 | errText: "Invalid JSON (path: '/id' errors: error.expected.jsstring, error.something.else; path: '/name' errors: error.not.inventive)", 41 | content: content400(), 42 | }, 43 | // 401 44 | { 45 | httpCode: http.StatusUnauthorized, 46 | errCode: ErrCodeUnauthorized, 47 | errText: "invalid username or password.", 48 | content: `{"message": "invalid username or password."}`, 49 | }, 50 | // 403 51 | { 52 | httpCode: http.StatusForbidden, 53 | errCode: ErrCodeForbidden, 54 | errText: "Not Authorized to perform this action!", 55 | content: `{"message": "Not Authorized to perform this action!"}`, 56 | }, 57 | // 404 58 | { 59 | httpCode: http.StatusNotFound, 60 | errCode: ErrCodeNotFound, 61 | errText: "App '/not_existent' does not exist", 62 | content: `{"message": "App '/not_existent' does not exist"}`, 63 | }, 64 | // 405 65 | { 66 | httpCode: http.StatusMethodNotAllowed, 67 | errCode: ErrCodeMethodNotAllowed, 68 | errText: "", 69 | content: `{"message": null}`, 70 | }, 71 | // 409 POST 72 | { 73 | httpCode: http.StatusConflict, 74 | nameSuffix: "POST", 75 | errCode: ErrCodeDuplicateID, 76 | errText: "An app with id [/existing_app] already exists.", 77 | content: `{"message": "An app with id [/existing_app] already exists."}`, 78 | }, 79 | // 409 PUT 80 | { 81 | httpCode: http.StatusConflict, 82 | nameSuffix: "PUT", 83 | errCode: ErrCodeAppLocked, 84 | errText: "App is locked (locking deployment IDs: 97c136bf-5a28-4821-9d94-480d9fbb01c8)", 85 | content: `{"message":"App is locked", "deployments": [ { "id": "97c136bf-5a28-4821-9d94-480d9fbb01c8" } ] }`, 86 | }, 87 | // 422 pre-1.0 "details" key 88 | { 89 | httpCode: 422, 90 | nameSuffix: "pre-1.0 details key", 91 | errCode: ErrCodeInvalidBean, 92 | errText: "Something is not valid (attribute 'upgradeStrategy.minimumHealthCapacity': is greater than 1; attribute 'foobar': foo does not have enough bar)", 93 | content: content422("details"), 94 | }, 95 | // 422 pre-1.0 "errors" key 96 | { 97 | httpCode: 422, 98 | nameSuffix: "pre-1.0 errors key", 99 | errCode: ErrCodeInvalidBean, 100 | errText: "Something is not valid (attribute 'upgradeStrategy.minimumHealthCapacity': is greater than 1; attribute 'foobar': foo does not have enough bar)", 101 | content: content422("errors"), 102 | }, 103 | // 422 1.0 "invalid object" 104 | { 105 | httpCode: 422, 106 | nameSuffix: "invalid object", 107 | errCode: ErrCodeInvalidBean, 108 | errText: "Object is not valid (path: 'upgradeStrategy.minimumHealthCapacity' errors: is greater than 1; path: '/value' errors: service port conflict app /app1, service port conflict app /app2)", 109 | content: content422V1(), 110 | }, 111 | // 499 unknown error 112 | { 113 | httpCode: 499, 114 | nameSuffix: "unknown error", 115 | errCode: ErrCodeUnknown, 116 | errText: "unknown error", 117 | content: `{"message": "unknown error"}`, 118 | }, 119 | // 500 120 | { 121 | httpCode: http.StatusInternalServerError, 122 | errCode: ErrCodeServer, 123 | errText: "internal server error", 124 | content: `{"message": "internal server error"}`, 125 | }, 126 | // 503 (no JSON) 127 | { 128 | httpCode: http.StatusServiceUnavailable, 129 | nameSuffix: "no JSON", 130 | errCode: ErrCodeServer, 131 | errText: "No server is available to handle this request.", 132 | content: `No server is available to handle this request.`, 133 | }, 134 | } 135 | 136 | for _, test := range tests { 137 | name := fmt.Sprintf("%d", test.httpCode) 138 | if len(test.nameSuffix) > 0 { 139 | name = fmt.Sprintf("%s (%s)", name, test.nameSuffix) 140 | } 141 | apiErr := NewAPIError(test.httpCode, []byte(test.content)) 142 | gotErrCode := apiErr.(*APIError).ErrCode 143 | assert.Equal(t, test.errCode, gotErrCode, fmt.Sprintf("HTTP code %s (error code): got %d, want %d", name, gotErrCode, test.errCode)) 144 | pureErrText := strings.TrimPrefix(apiErr.Error(), "Marathon API error: ") 145 | assert.Equal(t, pureErrText, test.errText, fmt.Sprintf("HTTP code %s (error text)", name)) 146 | } 147 | } 148 | 149 | func content400() string { 150 | return `{ 151 | "message": "Invalid JSON", 152 | "details": [ 153 | { 154 | "path": "/id", 155 | "errors": ["error.expected.jsstring", "error.something.else"] 156 | }, 157 | { 158 | "path": "/name", 159 | "errors": ["error.not.inventive"] 160 | } 161 | ] 162 | }` 163 | } 164 | 165 | func content422(detailsPropKey string) string { 166 | return fmt.Sprintf(`{ 167 | "message": "Something is not valid", 168 | "%s": [ 169 | { 170 | "attribute": "upgradeStrategy.minimumHealthCapacity", 171 | "error": "is greater than 1" 172 | }, 173 | { 174 | "attribute": "foobar", 175 | "error": "foo does not have enough bar" 176 | } 177 | ] 178 | }`, detailsPropKey) 179 | } 180 | 181 | func content422V1() string { 182 | return `{ 183 | "message": "Object is not valid", 184 | "details": [ 185 | { 186 | "path": "upgradeStrategy.minimumHealthCapacity", 187 | "errors": ["is greater than 1"] 188 | }, 189 | { 190 | "path": "/value", 191 | "errors": ["service port conflict app /app1", "service port conflict app /app2"] 192 | } 193 | ] 194 | }` 195 | } 196 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "net/http" 23 | "strings" 24 | ) 25 | 26 | const ( 27 | // ErrCodeBadRequest specifies a 400 Bad Request error. 28 | ErrCodeBadRequest = iota 29 | // ErrCodeUnauthorized specifies a 401 Unauthorized error. 30 | ErrCodeUnauthorized 31 | // ErrCodeForbidden specifies a 403 Forbidden error. 32 | ErrCodeForbidden 33 | // ErrCodeNotFound specifies a 404 Not Found error. 34 | ErrCodeNotFound 35 | // ErrCodeDuplicateID specifies a PUT 409 Conflict error. 36 | ErrCodeDuplicateID 37 | // ErrCodeAppLocked specifies a POST 409 Conflict error. 38 | ErrCodeAppLocked 39 | // ErrCodeInvalidBean specifies a 422 UnprocessableEntity error. 40 | ErrCodeInvalidBean 41 | // ErrCodeServer specifies a 500+ Server error. 42 | ErrCodeServer 43 | // ErrCodeUnknown specifies an unknown error. 44 | ErrCodeUnknown 45 | // ErrCodeMethodNotAllowed specifies a 405 Method Not Allowed. 46 | ErrCodeMethodNotAllowed 47 | ) 48 | 49 | // InvalidEndpointError indicates a endpoint error in the marathon urls 50 | type InvalidEndpointError struct { 51 | message string 52 | } 53 | 54 | // Error returns the string message 55 | func (e *InvalidEndpointError) Error() string { 56 | return e.message 57 | } 58 | 59 | // newInvalidEndpointError creates a new error 60 | func newInvalidEndpointError(message string, args ...interface{}) error { 61 | return &InvalidEndpointError{message: fmt.Sprintf(message, args...)} 62 | } 63 | 64 | // APIError represents a generic API error. 65 | type APIError struct { 66 | // ErrCode specifies the nature of the error. 67 | ErrCode int 68 | message string 69 | } 70 | 71 | func (e *APIError) Error() string { 72 | return fmt.Sprintf("Marathon API error: %s", e.message) 73 | } 74 | 75 | // NewAPIError creates a new APIError instance from the given response code and content. 76 | func NewAPIError(code int, content []byte) error { 77 | var errDef errorDefinition 78 | switch { 79 | case code == http.StatusBadRequest: 80 | errDef = &badRequestDef{} 81 | case code == http.StatusUnauthorized: 82 | errDef = &simpleErrDef{code: ErrCodeUnauthorized} 83 | case code == http.StatusForbidden: 84 | errDef = &simpleErrDef{code: ErrCodeForbidden} 85 | case code == http.StatusNotFound: 86 | errDef = &simpleErrDef{code: ErrCodeNotFound} 87 | case code == http.StatusMethodNotAllowed: 88 | errDef = &simpleErrDef{code: ErrCodeMethodNotAllowed} 89 | case code == http.StatusConflict: 90 | errDef = &conflictDef{} 91 | case code == 422: 92 | errDef = &unprocessableEntityDef{} 93 | case code >= http.StatusInternalServerError: 94 | errDef = &simpleErrDef{code: ErrCodeServer} 95 | default: 96 | errDef = &simpleErrDef{code: ErrCodeUnknown} 97 | } 98 | 99 | return parseContent(errDef, content) 100 | } 101 | 102 | type errorDefinition interface { 103 | message() string 104 | errCode() int 105 | } 106 | 107 | func parseContent(errDef errorDefinition, content []byte) error { 108 | // If the content cannot be JSON-unmarshalled, we assume that it's not JSON 109 | // and encode it into the APIError instance as-is. 110 | errMessage := string(content) 111 | if err := json.Unmarshal(content, errDef); err == nil { 112 | errMessage = errDef.message() 113 | } 114 | 115 | return &APIError{message: errMessage, ErrCode: errDef.errCode()} 116 | } 117 | 118 | type simpleErrDef struct { 119 | Message string `json:"message"` 120 | code int 121 | } 122 | 123 | func (def *simpleErrDef) message() string { 124 | return def.Message 125 | } 126 | 127 | func (def *simpleErrDef) errCode() int { 128 | return def.code 129 | } 130 | 131 | type detailDescription struct { 132 | Path string `json:"path"` 133 | Errors []string `json:"errors"` 134 | } 135 | 136 | func (d detailDescription) String() string { 137 | return fmt.Sprintf("path: '%s' errors: %s", d.Path, strings.Join(d.Errors, ", ")) 138 | } 139 | 140 | type badRequestDef struct { 141 | Message string `json:"message"` 142 | Details []detailDescription `json:"details"` 143 | } 144 | 145 | func (def *badRequestDef) message() string { 146 | var details []string 147 | for _, detail := range def.Details { 148 | details = append(details, detail.String()) 149 | } 150 | 151 | return fmt.Sprintf("%s (%s)", def.Message, strings.Join(details, "; ")) 152 | } 153 | 154 | func (def *badRequestDef) errCode() int { 155 | return ErrCodeBadRequest 156 | } 157 | 158 | type conflictDef struct { 159 | Message string `json:"message"` 160 | Deployments []struct { 161 | ID string `json:"id"` 162 | } `json:"deployments"` 163 | } 164 | 165 | func (def *conflictDef) message() string { 166 | if len(def.Deployments) == 0 { 167 | // 409 Conflict response to "POST /v2/apps". 168 | return def.Message 169 | } 170 | 171 | // 409 Conflict response to "PUT /v2/apps/{appId}". 172 | var ids []string 173 | for _, deployment := range def.Deployments { 174 | ids = append(ids, deployment.ID) 175 | } 176 | return fmt.Sprintf("%s (locking deployment IDs: %s)", def.Message, strings.Join(ids, ", ")) 177 | } 178 | 179 | func (def *conflictDef) errCode() int { 180 | if len(def.Deployments) == 0 { 181 | return ErrCodeDuplicateID 182 | } 183 | 184 | return ErrCodeAppLocked 185 | } 186 | 187 | type unprocessableEntityDetails []struct { 188 | // Used in Marathon >= 1.0.0-RC1. 189 | detailDescription 190 | // Used in Marathon < 1.0.0-RC1. 191 | Attribute string `json:"attribute"` 192 | Error string `json:"error"` 193 | } 194 | 195 | type unprocessableEntityDef struct { 196 | Message string `json:"message"` 197 | // Name used in Marathon >= 0.15.0. 198 | Details unprocessableEntityDetails `json:"details"` 199 | // Name used in Marathon < 0.15.0. 200 | Errors unprocessableEntityDetails `json:"errors"` 201 | } 202 | 203 | func (def *unprocessableEntityDef) message() string { 204 | joinDetails := func(details unprocessableEntityDetails) []string { 205 | var res []string 206 | for _, detail := range details { 207 | res = append(res, fmt.Sprintf("attribute '%s': %s", detail.Attribute, detail.Error)) 208 | } 209 | return res 210 | } 211 | 212 | var details []string 213 | switch { 214 | case len(def.Errors) > 0: 215 | details = joinDetails(def.Errors) 216 | case len(def.Details) > 0 && len(def.Details[0].Attribute) > 0: 217 | details = joinDetails(def.Details) 218 | default: 219 | for _, detail := range def.Details { 220 | details = append(details, detail.detailDescription.String()) 221 | } 222 | } 223 | 224 | return fmt.Sprintf("%s (%s)", def.Message, strings.Join(details, "; ")) 225 | } 226 | 227 | func (def *unprocessableEntityDef) errCode() int { 228 | return ErrCodeInvalidBean 229 | } 230 | -------------------------------------------------------------------------------- /docker_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | "github.com/stretchr/testify/require" 24 | ) 25 | 26 | func createPortMapping(containerPort int, protocol string) *PortMapping { 27 | return &PortMapping{ 28 | ContainerPort: containerPort, 29 | HostPort: 0, 30 | ServicePort: 0, 31 | Protocol: protocol, 32 | } 33 | } 34 | 35 | func TestDockerAddParameter(t *testing.T) { 36 | docker := NewDockerApplication().Container.Docker 37 | docker.AddParameter("k1", "v1").AddParameter("k2", "v2") 38 | 39 | assert.Equal(t, 2, len(*docker.Parameters)) 40 | assert.Equal(t, (*docker.Parameters)[0].Key, "k1") 41 | assert.Equal(t, (*docker.Parameters)[0].Value, "v1") 42 | assert.Equal(t, (*docker.Parameters)[1].Key, "k2") 43 | assert.Equal(t, (*docker.Parameters)[1].Value, "v2") 44 | 45 | docker.EmptyParameters() 46 | assert.NotNil(t, docker.Parameters) 47 | assert.Equal(t, 0, len(*docker.Parameters)) 48 | } 49 | func TestDockerExpose(t *testing.T) { 50 | apps := []*Application{ 51 | NewDockerApplication(), 52 | NewDockerApplication(), 53 | } 54 | 55 | // Marathon < 1.5 56 | apps[0].Container.Docker.Expose(8080).Expose(80, 443) 57 | 58 | // Marathon >= 1.5 59 | apps[1].Container.Expose(8080).Expose(80, 443) 60 | 61 | portMappings := []*[]PortMapping{ 62 | apps[0].Container.Docker.PortMappings, 63 | apps[1].Container.PortMappings, 64 | } 65 | 66 | for _, portMapping := range portMappings { 67 | assert.Equal(t, 3, len(*portMapping)) 68 | 69 | assert.Equal(t, *createPortMapping(8080, "tcp"), (*portMapping)[0]) 70 | assert.Equal(t, *createPortMapping(80, "tcp"), (*portMapping)[1]) 71 | assert.Equal(t, *createPortMapping(443, "tcp"), (*portMapping)[2]) 72 | } 73 | } 74 | 75 | func TestDockerExposeUDP(t *testing.T) { 76 | apps := []*Application{ 77 | NewDockerApplication(), 78 | NewDockerApplication(), 79 | } 80 | 81 | // Marathon < 1.5 82 | apps[0].Container.Docker.ExposeUDP(53).ExposeUDP(5060, 6881) 83 | 84 | // Marathon >= 1.5 85 | apps[1].Container.ExposeUDP(53).ExposeUDP(5060, 6881) 86 | 87 | portMappings := []*[]PortMapping{ 88 | apps[0].Container.Docker.PortMappings, 89 | apps[1].Container.PortMappings, 90 | } 91 | 92 | for _, portMapping := range portMappings { 93 | assert.Equal(t, 3, len(*portMapping)) 94 | assert.Equal(t, *createPortMapping(53, "udp"), (*portMapping)[0]) 95 | assert.Equal(t, *createPortMapping(5060, "udp"), (*portMapping)[1]) 96 | assert.Equal(t, *createPortMapping(6881, "udp"), (*portMapping)[2]) 97 | } 98 | } 99 | 100 | func TestPortMappingLabels(t *testing.T) { 101 | pm := createPortMapping(80, "tcp") 102 | 103 | pm.AddLabel("hello", "world").AddLabel("foo", "bar") 104 | 105 | assert.Equal(t, 2, len(*pm.Labels)) 106 | assert.Equal(t, "world", (*pm.Labels)["hello"]) 107 | assert.Equal(t, "bar", (*pm.Labels)["foo"]) 108 | 109 | pm.EmptyLabels() 110 | 111 | assert.NotNil(t, pm.Labels) 112 | assert.Equal(t, 0, len(*pm.Labels)) 113 | } 114 | 115 | func TestPortMappingNetworkNames(t *testing.T) { 116 | pm := createPortMapping(80, "tcp") 117 | 118 | pm.AddNetwork("test") 119 | 120 | assert.Equal(t, 1, len(*pm.NetworkNames)) 121 | assert.Equal(t, "test", (*pm.NetworkNames)[0]) 122 | 123 | pm.EmptyNetworkNames() 124 | 125 | assert.NotNil(t, pm.NetworkNames) 126 | assert.Equal(t, 0, len(*pm.NetworkNames)) 127 | } 128 | 129 | func TestVolume(t *testing.T) { 130 | container := NewDockerApplication().Container 131 | 132 | container.Volume("hp1", "cp1", "RW") 133 | container.Volume("hp2", "cp2", "R") 134 | 135 | assert.Equal(t, 2, len(*container.Volumes)) 136 | assert.Equal(t, (*container.Volumes)[0].HostPath, "hp1") 137 | assert.Equal(t, (*container.Volumes)[0].ContainerPath, "cp1") 138 | assert.Equal(t, (*container.Volumes)[0].Mode, "RW") 139 | assert.Equal(t, (*container.Volumes)[1].HostPath, "hp2") 140 | assert.Equal(t, (*container.Volumes)[1].ContainerPath, "cp2") 141 | assert.Equal(t, (*container.Volumes)[1].Mode, "R") 142 | } 143 | 144 | func TestSecretVolume(t *testing.T) { 145 | container := NewDockerApplication().Container 146 | 147 | container.Volume("", "oldPath", "") 148 | 149 | sv1 := (*container.Volumes)[0] 150 | assert.Equal(t, sv1.ContainerPath, "oldPath") 151 | 152 | sv1.SetSecretVolume("newPath", "some-secret") 153 | assert.Equal(t, sv1.ContainerPath, "newPath") 154 | assert.Equal(t, sv1.Secret, "some-secret") 155 | } 156 | 157 | func TestExternalVolume(t *testing.T) { 158 | container := NewDockerApplication().Container 159 | 160 | container.Volume("", "cp", "RW") 161 | ev := (*container.Volumes)[0].SetExternalVolume("myVolume", "dvdi") 162 | 163 | ev.AddOption("prop", "pval") 164 | ev.AddOption("dvdi", "rexray") 165 | 166 | ev1 := (*container.Volumes)[0].External 167 | assert.Equal(t, ev1.Name, "myVolume") 168 | assert.Equal(t, ev1.Provider, "dvdi") 169 | if assert.Equal(t, len(*ev1.Options), 2) { 170 | assert.Equal(t, (*ev1.Options)["dvdi"], "rexray") 171 | assert.Equal(t, (*ev1.Options)["prop"], "pval") 172 | } 173 | 174 | // empty the external volume again 175 | (*container.Volumes)[0].EmptyExternalVolume() 176 | ev2 := (*container.Volumes)[0].External 177 | assert.Equal(t, ev2.Name, "") 178 | assert.Equal(t, ev2.Provider, "") 179 | } 180 | 181 | func TestDockerPersistentVolume(t *testing.T) { 182 | docker := NewDockerApplication() 183 | container := docker.Container.Volume("/host", "/container", "RW") 184 | require.Equal(t, 1, len(*docker.Container.Volumes)) 185 | 186 | pVol := (*container.Volumes)[0].SetPersistentVolume() 187 | pVol.SetType(PersistentVolumeTypeMount) 188 | pVol.SetSize(256) 189 | pVol.SetMaxSize(128) 190 | pVol.AddConstraint("cons1", "EQUAL", "tag1") 191 | pVol.AddConstraint("cons2", "UNIQUE") 192 | 193 | assert.Equal(t, 256, pVol.Size) 194 | assert.Equal(t, PersistentVolumeTypeMount, pVol.Type) 195 | assert.Equal(t, 128, pVol.MaxSize) 196 | 197 | if assert.NotNil(t, pVol.Constraints) { 198 | constraints := *pVol.Constraints 199 | require.Equal(t, 2, len(constraints)) 200 | assert.Equal(t, []string{"cons1", "EQUAL", "tag1"}, constraints[0]) 201 | assert.Equal(t, []string{"cons2", "UNIQUE"}, constraints[1]) 202 | } 203 | 204 | pVol.EmptyConstraints() 205 | if assert.NotNil(t, pVol.Constraints) { 206 | assert.Empty(t, len(*pVol.Constraints)) 207 | } 208 | } 209 | 210 | func TestDockerPullConfig(t *testing.T) { 211 | secretName := "mysecret1" 212 | app := NewDockerApplication() 213 | pullConfig := NewPullConfig(secretName) 214 | app.Container.Docker.SetPullConfig(pullConfig) 215 | 216 | if assert.NotNil(t, app.Container.Docker.PullConfig) { 217 | assert.Equal(t, secretName, app.Container.Docker.PullConfig.Secret) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /task.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | ) 23 | 24 | // Tasks is a collection of marathon tasks 25 | type Tasks struct { 26 | Tasks []Task `json:"tasks"` 27 | } 28 | 29 | // Task is the definition for a marathon task 30 | type Task struct { 31 | ID string `json:"id"` 32 | AppID string `json:"appId"` 33 | Host string `json:"host"` 34 | HealthCheckResults []*HealthCheckResult `json:"healthCheckResults"` 35 | Ports []int `json:"ports"` 36 | ServicePorts []int `json:"servicePorts"` 37 | SlaveID string `json:"slaveId"` 38 | StagedAt string `json:"stagedAt"` 39 | StartedAt string `json:"startedAt"` 40 | State string `json:"state"` 41 | IPAddresses []*IPAddress `json:"ipAddresses"` 42 | Version string `json:"version"` 43 | } 44 | 45 | // IPAddress represents a task's IP address and protocol. 46 | type IPAddress struct { 47 | IPAddress string `json:"ipAddress"` 48 | Protocol string `json:"protocol"` 49 | } 50 | 51 | // AllTasksOpts contains a payload for AllTasks method 52 | // status: Return only those tasks whose status matches this parameter. 53 | // If not specified, all tasks are returned. Possible values: running, staging. Default: none. 54 | type AllTasksOpts struct { 55 | Status string `url:"status,omitempty"` 56 | } 57 | 58 | // KillApplicationTasksOpts contains a payload for KillApplicationTasks method 59 | // host: kill only those tasks on a specific host (optional) 60 | // scale: Scale the app down (i.e. decrement its instances setting by the number of tasks killed) after killing the specified tasks 61 | type KillApplicationTasksOpts struct { 62 | Host string `url:"host,omitempty"` 63 | Scale bool `url:"scale,omitempty"` 64 | Force bool `url:"force,omitempty"` 65 | } 66 | 67 | // KillTaskOpts contains a payload for task killing methods 68 | // scale: Scale the app down 69 | type KillTaskOpts struct { 70 | Scale bool `url:"scale,omitempty"` 71 | Force bool `url:"force,omitempty"` 72 | Wipe bool `url:"wipe,omitempty"` 73 | } 74 | 75 | // HasHealthCheckResults checks if the task has any health checks 76 | func (r *Task) HasHealthCheckResults() bool { 77 | return r.HealthCheckResults != nil && len(r.HealthCheckResults) > 0 78 | } 79 | 80 | // AllTasks lists tasks of all applications. 81 | // opts: AllTasksOpts request payload 82 | func (r *marathonClient) AllTasks(opts *AllTasksOpts) (*Tasks, error) { 83 | path, err := addOptions(marathonAPITasks, opts) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | tasks := new(Tasks) 89 | if err := r.apiGet(path, nil, tasks); err != nil { 90 | return nil, err 91 | } 92 | 93 | return tasks, nil 94 | } 95 | 96 | // Tasks retrieves a list of tasks for an application 97 | // id: the id of the application 98 | func (r *marathonClient) Tasks(id string) (*Tasks, error) { 99 | tasks := new(Tasks) 100 | if err := r.apiGet(fmt.Sprintf("%s/%s/tasks", marathonAPIApps, trimRootPath(id)), nil, tasks); err != nil { 101 | return nil, err 102 | } 103 | 104 | return tasks, nil 105 | } 106 | 107 | // KillApplicationTasks kills all tasks relating to an application 108 | // id: the id of the application 109 | // opts: KillApplicationTasksOpts request payload 110 | func (r *marathonClient) KillApplicationTasks(id string, opts *KillApplicationTasksOpts) (*Tasks, error) { 111 | path := fmt.Sprintf("%s/%s/tasks", marathonAPIApps, trimRootPath(id)) 112 | path, err := addOptions(path, opts) 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | tasks := new(Tasks) 118 | if err := r.apiDelete(path, nil, tasks); err != nil { 119 | return nil, err 120 | } 121 | 122 | return tasks, nil 123 | } 124 | 125 | // KillTask kills the task associated with a given ID 126 | // taskID: the id for the task 127 | // opts: KillTaskOpts request payload 128 | func (r *marathonClient) KillTask(taskID string, opts *KillTaskOpts) (*Task, error) { 129 | appName := taskID[0:strings.LastIndex(taskID, ".")] 130 | appName = strings.Replace(appName, "_", "/", -1) 131 | taskID = strings.Replace(taskID, "/", "_", -1) 132 | 133 | path := fmt.Sprintf("%s/%s/tasks/%s", marathonAPIApps, appName, taskID) 134 | path, err := addOptions(path, opts) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | wrappedTask := new(struct { 140 | Task Task `json:"task"` 141 | }) 142 | 143 | if err := r.apiDelete(path, nil, wrappedTask); err != nil { 144 | return nil, err 145 | } 146 | 147 | return &wrappedTask.Task, nil 148 | } 149 | 150 | // KillTasks kills tasks associated with given array of ids 151 | // tasks: the array of task ids 152 | // opts: KillTaskOpts request payload 153 | func (r *marathonClient) KillTasks(tasks []string, opts *KillTaskOpts) error { 154 | path := fmt.Sprintf("%s/delete", marathonAPITasks) 155 | path, err := addOptions(path, opts) 156 | if err != nil { 157 | return nil 158 | } 159 | 160 | var post struct { 161 | IDs []string `json:"ids"` 162 | } 163 | post.IDs = tasks 164 | 165 | return r.ApiPost(path, &post, nil) 166 | } 167 | 168 | // TaskEndpoints gets the endpoints i.e. HOST_IP:DYNAMIC_PORT for a specific application service 169 | // I.e. a container running apache, might have ports 80/443 (translated to X dynamic ports), but i want 170 | // port 80 only and i only want those whom have passed the health check 171 | // 172 | // Note: I've NO IDEA how to associate the health_check_result to the actual port, I don't think it's 173 | // possible at the moment, however, given marathon will fail and restart an application even if one of x ports of a task is 174 | // down, the per port check is redundant??? .. personally, I like it anyhow, but hey 175 | // 176 | 177 | // name: the identifier for the application 178 | // port: the container port you are interested in 179 | // health: whether to check the health or not 180 | func (r *marathonClient) TaskEndpoints(name string, port int, healthCheck bool) ([]string, error) { 181 | // step: get the application details 182 | application, err := r.Application(name) 183 | if err != nil { 184 | return nil, err 185 | } 186 | 187 | // step: we need to get the port index of the service we are interested in 188 | portIndex, err := application.Container.Docker.ServicePortIndex(port) 189 | if err != nil { 190 | portIndex, err = application.Container.ServicePortIndex(port) 191 | if err != nil { 192 | return nil, err 193 | } 194 | } 195 | 196 | // step: do we have any tasks? 197 | if application.Tasks == nil || len(application.Tasks) == 0 { 198 | return nil, nil 199 | } 200 | 201 | // step: if we are checking health the 'service' has a health check? 202 | healthCheck = healthCheck && application.HasHealthChecks() 203 | 204 | // step: iterate the tasks and extract the dynamic ports 205 | var list []string 206 | for _, task := range application.Tasks { 207 | if !healthCheck || task.allHealthChecksAlive() { 208 | endpoint := fmt.Sprintf("%s:%d", task.Host, task.Ports[portIndex]) 209 | list = append(list, endpoint) 210 | } 211 | } 212 | 213 | return list, nil 214 | } 215 | 216 | func (r *Task) allHealthChecksAlive() bool { 217 | // check: does the task have a health check result, if NOT, it's because the 218 | // health of the task hasn't yet been performed, hence we assume it as DOWN 219 | if !r.HasHealthCheckResults() { 220 | return false 221 | } 222 | // step: check the health results then 223 | for _, check := range r.HealthCheckResults { 224 | if !check.Alive { 225 | return false 226 | } 227 | } 228 | 229 | return true 230 | } 231 | -------------------------------------------------------------------------------- /group.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | import ( 20 | "fmt" 21 | "time" 22 | ) 23 | 24 | // Group is a marathon application group 25 | type Group struct { 26 | ID string `json:"id"` 27 | Apps []*Application `json:"apps"` 28 | Dependencies []string `json:"dependencies"` 29 | Groups []*Group `json:"groups"` 30 | } 31 | 32 | // Groups is a collection of marathon application groups 33 | type Groups struct { 34 | ID string `json:"id"` 35 | Apps []*Application `json:"apps"` 36 | Dependencies []string `json:"dependencies"` 37 | Groups []*Group `json:"groups"` 38 | } 39 | 40 | // GetGroupOpts contains a payload for Group and Groups method 41 | // embed: Embeds nested resources that match the supplied path. 42 | // You can specify this parameter multiple times with different values 43 | type GetGroupOpts struct { 44 | Embed []string `url:"embed,omitempty"` 45 | } 46 | 47 | // DeleteGroupOpts contains a payload for DeleteGroup method 48 | // force: overrides a currently running deployment. 49 | type DeleteGroupOpts struct { 50 | Force bool `url:"force,omitempty"` 51 | } 52 | 53 | // UpdateGroupOpts contains a payload for UpdateGroup method 54 | // force: overrides a currently running deployment. 55 | type UpdateGroupOpts struct { 56 | Force bool `url:"force,omitempty"` 57 | } 58 | 59 | // NewApplicationGroup create a new application group 60 | // name: the name of the group 61 | func NewApplicationGroup(name string) *Group { 62 | return &Group{ 63 | ID: name, 64 | Apps: make([]*Application, 0), 65 | Dependencies: make([]string, 0), 66 | Groups: make([]*Group, 0), 67 | } 68 | } 69 | 70 | // Name sets the name of the group 71 | // name: the name of the group 72 | func (r *Group) Name(name string) *Group { 73 | r.ID = validateID(name) 74 | return r 75 | } 76 | 77 | // App add a application to the group in question 78 | // application: a pointer to the Application 79 | func (r *Group) App(application *Application) *Group { 80 | if r.Apps == nil { 81 | r.Apps = make([]*Application, 0) 82 | } 83 | r.Apps = append(r.Apps, application) 84 | return r 85 | } 86 | 87 | // Groups retrieves a list of all the groups from marathon 88 | func (r *marathonClient) Groups() (*Groups, error) { 89 | groups := new(Groups) 90 | if err := r.apiGet(marathonAPIGroups, "", groups); err != nil { 91 | return nil, err 92 | } 93 | return groups, nil 94 | } 95 | 96 | // Group retrieves the configuration of a specific group from marathon 97 | // name: the identifier for the group 98 | func (r *marathonClient) Group(name string) (*Group, error) { 99 | group := new(Group) 100 | if err := r.apiGet(fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)), nil, group); err != nil { 101 | return nil, err 102 | } 103 | return group, nil 104 | } 105 | 106 | // GroupsBy retrieves a list of all the groups from marathon by embed options 107 | // opts: GetGroupOpts request payload 108 | func (r *marathonClient) GroupsBy(opts *GetGroupOpts) (*Groups, error) { 109 | path, err := addOptions(marathonAPIGroups, opts) 110 | if err != nil { 111 | return nil, err 112 | } 113 | groups := new(Groups) 114 | if err := r.apiGet(path, "", groups); err != nil { 115 | return nil, err 116 | } 117 | return groups, nil 118 | } 119 | 120 | // GroupBy retrieves the configuration of a specific group from marathon 121 | // name: the identifier for the group 122 | // opts: GetGroupOpts request payload 123 | func (r *marathonClient) GroupBy(name string, opts *GetGroupOpts) (*Group, error) { 124 | path, err := addOptions(fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)), opts) 125 | if err != nil { 126 | return nil, err 127 | } 128 | group := new(Group) 129 | if err := r.apiGet(path, nil, group); err != nil { 130 | return nil, err 131 | } 132 | return group, nil 133 | } 134 | 135 | // HasGroup checks if the group exists in marathon 136 | // name: the identifier for the group 137 | func (r *marathonClient) HasGroup(name string) (bool, error) { 138 | path := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)) 139 | err := r.apiGet(path, "", nil) 140 | if err != nil { 141 | if apiErr, ok := err.(*APIError); ok && apiErr.ErrCode == ErrCodeNotFound { 142 | return false, nil 143 | } 144 | return false, err 145 | } 146 | return true, nil 147 | } 148 | 149 | // CreateGroup creates a new group in marathon 150 | // group: a pointer the Group structure defining the group 151 | func (r *marathonClient) CreateGroup(group *Group) error { 152 | return r.ApiPost(marathonAPIGroups, group, nil) 153 | } 154 | 155 | // WaitOnGroup waits for all the applications in a group to be deployed 156 | // group: the identifier for the group 157 | // timeout: a duration of time to wait before considering it failed (all tasks in all apps running defined as deployed) 158 | func (r *marathonClient) WaitOnGroup(name string, timeout time.Duration) error { 159 | err := deadline(timeout, func(stop_channel chan bool) error { 160 | var flick atomicSwitch 161 | go func() { 162 | <-stop_channel 163 | close(stop_channel) 164 | flick.SwitchOn() 165 | }() 166 | for !flick.IsSwitched() { 167 | if group, err := r.Group(name); err != nil { 168 | continue 169 | } else { 170 | allRunning := true 171 | // for each of the application, check if the tasks and running 172 | for _, appID := range group.Apps { 173 | // Arrrgghhh!! .. so we can't use application instances from the Application struct like with app wait on as it 174 | // appears the instance count is not set straight away!! .. it defaults to zero and changes probably at the 175 | // dependencies gets deployed. Which is probably how it internally handles dependencies .. 176 | // step: grab the application 177 | application, err := r.Application(appID.ID) 178 | if err != nil { 179 | allRunning = false 180 | break 181 | } 182 | 183 | if application.Tasks == nil { 184 | allRunning = false 185 | } else if len(application.Tasks) != *appID.Instances { 186 | allRunning = false 187 | } else if application.TasksRunning != *appID.Instances { 188 | allRunning = false 189 | } else if len(application.DeploymentIDs()) > 0 { 190 | allRunning = false 191 | } 192 | } 193 | // has anyone toggle the flag? 194 | if allRunning { 195 | return nil 196 | } 197 | } 198 | time.Sleep(r.config.PollingWaitTime) 199 | } 200 | return nil 201 | }) 202 | 203 | return err 204 | } 205 | 206 | // DeleteGroup deletes a group from marathon 207 | // name: the identifier for the group 208 | // force: used to force the delete operation in case of blocked deployment 209 | func (r *marathonClient) DeleteGroup(name string, force bool) (*DeploymentID, error) { 210 | version := new(DeploymentID) 211 | path := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)) 212 | if force { 213 | path += "?force=true" 214 | } 215 | if err := r.apiDelete(path, nil, version); err != nil { 216 | return nil, err 217 | } 218 | 219 | return version, nil 220 | } 221 | 222 | // UpdateGroup updates the parameters of a groups 223 | // name: the identifier for the group 224 | // group: the group structure with the new params 225 | // force: used to force the update operation in case of blocked deployment 226 | func (r *marathonClient) UpdateGroup(name string, group *Group, force bool) (*DeploymentID, error) { 227 | deploymentID := new(DeploymentID) 228 | path := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)) 229 | if force { 230 | path += "?force=true" 231 | } 232 | if err := r.apiPut(path, group, deploymentID); err != nil { 233 | return nil, err 234 | } 235 | 236 | return deploymentID, nil 237 | } 238 | -------------------------------------------------------------------------------- /health.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The go-marathon Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package marathon 18 | 19 | // HealthCheck is the definition for an application health check 20 | type HealthCheck struct { 21 | Command *Command `json:"command,omitempty"` 22 | PortIndex *int `json:"portIndex,omitempty"` 23 | Port *int `json:"port,omitempty"` 24 | Path *string `json:"path,omitempty"` 25 | MaxConsecutiveFailures *int `json:"maxConsecutiveFailures,omitempty"` 26 | Protocol string `json:"protocol,omitempty"` 27 | GracePeriodSeconds int `json:"gracePeriodSeconds,omitempty"` 28 | IntervalSeconds int `json:"intervalSeconds,omitempty"` 29 | TimeoutSeconds int `json:"timeoutSeconds,omitempty"` 30 | IgnoreHTTP1xx *bool `json:"ignoreHttp1xx,omitempty"` 31 | } 32 | 33 | // HTTPHealthCheck describes an HTTP based health check 34 | type HTTPHealthCheck struct { 35 | Endpoint string `json:"endpoint,omitempty"` 36 | Path string `json:"path,omitempty"` 37 | Scheme string `json:"scheme,omitempty"` 38 | } 39 | 40 | // TCPHealthCheck describes a TCP based health check 41 | type TCPHealthCheck struct { 42 | Endpoint string `json:"endpoint,omitempty"` 43 | } 44 | 45 | // CommandHealthCheck describes a shell-based health check 46 | type CommandHealthCheck struct { 47 | Command PodCommand `json:"command,omitempty"` 48 | } 49 | 50 | // PodHealthCheck describes how to determine a pod's health 51 | type PodHealthCheck struct { 52 | HTTP *HTTPHealthCheck `json:"http,omitempty"` 53 | TCP *TCPHealthCheck `json:"tcp,omitempty"` 54 | Exec *CommandHealthCheck `json:"exec,omitempty"` 55 | GracePeriodSeconds *int `json:"gracePeriodSeconds,omitempty"` 56 | IntervalSeconds *int `json:"intervalSeconds,omitempty"` 57 | MaxConsecutiveFailures *int `json:"maxConsecutiveFailures,omitempty"` 58 | TimeoutSeconds *int `json:"timeoutSeconds,omitempty"` 59 | DelaySeconds *int `json:"delaySeconds,omitempty"` 60 | } 61 | 62 | // NewPodHealthCheck creates an empty PodHealthCheck 63 | func NewPodHealthCheck() *PodHealthCheck { 64 | return &PodHealthCheck{} 65 | } 66 | 67 | // NewHTTPHealthCheck creates an empty HTTPHealthCheck 68 | func NewHTTPHealthCheck() *HTTPHealthCheck { 69 | return &HTTPHealthCheck{} 70 | } 71 | 72 | // NewTCPHealthCheck creates an empty TCPHealthCheck 73 | func NewTCPHealthCheck() *TCPHealthCheck { 74 | return &TCPHealthCheck{} 75 | } 76 | 77 | // NewCommandHealthCheck creates an empty CommandHealthCheck 78 | func NewCommandHealthCheck() *CommandHealthCheck { 79 | return &CommandHealthCheck{} 80 | } 81 | 82 | // SetCommand sets the given command on the health check. 83 | func (h *HealthCheck) SetCommand(c Command) *HealthCheck { 84 | h.Command = &c 85 | return h 86 | } 87 | 88 | // SetPortIndex sets the given port index on the health check. 89 | func (h *HealthCheck) SetPortIndex(i int) *HealthCheck { 90 | h.PortIndex = &i 91 | return h 92 | } 93 | 94 | // SetPort sets the given port on the health check. 95 | func (h *HealthCheck) SetPort(i int) *HealthCheck { 96 | h.Port = &i 97 | return h 98 | } 99 | 100 | // SetPath sets the given path on the health check. 101 | func (h *HealthCheck) SetPath(p string) *HealthCheck { 102 | h.Path = &p 103 | return h 104 | } 105 | 106 | // SetMaxConsecutiveFailures sets the maximum consecutive failures on the health check. 107 | func (h *HealthCheck) SetMaxConsecutiveFailures(i int) *HealthCheck { 108 | h.MaxConsecutiveFailures = &i 109 | return h 110 | } 111 | 112 | // SetIgnoreHTTP1xx sets ignore http 1xx on the health check. 113 | func (h *HealthCheck) SetIgnoreHTTP1xx(ignore bool) *HealthCheck { 114 | h.IgnoreHTTP1xx = &ignore 115 | return h 116 | } 117 | 118 | // NewDefaultHealthCheck creates a default application health check 119 | func NewDefaultHealthCheck() *HealthCheck { 120 | portIndex := 0 121 | path := "" 122 | maxConsecutiveFailures := 3 123 | 124 | return &HealthCheck{ 125 | Protocol: "HTTP", 126 | Path: &path, 127 | PortIndex: &portIndex, 128 | MaxConsecutiveFailures: &maxConsecutiveFailures, 129 | GracePeriodSeconds: 30, 130 | IntervalSeconds: 10, 131 | TimeoutSeconds: 5, 132 | } 133 | } 134 | 135 | // HealthCheckResult is the health check result 136 | type HealthCheckResult struct { 137 | Alive bool `json:"alive"` 138 | ConsecutiveFailures int `json:"consecutiveFailures"` 139 | FirstSuccess string `json:"firstSuccess"` 140 | LastFailure string `json:"lastFailure"` 141 | LastFailureCause string `json:"lastFailureCause"` 142 | LastSuccess string `json:"lastSuccess"` 143 | TaskID string `json:"taskId"` 144 | } 145 | 146 | // Command is the command health check type 147 | type Command struct { 148 | Value string `json:"value"` 149 | } 150 | 151 | // SetHTTPHealthCheck configures the pod's health check for an HTTP endpoint. 152 | // Note this will erase any configured TCP/Exec health checks. 153 | func (p *PodHealthCheck) SetHTTPHealthCheck(h *HTTPHealthCheck) *PodHealthCheck { 154 | p.HTTP = h 155 | p.TCP = nil 156 | p.Exec = nil 157 | return p 158 | } 159 | 160 | // SetTCPHealthCheck configures the pod's health check for a TCP endpoint. 161 | // Note this will erase any configured HTTP/Exec health checks. 162 | func (p *PodHealthCheck) SetTCPHealthCheck(t *TCPHealthCheck) *PodHealthCheck { 163 | p.TCP = t 164 | p.HTTP = nil 165 | p.Exec = nil 166 | return p 167 | } 168 | 169 | // SetExecHealthCheck configures the pod's health check for a command. 170 | // Note this will erase any configured HTTP/TCP health checks. 171 | func (p *PodHealthCheck) SetExecHealthCheck(e *CommandHealthCheck) *PodHealthCheck { 172 | p.Exec = e 173 | p.HTTP = nil 174 | p.TCP = nil 175 | return p 176 | } 177 | 178 | // SetGracePeriod sets the health check initial grace period, in seconds 179 | func (p *PodHealthCheck) SetGracePeriod(gracePeriodSeconds int) *PodHealthCheck { 180 | p.GracePeriodSeconds = &gracePeriodSeconds 181 | return p 182 | } 183 | 184 | // SetInterval sets the health check polling interval, in seconds 185 | func (p *PodHealthCheck) SetInterval(intervalSeconds int) *PodHealthCheck { 186 | p.IntervalSeconds = &intervalSeconds 187 | return p 188 | } 189 | 190 | // SetMaxConsecutiveFailures sets the maximum consecutive failures on the health check 191 | func (p *PodHealthCheck) SetMaxConsecutiveFailures(maxFailures int) *PodHealthCheck { 192 | p.MaxConsecutiveFailures = &maxFailures 193 | return p 194 | } 195 | 196 | // SetTimeout sets the length of time the health check will await a result, in seconds 197 | func (p *PodHealthCheck) SetTimeout(timeoutSeconds int) *PodHealthCheck { 198 | p.TimeoutSeconds = &timeoutSeconds 199 | return p 200 | } 201 | 202 | // SetDelay sets the length of time a pod will delay running health checks on initial launch, in seconds 203 | func (p *PodHealthCheck) SetDelay(delaySeconds int) *PodHealthCheck { 204 | p.DelaySeconds = &delaySeconds 205 | return p 206 | } 207 | 208 | // SetEndpoint sets the name of the pod health check endpoint 209 | func (h *HTTPHealthCheck) SetEndpoint(endpoint string) *HTTPHealthCheck { 210 | h.Endpoint = endpoint 211 | return h 212 | } 213 | 214 | // SetPath sets the HTTP path of the pod health check endpoint 215 | func (h *HTTPHealthCheck) SetPath(path string) *HTTPHealthCheck { 216 | h.Path = path 217 | return h 218 | } 219 | 220 | // SetScheme sets the HTTP scheme of the pod health check endpoint 221 | func (h *HTTPHealthCheck) SetScheme(scheme string) *HTTPHealthCheck { 222 | h.Scheme = scheme 223 | return h 224 | } 225 | 226 | // SetEndpoint sets the name of the pod health check endpoint 227 | func (t *TCPHealthCheck) SetEndpoint(endpoint string) *TCPHealthCheck { 228 | t.Endpoint = endpoint 229 | return t 230 | } 231 | 232 | // SetCommand sets a CommandHealthCheck's underlying PodCommand 233 | func (c *CommandHealthCheck) SetCommand(p PodCommand) *CommandHealthCheck { 234 | c.Command = p 235 | return c 236 | } 237 | --------------------------------------------------------------------------------