Repository: gambol99/go-marathon Branch: master Commit: 94e7bcb625cd Files: 79 Total size: 414.9 KB Directory structure: gitextract_zhlni1rs/ ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── application.go ├── application_marshalling.go ├── application_marshalling_test.go ├── application_test.go ├── client.go ├── client_test.go ├── cluster.go ├── cluster_test.go ├── config.go ├── const.go ├── deployment.go ├── deployment_test.go ├── docker.go ├── docker_test.go ├── error.go ├── error_test.go ├── events.go ├── examples/ │ ├── Makefile │ ├── applications/ │ │ └── main.go │ ├── docker-compose.yml │ ├── events_callback_transport/ │ │ └── main.go │ ├── events_sse_transport/ │ │ └── main.go │ ├── glog/ │ │ └── main.go │ ├── groups/ │ │ └── main.go │ ├── multiple_endpoints/ │ │ └── main.go │ ├── pods/ │ │ └── main.go │ ├── queue/ │ │ └── main.go │ └── tasks/ │ └── main.go ├── group.go ├── group_test.go ├── health.go ├── health_test.go ├── info.go ├── info_test.go ├── last_task_failure.go ├── network.go ├── offer.go ├── pod.go ├── pod_container.go ├── pod_container_image.go ├── pod_container_marshalling.go ├── pod_instance.go ├── pod_instance_status.go ├── pod_instance_test.go ├── pod_marshalling.go ├── pod_marshalling_test.go ├── pod_scheduling.go ├── pod_status.go ├── pod_status_test.go ├── pod_test.go ├── port_definition.go ├── queue.go ├── queue_test.go ├── readiness.go ├── readiness_test.go ├── residency.go ├── residency_test.go ├── resources.go ├── subscription.go ├── subscription_test.go ├── task.go ├── task_test.go ├── testing_utils_test.go ├── tests/ │ ├── app-definitions/ │ │ ├── TestApplicationString-1.5-output.json │ │ └── TestApplicationString-output.json │ └── rest-api/ │ └── methods.yml ├── unreachable_strategy.go ├── unreachable_strategy_test.go ├── upgrade_strategy.go ├── utils.go ├── utils_test.go └── volume.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so go-marathon.iml .idea/ Gemfile.lock thin.* examples/applications/applications examples/events_callback_transport/events_callback_transport examples/events_sse_transport/events_sse_transport examples/glog/glog examples/groups/groups examples/multiple_endpoints/multiple_endpoints examples/pods/pods examples/queue/queue examples/tasks/tasks coverage # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof tests/rest-api/rest-api ================================================ FILE: .travis.yml ================================================ env: global: secure: YiSCbBUz0VMONSBZ6TfRaSM9bFBuT5xvaknt9WxWczPSiSgiY8+dGYlsOaX2jzI26J4zA8KxIyxOihN1UE28tkkGoXRkRovoQuOl9YUYp+VCtZdaeksZ7tJ/j/b6aYGpGN3GRRfxkuIhXw1ghZLgqdCVtqfmD3GODlmeuFE01ug= language: go go: - 1.6 - 1.7 - 1.8 - 1.9 - "1.10" - "1.11" install: - go get github.com/mattn/goveralls script: - make test examples - if ([[ ${TRAVIS_BRANCH} == "master" ]] && [[ ${TRAVIS_EVENT_TYPE} == "push" ]]); then make coverage; goveralls -coverprofile=coverage -service=travis-ci; fi ================================================ FILE: CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added - [#273][PR273] Implement readiness checks. - [#267][PR267] Add DCOS path parameter for additional marathon instances. ## [0.7.1] - 2017-02-20 ### Fixed - [#261][PR261] Fix URL parsing for Go 1.8. ## [0.7.0] - 2017-02-17 ### Added - [#256][PR256] Expose task state. ### Changed - [#259][PR259] Add 'omitempty' to UpgradeStrategy properties. ## [0.6.0] - 2016-12-14 ### Added - [#246][PR246] Add TaskKillGracePeriodSeconds support. - [#244][PR244] Add taskStats support. ### Changed - [#242][PR242] Pointerize IPAddressPerTask.Discovery. ## [0.5.1] - 2016-11-09 ### Fixed - [#239][PR239] Fix scheme-less endpoint with port. ## [0.5.0] - 2016-11-07 ### Fixed - [#231][PR231] Fixed Marathon cluster code - [#229][PR229] Add LastFailureCause field to HealthCheckResult. ## [0.4.0] - 2016-10-28 ### Added - [#223][PR223] Add support for IP-per-task. - [#220][PR220] Add external volume definition to container. - [#211][PR211] Close event channel on event listener removal. ### Fixed - [#218][PR218] Remove TimeWaitPolling from marathonClient. - [#214][PR214] Remove extra pointer layers when passing to r.api*. ## [0.3.0] - 2016-09-28 - [#201][PR201]: Subscribe method is now exposed on the client to allow subscription of callback URL's ### Fixed - [#205][PR205]: Fix memory leak by signalling goroutine termination on event listener removal. ### Changed - [#205][PR205]: Change AddEventsListener to return event channel instead of taking one. ## [0.2.0] - 2016-09-23 ### Added - [#196][PR196]: Port definitions. - [#191][PR191]: name and labels to portMappings. ### Changed - [#191][PR191] ExposePort() now takes a portMapping instance. ### Fixed - [#202][PR202]: Timeout error in WaitOnApplication. ## [0.1.1] - 2016-09-07 ### Fixed - Drop question mark-only query parameter in Applications(url.Values) manually due to changed behavior in Go 1.7's net/url.Parse. ## [0.1.0] - 2016-08-01 ### Added - Field `message` to the EventStatusUpdate struct. - Method `Host()` to set host mode explicitly. - Field `port` to HealthCheck. - Support for launch queues. - Convenience method `AddFetchURIs()`. - Support for forced operations across all methods. - Filtering method variants (`*By`-suffixed). - Support for Marathon DCOS token. - Basic auth and HTTP client settings. - Marshalling of `Deployment.DeploymentStep` for Marathon v1.X. - Field `ipAddresses` to tasks and events. - Field `slaveId` to tasks. - Convenience methods to populate/clear pointerized values. - Method `ApplicationByVersion()` to retrieve version-specific apps. - Support for fetch URIs. - Parse API error responses on all error types for programmatic evaluation. ### Changed - Consider app as unhealthy in ApplicationOK if health check is missing. (Ensures result stability during all phases of deployment.) - Various identifiers violating golint rules. - Do not set "bridged" mode on Docker containers by default. ### Fixed - Flawed unmarshalling of `CurrentStep` in events. - Missing omitempty tag modifiers on `Application.Uris`. - Missing leading slash in path used by `Ping()`. - Flawed `KillTask()` in case of hierarchical app ID path. - Missing omitempty tag modifier on `PortMapping.Protocol`. - Nil dereference on empty debug log. - Various occasions where omitted and empty fields could not be distinguished. ## 0.0.1 - 2016-01-27 ### Added - Initial SemVer release. [Unreleased]: https://github.com/gambol99/go-marathon/compare/v0.7.1...HEAD [0.7.1]: https://github.com/gambol99/go-marathon/compare/v0.7.0...v0.7.1 [0.7.0]: https://github.com/gambol99/go-marathon/compare/v0.6.0...v0.7.0 [0.6.0]: https://github.com/gambol99/go-marathon/compare/v0.5.1...v0.6.0 [0.5.1]: https://github.com/gambol99/go-marathon/compare/v0.5.0...v0.5.1 [0.5.0]: https://github.com/gambol99/go-marathon/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/gambol99/go-marathon/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/gambol99/go-marathon/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/gambol99/go-marathon/compare/v0.1.1...v0.2.0 [0.1.1]: https://github.com/gambol99/go-marathon/compare/v0.1.0...v0.1.1 [0.1.0]: https://github.com/gambol99/go-marathon/compare/v0.0.1...v0.1.0 [PR273]: https://github.com/gambol99/go-marathon/pull/273 [PR267]: https://github.com/gambol99/go-marathon/pull/267 [PR261]: https://github.com/gambol99/go-marathon/pull/261 [PR259]: https://github.com/gambol99/go-marathon/pull/259 [PR256]: https://github.com/gambol99/go-marathon/pull/256 [PR246]: https://github.com/gambol99/go-marathon/pull/246 [PR244]: https://github.com/gambol99/go-marathon/pull/244 [PR242]: https://github.com/gambol99/go-marathon/pull/242 [PR239]: https://github.com/gambol99/go-marathon/pull/239 [PR231]: https://github.com/gambol99/go-marathon/pull/231 [PR229]: https://github.com/gambol99/go-marathon/pull/229 [PR223]: https://github.com/gambol99/go-marathon/pull/223 [PR220]: https://github.com/gambol99/go-marathon/pull/220 [PR218]: https://github.com/gambol99/go-marathon/pull/218 [PR214]: https://github.com/gambol99/go-marathon/pull/214 [PR211]: https://github.com/gambol99/go-marathon/pull/211 [PR205]: https://github.com/gambol99/go-marathon/pull/205 [PR202]: https://github.com/gambol99/go-marathon/pull/202 [PR201]: https://github.com/gambol99/go-marathon/pull/201 [PR196]: https://github.com/gambol99/go-marathon/pull/196 [PR191]: https://github.com/gambol99/go-marathon/pull/191 ================================================ FILE: CONTRIBUTING.md ================================================ # Contribution Guidelines ## Pre-Development - Look for an existing Github issue describing the bug you have found/feature request you would like to see getting implemented. - 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. ## Development - Fork the repository. - Create a feature branch (`git checkout -b my-new-feature master`). - Commit your changes, preferring one commit per logical unit of work. Often times, this simply means having a single commit. - If applicable, update the documentation in the [README file](README.md). - In the vast majority of cases, you should add/amend a (regression) test for your bug fix/feature. - Push your branch (`git push origin my-new-feature`). - Create a new pull request. - 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. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ # # Author: Rohith (gambol99@gmail.com) # Date: 2015-02-10 15:35:14 +0000 (Tue, 10 Feb 2015) # # vim:ts=2:sw=2:et # HARDWARE=$(shell uname -m) VERSION=$(shell awk '/const Version/ { print $$4 }' version.go | sed 's/"//g') DEPS=$(shell go list -f '{{range .TestImports}}{{.}} {{end}}' ./...) PACKAGES=$(shell go list ./...) VETARGS?=-asmdecl -atomic -bool -buildtags -copylocks -methods -nilfunc -printf -rangeloops -shift -structtags -unsafeptr .PHONY: test examples changelog check-format coverage cover build: go build deps: @echo "--> Installing build dependencies" @go get -d -v ./... $(DEPS) lint: @echo "--> Running golint" @which golint 2>/dev/null ; if [ $$? -eq 1 ]; then \ go get -u github.com/golang/lint/golint; \ fi @golint . vet: @echo "--> Running go tool vet $(VETARGS) ." @go tool vet 2>/dev/null ; if [ $$? -eq 3 ]; then \ go get golang.org/x/tools/cmd/vet; \ fi @go tool vet $(VETARGS) . cover: @echo "--> Running go test --cover" @go test --cover coverage: @echo "--> Running go coverage" @go test -covermode=count -coverprofile=coverage format: @echo "--> Running go fmt" @go fmt $(PACKAGES) check-format: @echo "--> Checking format" @if gofmt -l . 2>&1 | grep -q '.go'; then \ echo "found unformatted files:"; \ echo; \ gofmt -l .; \ exit 1; \ fi test: deps vet @echo "--> Running go tests" @go test -race -v @$(MAKE) cover examples: make -C examples all changelog: release git log $(shell git tag | tail -n1)..HEAD --no-merges --format=%B > changelog ================================================ FILE: README.md ================================================ [![Build Status](https://travis-ci.org/gambol99/go-marathon.svg?branch=master)](https://travis-ci.org/gambol99/go-marathon) [![GoDoc](http://godoc.org/github.com/gambol99/go-marathon?status.png)](http://godoc.org/github.com/gambol99/go-marathon) [![Go Report Card](https://goreportcard.com/badge/github.com/katallaxie/go-marathon)](https://goreportcard.com/report/github.com/katallaxie/go-marathon) [![Coverage Status](https://coveralls.io/repos/github/gambol99/go-marathon/badge.svg?branch=master)](https://coveralls.io/github/gambol99/go-marathon?branch=master) # Go-Marathon Go-marathon is a API library for working with [Marathon](https://mesosphere.github.io/marathon/). It currently supports - Application and group deployment - Helper filters for pulling the status, configuration and tasks - Multiple Endpoint support for HA deployments - Marathon Event Subscriptions and Event Streams - Pods Note: the library is still under active development; users should expect frequent (possibly breaking) API changes for the time being. It requires Go version 1.6 or higher. ## Code Examples There is also an examples directory in the source which shows hints and snippets of code of how to use it — which is probably the best place to start. You can use `examples/docker-compose.yml` in order to start a test cluster. ### Creating a client ```go import ( marathon "github.com/gambol99/go-marathon" ) marathonURL := "http://10.241.1.71:8080" config := marathon.NewDefaultConfig() config.URL = marathonURL client, err := marathon.NewClient(config) if err != nil { log.Fatalf("Failed to create a client for marathon, error: %s", err) } applications, err := client.Applications(nil) ... ``` Note, you can also specify multiple endpoint for Marathon (i.e. you have setup Marathon in HA mode and having multiple running) ```go marathonURL := "http://10.241.1.71:8080,10.241.1.72:8080,10.241.1.73:8080" ``` The first one specified will be used, if that goes offline the member is marked as *"unavailable"* and a background process will continue to ping the member until it's back online. You can also pass a custom path to the URL, which is especially needed in case of DCOS: ```go marathonURL := "http://10.241.1.71:8080/cluster,10.241.1.72:8080/cluster,10.241.1.73:8080/cluster" ``` If you specify a `DCOSToken` in the configuration file but do not pass a custom URL path, `/marathon` will be used. ### Customizing the HTTP Clients HTTP clients with reasonable timeouts are used by default. It is possible to pass custom clients to the configuration though if the behavior should be customized (e.g., to bypass TLS verification, load root CAs, or change timeouts). Two clients can be given independently of each other: - `HTTPClient` used only for (non-SSE) HTTP API requests. By default, an http.Client with 10 seconds timeout for the entire request is used. - `HTTPSSEClient` used only for SSE-based subscription requests. Note that `HTTPSSEClient` cannot have a response read timeout set as this breaks SSE communication; trying to do so will lead to an error during the SSE connection setup. By default, an http.Client with 5 seconds timeout for dial and TLS handshake, and 10 seconds timeout for response headers received is used. If no `HTTPSSEClient` is given but an `HTTPClient` is, it will be used for SSE subscriptions as well (thereby overriding the default SSE HTTP client). ```go marathonURL := "http://10.241.1.71:8080" config := marathon.NewDefaultConfig() config.URL = marathonURL config.HTTPClient = &http.Client{ Timeout: (time.Duration(10) * time.Second), Transport: &http.Transport{ Dial: (&net.Dialer{ Timeout: 10 * time.Second, KeepAlive: 10 * time.Second, }).Dial, TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, }, } config.HTTPSSEClient = &http.Client{ // Invalid to set Timeout as it contains timeout for reading a response body Transport: &http.Transport{ Dial: (&net.Dialer{ Timeout: 10 * time.Second, KeepAlive: 10 * time.Second, }).Dial, TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, }, } ``` ### Listing the applications ```go applications, err := client.Applications(nil) if err != nil { log.Fatalf("Failed to list applications: %s", err) } log.Printf("Found %d application(s) running", len(applications.Apps)) for _, application := range applications.Apps { log.Printf("Application: %s", application) appID := application.ID details, err := client.Application(appID) if err != nil { log.Fatalf("Failed to get application %s: %s", appID, err) } if details.Tasks != nil { for _, task := range details.Tasks { log.Printf("application %s has task: %s", appID, task) } } } ``` ### Creating a new application ```go log.Printf("Deploying a new application") application := marathon.NewDockerApplication(). Name(applicationName). CPU(0.1). Memory(64). Storage(0.0). Count(2). AddArgs("/usr/sbin/apache2ctl", "-D", "FOREGROUND"). AddEnv("NAME", "frontend_http"). AddEnv("SERVICE_80_NAME", "test_http"). CheckHTTP("/health", 10, 5) application. Container.Docker.Container("quay.io/gambol99/apache-php:latest"). Bridged(). Expose(80). Expose(443) if _, err := client.CreateApplication(application); err != nil { log.Fatalf("Failed to create application: %s, error: %s", application, err) } else { log.Printf("Created the application: %s", application) } ``` Note: Applications may also be defined by means of initializing a `marathon.Application` struct instance directly. However, go-marathon's DSL as shown above provides a more concise way to achieve the same. ### Scaling application Change the number of application instances to 4 ```go log.Printf("Scale to 4 instances") if err := client.ScaleApplicationInstances(application.ID, 4); err != nil { log.Fatalf("Failed to delete the application: %s, error: %s", application, err) } else { client.WaitOnApplication(application.ID, 30 * time.Second) log.Printf("Successfully scaled the application") } ``` ### Pods Pods allow you to deploy groups of tasks as a unit. All tasks in a single instance of a pod share networking and storage. View the [Marathon documentation](https://mesosphere.github.io/marathon/docs/pods.html) for more details on this feature. Examples of their usage can be seen in the `examples/pods` directory, and a smaller snippet is below. ```Go // Initialize a single-container pod running nginx pod := marathon.NewPod() image := marathon.NewDockerPodContainerImage().SetID("nginx") container := marathon.NewPodContainer(). SetName("container", i). CPUs(0.1). Memory(128). SetImage(image) pod.Name("mypod").AddContainer(container) // Create it and wait for it to start up pod, err := client.CreatePod(pod) err = client.WaitOnPod(pod.ID, time.Minute*1) // Scale it pod.Count(5) pod, err = client.UpdatePod(pod, true) // Delete it id, err := client.DeletePod(pod.ID, true) ``` ### Subscription & Events Request to listen to events related to applications — namely status updates, health checks changes and failures. There are two different event transports controlled by `EventsTransport` setting with the following possible values: `EventsTransportSSE` and `EventsTransportCallback` (default value). See [Event Stream](https://mesosphere.github.io/marathon/docs/rest-api.html#event-stream) and [Event Subscriptions](https://mesosphere.github.io/marathon/docs/rest-api.html#event-subscriptions) for details. Event subscriptions can also be individually controlled with the `Subscribe` and `Unsubscribe` functions. See [Controlling subscriptions](#controlling-subscriptions) for more details. #### Event Stream Only available in Marathon >= 0.9.0. Does not require any special configuration or prerequisites. ```go // Configure client config := marathon.NewDefaultConfig() config.URL = marathonURL config.EventsTransport = marathon.EventsTransportSSE client, err := marathon.NewClient(config) if err != nil { log.Fatalf("Failed to create a client for marathon, error: %s", err) } // Register for events events, err = client.AddEventsListener(marathon.EventIDApplications) if err != nil { log.Fatalf("Failed to register for events, %s", err) } timer := time.After(60 * time.Second) done := false // Receive events from channel for 60 seconds for { if done { break } select { case <-timer: log.Printf("Exiting the loop") done = true case event := <-events: log.Printf("Received event: %s", event) } } // Unsubscribe from Marathon events client.RemoveEventsListener(events) ``` #### Event Subscriptions Requires to start a built-in web server accessible by Marathon to connect and push events to. Consider the following additional settings: - `EventsInterface` — the interface we should be listening on for events. Default `"eth0"`. - `EventsPort` — built-in web server port. Default `10001`. - `CallbackURL` — custom callback URL. Default `""`. ```go // Configure client config := marathon.NewDefaultConfig() config.URL = marathonURL config.EventsInterface = marathonInterface config.EventsPort = marathonPort client, err := marathon.NewClient(config) if err != nil { log.Fatalf("Failed to create a client for marathon, error: %s", err) } // Register for events events, err = client.AddEventsListener(marathon.EventIDApplications) if err != nil { log.Fatalf("Failed to register for events, %s", err) } timer := time.After(60 * time.Second) done := false // Receive events from channel for 60 seconds for { if done { break } select { case <-timer: log.Printf("Exiting the loop") done = true case event := <-events: log.Printf("Received event: %s", event) } } // Unsubscribe from Marathon events client.RemoveEventsListener(events) ``` See [events.go](events.go) for a full list of event IDs. #### Controlling subscriptions If you simply want to (de)register event subscribers (i.e. without starting an internal web server) you can use the `Subscribe` and `Unsubscribe` methods. ```go // Configure client config := marathon.NewDefaultConfig() config.URL = marathonURL client, err := marathon.NewClient(config) if err != nil { log.Fatalf("Failed to create a client for marathon, error: %s", err) } // Register an event subscriber via a callback URL callbackURL := "http://10.241.1.71:9494" if err := client.Subscribe(callbackURL); err != nil { log.Fatalf("Unable to register the callbackURL [%s], error: %s", callbackURL, err) } // Deregister the same subscriber if err := client.Unsubscribe(callbackURL); err != nil { log.Fatalf("Unable to deregister the callbackURL [%s], error: %s", callbackURL, err) } ``` ## Contributing See the [contribution guidelines](CONTRIBUTING.md). ## Development ### Marathon Fake go-marathon employs a [fake Marathon implementation](https://github.com/gambol99/go-marathon/blob/master/testing_utils_test.go) for testing purposes. It [maintains a YML-encoded list of HTTP response messages](https://github.com/gambol99/go-marathon/blob/master/tests/rest-api/methods.yml) which are returned upon a successful match based upon a number of attributes, the so-called _message identifier_: - HTTP URI (without the protocol and the hostname, e.g., `/v2/apps`) - HTTP method (e.g., `GET`) - response content (i.e., the message returned) - scope (see below) #### Response Content The response content can be provided in one of two forms: - **static:** A pure response message returned on every match, including repeated queries. - **index:** A list of response messages associated to a particular (indexed) sequence order. A message will be returned _iff_ it matches and its zero-based index equals the current request count. An example for a trivial static response content is ```yaml - uri: /v2/apps method: POST content: | { "app": { } } ``` which would be returned for every POST request targetting `/v2/apps`. An indexed response content would look like: ```yaml - uri: /v2/apps method: POST contentSequence: - index: 1 - content: | { "app": { "id": "foo" } } - index: 3 - content: | { "app": { "id": "bar" } } ``` What this means is that the first POST request to `/v2/apps` would yield a 404, the second one the _foo_ app, the third one 404 again, the fourth one _bar_, and every following request thereafter a 404 again. Indexed responses enable more flexible testing required by some use cases. Trying to define both a static and indexed response content constitutes an error and leads to `panic`. #### Scope By default, all responses are defined globally: Every message can be queried by any request across all tests. This enables reusability and allows to keep the YML definition fairly short. For certain cases, however, it is desirable to define a set of responses that are delivered exclusively for a particular test. Scopes offer a means to do so by representing a concept similar to [namespaces](https://en.wikipedia.org/wiki/Namespace). Combined with indexed responses, they allow to return different responses for message identifiers already defined at the global level. Scopes do not have a particular format -- they are just strings. A scope must be defined in two places: The message specification and the server configuration. They are pure strings without any particular structure. Given the messages specification ```yaml - uri: /v2/apps method: GET # Note: no scope defined. content: | { "app": { "id": "foo" } } - uri: /v2/apps method: GET scope: v1.1.1 # This one does have a scope. contentSequence: - index: 1 - content: | { "app": { "id": "bar" } } ``` and the tests ```go func TestFoo(t * testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) // No custom configs given. defer endpoint.Close() app, err := endpoint.Client.Applications(nil) // Do something with "foo" } func TestFoo(t * testing.T) { endpoint := newFakeMarathonEndpoint(t, &configContainer{ server: &serverConfig{ scope: "v1.1.1", // Matches the message spec's scope. }, }) defer endpoint.Close() app, err := endpoint.Client.Applications(nil) // Do something with "bar" } ``` The "foo" response can be used by all tests using the default fake endpoint (such as `TestFoo`), while the "bar" response is only visible by tests that explicitly set the scope to `1.1.1` (as `TestBar` does) and query the endpoint twice. ================================================ FILE: application.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "encoding/json" "errors" "fmt" "net/url" "time" ) var ( // ErrNoApplicationContainer is thrown when a container has been specified yet ErrNoApplicationContainer = errors.New("you have not specified a docker container yet") ) // Applications is a collection of applications type Applications struct { Apps []Application `json:"apps"` } // IPAddressPerTask is used by IP-per-task functionality https://mesosphere.github.io/marathon/docs/ip-per-task.html type IPAddressPerTask struct { Groups *[]string `json:"groups,omitempty"` Labels *map[string]string `json:"labels,omitempty"` Discovery *Discovery `json:"discovery,omitempty"` NetworkName string `json:"networkName,omitempty"` } // Discovery provides info about ports expose by IP-per-task functionality type Discovery struct { Ports *[]Port `json:"ports,omitempty"` } // Port provides info about ports used by IP-per-task type Port struct { Number int `json:"number,omitempty"` Name string `json:"name,omitempty"` Protocol string `json:"protocol,omitempty"` } // Application is the definition for an application in marathon type Application struct { ID string `json:"id,omitempty"` Cmd *string `json:"cmd,omitempty"` Args *[]string `json:"args,omitempty"` Constraints *[][]string `json:"constraints,omitempty"` Container *Container `json:"container,omitempty"` CPUs float64 `json:"cpus,omitempty"` GPUs *float64 `json:"gpus,omitempty"` Disk *float64 `json:"disk,omitempty"` Networks *[]PodNetwork `json:"networks,omitempty"` // Contains non-secret environment variables. Secrets environment variables are part of the Secrets map. Env *map[string]string `json:"-"` Executor *string `json:"executor,omitempty"` HealthChecks *[]HealthCheck `json:"healthChecks,omitempty"` ReadinessChecks *[]ReadinessCheck `json:"readinessChecks,omitempty"` Instances *int `json:"instances,omitempty"` Mem *float64 `json:"mem,omitempty"` Tasks []*Task `json:"tasks,omitempty"` Ports []int `json:"ports"` PortDefinitions *[]PortDefinition `json:"portDefinitions,omitempty"` RequirePorts *bool `json:"requirePorts,omitempty"` BackoffSeconds *float64 `json:"backoffSeconds,omitempty"` BackoffFactor *float64 `json:"backoffFactor,omitempty"` MaxLaunchDelaySeconds *float64 `json:"maxLaunchDelaySeconds,omitempty"` TaskKillGracePeriodSeconds *float64 `json:"taskKillGracePeriodSeconds,omitempty"` Deployments []map[string]string `json:"deployments,omitempty"` // Available when embedding readiness information through query parameter. ReadinessCheckResults *[]ReadinessCheckResult `json:"readinessCheckResults,omitempty"` Dependencies []string `json:"dependencies"` TasksRunning int `json:"tasksRunning,omitempty"` TasksStaged int `json:"tasksStaged,omitempty"` TasksHealthy int `json:"tasksHealthy,omitempty"` TasksUnhealthy int `json:"tasksUnhealthy,omitempty"` TaskStats map[string]TaskStats `json:"taskStats,omitempty"` User string `json:"user,omitempty"` UpgradeStrategy *UpgradeStrategy `json:"upgradeStrategy,omitempty"` UnreachableStrategy *UnreachableStrategy `json:"unreachableStrategy,omitempty"` KillSelection string `json:"killSelection,omitempty"` Uris *[]string `json:"uris,omitempty"` Version string `json:"version,omitempty"` VersionInfo *VersionInfo `json:"versionInfo,omitempty"` Labels *map[string]string `json:"labels,omitempty"` AcceptedResourceRoles []string `json:"acceptedResourceRoles,omitempty"` LastTaskFailure *LastTaskFailure `json:"lastTaskFailure,omitempty"` Fetch *[]Fetch `json:"fetch,omitempty"` IPAddressPerTask *IPAddressPerTask `json:"ipAddress,omitempty"` Residency *Residency `json:"residency,omitempty"` Secrets *map[string]Secret `json:"-"` Role *string `json:"role,omitempty"` } // ApplicationVersions is a collection of application versions for a specific app in marathon type ApplicationVersions struct { Versions []string `json:"versions"` } // ApplicationVersion is the application version response from marathon type ApplicationVersion struct { Version string `json:"version"` } // VersionInfo is the application versioning details from marathon type VersionInfo struct { LastScalingAt string `json:"lastScalingAt,omitempty"` LastConfigChangeAt string `json:"lastConfigChangeAt,omitempty"` } // Fetch will download URI before task starts type Fetch struct { URI string `json:"uri"` Executable bool `json:"executable"` Extract bool `json:"extract"` Cache bool `json:"cache"` } // GetAppOpts contains a payload for Application method // embed: Embeds nested resources that match the supplied path. // You can specify this parameter multiple times with different values type GetAppOpts struct { Embed []string `url:"embed,omitempty"` } // DeleteAppOpts contains a payload for DeleteApplication method // force: overrides a currently running deployment. type DeleteAppOpts struct { Force bool `url:"force,omitempty"` } // TaskStats is a container for Stats type TaskStats struct { Stats Stats `json:"stats"` } // Stats is a collection of aggregate statistics about an application's tasks type Stats struct { Counts map[string]int `json:"counts"` LifeTime map[string]float64 `json:"lifeTime"` } // Secret is the environment variable and secret store path associated with a secret. // The value for EnvVar is populated from the env field, and Source is populated from // the secrets field of the application json. type Secret struct { EnvVar string Source string } // SetIPAddressPerTask defines that the application will have a IP address defines by a external agent. // This configuration is not allowed to be used with Port or PortDefinitions. Thus, the implementation // clears both. func (r *Application) SetIPAddressPerTask(ipAddressPerTask IPAddressPerTask) *Application { r.Ports = make([]int, 0) r.EmptyPortDefinitions() r.IPAddressPerTask = &ipAddressPerTask return r } // NewDockerApplication creates a default docker application func NewDockerApplication() *Application { application := new(Application) application.Container = NewDockerContainer() return application } // Name sets the name / ID of the application i.e. the identifier for this application func (r *Application) Name(id string) *Application { r.ID = validateID(id) return r } // Command sets the cmd of the application func (r *Application) Command(cmd string) *Application { r.Cmd = &cmd return r } // CPU set the amount of CPU shares per instance which is assigned to the application // cpu: the CPU shared (check Docker docs) per instance func (r *Application) CPU(cpu float64) *Application { r.CPUs = cpu return r } // SetGPUs set the amount of GPU per instance which is assigned to the application // gpu: the GPU (check MESOS docs) per instance func (r *Application) SetGPUs(gpu float64) *Application { r.GPUs = &gpu return r } // EmptyGPUs explicitly empties GPUs -- use this if you need to empty // gpus of an application that already has gpus set (setting port definitions to nil will // keep the current value) func (r *Application) EmptyGPUs() *Application { g := 0.0 r.GPUs = &g return r } // Storage sets the amount of disk space the application is assigned, which for docker // application I don't believe is relevant // disk: the disk space in MB func (r *Application) Storage(disk float64) *Application { r.Disk = &disk return r } // AllTaskRunning checks to see if all the application tasks are running, i.e. the instances is equal // to the number of running tasks func (r *Application) AllTaskRunning() bool { if r.Instances == nil || *r.Instances == 0 { return true } if r.Tasks == nil { return false } if r.TasksRunning == *r.Instances { return true } return false } // DependsOn adds one or more dependencies for this application. Note, if you want to wait for // an application dependency to actually be UP, i.e. not just deployed, you need a health check // on the dependant app. // names: the application id(s) this application depends on func (r *Application) DependsOn(names ...string) *Application { if r.Dependencies == nil { r.Dependencies = make([]string, 0) } r.Dependencies = append(r.Dependencies, names...) return r } // Memory sets he amount of memory the application can consume per instance // memory: the amount of MB to assign func (r *Application) Memory(memory float64) *Application { r.Mem = &memory return r } // AddPortDefinition adds a port definition. Port definitions are used to define ports that // should be considered part of a resource. They are necessary when you are using HOST // networking and no port mappings are specified. func (r *Application) AddPortDefinition(portDefinition PortDefinition) *Application { if r.PortDefinitions == nil { r.EmptyPortDefinitions() } portDefinitions := *r.PortDefinitions portDefinitions = append(portDefinitions, portDefinition) r.PortDefinitions = &portDefinitions return r } // EmptyPortDefinitions explicitly empties port definitions -- use this if you need to empty // port definitions of an application that already has port definitions set (setting port definitions to nil will // keep the current value) func (r *Application) EmptyPortDefinitions() *Application { r.PortDefinitions = &[]PortDefinition{} return r } // Count sets the number of instances of the application to run // count: the number of instances to run func (r *Application) Count(count int) *Application { r.Instances = &count return r } // SetTaskKillGracePeriod sets the number of seconds between escalating from SIGTERM to SIGKILL // when signalling tasks to terminate. Using this grace period, tasks should perform orderly shut down // immediately upon receiving SIGTERM. // seconds: the number of seconds func (r *Application) SetTaskKillGracePeriod(seconds float64) *Application { r.TaskKillGracePeriodSeconds = &seconds return r } // AddArgs adds one or more arguments to the applications // arguments: the argument(s) you are adding func (r *Application) AddArgs(arguments ...string) *Application { if r.Args == nil { r.EmptyArgs() } args := *r.Args args = append(args, arguments...) r.Args = &args return r } // EmptyArgs explicitly empties arguments -- use this if you need to empty // arguments of an application that already has arguments set (setting args to nil will // keep the current value) func (r *Application) EmptyArgs() *Application { r.Args = &[]string{} return r } // AddConstraint adds a new constraint // constraints: the constraint definition, one constraint per array element func (r *Application) AddConstraint(constraints ...string) *Application { if r.Constraints == nil { r.EmptyConstraints() } c := *r.Constraints c = append(c, constraints) r.Constraints = &c return r } // EmptyConstraints explicitly empties constraints -- use this if you need to empty // constraints of an application that already has constraints set (setting constraints to nil will // keep the current value) func (r *Application) EmptyConstraints() *Application { r.Constraints = &[][]string{} return r } // AddLabel adds a label to the application // name: the name of the label // value: value for this label func (r *Application) AddLabel(name, value string) *Application { if r.Labels == nil { r.EmptyLabels() } (*r.Labels)[name] = value return r } // EmptyLabels explicitly empties the labels -- use this if you need to empty // the labels of an application that already has labels set (setting labels to nil will // keep the current value) func (r *Application) EmptyLabels() *Application { r.Labels = &map[string]string{} return r } // AddEnv adds an environment variable to the application // name: the name of the variable // value: go figure, the value associated to the above func (r *Application) AddEnv(name, value string) *Application { if r.Env == nil { r.EmptyEnvs() } (*r.Env)[name] = value return r } // EmptyEnvs explicitly empties the envs -- use this if you need to empty // the environments of an application that already has environments set (setting env to nil will // keep the current value) func (r *Application) EmptyEnvs() *Application { r.Env = &map[string]string{} return r } // AddSecret adds a secret declaration // envVar: the name of the environment variable // name: the name of the secret // source: the source ID of the secret func (r *Application) AddSecret(envVar, name, source string) *Application { if r.Secrets == nil { r.EmptySecrets() } (*r.Secrets)[name] = Secret{EnvVar: envVar, Source: source} return r } // EmptySecrets explicitly empties the secrets -- use this if you need to empty // the secrets of an application that already has secrets set (setting secrets to nil will // keep the current value) func (r *Application) EmptySecrets() *Application { r.Secrets = &map[string]Secret{} return r } // SetExecutor sets the executor func (r *Application) SetExecutor(executor string) *Application { r.Executor = &executor return r } // AddHealthCheck adds a health check // healthCheck the health check that should be added func (r *Application) AddHealthCheck(healthCheck HealthCheck) *Application { if r.HealthChecks == nil { r.EmptyHealthChecks() } healthChecks := *r.HealthChecks healthChecks = append(healthChecks, healthCheck) r.HealthChecks = &healthChecks return r } // EmptyHealthChecks explicitly empties health checks -- use this if you need to empty // health checks of an application that already has health checks set (setting health checks to nil will // keep the current value) func (r *Application) EmptyHealthChecks() *Application { r.HealthChecks = &[]HealthCheck{} return r } // HasHealthChecks is a helper method, used to check if an application has health checks func (r *Application) HasHealthChecks() bool { return r.HealthChecks != nil && len(*r.HealthChecks) > 0 } // AddReadinessCheck adds a readiness check. func (r *Application) AddReadinessCheck(readinessCheck ReadinessCheck) *Application { if r.ReadinessChecks == nil { r.EmptyReadinessChecks() } readinessChecks := *r.ReadinessChecks readinessChecks = append(readinessChecks, readinessCheck) r.ReadinessChecks = &readinessChecks return r } // EmptyReadinessChecks empties the readiness checks. func (r *Application) EmptyReadinessChecks() *Application { r.ReadinessChecks = &[]ReadinessCheck{} return r } // DeploymentIDs retrieves the application deployments IDs func (r *Application) DeploymentIDs() []*DeploymentID { var deployments []*DeploymentID if r.Deployments == nil { return deployments } // step: extract the deployment id from the result for _, deploy := range r.Deployments { if id, found := deploy["id"]; found { deployment := &DeploymentID{ Version: r.Version, DeploymentID: id, } deployments = append(deployments, deployment) } } return deployments } // CheckHTTP adds a HTTP check to an application // port: the port the check should be checking // interval: the interval in seconds the check should be performed func (r *Application) CheckHTTP(path string, port, interval int) (*Application, error) { if r.Container == nil || r.Container.Docker == nil { return nil, ErrNoApplicationContainer } // step: get the port index portIndex, err := r.Container.Docker.ServicePortIndex(port) if err != nil { portIndex, err = r.Container.ServicePortIndex(port) if err != nil { return nil, err } } health := NewDefaultHealthCheck() health.IntervalSeconds = interval *health.Path = path *health.PortIndex = portIndex // step: add to the checks r.AddHealthCheck(*health) return r, nil } // CheckTCP adds a TCP check to an application; note the port mapping must already exist, or an // error will thrown // port: the port the check should, err, check // interval: the interval in seconds the check should be performed func (r *Application) CheckTCP(port, interval int) (*Application, error) { if r.Container == nil || r.Container.Docker == nil { return nil, ErrNoApplicationContainer } // step: get the port index portIndex, err := r.Container.Docker.ServicePortIndex(port) if err != nil { portIndex, err = r.Container.ServicePortIndex(port) if err != nil { return nil, err } } health := NewDefaultHealthCheck() health.Protocol = "TCP" health.IntervalSeconds = interval *health.PortIndex = portIndex // step: add to the checks r.AddHealthCheck(*health) return r, nil } // AddUris adds one or more uris to the applications // arguments: the uri(s) you are adding func (r *Application) AddUris(newUris ...string) *Application { if r.Uris == nil { r.EmptyUris() } uris := *r.Uris uris = append(uris, newUris...) r.Uris = &uris return r } // EmptyUris explicitly empties uris -- use this if you need to empty // uris of an application that already has uris set (setting uris to nil will // keep the current value) func (r *Application) EmptyUris() *Application { r.Uris = &[]string{} return r } // AddFetchURIs adds one or more fetch URIs to the application. // fetchURIs: the fetch URI(s) to add. func (r *Application) AddFetchURIs(fetchURIs ...Fetch) *Application { if r.Fetch == nil { r.EmptyFetchURIs() } fetch := *r.Fetch fetch = append(fetch, fetchURIs...) r.Fetch = &fetch return r } // EmptyFetchURIs explicitly empties fetch URIs -- use this if you need to empty // fetch URIs of an application that already has fetch URIs set. // Setting fetch URIs to nil will keep the current value. func (r *Application) EmptyFetchURIs() *Application { r.Fetch = &[]Fetch{} return r } // SetUpgradeStrategy sets the upgrade strategy. func (r *Application) SetUpgradeStrategy(us UpgradeStrategy) *Application { r.UpgradeStrategy = &us return r } // EmptyUpgradeStrategy explicitly empties the upgrade strategy -- use this if // you need to empty the upgrade strategy of an application that already has // the upgrade strategy set (setting it to nil will keep the current value). func (r *Application) EmptyUpgradeStrategy() *Application { r.UpgradeStrategy = &UpgradeStrategy{} return r } // SetUnreachableStrategy sets the unreachable strategy. func (r *Application) SetUnreachableStrategy(us UnreachableStrategy) *Application { r.UnreachableStrategy = &us return r } // EmptyUnreachableStrategy explicitly empties the unreachable strategy -- use this if // you need to empty the unreachable strategy of an application that already has // the unreachable strategy set (setting it to nil will keep the current value). func (r *Application) EmptyUnreachableStrategy() *Application { r.UnreachableStrategy = &UnreachableStrategy{} return r } // SetResidency sets behavior for resident applications, an application is resident when // it has local persistent volumes set func (r *Application) SetResidency(whenLost TaskLostBehaviorType) *Application { r.Residency = &Residency{ TaskLostBehavior: whenLost, } return r } // EmptyResidency explicitly empties the residency -- use this if // you need to empty the residency of an application that already has // the residency set (setting it to nil will keep the current value). func (r *Application) EmptyResidency() *Application { r.Residency = &Residency{} return r } // String returns the json representation of this application func (r *Application) String() string { s, err := json.MarshalIndent(r, "", " ") if err != nil { return fmt.Sprintf(`{"error": "error decoding type into json: %s"}`, err) } return string(s) } // Applications retrieves an array of all the applications which are running in marathon func (r *marathonClient) Applications(v url.Values) (*Applications, error) { query := v.Encode() if query != "" { query = "?" + query } applications := new(Applications) err := r.apiGet(marathonAPIApps+query, nil, applications) if err != nil { return nil, err } return applications, nil } // ListApplications retrieves an array of the application names currently running in marathon func (r *marathonClient) ListApplications(v url.Values) ([]string, error) { applications, err := r.Applications(v) if err != nil { return nil, err } var list []string for _, application := range applications.Apps { list = append(list, application.ID) } return list, nil } // HasApplicationVersion checks to see if the application version exists in Marathon // name: the id used to identify the application // version: the version (normally a timestamp) your looking for func (r *marathonClient) HasApplicationVersion(name, version string) (bool, error) { id := trimRootPath(name) versions, err := r.ApplicationVersions(id) if err != nil { return false, err } return contains(versions.Versions, version), nil } // ApplicationVersions is a list of versions which has been deployed with marathon for a specific application // name: the id used to identify the application func (r *marathonClient) ApplicationVersions(name string) (*ApplicationVersions, error) { path := fmt.Sprintf("%s/versions", buildPath(name)) versions := new(ApplicationVersions) if err := r.apiGet(path, nil, versions); err != nil { return nil, err } return versions, nil } // SetApplicationVersion changes the version of the application // name: the id used to identify the application // version: the version (normally a timestamp) you wish to change to func (r *marathonClient) SetApplicationVersion(name string, version *ApplicationVersion) (*DeploymentID, error) { path := buildPath(name) deploymentID := new(DeploymentID) if err := r.apiPut(path, version, deploymentID); err != nil { return nil, err } return deploymentID, nil } // Application retrieves the application configuration from marathon // name: the id used to identify the application func (r *marathonClient) Application(name string) (*Application, error) { var wrapper struct { Application *Application `json:"app"` } if err := r.apiGet(buildPath(name), nil, &wrapper); err != nil { return nil, err } return wrapper.Application, nil } // ApplicationBy retrieves the application configuration from marathon // name: the id used to identify the application // opts: GetAppOpts request payload func (r *marathonClient) ApplicationBy(name string, opts *GetAppOpts) (*Application, error) { path, err := addOptions(buildPath(name), opts) if err != nil { return nil, err } var wrapper struct { Application *Application `json:"app"` } if err := r.apiGet(path, nil, &wrapper); err != nil { return nil, err } return wrapper.Application, nil } // ApplicationByVersion retrieves the application configuration from marathon // name: the id used to identify the application // version: the version of the configuration you would like to receive func (r *marathonClient) ApplicationByVersion(name, version string) (*Application, error) { app := new(Application) path := fmt.Sprintf("%s/versions/%s", buildPath(name), version) if err := r.apiGet(path, nil, app); err != nil { return nil, err } return app, nil } // ApplicationOK validates that the application, or more appropriately it's tasks have passed all the health checks. // If no health checks exist, we simply return true // name: the id used to identify the application func (r *marathonClient) ApplicationOK(name string) (bool, error) { // step: get the application application, err := r.Application(name) if err != nil { return false, err } // step: check if all the tasks are running? if !application.AllTaskRunning() { return false, nil } // step: if the application has not health checks, just return true if application.HealthChecks == nil || len(*application.HealthChecks) == 0 { return true, nil } // step: iterate the application checks and look for false for _, task := range application.Tasks { // Health check results may not be available immediately. Assume // non-healthiness if they are missing for any task. if task.HealthCheckResults == nil { return false, nil } for _, check := range task.HealthCheckResults { //When a task is flapping in Marathon, this is sometimes nil if check == nil || !check.Alive { return false, nil } } } return true, nil } // ApplicationDeployments retrieves an array of Deployment IDs for an application // name: the id used to identify the application func (r *marathonClient) ApplicationDeployments(name string) ([]*DeploymentID, error) { application, err := r.Application(name) if err != nil { return nil, err } return application.DeploymentIDs(), nil } // CreateApplication creates a new application in Marathon // application: the structure holding the application configuration func (r *marathonClient) CreateApplication(application *Application) (*Application, error) { result := new(Application) if err := r.ApiPost(marathonAPIApps, application, result); err != nil { return nil, err } return result, nil } // WaitOnApplication waits for an application to be deployed // name: the id of the application // timeout: a duration of time to wait for an application to deploy func (r *marathonClient) WaitOnApplication(name string, timeout time.Duration) error { return r.wait(name, timeout, r.appExistAndRunning) } func (r *marathonClient) appExistAndRunning(name string) bool { app, err := r.Application(name) if apiErr, ok := err.(*APIError); ok && apiErr.ErrCode == ErrCodeNotFound { return false } if err == nil && app.AllTaskRunning() { return true } return false } // DeleteApplication deletes an application from marathon // name: the id used to identify the application // force: used to force the delete operation in case of blocked deployment func (r *marathonClient) DeleteApplication(name string, force bool) (*DeploymentID, error) { path := buildPathWithForceParam(name, force) // step: check of the application already exists deployID := new(DeploymentID) if err := r.apiDelete(path, nil, deployID); err != nil { return nil, err } return deployID, nil } // RestartApplication performs a rolling restart of marathon application // name: the id used to identify the application func (r *marathonClient) RestartApplication(name string, force bool) (*DeploymentID, error) { deployment := new(DeploymentID) var options struct{} path := buildPathWithForceParam(fmt.Sprintf("%s/restart", name), force) if err := r.ApiPost(path, &options, deployment); err != nil { return nil, err } return deployment, nil } // ScaleApplicationInstances changes the number of instance an application is running // name: the id used to identify the application // instances: the number of instances you wish to change to // force: used to force the scale operation in case of blocked deployment func (r *marathonClient) ScaleApplicationInstances(name string, instances int, force bool) (*DeploymentID, error) { changes := new(Application) changes.ID = validateID(name) changes.Instances = &instances path := buildPathWithForceParam(name, force) deployID := new(DeploymentID) if err := r.apiPut(path, changes, deployID); err != nil { return nil, err } return deployID, nil } // UpdateApplication updates an application in Marathon // application: the structure holding the application configuration func (r *marathonClient) UpdateApplication(application *Application, force bool) (*DeploymentID, error) { result := new(DeploymentID) path := buildPathWithForceParam(application.ID, force) if err := r.apiPut(path, application, result); err != nil { return nil, err } return result, nil } func buildPathWithForceParam(rootPath string, force bool) string { path := buildPath(rootPath) if force { path += "?force=true" } return path } func buildPath(path string) string { return fmt.Sprintf("%s/%s", marathonAPIApps, trimRootPath(path)) } // EmptyLabels explicitly empties labels -- use this if you need to empty // labels of an application that already has IP per task with labels defined func (i *IPAddressPerTask) EmptyLabels() *IPAddressPerTask { i.Labels = &map[string]string{} return i } // AddLabel adds a label to an IPAddressPerTask // name: The label name // value: The label value func (i *IPAddressPerTask) AddLabel(name, value string) *IPAddressPerTask { if i.Labels == nil { i.EmptyLabels() } (*i.Labels)[name] = value return i } // EmptyGroups explicitly empties groups -- use this if you need to empty // groups of an application that already has IP per task with groups defined func (i *IPAddressPerTask) EmptyGroups() *IPAddressPerTask { i.Groups = &[]string{} return i } // AddGroup adds a group to an IPAddressPerTask // group: The group name func (i *IPAddressPerTask) AddGroup(group string) *IPAddressPerTask { if i.Groups == nil { i.EmptyGroups() } groups := *i.Groups groups = append(groups, group) i.Groups = &groups return i } // SetDiscovery define the discovery to an IPAddressPerTask // discovery: The discovery struct func (i *IPAddressPerTask) SetDiscovery(discovery Discovery) *IPAddressPerTask { i.Discovery = &discovery return i } // EmptyPorts explicitly empties discovey port -- use this if you need to empty // discovey port of an application that already has IP per task with discovey ports // defined func (d *Discovery) EmptyPorts() *Discovery { d.Ports = &[]Port{} return d } // AddPort adds a port to the discovery info of a IP per task applicable // port: The discovery port func (d *Discovery) AddPort(port Port) *Discovery { if d.Ports == nil { d.EmptyPorts() } ports := *d.Ports ports = append(ports, port) d.Ports = &ports return d } // EmptyNetworks explicitly empties networks func (r *Application) EmptyNetworks() *Application { r.Networks = &[]PodNetwork{} return r } // SetNetwork sets the networking mode func (r *Application) SetNetwork(name string, mode PodNetworkMode) *Application { if r.Networks == nil { r.EmptyNetworks() } network := PodNetwork{Name: name, Mode: mode} networks := *r.Networks networks = append(networks, network) r.Networks = &networks return r } ================================================ FILE: application_marshalling.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "encoding/json" "fmt" ) // Alias aliases the Application struct so that it will be marshaled/unmarshaled automatically type Alias Application // TmpEnvSecret holds the secret values deserialized from the environment variables field type TmpEnvSecret struct { Secret string `json:"secret,omitempty"` } // TmpSecret holds the deserialized secrets field in a Marathon application configuration type TmpSecret struct { Source string `json:"source,omitempty"` } // UnmarshalJSON unmarshals the given Application JSON as expected except for environment variables and secrets. // Environment varialbes are stored in the Env field. Secrets, including the environment variable part, // are stored in the Secrets field. func (app *Application) UnmarshalJSON(b []byte) error { aux := &struct { *Alias Env map[string]interface{} `json:"env"` Secrets map[string]TmpSecret `json:"secrets"` }{ Alias: (*Alias)(app), } if err := json.Unmarshal(b, aux); err != nil { return fmt.Errorf("malformed application definition %v", err) } env := &map[string]string{} secrets := &map[string]Secret{} for envName, genericEnvValue := range aux.Env { switch envValOrSecret := genericEnvValue.(type) { case string: (*env)[envName] = envValOrSecret case map[string]interface{}: for secret, secretStore := range envValOrSecret { if secStore, ok := secretStore.(string); ok && secret == "secret" { (*secrets)[secStore] = Secret{EnvVar: envName} break } return fmt.Errorf("unexpected secret field %v of value type %T", secret, envValOrSecret[secret]) } default: return fmt.Errorf("unexpected environment variable type %T", envValOrSecret) } } app.Env = env for k, v := range aux.Secrets { tmp := (*secrets)[k] tmp.Source = v.Source (*secrets)[k] = tmp } app.Secrets = secrets return nil } // MarshalJSON marshals the given Application as expected except for environment variables and secrets, // which are marshaled from specialized structs. The environment variable piece of the secrets and other // normal environment variables are combined and marshaled to the env field. The secrets and the related // source are marshaled into the secrets field. func (app *Application) MarshalJSON() ([]byte, error) { env := make(map[string]interface{}) secrets := make(map[string]TmpSecret) if app.Env != nil { for k, v := range *app.Env { env[string(k)] = string(v) } } if app.Secrets != nil { for k, v := range *app.Secrets { env[v.EnvVar] = TmpEnvSecret{Secret: k} secrets[k] = TmpSecret{v.Source} } } aux := &struct { *Alias Env map[string]interface{} `json:"env,omitempty"` Secrets map[string]TmpSecret `json:"secrets,omitempty"` }{Alias: (*Alias)(app), Env: env, Secrets: secrets} return json.Marshal(aux) } ================================================ FILE: application_marshalling_test.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "encoding/json" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestEnvironmentVariableUnmarshal(t *testing.T) { defaultConfig := NewDefaultConfig() configs := &configContainer{ client: &defaultConfig, server: &serverConfig{ scope: "environment-variables", }, } endpoint := newFakeMarathonEndpoint(t, configs) defer endpoint.Close() application, err := endpoint.Client.Application(fakeAppName) require.NoError(t, err) env := application.Env secrets := application.Secrets require.NotNil(t, env) assert.Equal(t, "bar", (*env)["FOO"]) assert.Equal(t, "TOP", (*secrets)["secret"].EnvVar) assert.Equal(t, "/path/to/secret", (*secrets)["secret"].Source) } func TestMalformedPayloadUnmarshal(t *testing.T) { var tests = []struct { expected string given []byte description string }{ { expected: "unexpected secret field", given: []byte(`{"env": {"FOO": "bar", "SECRET": {"not_secret": "secret1"}}, "secrets": {"secret1": {"source": "/path/to/secret"}}}`), description: "Field in environment secret not equal to secret.", }, { expected: "unexpected secret field", given: []byte(`{"env": {"FOO": "bar", "SECRET": {"secret": 1}}, "secrets": {"secret1": {"source": "/path/to/secret"}}}`), description: "Invalid value in environment secret.", }, { expected: "unexpected environment variable type", given: []byte(`{"env": {"FOO": 1, "SECRET": {"secret": "secret1"}}, "secrets": {"secret1": {"source": "/path/to/secret"}}}`), description: "Invalid environment variable type.", }, { expected: "malformed application definition", given: []byte(`{"env": "value"}`), description: "Bad application definition.", }, } for _, test := range tests { tmpApp := new(Application) err := json.Unmarshal(test.given, &tmpApp) if assert.Error(t, err, test.description) { assert.True(t, strings.HasPrefix(err.Error(), test.expected), test.description) } } } func TestEnvironmentVariableMarshal(t *testing.T) { testApp := new(Application) targetString := []byte(`{"ports":null,"dependencies":null,"env":{"FOO":"bar","TOP":{"secret":"secret1"}},"secrets":{"secret1":{"source":"/path/to/secret"}}}`) testApp.AddEnv("FOO", "bar") testApp.AddSecret("TOP", "secret1", "/path/to/secret") app, err := json.Marshal(testApp) if assert.NoError(t, err) { assert.Equal(t, targetString, app) } } ================================================ FILE: application_test.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "fmt" "io/ioutil" "net/http" "net/url" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestApplicationDependsOn(t *testing.T) { app := NewDockerApplication() app.DependsOn("fake-app") app.DependsOn("fake-app1", "fake-app2") assert.Equal(t, 3, len(app.Dependencies)) } func TestApplicationMemory(t *testing.T) { app := NewDockerApplication() app.Memory(50.0) assert.Equal(t, 50.0, *app.Mem) } func TestApplicationString(t *testing.T) { type test struct { name string app *Application expectedAppJSONPath string setup func(*Application) } tests := []test{ { name: "marathon < 1.5", app: NewDockerApplication(). Name("my-app"). CPU(0.1). Memory(64). Storage(0.0). Count(2). AddArgs("/usr/sbin/apache2ctl", "-D", "FOREGROUND"). AddEnv("NAME", "frontend_http"). AddEnv("SERVICE_80_NAME", "test_http"), expectedAppJSONPath: "tests/app-definitions/TestApplicationString-output.json", setup: func(app *Application) { app. Container.Docker.Container("quay.io/gambol99/apache-php:latest"). Bridged(). Expose(80). Expose(443) }, }, { name: "marathon > 1.5", app: NewDockerApplication(). Name("my-app"). CPU(0.1). Memory(64). Storage(0.0). Count(2). SetNetwork("", "container/bridge"). AddArgs("/usr/sbin/apache2ctl", "-D", "FOREGROUND"). AddEnv("NAME", "frontend_http"). AddEnv("SERVICE_80_NAME", "test_http"), expectedAppJSONPath: "tests/app-definitions/TestApplicationString-1.5-output.json", setup: func(app *Application) { app. Container.Expose(80).Expose(443). Docker.Container("quay.io/gambol99/apache-php:latest") }, }, } for _, test := range tests { label := fmt.Sprintf("test: %s", test.name) test.setup(test.app) _, err := test.app.CheckHTTP("/health", 80, 5) assert.Nil(t, err) expectedAppJSONBytes, err := ioutil.ReadFile(test.expectedAppJSONPath) if err != nil { panic(err) } expectedAppJSON := strings.TrimSpace(string(expectedAppJSONBytes)) assert.Equal(t, expectedAppJSON, test.app.String(), label) } } func TestApplicationCount(t *testing.T) { app := NewDockerApplication() assert.Nil(t, app.Instances) app.Count(1) assert.Equal(t, 1, *app.Instances) } func TestApplicationStorage(t *testing.T) { app := NewDockerApplication() assert.Nil(t, app.Disk) app.Storage(0.10) assert.Equal(t, 0.10, *app.Disk) } func TestApplicationAllTaskRunning(t *testing.T) { app := NewDockerApplication() app.Instances = nil app.Tasks = nil assert.True(t, app.AllTaskRunning()) var cnt int app.Instances = &cnt cnt = 0 assert.True(t, app.AllTaskRunning()) cnt = 1 assert.False(t, app.AllTaskRunning()) app.Tasks = []*Task{} app.TasksRunning = 1 assert.True(t, app.AllTaskRunning()) cnt = 2 app.TasksRunning = 1 assert.False(t, app.AllTaskRunning()) } func TestApplicationName(t *testing.T) { app := NewDockerApplication() assert.Equal(t, "", app.ID) app.Name(fakeAppName) assert.Equal(t, fakeAppName, app.ID) } func TestApplicationCommand(t *testing.T) { app := NewDockerApplication() assert.Equal(t, "", app.ID) app.Command("format C:") assert.Equal(t, "format C:", *app.Cmd) } func TestApplicationCPU(t *testing.T) { app := NewDockerApplication() assert.Equal(t, 0.0, app.CPUs) app.CPU(0.1) assert.Equal(t, 0.1, app.CPUs) } func TestApplicationSetGPUs(t *testing.T) { app := NewDockerApplication() assert.Nil(t, app.GPUs) app.SetGPUs(0.1) assert.Equal(t, 0.1, *app.GPUs) } func TestApplicationEmptyGPUs(t *testing.T) { app := NewDockerApplication() assert.Nil(t, app.GPUs) app.EmptyGPUs() assert.Equal(t, 0.0, *app.GPUs) } func TestApplicationArgs(t *testing.T) { app := NewDockerApplication() assert.Nil(t, app.Args) app.AddArgs("-p").AddArgs("option", "-v") assert.Equal(t, 3, len(*app.Args)) assert.Equal(t, "-p", (*app.Args)[0]) assert.Equal(t, "option", (*app.Args)[1]) assert.Equal(t, "-v", (*app.Args)[2]) app.EmptyArgs() assert.NotNil(t, app.Args) assert.Equal(t, 0, len(*app.Args)) } func ExampleApplication_AddConstraint() { app := NewDockerApplication() // add two constraints app.AddConstraint("hostname", "UNIQUE"). AddConstraint("rack_id", "CLUSTER", "rack-1") } func TestApplicationConstraints(t *testing.T) { app := NewDockerApplication() assert.Nil(t, app.Constraints) app.AddConstraint("hostname", "UNIQUE"). AddConstraint("rack_id", "CLUSTER", "rack-1") assert.Equal(t, 2, len(*app.Constraints)) assert.Equal(t, []string{"hostname", "UNIQUE"}, (*app.Constraints)[0]) assert.Equal(t, []string{"rack_id", "CLUSTER", "rack-1"}, (*app.Constraints)[1]) app.EmptyConstraints() assert.NotNil(t, app.Constraints) assert.Equal(t, 0, len(*app.Constraints)) } func TestApplicationLabels(t *testing.T) { app := NewDockerApplication() assert.Nil(t, app.Labels) app.AddLabel("hello", "world").AddLabel("foo", "bar") assert.Equal(t, 2, len(*app.Labels)) assert.Equal(t, "world", (*app.Labels)["hello"]) assert.Equal(t, "bar", (*app.Labels)["foo"]) app.EmptyLabels() assert.NotNil(t, app.Labels) assert.Equal(t, 0, len(*app.Labels)) } func TestApplicationEnvs(t *testing.T) { app := NewDockerApplication() assert.Nil(t, app.Env) app.AddEnv("hello", "world").AddEnv("foo", "bar") if assert.Equal(t, 2, len((*app.Env))) { assert.Equal(t, "world", (*app.Env)["hello"]) assert.Equal(t, "bar", (*app.Env)["foo"]) } app.EmptyEnvs() assert.NotNil(t, app.Env) assert.Equal(t, 0, len(*app.Env)) } func TestApplicationSecrets(t *testing.T) { app := NewDockerApplication() assert.Nil(t, app.Env) app.AddSecret("MY_FIRST_SECRET", "secret0", "path/to/my/secret") app.AddSecret("MY_SECOND_SECRET", "secret1", "path/to/my/other/secret") if assert.Equal(t, 2, len(*app.Secrets)) { assert.Equal(t, Secret{EnvVar: "MY_FIRST_SECRET", Source: "path/to/my/secret"}, (*app.Secrets)["secret0"]) assert.Equal(t, Secret{EnvVar: "MY_SECOND_SECRET", Source: "path/to/my/other/secret"}, (*app.Secrets)["secret1"]) } app.EmptySecrets() assert.NotNil(t, app.Secrets) assert.Equal(t, 0, len(*app.Secrets)) } func TestApplicationSetExecutor(t *testing.T) { app := NewDockerApplication() assert.Nil(t, app.Executor) app.SetExecutor("executor") assert.Equal(t, "executor", *app.Executor) app.SetExecutor("") assert.Equal(t, "", *app.Executor) } func TestApplicationHealthChecks(t *testing.T) { app := NewDockerApplication() assert.Nil(t, app.HealthChecks) hc1 := NewDefaultHealthCheck() hc2 := NewDefaultHealthCheck() app.AddHealthCheck(*hc1).AddHealthCheck(*hc2) assert.Equal(t, 2, len(*app.HealthChecks)) assert.Equal(t, *hc1, (*app.HealthChecks)[0]) assert.Equal(t, *hc2, (*app.HealthChecks)[1]) app.EmptyHealthChecks() assert.NotNil(t, app.HealthChecks) assert.Equal(t, 0, len(*app.HealthChecks)) } func TestApplicationReadinessChecks(t *testing.T) { app := NewDockerApplication() require.Nil(t, app.HealthChecks) rc := ReadinessCheck{} rc.SetName("/readiness") app.AddReadinessCheck(rc) require.Equal(t, 1, len(*app.ReadinessChecks)) assert.Equal(t, "/readiness", *((*app.ReadinessChecks)[0].Name)) app.EmptyReadinessChecks() require.NotNil(t, app.ReadinessChecks) assert.Equal(t, 0, len(*app.ReadinessChecks)) } func TestApplicationPortDefinitions(t *testing.T) { app := NewDockerApplication() assert.Nil(t, app.PortDefinitions) pd1 := new(PortDefinition) pd1.SetProtocol("tcp").SetName("es").SetPort(9092).AddLabel("foo", "bar") pd2 := new(PortDefinition) pd2.SetProtocol("udp,tcp").SetName("syslog").SetPort(514) app.AddPortDefinition(*pd1).AddPortDefinition(*pd2) assert.Equal(t, 2, len(*app.PortDefinitions)) assert.Equal(t, *pd1, (*app.PortDefinitions)[0]) assert.Equal(t, 1, len(*(*app.PortDefinitions)[0].Labels)) assert.Equal(t, *pd2, (*app.PortDefinitions)[1]) assert.Nil(t, (*app.PortDefinitions)[1].Labels) (*app.PortDefinitions)[0].EmptyLabels() assert.NotNil(t, (*app.PortDefinitions)[0].Labels) assert.Equal(t, 0, len(*(*app.PortDefinitions)[0].Labels)) app.EmptyPortDefinitions() assert.NotNil(t, app.PortDefinitions) assert.Equal(t, 0, len(*app.PortDefinitions)) } func TestHasHealthChecks(t *testing.T) { apps := []*Application{ NewDockerApplication(), NewDockerApplication(), } for i := range apps { assert.False(t, apps[i].HasHealthChecks()) } // Marathon < 1.5 apps[0].Container.Docker.Container("quay.io/gambol99/apache-php:latest").Expose(80) // Marathon >= 1.5 apps[1].Container.Expose(80).Docker.Container("quay.io/gambol99/apache-php:latest") for i := range apps { _, err := apps[i].CheckTCP(80, 10) assert.NoError(t, err) assert.True(t, apps[i].HasHealthChecks()) } } func TestApplicationCheckTCP(t *testing.T) { apps := []*Application{ NewDockerApplication(), NewDockerApplication(), } for i := range apps { assert.False(t, apps[i].HasHealthChecks()) _, err := apps[i].CheckTCP(80, 10) assert.Error(t, err) assert.False(t, apps[i].HasHealthChecks()) } // Marathon < 1.5 apps[0].Container.Docker.Container("quay.io/gambol99/apache-php:latest").Expose(80) // Marathon >= 1.5 apps[1].Container.Expose(80).Docker.Container("quay.io/gambol99/apache-php:latest") for i := range apps { _, err := apps[i].CheckTCP(80, 10) assert.NoError(t, err) assert.True(t, apps[i].HasHealthChecks()) check := (*apps[i].HealthChecks)[0] assert.Equal(t, "TCP", check.Protocol) assert.Equal(t, 10, check.IntervalSeconds) assert.Equal(t, 0, *check.PortIndex) } } func TestApplicationCheckHTTP(t *testing.T) { apps := []*Application{ NewDockerApplication(), NewDockerApplication(), } for i := range apps { assert.False(t, apps[i].HasHealthChecks()) _, err := apps[i].CheckHTTP("/", 80, 10) assert.Error(t, err) assert.False(t, apps[i].HasHealthChecks()) } // Marathon < 1.5 apps[0].Container.Docker.Container("quay.io/gambol99/apache-php:latest").Expose(80) // Marathon >= 1.5 apps[1].Container.Expose(80).Docker.Container("quay.io/gambol99/apache-php:latest") for i := range apps { _, err := apps[i].CheckHTTP("/health", 80, 10) assert.NoError(t, err) assert.True(t, apps[i].HasHealthChecks()) check := (*apps[i].HealthChecks)[0] assert.Equal(t, "HTTP", check.Protocol) assert.Equal(t, 10, check.IntervalSeconds) assert.Equal(t, "/health", *check.Path) assert.Equal(t, 0, *check.PortIndex) } } func TestCreateApplication(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() application := NewDockerApplication() application.Name(fakeAppName) app, err := endpoint.Client.CreateApplication(application) assert.NoError(t, err) assert.NotNil(t, app) assert.Equal(t, application.ID, fakeAppName) assert.Equal(t, app.Deployments[0]["id"], "f44fd4fc-4330-4600-a68b-99c7bd33014a") } func TestUpdateApplication(t *testing.T) { for _, force := range []bool{false, true} { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() application := NewDockerApplication() application.Name(fakeAppName) id, err := endpoint.Client.UpdateApplication(application, force) assert.NoError(t, err) assert.Equal(t, id.DeploymentID, "83b215a6-4e26-4e44-9333-5c385eda6438") assert.Equal(t, id.Version, "2014-08-26T07:37:50.462Z") } } func TestApplications(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() applications, err := endpoint.Client.Applications(nil) assert.NoError(t, err) assert.NotNil(t, applications) assert.Equal(t, len(applications.Apps), 2) assert.Equal(t, (*applications.Apps[0].Secrets)["secret0"].EnvVar, "SECRET1") assert.Equal(t, (*applications.Apps[0].Secrets)["secret0"].Source, "secret/definition/id") v := url.Values{} v.Set("cmd", "nginx") applications, err = endpoint.Client.Applications(v) assert.NoError(t, err) assert.NotNil(t, applications) assert.Equal(t, len(applications.Apps), 1) } func TestApplicationsEmbedTaskStats(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() v := url.Values{} v.Set("embed", "apps.taskStats") applications, err := endpoint.Client.Applications(v) assert.NoError(t, err) assert.NotNil(t, applications) assert.Equal(t, len(applications.Apps), 1) assert.NotNil(t, applications.Apps[0].TaskStats) assert.Equal(t, applications.Apps[0].TaskStats["startedAfterLastScaling"].Stats.Counts["healthy"], 1) assert.Equal(t, applications.Apps[0].TaskStats["startedAfterLastScaling"].Stats.LifeTime["averageSeconds"], 17024.575) } func TestApplicationsEmbedReadiness(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() v := url.Values{} v.Set("embed", "apps.readiness") applications, err := endpoint.Client.Applications(v) require.NoError(t, err) require.NotNil(t, applications) require.Equal(t, len(applications.Apps), 1) require.NotNil(t, applications.Apps[0].ReadinessCheckResults) require.True(t, len(*applications.Apps[0].ReadinessCheckResults) > 0) actualRes := (*applications.Apps[0].ReadinessCheckResults)[0] expectedRes := ReadinessCheckResult{ Name: "myReadyCheck", TaskID: "test_frontend_app1.c9de6033", Ready: false, LastResponse: ReadinessLastResponse{ Body: "{}", ContentType: "application/json", Status: 500, }, } assert.Equal(t, expectedRes, actualRes) } func TestListApplications(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() applications, err := endpoint.Client.ListApplications(nil) assert.NoError(t, err) assert.NotNil(t, applications) assert.Equal(t, len(applications), 2) assert.Equal(t, applications[0], fakeAppName) assert.Equal(t, applications[1], fakeAppNameBroken) } func TestApplicationVersions(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() versions, err := endpoint.Client.ApplicationVersions(fakeAppName) assert.NoError(t, err) assert.NotNil(t, versions) assert.NotNil(t, versions.Versions) assert.Equal(t, len(versions.Versions), 1) assert.Equal(t, versions.Versions[0], "2014-04-04T06:25:31.399Z") /* check we get an error on app not there */ _, err = endpoint.Client.ApplicationVersions("/not/there") assert.Error(t, err) } func TestRestartApplication(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() id, err := endpoint.Client.RestartApplication(fakeAppName, false) assert.NoError(t, err) assert.NotNil(t, id) assert.Equal(t, "83b215a6-4e26-4e44-9333-5c385eda6438", id.DeploymentID) assert.Equal(t, "2014-08-26T07:37:50.462Z", id.Version) id, err = endpoint.Client.RestartApplication("/not/there", false) assert.Error(t, err) assert.Nil(t, id) } func TestApplicationUris(t *testing.T) { app := NewDockerApplication() assert.Nil(t, app.Uris) app.AddUris("file://uri1.tar.gz").AddUris("file://uri2.tar.gz", "file://uri3.tar.gz") assert.Equal(t, 3, len(*app.Uris)) assert.Equal(t, "file://uri1.tar.gz", (*app.Uris)[0]) assert.Equal(t, "file://uri2.tar.gz", (*app.Uris)[1]) assert.Equal(t, "file://uri3.tar.gz", (*app.Uris)[2]) app.EmptyUris() assert.NotNil(t, app.Uris) assert.Equal(t, 0, len(*app.Uris)) } func TestApplicationFetchURIs(t *testing.T) { app := NewDockerApplication() assert.Nil(t, app.Fetch) app.AddFetchURIs(Fetch{URI: "file://uri1.tar.gz"}). AddFetchURIs(Fetch{URI: "file://uri2.tar.gz"}, Fetch{URI: "file://uri3.tar.gz"}) assert.Equal(t, 3, len(*app.Fetch)) assert.Equal(t, Fetch{URI: "file://uri1.tar.gz"}, (*app.Fetch)[0]) assert.Equal(t, Fetch{URI: "file://uri2.tar.gz"}, (*app.Fetch)[1]) assert.Equal(t, Fetch{URI: "file://uri3.tar.gz"}, (*app.Fetch)[2]) app.EmptyFetchURIs() assert.NotNil(t, app.Fetch) assert.Equal(t, 0, len(*app.Fetch)) } func TestSetApplicationVersion(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() deployment, err := endpoint.Client.SetApplicationVersion(fakeAppName, &ApplicationVersion{Version: "2014-08-26T07:37:50.462Z"}) assert.NoError(t, err) assert.NotNil(t, deployment) assert.NotNil(t, deployment.Version) assert.NotNil(t, deployment.DeploymentID) assert.Equal(t, deployment.Version, "2014-08-26T07:37:50.462Z") assert.Equal(t, deployment.DeploymentID, "83b215a6-4e26-4e44-9333-5c385eda6438") _, err = endpoint.Client.SetApplicationVersion("/not/there", &ApplicationVersion{Version: "2014-04-04T06:25:31.399Z"}) assert.Error(t, err) } func TestHasApplicationVersion(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() found, err := endpoint.Client.HasApplicationVersion(fakeAppName, "2014-04-04T06:25:31.399Z") assert.NoError(t, err) assert.True(t, found) found, err = endpoint.Client.HasApplicationVersion(fakeAppName, "###2015-04-04T06:25:31.399Z") assert.NoError(t, err) assert.False(t, found) } func TestDeleteApplication(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() for _, force := range []bool{false, true} { id, err := endpoint.Client.DeleteApplication(fakeAppName, force) assert.NoError(t, err) assert.NotNil(t, id) assert.Equal(t, "83b215a6-4e26-4e44-9333-5c385eda6438", id.DeploymentID) assert.Equal(t, "2014-08-26T07:37:50.462Z", id.Version) _, err = endpoint.Client.DeleteApplication("no_such_app", force) assert.Error(t, err) } } func TestApplicationOK(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() ok, err := endpoint.Client.ApplicationOK(fakeAppName) assert.NoError(t, err) assert.True(t, ok) ok, err = endpoint.Client.ApplicationOK(fakeAppNameBroken) assert.NoError(t, err) assert.False(t, ok) ok, err = endpoint.Client.ApplicationOK(fakeAppNameUnhealthy) assert.NoError(t, err) assert.False(t, ok) } func verifyApplication(application *Application, t *testing.T) { assert.NotNil(t, application) assert.Equal(t, application.ID, fakeAppName) assert.NotNil(t, application.HealthChecks) assert.NotNil(t, application.Tasks) assert.Equal(t, len(*application.HealthChecks), 1) assert.Equal(t, len(application.Tasks), 2) assert.Equal(t, application.Residency, &Residency{ TaskLostBehavior: TaskLostBehaviorTypeRelaunchAfterTimeout, RelaunchEscalationTimeoutSeconds: 60, }) } func TestApplication(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() application, err := endpoint.Client.Application(fakeAppName) assert.NoError(t, err) verifyApplication(application, t) _, err = endpoint.Client.Application("no_such_app") assert.Error(t, err) apiErr, ok := err.(*APIError) assert.True(t, ok) assert.Equal(t, ErrCodeNotFound, apiErr.ErrCode) config := NewDefaultConfig() config.URL = "http://non-existing-marathon-host.local:5555" // Reduce timeout to speed up test execution time. config.HTTPClient = &http.Client{ Timeout: 100 * time.Millisecond, } endpoint = newFakeMarathonEndpoint(t, &configContainer{ client: &config, }) defer endpoint.Close() _, err = endpoint.Client.Application(fakeAppName) assert.Error(t, err) _, ok = err.(*APIError) assert.False(t, ok) } func TestApplicationConfiguration(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() application, err := endpoint.Client.ApplicationByVersion(fakeAppName, "2014-09-12T23:28:21.737Z") assert.NoError(t, err) verifyApplication(application, t) _, err = endpoint.Client.ApplicationByVersion(fakeAppName, "no_such_version") assert.Error(t, err) apiErr, ok := err.(*APIError) assert.True(t, ok) assert.Equal(t, ErrCodeNotFound, apiErr.ErrCode) _, err = endpoint.Client.ApplicationByVersion("no_such_app", "latest") assert.Error(t, err) apiErr, ok = err.(*APIError) assert.True(t, ok) assert.Equal(t, ErrCodeNotFound, apiErr.ErrCode) } func TestWaitOnApplication(t *testing.T) { waitTime := 100 * time.Millisecond tests := []struct { desc string timeout time.Duration appName string testScope string shouldSucceed bool }{ { desc: "initially existing app", timeout: 0, appName: fakeAppName, shouldSucceed: true, }, { desc: "delayed existing app | timeout > ticker", timeout: 200 * time.Millisecond, appName: fakeAppName, testScope: "wait-on-app", shouldSucceed: true, }, { desc: "delayed existing app | timeout < ticker", timeout: 50 * time.Millisecond, appName: fakeAppName, testScope: "wait-on-app", shouldSucceed: false, }, { desc: "missing app | timeout > ticker", timeout: 200 * time.Millisecond, appName: "no_such_app", shouldSucceed: false, }, { desc: "missing app | timeout < ticker", timeout: 50 * time.Millisecond, appName: "no_such_app", shouldSucceed: false, }, } for _, test := range tests { defaultConfig := NewDefaultConfig() defaultConfig.PollingWaitTime = waitTime configs := &configContainer{ client: &defaultConfig, server: &serverConfig{ scope: test.testScope, }, } endpoint := newFakeMarathonEndpoint(t, configs) defer endpoint.Close() errCh := make(chan error) go func() { errCh <- endpoint.Client.WaitOnApplication(test.appName, test.timeout) }() select { case <-time.After(400 * time.Millisecond): assert.Fail(t, fmt.Sprintf("%s: WaitOnApplication did not complete in time", test.desc)) case err := <-errCh: if test.shouldSucceed { assert.NoError(t, err, test.desc) } else { assert.IsType(t, err, ErrTimeoutError, test.desc) } } } } func TestAppExistAndRunning(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() client := endpoint.Client.(*marathonClient) assert.True(t, client.appExistAndRunning(fakeAppName)) assert.False(t, client.appExistAndRunning("no_such_app")) } func TestSetIPPerTask(t *testing.T) { app := Application{} app.Ports = append(app.Ports, 10) app.AddPortDefinition(PortDefinition{}) assert.Nil(t, app.IPAddressPerTask) assert.Equal(t, 1, len(app.Ports)) assert.Equal(t, 1, len(*app.PortDefinitions)) app.SetIPAddressPerTask(IPAddressPerTask{}) assert.NotNil(t, app.IPAddressPerTask) assert.Equal(t, 0, len(app.Ports)) assert.Equal(t, 0, len(*app.PortDefinitions)) } func TestIPAddressPerTask(t *testing.T) { ipPerTask := IPAddressPerTask{} assert.Nil(t, ipPerTask.Groups) assert.Nil(t, ipPerTask.Labels) assert.Nil(t, ipPerTask.Discovery) ipPerTask. AddGroup("label"). AddLabel("key", "value"). SetDiscovery(Discovery{ Ports: &[]Port{}, }) assert.Equal(t, 1, len(*ipPerTask.Groups)) assert.Equal(t, "label", (*ipPerTask.Groups)[0]) assert.Equal(t, "value", (*ipPerTask.Labels)["key"]) assert.NotEmpty(t, *ipPerTask.Discovery) ipPerTask.EmptyGroups() assert.Equal(t, 0, len(*ipPerTask.Groups)) ipPerTask.EmptyLabels() assert.Equal(t, 0, len(*ipPerTask.Labels)) } func TestIPAddressPerTaskDiscovery(t *testing.T) { disc := Discovery{} assert.Nil(t, disc.Ports) disc.AddPort(Port{}) assert.NotNil(t, disc.Ports) assert.Equal(t, 1, len(*disc.Ports)) disc.EmptyPorts() assert.NotNil(t, disc.Ports) assert.Equal(t, 0, len(*disc.Ports)) } func TestUpgradeStrategy(t *testing.T) { app := Application{} assert.Nil(t, app.UpgradeStrategy) us := new(UpgradeStrategy) us.SetMinimumHealthCapacity(1.0).SetMaximumOverCapacity(0.0) app.SetUpgradeStrategy(*us) testUs := app.UpgradeStrategy assert.Equal(t, 1.0, *testUs.MinimumHealthCapacity) assert.Equal(t, 0.0, *testUs.MaximumOverCapacity) app.EmptyUpgradeStrategy() us = app.UpgradeStrategy assert.NotNil(t, us) assert.Nil(t, us.MinimumHealthCapacity) assert.Nil(t, us.MaximumOverCapacity) } func TestBridgedNetworking(t *testing.T) { app := NewDockerApplication().SetNetwork("test", "container/bridge") networks := *app.Networks assert.Equal(t, networks[0].Mode, BridgeNetworkMode) } func TestContainerNetworking(t *testing.T) { app := NewDockerApplication().SetNetwork("test", "container") networks := *app.Networks assert.Equal(t, networks[0].Mode, ContainerNetworkMode) } func TestHostNetworking(t *testing.T) { app := NewDockerApplication().SetNetwork("test", "host") networks := *app.Networks assert.Equal(t, networks[0].Mode, HostNetworkMode) } ================================================ FILE: client.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "bytes" "encoding/json" "errors" "fmt" "io" "io/ioutil" "log" "net" "net/http" "net/url" "regexp" "strings" "sync" "time" ) // Marathon is the interface to the marathon API type Marathon interface { // -- GENERIC API ACCESS --- ApiPost(path string, post, result interface{}) error // -- APPLICATIONS --- // get a listing of the application ids ListApplications(url.Values) ([]string, error) // a list of application versions ApplicationVersions(name string) (*ApplicationVersions, error) // check a application version exists HasApplicationVersion(name, version string) (bool, error) // change an application to a different version SetApplicationVersion(name string, version *ApplicationVersion) (*DeploymentID, error) // check if an application is ok ApplicationOK(name string) (bool, error) // create an application in marathon CreateApplication(application *Application) (*Application, error) // delete an application DeleteApplication(name string, force bool) (*DeploymentID, error) // update an application in marathon UpdateApplication(application *Application, force bool) (*DeploymentID, error) // a list of deployments on a application ApplicationDeployments(name string) ([]*DeploymentID, error) // scale a application ScaleApplicationInstances(name string, instances int, force bool) (*DeploymentID, error) // restart an application RestartApplication(name string, force bool) (*DeploymentID, error) // get a list of applications from marathon Applications(url.Values) (*Applications, error) // get an application by name Application(name string) (*Application, error) // get an application by options ApplicationBy(name string, opts *GetAppOpts) (*Application, error) // get an application by name and version ApplicationByVersion(name, version string) (*Application, error) // wait of application WaitOnApplication(name string, timeout time.Duration) error // -- PODS --- // whether this version of Marathon supports pods SupportsPods() (bool, error) // get pod status PodStatus(name string) (*PodStatus, error) // get all pod statuses PodStatuses() ([]*PodStatus, error) // get pod Pod(name string) (*Pod, error) // get all pods Pods() ([]Pod, error) // create pod CreatePod(pod *Pod) (*Pod, error) // update pod UpdatePod(pod *Pod, force bool) (*Pod, error) // delete pod DeletePod(name string, force bool) (*DeploymentID, error) // wait on pod to be deployed WaitOnPod(name string, timeout time.Duration) error // check if a pod is running PodIsRunning(name string) bool // get versions of a pod PodVersions(name string) ([]string, error) // get pod by version PodByVersion(name, version string) (*Pod, error) // delete instances of a pod DeletePodInstances(name string, instances []string) ([]*PodInstance, error) // delete pod instance DeletePodInstance(name, instance string) (*PodInstance, error) // -- TASKS --- // get a list of tasks for a specific application Tasks(application string) (*Tasks, error) // get a list of all tasks AllTasks(opts *AllTasksOpts) (*Tasks, error) // get the endpoints for a service on a application TaskEndpoints(name string, port int, healthCheck bool) ([]string, error) // kill all the tasks for any application KillApplicationTasks(applicationID string, opts *KillApplicationTasksOpts) (*Tasks, error) // kill a single task KillTask(taskID string, opts *KillTaskOpts) (*Task, error) // kill the given array of tasks KillTasks(taskIDs []string, opts *KillTaskOpts) error // --- GROUPS --- // list all the groups in the system Groups() (*Groups, error) // retrieve a specific group from marathon Group(name string) (*Group, error) // list all groups in marathon by options GroupsBy(opts *GetGroupOpts) (*Groups, error) // retrieve a specific group from marathon by options GroupBy(name string, opts *GetGroupOpts) (*Group, error) // create a group deployment CreateGroup(group *Group) error // delete a group DeleteGroup(name string, force bool) (*DeploymentID, error) // update a groups UpdateGroup(id string, group *Group, force bool) (*DeploymentID, error) // check if a group exists HasGroup(name string) (bool, error) // wait for an group to be deployed WaitOnGroup(name string, timeout time.Duration) error // --- DEPLOYMENTS --- // get a list of the deployments Deployments() ([]*Deployment, error) // delete a deployment DeleteDeployment(id string, force bool) (*DeploymentID, error) // check to see if a deployment exists HasDeployment(id string) (bool, error) // wait of a deployment to finish WaitOnDeployment(id string, timeout time.Duration) error // --- SUBSCRIPTIONS --- // a list of current subscriptions Subscriptions() (*Subscriptions, error) // add a events listener AddEventsListener(filter int) (EventsChannel, error) // remove a events listener RemoveEventsListener(channel EventsChannel) // Subscribe a callback URL Subscribe(string) error // Unsubscribe a callback URL Unsubscribe(string) error // --- QUEUE --- // get marathon launch queue Queue() (*Queue, error) // resets task launch delay of the specific application DeleteQueueDelay(appID string) error // --- MISC --- // get the marathon url GetMarathonURL() string // ping the marathon Ping() (bool, error) // grab the marathon server info Info() (*Info, error) // retrieve the leader info Leader() (string, error) // cause the current leader to abdicate AbdicateLeader() (string, error) } var ( // ErrMarathonDown is thrown when all the marathon endpoints are down ErrMarathonDown = errors.New("all the Marathon hosts are presently down") // ErrTimeoutError is thrown when the operation has timed out ErrTimeoutError = errors.New("the operation has timed out") // Default HTTP client used for SSE subscription requests // It is invalid to set client.Timeout because it includes time to read response so // set dial, tls handshake and response header timeouts instead defaultHTTPSSEClient = &http.Client{ Transport: &http.Transport{ Dial: (&net.Dialer{ Timeout: 5 * time.Second, }).Dial, ResponseHeaderTimeout: 10 * time.Second, TLSHandshakeTimeout: 5 * time.Second, }, } // Default HTTP client used for non SSE requests defaultHTTPClient = &http.Client{ Timeout: 10 * time.Second, } ) // EventsChannelContext holds contextual data for an EventsChannel. type EventsChannelContext struct { filter int done chan struct{} completion *sync.WaitGroup } type marathonClient struct { sync.RWMutex // the configuration for the client config Config // the flag used to prevent multiple SSE subscriptions subscribedToSSE bool // the ip address of the client ipAddress string // the http server eventsHTTP *http.Server // the marathon hosts hosts *cluster // a map of service you wish to listen to listeners map[EventsChannel]EventsChannelContext // a custom log function for debug messages debugLog func(format string, v ...interface{}) // the marathon HTTP client to ensure consistency in requests client *httpClient } type httpClient struct { // the configuration for the marathon HTTP client config Config } // newRequestError signals that creating a new http.Request failed type newRequestError struct { error } // NewClient creates a new marathon client // config: the configuration to use func NewClient(config Config) (Marathon, error) { // step: if the SSE HTTP client is missing, prefer a configured regular // client, and otherwise use the default SSE HTTP client. if config.HTTPSSEClient == nil { config.HTTPSSEClient = defaultHTTPSSEClient if config.HTTPClient != nil { config.HTTPSSEClient = config.HTTPClient } } // step: if a regular HTTP client is missing, use the default one. if config.HTTPClient == nil { config.HTTPClient = defaultHTTPClient } // step: if no polling wait time is set, default to 500 milliseconds. if config.PollingWaitTime == 0 { config.PollingWaitTime = defaultPollingWaitTime } // step: setup shared client client := &httpClient{config: config} // step: create a new cluster hosts, err := newCluster(client, config.URL, config.DCOSToken != "") if err != nil { return nil, err } debugLog := func(string, ...interface{}) {} if config.LogOutput != nil { logger := log.New(config.LogOutput, "", 0) debugLog = func(format string, v ...interface{}) { logger.Printf(format, v...) } } return &marathonClient{ config: config, listeners: make(map[EventsChannel]EventsChannelContext), hosts: hosts, debugLog: debugLog, client: client, }, nil } // GetMarathonURL retrieves the marathon url func (r *marathonClient) GetMarathonURL() string { return r.config.URL } // Ping pings the current marathon endpoint (note, this is not a ICMP ping, but a rest api call) func (r *marathonClient) Ping() (bool, error) { if err := r.apiGet(marathonAPIPing, nil, nil); err != nil { return false, err } return true, nil } func (r *marathonClient) apiHead(path string, result interface{}) error { return r.apiCall("HEAD", path, nil, result) } func (r *marathonClient) apiGet(path string, post, result interface{}) error { return r.apiCall("GET", path, post, result) } func (r *marathonClient) apiPut(path string, post, result interface{}) error { return r.apiCall("PUT", path, post, result) } func (r *marathonClient) ApiPost(path string, post, result interface{}) error { return r.apiCall("POST", path, post, result) } func (r *marathonClient) apiDelete(path string, post, result interface{}) error { return r.apiCall("DELETE", path, post, result) } func (r *marathonClient) apiCall(method, path string, body, result interface{}) error { const deploymentHeader = "Marathon-Deployment-Id" for { // step: marshall the request to json var requestBody []byte var err error if body != nil { if requestBody, err = json.Marshal(body); err != nil { return err } } // step: create the API request request, member, err := r.buildAPIRequest(method, path, bytes.NewReader(requestBody)) if err != nil { return err } // step: perform the API request response, err := r.client.Do(request) if err != nil { r.hosts.markDown(member) // step: attempt the request on another member r.debugLog("apiCall(): request failed on host: %s, error: %s, trying another", member, err) continue } defer response.Body.Close() // step: read the response body respBody, err := ioutil.ReadAll(response.Body) if err != nil { return err } if len(requestBody) > 0 { r.debugLog("apiCall(): %v %v %s returned %v %s", request.Method, request.URL.String(), requestBody, response.Status, oneLogLine(respBody)) } else { r.debugLog("apiCall(): %v %v returned %v %s", request.Method, request.URL.String(), response.Status, oneLogLine(respBody)) } // step: check for a successful response if response.StatusCode >= 200 && response.StatusCode <= 299 { if result != nil { // If we have a deployment ID header and no response body, give them that // This specifically handles the use case of a DELETE on an app/pod // We need a way to retrieve the deployment ID deploymentID := response.Header.Get(deploymentHeader) if len(respBody) == 0 && deploymentID != "" { d := DeploymentID{ DeploymentID: deploymentID, } if deployID, ok := result.(*DeploymentID); ok { *deployID = d } } else { if err := json.Unmarshal(respBody, result); err != nil { return fmt.Errorf("failed to unmarshal response from Marathon: %s", err) } } } return nil } // step: if the member node returns a >= 500 && <= 599 we should try another node? if response.StatusCode >= 500 && response.StatusCode <= 599 { // step: mark the host as down r.hosts.markDown(member) r.debugLog("apiCall(): request failed, host: %s, status: %d, trying another", member, response.StatusCode) continue } return NewAPIError(response.StatusCode, respBody) } } // wait waits until the provided function returns true (or times out) func (r *marathonClient) wait(name string, timeout time.Duration, fn func(string) bool) error { timer := time.NewTimer(timeout) defer timer.Stop() ticker := time.NewTicker(r.config.PollingWaitTime) defer ticker.Stop() for { if fn(name) { return nil } select { case <-timer.C: return ErrTimeoutError case <-ticker.C: continue } } } // buildAPIRequest creates a default API request. // It fails when there is no available member in the cluster anymore or when the request can not be built. func (r *marathonClient) buildAPIRequest(method, path string, reader io.Reader) (request *http.Request, member string, err error) { // Grab a member from the cluster member, err = r.hosts.getMember() if err != nil { return nil, "", ErrMarathonDown } // Build the HTTP request to Marathon request, err = r.client.buildMarathonJSONRequest(method, member, path, reader) if err != nil { return nil, member, newRequestError{err} } return request, member, nil } // buildMarathonJSONRequest is like buildMarathonRequest but sets the // Content-Type and Accept headers to application/json. func (rc *httpClient) buildMarathonJSONRequest(method, member, path string, reader io.Reader) (request *http.Request, err error) { req, err := rc.buildMarathonRequest(method, member, path, reader) if err == nil { req.Header.Add("Content-Type", "application/json") req.Header.Add("Accept", "application/json") } return req, err } // buildMarathonRequest creates a new HTTP request and configures it according to the *httpClient configuration. // The path must not contain a leading "/", otherwise buildMarathonRequest will panic. func (rc *httpClient) buildMarathonRequest(method, member, path string, reader io.Reader) (request *http.Request, err error) { if strings.HasPrefix(path, "/") { panic(fmt.Sprintf("Path '%s' must not start with a leading slash", path)) } // Create the endpoint URL url := fmt.Sprintf("%s/%s", member, path) // Instantiate an HTTP request request, err = http.NewRequest(method, url, reader) if err != nil { return nil, err } // Add any basic auth and the content headers if rc.config.HTTPBasicAuthUser != "" && rc.config.HTTPBasicPassword != "" { request.SetBasicAuth(rc.config.HTTPBasicAuthUser, rc.config.HTTPBasicPassword) } if rc.config.DCOSToken != "" { request.Header.Add("Authorization", "token="+rc.config.DCOSToken) } return request, nil } func (rc *httpClient) Do(request *http.Request) (response *http.Response, err error) { return rc.config.HTTPClient.Do(request) } var oneLogLineRegex = regexp.MustCompile(`(?m)^\s*`) // oneLogLine removes indentation at the beginning of each line and // escapes new line characters. func oneLogLine(in []byte) []byte { return bytes.Replace(oneLogLineRegex.ReplaceAll(in, nil), []byte("\n"), []byte("\\n "), -1) } ================================================ FILE: client_test.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "bytes" "testing" "net/http" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewClient(t *testing.T) { config := Config{ URL: "http://marathon", } cl, err := NewClient(config) if !assert.Nil(t, err) { return } conf := cl.(*marathonClient).config assert.Equal(t, conf.HTTPClient, defaultHTTPClient) assert.Equal(t, conf.HTTPSSEClient, defaultHTTPSSEClient) assert.Zero(t, conf.HTTPSSEClient.Timeout) assert.Equal(t, conf.PollingWaitTime, defaultPollingWaitTime) } func TestHTTPClientDefaults(t *testing.T) { customHTTPRegularClient := http.DefaultClient tests := []struct { name string httpRegularClient *http.Client httpSSEClient *http.Client wantHTTPRegularClient *http.Client wantHTTPSSEClient *http.Client }{ { name: "regular HTTP client missing", httpRegularClient: nil, wantHTTPRegularClient: defaultHTTPClient, }, { name: "SSE and regular HTTP clients missing", httpSSEClient: nil, wantHTTPSSEClient: defaultHTTPSSEClient, }, { name: "SSE HTTP client missing, regular HTTP client available", httpSSEClient: nil, httpRegularClient: customHTTPRegularClient, wantHTTPSSEClient: customHTTPRegularClient, }, } for _, test := range tests { config := NewDefaultConfig() config.HTTPClient = test.httpRegularClient config.HTTPSSEClient = test.httpSSEClient client, err := NewClient(config) if !assert.NoError(t, err, test.name) { continue } maraClient := client.(*marathonClient) if test.wantHTTPRegularClient != nil { if !assert.Equal(t, test.wantHTTPRegularClient, maraClient.config.HTTPClient, test.name) { continue } } if test.wantHTTPSSEClient != nil { if !assert.Equal(t, test.wantHTTPSSEClient, maraClient.config.HTTPSSEClient, test.name) { continue } } } } func TestLogOutput(t *testing.T) { buf := bytes.NewBuffer(nil) config := Config{ URL: "http://marathon", LogOutput: buf, } cl, err := NewClient(config) require.Nil(t, err) cl.(*marathonClient).debugLog("this is a %s", "test") assert.Equal(t, "this is a test\n", buf.String()) } func TestInvalidConfig(t *testing.T) { config := Config{ URL: "", } _, err := NewClient(config) assert.Error(t, err) } func TestPing(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() pong, err := endpoint.Client.Ping() assert.NoError(t, err) assert.True(t, pong) } func TestGetMarathonURL(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() assert.Equal(t, endpoint.Client.GetMarathonURL(), endpoint.URL) } func TestAPIRequest(t *testing.T) { cases := []struct { Username string Password string ServerUsername string ServerPassword string Ok bool }{ { Username: "should_pass", Password: "", ServerUsername: "", ServerPassword: "", Ok: true, }, { Username: "bad_username", Password: "", ServerUsername: "test", ServerPassword: "password", Ok: false, }, { Username: "test", Password: "bad_password", ServerUsername: "test", ServerPassword: "password", Ok: false, }, { Username: "", Password: "", ServerUsername: "test", ServerPassword: "password", Ok: false, }, { Username: "test", Password: "password", ServerUsername: "test", ServerPassword: "password", Ok: true, }, } for i, x := range cases { var endpoint *endpoint config := NewDefaultConfig() config.HTTPBasicAuthUser = x.Username config.HTTPBasicPassword = x.Password endpoint = newFakeMarathonEndpoint(t, &configContainer{ client: &config, server: &serverConfig{ username: x.ServerUsername, password: x.ServerPassword, }, }) _, err := endpoint.Client.Applications(nil) if x.Ok && err != nil { t.Errorf("case %d, did not expect an error: %s", i, err) } if !x.Ok && err == nil { t.Errorf("case %d, expected to received an error", i) } endpoint.Close() } } func TestBuildApiRequestFailure(t *testing.T) { tests := []struct { name string expectedError error expectedErrorType interface{} path string clusterDown bool }{ { name: "cluster down", expectedError: ErrMarathonDown, clusterDown: true, }, { name: "invalid request parameter", expectedErrorType: newRequestError{}, path: "%zzzzz", }, } for _, test := range tests { if test.expectedError == nil && test.expectedErrorType == nil { panic("Testcase requires at least one of 'expectedError' or 'expectedErrorType'") } clientCfg := NewDefaultConfig() config := configContainer{client: &clientCfg} endpoint := newFakeMarathonEndpoint(t, &config) client := endpoint.Client.(*marathonClient) if test.clusterDown { for _, member := range client.hosts.members { member.status = memberStatusDown } } _, _, err := client.buildAPIRequest("GET", test.path, nil) if test.expectedError != nil { assert.Equal(t, test.expectedError, err) } if test.expectedErrorType != nil { assert.IsType(t, test.expectedErrorType, err) } endpoint.Close() } } func TestOneLogLine(t *testing.T) { in := ` a b c d\n efgh i\r\n j\t {"json": "works", "f o o": "ba r" } ` assert.Equal(t, `a\n b c\n d\n\n efgh\n i\r\n\n j\t\n {"json": "works",\n "f o o": "ba r"\n }\n `, string(oneLogLine([]byte(in)))) } func TestAPIRequestDCOS(t *testing.T) { cases := []struct { DCOSToken string ServerDCOSToken string ServerUsername string ServerPassword string Ok bool }{ { DCOSToken: "should_pass", ServerDCOSToken: "should_pass", ServerUsername: "", ServerPassword: "", Ok: true, }, { DCOSToken: "should_pass", ServerDCOSToken: "", ServerUsername: "", ServerPassword: "", Ok: true, }, { DCOSToken: "should_not_pass", ServerDCOSToken: "different_token", ServerUsername: "", ServerPassword: "", Ok: false, }, } for i, x := range cases { var endpoint *endpoint config := NewDefaultConfig() config.DCOSToken = x.DCOSToken endpoint = newFakeMarathonEndpoint(t, &configContainer{ client: &config, server: &serverConfig{ dcosToken: x.ServerDCOSToken, username: x.ServerUsername, password: x.ServerPassword, }, }) _, err := endpoint.Client.Applications(nil) if x.Ok && err != nil { t.Errorf("case %d, did not expect an error: %s", i, err) } if !x.Ok && err == nil { t.Errorf("case %d, expected to received an error", i) } endpoint.Close() } } ================================================ FILE: cluster.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "fmt" "net/url" "strings" "sync" "time" ) const ( memberStatusUp = 0 memberStatusDown = 1 ) // the status of a member node type memberStatus int // cluster is a collection of marathon nodes type cluster struct { sync.RWMutex // a collection of nodes members []*member // the marathon HTTP client to ensure consistency in requests client *httpClient // healthCheckInterval is the interval by which we probe down nodes for // availability again. healthCheckInterval time.Duration } // member represents an individual endpoint type member struct { // the name / ip address of the host endpoint string // the status of the host status memberStatus } // newCluster returns a new marathon cluster func newCluster(client *httpClient, marathonURL string, isDCOS bool) (*cluster, error) { // step: extract and basic validate the endpoints var members []*member var defaultProto string for _, endpoint := range strings.Split(marathonURL, ",") { // step: check for nothing if endpoint == "" { return nil, newInvalidEndpointError("endpoint is blank") } // step: prepend scheme if missing on (non-initial) endpoint. if !strings.HasPrefix(endpoint, "http://") && !strings.HasPrefix(endpoint, "https://") { if defaultProto == "" { return nil, newInvalidEndpointError("missing scheme on (first) endpoint") } endpoint = fmt.Sprintf("%s://%s", defaultProto, endpoint) } // step: parse the url u, err := url.Parse(endpoint) if err != nil { return nil, newInvalidEndpointError("invalid endpoint '%s': %s", endpoint, err) } if defaultProto == "" { defaultProto = u.Scheme } // step: check for empty hosts if u.Host == "" { return nil, newInvalidEndpointError("endpoint: %s must have a host", endpoint) } // step: if DCOS is set and no path is given, set the default DCOS path. // done in order to maintain compatibility with automatic addition of the // default DCOS path. if isDCOS && strings.TrimLeft(u.Path, "/") == "" { u.Path = defaultDCOSPath } // step: create a new node for this endpoint members = append(members, &member{endpoint: u.String()}) } return &cluster{ client: client, members: members, healthCheckInterval: 5 * time.Second, }, nil } // retrieve the current member, i.e. the current endpoint in use func (c *cluster) getMember() (string, error) { c.RLock() defer c.RUnlock() for _, n := range c.members { if n.status == memberStatusUp { return n.endpoint, nil } } return "", ErrMarathonDown } // markDown marks down the current endpoint func (c *cluster) markDown(endpoint string) { c.Lock() defer c.Unlock() for _, n := range c.members { // step: check if this is the node and it's marked as up - The double checking on the // nodes status ensures the multiple calls don't create multiple checks if n.status == memberStatusUp && n.endpoint == endpoint { n.status = memberStatusDown go c.healthCheckNode(n) break } } } // healthCheckNode performs a health check on the node and when active updates the status func (c *cluster) healthCheckNode(node *member) { // step: wait for the node to become active ... we are assuming a /ping is enough here ticker := time.NewTicker(c.healthCheckInterval) defer ticker.Stop() for range ticker.C { req, err := c.client.buildMarathonRequest("GET", node.endpoint, "ping", nil) if err == nil { res, err := c.client.Do(req) if err == nil && res.StatusCode == 200 { // step: mark the node as active again c.Lock() node.status = memberStatusUp c.Unlock() break } } } } // activeMembers returns a list of active members func (c *cluster) activeMembers() []string { return c.membersList(memberStatusUp) } // nonActiveMembers returns a list of non-active members in the cluster func (c *cluster) nonActiveMembers() []string { return c.membersList(memberStatusDown) } // memberList returns a list of members of a specified status func (c *cluster) membersList(status memberStatus) []string { c.RLock() defer c.RUnlock() var list []string for _, m := range c.members { if m.status == status { list = append(list, m.endpoint) } } return list } // size returns the size of the cluster func (c *cluster) size() int { return len(c.members) } // String returns a string representation func (m member) String() string { status := "UP" if m.status == memberStatusDown { status = "DOWN" } return fmt.Sprintf("member: %s:%s", m.endpoint, status) } ================================================ FILE: cluster_test.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSize(t *testing.T) { cluster, err := newStandardCluster(fakeMarathonURL) assert.NoError(t, err) assert.Equal(t, cluster.size(), 3) } func TestActive(t *testing.T) { cluster, err := newStandardCluster(fakeMarathonURL) assert.NoError(t, err) assert.Equal(t, len(cluster.activeMembers()), 3) } func TestNonActive(t *testing.T) { cluster, err := newStandardCluster(fakeMarathonURL) assert.NoError(t, err) assert.Equal(t, len(cluster.nonActiveMembers()), 0) } func TestGetMember(t *testing.T) { cases := []struct { isDCOS bool MarathonURL string member string }{ { isDCOS: false, MarathonURL: fakeMarathonURL, member: "http://127.0.0.1:3000", }, { isDCOS: false, MarathonURL: fakeMarathonURLWithPath, member: "http://127.0.0.1:3000/path", }, { isDCOS: true, MarathonURL: fakeMarathonURL, member: "http://127.0.0.1:3000/marathon", }, { isDCOS: true, MarathonURL: fakeMarathonURLWithPath, member: "http://127.0.0.1:3000/path", }, } for _, x := range cases { cluster, err := newCluster(&httpClient{config: Config{HTTPClient: defaultHTTPClient}}, x.MarathonURL, x.isDCOS) assert.NoError(t, err) member, err := cluster.getMember() assert.NoError(t, err) assert.Equal(t, member, x.member) } } func TestMarkDown(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() cluster, err := newStandardCluster(endpoint.URL) require.NoError(t, err) require.Equal(t, len(cluster.activeMembers()), 3) cluster.healthCheckInterval = 2500 * time.Millisecond members := cluster.activeMembers() cluster.markDown(members[0]) cluster.markDown(members[1]) require.Equal(t, len(cluster.activeMembers()), 1) ticker := time.NewTicker(250 * time.Millisecond) defer ticker.Stop() timeout := time.NewTimer(5 * time.Second) defer timeout.Stop() var numFoundMembers int for { numFoundMembers = len(cluster.activeMembers()) if numFoundMembers == 3 { break } select { case <-ticker.C: continue case <-timeout.C: t.Fatalf("found %d active member(s), want 3", numFoundMembers) } } } func TestValidClusterHosts(t *testing.T) { cs := []struct { URL string Expect []string }{ { URL: "http://127.0.0.1", Expect: []string{"http://127.0.0.1"}, }, { URL: "http://127.0.0.1:8080", Expect: []string{"http://127.0.0.1:8080"}, }, { URL: "http://127.0.0.1:8080,http://127.0.0.2:8081", Expect: []string{"http://127.0.0.1:8080", "http://127.0.0.2:8081"}, }, { URL: "https://127.0.0.1:8080,http://127.0.0.2:8081", Expect: []string{"https://127.0.0.1:8080", "http://127.0.0.2:8081"}, }, { URL: "http://127.0.0.1:8080,127.0.0.2", Expect: []string{"http://127.0.0.1:8080", "http://127.0.0.2"}, }, { URL: "https://127.0.0.1:8080,127.0.0.2", Expect: []string{"https://127.0.0.1:8080", "https://127.0.0.2"}, }, { URL: "http://127.0.0.1:8080,127.0.0.2:8080", Expect: []string{"http://127.0.0.1:8080", "http://127.0.0.2:8080"}, }, { URL: "http://127.0.0.1:8080,https://127.0.0.2", Expect: []string{"http://127.0.0.1:8080", "https://127.0.0.2"}, }, { URL: "http://127.0.0.1:8080,https://127.0.0.2:8080", Expect: []string{"http://127.0.0.1:8080", "https://127.0.0.2:8080"}, }, { URL: "http://127.0.0.1:8080/path1,127.0.0.2/path2", Expect: []string{"http://127.0.0.1:8080/path1", "http://127.0.0.2/path2"}, }, } for _, x := range cs { c, err := newStandardCluster(x.URL) if !assert.NoError(t, err, "URL '%s' should not have thrown an error: %s", x.URL, err) { continue } assert.Equal(t, x.Expect, c.activeMembers(), "URL '%s', expected: %v, got: %s", x.URL, x.Expect, c.activeMembers()) } } func TestInvalidClusterHosts(t *testing.T) { for _, invalidHost := range []string{ "", "://", "http://", "http://,,", "http://%42", "http://,127.0.0.1:3000,127.0.0.1:3000", "http://127.0.0.1:3000,,127.0.0.1:3000", "http://127.0.0.1:3000,127.0.0.1:3000,", "foo://127.0.0.1:3000", } { _, err := newStandardCluster(invalidHost) if !assert.Error(t, err) { t.Errorf("undetected invalid host: %s", invalidHost) } } } func newStandardCluster(url string) (*cluster, error) { return newCluster(&httpClient{config: Config{HTTPClient: defaultHTTPClient}}, url, false) } ================================================ FILE: config.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "io" "io/ioutil" "net/http" "time" ) const defaultPollingWaitTime = 500 * time.Millisecond const defaultDCOSPath = "marathon" // EventsTransport describes which transport should be used to deliver Marathon events type EventsTransport int // Config holds the settings and options for the client type Config struct { // URL is the url for marathon URL string // EventsTransport is the events transport: EventsTransportCallback or EventsTransportSSE EventsTransport EventsTransport // EventsPort is the event handler port EventsPort int // the interface we should be listening on for events EventsInterface string // HTTPBasicAuthUser is the http basic auth HTTPBasicAuthUser string // HTTPBasicPassword is the http basic password HTTPBasicPassword string // CallbackURL custom callback url CallbackURL string // DCOSToken for DCOS environment, This will override the Authorization header DCOSToken string // LogOutput the output for debug log messages LogOutput io.Writer // HTTPClient is the HTTP client HTTPClient *http.Client // HTTPSSEClient is the HTTP client used for SSE subscriptions, can't have client.Timeout set HTTPSSEClient *http.Client // wait time (in milliseconds) between repetitive requests to the API during polling PollingWaitTime time.Duration } // NewDefaultConfig create a default client config func NewDefaultConfig() Config { return Config{ URL: "http://127.0.0.1:8080", EventsTransport: EventsTransportCallback, EventsPort: 10001, EventsInterface: "eth0", LogOutput: ioutil.Discard, PollingWaitTime: defaultPollingWaitTime, } } ================================================ FILE: const.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon const ( defaultEventsURL = "/event" /* --- api related constants --- */ marathonAPIVersion = "v2" marathonAPIEventStream = marathonAPIVersion + "/events" marathonAPISubscription = marathonAPIVersion + "/eventSubscriptions" marathonAPIApps = marathonAPIVersion + "/apps" marathonAPIPods = marathonAPIVersion + "/pods" marathonAPITasks = marathonAPIVersion + "/tasks" marathonAPIDeployments = marathonAPIVersion + "/deployments" marathonAPIGroups = marathonAPIVersion + "/groups" marathonAPIQueue = marathonAPIVersion + "/queue" marathonAPIInfo = marathonAPIVersion + "/info" marathonAPILeader = marathonAPIVersion + "/leader" marathonAPIPing = "ping" ) const ( // EventsTransportCallback activates callback events transport EventsTransportCallback EventsTransport = 1 << iota // EventsTransportSSE activates stream events transport EventsTransportSSE ) ================================================ FILE: deployment.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "encoding/json" "fmt" "time" ) // Deployment is a marathon deployment definition type Deployment struct { ID string `json:"id"` Version string `json:"version"` CurrentStep int `json:"currentStep"` TotalSteps int `json:"totalSteps"` AffectedApps []string `json:"affectedApps"` AffectedPods []string `json:"affectedPods"` Steps [][]*DeploymentStep `json:"-"` XXStepsRaw json.RawMessage `json:"steps"` // Holds raw steps JSON to unmarshal later CurrentActions []*DeploymentStep `json:"currentActions"` } // DeploymentID is the identifier for a application deployment type DeploymentID struct { DeploymentID string `json:"deploymentId"` Version string `json:"version"` } // DeploymentStep is a step in the application deployment plan type DeploymentStep struct { Action string `json:"action"` App string `json:"app"` ReadinessCheckResults *[]ReadinessCheckResult `json:"readinessCheckResults,omitempty"` } // StepActions is a series of deployment steps type StepActions struct { Actions []struct { Action string `json:"action"` // 1.1.2 and after Type string `json:"type"` // 1.1.1 and before App string `json:"app"` } } // DeploymentPlan is a collection of steps for application deployment type DeploymentPlan struct { ID string `json:"id"` Version string `json:"version"` Original *Group `json:"original"` Target *Group `json:"target"` Steps []*StepActions `json:"steps"` } // Deployments retrieves a list of current deployments func (r *marathonClient) Deployments() ([]*Deployment, error) { var deployments []*Deployment err := r.apiGet(marathonAPIDeployments, nil, &deployments) if err != nil { return nil, err } // Allows loading of deployment steps from the Marathon v1.X API // Implements a fix for issue https://github.com/gambol99/go-marathon/issues/153 for _, deployment := range deployments { // Unmarshal pre-v1.X step if err := json.Unmarshal(deployment.XXStepsRaw, &deployment.Steps); err != nil { deployment.Steps = make([][]*DeploymentStep, 0) var steps []*StepActions // Unmarshal v1.X Marathon step if err := json.Unmarshal(deployment.XXStepsRaw, &steps); err != nil { return nil, err } for stepIndex, step := range steps { deployment.Steps = append(deployment.Steps, make([]*DeploymentStep, len(step.Actions))) for actionIndex, action := range step.Actions { var stepAction string if action.Type != "" { stepAction = action.Type } else { stepAction = action.Action } deployment.Steps[stepIndex][actionIndex] = &DeploymentStep{ Action: stepAction, App: action.App, } } } } } return deployments, nil } // DeleteDeployment delete a current deployment from marathon // id: the deployment id you wish to delete // force: whether or not to force the deletion func (r *marathonClient) DeleteDeployment(id string, force bool) (*DeploymentID, error) { path := fmt.Sprintf("%s/%s", marathonAPIDeployments, id) // if force=true, no body is returned if force { path += "?force=true" return nil, r.apiDelete(path, nil, nil) } deployment := new(DeploymentID) err := r.apiDelete(path, nil, deployment) if err != nil { return nil, err } return deployment, nil } // HasDeployment checks to see if a deployment exists // id: the deployment id you are looking for func (r *marathonClient) HasDeployment(id string) (bool, error) { deployments, err := r.Deployments() if err != nil { return false, err } for _, deployment := range deployments { if deployment.ID == id { return true, nil } } return false, nil } // WaitOnDeployment waits on a deployment to finish // version: the version of the application // timeout: the timeout to wait for the deployment to take, otherwise return an error func (r *marathonClient) WaitOnDeployment(id string, timeout time.Duration) error { if found, err := r.HasDeployment(id); err != nil { return err } else if !found { return nil } nowTime := time.Now() stopTime := nowTime.Add(timeout) if timeout <= 0 { stopTime = nowTime.Add(time.Duration(900) * time.Second) } // step: a somewhat naive implementation, but it will work for { if time.Now().After(stopTime) { return ErrTimeoutError } found, err := r.HasDeployment(id) if err != nil { return err } if !found { return nil } time.Sleep(r.config.PollingWaitTime) } } ================================================ FILE: deployment_test.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestDeployments(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() deployments, err := endpoint.Client.Deployments() require.NoError(t, err) require.NotNil(t, deployments) require.Equal(t, len(deployments), 1) deployment := deployments[0] require.NotNil(t, deployment) assert.Equal(t, deployment.ID, "867ed450-f6a8-4d33-9b0e-e11c5513990b") require.NotNil(t, deployment.Steps) assert.Equal(t, len(deployment.Steps), 1) } func TestDeploymentsV1(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, &configContainer{ server: &serverConfig{ scope: "v1.1.1", }, }) defer endpoint.Close() deployments, err := endpoint.Client.Deployments() assert.NoError(t, err) assert.NotNil(t, deployments) assert.Equal(t, len(deployments), 1) deployment := deployments[0] assert.NotNil(t, deployment) assert.Equal(t, deployment.ID, "2620aa06-1001-4eea-8861-a51957d4fd80") assert.NotNil(t, deployment.Steps) assert.Equal(t, len(deployment.Steps), 2) require.Equal(t, len(deployment.CurrentActions), 1) curAction := deployment.CurrentActions[0] require.NotNil(t, curAction) require.NotNil(t, curAction.ReadinessCheckResults) require.True(t, len(*curAction.ReadinessCheckResults) > 0) actualRes := (*curAction.ReadinessCheckResults)[0] expectedRes := ReadinessCheckResult{ Name: "myReadyCheck", TaskID: "test_frontend_app1.c9de6033", Ready: false, LastResponse: ReadinessLastResponse{ Body: "{}", ContentType: "application/json", Status: 500, }, } assert.Equal(t, expectedRes, actualRes) } func TestDeleteDeployment(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() id, err := endpoint.Client.DeleteDeployment(fakeDeploymentID, false) require.NoError(t, err) assert.Equal(t, id.DeploymentID, "0b1467fc-d5cd-4bbc-bac2-2805351cee1e") assert.Equal(t, id.Version, "2014-08-26T08:20:26.171Z") } func TestDeleteDeploymentForce(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() resp, err := endpoint.Client.DeleteDeployment(fakeDeploymentID, true) require.NoError(t, err) assert.Nil(t, resp) } ================================================ FILE: docker.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "errors" "fmt" ) // Container is the definition for a container type in marathon type Container struct { Type string `json:"type,omitempty"` Docker *Docker `json:"docker,omitempty"` Volumes *[]Volume `json:"volumes,omitempty"` PortMappings *[]PortMapping `json:"portMappings,omitempty"` } // PortMapping is the portmapping structure between container and mesos type PortMapping struct { ContainerPort int `json:"containerPort,omitempty"` HostPort int `json:"hostPort"` Labels *map[string]string `json:"labels,omitempty"` Name string `json:"name,omitempty"` ServicePort int `json:"servicePort,omitempty"` Protocol string `json:"protocol,omitempty"` NetworkNames *[]string `json:"networkNames,omitempty"` } // Parameters is the parameters to pass to the docker client when creating the container type Parameters struct { Key string `json:"key,omitempty"` Value string `json:"value,omitempty"` } // Volume is the docker volume details associated to the container type Volume struct { ContainerPath string `json:"containerPath,omitempty"` HostPath string `json:"hostPath,omitempty"` External *ExternalVolume `json:"external,omitempty"` Mode string `json:"mode,omitempty"` Persistent *PersistentVolume `json:"persistent,omitempty"` Secret string `json:"secret,omitempty"` } // PersistentVolumeType is the a persistent docker volume to be mounted type PersistentVolumeType string const ( // PersistentVolumeTypeRoot is the root path of the persistent volume PersistentVolumeTypeRoot PersistentVolumeType = "root" // PersistentVolumeTypePath is the mount path of the persistent volume PersistentVolumeTypePath PersistentVolumeType = "path" // PersistentVolumeTypeMount is the mount type of the persistent volume PersistentVolumeTypeMount PersistentVolumeType = "mount" ) // PersistentVolume declares a Volume to be Persistent, and sets // the size (in MiB) and optional type, max size (MiB) and constraints for the Volume. type PersistentVolume struct { Type PersistentVolumeType `json:"type,omitempty"` Size int `json:"size"` MaxSize int `json:"maxSize,omitempty"` Constraints *[][]string `json:"constraints,omitempty"` } // SetType sets the type of mesos disk resource to use // type: PersistentVolumeType enum func (p *PersistentVolume) SetType(tp PersistentVolumeType) *PersistentVolume { p.Type = tp return p } // SetSize sets size of the persistent volume // size: size in MiB func (p *PersistentVolume) SetSize(size int) *PersistentVolume { p.Size = size return p } // SetMaxSize sets maximum size of an exclusive mount-disk resource to consider; // does not apply to root or path disk resource types // maxSize: size in MiB func (p *PersistentVolume) SetMaxSize(maxSize int) *PersistentVolume { p.MaxSize = maxSize return p } // AddConstraint adds a new constraint // constraints: the constraint definition, one constraint per array element func (p *PersistentVolume) AddConstraint(constraints ...string) *PersistentVolume { if p.Constraints == nil { p.EmptyConstraints() } c := *p.Constraints c = append(c, constraints) p.Constraints = &c return p } // EmptyConstraints explicitly empties constraints -- use this if you need to empty // constraints of an application that already has constraints set (setting constraints to nil will // keep the current value) func (p *PersistentVolume) EmptyConstraints() *PersistentVolume { p.Constraints = &[][]string{} return p } // ExternalVolume is an external volume definition type ExternalVolume struct { Name string `json:"name,omitempty"` Provider string `json:"provider,omitempty"` Options *map[string]string `json:"options,omitempty"` } // PullConfig specifies a secret for authentication with a private Docker registry type PullConfig struct { Secret string `json:"secret,omitempty"` } // NewPullConfig creats a *PullConfig based on a given secret func NewPullConfig(secret string) *PullConfig { return &PullConfig{ Secret: secret, } } // Docker is the docker definition from a marathon application type Docker struct { ForcePullImage *bool `json:"forcePullImage,omitempty"` Image string `json:"image,omitempty"` Network string `json:"network,omitempty"` Parameters *[]Parameters `json:"parameters,omitempty"` PortMappings *[]PortMapping `json:"portMappings,omitempty"` Privileged *bool `json:"privileged,omitempty"` PullConfig *PullConfig `json:"pullConfig,omitempty"` } // Volume attachs a volume to the container // host_path: the path on the docker host to map // container_path: the path inside the container to map the host volume // mode: the mode to map the container func (container *Container) Volume(hostPath, containerPath, mode string) *Container { if container.Volumes == nil { container.EmptyVolumes() } volumes := *container.Volumes volumes = append(volumes, Volume{ ContainerPath: containerPath, HostPath: hostPath, Mode: mode, }) container.Volumes = &volumes return container } // EmptyVolumes explicitly empties the volumes -- use this if you need to empty // volumes of an application that already has volumes set (setting volumes to nil will // keep the current value) func (container *Container) EmptyVolumes() *Container { container.Volumes = &[]Volume{} return container } // SetPersistentVolume defines persistent properties for volume func (v *Volume) SetPersistentVolume() *PersistentVolume { ev := &PersistentVolume{} v.Persistent = ev return ev } // SetSecretVolume defines secret and containerPath for volume func (v *Volume) SetSecretVolume(containerPath, secret string) *Volume { v.ContainerPath = containerPath v.Secret = secret return v } // EmptyPersistentVolume empties the persistent volume definition func (v *Volume) EmptyPersistentVolume() *Volume { v.Persistent = &PersistentVolume{} return v } // SetExternalVolume define external elements for a volume // name: the name of the volume // provider: the provider of the volume (e.g. dvdi) func (v *Volume) SetExternalVolume(name, provider string) *ExternalVolume { ev := &ExternalVolume{ Name: name, Provider: provider, } v.External = ev return ev } // EmptyExternalVolume emptys the external volume definition func (v *Volume) EmptyExternalVolume() *Volume { v.External = &ExternalVolume{} return v } // AddOption adds an option to an ExternalVolume // name: the name of the option // value: value for the option func (ev *ExternalVolume) AddOption(name, value string) *ExternalVolume { if ev.Options == nil { ev.EmptyOptions() } (*ev.Options)[name] = value return ev } // EmptyOptions explicitly empties the options func (ev *ExternalVolume) EmptyOptions() *ExternalVolume { ev.Options = &map[string]string{} return ev } // NewDockerContainer creates a default docker container for you func NewDockerContainer() *Container { container := &Container{} container.Type = "DOCKER" container.Docker = &Docker{} return container } // SetForcePullImage sets whether the docker image should always be force pulled before // starting an instance // forcePull: true / false func (docker *Docker) SetForcePullImage(forcePull bool) *Docker { docker.ForcePullImage = &forcePull return docker } // SetPrivileged sets whether the docker image should be started // with privilege turned on // priv: true / false func (docker *Docker) SetPrivileged(priv bool) *Docker { docker.Privileged = &priv return docker } // Container sets the image of the container // image: the image name you are using func (docker *Docker) Container(image string) *Docker { docker.Image = image return docker } // Bridged sets the networking mode to bridged func (docker *Docker) Bridged() *Docker { docker.Network = "BRIDGE" return docker } // Host sets the networking mode to host func (docker *Docker) Host() *Docker { docker.Network = "HOST" return docker } // Expose sets the container to expose the following TCP ports // ports: the TCP ports the container is exposing func (container *Container) Expose(ports ...int) *Container { for _, port := range ports { container.ExposePort(PortMapping{ ContainerPort: port, HostPort: 0, ServicePort: 0, Protocol: "tcp"}) } return container } // Expose sets the container to expose the following TCP ports // ports: the TCP ports the container is exposing func (docker *Docker) Expose(ports ...int) *Docker { for _, port := range ports { docker.ExposePort(PortMapping{ ContainerPort: port, HostPort: 0, ServicePort: 0, Protocol: "tcp"}) } return docker } // ExposeUDP sets the container to expose the following UDP ports // ports: the UDP ports the container is exposing func (container *Container) ExposeUDP(ports ...int) *Container { for _, port := range ports { container.ExposePort(PortMapping{ ContainerPort: port, HostPort: 0, ServicePort: 0, Protocol: "udp"}) } return container } // ExposeUDP sets the container to expose the following UDP ports // ports: the UDP ports the container is exposing func (docker *Docker) ExposeUDP(ports ...int) *Docker { for _, port := range ports { docker.ExposePort(PortMapping{ ContainerPort: port, HostPort: 0, ServicePort: 0, Protocol: "udp"}) } return docker } // ExposePort exposes an port in the container func (container *Container) ExposePort(portMapping PortMapping) *Container { if container.PortMappings == nil { container.EmptyPortMappings() } portMappings := *container.PortMappings portMappings = append(portMappings, portMapping) container.PortMappings = &portMappings return container } // ExposePort exposes an port in the container func (docker *Docker) ExposePort(portMapping PortMapping) *Docker { if docker.PortMappings == nil { docker.EmptyPortMappings() } portMappings := *docker.PortMappings portMappings = append(portMappings, portMapping) docker.PortMappings = &portMappings return docker } // EmptyPortMappings explicitly empties the port mappings -- use this if you need to empty // port mappings of an application that already has port mappings set (setting port mappings to nil will // keep the current value) func (container *Container) EmptyPortMappings() *Container { container.PortMappings = &[]PortMapping{} return container } // EmptyPortMappings explicitly empties the port mappings -- use this if you need to empty // port mappings of an application that already has port mappings set (setting port mappings to nil will // keep the current value) func (docker *Docker) EmptyPortMappings() *Docker { docker.PortMappings = &[]PortMapping{} return docker } // AddLabel adds a label to a PortMapping // name: the name of the label // value: value for this label func (p *PortMapping) AddLabel(name, value string) *PortMapping { if p.Labels == nil { p.EmptyLabels() } (*p.Labels)[name] = value return p } // EmptyLabels explicitly empties the labels -- use this if you need to empty // the labels of a port mapping that already has labels set (setting labels to // nil will keep the current value) func (p *PortMapping) EmptyLabels() *PortMapping { p.Labels = &map[string]string{} return p } // AddParameter adds a parameter to the docker execution line when creating the container // key: the name of the option to add // value: the value of the option func (docker *Docker) AddParameter(key string, value string) *Docker { if docker.Parameters == nil { docker.EmptyParameters() } parameters := *docker.Parameters parameters = append(parameters, Parameters{ Key: key, Value: value}) docker.Parameters = ¶meters return docker } // EmptyParameters explicitly empties the parameters -- use this if you need to empty // parameters of an application that already has parameters set (setting parameters to nil will // keep the current value) func (docker *Docker) EmptyParameters() *Docker { docker.Parameters = &[]Parameters{} return docker } // ServicePortIndex finds the service port index of the exposed port // port: the port you are looking for func (container *Container) ServicePortIndex(port int) (int, error) { if container.PortMappings == nil || len(*container.PortMappings) == 0 { return 0, errors.New("The container does not contain any port mappings to search") } // step: iterate and find the port for index, containerPort := range *container.PortMappings { if containerPort.ContainerPort == port { return index, nil } } // step: we didn't find the port in the mappings return 0, fmt.Errorf("The container port %d was not found in the container port mappings", port) } // ServicePortIndex finds the service port index of the exposed port // port: the port you are looking for func (docker *Docker) ServicePortIndex(port int) (int, error) { if docker.PortMappings == nil || len(*docker.PortMappings) == 0 { return 0, errors.New("The docker does not contain any port mappings to search") } // step: iterate and find the port for index, containerPort := range *docker.PortMappings { if containerPort.ContainerPort == port { return index, nil } } // step: we didn't find the port in the mappings return 0, fmt.Errorf("The docker port %d was not found in the container port mappings", port) } // SetPullConfig adds *PullConfig to Docker func (docker *Docker) SetPullConfig(pullConfig *PullConfig) *Docker { docker.PullConfig = pullConfig return docker } // AddNetwork adds a network name to a PortMapping // name: the name of the network func (p *PortMapping) AddNetwork(name string) *PortMapping { if p.NetworkNames == nil { p.EmptyNetworkNames() } networks := *p.NetworkNames networks = append(networks, name) p.NetworkNames = &networks return p } // EmptyNetworkNames explicitly empties the network names -- use this if you need to empty // the network names of a port mapping that already has network names set func (p *PortMapping) EmptyNetworkNames() *PortMapping { p.NetworkNames = &[]string{} return p } ================================================ FILE: docker_test.go ================================================ /* Copyright 2015 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func createPortMapping(containerPort int, protocol string) *PortMapping { return &PortMapping{ ContainerPort: containerPort, HostPort: 0, ServicePort: 0, Protocol: protocol, } } func TestDockerAddParameter(t *testing.T) { docker := NewDockerApplication().Container.Docker docker.AddParameter("k1", "v1").AddParameter("k2", "v2") assert.Equal(t, 2, len(*docker.Parameters)) assert.Equal(t, (*docker.Parameters)[0].Key, "k1") assert.Equal(t, (*docker.Parameters)[0].Value, "v1") assert.Equal(t, (*docker.Parameters)[1].Key, "k2") assert.Equal(t, (*docker.Parameters)[1].Value, "v2") docker.EmptyParameters() assert.NotNil(t, docker.Parameters) assert.Equal(t, 0, len(*docker.Parameters)) } func TestDockerExpose(t *testing.T) { apps := []*Application{ NewDockerApplication(), NewDockerApplication(), } // Marathon < 1.5 apps[0].Container.Docker.Expose(8080).Expose(80, 443) // Marathon >= 1.5 apps[1].Container.Expose(8080).Expose(80, 443) portMappings := []*[]PortMapping{ apps[0].Container.Docker.PortMappings, apps[1].Container.PortMappings, } for _, portMapping := range portMappings { assert.Equal(t, 3, len(*portMapping)) assert.Equal(t, *createPortMapping(8080, "tcp"), (*portMapping)[0]) assert.Equal(t, *createPortMapping(80, "tcp"), (*portMapping)[1]) assert.Equal(t, *createPortMapping(443, "tcp"), (*portMapping)[2]) } } func TestDockerExposeUDP(t *testing.T) { apps := []*Application{ NewDockerApplication(), NewDockerApplication(), } // Marathon < 1.5 apps[0].Container.Docker.ExposeUDP(53).ExposeUDP(5060, 6881) // Marathon >= 1.5 apps[1].Container.ExposeUDP(53).ExposeUDP(5060, 6881) portMappings := []*[]PortMapping{ apps[0].Container.Docker.PortMappings, apps[1].Container.PortMappings, } for _, portMapping := range portMappings { assert.Equal(t, 3, len(*portMapping)) assert.Equal(t, *createPortMapping(53, "udp"), (*portMapping)[0]) assert.Equal(t, *createPortMapping(5060, "udp"), (*portMapping)[1]) assert.Equal(t, *createPortMapping(6881, "udp"), (*portMapping)[2]) } } func TestPortMappingLabels(t *testing.T) { pm := createPortMapping(80, "tcp") pm.AddLabel("hello", "world").AddLabel("foo", "bar") assert.Equal(t, 2, len(*pm.Labels)) assert.Equal(t, "world", (*pm.Labels)["hello"]) assert.Equal(t, "bar", (*pm.Labels)["foo"]) pm.EmptyLabels() assert.NotNil(t, pm.Labels) assert.Equal(t, 0, len(*pm.Labels)) } func TestPortMappingNetworkNames(t *testing.T) { pm := createPortMapping(80, "tcp") pm.AddNetwork("test") assert.Equal(t, 1, len(*pm.NetworkNames)) assert.Equal(t, "test", (*pm.NetworkNames)[0]) pm.EmptyNetworkNames() assert.NotNil(t, pm.NetworkNames) assert.Equal(t, 0, len(*pm.NetworkNames)) } func TestVolume(t *testing.T) { container := NewDockerApplication().Container container.Volume("hp1", "cp1", "RW") container.Volume("hp2", "cp2", "R") assert.Equal(t, 2, len(*container.Volumes)) assert.Equal(t, (*container.Volumes)[0].HostPath, "hp1") assert.Equal(t, (*container.Volumes)[0].ContainerPath, "cp1") assert.Equal(t, (*container.Volumes)[0].Mode, "RW") assert.Equal(t, (*container.Volumes)[1].HostPath, "hp2") assert.Equal(t, (*container.Volumes)[1].ContainerPath, "cp2") assert.Equal(t, (*container.Volumes)[1].Mode, "R") } func TestSecretVolume(t *testing.T) { container := NewDockerApplication().Container container.Volume("", "oldPath", "") sv1 := (*container.Volumes)[0] assert.Equal(t, sv1.ContainerPath, "oldPath") sv1.SetSecretVolume("newPath", "some-secret") assert.Equal(t, sv1.ContainerPath, "newPath") assert.Equal(t, sv1.Secret, "some-secret") } func TestExternalVolume(t *testing.T) { container := NewDockerApplication().Container container.Volume("", "cp", "RW") ev := (*container.Volumes)[0].SetExternalVolume("myVolume", "dvdi") ev.AddOption("prop", "pval") ev.AddOption("dvdi", "rexray") ev1 := (*container.Volumes)[0].External assert.Equal(t, ev1.Name, "myVolume") assert.Equal(t, ev1.Provider, "dvdi") if assert.Equal(t, len(*ev1.Options), 2) { assert.Equal(t, (*ev1.Options)["dvdi"], "rexray") assert.Equal(t, (*ev1.Options)["prop"], "pval") } // empty the external volume again (*container.Volumes)[0].EmptyExternalVolume() ev2 := (*container.Volumes)[0].External assert.Equal(t, ev2.Name, "") assert.Equal(t, ev2.Provider, "") } func TestDockerPersistentVolume(t *testing.T) { docker := NewDockerApplication() container := docker.Container.Volume("/host", "/container", "RW") require.Equal(t, 1, len(*docker.Container.Volumes)) pVol := (*container.Volumes)[0].SetPersistentVolume() pVol.SetType(PersistentVolumeTypeMount) pVol.SetSize(256) pVol.SetMaxSize(128) pVol.AddConstraint("cons1", "EQUAL", "tag1") pVol.AddConstraint("cons2", "UNIQUE") assert.Equal(t, 256, pVol.Size) assert.Equal(t, PersistentVolumeTypeMount, pVol.Type) assert.Equal(t, 128, pVol.MaxSize) if assert.NotNil(t, pVol.Constraints) { constraints := *pVol.Constraints require.Equal(t, 2, len(constraints)) assert.Equal(t, []string{"cons1", "EQUAL", "tag1"}, constraints[0]) assert.Equal(t, []string{"cons2", "UNIQUE"}, constraints[1]) } pVol.EmptyConstraints() if assert.NotNil(t, pVol.Constraints) { assert.Empty(t, len(*pVol.Constraints)) } } func TestDockerPullConfig(t *testing.T) { secretName := "mysecret1" app := NewDockerApplication() pullConfig := NewPullConfig(secretName) app.Container.Docker.SetPullConfig(pullConfig) if assert.NotNil(t, app.Container.Docker.PullConfig) { assert.Equal(t, secretName, app.Container.Docker.PullConfig.Secret) } } ================================================ FILE: error.go ================================================ /* Copyright 2015 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "encoding/json" "fmt" "net/http" "strings" ) const ( // ErrCodeBadRequest specifies a 400 Bad Request error. ErrCodeBadRequest = iota // ErrCodeUnauthorized specifies a 401 Unauthorized error. ErrCodeUnauthorized // ErrCodeForbidden specifies a 403 Forbidden error. ErrCodeForbidden // ErrCodeNotFound specifies a 404 Not Found error. ErrCodeNotFound // ErrCodeDuplicateID specifies a PUT 409 Conflict error. ErrCodeDuplicateID // ErrCodeAppLocked specifies a POST 409 Conflict error. ErrCodeAppLocked // ErrCodeInvalidBean specifies a 422 UnprocessableEntity error. ErrCodeInvalidBean // ErrCodeServer specifies a 500+ Server error. ErrCodeServer // ErrCodeUnknown specifies an unknown error. ErrCodeUnknown // ErrCodeMethodNotAllowed specifies a 405 Method Not Allowed. ErrCodeMethodNotAllowed ) // InvalidEndpointError indicates a endpoint error in the marathon urls type InvalidEndpointError struct { message string } // Error returns the string message func (e *InvalidEndpointError) Error() string { return e.message } // newInvalidEndpointError creates a new error func newInvalidEndpointError(message string, args ...interface{}) error { return &InvalidEndpointError{message: fmt.Sprintf(message, args...)} } // APIError represents a generic API error. type APIError struct { // ErrCode specifies the nature of the error. ErrCode int message string } func (e *APIError) Error() string { return fmt.Sprintf("Marathon API error: %s", e.message) } // NewAPIError creates a new APIError instance from the given response code and content. func NewAPIError(code int, content []byte) error { var errDef errorDefinition switch { case code == http.StatusBadRequest: errDef = &badRequestDef{} case code == http.StatusUnauthorized: errDef = &simpleErrDef{code: ErrCodeUnauthorized} case code == http.StatusForbidden: errDef = &simpleErrDef{code: ErrCodeForbidden} case code == http.StatusNotFound: errDef = &simpleErrDef{code: ErrCodeNotFound} case code == http.StatusMethodNotAllowed: errDef = &simpleErrDef{code: ErrCodeMethodNotAllowed} case code == http.StatusConflict: errDef = &conflictDef{} case code == 422: errDef = &unprocessableEntityDef{} case code >= http.StatusInternalServerError: errDef = &simpleErrDef{code: ErrCodeServer} default: errDef = &simpleErrDef{code: ErrCodeUnknown} } return parseContent(errDef, content) } type errorDefinition interface { message() string errCode() int } func parseContent(errDef errorDefinition, content []byte) error { // If the content cannot be JSON-unmarshalled, we assume that it's not JSON // and encode it into the APIError instance as-is. errMessage := string(content) if err := json.Unmarshal(content, errDef); err == nil { errMessage = errDef.message() } return &APIError{message: errMessage, ErrCode: errDef.errCode()} } type simpleErrDef struct { Message string `json:"message"` code int } func (def *simpleErrDef) message() string { return def.Message } func (def *simpleErrDef) errCode() int { return def.code } type detailDescription struct { Path string `json:"path"` Errors []string `json:"errors"` } func (d detailDescription) String() string { return fmt.Sprintf("path: '%s' errors: %s", d.Path, strings.Join(d.Errors, ", ")) } type badRequestDef struct { Message string `json:"message"` Details []detailDescription `json:"details"` } func (def *badRequestDef) message() string { var details []string for _, detail := range def.Details { details = append(details, detail.String()) } return fmt.Sprintf("%s (%s)", def.Message, strings.Join(details, "; ")) } func (def *badRequestDef) errCode() int { return ErrCodeBadRequest } type conflictDef struct { Message string `json:"message"` Deployments []struct { ID string `json:"id"` } `json:"deployments"` } func (def *conflictDef) message() string { if len(def.Deployments) == 0 { // 409 Conflict response to "POST /v2/apps". return def.Message } // 409 Conflict response to "PUT /v2/apps/{appId}". var ids []string for _, deployment := range def.Deployments { ids = append(ids, deployment.ID) } return fmt.Sprintf("%s (locking deployment IDs: %s)", def.Message, strings.Join(ids, ", ")) } func (def *conflictDef) errCode() int { if len(def.Deployments) == 0 { return ErrCodeDuplicateID } return ErrCodeAppLocked } type unprocessableEntityDetails []struct { // Used in Marathon >= 1.0.0-RC1. detailDescription // Used in Marathon < 1.0.0-RC1. Attribute string `json:"attribute"` Error string `json:"error"` } type unprocessableEntityDef struct { Message string `json:"message"` // Name used in Marathon >= 0.15.0. Details unprocessableEntityDetails `json:"details"` // Name used in Marathon < 0.15.0. Errors unprocessableEntityDetails `json:"errors"` } func (def *unprocessableEntityDef) message() string { joinDetails := func(details unprocessableEntityDetails) []string { var res []string for _, detail := range details { res = append(res, fmt.Sprintf("attribute '%s': %s", detail.Attribute, detail.Error)) } return res } var details []string switch { case len(def.Errors) > 0: details = joinDetails(def.Errors) case len(def.Details) > 0 && len(def.Details[0].Attribute) > 0: details = joinDetails(def.Details) default: for _, detail := range def.Details { details = append(details, detail.detailDescription.String()) } } return fmt.Sprintf("%s (%s)", def.Message, strings.Join(details, "; ")) } func (def *unprocessableEntityDef) errCode() int { return ErrCodeInvalidBean } ================================================ FILE: error_test.go ================================================ /* Copyright 2015 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "fmt" "net/http" "strings" "testing" "github.com/stretchr/testify/assert" ) func TestErrors(t *testing.T) { tests := []struct { httpCode int nameSuffix string errCode int errText string content string }{ // 400 { httpCode: http.StatusBadRequest, errCode: ErrCodeBadRequest, errText: "Invalid JSON (path: '/id' errors: error.expected.jsstring, error.something.else; path: '/name' errors: error.not.inventive)", content: content400(), }, // 401 { httpCode: http.StatusUnauthorized, errCode: ErrCodeUnauthorized, errText: "invalid username or password.", content: `{"message": "invalid username or password."}`, }, // 403 { httpCode: http.StatusForbidden, errCode: ErrCodeForbidden, errText: "Not Authorized to perform this action!", content: `{"message": "Not Authorized to perform this action!"}`, }, // 404 { httpCode: http.StatusNotFound, errCode: ErrCodeNotFound, errText: "App '/not_existent' does not exist", content: `{"message": "App '/not_existent' does not exist"}`, }, // 405 { httpCode: http.StatusMethodNotAllowed, errCode: ErrCodeMethodNotAllowed, errText: "", content: `{"message": null}`, }, // 409 POST { httpCode: http.StatusConflict, nameSuffix: "POST", errCode: ErrCodeDuplicateID, errText: "An app with id [/existing_app] already exists.", content: `{"message": "An app with id [/existing_app] already exists."}`, }, // 409 PUT { httpCode: http.StatusConflict, nameSuffix: "PUT", errCode: ErrCodeAppLocked, errText: "App is locked (locking deployment IDs: 97c136bf-5a28-4821-9d94-480d9fbb01c8)", content: `{"message":"App is locked", "deployments": [ { "id": "97c136bf-5a28-4821-9d94-480d9fbb01c8" } ] }`, }, // 422 pre-1.0 "details" key { httpCode: 422, nameSuffix: "pre-1.0 details key", errCode: ErrCodeInvalidBean, errText: "Something is not valid (attribute 'upgradeStrategy.minimumHealthCapacity': is greater than 1; attribute 'foobar': foo does not have enough bar)", content: content422("details"), }, // 422 pre-1.0 "errors" key { httpCode: 422, nameSuffix: "pre-1.0 errors key", errCode: ErrCodeInvalidBean, errText: "Something is not valid (attribute 'upgradeStrategy.minimumHealthCapacity': is greater than 1; attribute 'foobar': foo does not have enough bar)", content: content422("errors"), }, // 422 1.0 "invalid object" { httpCode: 422, nameSuffix: "invalid object", errCode: ErrCodeInvalidBean, 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)", content: content422V1(), }, // 499 unknown error { httpCode: 499, nameSuffix: "unknown error", errCode: ErrCodeUnknown, errText: "unknown error", content: `{"message": "unknown error"}`, }, // 500 { httpCode: http.StatusInternalServerError, errCode: ErrCodeServer, errText: "internal server error", content: `{"message": "internal server error"}`, }, // 503 (no JSON) { httpCode: http.StatusServiceUnavailable, nameSuffix: "no JSON", errCode: ErrCodeServer, errText: "No server is available to handle this request.", content: `No server is available to handle this request.`, }, } for _, test := range tests { name := fmt.Sprintf("%d", test.httpCode) if len(test.nameSuffix) > 0 { name = fmt.Sprintf("%s (%s)", name, test.nameSuffix) } apiErr := NewAPIError(test.httpCode, []byte(test.content)) gotErrCode := apiErr.(*APIError).ErrCode assert.Equal(t, test.errCode, gotErrCode, fmt.Sprintf("HTTP code %s (error code): got %d, want %d", name, gotErrCode, test.errCode)) pureErrText := strings.TrimPrefix(apiErr.Error(), "Marathon API error: ") assert.Equal(t, pureErrText, test.errText, fmt.Sprintf("HTTP code %s (error text)", name)) } } func content400() string { return `{ "message": "Invalid JSON", "details": [ { "path": "/id", "errors": ["error.expected.jsstring", "error.something.else"] }, { "path": "/name", "errors": ["error.not.inventive"] } ] }` } func content422(detailsPropKey string) string { return fmt.Sprintf(`{ "message": "Something is not valid", "%s": [ { "attribute": "upgradeStrategy.minimumHealthCapacity", "error": "is greater than 1" }, { "attribute": "foobar", "error": "foo does not have enough bar" } ] }`, detailsPropKey) } func content422V1() string { return `{ "message": "Object is not valid", "details": [ { "path": "upgradeStrategy.minimumHealthCapacity", "errors": ["is greater than 1"] }, { "path": "/value", "errors": ["service port conflict app /app1", "service port conflict app /app2"] } ] }` } ================================================ FILE: events.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import "fmt" // EventType is a wrapper for a marathon event type EventType struct { EventType string `json:"eventType"` } const ( // EventIDAPIRequest is the event listener ID for the corresponding event. EventIDAPIRequest = 1 << iota // EventIDStatusUpdate is the event listener ID for the corresponding event. EventIDStatusUpdate // EventIDFrameworkMessage is the event listener ID for the corresponding event. EventIDFrameworkMessage // EventIDSubscription is the event listener ID for the corresponding event. EventIDSubscription // EventIDUnsubscribed is the event listener ID for the corresponding event. EventIDUnsubscribed // EventIDStreamAttached is the event listener ID for the corresponding event. EventIDStreamAttached // EventIDStreamDetached is the event listener ID for the corresponding event. EventIDStreamDetached // EventIDAddHealthCheck is the event listener ID for the corresponding event. EventIDAddHealthCheck // EventIDRemoveHealthCheck is the event listener ID for the corresponding event. EventIDRemoveHealthCheck // EventIDFailedHealthCheck is the event listener ID for the corresponding event. EventIDFailedHealthCheck // EventIDChangedHealthCheck is the event listener ID for the corresponding event. EventIDChangedHealthCheck // EventIDGroupChangeSuccess is the event listener ID for the corresponding event. EventIDGroupChangeSuccess // EventIDGroupChangeFailed is the event listener ID for the corresponding event. EventIDGroupChangeFailed // EventIDDeploymentSuccess is the event listener ID for the corresponding event. EventIDDeploymentSuccess // EventIDDeploymentFailed is the event listener ID for the corresponding event. EventIDDeploymentFailed // EventIDDeploymentInfo is the event listener ID for the corresponding event. EventIDDeploymentInfo // EventIDDeploymentStepSuccess is the event listener ID for the corresponding event. EventIDDeploymentStepSuccess // EventIDDeploymentStepFailed is the event listener ID for the corresponding event. EventIDDeploymentStepFailed // EventIDAppTerminated is the event listener ID for the corresponding event. EventIDAppTerminated //EventIDApplications comprises all listener IDs for application events. EventIDApplications = EventIDStatusUpdate | EventIDChangedHealthCheck | EventIDFailedHealthCheck | EventIDAppTerminated //EventIDSubscriptions comprises all listener IDs for subscription events. EventIDSubscriptions = EventIDSubscription | EventIDUnsubscribed | EventIDStreamAttached | EventIDStreamDetached ) var ( eventTypesMap map[string]int ) func init() { eventTypesMap = map[string]int{ "api_post_event": EventIDAPIRequest, "status_update_event": EventIDStatusUpdate, "framework_message_event": EventIDFrameworkMessage, "subscribe_event": EventIDSubscription, "unsubscribe_event": EventIDUnsubscribed, "event_stream_attached": EventIDStreamAttached, "event_stream_detached": EventIDStreamDetached, "add_health_check_event": EventIDAddHealthCheck, "remove_health_check_event": EventIDRemoveHealthCheck, "failed_health_check_event": EventIDFailedHealthCheck, "health_status_changed_event": EventIDChangedHealthCheck, "group_change_success": EventIDGroupChangeSuccess, "group_change_failed": EventIDGroupChangeFailed, "deployment_success": EventIDDeploymentSuccess, "deployment_failed": EventIDDeploymentFailed, "deployment_info": EventIDDeploymentInfo, "deployment_step_success": EventIDDeploymentStepSuccess, "deployment_step_failure": EventIDDeploymentStepFailed, "app_terminated_event": EventIDAppTerminated, } } // // Events taken from: https://mesosphere.github.io/marathon/docs/event-bus.html // // Event is the definition for a event in marathon type Event struct { ID int Name string Event interface{} } func (event *Event) String() string { return fmt.Sprintf("type: %s, event: %s", event.Name, event.Event) } // EventsChannel is a channel to receive events upon type EventsChannel chan *Event /* --- API Request --- */ // EventAPIRequest describes an 'api_post_event' event. type EventAPIRequest struct { EventType string `json:"eventType"` ClientIP string `json:"clientIp"` Timestamp string `json:"timestamp"` URI string `json:"uri"` AppDefinition *Application `json:"appDefinition"` } /* --- Status Update --- */ // EventStatusUpdate describes a 'status_update_event' event. type EventStatusUpdate struct { EventType string `json:"eventType"` Timestamp string `json:"timestamp,omitempty"` SlaveID string `json:"slaveId,omitempty"` TaskID string `json:"taskId"` TaskStatus string `json:"taskStatus"` Message string `json:"message,omitempty"` AppID string `json:"appId"` Host string `json:"host"` Ports []int `json:"ports,omitempty"` IPAddresses []*IPAddress `json:"ipAddresses"` Version string `json:"version,omitempty"` } // EventAppTerminated describes an 'app_terminated_event' event. type EventAppTerminated struct { EventType string `json:"eventType"` Timestamp string `json:"timestamp,omitempty"` AppID string `json:"appId"` } /* --- Framework Message --- */ // EventFrameworkMessage describes a 'framework_message_event' event. type EventFrameworkMessage struct { EventType string `json:"eventType"` ExecutorID string `json:"executorId"` Message string `json:"message"` SlaveID string `json:"slaveId"` Timestamp string `json:"timestamp"` } /* --- Event Subscription --- */ // EventSubscription describes a 'subscribe_event' event. type EventSubscription struct { CallbackURL string `json:"callbackUrl"` ClientIP string `json:"clientIp"` EventType string `json:"eventType"` Timestamp string `json:"timestamp"` } // EventUnsubscription describes an 'unsubscribe_event' event. type EventUnsubscription struct { CallbackURL string `json:"callbackUrl"` ClientIP string `json:"clientIp"` EventType string `json:"eventType"` Timestamp string `json:"timestamp"` } // EventStreamAttached describes an 'event_stream_attached' event. type EventStreamAttached struct { RemoteAddress string `json:"remoteAddress"` EventType string `json:"eventType"` Timestamp string `json:"timestamp"` } // EventStreamDetached describes an 'event_stream_detached' event. type EventStreamDetached struct { RemoteAddress string `json:"remoteAddress"` EventType string `json:"eventType"` Timestamp string `json:"timestamp"` } /* --- Health Checks --- */ // EventAddHealthCheck describes an 'add_health_check_event' event. type EventAddHealthCheck struct { AppID string `json:"appId"` EventType string `json:"eventType"` HealthCheck struct { GracePeriodSeconds float64 `json:"gracePeriodSeconds"` IntervalSeconds float64 `json:"intervalSeconds"` MaxConsecutiveFailures float64 `json:"maxConsecutiveFailures"` Path string `json:"path"` PortIndex float64 `json:"portIndex"` Protocol string `json:"protocol"` TimeoutSeconds float64 `json:"timeoutSeconds"` } `json:"healthCheck"` Timestamp string `json:"timestamp"` } // EventRemoveHealthCheck describes a 'remove_health_check_event' event. type EventRemoveHealthCheck struct { AppID string `json:"appId"` EventType string `json:"eventType"` HealthCheck struct { GracePeriodSeconds float64 `json:"gracePeriodSeconds"` IntervalSeconds float64 `json:"intervalSeconds"` MaxConsecutiveFailures float64 `json:"maxConsecutiveFailures"` Path string `json:"path"` PortIndex float64 `json:"portIndex"` Protocol string `json:"protocol"` TimeoutSeconds float64 `json:"timeoutSeconds"` } `json:"healthCheck"` Timestamp string `json:"timestamp"` } // EventFailedHealthCheck describes a 'failed_health_check_event' event. type EventFailedHealthCheck struct { AppID string `json:"appId"` EventType string `json:"eventType"` HealthCheck struct { GracePeriodSeconds float64 `json:"gracePeriodSeconds"` IntervalSeconds float64 `json:"intervalSeconds"` MaxConsecutiveFailures float64 `json:"maxConsecutiveFailures"` Path string `json:"path"` PortIndex float64 `json:"portIndex"` Protocol string `json:"protocol"` TimeoutSeconds float64 `json:"timeoutSeconds"` } `json:"healthCheck"` Timestamp string `json:"timestamp"` } // EventHealthCheckChanged describes a 'health_status_changed_event' event. type EventHealthCheckChanged struct { EventType string `json:"eventType"` Timestamp string `json:"timestamp,omitempty"` AppID string `json:"appId"` TaskID string `json:"taskId"` Version string `json:"version,omitempty"` Alive bool `json:"alive"` } /* --- Deployments --- */ // EventGroupChangeSuccess describes a 'group_change_success' event. type EventGroupChangeSuccess struct { EventType string `json:"eventType"` GroupID string `json:"groupId"` Timestamp string `json:"timestamp"` Version string `json:"version"` } // EventGroupChangeFailed describes a 'group_change_failed' event. type EventGroupChangeFailed struct { EventType string `json:"eventType"` GroupID string `json:"groupId"` Timestamp string `json:"timestamp"` Version string `json:"version"` Reason string `json:"reason"` } // EventDeploymentSuccess describes a 'deployment_success' event. type EventDeploymentSuccess struct { ID string `json:"id"` EventType string `json:"eventType"` Timestamp string `json:"timestamp"` Plan *DeploymentPlan `json:"plan"` } // EventDeploymentFailed describes a 'deployment_failed' event. type EventDeploymentFailed struct { ID string `json:"id"` EventType string `json:"eventType"` Timestamp string `json:"timestamp"` } // EventDeploymentInfo describes a 'deployment_info' event. type EventDeploymentInfo struct { EventType string `json:"eventType"` CurrentStep *StepActions `json:"currentStep"` Timestamp string `json:"timestamp"` Plan *DeploymentPlan `json:"plan"` } // EventDeploymentStepSuccess describes a 'deployment_step_success' event. type EventDeploymentStepSuccess struct { EventType string `json:"eventType"` CurrentStep *StepActions `json:"currentStep"` Timestamp string `json:"timestamp"` Plan *DeploymentPlan `json:"plan"` } // EventDeploymentStepFailure describes a 'deployment_step_failure' event. type EventDeploymentStepFailure struct { EventType string `json:"eventType"` CurrentStep *StepActions `json:"currentStep"` Timestamp string `json:"timestamp"` Plan *DeploymentPlan `json:"plan"` } // GetEvent returns allocated empty event object which corresponds to provided event type // eventType: the type of Marathon event func GetEvent(eventType string) (*Event, error) { // step: check it's supported id, found := eventTypesMap[eventType] if found { event := new(Event) event.ID = id event.Name = eventType switch eventType { case "api_post_event": event.Event = new(EventAPIRequest) case "status_update_event": event.Event = new(EventStatusUpdate) case "framework_message_event": event.Event = new(EventFrameworkMessage) case "subscribe_event": event.Event = new(EventSubscription) case "unsubscribe_event": event.Event = new(EventUnsubscription) case "event_stream_attached": event.Event = new(EventStreamAttached) case "event_stream_detached": event.Event = new(EventStreamDetached) case "add_health_check_event": event.Event = new(EventAddHealthCheck) case "remove_health_check_event": event.Event = new(EventRemoveHealthCheck) case "failed_health_check_event": event.Event = new(EventFailedHealthCheck) case "health_status_changed_event": event.Event = new(EventHealthCheckChanged) case "group_change_success": event.Event = new(EventGroupChangeSuccess) case "group_change_failed": event.Event = new(EventGroupChangeFailed) case "deployment_success": event.Event = new(EventDeploymentSuccess) case "deployment_failed": event.Event = new(EventDeploymentFailed) case "deployment_info": event.Event = new(EventDeploymentInfo) case "deployment_step_success": event.Event = new(EventDeploymentStepSuccess) case "deployment_step_failure": event.Event = new(EventDeploymentStepFailure) case "app_terminated_event": event.Event = new(EventAppTerminated) } return event, nil } return nil, fmt.Errorf("the event type: %s was not found or supported", eventType) } ================================================ FILE: examples/Makefile ================================================ all: find * -type d -exec bash -exc "cd {}; go build . || kill $${PPID}" \; ================================================ FILE: examples/applications/main.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "flag" "log" "time" marathon "github.com/gambol99/go-marathon" ) var marathonURL string func init() { flag.StringVar(&marathonURL, "url", "http://127.0.0.1:8080", "the url for the marathon endpoint") } func assert(err error) { if err != nil { log.Fatalf("Failed, error: %s", err) } } func waitOnDeployment(client marathon.Marathon, id *marathon.DeploymentID) { assert(client.WaitOnDeployment(id.DeploymentID, 0)) } func main() { flag.Parse() config := marathon.NewDefaultConfig() config.URL = marathonURL client, err := marathon.NewClient(config) assert(err) applications, err := client.Applications(nil) assert(err) log.Printf("Found %d application running", len(applications.Apps)) for _, application := range applications.Apps { log.Printf("Application: %v", application) details, err := client.Application(application.ID) assert(err) if details.Tasks != nil && len(details.Tasks) > 0 { for _, task := range details.Tasks { log.Printf("task: %v", task) } health, err := client.ApplicationOK(details.ID) assert(err) log.Printf("Application: %s, healthy: %t", details.ID, health) } } applicationName := "/my/product" if _, err := client.Application(applicationName); err == nil { deployID, err := client.DeleteApplication(applicationName, false) assert(err) waitOnDeployment(client, deployID) } log.Printf("Deploying a new application") application := marathon.NewDockerApplication(). Name(applicationName). CPU(0.1). Memory(64). Storage(0.0). Count(2). AddArgs("/usr/sbin/apache2ctl", "-D", "FOREGROUND"). AddEnv("NAME", "frontend_http"). AddEnv("SERVICE_80_NAME", "test_http") application. Container.Docker.Container("quay.io/gambol99/apache-php:latest"). Bridged(). Expose(80). Expose(443) *application.RequirePorts = true _, err = client.CreateApplication(application) assert(err) client.WaitOnApplication(application.ID, 30*time.Second) log.Printf("Scaling the application to 4 instances") deployID, err := client.ScaleApplicationInstances(application.ID, 4, false) assert(err) client.WaitOnApplication(application.ID, 30*time.Second) log.Printf("Successfully scaled the application, deployID: %s", deployID.DeploymentID) log.Printf("Deleting the application: %s", applicationName) deployID, err = client.DeleteApplication(application.ID, true) assert(err) waitOnDeployment(client, deployID) log.Printf("Successfully deleted the application") log.Printf("Starting the application again") _, err = client.CreateApplication(application) assert(err) log.Printf("Created the application: %s", application.ID) log.Printf("Delete all the tasks") _, err = client.KillApplicationTasks(application.ID, nil) assert(err) } ================================================ FILE: examples/docker-compose.yml ================================================ # Based on https://github.com/meltwater/docker-mesos zookeeper: image: mesoscloud/zookeeper:3.4.6-centos-7 ports: - "2181:2181" - "2888:2888" - "3888:3888" environment: SERVERS: server.1=127.0.0.1 MYID: 1 mesosmaster: image: mesoscloud/mesos-master:0.24.1-centos-7 net: host environment: MESOS_ZK: zk://localhost:2181/mesos MESOS_QUORUM: 1 MESOS_CLUSTER: local MESOS_HOSTNAME: localhost mesosslave: image: mesoscloud/mesos-slave:0.24.1-centos-7 net: host privileged: true volumes: - /sys:/sys # /cgroup is needed on some older Linux versions # - /cgroup:/cgroup # /usr/bin/docker is needed if you're running an older docker version # - /usr/local/bin/docker:/usr/bin/docker:r - /var/run/docker.sock:/var/run/docker.sock:rw environment: MESOS_MASTER: zk://localhost:2181/mesos MESOS_EXECUTOR_SHUTDOWN_GRACE_PERIOD: 90secs MESOS_DOCKER_STOP_TIMEOUT: 60secs # If your workstation doesn't have a resolvable hostname/FQDN then $MESOS_HOSTNAME needs to be set to its IP-address # MESOS_HOSTNAME: 192.168.178.39 marathon: image: mesoscloud/marathon:0.11.0-centos-7 net: host environment: MARATHON_ZK: zk://localhost:2181/marathon MARATHON_MASTER: zk://localhost:2181/mesos MARATHON_EVENT_SUBSCRIBER: http_callback MARATHON_TASK_LAUNCH_TIMEOUT: 300000 ================================================ FILE: examples/events_callback_transport/main.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "flag" "log" "time" marathon "github.com/gambol99/go-marathon" ) var marathonURL string var marathonInterface string var marathonPort int var timeout int func init() { flag.StringVar(&marathonURL, "url", "http://127.0.0.1:8080", "the url for the Marathon endpoint") flag.StringVar(&marathonInterface, "interface", "eth0", "the interface we should use for events") flag.IntVar(&marathonPort, "port", 19999, "the port the events service should run on") flag.IntVar(&timeout, "timeout", 60, "listen to events for x seconds") } func assert(err error) { if err != nil { log.Fatalf("Failed, error: %s", err) } } func main() { flag.Parse() config := marathon.NewDefaultConfig() config.URL = marathonURL config.EventsInterface = marathonInterface config.EventsPort = marathonPort log.Printf("Creating a client, Marathon: %s", marathonURL) client, err := marathon.NewClient(config) assert(err) // Register for events events, err := client.AddEventsListener(marathon.EventIDApplications) assert(err) deployments, err := client.AddEventsListener(marathon.EventIDDeploymentStepSuccess) assert(err) // Listen for x seconds and then split timer := time.After(time.Duration(timeout) * time.Second) done := false for { if done { break } select { case <-timer: log.Printf("Exiting the loop") done = true case event := <-events: log.Printf("Received application event: %s", event) case event := <-deployments: log.Printf("Received deployment event: %v", event) var deployment *marathon.EventDeploymentStepSuccess deployment = event.Event.(*marathon.EventDeploymentStepSuccess) log.Printf("deployment step: %v", deployment.CurrentStep) } } log.Printf("Removing our subscription") client.RemoveEventsListener(events) client.RemoveEventsListener(deployments) } ================================================ FILE: examples/events_sse_transport/main.go ================================================ /* Copyright 2015 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "flag" "log" "time" marathon "github.com/gambol99/go-marathon" ) var marathonURL string var timeout int func init() { flag.StringVar(&marathonURL, "url", "http://127.0.0.1:8080", "the url for the Marathon endpoint") flag.IntVar(&timeout, "timeout", 60, "listen to events for x seconds") } func assert(err error) { if err != nil { log.Fatalf("Failed, error: %s", err) } } func main() { flag.Parse() config := marathon.NewDefaultConfig() config.URL = marathonURL config.EventsTransport = marathon.EventsTransportSSE log.Printf("Creating a client, Marathon: %s", marathonURL) client, err := marathon.NewClient(config) assert(err) // Register for events events, err := client.AddEventsListener(marathon.EventIDApplications) assert(err) deployments, err := client.AddEventsListener(marathon.EventIDDeploymentStepSuccess) assert(err) // Listen for x seconds and then split timer := time.After(time.Duration(timeout) * time.Second) done := false for { if done { break } select { case <-timer: log.Printf("Exiting the loop") done = true case event := <-events: log.Printf("Received application event: %s", event) case event := <-deployments: log.Printf("Received deployment event: %v", event) var deployment *marathon.EventDeploymentStepSuccess deployment = event.Event.(*marathon.EventDeploymentStepSuccess) log.Printf("deployment step: %v", deployment.CurrentStep) } } log.Printf("Removing our subscription") client.RemoveEventsListener(events) client.RemoveEventsListener(deployments) } ================================================ FILE: examples/glog/main.go ================================================ /* Copyright 2015 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // go run main.go -logtostderr package main import ( "flag" marathon "github.com/gambol99/go-marathon" "github.com/golang/glog" ) var marathonURL string type logBridge struct{} func (l *logBridge) Write(b []byte) (n int, err error) { glog.InfoDepth(3, "go-marathon: "+string(b)) return len(b), nil } func init() { flag.StringVar(&marathonURL, "url", "http://127.0.0.1:8080", "the url for the marathon endpoint") } func main() { flag.Parse() config := marathon.NewDefaultConfig() config.URL = marathonURL config.LogOutput = new(logBridge) client, err := marathon.NewClient(config) if err != nil { glog.Exitln(err) } applications, err := client.Applications(nil) if err != nil { glog.Exitln(err) } for _, a := range applications.Apps { glog.Infof("App ID: %v\n", a.ID) } } ================================================ FILE: examples/groups/main.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "flag" "log" "time" marathon "github.com/gambol99/go-marathon" ) var marathonURL string func init() { flag.StringVar(&marathonURL, "url", "http://127.0.0.1:8080", "the url for the marathon endpoint") } func assert(err error) { if err != nil { log.Fatalf("Failed, error: %s", err) } } func main() { flag.Parse() config := marathon.NewDefaultConfig() config.URL = marathonURL client, err := marathon.NewClient(config) if err != nil { log.Fatalf("Failed to create a client for marathon, error: %s", err) } log.Printf("Retrieving a list of groups") if groups, err := client.Groups(); err != nil { log.Fatalf("Failed to retrieve the groups from maratho, error: %s", err) } else { for _, group := range groups.Groups { log.Printf("Found group: %s", group.ID) } } groupName := "/product/group" found, err := client.HasGroup(groupName) assert(err) if found { log.Printf("Deleting the group: %s, as it already exists", groupName) id, err := client.DeleteGroup(groupName, true) assert(err) err = client.WaitOnDeployment(id.DeploymentID, 0) assert(err) } /* step: the frontend app */ frontend := marathon.NewDockerApplication() frontend.Name("/product/group/frontend") frontend.CPU(0.1).Memory(64).Storage(0.0).Count(2) frontend.AddArgs("/usr/sbin/apache2ctl", "-D", "FOREGROUND") frontend.AddEnv("NAME", "frontend_http") frontend.AddEnv("SERVICE_80_NAME", "frontend_http") frontend.AddEnv("SERVICE_443_NAME", "frontend_https") frontend.AddEnv("BACKEND_MYSQL", "/product/group/mysql/3306;3306") frontend.AddEnv("BACKEND_CACHE", "/product/group/cache/6379;6379") frontend.DependsOn("/product/group/cache") frontend.DependsOn("/product/group/mysql") frontend.Container.Docker.Container("quay.io/gambol99/apache-php:latest").Expose(80).Expose(443) _, err = frontend.CheckHTTP("/hostname.php", 80, 10) assert(err) mysql := marathon.NewDockerApplication() mysql.Name("/product/group/mysql") mysql.CPU(0.1).Memory(128).Storage(0.0).Count(1) mysql.AddEnv("NAME", "group_cache") mysql.AddEnv("SERVICE_3306_NAME", "mysql") mysql.AddEnv("MYSQL_PASS", "mysql") mysql.Container.Docker.Container("tutum/mysql").Expose(3306) _, err = mysql.CheckTCP(3306, 10) assert(err) redis := marathon.NewDockerApplication() redis.Name("/product/group/cache") redis.CPU(0.1).Memory(64).Storage(0.0).Count(2) redis.AddEnv("NAME", "group_cache") redis.AddEnv("SERVICE_6379_NAME", "redis") redis.Container.Docker.Container("redis:latest").Expose(6379) _, err = redis.CheckTCP(6379, 10) assert(err) group := marathon.NewApplicationGroup(groupName) group.App(frontend).App(redis).App(mysql) assert(client.CreateGroup(group)) log.Printf("Successfully created the group: %s", group.ID) log.Printf("Updating the group parameters") frontend.Count(4) id, err := client.UpdateGroup(groupName, group, true) assert(err) log.Printf("Successfully updated the group: %s, version: %s", group.ID, id.DeploymentID) assert(client.WaitOnGroup(groupName, 500*time.Second)) } ================================================ FILE: examples/multiple_endpoints/main.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "flag" "log" "time" marathon "github.com/gambol99/go-marathon" ) const waitTime = 5 * time.Second var marathonURL string func init() { flag.StringVar(&marathonURL, "url", "http://127.0.0.1:8080,127.0.0.1:8080", "the url for the marathon endpoint") } func main() { flag.Parse() config := marathon.NewDefaultConfig() config.URL = marathonURL client, err := marathon.NewClient(config) if err != nil { log.Fatalf("Failed to create a client for marathon, error: %s", err) } for { if application, err := client.Applications(nil); err != nil { log.Fatalf("Failed to retrieve a list of applications, error: %s", err) } else { log.Printf("Retrieved a list of applications, %v", application) } log.Printf("Going to sleep for %s\n", waitTime) time.Sleep(waitTime) } } ================================================ FILE: examples/pods/main.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "flag" "fmt" "log" "time" marathon "github.com/gambol99/go-marathon" ) var marathonURL string var dcosToken string func init() { flag.StringVar(&marathonURL, "url", "http://127.0.0.1:8080", "the url for the marathon endpoint") flag.StringVar(&dcosToken, "token", "", "DCOS token for auth") } func assert(err error) { if err != nil { log.Fatalf("Failed, error: %s", err) } } func waitOnDeployment(client marathon.Marathon, id *marathon.DeploymentID) { assert(client.WaitOnDeployment(id.DeploymentID, 0)) } func createRawPod() *marathon.Pod { var containers []*marathon.PodContainer for i := 0; i < 2; i++ { container := &marathon.PodContainer{ Name: fmt.Sprintf("container%d", i), Exec: &marathon.PodExec{ Command: marathon.PodCommand{ Shell: "echo Hello World && sleep 600", }, }, Image: &marathon.PodContainerImage{ Kind: "DOCKER", ID: "nginx", ForcePull: marathon.Bool(true), }, VolumeMounts: []*marathon.PodVolumeMount{ &marathon.PodVolumeMount{ Name: "sharedvolume", MountPath: "/peers", }, }, Resources: &marathon.Resources{ Cpus: 0.1, Mem: 128, }, Env: map[string]string{ "key": "value", }, } containers = append(containers, container) } pod := &marathon.Pod{ ID: "/mypod", Containers: containers, Scaling: &marathon.PodScalingPolicy{ Kind: "fixed", Instances: 2, }, Volumes: []*marathon.PodVolume{ &marathon.PodVolume{ Name: "sharedvolume", Host: "/tmp", }, }, } return pod } func createConveniencePod() *marathon.Pod { pod := marathon.NewPod() pod.Name("mypod"). Count(2). AddVolume(marathon.NewPodVolume("sharedvolume", "/tmp")) for i := 0; i < 2; i++ { image := marathon.NewDockerPodContainerImage(). SetID("nginx") container := marathon.NewPodContainer(). SetName(fmt.Sprintf("container%d", i)). CPUs(0.1). Memory(128). SetImage(image). AddEnv("key", "value"). AddVolumeMount(marathon.NewPodVolumeMount("sharedvolume", "/peers")). SetCommand("echo Hello World && sleep 600") pod.AddContainer(container) } return pod } func doPlayground(client marathon.Marathon, pod *marathon.Pod) { // Create a pod fmt.Println("Creating pod...") pod, err := client.CreatePod(pod) assert(err) // Check its status fmt.Println("Waiting on pod...") err = client.WaitOnPod(pod.ID, time.Minute*1) assert(err) // Scale it fmt.Println("Scaling pod...") pod.Count(5) pod, err = client.UpdatePod(pod, true) assert(err) // Get instances status, err := client.PodStatus(pod.ID) fmt.Printf("Pod status: %s\n", status.Status) assert(err) // Kill an instance fmt.Println("Deleting an instance...") _, err = client.DeletePodInstance(pod.ID, status.Instances[0].ID) assert(err) // Delete it fmt.Println("Deleting the pod") id, err := client.DeletePod(pod.ID, true) assert(err) waitOnDeployment(client, id) } func main() { flag.Parse() config := marathon.NewDefaultConfig() config.URL = marathonURL config.DCOSToken = dcosToken client, err := marathon.NewClient(config) assert(err) fmt.Println("Convenience Pods:") podC := createConveniencePod() doPlayground(client, podC) fmt.Println("Raw Pods:") podR := createRawPod() doPlayground(client, podR) } ================================================ FILE: examples/queue/main.go ================================================ /* Copyright 2016 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "flag" "fmt" "log" "time" marathon "github.com/gambol99/go-marathon" ) var marathonURL string func init() { flag.StringVar(&marathonURL, "url", "http://127.0.0.1:8080", "the url for the marathon endpoint") } func main() { config := marathon.NewDefaultConfig() config.URL = marathonURL client, err := marathon.NewClient(config) if err != nil { log.Fatalf("Make new marathon client error: %v", err) } app := marathon.Application{} app.ID = "queue-test" app.Command("sleep 5") app.Count(1) app.Memory(32) fmt.Println("Creating/updating app.") // Update application will either create or update the app. _, err = client.UpdateApplication(&app, false) if err != nil { log.Fatalf("Update application error: %v", err) } // wait until marathon will launch tasks err = client.WaitOnApplication(app.ID, 10*time.Second) if err != nil { log.Fatalln("Application deploy failure, timeout.") } fmt.Println("Application was deployed.") // get marathon queue by chance for i := 0; i < 30; i++ { // Avoid shadowing err from outer scope. var queue *marathon.Queue queue, err = client.Queue() if err != nil { log.Fatalf("Get queue error: %v\n", err) } if len(queue.Items) > 0 { fmt.Println(queue) break } fmt.Printf("Queue is blank now, retry(%d)...\n", 30-i) time.Sleep(time.Second) } // delete marathon queue delay err = client.DeleteQueueDelay(app.ID) if err != nil { log.Fatalf("Delete queue delay error: %v\n", err) } fmt.Println("Queue delay deleted.") return } ================================================ FILE: examples/tasks/main.go ================================================ /* Copyright 2015 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "time" marathon "github.com/gambol99/go-marathon" ) const marathonURL = "http://127.0.0.1:8080" func main() { config := marathon.NewDefaultConfig() config.URL = marathonURL client, err := marathon.NewClient(config) if err != nil { panic(err) } app := marathon.Application{} app.ID = "tasks-test" app.Command("sleep 60") app.Count(3) fmt.Println("Creating app.") // Update application will either create or update the app. _, err = client.UpdateApplication(&app, false) if err != nil { panic(err) } // wait until marathon will launch tasks client.WaitOnApplication(app.ID, 10*time.Second) fmt.Println("Tasks were deployed.") tasks, err := client.Tasks(app.ID) if err != nil { panic(err) } host := tasks.Tasks[0].Host fmt.Printf("Killing tasks on the host: %s\n", host) _, err = client.KillApplicationTasks(app.ID, &marathon.KillApplicationTasksOpts{Scale: true, Host: host}) if err != nil { panic(err) } } ================================================ FILE: group.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "fmt" "time" ) // Group is a marathon application group type Group struct { ID string `json:"id"` Apps []*Application `json:"apps"` Dependencies []string `json:"dependencies"` Groups []*Group `json:"groups"` } // Groups is a collection of marathon application groups type Groups struct { ID string `json:"id"` Apps []*Application `json:"apps"` Dependencies []string `json:"dependencies"` Groups []*Group `json:"groups"` } // GetGroupOpts contains a payload for Group and Groups method // embed: Embeds nested resources that match the supplied path. // You can specify this parameter multiple times with different values type GetGroupOpts struct { Embed []string `url:"embed,omitempty"` } // DeleteGroupOpts contains a payload for DeleteGroup method // force: overrides a currently running deployment. type DeleteGroupOpts struct { Force bool `url:"force,omitempty"` } // UpdateGroupOpts contains a payload for UpdateGroup method // force: overrides a currently running deployment. type UpdateGroupOpts struct { Force bool `url:"force,omitempty"` } // NewApplicationGroup create a new application group // name: the name of the group func NewApplicationGroup(name string) *Group { return &Group{ ID: name, Apps: make([]*Application, 0), Dependencies: make([]string, 0), Groups: make([]*Group, 0), } } // Name sets the name of the group // name: the name of the group func (r *Group) Name(name string) *Group { r.ID = validateID(name) return r } // App add a application to the group in question // application: a pointer to the Application func (r *Group) App(application *Application) *Group { if r.Apps == nil { r.Apps = make([]*Application, 0) } r.Apps = append(r.Apps, application) return r } // Groups retrieves a list of all the groups from marathon func (r *marathonClient) Groups() (*Groups, error) { groups := new(Groups) if err := r.apiGet(marathonAPIGroups, "", groups); err != nil { return nil, err } return groups, nil } // Group retrieves the configuration of a specific group from marathon // name: the identifier for the group func (r *marathonClient) Group(name string) (*Group, error) { group := new(Group) if err := r.apiGet(fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)), nil, group); err != nil { return nil, err } return group, nil } // GroupsBy retrieves a list of all the groups from marathon by embed options // opts: GetGroupOpts request payload func (r *marathonClient) GroupsBy(opts *GetGroupOpts) (*Groups, error) { path, err := addOptions(marathonAPIGroups, opts) if err != nil { return nil, err } groups := new(Groups) if err := r.apiGet(path, "", groups); err != nil { return nil, err } return groups, nil } // GroupBy retrieves the configuration of a specific group from marathon // name: the identifier for the group // opts: GetGroupOpts request payload func (r *marathonClient) GroupBy(name string, opts *GetGroupOpts) (*Group, error) { path, err := addOptions(fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)), opts) if err != nil { return nil, err } group := new(Group) if err := r.apiGet(path, nil, group); err != nil { return nil, err } return group, nil } // HasGroup checks if the group exists in marathon // name: the identifier for the group func (r *marathonClient) HasGroup(name string) (bool, error) { path := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)) err := r.apiGet(path, "", nil) if err != nil { if apiErr, ok := err.(*APIError); ok && apiErr.ErrCode == ErrCodeNotFound { return false, nil } return false, err } return true, nil } // CreateGroup creates a new group in marathon // group: a pointer the Group structure defining the group func (r *marathonClient) CreateGroup(group *Group) error { return r.ApiPost(marathonAPIGroups, group, nil) } // WaitOnGroup waits for all the applications in a group to be deployed // group: the identifier for the group // timeout: a duration of time to wait before considering it failed (all tasks in all apps running defined as deployed) func (r *marathonClient) WaitOnGroup(name string, timeout time.Duration) error { err := deadline(timeout, func(stop_channel chan bool) error { var flick atomicSwitch go func() { <-stop_channel close(stop_channel) flick.SwitchOn() }() for !flick.IsSwitched() { if group, err := r.Group(name); err != nil { continue } else { allRunning := true // for each of the application, check if the tasks and running for _, appID := range group.Apps { // Arrrgghhh!! .. so we can't use application instances from the Application struct like with app wait on as it // appears the instance count is not set straight away!! .. it defaults to zero and changes probably at the // dependencies gets deployed. Which is probably how it internally handles dependencies .. // step: grab the application application, err := r.Application(appID.ID) if err != nil { allRunning = false break } if application.Tasks == nil { allRunning = false } else if len(application.Tasks) != *appID.Instances { allRunning = false } else if application.TasksRunning != *appID.Instances { allRunning = false } else if len(application.DeploymentIDs()) > 0 { allRunning = false } } // has anyone toggle the flag? if allRunning { return nil } } time.Sleep(r.config.PollingWaitTime) } return nil }) return err } // DeleteGroup deletes a group from marathon // name: the identifier for the group // force: used to force the delete operation in case of blocked deployment func (r *marathonClient) DeleteGroup(name string, force bool) (*DeploymentID, error) { version := new(DeploymentID) path := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)) if force { path += "?force=true" } if err := r.apiDelete(path, nil, version); err != nil { return nil, err } return version, nil } // UpdateGroup updates the parameters of a groups // name: the identifier for the group // group: the group structure with the new params // force: used to force the update operation in case of blocked deployment func (r *marathonClient) UpdateGroup(name string, group *Group, force bool) (*DeploymentID, error) { deploymentID := new(DeploymentID) path := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)) if force { path += "?force=true" } if err := r.apiPut(path, group, deploymentID); err != nil { return nil, err } return deploymentID, nil } ================================================ FILE: group_test.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "testing" "github.com/stretchr/testify/assert" ) func TestGroups(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() groups, err := endpoint.Client.Groups() assert.NoError(t, err) assert.NotNil(t, groups) assert.Equal(t, 1, len(groups.Groups)) group := groups.Groups[0] assert.Equal(t, fakeGroupName, group.ID) } func TestNewGroup(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() group, err := endpoint.Client.Group(fakeGroupName) assert.NoError(t, err) assert.NotNil(t, group) assert.Equal(t, 1, len(group.Apps)) assert.Equal(t, fakeGroupName, group.ID) group, err = endpoint.Client.Group(fakeGroupName1) assert.NoError(t, err) assert.NotNil(t, group) assert.Equal(t, fakeGroupName1, group.ID) assert.NotNil(t, group.Groups) assert.Equal(t, 1, len(group.Groups)) frontend := group.Groups[0] assert.Equal(t, "frontend", frontend.ID) assert.Equal(t, 3, len(frontend.Apps)) for _, app := range frontend.Apps { assert.NotNil(t, app.Container) assert.NotNil(t, app.Container.Docker) for _, network := range *app.Networks { assert.Equal(t, BridgeNetworkMode, network.Mode) } if len(*app.Container.PortMappings) == 0 { t.Fail() } } } // TODO @kamsz: How to work with old and new endpoints from methods.yml? // func TestGroup(t *testing.T) { // endpoint := newFakeMarathonEndpoint(t, nil) // defer endpoint.Close() // group, err := endpoint.Client.Group(fakeGroupName) // assert.NoError(t, err) // assert.NotNil(t, group) // assert.Equal(t, 1, len(group.Apps)) // assert.Equal(t, fakeGroupName, group.ID) // group, err = endpoint.Client.Group(fakeGroupName1) // assert.NoError(t, err) // assert.NotNil(t, group) // assert.Equal(t, fakeGroupName1, group.ID) // assert.NotNil(t, group.Groups) // assert.Equal(t, 1, len(group.Groups)) // frontend := group.Groups[0] // assert.Equal(t, "frontend", frontend.ID) // assert.Equal(t, 3, len(frontend.Apps)) // for _, app := range frontend.Apps { // assert.NotNil(t, app.Container) // assert.NotNil(t, app.Container.Docker) // assert.Equal(t, "BRIDGE", app.Container.Docker.Network) // if len(*app.Container.Docker.PortMappings) == 0 { // t.Fail() // } // } // } ================================================ FILE: health.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon // HealthCheck is the definition for an application health check type HealthCheck struct { Command *Command `json:"command,omitempty"` PortIndex *int `json:"portIndex,omitempty"` Port *int `json:"port,omitempty"` Path *string `json:"path,omitempty"` MaxConsecutiveFailures *int `json:"maxConsecutiveFailures,omitempty"` Protocol string `json:"protocol,omitempty"` GracePeriodSeconds int `json:"gracePeriodSeconds,omitempty"` IntervalSeconds int `json:"intervalSeconds,omitempty"` TimeoutSeconds int `json:"timeoutSeconds,omitempty"` IgnoreHTTP1xx *bool `json:"ignoreHttp1xx,omitempty"` } // HTTPHealthCheck describes an HTTP based health check type HTTPHealthCheck struct { Endpoint string `json:"endpoint,omitempty"` Path string `json:"path,omitempty"` Scheme string `json:"scheme,omitempty"` } // TCPHealthCheck describes a TCP based health check type TCPHealthCheck struct { Endpoint string `json:"endpoint,omitempty"` } // CommandHealthCheck describes a shell-based health check type CommandHealthCheck struct { Command PodCommand `json:"command,omitempty"` } // PodHealthCheck describes how to determine a pod's health type PodHealthCheck struct { HTTP *HTTPHealthCheck `json:"http,omitempty"` TCP *TCPHealthCheck `json:"tcp,omitempty"` Exec *CommandHealthCheck `json:"exec,omitempty"` GracePeriodSeconds *int `json:"gracePeriodSeconds,omitempty"` IntervalSeconds *int `json:"intervalSeconds,omitempty"` MaxConsecutiveFailures *int `json:"maxConsecutiveFailures,omitempty"` TimeoutSeconds *int `json:"timeoutSeconds,omitempty"` DelaySeconds *int `json:"delaySeconds,omitempty"` } // NewPodHealthCheck creates an empty PodHealthCheck func NewPodHealthCheck() *PodHealthCheck { return &PodHealthCheck{} } // NewHTTPHealthCheck creates an empty HTTPHealthCheck func NewHTTPHealthCheck() *HTTPHealthCheck { return &HTTPHealthCheck{} } // NewTCPHealthCheck creates an empty TCPHealthCheck func NewTCPHealthCheck() *TCPHealthCheck { return &TCPHealthCheck{} } // NewCommandHealthCheck creates an empty CommandHealthCheck func NewCommandHealthCheck() *CommandHealthCheck { return &CommandHealthCheck{} } // SetCommand sets the given command on the health check. func (h *HealthCheck) SetCommand(c Command) *HealthCheck { h.Command = &c return h } // SetPortIndex sets the given port index on the health check. func (h *HealthCheck) SetPortIndex(i int) *HealthCheck { h.PortIndex = &i return h } // SetPort sets the given port on the health check. func (h *HealthCheck) SetPort(i int) *HealthCheck { h.Port = &i return h } // SetPath sets the given path on the health check. func (h *HealthCheck) SetPath(p string) *HealthCheck { h.Path = &p return h } // SetMaxConsecutiveFailures sets the maximum consecutive failures on the health check. func (h *HealthCheck) SetMaxConsecutiveFailures(i int) *HealthCheck { h.MaxConsecutiveFailures = &i return h } // SetIgnoreHTTP1xx sets ignore http 1xx on the health check. func (h *HealthCheck) SetIgnoreHTTP1xx(ignore bool) *HealthCheck { h.IgnoreHTTP1xx = &ignore return h } // NewDefaultHealthCheck creates a default application health check func NewDefaultHealthCheck() *HealthCheck { portIndex := 0 path := "" maxConsecutiveFailures := 3 return &HealthCheck{ Protocol: "HTTP", Path: &path, PortIndex: &portIndex, MaxConsecutiveFailures: &maxConsecutiveFailures, GracePeriodSeconds: 30, IntervalSeconds: 10, TimeoutSeconds: 5, } } // HealthCheckResult is the health check result type HealthCheckResult struct { Alive bool `json:"alive"` ConsecutiveFailures int `json:"consecutiveFailures"` FirstSuccess string `json:"firstSuccess"` LastFailure string `json:"lastFailure"` LastFailureCause string `json:"lastFailureCause"` LastSuccess string `json:"lastSuccess"` TaskID string `json:"taskId"` } // Command is the command health check type type Command struct { Value string `json:"value"` } // SetHTTPHealthCheck configures the pod's health check for an HTTP endpoint. // Note this will erase any configured TCP/Exec health checks. func (p *PodHealthCheck) SetHTTPHealthCheck(h *HTTPHealthCheck) *PodHealthCheck { p.HTTP = h p.TCP = nil p.Exec = nil return p } // SetTCPHealthCheck configures the pod's health check for a TCP endpoint. // Note this will erase any configured HTTP/Exec health checks. func (p *PodHealthCheck) SetTCPHealthCheck(t *TCPHealthCheck) *PodHealthCheck { p.TCP = t p.HTTP = nil p.Exec = nil return p } // SetExecHealthCheck configures the pod's health check for a command. // Note this will erase any configured HTTP/TCP health checks. func (p *PodHealthCheck) SetExecHealthCheck(e *CommandHealthCheck) *PodHealthCheck { p.Exec = e p.HTTP = nil p.TCP = nil return p } // SetGracePeriod sets the health check initial grace period, in seconds func (p *PodHealthCheck) SetGracePeriod(gracePeriodSeconds int) *PodHealthCheck { p.GracePeriodSeconds = &gracePeriodSeconds return p } // SetInterval sets the health check polling interval, in seconds func (p *PodHealthCheck) SetInterval(intervalSeconds int) *PodHealthCheck { p.IntervalSeconds = &intervalSeconds return p } // SetMaxConsecutiveFailures sets the maximum consecutive failures on the health check func (p *PodHealthCheck) SetMaxConsecutiveFailures(maxFailures int) *PodHealthCheck { p.MaxConsecutiveFailures = &maxFailures return p } // SetTimeout sets the length of time the health check will await a result, in seconds func (p *PodHealthCheck) SetTimeout(timeoutSeconds int) *PodHealthCheck { p.TimeoutSeconds = &timeoutSeconds return p } // SetDelay sets the length of time a pod will delay running health checks on initial launch, in seconds func (p *PodHealthCheck) SetDelay(delaySeconds int) *PodHealthCheck { p.DelaySeconds = &delaySeconds return p } // SetEndpoint sets the name of the pod health check endpoint func (h *HTTPHealthCheck) SetEndpoint(endpoint string) *HTTPHealthCheck { h.Endpoint = endpoint return h } // SetPath sets the HTTP path of the pod health check endpoint func (h *HTTPHealthCheck) SetPath(path string) *HTTPHealthCheck { h.Path = path return h } // SetScheme sets the HTTP scheme of the pod health check endpoint func (h *HTTPHealthCheck) SetScheme(scheme string) *HTTPHealthCheck { h.Scheme = scheme return h } // SetEndpoint sets the name of the pod health check endpoint func (t *TCPHealthCheck) SetEndpoint(endpoint string) *TCPHealthCheck { t.Endpoint = endpoint return t } // SetCommand sets a CommandHealthCheck's underlying PodCommand func (c *CommandHealthCheck) SetCommand(p PodCommand) *CommandHealthCheck { c.Command = p return c } ================================================ FILE: health_test.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "testing" "github.com/stretchr/testify/assert" ) func TestCommand(t *testing.T) { hc := new(HealthCheck) command := Command{"curl localhost:8080"} hc.SetCommand(command) assert.Equal(t, command, (*hc.Command)) } func TestPortIndex(t *testing.T) { hc := new(HealthCheck) hc.SetPortIndex(0) assert.Equal(t, 0, (*hc.PortIndex)) } func TestPort(t *testing.T) { hc := new(HealthCheck) hc.SetPort(8000) assert.Equal(t, 8000, (*hc.Port)) } func TestPath(t *testing.T) { hc := new(HealthCheck) hc.SetPath("/path") assert.Equal(t, "/path", (*hc.Path)) } func TestMaxConsecutiveFailures(t *testing.T) { hc := new(HealthCheck) hc.SetMaxConsecutiveFailures(3) assert.Equal(t, 3, (*hc.MaxConsecutiveFailures)) } func TestIgnoreHTTP1xx(t *testing.T) { hc := new(HealthCheck) hc.SetIgnoreHTTP1xx(true) assert.True(t, (*hc.IgnoreHTTP1xx)) } ================================================ FILE: info.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon // Info is the detailed stats returned from marathon info type Info struct { EventSubscriber struct { HTTPEndpoints []string `json:"http_endpoints"` Type string `json:"type"` } `json:"event_subscriber"` FrameworkID string `json:"frameworkId"` HTTPConfig struct { AssetsPath interface{} `json:"assets_path"` HTTPPort float64 `json:"http_port"` HTTPSPort float64 `json:"https_port"` } `json:"http_config"` Leader string `json:"leader"` MarathonConfig struct { Checkpoint bool `json:"checkpoint"` Executor string `json:"executor"` FailoverTimeout float64 `json:"failover_timeout"` FrameworkName string `json:"framework_name"` Ha bool `json:"ha"` Hostname string `json:"hostname"` LeaderProxyConnectionTimeoutMs float64 `json:"leader_proxy_connection_timeout_ms"` LeaderProxyReadTimeoutMs float64 `json:"leader_proxy_read_timeout_ms"` LocalPortMax float64 `json:"local_port_max"` LocalPortMin float64 `json:"local_port_min"` Master string `json:"master"` MesosLeaderUIURL string `json:"mesos_leader_ui_url"` WebUIURL string `json:"webui_url"` MesosRole string `json:"mesos_role"` MesosUser string `json:"mesos_user"` ReconciliationInitialDelay float64 `json:"reconciliation_initial_delay"` ReconciliationInterval float64 `json:"reconciliation_interval"` TaskLaunchTimeout float64 `json:"task_launch_timeout"` TaskReservationTimeout float64 `json:"task_reservation_timeout"` } `json:"marathon_config"` Name string `json:"name"` Version string `json:"version"` ZookeeperConfig struct { Zk string `json:"zk"` ZkFutureTimeout struct { Duration float64 `json:"duration"` } `json:"zk_future_timeout"` ZkHosts string `json:"zk_hosts"` ZkPath string `json:"zk_path"` ZkState string `json:"zk_state"` ZkTimeout float64 `json:"zk_timeout"` } `json:"zookeeper_config"` } // Info retrieves the info stats from marathon func (r *marathonClient) Info() (*Info, error) { info := new(Info) if err := r.apiGet(marathonAPIInfo, nil, info); err != nil { return nil, err } return info, nil } // Leader retrieves the current marathon leader node func (r *marathonClient) Leader() (string, error) { var leader struct { Leader string `json:"leader"` } if err := r.apiGet(marathonAPILeader, nil, &leader); err != nil { return "", err } return leader.Leader, nil } // AbdicateLeader abdicates the marathon leadership func (r *marathonClient) AbdicateLeader() (string, error) { var message struct { Message string `json:"message"` } if err := r.apiDelete(marathonAPILeader, nil, &message); err != nil { return "", err } return message.Message, nil } ================================================ FILE: info_test.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "testing" "github.com/stretchr/testify/assert" ) func TestInfo(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() info, err := endpoint.Client.Info() assert.NoError(t, err) assert.Equal(t, info.FrameworkID, "20140730-222531-1863654316-5050-10422-0000") assert.Equal(t, info.Leader, "127.0.0.1:8080") assert.Equal(t, info.Version, "0.7.0-SNAPSHOT") } func TestLeader(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() leader, err := endpoint.Client.Leader() assert.NoError(t, err) assert.Equal(t, leader, "127.0.0.1:8080") } func TestAbdicateLeader(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() message, err := endpoint.Client.AbdicateLeader() assert.NoError(t, err) assert.Equal(t, message, "Leadership abdicted") } ================================================ FILE: last_task_failure.go ================================================ /* Copyright 2015 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon // LastTaskFailure provides details on the last error experienced by an application type LastTaskFailure struct { AppID string `json:"appId,omitempty"` Host string `json:"host,omitempty"` Message string `json:"message,omitempty"` SlaveID string `json:"slaveId,omitempty"` State string `json:"state,omitempty"` TaskID string `json:"taskId,omitempty"` Timestamp string `json:"timestamp,omitempty"` Version string `json:"version,omitempty"` } ================================================ FILE: network.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon // PodNetworkMode is the mode of a network descriptor type PodNetworkMode string const ( ContainerNetworkMode PodNetworkMode = "container" BridgeNetworkMode PodNetworkMode = "container/bridge" HostNetworkMode PodNetworkMode = "host" ) // PodNetwork contains network descriptors for a pod type PodNetwork struct { Name string `json:"name,omitempty"` Mode PodNetworkMode `json:"mode,omitempty"` Labels map[string]string `json:"labels,omitempty"` } // PodEndpoint describes an endpoint for a pod's container type PodEndpoint struct { Name string `json:"name,omitempty"` ContainerPort int `json:"containerPort,omitempty"` HostPort int `json:"hostPort,omitempty"` Protocol []string `json:"protocol,omitempty"` Labels map[string]string `json:"labels,omitempty"` } // NewPodNetwork creates an empty PodNetwork func NewPodNetwork(name string) *PodNetwork { return &PodNetwork{ Name: name, Labels: map[string]string{}, } } // NewPodEndpoint creates an empty PodEndpoint func NewPodEndpoint() *PodEndpoint { return &PodEndpoint{ Protocol: []string{}, Labels: map[string]string{}, } } // NewBridgePodNetwork creates a PodNetwork for a container in bridge mode func NewBridgePodNetwork() *PodNetwork { pn := NewPodNetwork("") return pn.SetMode(BridgeNetworkMode) } // NewContainerPodNetwork creates a PodNetwork for a container func NewContainerPodNetwork(name string) *PodNetwork { pn := NewPodNetwork(name) return pn.SetMode(ContainerNetworkMode) } // NewHostPodNetwork creates a PodNetwork for a container in host mode func NewHostPodNetwork() *PodNetwork { pn := NewPodNetwork("") return pn.SetMode(HostNetworkMode) } // SetName sets the name of a PodNetwork func (n *PodNetwork) SetName(name string) *PodNetwork { n.Name = name return n } // SetMode sets the mode of a PodNetwork func (n *PodNetwork) SetMode(mode PodNetworkMode) *PodNetwork { n.Mode = mode return n } // Label sets a label of a PodNetwork func (n *PodNetwork) Label(key, value string) *PodNetwork { n.Labels[key] = value return n } // SetName sets the name for a PodEndpoint func (e *PodEndpoint) SetName(name string) *PodEndpoint { e.Name = name return e } // SetContainerPort sets the container port for a PodEndpoint func (e *PodEndpoint) SetContainerPort(port int) *PodEndpoint { e.ContainerPort = port return e } // SetHostPort sets the host port for a PodEndpoint func (e *PodEndpoint) SetHostPort(port int) *PodEndpoint { e.HostPort = port return e } // AddProtocol appends a protocol for a PodEndpoint func (e *PodEndpoint) AddProtocol(protocol string) *PodEndpoint { e.Protocol = append(e.Protocol, protocol) return e } // Label sets a label for a PodEndpoint func (e *PodEndpoint) Label(key, value string) *PodEndpoint { e.Labels[key] = value return e } ================================================ FILE: offer.go ================================================ /* Copyright 2019 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon // based on https://github.com/mesosphere/marathon/blob/e7b1456ad0cfba23c9fdfa3c5d638a4b9aeb60d0/docs/docs/rest-api/public/api/v2/types/offer.raml // Offer describes a Mesos offer to a framework type Offer struct { ID string `json:"id"` Hostname string `json:"hostname"` AgentID string `json:"agentId"` Resources []OfferResource `json:"resources"` Attributes []AgentAttribute `json:"attributes"` } // OfferResource describes a resource that is part of an offer type OfferResource struct { Name string `json:"name"` Role string `json:"role"` Scalar *float64 `json:"scalar,omitempty"` Ranges []NumberRange `json:"ranges,omitempty"` Set []string `json:"set,omitempty"` } // NumberRange is a range of numbers type NumberRange struct { Begin int64 `json:"begin"` End int64 `json:"end"` } // AgentAttribute describes an attribute of an agent node type AgentAttribute struct { Name string `json:"name"` Text *string `json:"text,omitempty"` Scalar *float64 `json:"scalar,omitempty"` Ranges []NumberRange `json:"ranges,omitempty"` Set []string `json:"set,omitempty"` } ================================================ FILE: pod.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "fmt" ) // Pod is the definition for an pod in marathon type Pod struct { ID string `json:"id,omitempty"` Labels map[string]string `json:"labels,omitempty"` Version string `json:"version,omitempty"` User string `json:"user,omitempty"` // Non-secret environment variables. Actual secrets are stored in Secrets // Magic happens at marshaling/unmarshaling to get them into the correct schema Env map[string]string `json:"-"` Secrets map[string]Secret `json:"-"` Containers []*PodContainer `json:"containers,omitempty"` Volumes []*PodVolume `json:"volumes,omitempty"` Networks []*PodNetwork `json:"networks,omitempty"` Scaling *PodScalingPolicy `json:"scaling,omitempty"` Scheduling *PodSchedulingPolicy `json:"scheduling,omitempty"` ExecutorResources *ExecutorResources `json:"executorResources,omitempty"` Role *string `json:"role,omitempty"` } // PodScalingPolicy is the scaling policy of the pod type PodScalingPolicy struct { Kind string `json:"kind"` Instances int `json:"instances"` MaxInstances int `json:"maxInstances,omitempty"` } // NewPod create an empty pod func NewPod() *Pod { return &Pod{ Labels: map[string]string{}, Env: map[string]string{}, Containers: []*PodContainer{}, Secrets: map[string]Secret{}, Volumes: []*PodVolume{}, Networks: []*PodNetwork{}, } } // Name sets the name / ID of the pod i.e. the identifier for this pod func (p *Pod) Name(id string) *Pod { p.ID = validateID(id) return p } // SetUser sets the user to run the pod as func (p *Pod) SetUser(user string) *Pod { p.User = user return p } // EmptyLabels empties the labels in a pod func (p *Pod) EmptyLabels() *Pod { p.Labels = make(map[string]string) return p } // AddLabel adds a label to a pod func (p *Pod) AddLabel(key, value string) *Pod { p.Labels[key] = value return p } // SetLabels sets the labels for a pod func (p *Pod) SetLabels(labels map[string]string) *Pod { p.Labels = labels return p } // EmptyEnvs empties the environment variables for a pod func (p *Pod) EmptyEnvs() *Pod { p.Env = make(map[string]string) return p } // AddEnv adds an environment variable to a pod func (p *Pod) AddEnv(name, value string) *Pod { if p.Env == nil { p = p.EmptyEnvs() } p.Env[name] = value return p } // ExtendEnv extends the environment with the new environment variables func (p *Pod) ExtendEnv(env map[string]string) *Pod { if p.Env == nil { p = p.EmptyEnvs() } for k, v := range env { p.AddEnv(k, v) } return p } // AddContainer adds a container to a pod func (p *Pod) AddContainer(container *PodContainer) *Pod { p.Containers = append(p.Containers, container) return p } // EmptySecrets empties the secret sources in a pod func (p *Pod) EmptySecrets() *Pod { p.Secrets = make(map[string]Secret) return p } // GetSecretSource gets the source of the named secret func (p *Pod) GetSecretSource(name string) (string, error) { if val, ok := p.Secrets[name]; ok { return val.Source, nil } return "", fmt.Errorf("secret does not exist") } // AddSecret adds the secret to the pod func (p *Pod) AddSecret(envVar, secretName, sourceName string) *Pod { if p.Secrets == nil { p = p.EmptySecrets() } p.Secrets[secretName] = Secret{EnvVar: envVar, Source: sourceName} return p } // AddVolume adds a volume to a pod func (p *Pod) AddVolume(vol *PodVolume) *Pod { p.Volumes = append(p.Volumes, vol) return p } // AddNetwork adds a PodNetwork to a pod func (p *Pod) AddNetwork(net *PodNetwork) *Pod { p.Networks = append(p.Networks, net) return p } // Count sets the count of the pod func (p *Pod) Count(count int) *Pod { p.Scaling = &PodScalingPolicy{ Kind: "fixed", Instances: count, } return p } // SetPodSchedulingPolicy sets the PodSchedulingPolicy of a pod func (p *Pod) SetPodSchedulingPolicy(policy *PodSchedulingPolicy) *Pod { p.Scheduling = policy return p } // SetExecutorResources sets the resources for the pod executor func (p *Pod) SetExecutorResources(resources *ExecutorResources) *Pod { p.ExecutorResources = resources return p } // SupportsPods determines if this version of marathon supports pods // If HEAD returns 200 it does func (r *marathonClient) SupportsPods() (bool, error) { if err := r.apiHead(marathonAPIPods, nil); err != nil { // If we get a 404 we can return a strict false, otherwise it could be // a valid error if apiErr, ok := err.(*APIError); ok && apiErr.ErrCode == ErrCodeNotFound { return false, nil } return false, err } return true, nil } // Pod gets a pod object from marathon by name func (r *marathonClient) Pod(name string) (*Pod, error) { uri := buildPodURI(name) result := new(Pod) if err := r.apiGet(uri, nil, result); err != nil { return nil, err } return result, nil } // Pods gets all pods from marathon func (r *marathonClient) Pods() ([]Pod, error) { var result []Pod if err := r.apiGet(marathonAPIPods, nil, &result); err != nil { return nil, err } return result, nil } // CreatePod creates a new pod in Marathon func (r *marathonClient) CreatePod(pod *Pod) (*Pod, error) { result := new(Pod) if err := r.ApiPost(marathonAPIPods, &pod, result); err != nil { return nil, err } return result, nil } // DeletePod deletes a pod from marathon func (r *marathonClient) DeletePod(name string, force bool) (*DeploymentID, error) { uri := fmt.Sprintf("%s?force=%v", buildPodURI(name), force) deployID := new(DeploymentID) if err := r.apiDelete(uri, nil, deployID); err != nil { return nil, err } return deployID, nil } // UpdatePod creates a new pod in Marathon func (r *marathonClient) UpdatePod(pod *Pod, force bool) (*Pod, error) { uri := fmt.Sprintf("%s?force=%v", buildPodURI(pod.ID), force) result := new(Pod) if err := r.apiPut(uri, pod, result); err != nil { return nil, err } return result, nil } // PodVersions gets all the deployed versions of a pod func (r *marathonClient) PodVersions(name string) ([]string, error) { uri := buildPodVersionURI(name) var result []string if err := r.apiGet(uri, nil, &result); err != nil { return nil, err } return result, nil } // PodByVersion gets a pod by a version identifier func (r *marathonClient) PodByVersion(name, version string) (*Pod, error) { uri := fmt.Sprintf("%s/%s", buildPodVersionURI(name), version) result := new(Pod) if err := r.apiGet(uri, nil, result); err != nil { return nil, err } return result, nil } func buildPodVersionURI(name string) string { return fmt.Sprintf("%s/%s::versions", marathonAPIPods, trimRootPath(name)) } func buildPodURI(path string) string { return fmt.Sprintf("%s/%s", marathonAPIPods, trimRootPath(path)) } ================================================ FILE: pod_container.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon // PodContainer describes a container in a pod type PodContainer struct { Name string `json:"name,omitempty"` Exec *PodExec `json:"exec,omitempty"` Resources *Resources `json:"resources,omitempty"` Endpoints []*PodEndpoint `json:"endpoints,omitempty"` Image *PodContainerImage `json:"image,omitempty"` Env map[string]string `json:"-"` Secrets map[string]Secret `json:"-"` User string `json:"user,omitempty"` HealthCheck *PodHealthCheck `json:"healthCheck,omitempty"` VolumeMounts []*PodVolumeMount `json:"volumeMounts,omitempty"` Artifacts []*PodArtifact `json:"artifacts,omitempty"` Labels map[string]string `json:"labels,omitempty"` Lifecycle PodLifecycle `json:"lifecycle,omitempty"` } // PodLifecycle describes the lifecycle of a pod type PodLifecycle struct { KillGracePeriodSeconds *float64 `json:"killGracePeriodSeconds,omitempty"` } // PodCommand is the command to run as the entrypoint of the container type PodCommand struct { Shell string `json:"shell,omitempty"` } // PodExec contains the PodCommand type PodExec struct { Command PodCommand `json:"command,omitempty"` } // PodArtifact describes how to obtain a generic artifact for a pod type PodArtifact struct { URI string `json:"uri,omitempty"` Extract *bool `json:"extract,omitempty"` Executable *bool `json:"executable,omitempty"` Cache *bool `json:"cache,omitempty"` DestPath string `json:"destPath,omitempty"` } // NewPodContainer creates an empty PodContainer func NewPodContainer() *PodContainer { return &PodContainer{ Endpoints: []*PodEndpoint{}, Env: map[string]string{}, VolumeMounts: []*PodVolumeMount{}, Artifacts: []*PodArtifact{}, Labels: map[string]string{}, Resources: NewResources(), } } // SetName sets the name of a pod container func (p *PodContainer) SetName(name string) *PodContainer { p.Name = name return p } // SetCommand sets the shell command of a pod container func (p *PodContainer) SetCommand(name string) *PodContainer { p.Exec = &PodExec{ Command: PodCommand{ Shell: name, }, } return p } // CPUs sets the CPUs of a pod container func (p *PodContainer) CPUs(cpu float64) *PodContainer { p.Resources.Cpus = cpu return p } // Memory sets the memory of a pod container func (p *PodContainer) Memory(memory float64) *PodContainer { p.Resources.Mem = memory return p } // Storage sets the storage capacity of a pod container func (p *PodContainer) Storage(disk float64) *PodContainer { p.Resources.Disk = disk return p } // GPUs sets the GPU requirements of a pod container func (p *PodContainer) GPUs(gpu int32) *PodContainer { p.Resources.Gpus = gpu return p } // AddEndpoint appends an endpoint for a pod container func (p *PodContainer) AddEndpoint(endpoint *PodEndpoint) *PodContainer { p.Endpoints = append(p.Endpoints, endpoint) return p } // SetImage sets the image of a pod container func (p *PodContainer) SetImage(image *PodContainerImage) *PodContainer { p.Image = image return p } // EmptyEnvs initialized env to empty func (p *PodContainer) EmptyEnvs() *PodContainer { p.Env = make(map[string]string) return p } // AddEnv adds an environment variable for a pod container func (p *PodContainer) AddEnv(name, value string) *PodContainer { if p.Env == nil { p = p.EmptyEnvs() } p.Env[name] = value return p } // ExtendEnv extends the environment for a pod container func (p *PodContainer) ExtendEnv(env map[string]string) *PodContainer { if p.Env == nil { p = p.EmptyEnvs() } for k, v := range env { p.AddEnv(k, v) } return p } // AddSecret adds a secret to the environment for a pod container func (p *PodContainer) AddSecret(name, secretName string) *PodContainer { if p.Env == nil { p = p.EmptyEnvs() } p.Env[name] = secretName return p } // SetUser sets the user to run the pod as func (p *PodContainer) SetUser(user string) *PodContainer { p.User = user return p } // SetHealthCheck sets the health check of a pod container func (p *PodContainer) SetHealthCheck(healthcheck *PodHealthCheck) *PodContainer { p.HealthCheck = healthcheck return p } // AddVolumeMount appends a volume mount to a pod container func (p *PodContainer) AddVolumeMount(mount *PodVolumeMount) *PodContainer { p.VolumeMounts = append(p.VolumeMounts, mount) return p } // AddArtifact appends an artifact to a pod container func (p *PodContainer) AddArtifact(artifact *PodArtifact) *PodContainer { p.Artifacts = append(p.Artifacts, artifact) return p } // AddLabel adds a label to a pod container func (p *PodContainer) AddLabel(key, value string) *PodContainer { p.Labels[key] = value return p } // SetLifecycle sets the lifecycle of a pod container func (p *PodContainer) SetLifecycle(lifecycle PodLifecycle) *PodContainer { p.Lifecycle = lifecycle return p } ================================================ FILE: pod_container_image.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon // ImageType represents the image format type type ImageType string const ( // ImageTypeDocker is the docker format ImageTypeDocker ImageType = "DOCKER" // ImageTypeAppC is the appc format ImageTypeAppC ImageType = "APPC" ) // PodContainerImage describes how to retrieve the container image type PodContainerImage struct { Kind ImageType `json:"kind,omitempty"` ID string `json:"id,omitempty"` ForcePull *bool `json:"forcePull,omitempty"` PullConfig *PullConfig `json:"pullConfig,omitempty"` } // NewPodContainerImage creates an empty PodContainerImage func NewPodContainerImage() *PodContainerImage { return &PodContainerImage{} } // SetKind sets the Kind of the image func (i *PodContainerImage) SetKind(typ ImageType) *PodContainerImage { i.Kind = typ return i } // SetID sets the ID of the image func (i *PodContainerImage) SetID(id string) *PodContainerImage { i.ID = id return i } // SetPullConfig adds *PullConfig to PodContainerImage func (i *PodContainerImage) SetPullConfig(pullConfig *PullConfig) *PodContainerImage { i.PullConfig = pullConfig return i } // NewDockerPodContainerImage creates a docker PodContainerImage func NewDockerPodContainerImage() *PodContainerImage { return NewPodContainerImage().SetKind(ImageTypeDocker) } ================================================ FILE: pod_container_marshalling.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "encoding/json" "fmt" ) // PodContainerAlias aliases the PodContainer struct so that it will be marshaled/unmarshaled automatically type PodContainerAlias PodContainer // UnmarshalJSON unmarshals the given PodContainer JSON as expected except for environment variables and secrets. // Environment variables are stored in the Env field. Secrets, including the environment variable part, // are stored in the Secrets field. func (p *PodContainer) UnmarshalJSON(b []byte) error { aux := &struct { *PodContainerAlias Env map[string]interface{} `json:"environment"` }{ PodContainerAlias: (*PodContainerAlias)(p), } if err := json.Unmarshal(b, aux); err != nil { return fmt.Errorf("malformed pod container definition %v", err) } env := map[string]string{} secrets := map[string]Secret{} for envName, genericEnvValue := range aux.Env { switch envValOrSecret := genericEnvValue.(type) { case string: env[envName] = envValOrSecret case map[string]interface{}: for secret, secretStore := range envValOrSecret { if secStore, ok := secretStore.(string); ok && secret == "secret" { secrets[secStore] = Secret{EnvVar: envName} break } return fmt.Errorf("unexpected secret field %v of value type %T", secret, envValOrSecret[secret]) } default: return fmt.Errorf("unexpected environment variable type %T", envValOrSecret) } } p.Env = env for k, v := range aux.Secrets { tmp := secrets[k] tmp.Source = v.Source secrets[k] = tmp } p.Secrets = secrets return nil } // MarshalJSON marshals the given PodContainer as expected except for environment variables and secrets, // which are marshaled from specialized structs. The environment variable piece of the secrets and other // normal environment variables are combined and marshaled to the env field. The secrets and the related // source are marshaled into the secrets field. func (p *PodContainer) MarshalJSON() ([]byte, error) { env := make(map[string]interface{}) secrets := make(map[string]TmpSecret) if p.Env != nil { for k, v := range p.Env { env[string(k)] = string(v) } } if p.Secrets != nil { for k, v := range p.Secrets { env[v.EnvVar] = TmpEnvSecret{Secret: k} secrets[k] = TmpSecret{v.Source} } } aux := &struct { *PodContainerAlias Env map[string]interface{} `json:"environment,omitempty"` }{PodContainerAlias: (*PodContainerAlias)(p), Env: env} return json.Marshal(aux) } ================================================ FILE: pod_instance.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "fmt" "time" ) // PodInstance is the representation of an instance as returned by deleting an instance type PodInstance struct { InstanceID PodInstanceID `json:"instanceId"` AgentInfo PodAgentInfo `json:"agentInfo"` TasksMap map[string]PodTask `json:"tasksMap"` RunSpecVersion time.Time `json:"runSpecVersion"` State PodInstanceStateHistory `json:"state"` UnreachableStrategy EnabledUnreachableStrategy `json:"unreachableStrategy"` } // PodInstanceStateHistory is the pod instance's state type PodInstanceStateHistory struct { Condition PodTaskCondition `json:"condition"` Since time.Time `json:"since"` ActiveSince time.Time `json:"activeSince"` } // PodInstanceID contains the instance ID type PodInstanceID struct { ID string `json:"idString"` } // PodAgentInfo contains info about the agent the instance is running on type PodAgentInfo struct { Host string `json:"host"` AgentID string `json:"agentId"` Attributes []string `json:"attributes"` } // PodTask contains the info about the specific task within the instance type PodTask struct { TaskID string `json:"taskId"` RunSpecVersion time.Time `json:"runSpecVersion"` Status PodTaskStatus `json:"status"` } // PodTaskStatus is the current status of the task type PodTaskStatus struct { StagedAt time.Time `json:"stagedAt"` StartedAt time.Time `json:"startedAt"` MesosStatus string `json:"mesosStatus"` Condition PodTaskCondition `json:"condition"` NetworkInfo PodNetworkInfo `json:"networkInfo"` } // PodTaskCondition contains a string representation of the condition type PodTaskCondition struct { Str string `json:"str"` } // PodNetworkInfo contains the network info for a task type PodNetworkInfo struct { HostName string `json:"hostName"` HostPorts []int `json:"hostPorts"` IPAddresses []IPAddress `json:"ipAddresses"` } // DeletePodInstances deletes all instances of the named pod func (r *marathonClient) DeletePodInstances(name string, instances []string) ([]*PodInstance, error) { uri := buildPodInstancesURI(name) var result []*PodInstance if err := r.apiDelete(uri, instances, &result); err != nil { return nil, err } return result, nil } // DeletePodInstance deletes a specific instance of a pod func (r *marathonClient) DeletePodInstance(name, instance string) (*PodInstance, error) { uri := fmt.Sprintf("%s/%s", buildPodInstancesURI(name), instance) result := new(PodInstance) if err := r.apiDelete(uri, nil, result); err != nil { return nil, err } return result, nil } func buildPodInstancesURI(path string) string { return fmt.Sprintf("%s/%s::instances", marathonAPIPods, trimRootPath(path)) } ================================================ FILE: pod_instance_status.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon // PodInstanceState is the state of a specific pod instance type PodInstanceState string const ( // PodInstanceStatePending is when an instance is pending scheduling PodInstanceStatePending PodInstanceState = "PENDING" // PodInstanceStateStaging is when an instance is staged to be scheduled PodInstanceStateStaging PodInstanceState = "STAGING" // PodInstanceStateStable is when an instance is stably running PodInstanceStateStable PodInstanceState = "STABLE" // PodInstanceStateDegraded is when an instance is degraded status PodInstanceStateDegraded PodInstanceState = "DEGRADED" // PodInstanceStateTerminal is when an instance is terminal PodInstanceStateTerminal PodInstanceState = "TERMINAL" ) // PodInstanceStatus is the status of a pod instance type PodInstanceStatus struct { AgentHostname string `json:"agentHostname,omitempty"` Conditions []*StatusCondition `json:"conditions,omitempty"` Containers []*ContainerStatus `json:"containers,omitempty"` ID string `json:"id,omitempty"` LastChanged string `json:"lastChanged,omitempty"` LastUpdated string `json:"lastUpdated,omitempty"` Message string `json:"message,omitempty"` Networks []*PodNetworkStatus `json:"networks,omitempty"` Resources *Resources `json:"resources,omitempty"` SpecReference string `json:"specReference,omitempty"` Status PodInstanceState `json:"status,omitempty"` StatusSince string `json:"statusSince,omitempty"` } // PodNetworkStatus is the networks attached to a pod instance type PodNetworkStatus struct { Addresses []string `json:"addresses,omitempty"` Name string `json:"name,omitempty"` } // StatusCondition describes info about a status change type StatusCondition struct { Name string `json:"name,omitempty"` Value string `json:"value,omitempty"` Reason string `json:"reason,omitempty"` LastChanged string `json:"lastChanged,omitempty"` LastUpdated string `json:"lastUpdated,omitempty"` } // ContainerStatus contains all status information for a container instance type ContainerStatus struct { Conditions []*StatusCondition `json:"conditions,omitempty"` ContainerID string `json:"containerId,omitempty"` Endpoints []*PodEndpoint `json:"endpoints,omitempty"` LastChanged string `json:"lastChanged,omitempty"` LastUpdated string `json:"lastUpdated,omitempty"` Message string `json:"message,omitempty"` Name string `json:"name,omitempty"` Resources *Resources `json:"resources,omitempty"` Status string `json:"status,omitempty"` StatusSince string `json:"statusSince,omitempty"` Termination *ContainerTerminationState `json:"termination,omitempty"` } // ContainerTerminationState describes why a container terminated type ContainerTerminationState struct { ExitCode int `json:"exitCode,omitempty"` Message string `json:"message,omitempty"` } ================================================ FILE: pod_instance_test.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const fakePodInstanceName = "fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003" func TestDeletePodInstance(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() podInstance, err := endpoint.Client.DeletePodInstance(fakePodName, fakePodInstanceName) require.NoError(t, err) assert.Equal(t, podInstance.InstanceID.ID, fakePodInstanceName) } func TestDeletePodInstances(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() instances := []string{fakePodInstanceName} podInstances, err := endpoint.Client.DeletePodInstances(fakePodName, instances) require.NoError(t, err) assert.Equal(t, podInstances[0].InstanceID.ID, fakePodInstanceName) } ================================================ FILE: pod_marshalling.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "encoding/json" "fmt" ) // PodAlias aliases the Pod struct so that it will be marshaled/unmarshaled automatically type PodAlias Pod // UnmarshalJSON unmarshals the given Pod JSON as expected except for environment variables and secrets. // Environment variables are stored in the Env field. Secrets, including the environment variable part, // are stored in the Secrets field. func (p *Pod) UnmarshalJSON(b []byte) error { aux := &struct { *PodAlias Env map[string]interface{} `json:"environment"` Secrets map[string]TmpSecret `json:"secrets"` }{ PodAlias: (*PodAlias)(p), } if err := json.Unmarshal(b, aux); err != nil { return fmt.Errorf("malformed pod definition %v", err) } env := map[string]string{} secrets := map[string]Secret{} for envName, genericEnvValue := range aux.Env { switch envValOrSecret := genericEnvValue.(type) { case string: env[envName] = envValOrSecret case map[string]interface{}: for secret, secretStore := range envValOrSecret { if secStore, ok := secretStore.(string); ok && secret == "secret" { secrets[secStore] = Secret{EnvVar: envName} break } return fmt.Errorf("unexpected secret field %v of value type %T", secret, envValOrSecret[secret]) } default: return fmt.Errorf("unexpected environment variable type %T", envValOrSecret) } } p.Env = env for k, v := range aux.Secrets { tmp := secrets[k] tmp.Source = v.Source secrets[k] = tmp } p.Secrets = secrets return nil } // MarshalJSON marshals the given Pod as expected except for environment variables and secrets, // which are marshaled from specialized structs. The environment variable piece of the secrets and other // normal environment variables are combined and marshaled to the env field. The secrets and the related // source are marshaled into the secrets field. func (p *Pod) MarshalJSON() ([]byte, error) { env := make(map[string]interface{}) secrets := make(map[string]TmpSecret) if p.Env != nil { for k, v := range p.Env { env[string(k)] = string(v) } } if p.Secrets != nil { for k, v := range p.Secrets { // Only add it to the root level pod environment if it's used // Otherwise it's likely in one of the container environments if v.EnvVar != "" { env[v.EnvVar] = TmpEnvSecret{Secret: k} } secrets[k] = TmpSecret{v.Source} } } aux := &struct { *PodAlias Env map[string]interface{} `json:"environment,omitempty"` Secrets map[string]TmpSecret `json:"secrets,omitempty"` }{PodAlias: (*PodAlias)(p), Env: env, Secrets: secrets} return json.Marshal(aux) } ================================================ FILE: pod_marshalling_test.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "encoding/json" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestPodEnvironmentVariableUnmarshal(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() pod, err := endpoint.Client.Pod(fakePodName) require.NoError(t, err) env := pod.Env secrets := pod.Secrets require.NotNil(t, env) assert.Equal(t, "value", env["key1"]) assert.Equal(t, "key2", secrets["secret0"].EnvVar) assert.Equal(t, "source0", secrets["secret0"].Source) assert.Equal(t, "value3", pod.Containers[0].Env["key3"]) assert.Equal(t, "key4", pod.Containers[0].Secrets["secret1"].EnvVar) assert.Equal(t, "source1", secrets["secret1"].Source) } func TestPodMalformedPayloadUnmarshal(t *testing.T) { var tests = []struct { expected string given []byte description string }{ { expected: "unexpected secret field", given: []byte(`{"environment": {"FOO": "bar", "SECRET": {"not_secret": "secret1"}}, "secrets": {"secret1": {"source": "/path/to/secret"}}}`), description: "Field in environment secret not equal to secret.", }, { expected: "unexpected secret field", given: []byte(`{"environment": {"FOO": "bar", "SECRET": {"secret": 1}}, "secrets": {"secret1": {"source": "/path/to/secret"}}}`), description: "Invalid value in environment secret.", }, { expected: "unexpected environment variable type", given: []byte(`{"environment": {"FOO": 1, "SECRET": {"secret": "secret1"}}, "secrets": {"secret1": {"source": "/path/to/secret"}}}`), description: "Invalid environment variable type.", }, { expected: "malformed pod definition", given: []byte(`{"environment": "value"}`), description: "Bad pod definition.", }, } for _, test := range tests { tmpPod := new(Pod) err := json.Unmarshal(test.given, &tmpPod) if assert.Error(t, err, test.description) { assert.True(t, strings.HasPrefix(err.Error(), test.expected), test.description) } } } func TestPodEnvironmentVariableMarshal(t *testing.T) { testPod := new(Pod) targetString := []byte(`{"containers":[{"lifecycle":{},"environment":{"FOO2":"bar2","TOP2":"secret1"}}],"environment":{"FOO":"bar","TOP":{"secret":"secret1"}},"secrets":{"secret1":{"source":"/path/to/secret"}}}`) testPod.AddEnv("FOO", "bar") testPod.AddSecret("TOP", "secret1", "/path/to/secret") testContainer := new(PodContainer) testContainer.AddSecret("TOP2", "secret1") testContainer.AddEnv("FOO2", "bar2") testPod.AddContainer(testContainer) pod, err := json.Marshal(testPod) if assert.NoError(t, err) { assert.Equal(t, targetString, pod) } } func TestPodContainerArtifactBoolMarshal(t *testing.T) { targetString := `{"containers":[{"artifacts":[{"extract":false}],"lifecycle":{}}]}` testPod := new(Pod) testArtifact := new(PodArtifact) testArtifact.Extract = Bool(false) testContainer := new(PodContainer) testContainer.AddArtifact(testArtifact) testPod.AddContainer(testContainer) pod, err := json.Marshal(testPod) if assert.NoError(t, err) { assert.Equal(t, targetString, string(pod)) } } ================================================ FILE: pod_scheduling.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon // PodBackoff describes the backoff for re-run attempts of a pod type PodBackoff struct { Backoff *float64 `json:"backoff,omitempty"` BackoffFactor *float64 `json:"backoffFactor,omitempty"` MaxLaunchDelay *float64 `json:"maxLaunchDelay,omitempty"` } // PodUpgrade describes the policy for upgrading a pod in-place type PodUpgrade struct { MinimumHealthCapacity *float64 `json:"minimumHealthCapacity,omitempty"` MaximumOverCapacity *float64 `json:"maximumOverCapacity,omitempty"` } // PodPlacement supports constraining which hosts a pod is placed on type PodPlacement struct { Constraints *[]Constraint `json:"constraints"` AcceptedResourceRoles []string `json:"acceptedResourceRoles,omitempty"` } // PodSchedulingPolicy is the overarching pod scheduling policy type PodSchedulingPolicy struct { Backoff *PodBackoff `json:"backoff,omitempty"` Upgrade *PodUpgrade `json:"upgrade,omitempty"` Placement *PodPlacement `json:"placement,omitempty"` UnreachableStrategy *UnreachableStrategy `json:"unreachableStrategy,omitempty"` KillSelection string `json:"killSelection,omitempty"` } // Constraint describes the constraint for pod placement type Constraint struct { FieldName string `json:"fieldName"` Operator string `json:"operator"` Value string `json:"value,omitempty"` } // NewPodPlacement creates an empty PodPlacement func NewPodPlacement() *PodPlacement { return &PodPlacement{ Constraints: &[]Constraint{}, AcceptedResourceRoles: []string{}, } } // AddConstraint adds a new constraint // constraints: the constraint definition, one constraint per array element func (p *PodPlacement) AddConstraint(constraint Constraint) *PodPlacement { c := *p.Constraints c = append(c, constraint) p.Constraints = &c return p } // NewPodSchedulingPolicy creates an empty PodSchedulingPolicy func NewPodSchedulingPolicy() *PodSchedulingPolicy { return &PodSchedulingPolicy{ Placement: NewPodPlacement(), } } // NewPodBackoff creates an empty PodBackoff func NewPodBackoff() *PodBackoff { return &PodBackoff{} } // NewPodUpgrade creates a new PodUpgrade func NewPodUpgrade() *PodUpgrade { return &PodUpgrade{} } // SetBackoff sets the base backoff interval for failed pod launches, in seconds func (p *PodBackoff) SetBackoff(backoffSeconds float64) *PodBackoff { p.Backoff = &backoffSeconds return p } // SetBackoffFactor sets the backoff interval growth factor for failed pod launches func (p *PodBackoff) SetBackoffFactor(backoffFactor float64) *PodBackoff { p.BackoffFactor = &backoffFactor return p } // SetMaxLaunchDelay sets the maximum backoff interval for failed pod launches, in seconds func (p *PodBackoff) SetMaxLaunchDelay(maxLaunchDelaySeconds float64) *PodBackoff { p.MaxLaunchDelay = &maxLaunchDelaySeconds return p } // SetMinimumHealthCapacity sets the minimum amount of pod instances for healthy operation, expressed as a fraction of instance count func (p *PodUpgrade) SetMinimumHealthCapacity(capacity float64) *PodUpgrade { p.MinimumHealthCapacity = &capacity return p } // SetMaximumOverCapacity sets the maximum amount of pod instances above the instance count, expressed as a fraction of instance count func (p *PodUpgrade) SetMaximumOverCapacity(capacity float64) *PodUpgrade { p.MaximumOverCapacity = &capacity return p } // SetBackoff sets the pod's backoff settings func (p *PodSchedulingPolicy) SetBackoff(backoff *PodBackoff) *PodSchedulingPolicy { p.Backoff = backoff return p } // SetUpgrade sets the pod's upgrade settings func (p *PodSchedulingPolicy) SetUpgrade(upgrade *PodUpgrade) *PodSchedulingPolicy { p.Upgrade = upgrade return p } // SetPlacement sets the pod's placement settings func (p *PodSchedulingPolicy) SetPlacement(placement *PodPlacement) *PodSchedulingPolicy { p.Placement = placement return p } // SetKillSelection sets the pod's kill selection criteria when terminating pod instances func (p *PodSchedulingPolicy) SetKillSelection(killSelection string) *PodSchedulingPolicy { p.KillSelection = killSelection return p } // SetUnreachableStrategy sets the pod's unreachable strategy for lost instances func (p *PodSchedulingPolicy) SetUnreachableStrategy(strategy EnabledUnreachableStrategy) *PodSchedulingPolicy { p.UnreachableStrategy = &UnreachableStrategy{ EnabledUnreachableStrategy: strategy, } return p } // SetUnreachableStrategyDisabled disables the pod's unreachable strategy func (p *PodSchedulingPolicy) SetUnreachableStrategyDisabled() *PodSchedulingPolicy { p.UnreachableStrategy = &UnreachableStrategy{ AbsenceReason: UnreachableStrategyAbsenceReasonDisabled, } return p } ================================================ FILE: pod_status.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "fmt" "time" ) // PodState defines the state of a pod type PodState string const ( // PodStateDegraded is a degraded pod PodStateDegraded PodState = "DEGRADED" // PodStateStable is a stable pod PodStateStable PodState = "STABLE" // PodStateTerminal is a terminal pod PodStateTerminal PodState = "TERMINAL" ) // PodStatus describes the pod status type PodStatus struct { ID string `json:"id,omitempty"` Spec *Pod `json:"spec,omitempty"` Status PodState `json:"status,omitempty"` StatusSince string `json:"statusSince,omitempty"` Message string `json:"message,omitempty"` Instances []*PodInstanceStatus `json:"instances,omitempty"` TerminationHistory []*PodTerminationHistory `json:"terminationHistory,omitempty"` LastUpdated string `json:"lastUpdated,omitempty"` LastChanged string `json:"lastChanged,omitempty"` } // PodTerminationHistory is the termination history of the pod type PodTerminationHistory struct { InstanceID string `json:"instanceId,omitempty"` StartedAt string `json:"startedAt,omitempty"` TerminatedAt string `json:"terminatedAt,omitempty"` Message string `json:"message,omitempty"` Containers []*ContainerTerminationHistory `json:"containers,omitempty"` } // ContainerTerminationHistory is the termination history of a container in a pod type ContainerTerminationHistory struct { ContainerID string `json:"containerId,omitempty"` LastKnownState string `json:"lastKnownState,omitempty"` Termination *ContainerTerminationState `json:"termination,omitempty"` } // PodStatus retrieves the pod configuration from marathon func (r *marathonClient) PodStatus(name string) (*PodStatus, error) { var podStatus PodStatus if err := r.apiGet(buildPodStatusURI(name), nil, &podStatus); err != nil { return nil, err } return &podStatus, nil } // PodStatuses retrieves all pod configuration from marathon func (r *marathonClient) PodStatuses() ([]*PodStatus, error) { var podStatuses []*PodStatus if err := r.apiGet(buildPodStatusURI(""), nil, &podStatuses); err != nil { return nil, err } return podStatuses, nil } // WaitOnPod blocks until a pod to be deployed func (r *marathonClient) WaitOnPod(name string, timeout time.Duration) error { return r.wait(name, timeout, r.PodIsRunning) } // PodIsRunning returns whether the pod is stably running func (r *marathonClient) PodIsRunning(name string) bool { podStatus, err := r.PodStatus(name) if apiErr, ok := err.(*APIError); ok && apiErr.ErrCode == ErrCodeNotFound { return false } if err == nil && podStatus.Status == PodStateStable { return true } return false } func buildPodStatusURI(path string) string { return fmt.Sprintf("%s/%s::status", marathonAPIPods, trimRootPath(path)) } ================================================ FILE: pod_status_test.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGetPodStatus(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() podStatus, err := endpoint.Client.PodStatus(fakePodName) require.NoError(t, err) if assert.NotNil(t, podStatus) { assert.Equal(t, podStatus.Spec.ID, fakePodName) } } func TestGetAllPodStatus(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() podStatuses, err := endpoint.Client.PodStatuses() require.NoError(t, err) assert.Equal(t, podStatuses[0].Spec.ID, fakePodName) } func TestWaitOnPod(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() err := endpoint.Client.WaitOnPod(fakePodName, 1*time.Microsecond) require.NoError(t, err) } func TestPodIsRunning(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() exists := endpoint.Client.PodIsRunning(fakePodName) assert.True(t, exists) exists = endpoint.Client.PodIsRunning("not_existing") assert.False(t, exists) exists = endpoint.Client.PodIsRunning(secondFakePodName) assert.False(t, exists) } ================================================ FILE: pod_test.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const key = "testKey" const val = "testValue" const fakePodName = "/fake-pod" const secondFakePodName = "/fake-pod2" func TestPodLabels(t *testing.T) { pod := NewPod() pod.AddLabel(key, val) if assert.Equal(t, len(pod.Labels), 1) { assert.Equal(t, pod.Labels[key], val) } pod.EmptyLabels() assert.Equal(t, len(pod.Labels), 0) } func TestPodEnvironmentVars(t *testing.T) { pod := NewPod() pod.AddEnv(key, val) newVal, ok := pod.Env[key] assert.Equal(t, newVal, val) assert.Equal(t, ok, true) badVal, ok := pod.Env["fakeKey"] assert.Equal(t, badVal, "") assert.Equal(t, ok, false) pod.EmptyEnvs() assert.Equal(t, len(pod.Env), 0) } func TestSecrets(t *testing.T) { pod := NewPod() pod.AddSecret("randomVar", key, val) newVal, err := pod.GetSecretSource(key) assert.Equal(t, newVal, val) assert.Equal(t, err, nil) badVal, err := pod.GetSecretSource("fakeKey") assert.Equal(t, badVal, "") assert.NotNil(t, err) pod.EmptySecrets() assert.Equal(t, len(pod.Env), 0) } func TestSupportsPod(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) supports, err := endpoint.Client.SupportsPods() if assert.Nil(t, err) { assert.Equal(t, supports, true) } // Manually closing to test lack of support endpoint.Close() supports, err = endpoint.Client.SupportsPods() assert.NotNil(t, err) assert.Equal(t, supports, false) } func TestGetPod(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() pod, err := endpoint.Client.Pod(fakePodName) require.NoError(t, err) if assert.NotNil(t, pod) { assert.Equal(t, pod.ID, fakePodName) } } func TestGetAllPods(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() pods, err := endpoint.Client.Pods() require.NoError(t, err) if assert.Equal(t, len(pods), 2) { assert.Equal(t, pods[0].ID, fakePodName) assert.Equal(t, pods[1].ID, secondFakePodName) } } func TestCreatePod(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() pod := NewPod().Name(fakePodName) pod, err := endpoint.Client.CreatePod(pod) require.NoError(t, err) if assert.NotNil(t, pod) { assert.Equal(t, pod.ID, fakePodName) } } func TestUpdatePod(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() pod := NewPod().Name(fakePodName) pod, err := endpoint.Client.CreatePod(pod) require.NoError(t, err) pod, err = endpoint.Client.UpdatePod(pod, true) require.NoError(t, err) if assert.NotNil(t, pod) { assert.Equal(t, pod.ID, fakePodName) assert.Equal(t, pod.Scaling.Instances, 2) } } func TestDeletePod(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() id, err := endpoint.Client.DeletePod(fakePodName, true) require.NoError(t, err) if assert.NotNil(t, id) { assert.Equal(t, id.DeploymentID, "c0e7434c-df47-4d23-99f1-78bd78662231") } } func TestVersions(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() versions, err := endpoint.Client.PodVersions(fakePodName) require.NoError(t, err) assert.Equal(t, versions[0], "2014-08-18T22:36:41.451Z") } func TestGetPodByVersion(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() pod, err := endpoint.Client.PodByVersion(fakePodName, "2014-08-18T22:36:41.451Z") require.NoError(t, err) assert.Equal(t, pod.ID, fakePodName) } func TestAddPodImagePullConfig(t *testing.T) { container := new(PodContainer) container.Image = new(PodContainerImage) pullConfig := NewPullConfig("pullConfig-secret") container.Image.SetPullConfig(pullConfig) if assert.NotNil(t, container.Image.PullConfig) { assert.Equal(t, "pullConfig-secret", container.Image.PullConfig.Secret) } } ================================================ FILE: port_definition.go ================================================ /* Copyright 2016 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon // PortDefinition is a definition of a port that should be considered // part of a resource. Port definitions are necessary when you are // using HOST networking and no port mappings are specified. type PortDefinition struct { Port *int `json:"port,omitempty"` Protocol string `json:"protocol,omitempty"` Name string `json:"name,omitempty"` Labels *map[string]string `json:"labels,omitempty"` } // SetPort sets the given port for the PortDefinition func (p *PortDefinition) SetPort(port int) *PortDefinition { if p.Port == nil { p.EmptyPort() } p.Port = &port return p } // EmptyPort sets the port to 0 for the PortDefinition func (p *PortDefinition) EmptyPort() *PortDefinition { port := 0 p.Port = &port return p } // SetProtocol sets the protocol for the PortDefinition // protocol: the protocol as a string func (p *PortDefinition) SetProtocol(protocol string) *PortDefinition { p.Protocol = protocol return p } // SetName sets the name for the PortDefinition // name: the name of the PortDefinition func (p *PortDefinition) SetName(name string) *PortDefinition { p.Name = name return p } // AddLabel adds a label to the PortDefinition // name: the name of the label // value: value for this label func (p *PortDefinition) AddLabel(name, value string) *PortDefinition { if p.Labels == nil { p.EmptyLabels() } (*p.Labels)[name] = value return p } // EmptyLabels explicitly empties the labels -- use this if you need to empty // the labels of a PortDefinition that already has labels set // (setting labels to nill will keep the current value) func (p *PortDefinition) EmptyLabels() *PortDefinition { p.Labels = &map[string]string{} return p } ================================================ FILE: queue.go ================================================ /* Copyright 2016 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "fmt" ) // Queue is the definition of marathon queue type Queue struct { Items []Item `json:"queue"` } // Item is the definition of element in the queue type Item struct { Count int `json:"count"` Delay Delay `json:"delay"` Application *Application `json:"app"` Pod *Pod `json:"pod"` Role string `json:"role"` Since string `json:"since"` ProcessedOffersSummary ProcessedOffersSummary `json:"processedOffersSummary"` LastUnusedOffers []UnusedOffer `json:"lastUnusedOffers,omitempty"` } // Delay cotains the application postpone information type Delay struct { Overdue bool `json:"overdue"` TimeLeftSeconds int `json:"timeLeftSeconds"` } // ProcessedOffersSummary contains statistics for processed offers. type ProcessedOffersSummary struct { ProcessedOffersCount int32 `json:"processedOffersCount"` UnusedOffersCount int32 `json:"unusedOffersCount"` LastUnusedOfferAt *string `json:"lastUnusedOfferAt,omitempty"` LastUsedOfferAt *string `json:"lastUsedOfferAt,omitempty"` RejectSummaryLastOffers []DeclinedOfferStep `json:"rejectSummaryLastOffers,omitempty"` RejectSummaryLaunchAttempt []DeclinedOfferStep `json:"rejectSummaryLaunchAttempt,omitempty"` } // DeclinedOfferStep contains how often an offer was declined for a specific reason type DeclinedOfferStep struct { Reason string `json:"reason"` Declined int32 `json:"declined"` Processed int32 `json:"processed"` } // UnusedOffer contains which offers weren't used and why type UnusedOffer struct { Offer Offer `json:"offer"` Reason []string `json:"reason"` Timestamp string `json:"timestamp"` } // Queue retrieves content of the marathon launch queue func (r *marathonClient) Queue() (*Queue, error) { var queue *Queue err := r.apiGet(marathonAPIQueue, nil, &queue) if err != nil { return nil, err } return queue, nil } // DeleteQueueDelay resets task launch delay of the specific application // appID: the ID of the application func (r *marathonClient) DeleteQueueDelay(appID string) error { path := fmt.Sprintf("%s/%s/delay", marathonAPIQueue, trimRootPath(appID)) return r.apiDelete(path, nil, nil) } ================================================ FILE: queue_test.go ================================================ /* Copyright 2016 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "testing" "github.com/stretchr/testify/assert" ) func TestQueue(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() queue, err := endpoint.Client.Queue() assert.NoError(t, err) assert.NotNil(t, queue) assert.Len(t, queue.Items, 1) item := queue.Items[0] assert.Equal(t, item.Count, 10) assert.Equal(t, item.Delay.Overdue, true) assert.Equal(t, item.Delay.TimeLeftSeconds, 784) assert.NotEmpty(t, item.Application.ID) } func TestDeleteQueueDelay(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() err := endpoint.Client.DeleteQueueDelay(fakeAppName) assert.NoError(t, err) } ================================================ FILE: readiness.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import "time" // ReadinessCheck represents a readiness check. type ReadinessCheck struct { Name *string `json:"name,omitempty"` Protocol string `json:"protocol,omitempty"` Path string `json:"path,omitempty"` PortName string `json:"portName,omitempty"` IntervalSeconds int `json:"intervalSeconds,omitempty"` TimeoutSeconds int `json:"timeoutSeconds,omitempty"` HTTPStatusCodesForReady *[]int `json:"httpStatusCodesForReady,omitempty"` PreserveLastResponse *bool `json:"preserveLastResponse,omitempty"` } // SetName sets the name on the readiness check. func (rc *ReadinessCheck) SetName(name string) *ReadinessCheck { rc.Name = &name return rc } // SetProtocol sets the protocol on the readiness check. func (rc *ReadinessCheck) SetProtocol(proto string) *ReadinessCheck { rc.Protocol = proto return rc } // SetPath sets the path on the readiness check. func (rc *ReadinessCheck) SetPath(p string) *ReadinessCheck { rc.Path = p return rc } // SetPortName sets the port name on the readiness check. func (rc *ReadinessCheck) SetPortName(name string) *ReadinessCheck { rc.PortName = name return rc } // SetInterval sets the interval on the readiness check. func (rc *ReadinessCheck) SetInterval(interval time.Duration) *ReadinessCheck { secs := int(interval.Seconds()) rc.IntervalSeconds = secs return rc } // SetTimeout sets the timeout on the readiness check. func (rc *ReadinessCheck) SetTimeout(timeout time.Duration) *ReadinessCheck { secs := int(timeout.Seconds()) rc.TimeoutSeconds = secs return rc } // SetHTTPStatusCodesForReady sets the HTTP status codes for ready on the // readiness check. func (rc *ReadinessCheck) SetHTTPStatusCodesForReady(codes []int) *ReadinessCheck { rc.HTTPStatusCodesForReady = &codes return rc } // SetPreserveLastResponse sets the preserve last response flag on the // readiness check. func (rc *ReadinessCheck) SetPreserveLastResponse(preserve bool) *ReadinessCheck { rc.PreserveLastResponse = &preserve return rc } // ReadinessLastResponse holds the result of the last response embedded in a // readiness check result. type ReadinessLastResponse struct { Body string `json:"body"` ContentType string `json:"contentType"` Status int `json:"status"` } // ReadinessCheckResult is the result of a readiness check. type ReadinessCheckResult struct { Name string `json:"name"` TaskID string `json:"taskId"` Ready bool `json:"ready"` LastResponse ReadinessLastResponse `json:"lastResponse,omitempty"` } ================================================ FILE: readiness_test.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "testing" "time" "github.com/stretchr/testify/assert" ) func TestReadinessCheck(t *testing.T) { rc := ReadinessCheck{} rc.SetName("readiness"). SetProtocol("HTTP"). SetPath("/ready"). SetPortName("http"). SetInterval(3 * time.Second). SetTimeout(5 * time.Second). SetHTTPStatusCodesForReady([]int{200, 201}). SetPreserveLastResponse(true) if assert.NotNil(t, rc.Name) { assert.Equal(t, "readiness", *rc.Name) } assert.Equal(t, rc.Protocol, "HTTP") assert.Equal(t, rc.Path, "/ready") assert.Equal(t, rc.PortName, "http") assert.Equal(t, rc.IntervalSeconds, 3) assert.Equal(t, rc.TimeoutSeconds, 5) if assert.NotNil(t, rc.HTTPStatusCodesForReady) { assert.Equal(t, *rc.HTTPStatusCodesForReady, []int{200, 201}) } if assert.NotNil(t, rc.PreserveLastResponse) { assert.True(t, *rc.PreserveLastResponse) } } ================================================ FILE: residency.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import "time" // TaskLostBehaviorType sets action taken when the resident task is lost type TaskLostBehaviorType string const ( // TaskLostBehaviorTypeWaitForever indicates to not take any action when the resident task is lost TaskLostBehaviorTypeWaitForever TaskLostBehaviorType = "WAIT_FOREVER" // TaskLostBehaviorTypeRelaunchAfterTimeout indicates to try relaunching the lost resident task on // another node after the relaunch escalation timeout has elapsed TaskLostBehaviorTypeRelaunchAfterTimeout TaskLostBehaviorType = "RELAUNCH_AFTER_TIMEOUT" ) // Residency defines how terminal states of tasks with local persistent volumes are handled type Residency struct { TaskLostBehavior TaskLostBehaviorType `json:"taskLostBehavior,omitempty"` RelaunchEscalationTimeoutSeconds int `json:"relaunchEscalationTimeoutSeconds,omitempty"` } // SetTaskLostBehavior sets the residency behavior func (r *Residency) SetTaskLostBehavior(behavior TaskLostBehaviorType) *Residency { r.TaskLostBehavior = behavior return r } // SetRelaunchEscalationTimeout sets the residency relaunch escalation timeout with seconds precision func (r *Residency) SetRelaunchEscalationTimeout(timeout time.Duration) *Residency { r.RelaunchEscalationTimeoutSeconds = int(timeout.Seconds()) return r } ================================================ FILE: residency_test.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "testing" "time" "github.com/stretchr/testify/assert" ) func TestResidency(t *testing.T) { app := NewDockerApplication() app = app.SetResidency(TaskLostBehaviorTypeWaitForever) if assert.NotNil(t, app.Residency) { res := app.Residency assert.Equal(t, res.TaskLostBehavior, TaskLostBehaviorTypeWaitForever) res.SetRelaunchEscalationTimeout(2525 * time.Millisecond) // should be trimmed to seconds precision assert.Equal(t, app.Residency.RelaunchEscalationTimeoutSeconds, 2) res.SetTaskLostBehavior(TaskLostBehaviorTypeRelaunchAfterTimeout) assert.Equal(t, res.TaskLostBehavior, TaskLostBehaviorTypeRelaunchAfterTimeout) } app = app.EmptyResidency() if assert.NotNil(t, app.Residency) { assert.Equal(t, app.Residency, &Residency{}) } } ================================================ FILE: resources.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon // ExecutorResources are the resources supported by an executor (a task running a pod) type ExecutorResources struct { Cpus float64 `json:"cpus,omitempty"` Mem float64 `json:"mem,omitempty"` Disk float64 `json:"disk,omitempty"` } // Resources are the full set of resources for a task type Resources struct { Cpus float64 `json:"cpus"` Mem float64 `json:"mem"` Disk float64 `json:"disk,omitempty"` Gpus int32 `json:"gpus,omitempty"` } // NewResources creates an empty Resources func NewResources() *Resources { return &Resources{} } ================================================ FILE: subscription.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "encoding/json" "fmt" "io/ioutil" "net" "net/http" "strings" "sync" "time" "github.com/donovanhide/eventsource" ) // Subscriptions is a collection to urls that marathon is implementing a callback on type Subscriptions struct { CallbackURLs []string `json:"callbackUrls"` } // Subscriptions retrieves a list of registered subscriptions func (r *marathonClient) Subscriptions() (*Subscriptions, error) { subscriptions := new(Subscriptions) if err := r.apiGet(marathonAPISubscription, nil, subscriptions); err != nil { return nil, err } return subscriptions, nil } // AddEventsListener adds your self as a listener to events from Marathon // channel: a EventsChannel used to receive event on func (r *marathonClient) AddEventsListener(filter int) (EventsChannel, error) { r.Lock() defer r.Unlock() // step: someone has asked to start listening to event, we need to register for events // if we haven't done so already if err := r.registerSubscription(); err != nil { return nil, err } channel := make(EventsChannel) r.listeners[channel] = EventsChannelContext{ filter: filter, done: make(chan struct{}, 1), completion: &sync.WaitGroup{}, } return channel, nil } // RemoveEventsListener removes the channel from the events listeners // channel: the channel you are removing func (r *marathonClient) RemoveEventsListener(channel EventsChannel) { r.Lock() defer r.Unlock() if context, found := r.listeners[channel]; found { close(context.done) delete(r.listeners, channel) // step: if there is no one else listening, let's remove ourselves // from the events callback if r.config.EventsTransport == EventsTransportCallback && len(r.listeners) == 0 { r.Unsubscribe(r.SubscriptionURL()) } // step: wait for pending goroutines to finish and close channel go func(completion *sync.WaitGroup) { completion.Wait() close(channel) }(context.completion) } } // SubscriptionURL retrieves the subscription callback URL used when registering func (r *marathonClient) SubscriptionURL() string { if r.config.CallbackURL != "" { return fmt.Sprintf("%s%s", r.config.CallbackURL, defaultEventsURL) } return fmt.Sprintf("http://%s:%d%s", r.ipAddress, r.config.EventsPort, defaultEventsURL) } // registerSubscription registers ourselves with Marathon to receive events from configured transport facility func (r *marathonClient) registerSubscription() error { switch r.config.EventsTransport { case EventsTransportCallback: return r.registerCallbackSubscription() case EventsTransportSSE: return r.registerSSESubscription() default: return fmt.Errorf("the events transport: %d is not supported", r.config.EventsTransport) } } func (r *marathonClient) registerCallbackSubscription() error { if r.eventsHTTP == nil { ipAddress, err := getInterfaceAddress(r.config.EventsInterface) if err != nil { return fmt.Errorf("Unable to get the ip address from the interface: %s, error: %s", r.config.EventsInterface, err) } // step: set the ip address r.ipAddress = ipAddress binding := fmt.Sprintf("%s:%d", ipAddress, r.config.EventsPort) // step: register the handler http.HandleFunc(defaultEventsURL, r.handleCallbackEvent) // step: create the http server r.eventsHTTP = &http.Server{ Addr: binding, Handler: nil, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, MaxHeaderBytes: 1 << 20, } // @todo need to add a timeout value here listener, err := net.Listen("tcp", binding) if err != nil { return nil } go func() { for { r.eventsHTTP.Serve(listener) } }() } // step: get the callback url callback := r.SubscriptionURL() // step: check if the callback is registered found, err := r.HasSubscription(callback) if err != nil { return err } if !found { // step: we need to register ourselves if err := r.Subscribe(callback); err != nil { return err } } return nil } // registerSSESubscription starts a go routine that continuously tries to // connect to the SSE stream and to process the received events. To establish // the connection it tries the active cluster members until no more member is // active. When this happens it will retry to get a connection every 5 seconds. func (r *marathonClient) registerSSESubscription() error { if r.subscribedToSSE { return nil } if r.config.HTTPSSEClient.Timeout != 0 { return fmt.Errorf( "global timeout must not be set for SSE connections (found %s) -- remove global timeout from HTTP client or provide separate SSE HTTP client without global timeout", r.config.HTTPSSEClient.Timeout, ) } go func() { for { stream, err := r.connectToSSE() if err != nil { r.debugLog("Error connecting SSE subscription: %s", err) <-time.After(5 * time.Second) continue } err = r.listenToSSE(stream) stream.Close() r.debugLog("Error on SSE subscription: %s", err) } }() r.subscribedToSSE = true return nil } // connectToSSE tries to establish an *eventsource.Stream to any of the Marathon cluster members, marking the // member as down on connection failure, until there is no more active member in the cluster. // Given the http request can not be built, it will panic as this case should never happen. func (r *marathonClient) connectToSSE() (*eventsource.Stream, error) { for { request, member, err := r.buildAPIRequest("GET", marathonAPIEventStream, nil) if err != nil { switch err.(type) { case newRequestError: panic(fmt.Sprintf("Requests for SSE subscriptions should never fail to be created: %s", err.Error())) default: return nil, err } } // The event source library manipulates the HTTPClient. So we create a new one and copy // its underlying fields for performance reasons. See note that at least the Transport // should be reused here: https://golang.org/pkg/net/http/#Client httpClient := &http.Client{ Transport: r.config.HTTPSSEClient.Transport, CheckRedirect: r.config.HTTPSSEClient.CheckRedirect, Jar: r.config.HTTPSSEClient.Jar, Timeout: r.config.HTTPSSEClient.Timeout, } stream, err := eventsource.SubscribeWith("", httpClient, request) if err != nil { r.debugLog("Error subscribing to Marathon event stream: %s", err) r.hosts.markDown(member) continue } return stream, nil } } func (r *marathonClient) listenToSSE(stream *eventsource.Stream) error { for { select { case ev := <-stream.Events: if err := r.handleEvent(ev.Data()); err != nil { r.debugLog("listenToSSE(): failed to handle event: %v", err) } case err := <-stream.Errors: return err } } } // Subscribe adds a URL to Marathon's callback facility // callback : the URL you wish to subscribe func (r *marathonClient) Subscribe(callback string) error { path := fmt.Sprintf("%s?callbackUrl=%s", marathonAPISubscription, callback) return r.ApiPost(path, "", nil) } // Unsubscribe removes a URL from Marathon's callback facility // callback : the URL you wish to unsubscribe func (r *marathonClient) Unsubscribe(callback string) error { // step: remove from the list of subscriptions return r.apiDelete(fmt.Sprintf("%s?callbackUrl=%s", marathonAPISubscription, callback), nil, nil) } // HasSubscription checks to see a subscription already exists with Marathon // callback: the url of the callback func (r *marathonClient) HasSubscription(callback string) (bool, error) { // step: generate our events callback subscriptions, err := r.Subscriptions() if err != nil { return false, err } for _, subscription := range subscriptions.CallbackURLs { if callback == subscription { return true, nil } } return false, nil } func (r *marathonClient) handleEvent(content string) error { // step: process and decode the event eventType := new(EventType) err := json.NewDecoder(strings.NewReader(content)).Decode(eventType) if err != nil { return fmt.Errorf("failed to decode the event type, content: %s, error: %s", content, err) } // step: check whether event type is handled event, err := GetEvent(eventType.EventType) if err != nil { return fmt.Errorf("unable to handle event, type: %s, error: %s", eventType.EventType, err) } // step: let's decode message err = json.NewDecoder(strings.NewReader(content)).Decode(event.Event) if err != nil { return fmt.Errorf("failed to decode the event, id: %d, error: %s", event.ID, err) } r.RLock() defer r.RUnlock() // step: check if anyone is listen for this event for channel, context := range r.listeners { // step: check if this listener wants this event type if event.ID&context.filter != 0 { context.completion.Add(1) go func(ch EventsChannel, context EventsChannelContext, e *Event) { defer context.completion.Done() select { case ch <- e: case <-context.done: // Terminates goroutine. } }(channel, context, event) } } return nil } func (r *marathonClient) handleCallbackEvent(writer http.ResponseWriter, request *http.Request) { body, err := ioutil.ReadAll(request.Body) if err != nil { // TODO should this return a 500? r.debugLog("handleCallbackEvent(): failed to read request body, error: %s", err) return } if err := r.handleEvent(string(body[:])); err != nil { // TODO should this return a 500? r.debugLog("handleCallbackEvent(): failed to handle event: %v", err) } } ================================================ FILE: subscription_test.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "net" "net/http" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( eventPublishTimeout time.Duration = 250 * time.Millisecond SSEConnectWaitTime time.Duration = 250 * time.Millisecond ) type testCaseList []testCase func (l testCaseList) find(name string) *testCase { for _, testCase := range l { if testCase.name == name { return &testCase } } return nil } type testCase struct { name string source string expectation interface{} } var testCases = testCaseList{ testCase{ name: "status_update_event", source: `{ "eventType": "status_update_event", "timestamp": "2014-03-01T23:29:30.158Z", "slaveId": "20140909-054127-177048842-5050-1494-0", "taskId": "my-app_0-1396592784349", "taskStatus": "TASK_RUNNING", "appId": "/my-app", "host": "slave-1234.acme.org", "ports": [31372], "version": "2014-04-04T06:26:23.051Z" }`, expectation: &EventStatusUpdate{ EventType: "status_update_event", Timestamp: "2014-03-01T23:29:30.158Z", SlaveID: "20140909-054127-177048842-5050-1494-0", TaskID: "my-app_0-1396592784349", TaskStatus: "TASK_RUNNING", AppID: "/my-app", Host: "slave-1234.acme.org", Ports: []int{31372}, Version: "2014-04-04T06:26:23.051Z", }, }, testCase{ name: "health_status_changed_event", source: `{ "eventType": "health_status_changed_event", "timestamp": "2014-03-01T23:29:30.158Z", "appId": "/my-app", "taskId": "my-app_0-1396592784349", "version": "2014-04-04T06:26:23.051Z", "alive": true }`, expectation: &EventHealthCheckChanged{ EventType: "health_status_changed_event", Timestamp: "2014-03-01T23:29:30.158Z", AppID: "/my-app", TaskID: "my-app_0-1396592784349", Version: "2014-04-04T06:26:23.051Z", Alive: true, }, }, testCase{ name: "failed_health_check_event", source: `{ "eventType": "failed_health_check_event", "timestamp": "2014-03-01T23:29:30.158Z", "appId": "/my-app", "taskId": "my-app_0-1396592784349", "healthCheck": { "protocol": "HTTP", "path": "/health", "portIndex": 0, "gracePeriodSeconds": 5, "intervalSeconds": 10, "timeoutSeconds": 10, "maxConsecutiveFailures": 3 } }`, expectation: &EventFailedHealthCheck{ EventType: "failed_health_check_event", Timestamp: "2014-03-01T23:29:30.158Z", AppID: "/my-app", HealthCheck: struct { GracePeriodSeconds float64 `json:"gracePeriodSeconds"` IntervalSeconds float64 `json:"intervalSeconds"` MaxConsecutiveFailures float64 `json:"maxConsecutiveFailures"` Path string `json:"path"` PortIndex float64 `json:"portIndex"` Protocol string `json:"protocol"` TimeoutSeconds float64 `json:"timeoutSeconds"` }{ GracePeriodSeconds: 5, IntervalSeconds: 10, MaxConsecutiveFailures: 3, Path: "/health", PortIndex: 0, Protocol: "HTTP", TimeoutSeconds: 10, }, }, }, // For Marathon 1.1.1 and before testCase{ name: "deployment_info", source: `{ "eventType": "deployment_info", "timestamp": "2016-07-29T08:03:52.542Z", "plan": { "id": "dcf63e4a-ef27-4816-e865-1730fcb26ac3", "version": "2016-07-29T08:03:52.542Z", "original": {}, "target": {}, "steps": [ { "actions": [ { "type": "ScaleApplication", "app": "/my-app" } ] } ] }, "currentStep": { "actions": [ { "type": "ScaleApplication", "app": "/my-app" } ] } }`, expectation: &EventDeploymentInfo{ EventType: "deployment_info", Timestamp: "2016-07-29T08:03:52.542Z", Plan: &DeploymentPlan{ ID: "dcf63e4a-ef27-4816-e865-1730fcb26ac3", Version: "2016-07-29T08:03:52.542Z", Original: &Group{}, Target: &Group{}, Steps: []*StepActions{ &StepActions{ Actions: []struct { Action string `json:"action"` Type string `json:"type"` App string `json:"app"` }{ { Type: "ScaleApplication", App: "/my-app", }, }, }, }, }, CurrentStep: &StepActions{ Actions: []struct { Action string `json:"action"` Type string `json:"type"` App string `json:"app"` }{ { Type: "ScaleApplication", App: "/my-app", }, }, }, }, }, // For Marathon 1.1.2 and after testCase{ name: "deployment_step_success", source: `{ "eventType": "deployment_step_success", "timestamp": "2016-07-29T08:03:52.542Z", "plan": { "id": "dcf63e4a-ef27-4816-e865-1730fcb26ac3", "version": "2016-07-29T08:03:52.542Z", "original": {}, "target": {}, "steps": [ { "actions": [ { "action": "ScaleApplication", "app": "/my-app" } ] } ] }, "currentStep": { "actions": [ { "action": "ScaleApplication", "app": "/my-app" } ] } }`, expectation: &EventDeploymentInfo{ EventType: "deployment_info", Timestamp: "2016-07-29T08:03:52.542Z", Plan: &DeploymentPlan{ ID: "dcf63e4a-ef27-4816-e865-1730fcb26ac3", Version: "2016-07-29T08:03:52.542Z", Original: &Group{}, Target: &Group{}, Steps: []*StepActions{ &StepActions{ Actions: []struct { Action string `json:"action"` Type string `json:"type"` App string `json:"app"` }{ { Action: "ScaleApplication", App: "/my-app", }, }, }, }, }, CurrentStep: &StepActions{ Actions: []struct { Action string `json:"action"` Type string `json:"type"` App string `json:"app"` }{ { Action: "ScaleApplication", App: "/my-app", }, }, }, }, }, } func TestSubscriptions(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() sub, err := endpoint.Client.Subscriptions() assert.NoError(t, err) assert.NotNil(t, sub) assert.NotNil(t, sub.CallbackURLs) assert.Equal(t, len(sub.CallbackURLs), 1) } func TestSubscribe(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() err := endpoint.Client.Subscribe("http://localhost:9292/callback") assert.NoError(t, err) } func TestUnsubscribe(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() err := endpoint.Client.Unsubscribe("http://localhost:9292/callback") assert.NoError(t, err) } func TestSSEWithGlobalTimeout(t *testing.T) { clientCfg := NewDefaultConfig() clientCfg.HTTPSSEClient = &http.Client{ Timeout: 1 * time.Second, } config := configContainer{ client: &clientCfg, } config.client.EventsTransport = EventsTransportSSE endpoint := newFakeMarathonEndpoint(t, &config) defer endpoint.Close() _, err := endpoint.Client.AddEventsListener(EventIDApplications) assert.Error(t, err) } func TestEventStreamEventsReceived(t *testing.T) { require.True(t, len(testCases) > 1, "must have at least 2 test cases to end prematurely") clientCfg := NewDefaultConfig() config := configContainer{ client: &clientCfg, } config.client.EventsTransport = EventsTransportSSE endpoint := newFakeMarathonEndpoint(t, &config) defer endpoint.Close() events, err := endpoint.Client.AddEventsListener(EventIDApplications | EventIDDeploymentInfo | EventIDDeploymentStepSuccess) assert.NoError(t, err) almostAllTestCases := testCases[:len(testCases)-1] finalTestCase := testCases[len(testCases)-1] // Give it a bit of time so that the subscription can be set up time.Sleep(SSEConnectWaitTime) // Publish all but one test event. for _, testCase := range almostAllTestCases { endpoint.Server.PublishEvent(testCase.source) } // Receive test events. for i := 0; i < len(almostAllTestCases); i++ { select { case event := <-events: tc := testCases.find(event.Name) if !assert.NotNil(t, tc, "received unknown event: %s", event.Name) { continue } assert.Equal(t, tc.expectation, event.Event) case <-time.After(eventPublishTimeout): assert.Fail(t, "did not receive event in time") } } // Publish last test event that we do not intend to consume anymore. endpoint.Server.PublishEvent(finalTestCase.source) // Give event stream some time to buffer another event. time.Sleep(eventPublishTimeout) // Trigger done channel closure. endpoint.Client.RemoveEventsListener(events) // Give pending goroutine time to consume done signal. time.Sleep(eventPublishTimeout) // Validate that channel is closed. select { case _, more := <-events: assert.False(t, more, "should not have received another event") default: assert.Fail(t, "channel was not closed") } } func TestConnectToSSESuccess(t *testing.T) { clientCfg := NewDefaultConfig() // Use non-existent address as first cluster member clientCfg.URL = "http://127.0.0.1:11111" clientCfg.EventsTransport = EventsTransportSSE config := configContainer{client: &clientCfg} endpoint := newFakeMarathonEndpoint(t, &config) defer endpoint.Close() client := endpoint.Client.(*marathonClient) // Add real server as member to the cluster client.hosts.members = append(client.hosts.members, &member{endpoint: endpoint.Server.httpSrv.URL}) // Connection should work as one of the Marathon members is up stream, err := client.connectToSSE() if assert.NoError(t, err, "expected no error in connectToSSE") { stream.Close() } } func TestConnectToSSEFailure(t *testing.T) { clientCfg := NewDefaultConfig() clientCfg.EventsTransport = EventsTransportSSE config := configContainer{client: &clientCfg} endpoint := newFakeMarathonEndpoint(t, &config) endpoint.Close() client := endpoint.Client.(*marathonClient) // No Marathon member is up, we should get an error stream, err := client.connectToSSE() if !assert.Error(t, err, "expected error in connectToSSE when all cluster members are down") { stream.Close() } } func TestRegisterSEESubscriptionReconnectsStreamOnError(t *testing.T) { clientCfg := NewDefaultConfig() clientCfg.HTTPSSEClient = &http.Client{ Transport: &http.Transport{ Dial: (&net.Dialer{ // set timeout to a small fraction of SSEConnectWaitTime to give client enough time // to detect error and reconnect during sleep Timeout: SSEConnectWaitTime / 10, }).Dial, }, } clientCfg.EventsTransport = EventsTransportSSE config := configContainer{client: &clientCfg} endpoint1 := newFakeMarathonEndpoint(t, &config) endpoint2 := newFakeMarathonEndpoint(t, &config) defer endpoint2.Close() client1 := endpoint1.Client.(*marathonClient) // Add the second server to the cluster members client1.hosts.members = append(client1.hosts.members, &member{endpoint: endpoint2.Server.httpSrv.URL}) events, err := endpoint1.Client.AddEventsListener(EventIDApplications) require.NoError(t, err) // Give it a bit of time so that the subscription can be set up time.Sleep(SSEConnectWaitTime) // This should make the SSE subscription fail and reconnect to another cluster member endpoint1.Close() // Give it a bit of time so that the subscription can reconnect time.Sleep(SSEConnectWaitTime) // Now that our SSE subscription failed over, we can publish on the second server and the message should be consumed endpoint2.Server.PublishEvent(testCases[0].source) select { case event := <-events: tc := testCases.find(event.Name) assert.NotNil(t, tc, "received unknown event: %s", event.Name) case <-time.After(eventPublishTimeout): assert.Fail(t, "did not receive event in time") } } ================================================ FILE: task.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "fmt" "strings" ) // Tasks is a collection of marathon tasks type Tasks struct { Tasks []Task `json:"tasks"` } // Task is the definition for a marathon task type Task struct { ID string `json:"id"` AppID string `json:"appId"` Host string `json:"host"` HealthCheckResults []*HealthCheckResult `json:"healthCheckResults"` Ports []int `json:"ports"` ServicePorts []int `json:"servicePorts"` SlaveID string `json:"slaveId"` StagedAt string `json:"stagedAt"` StartedAt string `json:"startedAt"` State string `json:"state"` IPAddresses []*IPAddress `json:"ipAddresses"` Version string `json:"version"` } // IPAddress represents a task's IP address and protocol. type IPAddress struct { IPAddress string `json:"ipAddress"` Protocol string `json:"protocol"` } // AllTasksOpts contains a payload for AllTasks method // status: Return only those tasks whose status matches this parameter. // If not specified, all tasks are returned. Possible values: running, staging. Default: none. type AllTasksOpts struct { Status string `url:"status,omitempty"` } // KillApplicationTasksOpts contains a payload for KillApplicationTasks method // host: kill only those tasks on a specific host (optional) // scale: Scale the app down (i.e. decrement its instances setting by the number of tasks killed) after killing the specified tasks type KillApplicationTasksOpts struct { Host string `url:"host,omitempty"` Scale bool `url:"scale,omitempty"` Force bool `url:"force,omitempty"` } // KillTaskOpts contains a payload for task killing methods // scale: Scale the app down type KillTaskOpts struct { Scale bool `url:"scale,omitempty"` Force bool `url:"force,omitempty"` Wipe bool `url:"wipe,omitempty"` } // HasHealthCheckResults checks if the task has any health checks func (r *Task) HasHealthCheckResults() bool { return r.HealthCheckResults != nil && len(r.HealthCheckResults) > 0 } // AllTasks lists tasks of all applications. // opts: AllTasksOpts request payload func (r *marathonClient) AllTasks(opts *AllTasksOpts) (*Tasks, error) { path, err := addOptions(marathonAPITasks, opts) if err != nil { return nil, err } tasks := new(Tasks) if err := r.apiGet(path, nil, tasks); err != nil { return nil, err } return tasks, nil } // Tasks retrieves a list of tasks for an application // id: the id of the application func (r *marathonClient) Tasks(id string) (*Tasks, error) { tasks := new(Tasks) if err := r.apiGet(fmt.Sprintf("%s/%s/tasks", marathonAPIApps, trimRootPath(id)), nil, tasks); err != nil { return nil, err } return tasks, nil } // KillApplicationTasks kills all tasks relating to an application // id: the id of the application // opts: KillApplicationTasksOpts request payload func (r *marathonClient) KillApplicationTasks(id string, opts *KillApplicationTasksOpts) (*Tasks, error) { path := fmt.Sprintf("%s/%s/tasks", marathonAPIApps, trimRootPath(id)) path, err := addOptions(path, opts) if err != nil { return nil, err } tasks := new(Tasks) if err := r.apiDelete(path, nil, tasks); err != nil { return nil, err } return tasks, nil } // KillTask kills the task associated with a given ID // taskID: the id for the task // opts: KillTaskOpts request payload func (r *marathonClient) KillTask(taskID string, opts *KillTaskOpts) (*Task, error) { appName := taskID[0:strings.LastIndex(taskID, ".")] appName = strings.Replace(appName, "_", "/", -1) taskID = strings.Replace(taskID, "/", "_", -1) path := fmt.Sprintf("%s/%s/tasks/%s", marathonAPIApps, appName, taskID) path, err := addOptions(path, opts) if err != nil { return nil, err } wrappedTask := new(struct { Task Task `json:"task"` }) if err := r.apiDelete(path, nil, wrappedTask); err != nil { return nil, err } return &wrappedTask.Task, nil } // KillTasks kills tasks associated with given array of ids // tasks: the array of task ids // opts: KillTaskOpts request payload func (r *marathonClient) KillTasks(tasks []string, opts *KillTaskOpts) error { path := fmt.Sprintf("%s/delete", marathonAPITasks) path, err := addOptions(path, opts) if err != nil { return nil } var post struct { IDs []string `json:"ids"` } post.IDs = tasks return r.ApiPost(path, &post, nil) } // TaskEndpoints gets the endpoints i.e. HOST_IP:DYNAMIC_PORT for a specific application service // I.e. a container running apache, might have ports 80/443 (translated to X dynamic ports), but i want // port 80 only and i only want those whom have passed the health check // // Note: I've NO IDEA how to associate the health_check_result to the actual port, I don't think it's // possible at the moment, however, given marathon will fail and restart an application even if one of x ports of a task is // down, the per port check is redundant??? .. personally, I like it anyhow, but hey // // name: the identifier for the application // port: the container port you are interested in // health: whether to check the health or not func (r *marathonClient) TaskEndpoints(name string, port int, healthCheck bool) ([]string, error) { // step: get the application details application, err := r.Application(name) if err != nil { return nil, err } // step: we need to get the port index of the service we are interested in portIndex, err := application.Container.Docker.ServicePortIndex(port) if err != nil { portIndex, err = application.Container.ServicePortIndex(port) if err != nil { return nil, err } } // step: do we have any tasks? if application.Tasks == nil || len(application.Tasks) == 0 { return nil, nil } // step: if we are checking health the 'service' has a health check? healthCheck = healthCheck && application.HasHealthChecks() // step: iterate the tasks and extract the dynamic ports var list []string for _, task := range application.Tasks { if !healthCheck || task.allHealthChecksAlive() { endpoint := fmt.Sprintf("%s:%d", task.Host, task.Ports[portIndex]) list = append(list, endpoint) } } return list, nil } func (r *Task) allHealthChecksAlive() bool { // check: does the task have a health check result, if NOT, it's because the // health of the task hasn't yet been performed, hence we assume it as DOWN if !r.HasHealthCheckResults() { return false } // step: check the health results then for _, check := range r.HealthCheckResults { if !check.Alive { return false } } return true } ================================================ FILE: task_test.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "testing" "github.com/stretchr/testify/assert" ) func TestHasHealthCheckResults(t *testing.T) { task := Task{} assert.False(t, task.HasHealthCheckResults()) task.HealthCheckResults = append(task.HealthCheckResults, &HealthCheckResult{}) assert.True(t, task.HasHealthCheckResults()) } func TestAllTasks(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() tasks, err := endpoint.Client.AllTasks(nil) assert.NoError(t, err) if assert.NotNil(t, tasks) { assert.Equal(t, len(tasks.Tasks), 2) } tasks, err = endpoint.Client.AllTasks(&AllTasksOpts{Status: "staging"}) assert.Nil(t, err) if assert.NotNil(t, tasks) { assert.Equal(t, len(tasks.Tasks), 0) } } func TestTasks(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() tasks, err := endpoint.Client.Tasks(fakeAppName) assert.NoError(t, err) if assert.NotNil(t, tasks) { assert.Equal(t, len(tasks.Tasks), 2) } } func TestKillApplicationTasks(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() tasks, err := endpoint.Client.KillApplicationTasks(fakeAppName, nil) assert.NoError(t, err) assert.NotNil(t, tasks) } func TestKillTask(t *testing.T) { cases := map[string]struct { TaskID string Result string }{ "CommonApp": {fakeTaskID, fakeTaskID}, "GroupApp": {"fake-group_fake-app.fake-task", "fake-group_fake-app.fake-task"}, "GroupAppWithSlashes": {"fake-group/fake-app.fake-task", "fake-group_fake-app.fake-task"}, } endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() for k, tc := range cases { task, err := endpoint.Client.KillTask(tc.TaskID, nil) assert.NoError(t, err, "TestCase: %s", k) assert.Equal(t, tc.Result, task.ID, "TestCase: %s", k) } } func TestKillTasks(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() err := endpoint.Client.KillTasks([]string{fakeTaskID}, nil) assert.NoError(t, err) } func TestTaskEndpoints(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() endpoints, err := endpoint.Client.TaskEndpoints(fakeAppNameBroken, 8080, true) assert.NoError(t, err) assert.NotNil(t, endpoints) assert.Equal(t, len(endpoints), 1, t) assert.Equal(t, endpoints[0], "10.141.141.10:31045", t) endpoints, err = endpoint.Client.TaskEndpoints(fakeAppNameBroken, 8080, false) assert.NoError(t, err) assert.NotNil(t, endpoints) assert.Equal(t, len(endpoints), 2, t) assert.Equal(t, endpoints[0], "10.141.141.10:31045", t) assert.Equal(t, endpoints[1], "10.141.141.10:31234", t) _, err = endpoint.Client.TaskEndpoints(fakeAppNameBroken, 80, true) assert.Error(t, err) } ================================================ FILE: testing_utils_test.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" "net/url" "strings" "sync" "testing" "github.com/donovanhide/eventsource" yaml "gopkg.in/yaml.v2" ) const ( fakeMarathonURL = "http://127.0.0.1:3000,127.0.0.1:3000,127.0.0.1:3000" fakeMarathonURLWithPath = "http://127.0.0.1:3000/path,127.0.0.1:3000/path,127.0.0.1:3000/path" fakeGroupName = "/test" fakeGroupName1 = "/qa/product/1" fakeAppName = "/fake-app" fakeTaskID = "fake-app.fake-task" fakeAppNameBroken = "/fake-app-broken" fakeDeploymentID = "867ed450-f6a8-4d33-9b0e-e11c5513990b" fakeAppNameUnhealthy = "/no-health-check-results-app" ) var ( fakeResponses map[string][]indexedResponse once sync.Once ) type indexedResponse struct { Index int `yaml:"index,omitempty"` Content string `yaml:"content,omitempty"` Headers map[string]string `yaml:"headers,omitempty"` } type responseIndices struct { sync.Mutex m map[string]int } func newResponseIndices() *responseIndices { return &responseIndices{m: map[string]int{}} } // restMethod represents an expected HTTP method and an associated fake response type restMethod struct { // the uri of the method URI string `yaml:"uri,omitempty"` // the http method type (GET|PUT etc) Method string `yaml:"method,omitempty"` // the content i.e. response Content string `yaml:"content,omitempty"` // ContentSequence is a sequence of responses that are returned in order. ContentSequence []indexedResponse `yaml:"contentSequence,omitempty"` // the test scope Scope string `yaml:"scope,omitempty"` // headers in the response Headers map[string]string `yaml:"headers,omitempty"` } // serverConfig holds the Marathon server configuration type serverConfig struct { // Username for basic auth username string // Password for basic auth password string // Token for authorization in case of DCOS environment dcosToken string // scope is an arbitrary test scope to distinguish fake responses from // otherwise equal HTTP methods and query strings. scope string } // configContainer holds both server and client Marathon configuration type configContainer struct { client *Config server *serverConfig } type fakeServer struct { io.Closer eventSrv *eventsource.Server httpSrv *httptest.Server fakeRespIndices *responseIndices } type endpoint struct { io.Closer Server fakeServer Client Marathon URL string } type fakeEvent struct { data string } func getTestURL(urlString string) string { parsedURL, err := url.Parse(urlString) if err != nil { panic(fmt.Sprintf("failed to parse URL '%s': %s", urlString, err)) } return fmt.Sprintf("%s://%s", parsedURL.Scheme, strings.Join([]string{parsedURL.Host, parsedURL.Host, parsedURL.Host}, ",")) } func newFakeMarathonEndpoint(t *testing.T, configs *configContainer) *endpoint { // step: read in the fake responses if required initFakeMarathonResponses(t) // step: create a fake SSE event service eventSrv := eventsource.NewServer() // step: fill in the default if required defaultConfig := NewDefaultConfig() if configs == nil { configs = &configContainer{} } if configs.client == nil { configs.client = &defaultConfig } if configs.server == nil { configs.server = &serverConfig{} } fakeRespIndices := newResponseIndices() // step: create the HTTP router mux := http.NewServeMux() mux.HandleFunc("/v2/events", authMiddleware(configs.server, eventSrv.Handler("event"))) mux.HandleFunc("/", authMiddleware(configs.server, func(writer http.ResponseWriter, reader *http.Request) { respKey := fakeResponseMapKey(reader.Method, reader.RequestURI, configs.server.scope) fakeRespIndices.Lock() fakeRespIndex := fakeRespIndices.m[respKey] fakeRespIndices.m[respKey]++ responses, found := fakeResponses[respKey] fakeRespIndices.Unlock() if found { for _, response := range responses { // Index < 0 indicates a static response. if response.Index < 0 || response.Index == fakeRespIndex { writer.Header().Add("Content-Type", "application/json") for k, v := range response.Headers { writer.Header().Add(k, v) } writer.Write([]byte(response.Content)) return } } } http.Error(writer, `{"message": "not found"}`, 404) })) // step: create HTTP test server httpSrv := httptest.NewServer(mux) if configs.client.URL == defaultConfig.URL { configs.client.URL = getTestURL(httpSrv.URL) } // step: create the client for the service client, err := NewClient(*configs.client) if err != nil { t.Fatalf("Failed to create the fake client, %s, error: %s", configs.client.URL, err) } return &endpoint{ Server: fakeServer{ eventSrv: eventSrv, httpSrv: httpSrv, fakeRespIndices: fakeRespIndices, }, Client: client, URL: configs.client.URL, } } // basicAuthMiddleware handles basic auth func basicAuthMiddleware(server *serverConfig, next http.HandlerFunc) func(http.ResponseWriter, *http.Request) { unauthorized := `{"message": "invalid username or password"}` return func(w http.ResponseWriter, r *http.Request) { // step: is authentication required? if server.username != "" && server.password != "" { u, p, found := r.BasicAuth() // step: if no auth found, error it if !found { http.Error(w, unauthorized, 401) return } // step: if username and password don't match, error it if server.username != u || server.password != p { http.Error(w, unauthorized, 401) return } } next(w, r) } } // authMiddleware handles basic auth and dcos_acs_token func authMiddleware(server *serverConfig, next http.HandlerFunc) func(http.ResponseWriter, *http.Request) { unauthorized := `{"message": "invalid username or password"}` return func(w http.ResponseWriter, r *http.Request) { // step: is authentication required? if server.dcosToken != "" { headerValue := r.Header.Get("Authorization") // step: if no auth found, error it if headerValue == "" { http.Error(w, unauthorized, 401) return } s := strings.Split(headerValue, "=") if s[1] != server.dcosToken { http.Error(w, unauthorized, 401) return } } else if server.username != "" && server.password != "" { u, p, found := r.BasicAuth() // step: if no auth found, error it if !found { http.Error(w, unauthorized, 401) return } // step: if username and password don't match, error it if server.username != u || server.password != p { http.Error(w, unauthorized, 401) return } } next(w, r) } } // initFakeMarathonResponses reads in the marathon fake responses from the yaml file func initFakeMarathonResponses(t *testing.T) { once.Do(func() { fakeResponses = make(map[string][]indexedResponse) var methods []*restMethod // step: read in the test method specification methodSpec, err := ioutil.ReadFile("./tests/rest-api/methods.yml") if err != nil { t.Fatalf("failed to read in the fake yaml responses: %s", err) } if err = yaml.Unmarshal([]byte(methodSpec), &methods); err != nil { t.Fatalf("failed to unmarshal the response: %s", err) } for _, method := range methods { key := fakeResponseMapKey(method.Method, method.URI, method.Scope) switch { case method.Content != "" && len(method.ContentSequence) > 0: panic("content and contentSequence must not be provided simultaneously") case len(method.ContentSequence) > 0: fakeResponses[key] = method.ContentSequence default: // This combines the cases where static content was defined or not. The // latter models an empty response (via an empty content) that should // not result into a 404. fakeResponses[key] = []indexedResponse{ indexedResponse{ // Index -1 indicates a static response. Index: -1, Content: method.Content, Headers: method.Headers, }, } } } }) } func fakeResponseMapKey(method, uri, scope string) string { return fmt.Sprintf("%s:%s:%s", method, uri, scope) } func (t fakeEvent) Id() string { return "0" } func (t fakeEvent) Event() string { return "MarathonEvent" } func (t fakeEvent) Data() string { return t.data } func (s *fakeServer) PublishEvent(event string) { s.eventSrv.Publish([]string{"event"}, fakeEvent{event}) } func (s *fakeServer) Close() { s.eventSrv.Close() s.httpSrv.Close() } func (e *endpoint) Close() { e.Server.Close() } ================================================ FILE: tests/app-definitions/TestApplicationString-1.5-output.json ================================================ { "id": "/my-app", "args": [ "/usr/sbin/apache2ctl", "-D", "FOREGROUND" ], "container": { "type": "DOCKER", "docker": { "image": "quay.io/gambol99/apache-php:latest" }, "portMappings": [ { "containerPort": 80, "hostPort": 0, "protocol": "tcp" }, { "containerPort": 443, "hostPort": 0, "protocol": "tcp" } ] }, "cpus": 0.1, "disk": 0, "networks": [ { "mode": "container/bridge" } ], "healthChecks": [ { "portIndex": 0, "path": "/health", "maxConsecutiveFailures": 3, "protocol": "HTTP", "gracePeriodSeconds": 30, "intervalSeconds": 5, "timeoutSeconds": 5 } ], "instances": 2, "mem": 64, "ports": null, "dependencies": null, "env": { "NAME": "frontend_http", "SERVICE_80_NAME": "test_http" } } ================================================ FILE: tests/app-definitions/TestApplicationString-output.json ================================================ { "id": "/my-app", "args": [ "/usr/sbin/apache2ctl", "-D", "FOREGROUND" ], "container": { "type": "DOCKER", "docker": { "image": "quay.io/gambol99/apache-php:latest", "network": "BRIDGE", "portMappings": [ { "containerPort": 80, "hostPort": 0, "protocol": "tcp" }, { "containerPort": 443, "hostPort": 0, "protocol": "tcp" } ] } }, "cpus": 0.1, "disk": 0, "healthChecks": [ { "portIndex": 0, "path": "/health", "maxConsecutiveFailures": 3, "protocol": "HTTP", "gracePeriodSeconds": 30, "intervalSeconds": 5, "timeoutSeconds": 5 } ], "instances": 2, "mem": 64, "ports": null, "dependencies": null, "env": { "NAME": "frontend_http", "SERVICE_80_NAME": "test_http" } } ================================================ FILE: tests/rest-api/methods.yml ================================================ - uri: /ping method: GET content: | pong - uri: /v2/apps/fake-app/versions method: GET content: | { "versions": [ "2014-04-04T06:25:31.399Z" ] } - uri: /v2/apps method: POST content: | { "args": null, "backoffFactor": 1.15, "backoffSeconds": 1, "maxLaunchDelaySeconds": 3600, "cmd": "env && python3 -m http.server $PORT0", "constraints": [ [ "hostname", "UNIQUE" ] ], "container": { "docker": { "image": "python:3" }, "type": "DOCKER", "volumes": [] }, "cpus": 0.25, "dependencies": [], "deployments": [ { "id": "f44fd4fc-4330-4600-a68b-99c7bd33014a" } ], "disk": 0.0, "env": {}, "executor": "", "healthChecks": [ { "command": null, "gracePeriodSeconds": 3, "intervalSeconds": 10, "maxConsecutiveFailures": 3, "path": "/", "portIndex": 0, "protocol": "HTTP", "timeoutSeconds": 5 } ], "id": "/fake-app", "instances": 2, "mem": 50.0, "requirePorts": false, "residency" : { "taskLostBehavior" : "RELAUNCH_AFTER_TIMEOUT", "relaunchEscalationTimeoutSeconds" : 60 }, "storeUrls": [], "upgradeStrategy": { "minimumHealthCapacity": 0.5, "maximumOverCapacity": 0.5 }, "user": null, "version": "2014-08-18T22:36:41.451Z" } - uri: /v2/apps method: GET content: | { "apps": [ { "args": null, "backoffFactor": 1.15, "backoffSeconds": 1, "cmd": "python3 -m http.server 8080", "constraints": [], "container": { "docker": { "image": "python:3" }, "portMappings": [ { "containerPort": 8080, "hostPort": 0, "servicePort": 9000, "protocol": "tcp" }, { "containerPort": 161, "hostPort": 0, "protocol": "udp" } ], "type": "DOCKER", "volumes": [] }, "cpus": 0.5, "dependencies": [], "deployments": [], "disk": 0.0, "networks": [ { "mode": "container/bridge" } ], "env": { "VAR": "VALUE", "SECRET1": { "secret": "secret0" } }, "executor": "", "healthChecks": [ { "command": null, "gracePeriodSeconds": 5, "intervalSeconds": 20, "maxConsecutiveFailures": 3, "path": "/", "portIndex": 0, "protocol": "HTTP", "timeoutSeconds": 20 } ], "id": "/fake-app", "instances": 2, "mem": 64.0, "requirePorts": false, "secrets": { "secret0": { "source": "secret/definition/id" } }, "storeUrls": [], "tasksRunning": 2, "tasksStaged": 0, "upgradeStrategy": { "minimumHealthCapacity": 1.0 }, "user": null, "version": "2014-09-25T02:26:59.256Z" }, { "args": null, "backoffFactor": 1.15, "backoffSeconds": 1, "cmd": "python3 -m http.server 8080", "constraints": [], "container": { "docker": { "image": "python:3" }, "portMappings": [ { "containerPort": 8080, "hostPort": 0, "servicePort": 9000, "protocol": "tcp" }, { "containerPort": 161, "hostPort": 0, "protocol": "udp" } ], "type": "DOCKER", "volumes": [] }, "cpus": 0.5, "dependencies": [], "deployments": [], "disk": 0.0, "networks": [ { "mode": "container/bridge" } ], "env": {}, "executor": "", "healthChecks": [ { "command": null, "gracePeriodSeconds": 5, "intervalSeconds": 20, "maxConsecutiveFailures": 3, "path": "/", "portIndex": 0, "protocol": "HTTP", "timeoutSeconds": 20 } ], "id": "/fake-app-broken", "instances": 2, "mem": 64.0, "requirePorts": false, "residency" : { "taskLostBehavior" : "RELAUNCH_AFTER_TIMEOUT", "relaunchEscalationTimeoutSeconds" : 60 }, "storeUrls": [], "tasksRunning": 2, "tasksStaged": 0, "upgradeStrategy": { "minimumHealthCapacity": 1.0 }, "user": null, "version": "2014-09-25T02:26:59.256Z" } ] } - uri: /v2/apps?embed=apps.taskStats method: GET content: | { "apps": [ { "taskStats": { "startedAfterLastScaling": { "stats": { "counts": { "staged": 0, "running": 1, "healthy": 1, "unhealthy": 0 }, "lifeTime": { "averageSeconds": 17024.575, "medianSeconds": 17024.575 } } } } } ] } - uri: /v2/apps?cmd=nginx method: GET content: | { "apps": [ { "args": null, "cmd": "nginx" } ] } - uri: /v2/apps/fake-app/restart method: POST content: | { "deploymentId": "83b215a6-4e26-4e44-9333-5c385eda6438", "version": "2014-08-26T07:37:50.462Z" } - uri: /v2/apps/fake-app method: DELETE content: | { "deploymentId": "83b215a6-4e26-4e44-9333-5c385eda6438", "version": "2014-08-26T07:37:50.462Z" } - uri: /v2/apps/fake-app?force=true method: DELETE content: | { "deploymentId": "83b215a6-4e26-4e44-9333-5c385eda6438", "version": "2014-08-26T07:37:50.462Z" } - uri: /v2/apps/fake-app/tasks method: GET content: | { "tasks": [{"id": "1"},{"id": "2"}] } - uri: /v2/apps/fake-app/tasks method: DELETE content: | { "tasks": [] } - uri: /v2/apps/fake-app/tasks/fake-app.fake-task method: DELETE content: | { "task": {"id": "fake-app.fake-task"} } - uri: /v2/apps/fake-group/fake-app/tasks/fake-group_fake-app.fake-task method: DELETE content: | { "task": {"id": "fake-group_fake-app.fake-task"} } - uri: /v2/apps/fake-app/versions/2014-09-12T23:28:21.737Z method: GET content: | { "args": null, "backoffFactor": 1.15, "backoffSeconds": 1, "cmd": "python toggle.py $PORT0", "constraints": [], "container": { "docker": { "image": "python:3" }, "portMappings": [ { "containerPort": 8080, "hostPort": 0, "servicePort": 9000, "protocol": "tcp" } ], "type": "DOCKER", "volumes": [] }, "cpus": 0.2, "dependencies": [], "deployments": [], "disk": 0.0, "networks": [ { "mode": "container/bridge" } ], "env": {}, "executor": "", "healthChecks": [ { "command": null, "gracePeriodSeconds": 5, "intervalSeconds": 10, "maxConsecutiveFailures": 3, "path": "/health", "portIndex": 0, "protocol": "HTTP", "timeoutSeconds": 10 } ], "id": "/fake-app", "instances": 2, "lastTaskFailure": { "appId": "/toggle", "host": "10.141.141.10", "message": "Abnormal executor termination", "slaveId": "14ac45bf-9a40-42cf-94ec-695130865592-S0", "state": "TASK_FAILED", "taskId": "toggle.cc427e60-5046-11e4-9e34-56847afe9799", "timestamp": "2014-09-12T23:23:41.711Z", "version": "2014-09-12T23:28:21.737Z" }, "mem": 32.0, "requirePorts": false, "residency" : { "taskLostBehavior" : "RELAUNCH_AFTER_TIMEOUT", "relaunchEscalationTimeoutSeconds" : 60 }, "storeUrls": [], "tasks": [ { "appId": "/toggle", "healthCheckResults": [ { "alive": true, "consecutiveFailures": 0, "firstSuccess": "2014-09-13T00:20:28.101Z", "lastFailure": null, "lastSuccess": "2014-09-13T00:25:07.506Z", "taskId": "toggle.802df2ae-3ad4-11e4-a400-56847afe9799" } ], "host": "10.141.141.10", "id": "toggle.802df2ae-3ad4-11e4-a400-56847afe9799", "ports": [ 31045 ], "stagedAt": "2014-09-12T23:28:28.594Z", "startedAt": "2014-09-13T00:24:46.959Z", "version": "2014-09-12T23:28:21.737Z" }, { "appId": "/toggle", "healthCheckResults": [ { "alive": true, "consecutiveFailures": 0, "firstSuccess": "2014-09-13T00:20:28.101Z", "lastFailure": null, "lastSuccess": "2014-09-13T00:25:07.508Z", "taskId": "toggle.7c99814d-3ad4-11e4-a400-56847afe9799" } ], "host": "10.141.141.10", "id": "toggle.7c99814d-3ad4-11e4-a400-56847afe9799", "ports": [ 31234 ], "stagedAt": "2014-09-12T23:28:22.587Z", "startedAt": "2014-09-13T00:24:46.965Z", "version": "2014-09-12T23:28:21.737Z" } ], "tasksRunning": 2, "tasksStaged": 0, "upgradeStrategy": { "minimumHealthCapacity": 1.0 }, "user": null, "version": "2014-09-12T23:28:21.737Z" } - uri: /v2/apps/fake-app method: GET content: | { "app": { "args": null, "backoffFactor": 1.15, "backoffSeconds": 1, "cmd": "python toggle.py $PORT0", "constraints": [], "container": { "docker": { "image": "python:3" }, "portMappings": [ { "containerPort": 8080, "hostPort": 0, "servicePort": 9000, "protocol": "tcp" } ], "type": "DOCKER", "volumes": [] }, "cpus": 0.2, "dependencies": [], "deployments": [], "disk": 0.0, "networks": [ { "mode": "container/bridge" } ], "env": {}, "executor": "", "healthChecks": [ { "command": null, "gracePeriodSeconds": 5, "intervalSeconds": 10, "maxConsecutiveFailures": 3, "path": "/health", "portIndex": 0, "protocol": "HTTP", "timeoutSeconds": 10 } ], "id": "/fake-app", "instances": 2, "lastTaskFailure": { "appId": "/toggle", "host": "10.141.141.10", "message": "Abnormal executor termination", "slaveId": "14ac45bf-9a40-42cf-94ec-695130865592-S0", "state": "TASK_FAILED", "taskId": "toggle.cc427e60-5046-11e4-9e34-56847afe9799", "timestamp": "2014-09-12T23:23:41.711Z", "version": "2014-09-12T23:28:21.737Z" }, "mem": 32.0, "requirePorts": false, "residency" : { "taskLostBehavior" : "RELAUNCH_AFTER_TIMEOUT", "relaunchEscalationTimeoutSeconds" : 60 }, "storeUrls": [], "tasks": [ { "appId": "/toggle", "healthCheckResults": [ { "alive": true, "consecutiveFailures": 0, "firstSuccess": "2014-09-13T00:20:28.101Z", "lastFailure": null, "lastSuccess": "2014-09-13T00:25:07.506Z", "taskId": "toggle.802df2ae-3ad4-11e4-a400-56847afe9799" } ], "host": "10.141.141.10", "id": "toggle.802df2ae-3ad4-11e4-a400-56847afe9799", "ports": [ 31045 ], "stagedAt": "2014-09-12T23:28:28.594Z", "startedAt": "2014-09-13T00:24:46.959Z", "version": "2014-09-12T23:28:21.737Z" }, { "appId": "/toggle", "healthCheckResults": [ { "alive": true, "consecutiveFailures": 0, "firstSuccess": "2014-09-13T00:20:28.101Z", "lastFailure": null, "lastSuccess": "2014-09-13T00:25:07.508Z", "taskId": "toggle.7c99814d-3ad4-11e4-a400-56847afe9799" } ], "host": "10.141.141.10", "id": "toggle.7c99814d-3ad4-11e4-a400-56847afe9799", "ports": [ 31234 ], "stagedAt": "2014-09-12T23:28:22.587Z", "startedAt": "2014-09-13T00:24:46.965Z", "version": "2014-09-12T23:28:21.737Z" } ], "tasksRunning": 2, "tasksStaged": 0, "upgradeStrategy": { "minimumHealthCapacity": 1.0 }, "user": null, "version": "2014-09-12T23:28:21.737Z" } } - uri: /v2/apps/fake-app method: GET scope: wait-on-app contentSequence: - index: 1 content: | { "app": { } } - uri: /v2/apps/fake-app-broken method: GET content: | { "app": { "args": null, "backoffFactor": 1.15, "backoffSeconds": 1, "cmd": "python toggle.py $PORT0", "constraints": [], "container": { "docker": { "image": "python:3" }, "portMappings": [ { "containerPort": 8080, "hostPort": 0, "servicePort": 9000, "protocol": "tcp" } ], "type": "DOCKER", "volumes": [] }, "cpus": 0.2, "dependencies": [], "deployments": [], "disk": 0.0, "networks": [ { "mode": "container/bridge" } ], "env": {}, "executor": "", "healthChecks": [ { "command": null, "gracePeriodSeconds": 5, "intervalSeconds": 10, "maxConsecutiveFailures": 3, "path": "/health", "portIndex": 0, "protocol": "HTTP", "timeoutSeconds": 10 } ], "id": "/fake-app-broken", "instances": 2, "lastTaskFailure": { "appId": "/toggle", "host": "10.141.141.10", "message": "Abnormal executor termination", "state": "TASK_FAILED", "taskId": "toggle.cc427e60-5046-11e4-9e34-56847afe9799", "timestamp": "2014-09-12T23:23:41.711Z", "version": "2014-09-12T23:28:21.737Z" }, "mem": 32.0, "requirePorts": false, "residency" : { "taskLostBehavior" : "RELAUNCH_AFTER_TIMEOUT", "relaunchEscalationTimeoutSeconds" : 60 }, "storeUrls": [], "tasks": [ { "appId": "/toggle", "healthCheckResults": [ { "alive": true, "consecutiveFailures": 0, "firstSuccess": "2014-09-13T00:20:28.101Z", "lastFailure": null, "lastSuccess": "2014-09-13T00:25:07.506Z", "taskId": "toggle.802df2ae-3ad4-11e4-a400-56847afe9799" } ], "host": "10.141.141.10", "id": "toggle.802df2ae-3ad4-11e4-a400-56847afe9799", "ports": [ 31045 ], "stagedAt": "2014-09-12T23:28:28.594Z", "startedAt": "2014-09-13T00:24:46.959Z", "version": "2014-09-12T23:28:21.737Z" }, { "appId": "/toggle", "healthCheckResults": [ { "alive": false, "consecutiveFailures": 0, "firstSuccess": "2014-09-13T00:20:28.101Z", "lastFailure": null, "lastSuccess": "2014-09-13T00:25:07.508Z", "taskId": "toggle.7c99814d-3ad4-11e4-a400-56847afe9799" } ], "host": "10.141.141.10", "id": "toggle.7c99814d-3ad4-11e4-a400-56847afe9799", "ports": [ 31234 ], "stagedAt": "2014-09-12T23:28:22.587Z", "startedAt": "2014-09-13T00:24:46.965Z", "version": "2014-09-12T23:28:21.737Z" } ], "tasksRunning": 2, "tasksStaged": 0, "upgradeStrategy": { "minimumHealthCapacity": 1.0 }, "user": null, "version": "2014-09-12T23:28:21.737Z" } } - uri: /v2/apps/fake-app/versions method: GET content: | { "versions": [ "2014-04-04T06:25:31.399Z" ] } - uri: /v2/apps/fake-app method: PUT content: | { "deploymentId": "83b215a6-4e26-4e44-9333-5c385eda6438", "version": "2014-08-26T07:37:50.462Z" } - uri: /v2/apps/fake-app?force=true method: PUT content: | { "deploymentId": "83b215a6-4e26-4e44-9333-5c385eda6438", "version": "2014-08-26T07:37:50.462Z" } - uri: /v2/pods method: HEAD - uri: /v2/pods/fake-pod::status method: GET content: | { "id": "/fake-pod", "spec": { "id": "/fake-pod", "labels": { "key": "value" }, "version": "2014-08-18T22:36:41.451Z", "user": "nobody", "environment": { "key": { "secret": "secret0" } }, "containers": [], "secrets": { "secret0": { "source": "source0" } }, "volumes": [], "networks": [], "scaling": { "kind": "fixed", "instances": 1 }, "scheduling": {} }, "status": "STABLE", "statusSince": "2017-07-13T21:33:17.349Z", "instances": [ { "id": "fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003", "status": "STABLE", "statusSince": "2017-07-13T21:33:17.349Z", "conditions": [], "agentHostname": "192.168.99.100", "resources": { "cpus": 0.1, "mem": 64, "disk": 0, "gpus": 0 }, "networks": [], "containers": [ { "name": "container1", "status": "TASK_RUNNING", "statusSince": "2017-07-13T21:33:17.349Z", "conditions": [], "containerId": "fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003.container1", "endpoints": [], "resources": { "cpus": 0.1, "mem": 32, "disk": 0, "gpus": 0 }, "lastUpdated": "2017-07-13T21:33:17.349Z", "lastChanged": "2017-07-13T21:33:17.349Z" } ], "specReference": "/v2/pods/fake-pod::versions/2017-07-13T21:33:07.389Z", "lastUpdated": "2017-07-13T21:33:17.349Z", "lastChanged": "2017-07-13T21:33:17.349Z" } ], "terminationHistory": [], "lastUpdated": "2017-07-13T22:13:46.635Z", "lastChanged": "2017-07-13T21:33:17.349Z" } - uri: /v2/pods/fake-pod2::status method: GET content: | { "id": "/fake-pod2", "spec": { "id": "/fake-pod2", "labels": { "key": "value" }, "version": "2014-08-18T22:36:41.451Z", "user": "nobody", "environment": { "key": { "secret": "secret0" } }, "containers": [], "secrets": { "secret0": { "source": "source0" } }, "volumes": [], "networks": [], "scaling": { "kind": "fixed", "instances": 1 }, "scheduling": {} }, "status": "DEGRADED", "statusSince": "2017-07-13T21:33:17.349Z", "instances": [ { "id": "fake-pod2.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003", "status": "DEGRADED", "statusSince": "2017-07-13T21:33:17.349Z", "conditions": [], "agentHostname": "192.168.99.100", "resources": { "cpus": 0.1, "mem": 64, "disk": 0, "gpus": 0 }, "networks": [], "containers": [ { "name": "container1", "status": "TASK_RUNNING", "statusSince": "2017-07-13T21:33:17.349Z", "conditions": [], "containerId": "fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003.container1", "endpoints": [], "resources": { "cpus": 0.1, "mem": 32, "disk": 0, "gpus": 0 }, "lastUpdated": "2017-07-13T21:33:17.349Z", "lastChanged": "2017-07-13T21:33:17.349Z" } ], "specReference": "/v2/pods/fake-pod::versions/2017-07-13T21:33:07.389Z", "lastUpdated": "2017-07-13T21:33:17.349Z", "lastChanged": "2017-07-13T21:33:17.349Z" } ], "terminationHistory": [], "lastUpdated": "2017-07-13T22:13:46.635Z", "lastChanged": "2017-07-13T21:33:17.349Z" } - uri: /v2/pods/::status method: GET content: | [ { "id": "/fake-pod", "spec": { "id": "/fake-pod", "labels": { "key": "value" }, "version": "2014-08-18T22:36:41.451Z", "user": "nobody", "environment": { "key": { "secret": "secret0" } }, "containers": [], "secrets": { "secret0": { "source": "source0" } }, "volumes": [], "networks": [], "scaling": { "kind": "fixed", "instances": 1 }, "scheduling": {} }, "status": "STABLE", "statusSince": "2017-07-13T21:33:17.349Z", "instances": [ { "id": "fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003", "status": "STABLE", "statusSince": "2017-07-13T21:33:17.349Z", "conditions": [], "agentHostname": "192.168.99.100", "resources": { "cpus": 0.1, "mem": 64, "disk": 0, "gpus": 0 }, "networks": [], "containers": [ { "name": "container1", "status": "TASK_RUNNING", "statusSince": "2017-07-13T21:33:17.349Z", "conditions": [], "containerId": "fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003.container1", "endpoints": [], "resources": { "cpus": 0.1, "mem": 32, "disk": 0, "gpus": 0 }, "lastUpdated": "2017-07-13T21:33:17.349Z", "lastChanged": "2017-07-13T21:33:17.349Z" } ], "specReference": "/v2/pods/fake-pod::versions/2017-07-13T21:33:07.389Z", "lastUpdated": "2017-07-13T21:33:17.349Z", "lastChanged": "2017-07-13T21:33:17.349Z" } ], "terminationHistory": [], "lastUpdated": "2017-07-13T22:13:46.635Z", "lastChanged": "2017-07-13T21:33:17.349Z" } ] - uri: /v2/pods/fake-pod method: GET content: | { "id": "/fake-pod", "labels": { "key": "value" }, "version": "2014-08-18T22:36:41.451Z", "user": "nobody", "environment": { "key1": "value", "key2": { "secret": "secret0" } }, "containers": [ { "environment": { "key3": "value3", "key4": { "secret": "secret1" } } } ], "secrets": { "secret0": { "source": "source0" }, "secret1": { "source": "source1" } }, "volumes": [ ], "networks": [ ], "scaling": { "kind": "fixed", "instances": 1 }, "scheduling": { } } - uri: /v2/pods method: GET content: | [ { "id": "/fake-pod", "labels": { "key": "value" }, "version": "2014-08-18T22:36:41.451Z", "user": "nobody", "environment": { "key1": "value", "key2": { "secret": "secret0" } }, "containers": [ ], "secrets": { "secret0": { "source": "source0" } }, "volumes": [ ], "networks": [ ], "scaling": { "kind": "fixed", "instances": 1 }, "scheduling": { } }, { "id": "/fake-pod2", "labels": { "key": "value" }, "version": "2014-08-18T22:36:41.451Z", "user": "nobody", "environment": { "key": "value", "key": { "secret": "secret0" } }, "containers": [ ], "secrets": { "secret0": { "source": "source0" } }, "volumes": [ ], "networks": [ ], "scaling": { "kind": "fixed", "instances": 1 }, "scheduling": { } } ] - uri: /v2/pods method: POST content: | { "id": "/fake-pod", "labels": { "key": "value" }, "version": "2014-08-18T22:36:41.451Z", "user": "nobody", "environment": { "key": "value", "key": { "secret": "secret0" } }, "containers": [ ], "secrets": { "secret0": { "source": "source0" } }, "volumes": [ ], "networks": [ ], "scaling": { "kind": "fixed", "instances": 1 }, "scheduling": { } } - uri: /v2/pods/fake-pod?force=true method: PUT content: | { "id": "/fake-pod", "labels": { "key": "value" }, "version": "2014-08-18T22:36:41.451Z", "user": "nobody", "environment": { "key": "value", "key": { "secret": "secret0" } }, "containers": [ ], "secrets": { "secret0": { "source": "source0" } }, "volumes": [ ], "networks": [ ], "scaling": { "kind": "fixed", "instances": 2 }, "scheduling": { } } - uri: /v2/pods/fake-pod?force=true method: DELETE headers: "Marathon-Deployment-Id": "c0e7434c-df47-4d23-99f1-78bd78662231" - uri: /v2/pods/fake-pod::versions method: GET content: | [ "2014-08-18T22:36:41.451Z" ] - uri: /v2/pods/fake-pod::versions/2014-08-18T22:36:41.451Z method: GET content: | { "id": "/fake-pod", "labels": { "key": "value" }, "version": "2014-08-18T22:36:41.451Z", "user": "nobody", "environment": { "key": "value", "key": { "secret": "secret0" } }, "containers": [ ], "secrets": { "secret0": { "source": "source0" } }, "volumes": [ ], "networks": [ ], "scaling": { "kind": "fixed", "instances": 1 }, "scheduling": { } } - uri: /v2/pods/fake-pod::instances/fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003 method: DELETE content: | { "instanceId": { "idString": "fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003" }, "agentInfo": { "host": "192.168.99.100", "agentId": "c3856950-ab1a-4aea-b1a8-168397c0fb33-S9", "attributes": [ "CgR6b25lEAMqBgoEcGR4Mw==", "CgZyZWdpb24QABoJCQAAAAAAAAhA", "Cg1pbnN0YW5jZV90eXBlEAMqCQoHY29tcHV0ZQ==", "CgNzZG4QAyoKCghjb250cmFpbA==", "CgJvcxADKgkKB2NlbnRvczc=" ] }, "tasksMap": { "fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003.container1": { "taskId": "fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003.container1", "runSpecVersion": "2017-07-13T23:04:05.835Z", "status": { "stagedAt": "2017-07-13T23:04:05.939Z", "startedAt": "2017-07-13T23:04:07.767Z", "mesosStatus": "Ck0KS3JjbHVzdGVyX2Rjb3Mtc2lkZWNhci5pbnN0YW5jZS05MWU5YzE1OC02ODFmLTExZTctYTE4ZS03MGIzZDU4MDAwMDMuc2lkZWNhchABKikKJ2MzODU2OTUwLWFiMWEtNGFlYS1iMWE4LTE2ODM5N2MwZmIzMy1TOTGxw/AZ/1nWQTpFCkNpbnN0YW5jZS1yY2x1c3Rlcl9kY29zLXNpZGVjYXIuOTFlOWMxNTgtNjgxZi0xMWU3LWExOGUtNzBiM2Q1ODAwMDAzSAJaEOVRUUkSVEbRi5mOLzx0bnxqkgIKvAEimAEKLgoRcmNsdXN0ZXIubG9jYXRpb24SGWdsb2JhbHJpb3QucGR4Mi5yY2x1c3RlcjEKGgoOcmNsdXN0ZXIuZ3JvdXASCHJjbHVzdGVyCiQKFHJjbHVzdGVyLmFwcGxpY2F0aW9uEgxkY29zLXNpZGVjYXIKJAoKRENPU19TUEFDRRIWL3JjbHVzdGVyL2Rjb3Mtc2lkZWNhcioRCAESDTEwLjQwLjI1My4xMDAyDHJjbHVzdGVyLWNuaRiiWyJOCiRiYjgyOWJiZS02OWExLTRiMmQtYWE4Zi0zNGY5MDQ4YjNiMjUSJgokY2UyZDFjOWMtZGQ5MC00MDYyLWI1M2QtYzdkNTRiZGI2ODI1", "condition": { "str": "running" }, "networkInfo": { "hostName": "192.168.99.100", "hostPorts": [], "ipAddresses": [ { "ipAddress": "192.168.99.100", "protocol": "IPv4" } ] } } } }, "runSpecVersion": "2017-07-13T23:04:05.835Z", "state": { "condition": { "str": "running" }, "since": "2017-07-13T23:04:07.767Z", "activeSince": "2017-07-13T23:04:07.767Z" }, "unreachableStrategy": { "inactiveAfterSeconds": 300, "expungeAfterSeconds": 600 } } - uri: /v2/pods/fake-pod::instances method: DELETE content: | [ { "instanceId": { "idString": "fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003" }, "agentInfo": { "host": "192.168.99.100", "agentId": "c3856950-ab1a-4aea-b1a8-168397c0fb33-S9", "attributes": [ "CgR6b25lEAMqBgoEcGR4Mw==", "CgZyZWdpb24QABoJCQAAAAAAAAhA", "Cg1pbnN0YW5jZV90eXBlEAMqCQoHY29tcHV0ZQ==", "CgNzZG4QAyoKCghjb250cmFpbA==", "CgJvcxADKgkKB2NlbnRvczc=" ] }, "tasksMap": { "fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003.container1": { "taskId": "fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003.container1", "runSpecVersion": "2017-07-13T23:04:05.835Z", "status": { "stagedAt": "2017-07-13T23:04:05.939Z", "startedAt": "2017-07-13T23:04:07.767Z", "mesosStatus": "Ck0KS3JjbHVzdGVyX2Rjb3Mtc2lkZWNhci5pbnN0YW5jZS05MWU5YzE1OC02ODFmLTExZTctYTE4ZS03MGIzZDU4MDAwMDMuc2lkZWNhchABKikKJ2MzODU2OTUwLWFiMWEtNGFlYS1iMWE4LTE2ODM5N2MwZmIzMy1TOTGxw/AZ/1nWQTpFCkNpbnN0YW5jZS1yY2x1c3Rlcl9kY29zLXNpZGVjYXIuOTFlOWMxNTgtNjgxZi0xMWU3LWExOGUtNzBiM2Q1ODAwMDAzSAJaEOVRUUkSVEbRi5mOLzx0bnxqkgIKvAEimAEKLgoRcmNsdXN0ZXIubG9jYXRpb24SGWdsb2JhbHJpb3QucGR4Mi5yY2x1c3RlcjEKGgoOcmNsdXN0ZXIuZ3JvdXASCHJjbHVzdGVyCiQKFHJjbHVzdGVyLmFwcGxpY2F0aW9uEgxkY29zLXNpZGVjYXIKJAoKRENPU19TUEFDRRIWL3JjbHVzdGVyL2Rjb3Mtc2lkZWNhcioRCAESDTEwLjQwLjI1My4xMDAyDHJjbHVzdGVyLWNuaRiiWyJOCiRiYjgyOWJiZS02OWExLTRiMmQtYWE4Zi0zNGY5MDQ4YjNiMjUSJgokY2UyZDFjOWMtZGQ5MC00MDYyLWI1M2QtYzdkNTRiZGI2ODI1", "condition": { "str": "running" }, "networkInfo": { "hostName": "192.168.99.100", "hostPorts": [], "ipAddresses": [ { "ipAddress": "192.168.99.100", "protocol": "IPv4" } ] } } } }, "runSpecVersion": "2017-07-13T23:04:05.835Z", "state": { "condition": { "str": "running" }, "since": "2017-07-13T23:04:07.767Z", "activeSince": "2017-07-13T23:04:07.767Z" }, "unreachableStrategy": { "inactiveAfterSeconds": 300, "expungeAfterSeconds": 600 } } ] - uri: /v2/groups method: GET content: | { "apps": [], "dependencies": [], "groups": [ { "apps": [ { "args": null, "backoffFactor": 1.15, "backoffSeconds": 1, "maxLaunchDelaySeconds": 3600, "cmd": "sleep 30", "constraints": [], "container": null, "cpus": 1.0, "dependencies": [], "disk": 0.0, "env": {}, "executor": "", "healthChecks": [], "id": "/test/app", "instances": 1, "mem": 128.0, "requirePorts": false, "storeUrls": [], "upgradeStrategy": { "minimumHealthCapacity": 1.0 }, "user": null, "version": "2014-08-28T01:05:40.586Z" } ], "dependencies": [], "groups": [], "id": "/test", "version": "2014-08-28T01:09:46.212Z" } ], "id": "/", "version": "2014-08-28T01:09:46.212Z" } - uri: /v2/groups/test method: GET content: | { "apps": [ { "args": null, "backoffFactor": 1.15, "backoffSeconds": 1, "maxLaunchDelaySeconds": 3600, "cmd": "sleep 30", "constraints": [], "container": null, "cpus": 1.0, "dependencies": [], "disk": 0.0, "env": {}, "executor": "", "healthChecks": [], "id": "/test/app", "instances": 1, "mem": 128.0, "requirePorts": false, "storeUrls": [], "upgradeStrategy": { "minimumHealthCapacity": 1.0 }, "user": null, "version": "2014-08-28T01:05:40.586Z" } ], "dependencies": [], "groups": [], "id": "/test", "version": "2014-08-28T01:09:46.212Z" } - uri: /v2/groups/qa/product/1 method: GET content: | { "id": "/qa/product/1", "groups": [ { "id": "frontend", "apps": [ { "cmd": "", "container": { "type": "DOCKER", "docker": { "image": "quay.io/gambol99/apache-php:latest" }, "portMappings": [ { "containerPort": 80, "hostPort": 0, "protocol": "tcp" }, { "containerPort": 443, "hostPort": 0, "protocol": "tcp" } ] }, "healthChecks": [ { "protocol": "HTTP", "path": "/hostname.php", "gracePeriodSeconds": 3, "intervalSeconds": 10, "portIndex": 0, "timeoutSeconds": 10, "maxConsecutiveFailures": 3 } ], "id": "apache", "mem": 64, "args": [], "networks": [ { "mode": "container/bridge" } ], "env": { "ENVIRONMENT": "qa", "SERVICE_80_NAME": "apache_http-qa-1", "SERVICE_443_NAME": "apache_https-qa-1", "NAME": "frontend", "BACKEND_MYSQL_MASTER": "mysql-qa-1;3306", "BACKEND_CACHE": "redis-qa-1;6379" }, "dependencies": [ "/qa/product/1/database", "/qa/product/1/caching" ] },{ "id": "database", "container": { "type": "DOCKER", "docker": { "image": "tutum/mysql" }, "portMappings": [ { "containerPort": 3306, "hostPort": 0, "protocol": "tcp" } ] }, "healthChecks": [ { "portIndex": 0, "protocol": "TCP", "gracePeriodSeconds": 10, "intervalSeconds": 10, "timeoutSeconds": 5, "maxConsecutiveFailures": 2 } ], "id": "mysql", "mem": 1024, "cmd": "", "networks": [ { "mode": "container/bridge" } ], "env": { "ENVIRONMENT": "qa", "SERVICE_NAME": "dbmaster", "SERVICE_3306_NAME": "mysql-qa-1", "MYSQL_PASS": "mysql", "REPLICATION_MASTER": "true", "REPLICATION_USER": "replication", "REPLICATION_PASS": "8d67as9f7sjhsdfsd" } },{ "container": { "type": "DOCKER", "docker": { "image": "redis" }, "portMappings": [ { "containerPort": 6379, "hostPort": 0, "protocol": "tcp" } ] }, "healthChecks": [ { "portIndex": 0, "protocol": "TCP", "gracePeriodSeconds": 10, "intervalSeconds": 10, "timeoutSeconds": 5, "maxConsecutiveFailures": 2 } ], "id": "caching", "cmd": "", "mem": 128, "networks": [ { "mode": "container/bridge" } ], "env": { "ENVIRONMENT": "qa", "SERVICE_6379_NAME": "redis-qa-1" } } ] } ] } - uri: /v2/groups/:groupId method: PUT content: | { "deploymentId": "c0e7434c-df47-4d23-99f1-78bd78662231", "version": "2014-08-28T16:45:41.063Z" } - uri: /v2/groups/:groupId method: DELETE content: | {"version":"2014-07-01T10:20:50.196Z"} - uri: /v2/tasks method: GET content: | { "tasks": [ { "appId": "/bridged-webapp", "healthCheckResults": [ { "alive": true, "consecutiveFailures": 0, "firstSuccess": "2014-10-03T22:57:02.246Z", "lastFailure": null, "lastSuccess": "2014-10-03T22:57:41.643Z", "taskId": "bridged-webapp.eb76c51f-4b4a-11e4-ae49-56847afe9799" } ], "host": "10.141.141.10", "id": "bridged-webapp.eb76c51f-4b4a-11e4-ae49-56847afe9799", "ports": [ 31000 ], "servicePorts": [ 9000 ], "stagedAt": "2014-10-03T22:16:27.811Z", "startedAt": "2014-10-03T22:57:41.587Z", "version": "2014-10-03T22:16:23.634Z" }, { "appId": "/bridged-webapp", "healthCheckResults": [ { "alive": true, "consecutiveFailures": 0, "firstSuccess": "2014-10-03T22:57:02.246Z", "lastFailure": null, "lastSuccess": "2014-10-03T22:57:41.649Z", "taskId": "bridged-webapp.ef0b5d91-4b4a-11e4-ae49-56847afe9799" } ], "host": "10.141.141.10", "id": "bridged-webapp.ef0b5d91-4b4a-11e4-ae49-56847afe9799", "ports": [ 31001 ], "servicePorts": [ 9000 ], "stagedAt": "2014-10-03T22:16:33.814Z", "startedAt": "2014-10-03T22:57:41.593Z", "version": "2014-10-03T22:16:23.634Z" } ] } - uri: /v2/tasks?status=staging method: GET content: | {"tasks":[]} - uri: /v2/tasks/delete method: POST - uri: /v2/deployments method: GET content: | [ { "affectedApps": [ "/test" ], "id": "867ed450-f6a8-4d33-9b0e-e11c5513990b", "steps": [ [ { "action": "ScaleApplication", "app": "/test" } ] ], "currentActions": [ { "action": "ScaleApplication", "app": "/test" } ], "version": "2014-08-26T08:18:03.595Z", "currentStep": 1, "totalSteps": 1 } ] - uri: /v2/deployments method: GET scope: v1.1.1 content: | [ { "id": "2620aa06-1001-4eea-8861-a51957d4fd80", "version": "2016-06-06T16:06:11.612Z", "affectedApps": [ "/test-app-v1" ], "steps": [ { "actions": [ { "type": "StartApplication", "app": "/test-app-v1" } ] }, { "actions": [ { "type": "ScaleApplication", "app": "/test-app-v1" } ] } ], "currentActions": [ { "action": "ScaleApplication", "app": "/test-app-v1", "readinessCheckResults": [ { "name": "myReadyCheck", "taskId": "test_frontend_app1.c9de6033", "ready": false, "lastResponse": { "body": "{}", "contentType": "application/json", "status": 500 } } ] } ], "currentStep": 2, "totalSteps": 2 } ] - uri: /v2/deployments/867ed450-f6a8-4d33-9b0e-e11c5513990b method: DELETE content: | { "deploymentId": "0b1467fc-d5cd-4bbc-bac2-2805351cee1e", "version": "2014-08-26T08:20:26.171Z" } - uri: /v2/deployments/867ed450-f6a8-4d33-9b0e-e11c5513990b?force=true method: DELETE content: | { } - uri: /v2/eventSubscriptions?callbackUrl=http://localhost:9292/callback method: POST content: | { "callbackUrl": "http://localhost:9292/callback", "clientIp": "0:0:0:0:0:0:0:1", "eventType": "subscribe_event" } - uri: /v2/eventSubscriptions method: POST content: | { "callbackUrl": "http://localhost:9292/callback", "clientIp": "0:0:0:0:0:0:0:1", "eventType": "subscribe_event" } - uri: /v2/eventSubscriptions method: GET content: | { "callbackUrls": [ "http://localhost:9292/callback" ] } - uri: /v2/eventSubscriptions?callbackUrl=http://localhost:9292/callback method: DELETE content: | { "callbackUrl": "http://localhost:9292/callback", "clientIp": "0:0:0:0:0:0:0:1", "eventType": "unsubscribe_event" } - uri: /v2/queue method: GET content: | { "queue": [ { "count": 10, "delay": { "overdue": true, "timeLeftSeconds": 784 }, "app": { "args": null, "backoffFactor": 1.15, "backoffSeconds": 1, "cmd": "python toggle.py $PORT0", "constraints": [], "container": null, "cpus": 0.2, "dependencies": [], "disk": 0.0, "env": {}, "executor": "", "healthChecks": [], "id": "/test", "instances": 3, "mem": 32.0, "requirePorts": false, "storeUrls": [], "upgradeStrategy": { "minimumHealthCapacity": 1.0 }, "user": null, "version": "2014-08-26T05:04:49.766Z" } } ] } - uri: /v2/queue/fake-app/delay method: DELETE - uri: /v2/leader method: GET content: | { "leader": "127.0.0.1:8080" } - uri: /v2/leader method: DELETE content: | { "message": "Leadership abdicted" } - uri: /v2/info method: GET content: | { "frameworkId": "20140730-222531-1863654316-5050-10422-0000", "leader": "127.0.0.1:8080", "http_config": { "assets_path": null, "http_port": 8080, "https_port": 8443 }, "event_subscriber": { "type": "http_callback", "http_endpoints": [ "localhost:9999/events" ] }, "marathon_config": { "checkpoint": false, "executor": "//cmd", "failover_timeout": 604800, "ha": true, "hostname": "127.0.0.1", "local_port_max": 49151, "local_port_min": 32767, "master": "zk://localhost:2181/mesos", "mesos_role": null, "mesos_user": "root", "reconciliation_initial_delay": 30000, "reconciliation_interval": 30000, "task_launch_timeout": 60000 }, "name": "marathon", "version": "0.7.0-SNAPSHOT", "zookeeper_config": { "zk": "zk://localhost:2181/marathon", "zk_future_timeout": { "duration": 10 }, "zk_hosts": "localhost:2181", "zk_path": "/marathon", "zk_state": "/marathon", "zk_timeout": 10 } } - uri: /ping method: GET content: | pong - uri: /marathon/v2/apps method: GET content: | { "apps": [ { "args": null, "backoffFactor": 1.15, "backoffSeconds": 1, "cmd": "python3 -m http.server 8080", "constraints": [], "container": { "docker": { "image": "python:3", "network": "BRIDGE", "portMappings": [ { "containerPort": 8080, "hostPort": 0, "servicePort": 9000, "protocol": "tcp" }, { "containerPort": 161, "hostPort": 0, "protocol": "udp" } ] }, "type": "DOCKER", "volumes": [] }, "cpus": 0.5, "dependencies": [], "deployments": [], "disk": 0.0, "env": {}, "executor": "", "healthChecks": [ { "command": null, "gracePeriodSeconds": 5, "intervalSeconds": 20, "maxConsecutiveFailures": 3, "path": "/", "portIndex": 0, "protocol": "HTTP", "timeoutSeconds": 20 } ], "id": "/fake-app", "instances": 2, "mem": 64.0, "requirePorts": false, "residency" : { "taskLostBehavior" : "RELAUNCH_AFTER_TIMEOUT", "relaunchEscalationTimeoutSeconds" : 60 }, "storeUrls": [], "tasksRunning": 2, "tasksStaged": 0, "upgradeStrategy": { "minimumHealthCapacity": 1.0 }, "user": null, "version": "2014-09-25T02:26:59.256Z" }, { "args": null, "backoffFactor": 1.15, "backoffSeconds": 1, "cmd": "python3 -m http.server 8080", "constraints": [], "container": { "docker": { "image": "python:3", "network": "BRIDGE", "portMappings": [ { "containerPort": 8080, "hostPort": 0, "servicePort": 9000, "protocol": "tcp" }, { "containerPort": 161, "hostPort": 0, "protocol": "udp" } ] }, "type": "DOCKER", "volumes": [] }, "cpus": 0.5, "dependencies": [], "deployments": [], "disk": 0.0, "env": {}, "executor": "", "healthChecks": [ { "command": null, "gracePeriodSeconds": 5, "intervalSeconds": 20, "maxConsecutiveFailures": 3, "path": "/", "portIndex": 0, "protocol": "HTTP", "timeoutSeconds": 20 } ], "id": "/fake-app-broken", "instances": 2, "mem": 64.0, "requirePorts": false, "storeUrls": [], "tasksRunning": 2, "tasksStaged": 0, "upgradeStrategy": { "minimumHealthCapacity": 1.0 }, "user": null, "version": "2014-09-25T02:26:59.256Z" } ] } - uri: /v2/apps/no-health-check-results-app method: GET content: | { "app": { "healthChecks": [ { "command": null, "gracePeriodSeconds": 5, "intervalSeconds": 10, "maxConsecutiveFailures": 3, "path": "/health", "portIndex": 0, "protocol": "HTTP", "timeoutSeconds": 10 } ], "id": "/no-health-check-results-app", "instances": 2, "mem": 32.0, "tasks": [ { "appId": "/no-health-check-results-app", "healthCheckResults": [ { "alive": true, "consecutiveFailures": 0, "firstSuccess": "2014-09-13T00:20:28.101Z", "lastFailure": null, "lastSuccess": "2014-09-13T00:25:07.506Z", "taskId": "toggle.802df2ae-3ad4-11e4-a400-56847afe9799" } ], "id": "task1.802df2ae-3ad4-11e4-a400-56847afe9799" }, { "appId": "/no-health-check-results-app", "id": "task2.7c99814d-3ad4-11e4-a400-56847afe9799" } ], "tasksRunning": 2, "tasksStaged": 0, "version": "2014-09-12T23:28:21.737Z" } } - uri: /v2/apps?embed=apps.readiness method: GET content: | { "apps": [ { "id": "/fake-app", "readinessCheckResults": [ { "name": "myReadyCheck", "taskId": "test_frontend_app1.c9de6033", "ready": false, "lastResponse": { "body": "{}", "contentType": "application/json", "status": 500 } } ] } ] } - uri: /v2/apps/fake-app method: GET scope: unreachablestrategy-present content: | { "app": { "id": "/fake-app", "unreachableStrategy": { "inactiveAfterSeconds": 3.0, "expungeAfterSeconds": 4.0 } } } - uri: /v2/apps/fake-app method: GET scope: unreachablestrategy-absent content: | { "app": { "id": "/fake-app", "unreachableStrategy": "disabled" } } - uri: /v2/apps/fake-app method: GET scope: environment-variables content: | { "app": { "id": "/fake-app", "env": { "FOO": "bar", "TOP": { "secret": "secret" } }, "secrets":{ "secret": { "source": "/path/to/secret" } } } } ================================================ FILE: unreachable_strategy.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "encoding/json" "fmt" ) // UnreachableStrategyAbsenceReasonDisabled signifies the reason of disabled unreachable strategy const UnreachableStrategyAbsenceReasonDisabled = "disabled" // UnreachableStrategy is the unreachable strategy applied to an application. type UnreachableStrategy struct { EnabledUnreachableStrategy AbsenceReason string } // EnabledUnreachableStrategy covers parameters pertaining to present unreachable strategies. type EnabledUnreachableStrategy struct { InactiveAfterSeconds *float64 `json:"inactiveAfterSeconds,omitempty"` ExpungeAfterSeconds *float64 `json:"expungeAfterSeconds,omitempty"` } type unreachableStrategy UnreachableStrategy // UnmarshalJSON unmarshals the given JSON into an UnreachableStrategy. It // populates parameters for present strategies, and otherwise only sets the // absence reason. func (us *UnreachableStrategy) UnmarshalJSON(b []byte) error { var u unreachableStrategy var errEnabledUS, errNonEnabledUS error if errEnabledUS = json.Unmarshal(b, &u); errEnabledUS == nil { *us = UnreachableStrategy(u) return nil } if errNonEnabledUS = json.Unmarshal(b, &us.AbsenceReason); errNonEnabledUS == nil { return nil } return fmt.Errorf("failed to unmarshal unreachable strategy: unmarshaling into enabled returned error '%s'; unmarshaling into non-enabled returned error '%s'", errEnabledUS, errNonEnabledUS) } // MarshalJSON marshals the unreachable strategy. func (us *UnreachableStrategy) MarshalJSON() ([]byte, error) { if us.AbsenceReason == "" { return json.Marshal(us.EnabledUnreachableStrategy) } return json.Marshal(us.AbsenceReason) } // SetInactiveAfterSeconds sets the period after which instance will be marked as inactive. func (us *UnreachableStrategy) SetInactiveAfterSeconds(cap float64) *UnreachableStrategy { us.InactiveAfterSeconds = &cap return us } // SetExpungeAfterSeconds sets the period after which instance will be expunged. func (us *UnreachableStrategy) SetExpungeAfterSeconds(cap float64) *UnreachableStrategy { us.ExpungeAfterSeconds = &cap return us } ================================================ FILE: unreachable_strategy_test.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestUnreachableStrategyAPI(t *testing.T) { app := Application{} require.Nil(t, app.UnreachableStrategy) us := new(UnreachableStrategy) us.SetExpungeAfterSeconds(30.0).SetInactiveAfterSeconds(5.0) app.SetUnreachableStrategy(*us) testUs := app.UnreachableStrategy assert.Equal(t, 30.0, *testUs.ExpungeAfterSeconds) assert.Equal(t, 5.0, *testUs.InactiveAfterSeconds) app.EmptyUnreachableStrategy() us = app.UnreachableStrategy require.NotNil(t, us) assert.Nil(t, us.ExpungeAfterSeconds) assert.Nil(t, us.InactiveAfterSeconds) } func TestUnreachableStrategyUnmarshalEnabled(t *testing.T) { defaultConfig := NewDefaultConfig() configs := &configContainer{ client: &defaultConfig, server: &serverConfig{ scope: "unreachablestrategy-present", }, } endpoint := newFakeMarathonEndpoint(t, configs) defer endpoint.Close() application, err := endpoint.Client.Application(fakeAppName) require.NoError(t, err) us := application.UnreachableStrategy require.NotNil(t, us) assert.Empty(t, us.AbsenceReason) if assert.NotNil(t, us.InactiveAfterSeconds) { assert.Equal(t, 3.0, *us.InactiveAfterSeconds) } if assert.NotNil(t, us.ExpungeAfterSeconds) { assert.Equal(t, 4.0, *us.ExpungeAfterSeconds) } } func TestUnreachableStrategyUnmarshalNonEnabled(t *testing.T) { defaultConfig := NewDefaultConfig() configs := &configContainer{ client: &defaultConfig, server: &serverConfig{ scope: "unreachablestrategy-absent", }, } endpoint := newFakeMarathonEndpoint(t, configs) defer endpoint.Close() application, err := endpoint.Client.Application(fakeAppName) require.NoError(t, err) us := application.UnreachableStrategy require.NotNil(t, us) assert.Equal(t, UnreachableStrategyAbsenceReasonDisabled, us.AbsenceReason) } func TestUnreachableStrategyUnmarshalIllegal(t *testing.T) { j := []byte(`{false}`) us := UnreachableStrategy{} assert.Error(t, us.UnmarshalJSON(j)) } func TestUnreachableStrategyMarshal(t *testing.T) { tests := []struct { name string us UnreachableStrategy wantJSON string }{ { name: "present", us: UnreachableStrategy{ EnabledUnreachableStrategy: EnabledUnreachableStrategy{ InactiveAfterSeconds: float64p(3.5), ExpungeAfterSeconds: float64p(4.5), }, AbsenceReason: "", }, wantJSON: `{"inactiveAfterSeconds":3.5,"expungeAfterSeconds":4.5}`, }, { name: "absent", us: UnreachableStrategy{ AbsenceReason: UnreachableStrategyAbsenceReasonDisabled, }, wantJSON: fmt.Sprintf(`"%s"`, UnreachableStrategyAbsenceReasonDisabled), }, } for _, test := range tests { label := fmt.Sprintf("test: %s", test.name) j, err := test.us.MarshalJSON() if assert.NoError(t, err, label) { assert.Equal(t, test.wantJSON, string(j), label) } } } func float64p(f float64) *float64 { return &f } ================================================ FILE: upgrade_strategy.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon // UpgradeStrategy is the upgrade strategy applied to an application. type UpgradeStrategy struct { MinimumHealthCapacity *float64 `json:"minimumHealthCapacity,omitempty"` MaximumOverCapacity *float64 `json:"maximumOverCapacity,omitempty"` } // SetMinimumHealthCapacity sets the minimum health capacity. func (us *UpgradeStrategy) SetMinimumHealthCapacity(cap float64) *UpgradeStrategy { us.MinimumHealthCapacity = &cap return us } // SetMaximumOverCapacity sets the maximum over capacity. func (us *UpgradeStrategy) SetMaximumOverCapacity(cap float64) *UpgradeStrategy { us.MaximumOverCapacity = &cap return us } ================================================ FILE: utils.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "errors" "fmt" "net" "net/url" "reflect" "strings" "sync/atomic" "time" "github.com/google/go-querystring/query" ) type atomicSwitch int64 func (r *atomicSwitch) IsSwitched() bool { return atomic.LoadInt64((*int64)(r)) != 0 } func (r *atomicSwitch) SwitchOn() { atomic.StoreInt64((*int64)(r), 1) } func (r *atomicSwitch) SwitchedOff() { atomic.StoreInt64((*int64)(r), 0) } func validateID(id string) string { if !strings.HasPrefix(id, "/") { return fmt.Sprintf("/%s", id) } return id } func trimRootPath(id string) string { if strings.HasPrefix(id, "/") { return strings.TrimPrefix(id, "/") } return id } func deadline(timeout time.Duration, work func(chan bool) error) error { result := make(chan error) timer := time.After(timeout) stopChannel := make(chan bool, 1) // allow the method to attempt go func() { result <- work(stopChannel) }() for { select { case err := <-result: return err case <-timer: stopChannel <- true return ErrTimeoutError } } } func getInterfaceAddress(name string) (string, error) { interfaces, err := net.Interfaces() if err != nil { return "", err } for _, iface := range interfaces { // step: get only the interface we're interested in if iface.Name == name { addrs, err := iface.Addrs() if err != nil { return "", err } // step: return the first address if len(addrs) > 0 { return parseIPAddr(addrs[0]), nil } } } return "", errors.New("Unable to determine or find the interface") } func contains(elements []string, value string) bool { for _, element := range elements { if element == value { return true } } return false } func parseIPAddr(addr net.Addr) string { return strings.SplitN(addr.String(), "/", 2)[0] } // addOptions adds the parameters in opt as URL query parameters to s. // opt must be a struct whose fields may contain "url" tags. func addOptions(s string, opt interface{}) (string, error) { v := reflect.ValueOf(opt) if v.Kind() == reflect.Ptr && v.IsNil() { return s, nil } u, err := url.Parse(s) if err != nil { return s, err } qs, err := query.Values(opt) if err != nil { return s, err } u.RawQuery = qs.Encode() return u.String(), nil } // Bool returns a pointer to the passed in bool value func Bool(b bool) *bool { return &b } ================================================ FILE: utils_test.go ================================================ /* Copyright 2014 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "net" "testing" "time" "github.com/stretchr/testify/assert" ) type stubAddr struct { addr string } func (sa stubAddr) Network() string { return "network" } func (sa stubAddr) String() string { return sa.addr + "/8" } func TestUtilsAtomicIsSwitched(t *testing.T) { var sw atomicSwitch assert.False(t, sw.IsSwitched()) sw.SwitchOn() assert.True(t, sw.IsSwitched()) } func TestUtilsAtomicIsSwitchedOff(t *testing.T) { var sw atomicSwitch assert.False(t, sw.IsSwitched()) sw.SwitchOn() assert.True(t, sw.IsSwitched()) sw.SwitchedOff() assert.False(t, sw.IsSwitched()) } func TestUtilsDeadline(t *testing.T) { err := deadline(time.Duration(5)*time.Millisecond, func(chan bool) error { <-time.After(time.Duration(1) * time.Second) return nil }) assert.Error(t, err) assert.Equal(t, ErrTimeoutError, err) err = deadline(time.Duration(5)*time.Second, func(chan bool) error { <-time.After(time.Duration(5) * time.Millisecond) return nil }) assert.NoError(t, err) } func TestUtilsContains(t *testing.T) { list := []string{"1", "2", "3"} assert.True(t, contains(list, "2")) assert.False(t, contains(list, "12")) } func TestUtilsValidateID(t *testing.T) { path := "test/path" assert.Equal(t, validateID(path), "/test/path") path = "/test/path" assert.Equal(t, validateID(path), "/test/path") } func TestUtilsGetInterfaceAddress(t *testing.T) { // Find actual IP address we can test against. interfaces, err := net.Interfaces() assert.NoError(t, err) assert.NotEqual(t, 0, len(interfaces)) iface := interfaces[0] expectedName := iface.Name addresses, err := iface.Addrs() assert.NoError(t, err) expectedIPAddress := parseIPAddr(addresses[0]) // Execute test. address, err := getInterfaceAddress(expectedName) assert.NoError(t, err) assert.Equal(t, expectedIPAddress, address) address, err = getInterfaceAddress("nothing") assert.Error(t, err) assert.Equal(t, "", address) } func TestUtilsTrimRootPath(t *testing.T) { path := "/test/path" assert.Equal(t, trimRootPath(path), "test/path") path = "test/path" assert.Equal(t, trimRootPath(path), "test/path") } func TestParseIPAddr(t *testing.T) { ipAddr := "127.0.0.1" addr := stubAddr{ipAddr} assert.Equal(t, ipAddr, parseIPAddr(addr)) } ================================================ FILE: volume.go ================================================ /* Copyright 2017 The go-marathon Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon // PodVolume describes a volume on the host type PodVolume struct { Name string `json:"name,omitempty"` Host string `json:"host,omitempty"` Secret string `json:"secret,omitempty"` Persistent *PersistentVolume `json:"persistent,omitempty"` } // PodVolumeMount describes how to mount a volume into a task type PodVolumeMount struct { Name string `json:"name,omitempty"` MountPath string `json:"mountPath,omitempty"` ReadOnly *bool `json:"readOnly,omitempty"` } // NewPodVolume creates a new PodVolume func NewPodVolume(name, path string) *PodVolume { return &PodVolume{ Name: name, Host: path, } } // NewPodVolume creates a new PodVolume for file based secrets func NewPodVolumeSecret(name, secretPath string) *PodVolume { return &PodVolume{ Name: name, Secret: secretPath, } } // NewPodVolumeMount creates a new PodVolumeMount func NewPodVolumeMount(name, mount string) *PodVolumeMount { return &PodVolumeMount{ Name: name, MountPath: mount, } } // SetPersistentVolume sets the persistence settings of a PodVolume func (pv *PodVolume) SetPersistentVolume(p *PersistentVolume) *PodVolume { pv.Persistent = p return pv }