pax_global_header00006660000000000000000000000064130526015610014511gustar00rootroot0000000000000052 comment=6b00a5b651b1beb2c6821863f7c60df490bd46c8 go-marathon-0.7.1/000077500000000000000000000000001305260156100137325ustar00rootroot00000000000000go-marathon-0.7.1/.gitignore000066400000000000000000000011441305260156100157220ustar00rootroot00000000000000# 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/queue/queue examples/tasks/tasks # 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 go-marathon-0.7.1/.travis.yml000066400000000000000000000003271305260156100160450ustar00rootroot00000000000000# # Author: Rohith (gambol99@gmail.com) # Date: 2015-02-17 17:11:18 +0000 (Tue, 17 Feb 2015) # # vim:ts=2:sw=2:et # language: go go: - 1.5 - 1.6 - 1.7 - 1.8 install: - make check-format test examples go-marathon-0.7.1/AUTHORS000066400000000000000000000030101305260156100147740ustar00rootroot00000000000000Anton Liparin atheatos Brian Knox Conor Mongey Daniel Bornkessel David Bosschaert Denis Parchenko Diego de Oliveira Dmitry Fedorov eduser25 Elliot Anderson François Samin Harpreet Sawhney Ian Babrou Israel Derdik Ivan Babrou Jacob Koren Jie Zhang Johan Haals juliendsv Kan Wu kevinschoon Luke Amdor Maarten Dirkse Marcelo Salazar Marvin Hoffmann Matt DeBoer Matthias Kadenbach Mike Solomon Mikhail Dyakov Nic Grayson ohmystack Raffaele Di Fazio Robert Jacob Rohith Timo Reimann Tracy Livengood Xavi Ramirez Yang Bai Yifa Zhang Zhanpeng Chen go-marathon-0.7.1/CHANGELOG.md000066400000000000000000000124271305260156100155510ustar00rootroot00000000000000# 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] ## [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 [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 go-marathon-0.7.1/CONTRIBUTING.md000066400000000000000000000021371305260156100161660ustar00rootroot00000000000000# 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. go-marathon-0.7.1/LICENSE000066400000000000000000000240171305260156100147430ustar00rootroot00000000000000Apache 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. go-marathon-0.7.1/Makefile000066400000000000000000000027311305260156100153750ustar00rootroot00000000000000# # 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 authors changelog check-format build: go build authors: git log --format='%aN <%aE>' | sort -u > AUTHORS 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 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 @echo "--> Running go tests" @go test -race -v @$(MAKE) vet @$(MAKE) cover examples: make -C examples all changelog: release git log $(shell git tag | tail -n1)..HEAD --no-merges --format=%B > changelog go-marathon-0.7.1/README.md000066400000000000000000000271611305260156100152200ustar00rootroot00000000000000[![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-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 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.5 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() ... ``` 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. ### Custom HTTP Client If you wish to override the http client (by default http.DefaultClient) used by the API; use cases bypassing TLS verification, load root CA's or change the timeouts etc, you can pass a custom client in the config. ```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, }, }, } ``` ### Listing the applications ```Go applications, err := client.Applications() if err != nil { log.Fatalf("Failed to list applications") } log.Printf("Found %d applications running", len(applications.Apps)) for _, application := range applications.Apps { log.Printf("Application: %s", 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: %s", task) } // check the health of the application health, err := client.ApplicationOK(details.ID) log.Printf("Application: %s, healthy: %t", details.ID, health) } } ``` ### 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") } ``` ### 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() // 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() // 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. go-marathon-0.7.1/application.go000066400000000000000000000656701305260156100166020ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon 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"` Disk *float64 `json:"disk,omitempty"` Env *map[string]string `json:"env,omitempty"` Executor *string `json:"executor,omitempty"` HealthChecks *[]HealthCheck `json:"healthChecks,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"` 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"` 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"` } // 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"` } // 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 } // 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 } // 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 } // 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(uri 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 { return nil, err } health := NewDefaultHealthCheck() health.IntervalSeconds = interval *health.Path = uri *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 { 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 } // 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) { uri := fmt.Sprintf("%s/versions", buildURI(name)) versions := new(ApplicationVersions) if err := r.apiGet(uri, 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) { uri := fmt.Sprintf(buildURI(name)) deploymentID := new(DeploymentID) if err := r.apiPut(uri, 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(buildURI(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) { u, err := addOptions(buildURI(name), opts) if err != nil { return nil, err } var wrapper struct { Application *Application `json:"app"` } if err := r.apiGet(u, 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) uri := fmt.Sprintf("%s/versions/%s", buildURI(name), version) if err := r.apiGet(uri, 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 { if r.appExistAndRunning(name) { return nil } timeoutTimer := time.After(timeout) ticker := time.NewTicker(r.config.PollingWaitTime) defer ticker.Stop() for { select { case <-timeoutTimer: return ErrTimeoutError case <-ticker.C: if r.appExistAndRunning(name) { return nil } } } } 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) { uri := buildURIWithForceParam(name, force) // step: check of the application already exists deployID := new(DeploymentID) if err := r.apiDelete(uri, 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{} uri := buildURIWithForceParam(fmt.Sprintf("%s/restart", name), force) if err := r.apiPost(uri, &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 uri := buildURIWithForceParam(name, force) deployID := new(DeploymentID) if err := r.apiPut(uri, 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) uri := buildURIWithForceParam(application.ID, force) if err := r.apiPut(uri, application, result); err != nil { return nil, err } return result, nil } func buildURIWithForceParam(path string, force bool) string { uri := buildURI(path) if force { uri += "?force=true" } return uri } func buildURI(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 } go-marathon-0.7.1/application_test.go000066400000000000000000000456431305260156100176370ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "fmt" "net/http" "net/url" "testing" "time" "github.com/stretchr/testify/assert" ) 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 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 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") 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 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) app.AddHealthCheck(HealthCheck{}.SetPath("/check1")). AddHealthCheck(HealthCheck{}.SetPath("/check2")) assert.Equal(t, 2, len(*app.HealthChecks)) assert.Equal(t, HealthCheck{}.SetPath("/check1"), (*app.HealthChecks)[0]) assert.Equal(t, HealthCheck{}.SetPath("/check2"), (*app.HealthChecks)[1]) app.EmptyHealthChecks() assert.NotNil(t, app.HealthChecks) assert.Equal(t, 0, len(*app.HealthChecks)) } func TestApplicationPortDefinitions(t *testing.T) { app := NewDockerApplication() assert.Nil(t, app.PortDefinitions) app.AddPortDefinition(PortDefinition{Protocol: "tcp", Name: "es"}.SetPort(9201).AddLabel("foo", "bar")). AddPortDefinition(PortDefinition{Protocol: "udp,tcp", Name: "syslog"}.SetPort(514)) assert.Equal(t, 2, len(*app.PortDefinitions)) assert.Equal(t, PortDefinition{Protocol: "tcp", Name: "es"}.SetPort(9201).AddLabel("foo", "bar"), (*app.PortDefinitions)[0]) assert.Equal(t, 1, len(*(*app.PortDefinitions)[0].Labels)) assert.Equal(t, PortDefinition{Protocol: "udp,tcp", Name: "syslog"}.SetPort(514), (*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) { app := NewDockerApplication() assert.False(t, app.HasHealthChecks()) app.Container.Docker.Container("quay.io/gambol99/apache-php:latest").Expose(80) _, err := app.CheckTCP(80, 10) assert.NoError(t, err) assert.True(t, app.HasHealthChecks()) } func TestApplicationCheckTCP(t *testing.T) { app := NewDockerApplication() assert.False(t, app.HasHealthChecks()) _, err := app.CheckTCP(80, 10) assert.Error(t, err) assert.False(t, app.HasHealthChecks()) app.Container.Docker.Container("quay.io/gambol99/apache-php:latest").Expose(80) _, err = app.CheckTCP(80, 10) assert.NoError(t, err) assert.True(t, app.HasHealthChecks()) check := (*app.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) { app := NewDockerApplication() assert.False(t, app.HasHealthChecks()) _, err := app.CheckHTTP("/", 80, 10) assert.Error(t, err) assert.False(t, app.HasHealthChecks()) app.Container.Docker.Container("quay.io/gambol99/apache-php:latest").Expose(80) _, err = app.CheckHTTP("/health", 80, 10) assert.NoError(t, err) assert.True(t, app.HasHealthChecks()) check := (*app.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) 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 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 */ versions, 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.EmptyUris() assert.NotNil(t, app.Uris) assert.Equal(t, 0, len(*app.Uris)) } 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) { for _, force := range []bool{false, true} { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() 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) id, 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) } 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{}) 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) app.SetUpgradeStrategy(UpgradeStrategy{}.SetMinimumHealthCapacity(1.0).SetMaximumOverCapacity(0.0)) us := app.UpgradeStrategy assert.Equal(t, 1.0, *us.MinimumHealthCapacity) assert.Equal(t, 0.0, *us.MaximumOverCapacity) app.EmptyUpgradeStrategy() us = app.UpgradeStrategy assert.NotNil(t, us) assert.Nil(t, us.MinimumHealthCapacity) assert.Nil(t, us.MaximumOverCapacity) } go-marathon-0.7.1/client.go000066400000000000000000000256041305260156100155460ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "bytes" "encoding/json" "errors" "fmt" "io" "io/ioutil" "log" "net/http" "net/url" "regexp" "sync" "time" ) // Marathon is the interface to the marathon API type Marathon interface { // -- 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 // -- 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 ( // ErrInvalidResponse is thrown when marathon responds with invalid or error response ErrInvalidResponse = errors.New("invalid response from Marathon") // 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") ) // 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 http client use for making requests httpClient *http.Client // the marathon hosts hosts *cluster // a map of service you wish to listen to listeners map[EventsChannel]EventsChannelContext // a custom logger for debug log messages debugLog *log.Logger } // NewClient creates a new marathon client // config: the configuration to use func NewClient(config Config) (Marathon, error) { // step: if no http client, set to default if config.HTTPClient == nil { config.HTTPClient = http.DefaultClient } // step: if no polling wait time is set, default to 500 milliseconds. if config.PollingWaitTime == 0 { config.PollingWaitTime = defaultPollingWaitTime } // step: create a new cluster hosts, err := newCluster(config.HTTPClient, config.URL) if err != nil { return nil, err } debugLogOutput := config.LogOutput if debugLogOutput == nil { debugLogOutput = ioutil.Discard } return &marathonClient{ config: config, listeners: make(map[EventsChannel]EventsChannelContext), hosts: hosts, httpClient: config.HTTPClient, debugLog: log.New(debugLogOutput, "", 0), }, 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) apiGet(uri string, post, result interface{}) error { return r.apiCall("GET", uri, post, result) } func (r *marathonClient) apiPut(uri string, post, result interface{}) error { return r.apiCall("PUT", uri, post, result) } func (r *marathonClient) apiPost(uri string, post, result interface{}) error { return r.apiCall("POST", uri, post, result) } func (r *marathonClient) apiDelete(uri string, post, result interface{}) error { return r.apiCall("DELETE", uri, post, result) } func (r *marathonClient) apiCall(method, uri string, body, result interface{}) error { for { // step: grab a member from the cluster and attempt to perform the request member, err := r.hosts.getMember() if err != nil { return ErrMarathonDown } // step: Create the endpoint url url := fmt.Sprintf("%s/%s", member, uri) if r.config.DCOSToken != "" { url = fmt.Sprintf("%s/%s", member+"/marathon", uri) } // step: marshall the request to json var requestBody []byte if body != nil { if requestBody, err = json.Marshal(body); err != nil { return err } } // step: create the api request request, err := r.buildAPIRequest(method, url, bytes.NewReader(requestBody)) if err != nil { return err } response, err := r.httpClient.Do(request) if err != nil { r.hosts.markDown(member) // step: attempt the request on another member r.debugLog.Printf("apiCall(): request failed on host: %s, error: %s, trying another\n", 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.Printf("apiCall(): %v %v %s returned %v %s\n", request.Method, request.URL.String(), requestBody, response.Status, oneLogLine(respBody)) } else { r.debugLog.Printf("apiCall(): %v %v returned %v %s\n", request.Method, request.URL.String(), response.Status, oneLogLine(respBody)) } // step: check for a successfull response if response.StatusCode >= 200 && response.StatusCode <= 299 { if result != nil { if err := json.Unmarshal(respBody, result); err != nil { r.debugLog.Printf("apiCall(): failed to unmarshall the response from marathon, error: %s\n", err) return ErrInvalidResponse } } 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.Printf("apiCall(): request failed, host: %s, status: %d, trying another\n", member, response.StatusCode) continue } return NewAPIError(response.StatusCode, respBody) } } // buildAPIRequest creates a default API request func (r *marathonClient) buildAPIRequest(method, url string, reader io.Reader) (*http.Request, error) { // Make the http request to Marathon request, err := http.NewRequest(method, url, reader) if err != nil { return nil, err } // Add any basic auth and the content headers if r.config.HTTPBasicAuthUser != "" && r.config.HTTPBasicPassword != "" { request.SetBasicAuth(r.config.HTTPBasicAuthUser, r.config.HTTPBasicPassword) } if r.config.DCOSToken != "" { request.Header.Add("Authorization", "token="+r.config.DCOSToken) } request.Header.Add("Content-Type", "application/json") request.Header.Add("Accept", "application/json") return request, nil } 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) } go-marathon-0.7.1/client_test.go000066400000000000000000000104021305260156100165730ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "net/http" "testing" "github.com/stretchr/testify/assert" ) 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, http.DefaultClient) assert.Equal(t, conf.PollingWaitTime, defaultPollingWaitTime) } 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 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() } } go-marathon-0.7.1/cluster.go000066400000000000000000000106251305260156100157460ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "fmt" "net/http" "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 http client client *http.Client } // 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 *http.Client, marathonURL string) (*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: create a new node for this endpoint members = append(members, &member{endpoint: u.String()}) } return &cluster{ client: client, members: members, }, 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 for { res, err := c.client.Get(fmt.Sprintf("%s/ping", node.endpoint)) if err == nil && res.StatusCode == 200 { break } <-time.After(time.Duration(5 * time.Second)) } // step: mark the node as active again c.Lock() defer c.Unlock() node.status = memberStatusUp } // activeMembers returns a list of active members 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) } go-marathon-0.7.1/cluster_test.go000066400000000000000000000102421305260156100170000ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "net/http" "testing" "time" "github.com/stretchr/testify/assert" ) func TestSize(t *testing.T) { cluster, err := newCluster(http.DefaultClient, fakeMarathonURL) assert.NoError(t, err) assert.Equal(t, cluster.size(), 3) } func TestActive(t *testing.T) { cluster, err := newCluster(http.DefaultClient, fakeMarathonURL) assert.NoError(t, err) assert.Equal(t, len(cluster.activeMembers()), 3) } func TestNonActive(t *testing.T) { cluster, err := newCluster(http.DefaultClient, fakeMarathonURL) assert.NoError(t, err) assert.Equal(t, len(cluster.nonActiveMembers()), 0) } func TestGetMember(t *testing.T) { cluster, err := newCluster(http.DefaultClient, fakeMarathonURL) assert.NoError(t, err) member, err := cluster.getMember() assert.NoError(t, err) assert.Equal(t, member, "http://127.0.0.1:3000") } func TestGetMemberWithPath(t *testing.T) { cluster, err := newCluster(http.DefaultClient, fakeMarathonURLWithPath) assert.NoError(t, err) member, err := cluster.getMember() assert.NoError(t, err) assert.Equal(t, member, "http://127.0.0.1:3000/path") } func TestMarkDown(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() cluster, err := newCluster(http.DefaultClient, endpoint.URL) assert.NoError(t, err) assert.Equal(t, len(cluster.activeMembers()), 3) members := cluster.activeMembers() cluster.markDown(members[0]) cluster.markDown(members[1]) assert.Equal(t, 1, len(cluster.activeMembers())) time.Sleep(10 * time.Millisecond) assert.Equal(t, len(cluster.activeMembers()), 3) } 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 := newCluster(http.DefaultClient, 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 := newCluster(http.DefaultClient, invalidHost) if !assert.Error(t, err) { t.Errorf("undetected invalid host: %s", invalidHost) } } } go-marathon-0.7.1/config.go000066400000000000000000000040211305260156100155230ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "io" "io/ioutil" "net/http" "time" ) const defaultPollingWaitTime = 500 * time.Millisecond // 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 // 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, } } go-marathon-0.7.1/const.go000066400000000000000000000027001305260156100154060ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon const ( defaultEventsURL = "/event" /* --- api related constants --- */ marathonAPIVersion = "v2" marathonAPIEventStream = marathonAPIVersion + "/events" marathonAPISubscription = marathonAPIVersion + "/eventSubscriptions" marathonAPIApps = marathonAPIVersion + "/apps" 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 ) go-marathon-0.7.1/deployment.go000066400000000000000000000114331305260156100164430ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon 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"` 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"` } // 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) { deployment := new(DeploymentID) err := r.apiDelete(fmt.Sprintf("%s/%s", marathonAPIDeployments, id), 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) } } go-marathon-0.7.1/deployment_test.go000066400000000000000000000037341305260156100175070ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "testing" "github.com/stretchr/testify/assert" ) func TestDeployments(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) 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, "867ed450-f6a8-4d33-9b0e-e11c5513990b") assert.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) } func TestDeleteDeployment(t *testing.T) { endpoint := newFakeMarathonEndpoint(t, nil) defer endpoint.Close() id, err := endpoint.Client.DeleteDeployment(fakeDeploymentID, false) assert.NoError(t, err) assert.NotNil(t, t) assert.Equal(t, id.DeploymentID, "0b1467fc-d5cd-4bbc-bac2-2805351cee1e") assert.Equal(t, id.Version, "2014-08-26T08:20:26.171Z") } go-marathon-0.7.1/docker.go000066400000000000000000000211101305260156100155230ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon 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"` } // 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"` } // 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"` } // 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"` } // 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"` } // 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 } // 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 (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 (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 (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 (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 (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 container port required was not found in the container port mappings") } go-marathon-0.7.1/docker_test.go000066400000000000000000000074211305260156100165730ustar00rootroot00000000000000/* Copyright 2015 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "testing" "github.com/stretchr/testify/assert" ) 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) { app := NewDockerApplication() app.Container.Docker.Expose(8080).Expose(80, 443) portMappings := app.Container.Docker.PortMappings assert.Equal(t, 3, len(*portMappings)) assert.Equal(t, *createPortMapping(8080, "tcp"), (*portMappings)[0]) assert.Equal(t, *createPortMapping(80, "tcp"), (*portMappings)[1]) assert.Equal(t, *createPortMapping(443, "tcp"), (*portMappings)[2]) } func TestDockerExposeUDP(t *testing.T) { app := NewDockerApplication() app.Container.Docker.ExposeUDP(53).ExposeUDP(5060, 6881) portMappings := app.Container.Docker.PortMappings assert.Equal(t, 3, len(*portMappings)) assert.Equal(t, *createPortMapping(53, "udp"), (*portMappings)[0]) assert.Equal(t, *createPortMapping(5060, "udp"), (*portMappings)[1]) assert.Equal(t, *createPortMapping(6881, "udp"), (*portMappings)[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 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 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, "") } go-marathon-0.7.1/error.go000066400000000000000000000135721305260156100154220ustar00rootroot00000000000000/* Copyright 2015 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon 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 ) // 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.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 } go-marathon-0.7.1/error_test.go000066400000000000000000000124201305260156100164500ustar00rootroot00000000000000/* Copyright 2015 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon 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"}`, }, // 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"] } ] }` } go-marathon-0.7.1/events.go000066400000000000000000000321121305260156100155640ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon 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"` } // 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) } go-marathon-0.7.1/examples/000077500000000000000000000000001305260156100155505ustar00rootroot00000000000000go-marathon-0.7.1/examples/Makefile000066400000000000000000000001151305260156100172050ustar00rootroot00000000000000all: find * -type d -exec bash -exc "cd {}; go build . || kill $${PPID}" \; go-marathon-0.7.1/examples/applications/000077500000000000000000000000001305260156100202365ustar00rootroot00000000000000go-marathon-0.7.1/examples/applications/main.go000066400000000000000000000064121305260156100215140ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package 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) } go-marathon-0.7.1/examples/docker-compose.yml000066400000000000000000000025151305260156100212100ustar00rootroot00000000000000# 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 go-marathon-0.7.1/examples/events_callback_transport/000077500000000000000000000000001305260156100230045ustar00rootroot00000000000000go-marathon-0.7.1/examples/events_callback_transport/main.go000066400000000000000000000045621305260156100242660ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package 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("Recieved application event: %s", event) case event := <-deployments: log.Printf("Recieved 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) } go-marathon-0.7.1/examples/events_sse_transport/000077500000000000000000000000001305260156100220425ustar00rootroot00000000000000go-marathon-0.7.1/examples/events_sse_transport/main.go000066400000000000000000000041661305260156100233240ustar00rootroot00000000000000/* Copyright 2015 Denis Parchenko 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("Recieved application event: %s", event) case event := <-deployments: log.Printf("Recieved 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) } go-marathon-0.7.1/examples/glog/000077500000000000000000000000001305260156100165005ustar00rootroot00000000000000go-marathon-0.7.1/examples/glog/main.go000066400000000000000000000024701305260156100177560ustar00rootroot00000000000000/* 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) } } go-marathon-0.7.1/examples/groups/000077500000000000000000000000001305260156100170675ustar00rootroot00000000000000go-marathon-0.7.1/examples/groups/main.go000066400000000000000000000070371305260156100203510ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package 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 paramaters") 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)) } go-marathon-0.7.1/examples/multiple_endpoints/000077500000000000000000000000001305260156100214665ustar00rootroot00000000000000go-marathon-0.7.1/examples/multiple_endpoints/main.go000066400000000000000000000025571305260156100227520ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package 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) } } go-marathon-0.7.1/examples/queue/000077500000000000000000000000001305260156100166745ustar00rootroot00000000000000go-marathon-0.7.1/examples/queue/main.go000066400000000000000000000041141305260156100201470ustar00rootroot00000000000000/* Copyright 2016 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package 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 } go-marathon-0.7.1/examples/tasks/000077500000000000000000000000001305260156100166755ustar00rootroot00000000000000go-marathon-0.7.1/examples/tasks/main.go000066400000000000000000000017221305260156100201520ustar00rootroot00000000000000package 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) } } go-marathon-0.7.1/group.go000066400000000000000000000162361305260156100154250ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon 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) { u, err := addOptions(marathonAPIGroups, opts) if err != nil { return nil, err } groups := new(Groups) if err := r.apiGet(u, "", 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) { u, err := addOptions(fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)), opts) if err != nil { return nil, err } group := new(Group) if err := r.apiGet(u, 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) { uri := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)) err := r.apiCall("GET", uri, "", 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) uri := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)) if force { uri = uri + "?force=true" } if err := r.apiDelete(uri, 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) uri := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)) if force { uri = uri + "?force=true" } if err := r.apiPut(uri, group, deploymentID); err != nil { return nil, err } return deploymentID, nil } go-marathon-0.7.1/group_test.go000066400000000000000000000034251305260156100164600ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon 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, len(groups.Groups), 1) group := groups.Groups[0] assert.Equal(t, group.ID, fakeGroupName) } 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, len(group.Apps), 1) assert.Equal(t, group.ID, fakeGroupName) group, err = endpoint.Client.Group(fakeGroupName1) assert.NoError(t, err) assert.NotNil(t, group) assert.Equal(t, group.ID, fakeGroupName1) assert.NotNil(t, group.Groups) assert.Equal(t, len(group.Groups), 1) frontend := group.Groups[0] assert.Equal(t, frontend.ID, "frontend") assert.Equal(t, len(frontend.Apps), 3) for _, app := range frontend.Apps { assert.NotNil(t, app.Container) assert.NotNil(t, app.Container.Docker) assert.Equal(t, app.Container.Docker.Network, "BRIDGE") if len(*app.Container.Docker.PortMappings) == 0 { t.Fail() } } } go-marathon-0.7.1/health.go000066400000000000000000000056521305260156100155360ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon // 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"` } // 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 } // 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"` } go-marathon-0.7.1/info.go000066400000000000000000000070311305260156100152150ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon // 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 } go-marathon-0.7.1/info_test.go000066400000000000000000000026451305260156100162620ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon 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") } go-marathon-0.7.1/last_task_failure.go000066400000000000000000000017141305260156100177600ustar00rootroot00000000000000/* 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"` State string `json:"state,omitempty"` TaskID string `json:"taskId,omitempty"` Timestamp string `json:"timestamp,omitempty"` Version string `json:"version,omitempty"` } go-marathon-0.7.1/port_definition.go000066400000000000000000000033201305260156100174530ustar00rootroot00000000000000/* Copyright 2016 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon // 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 { p.Port = &port 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 } go-marathon-0.7.1/queue.go000066400000000000000000000031311305260156100154030ustar00rootroot00000000000000/* Copyright 2016 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon 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"` } // Delay cotains the application postpone infomation type Delay struct { Overdue bool `json:"overdue"` TimeLeftSeconds int `json:"timeLeftSeconds"` } // 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 { uri := fmt.Sprintf("%s/%s/delay", marathonAPIQueue, trimRootPath(appID)) err := r.apiDelete(uri, nil, nil) if err != nil { return err } return nil } go-marathon-0.7.1/queue_test.go000066400000000000000000000023561305260156100164520ustar00rootroot00000000000000/* Copyright 2016 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon 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) } go-marathon-0.7.1/subscription.go000066400000000000000000000203121305260156100170030ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon 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 } func (r *marathonClient) registerSSESubscription() error { // Prevent multiple SSE subscriptions if r.subscribedToSSE { return nil } // Get a member from the cluster marathon, err := r.hosts.getMember() if err != nil { return err } request, err := r.buildAPIRequest("GET", fmt.Sprintf("%s/%s", marathon, marathonAPIEventStream), nil) if err != nil { return err } // Try to connect to stream, reusing the http client settings stream, err := eventsource.SubscribeWith("", r.httpClient, request) if err != nil { return err } go func() { for { select { case ev := <-stream.Events: if err := r.handleEvent(ev.Data()); err != nil { // TODO let the user handle this error instead of logging it here r.debugLog.Printf("registerSSESubscription(): failed to handle event: %v\n", err) } case err := <-stream.Errors: // TODO let the user handle this error instead of logging it here r.debugLog.Printf("registerSSESubscription(): failed to receive event: %v\n", err) } } }() r.subscribedToSSE = true return nil } // Subscribe adds a URL to Marathon's callback facility // callback : the URL you wish to subscribe func (r *marathonClient) Subscribe(callback string) error { uri := fmt.Sprintf("%s?callbackUrl=%s", marathonAPISubscription, callback) return r.apiPost(uri, "", 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.Printf("handleCallbackEvent(): failed to read request body, error: %s\n", err) return } if err := r.handleEvent(string(body[:])); err != nil { // TODO should this return a 500? r.debugLog.Printf("handleCallbackEvent(): failed to handle event: %v\n", err) } } go-marathon-0.7.1/subscription_test.go000066400000000000000000000220431305260156100200450ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon import ( "net/http" "testing" "time" "github.com/stretchr/testify/assert" ) const eventPublishTimeout 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 TestEventStreamConnectionErrorsForwarded(t *testing.T) { clientCfg := NewDefaultConfig() config := &configContainer{ client: &clientCfg, } config.client.EventsTransport = EventsTransportSSE config.client.URL = "http://non-existing-marathon-host.local:5555" // Reduce timeout to speed up test execution time. config.client.HTTPClient = &http.Client{ Timeout: 100 * time.Millisecond, } endpoint := newFakeMarathonEndpoint(t, config) defer endpoint.Close() _, err := endpoint.Client.AddEventsListener(EventIDApplications) assert.Error(t, err) } func TestEventStreamEventsReceived(t *testing.T) { if !assert.True(t, len(testCases) > 1, "must have at least 2 test cases to end prematurely") { return } 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] // 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") } } go-marathon-0.7.1/task.go000066400000000000000000000157611305260156100152350ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon 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"` } // 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) { u, err := addOptions(marathonAPITasks, opts) if err != nil { return nil, err } tasks := new(Tasks) if err := r.apiGet(u, 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) { u := fmt.Sprintf("%s/%s/tasks", marathonAPIApps, trimRootPath(id)) u, err := addOptions(u, opts) if err != nil { return nil, err } tasks := new(Tasks) if err := r.apiDelete(u, 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) u := fmt.Sprintf("%s/%s/tasks/%s", marathonAPIApps, appName, taskID) u, err := addOptions(u, opts) if err != nil { return nil, err } wrappedTask := new(struct { Task Task `json:"task"` }) if err := r.apiDelete(u, 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 { u := fmt.Sprintf("%s/delete", marathonAPITasks) u, err := addOptions(u, opts) if err != nil { return nil } var post struct { IDs []string `json:"ids"` } post.IDs = tasks return r.apiPost(u, &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 { 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 == false { return false } } return true } go-marathon-0.7.1/task_test.go000066400000000000000000000063561305260156100162740ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon 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) } go-marathon-0.7.1/testing_utils_test.go000066400000000000000000000211641305260156100202210ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon 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"` } 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"` } // 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") 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, 0) 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") } err = yaml.Unmarshal([]byte(methodSpec), &methods) if err != nil { t.Fatalf("failed to unmarshal the response") } 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, }, } } } }) } 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() error { s.eventSrv.Close() s.httpSrv.Close() return nil } func (e *endpoint) Close() error { return e.Server.Close() } go-marathon-0.7.1/tests/000077500000000000000000000000001305260156100150745ustar00rootroot00000000000000go-marathon-0.7.1/tests/rest-api/000077500000000000000000000000001305260156100166205ustar00rootroot00000000000000go-marathon-0.7.1/tests/rest-api/methods.yml000066400000000000000000001156541305260156100210220ustar00rootroot00000000000000- 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, "ports": [ 0 ], "requirePorts": false, "storeUrls": [], "upgradeStrategy": { "minimumHealthCapacity": 0.5, "maximumOverCapacity": 0.5 }, "uris": [], "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", "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, "ports": [ 10000, 10001 ], "requirePorts": false, "storeUrls": [], "tasksRunning": 2, "tasksStaged": 0, "upgradeStrategy": { "minimumHealthCapacity": 1.0 }, "uris": [], "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, "ports": [ 10000, 10001 ], "requirePorts": false, "storeUrls": [], "tasksRunning": 2, "tasksStaged": 0, "upgradeStrategy": { "minimumHealthCapacity": 1.0 }, "uris": [], "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", "network": "BRIDGE", "portMappings": [ { "containerPort": 8080, "hostPort": 0, "servicePort": 9000, "protocol": "tcp" } ] }, "type": "DOCKER", "volumes": [] }, "cpus": 0.2, "dependencies": [], "deployments": [], "disk": 0.0, "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", "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, "ports": [ 10000 ], "requirePorts": false, "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 }, "uris": [ "http://downloads.mesosphere.com/misc/toggle.tgz" ], "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", "network": "BRIDGE", "portMappings": [ { "containerPort": 8080, "hostPort": 0, "servicePort": 9000, "protocol": "tcp" } ] }, "type": "DOCKER", "volumes": [] }, "cpus": 0.2, "dependencies": [], "deployments": [], "disk": 0.0, "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", "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, "ports": [ 10000 ], "requirePorts": false, "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 }, "uris": [ "http://downloads.mesosphere.com/misc/toggle.tgz" ], "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", "network": "BRIDGE", "portMappings": [ { "containerPort": 8080, "hostPort": 0, "servicePort": 9000, "protocol": "tcp" } ] }, "type": "DOCKER", "volumes": [] }, "cpus": 0.2, "dependencies": [], "deployments": [], "disk": 0.0, "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, "ports": [ 10000 ], "requirePorts": false, "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 }, "uris": [ "http://downloads.mesosphere.com/misc/toggle.tgz" ], "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/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, "ports": [ 10000 ], "requirePorts": false, "storeUrls": [], "upgradeStrategy": { "minimumHealthCapacity": 1.0 }, "uris": [], "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, "ports": [ 10000 ], "requirePorts": false, "storeUrls": [], "upgradeStrategy": { "minimumHealthCapacity": 1.0 }, "uris": [], "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", "network": "BRIDGE", "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": [], "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", "network": "BRIDGE", "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": "", "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", "network": "BRIDGE", "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, "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": [] } ], "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/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, "ports": [10000], "requirePorts": false, "storeUrls": [], "upgradeStrategy": { "minimumHealthCapacity": 1.0 }, "uris": [ "http://downloads.mesosphere.com/misc/toggle.tgz" ], "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, "ports": [ 10000, 10001 ], "requirePorts": false, "storeUrls": [], "tasksRunning": 2, "tasksStaged": 0, "upgradeStrategy": { "minimumHealthCapacity": 1.0 }, "uris": [], "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, "ports": [ 10000, 10001 ], "requirePorts": false, "storeUrls": [], "tasksRunning": 2, "tasksStaged": 0, "upgradeStrategy": { "minimumHealthCapacity": 1.0 }, "uris": [], "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, "ports": [ 10000 ], "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" } } go-marathon-0.7.1/update_strategy.go000066400000000000000000000022721305260156100174700ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon // 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 } go-marathon-0.7.1/utils.go000066400000000000000000000054121305260156100154230ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon 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 } go-marathon-0.7.1/utils_test.go000066400000000000000000000054441305260156100164670ustar00rootroot00000000000000/* Copyright 2014 Rohith All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package marathon 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)) }