[
  {
    "path": ".gitignore",
    "content": "# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\ngo-marathon.iml\n.idea/\nGemfile.lock\nthin.*\nexamples/applications/applications\nexamples/events_callback_transport/events_callback_transport\nexamples/events_sse_transport/events_sse_transport\nexamples/glog/glog\nexamples/groups/groups\nexamples/multiple_endpoints/multiple_endpoints\nexamples/pods/pods\nexamples/queue/queue\nexamples/tasks/tasks\ncoverage\n\n# Folders\n_obj\n_test\n\n# Architecture specific extensions/prefixes\n*.[568vq]\n[568vq].out\n\n*.cgo1.go\n*.cgo2.c\n_cgo_defun.c\n_cgo_gotypes.go\n_cgo_export.*\n\n_testmain.go\n\n*.exe\n*.test\n*.prof\n\ntests/rest-api/rest-api\n"
  },
  {
    "path": ".travis.yml",
    "content": "env:\n  global:\n    secure: YiSCbBUz0VMONSBZ6TfRaSM9bFBuT5xvaknt9WxWczPSiSgiY8+dGYlsOaX2jzI26J4zA8KxIyxOihN1UE28tkkGoXRkRovoQuOl9YUYp+VCtZdaeksZ7tJ/j/b6aYGpGN3GRRfxkuIhXw1ghZLgqdCVtqfmD3GODlmeuFE01ug=\nlanguage: go\ngo:\n- 1.6\n- 1.7\n- 1.8\n- 1.9\n- \"1.10\"\n- \"1.11\"\ninstall:\n- go get github.com/mattn/goveralls\nscript:\n- make test examples\n- if ([[ ${TRAVIS_BRANCH} == \"master\" ]] && [[ ${TRAVIS_EVENT_TYPE} == \"push\" ]]); then\n    make coverage;\n    goveralls -coverprofile=coverage -service=travis-ci;\n  fi\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Change Log\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](http://keepachangelog.com/)\nand this project adheres to [Semantic Versioning](http://semver.org/).\n\n## [Unreleased]\n### Added\n- [#273][PR273] Implement readiness checks.\n- [#267][PR267] Add DCOS path parameter for additional marathon instances.\n\n## [0.7.1] - 2017-02-20\n### Fixed\n- [#261][PR261] Fix URL parsing for Go 1.8.\n\n## [0.7.0] - 2017-02-17\n### Added\n- [#256][PR256] Expose task state.\n\n### Changed\n- [#259][PR259] Add 'omitempty' to UpgradeStrategy properties.\n\n## [0.6.0] - 2016-12-14\n### Added\n- [#246][PR246] Add TaskKillGracePeriodSeconds support.\n- [#244][PR244] Add taskStats support.\n\n### Changed\n- [#242][PR242] Pointerize IPAddressPerTask.Discovery.\n\n## [0.5.1] - 2016-11-09\n### Fixed\n- [#239][PR239] Fix scheme-less endpoint with port.\n\n## [0.5.0] - 2016-11-07\n### Fixed\n- [#231][PR231] Fixed Marathon cluster code\n- [#229][PR229] Add LastFailureCause field to HealthCheckResult.\n\n## [0.4.0] - 2016-10-28\n### Added\n- [#223][PR223] Add support for IP-per-task.\n- [#220][PR220] Add external volume definition to container.\n- [#211][PR211] Close event channel on event listener removal.\n\n### Fixed\n- [#218][PR218] Remove TimeWaitPolling from marathonClient.\n- [#214][PR214] Remove extra pointer layers when passing to r.api*.\n\n## [0.3.0] - 2016-09-28\n- [#201][PR201]: Subscribe method is now exposed on the client to allow subscription of callback URL's\n\n### Fixed\n- [#205][PR205]: Fix memory leak by signalling goroutine termination on event listener removal.\n\n### Changed\n- [#205][PR205]: Change AddEventsListener to return event channel instead of taking one.\n\n## [0.2.0] - 2016-09-23\n### Added\n- [#196][PR196]: Port definitions.\n- [#191][PR191]: name and labels to portMappings.\n\n### Changed\n- [#191][PR191] ExposePort() now takes a portMapping instance.\n\n### Fixed\n- [#202][PR202]: Timeout error in WaitOnApplication.\n\n## [0.1.1] - 2016-09-07\n### Fixed\n- Drop question mark-only query parameter in Applications(url.Values) manually\n  due to changed behavior in Go 1.7's net/url.Parse.\n\n## [0.1.0] - 2016-08-01\n### Added\n- Field `message` to the EventStatusUpdate struct.\n- Method `Host()` to set host mode explicitly.\n- Field `port` to HealthCheck.\n- Support for launch queues.\n- Convenience method `AddFetchURIs()`.\n- Support for forced operations across all methods.\n- Filtering method variants (`*By`-suffixed).\n- Support for Marathon DCOS token.\n- Basic auth and HTTP client settings.\n- Marshalling of `Deployment.DeploymentStep` for Marathon v1.X.\n- Field `ipAddresses` to tasks and events.\n- Field `slaveId` to tasks.\n- Convenience methods to populate/clear pointerized values.\n- Method `ApplicationByVersion()` to retrieve version-specific apps.\n- Support for fetch URIs.\n- Parse API error responses on all error types for programmatic evaluation.\n\n### Changed\n- Consider app as unhealthy in ApplicationOK if health check is missing. (Ensures result stability during all phases of deployment.)\n- Various identifiers violating golint rules.\n- Do not set \"bridged\" mode on Docker containers by default.\n\n### Fixed\n- Flawed unmarshalling of `CurrentStep` in events.\n- Missing omitempty tag modifiers on `Application.Uris`.\n- Missing leading slash in path used by `Ping()`.\n- Flawed `KillTask()` in case of hierarchical app ID path.\n- Missing omitempty tag modifier on `PortMapping.Protocol`.\n- Nil dereference on empty debug log.\n- Various occasions where omitted and empty fields could not be distinguished.\n\n## 0.0.1 - 2016-01-27\n### Added\n- Initial SemVer release.\n\n[Unreleased]: https://github.com/gambol99/go-marathon/compare/v0.7.1...HEAD\n[0.7.1]: https://github.com/gambol99/go-marathon/compare/v0.7.0...v0.7.1\n[0.7.0]: https://github.com/gambol99/go-marathon/compare/v0.6.0...v0.7.0\n[0.6.0]: https://github.com/gambol99/go-marathon/compare/v0.5.1...v0.6.0\n[0.5.1]: https://github.com/gambol99/go-marathon/compare/v0.5.0...v0.5.1\n[0.5.0]: https://github.com/gambol99/go-marathon/compare/v0.4.0...v0.5.0\n[0.4.0]: https://github.com/gambol99/go-marathon/compare/v0.3.0...v0.4.0\n[0.3.0]: https://github.com/gambol99/go-marathon/compare/v0.2.0...v0.3.0\n[0.2.0]: https://github.com/gambol99/go-marathon/compare/v0.1.1...v0.2.0\n[0.1.1]: https://github.com/gambol99/go-marathon/compare/v0.1.0...v0.1.1\n[0.1.0]: https://github.com/gambol99/go-marathon/compare/v0.0.1...v0.1.0\n\n[PR273]: https://github.com/gambol99/go-marathon/pull/273\n[PR267]: https://github.com/gambol99/go-marathon/pull/267\n[PR261]: https://github.com/gambol99/go-marathon/pull/261\n[PR259]: https://github.com/gambol99/go-marathon/pull/259\n[PR256]: https://github.com/gambol99/go-marathon/pull/256\n[PR246]: https://github.com/gambol99/go-marathon/pull/246\n[PR244]: https://github.com/gambol99/go-marathon/pull/244\n[PR242]: https://github.com/gambol99/go-marathon/pull/242\n[PR239]: https://github.com/gambol99/go-marathon/pull/239\n[PR231]: https://github.com/gambol99/go-marathon/pull/231\n[PR229]: https://github.com/gambol99/go-marathon/pull/229\n[PR223]: https://github.com/gambol99/go-marathon/pull/223\n[PR220]: https://github.com/gambol99/go-marathon/pull/220\n[PR218]: https://github.com/gambol99/go-marathon/pull/218\n[PR214]: https://github.com/gambol99/go-marathon/pull/214\n[PR211]: https://github.com/gambol99/go-marathon/pull/211\n[PR205]: https://github.com/gambol99/go-marathon/pull/205\n[PR202]: https://github.com/gambol99/go-marathon/pull/202\n[PR201]: https://github.com/gambol99/go-marathon/pull/201\n[PR196]: https://github.com/gambol99/go-marathon/pull/196\n[PR191]: https://github.com/gambol99/go-marathon/pull/191\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contribution Guidelines\n\n## Pre-Development\n- Look for an existing Github issue describing the bug you have found/feature request you would like to see getting implemented.\n- 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.\n\n## Development\n- Fork the repository.\n- Create a feature branch (`git checkout -b my-new-feature master`).\n- Commit your changes, preferring one commit per logical unit of work. Often times, this simply means having a single commit.\n- If applicable, update the documentation in the [README file](README.md).\n- In the vast majority of cases, you should add/amend a (regression) test for your bug fix/feature.\n- Push your branch (`git push origin my-new-feature`).\n- Create a new pull request.\n- 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.\n"
  },
  {
    "path": "LICENSE",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction,\nand distribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by\nthe copyright owner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all\nother entities that control, are controlled by, or are under common\ncontrol with that entity. For the purposes of this definition,\n\"control\" means (i) the power, direct or indirect, to cause the\ndirection or management of such entity, whether by contract or\notherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity\nexercising permissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications,\nincluding but not limited to software source code, documentation\nsource, and configuration files.\n\n\"Object\" form shall mean any form resulting from mechanical\ntransformation or translation of a Source form, including but\nnot limited to compiled object code, generated documentation,\nand conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or\nObject form, made available under the License, as indicated by a\ncopyright notice that is included in or attached to the work\n(an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object\nform, that is based on (or derived from) the Work and for which the\neditorial revisions, annotations, elaborations, or other modifications\nrepresent, as a whole, an original work of authorship. For the purposes\nof this License, Derivative Works shall not include works that remain\nseparable from, or merely link (or bind by name) to the interfaces of,\nthe Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including\nthe original version of the Work and any modifications or additions\nto that Work or Derivative Works thereof, that is intentionally\nsubmitted to Licensor for inclusion in the Work by the copyright owner\nor by an individual or Legal Entity authorized to submit on behalf of\nthe copyright owner. For the purposes of this definition, \"submitted\"\nmeans any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems,\nand issue tracking systems that are managed by, or on behalf of, the\nLicensor for the purpose of discussing and improving the Work, but\nexcluding communication that is conspicuously marked or otherwise\ndesignated in writing by the copyright owner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity\non behalf of whom a Contribution has been received by Licensor and\nsubsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\nthis License, each Contributor hereby grants to You a perpetual,\nworldwide, non-exclusive, no-charge, royalty-free, irrevocable\ncopyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the\nWork and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\nthis License, each Contributor hereby grants to You a perpetual,\nworldwide, non-exclusive, no-charge, royalty-free, irrevocable\n(except as stated in this section) patent license to make, have made,\nuse, offer to sell, sell, import, and otherwise transfer the Work,\nwhere such license applies only to those patent claims licensable\nby such Contributor that are necessarily infringed by their\nContribution(s) alone or by combination of their Contribution(s)\nwith the Work to which such Contribution(s) was submitted. If You\ninstitute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work\nor a Contribution incorporated within the Work constitutes direct\nor contributory patent infringement, then any patent licenses\ngranted to You under this License for that Work shall terminate\nas of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\nWork or Derivative Works thereof in any medium, with or without\nmodifications, and in Source or Object form, provided that You\nmeet the following conditions:\n\n(a) You must give any other recipients of the Work or\nDerivative Works a copy of this License; and\n\n(b) You must cause any modified files to carry prominent notices\nstating that You changed the files; and\n\n(c) You must retain, in the Source form of any Derivative Works\nthat You distribute, all copyright, patent, trademark, and\nattribution notices from the Source form of the Work,\nexcluding those notices that do not pertain to any part of\nthe Derivative Works; and\n\n(d) If the Work includes a \"NOTICE\" text file as part of its\ndistribution, then any Derivative Works that You distribute must\ninclude a readable copy of the attribution notices contained\nwithin such NOTICE file, excluding those notices that do not\npertain to any part of the Derivative Works, in at least one\nof the following places: within a NOTICE text file distributed\nas part of the Derivative Works; within the Source form or\ndocumentation, if provided along with the Derivative Works; or,\nwithin a display generated by the Derivative Works, if and\nwherever such third-party notices normally appear. The contents\nof the NOTICE file are for informational purposes only and\ndo not modify the License. You may add Your own attribution\nnotices within Derivative Works that You distribute, alongside\nor as an addendum to the NOTICE text from the Work, provided\nthat such additional attribution notices cannot be construed\nas modifying the License.\n\nYou may add Your own copyright statement to Your modifications and\nmay provide additional or different license terms and conditions\nfor use, reproduction, or distribution of Your modifications, or\nfor any such Derivative Works as a whole, provided Your use,\nreproduction, and distribution of the Work otherwise complies with\nthe conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\nany Contribution intentionally submitted for inclusion in the Work\nby You to the Licensor shall be under the terms and conditions of\nthis License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify\nthe terms of any separate license agreement you may have executed\nwith Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\nnames, trademarks, service marks, or product names of the Licensor,\nexcept as required for reasonable and customary use in describing the\norigin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\nagreed to in writing, Licensor provides the Work (and each\nContributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\nimplied, including, without limitation, any warranties or conditions\nof TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\nPARTICULAR PURPOSE. You are solely responsible for determining the\nappropriateness of using or redistributing the Work and assume any\nrisks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\nwhether in tort (including negligence), contract, or otherwise,\nunless required by applicable law (such as deliberate and grossly\nnegligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special,\nincidental, or consequential damages of any character arising as a\nresult of this License or out of the use or inability to use the\nWork (including but not limited to damages for loss of goodwill,\nwork stoppage, computer failure or malfunction, or any and all\nother commercial damages or losses), even if such Contributor\nhas been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\nthe Work or Derivative Works thereof, You may choose to offer,\nand charge a fee for, acceptance of support, warranty, indemnity,\nor other liability obligations and/or rights consistent with this\nLicense. However, in accepting such obligations, You may act only\non Your own behalf and on Your sole responsibility, not on behalf\nof any other Contributor, and only if You agree to indemnify,\ndefend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason\nof your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\nTo apply the Apache License to your work, attach the following\nboilerplate notice, with the fields enclosed by brackets \"{}\"\nreplaced with your own identifying information. (Don't include\nthe brackets!)  The text should be enclosed in the appropriate\ncomment syntax for the file format. We also recommend that a\nfile or class name and description of purpose be included on the\nsame \"printed page\" as the copyright notice for easier\nidentification within third-party archives.\n\nCopyright {yyyy} {name of copyright owner}\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "Makefile",
    "content": "#\n#   Author: Rohith (gambol99@gmail.com)\n#   Date: 2015-02-10 15:35:14 +0000 (Tue, 10 Feb 2015)\n#\n#  vim:ts=2:sw=2:et\n#\nHARDWARE=$(shell uname -m)\nVERSION=$(shell awk '/const Version/ { print $$4 }' version.go | sed 's/\"//g')\nDEPS=$(shell go list -f '{{range .TestImports}}{{.}} {{end}}' ./...)\nPACKAGES=$(shell go list ./...)\nVETARGS?=-asmdecl -atomic -bool -buildtags -copylocks -methods -nilfunc -printf -rangeloops -shift -structtags -unsafeptr\n\n.PHONY: test examples changelog check-format coverage cover\n\nbuild:\n\tgo build\n\ndeps:\n\t@echo \"--> Installing build dependencies\"\n\t@go get -d -v ./... $(DEPS)\n\nlint:\n\t@echo \"--> Running golint\"\n\t@which golint 2>/dev/null ; if [ $$? -eq 1 ]; then \\\n\t\tgo get -u github.com/golang/lint/golint; \\\n\tfi\n\t@golint .\n\nvet:\n\t@echo \"--> Running go tool vet $(VETARGS) .\"\n\t@go tool vet 2>/dev/null ; if [ $$? -eq 3 ]; then \\\n\t\tgo get golang.org/x/tools/cmd/vet; \\\n\tfi\n\t@go tool vet $(VETARGS) .\n\ncover:\n\t@echo \"--> Running go test --cover\"\n\t@go test --cover\n\ncoverage:\n\t@echo \"--> Running go coverage\"\n\t@go test -covermode=count -coverprofile=coverage\n\nformat:\n\t@echo \"--> Running go fmt\"\n\t@go fmt $(PACKAGES)\n\ncheck-format:\n\t@echo \"--> Checking format\"\n\t@if gofmt -l . 2>&1 | grep -q '.go'; then \\\n\t\techo \"found unformatted files:\"; \\\n\t\techo; \\\n\t\tgofmt -l .; \\\n\t\texit 1; \\\n\tfi\n\ntest: deps vet\n\t@echo \"--> Running go tests\"\n\t@go test -race -v\n\t@$(MAKE) cover\n\nexamples:\n\tmake -C examples all\n\nchangelog: release\n\tgit log $(shell git tag | tail -n1)..HEAD --no-merges --format=%B > changelog\n"
  },
  {
    "path": "README.md",
    "content": "[![Build Status](https://travis-ci.org/gambol99/go-marathon.svg?branch=master)](https://travis-ci.org/gambol99/go-marathon)\n[![GoDoc](http://godoc.org/github.com/gambol99/go-marathon?status.png)](http://godoc.org/github.com/gambol99/go-marathon)\n[![Go Report Card](https://goreportcard.com/badge/github.com/katallaxie/go-marathon)](https://goreportcard.com/report/github.com/katallaxie/go-marathon)\n[![Coverage Status](https://coveralls.io/repos/github/gambol99/go-marathon/badge.svg?branch=master)](https://coveralls.io/github/gambol99/go-marathon?branch=master)\n\n# Go-Marathon\n\nGo-marathon is a API library for working with [Marathon](https://mesosphere.github.io/marathon/).\nIt currently supports\n\n- Application and group deployment\n- Helper filters for pulling the status, configuration and tasks\n- Multiple Endpoint support for HA deployments\n- Marathon Event Subscriptions and Event Streams\n- Pods\n\nNote: the library is still under active development; users should expect frequent (possibly breaking) API changes for the time being.\n\nIt requires Go version 1.6 or higher.\n\n## Code Examples\n\nThere is also an examples directory in the source which shows hints and snippets of code of how to use it —\nwhich is probably the best place to start.\n\nYou can use `examples/docker-compose.yml` in order to start a test cluster.\n\n### Creating a client\n\n```go\nimport (\n\tmarathon \"github.com/gambol99/go-marathon\"\n)\n\nmarathonURL := \"http://10.241.1.71:8080\"\nconfig := marathon.NewDefaultConfig()\nconfig.URL = marathonURL\nclient, err := marathon.NewClient(config)\nif err != nil {\n\tlog.Fatalf(\"Failed to create a client for marathon, error: %s\", err)\n}\n\napplications, err := client.Applications(nil)\n...\n```\n\nNote, you can also specify multiple endpoint for Marathon (i.e. you have setup Marathon in HA mode and having multiple running)\n\n```go\nmarathonURL := \"http://10.241.1.71:8080,10.241.1.72:8080,10.241.1.73:8080\"\n```\n\nThe first one specified will be used, if that goes offline the member is marked as *\"unavailable\"* and a\nbackground process will continue to ping the member until it's back online.\n\nYou can also pass a custom path to the URL, which is especially needed in case of DCOS:\n\n```go\nmarathonURL := \"http://10.241.1.71:8080/cluster,10.241.1.72:8080/cluster,10.241.1.73:8080/cluster\"\n```\n\nIf you specify a `DCOSToken` in the configuration file but do not pass a custom URL path, `/marathon` will be used.\n\n### Customizing the HTTP Clients\n\nHTTP clients with reasonable timeouts are used by default. It is possible to pass custom clients to the configuration though if the behavior should be customized (e.g., to bypass TLS verification, load root CAs, or change timeouts).\n\nTwo clients can be given independently of each other:\n\n- `HTTPClient` used only for (non-SSE) HTTP API requests. By default, an http.Client with 10 seconds timeout for the entire request is used.\n- `HTTPSSEClient` used only for SSE-based subscription requests. Note that `HTTPSSEClient` cannot have a response read timeout set as this breaks SSE communication; trying to do so will lead to an error during the SSE connection setup. By default, an http.Client with 5 seconds timeout for dial and TLS handshake, and 10 seconds timeout for response headers received is used.\n\nIf no `HTTPSSEClient` is given but an `HTTPClient` is, it will be used for SSE subscriptions as well (thereby overriding the default SSE HTTP client).\n\n```go\nmarathonURL := \"http://10.241.1.71:8080\"\nconfig := marathon.NewDefaultConfig()\nconfig.URL = marathonURL\nconfig.HTTPClient = &http.Client{\n    Timeout: (time.Duration(10) * time.Second),\n    Transport: &http.Transport{\n        Dial: (&net.Dialer{\n            Timeout:   10 * time.Second,\n            KeepAlive: 10 * time.Second,\n        }).Dial,\n        TLSClientConfig: &tls.Config{\n            InsecureSkipVerify: true,\n        },\n    },\n}\nconfig.HTTPSSEClient = &http.Client{\n    // Invalid to set Timeout as it contains timeout for reading a response body\n    Transport: &http.Transport{\n        Dial: (&net.Dialer{\n            Timeout:   10 * time.Second,\n            KeepAlive: 10 * time.Second,\n        }).Dial,\n        TLSClientConfig: &tls.Config{\n            InsecureSkipVerify: true,\n        },\n    },\n}\n```\n\n### Listing the applications\n\n```go\napplications, err := client.Applications(nil)\nif err != nil {\n\tlog.Fatalf(\"Failed to list applications: %s\", err)\n}\n\nlog.Printf(\"Found %d application(s) running\", len(applications.Apps))\nfor _, application := range applications.Apps {\n\tlog.Printf(\"Application: %s\", application)\n\tappID := application.ID\n\n\tdetails, err := client.Application(appID)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to get application %s: %s\", appID, err)\n\t}\n\tif details.Tasks != nil {\n\t\tfor _, task := range details.Tasks {\n\t\t\tlog.Printf(\"application %s has task: %s\", appID, task)\n\t\t}\n\t}\n}\n```\n\n### Creating a new application\n\n```go\nlog.Printf(\"Deploying a new application\")\napplication := marathon.NewDockerApplication().\n  Name(applicationName).\n  CPU(0.1).\n  Memory(64).\n  Storage(0.0).\n  Count(2).\n  AddArgs(\"/usr/sbin/apache2ctl\", \"-D\", \"FOREGROUND\").\n  AddEnv(\"NAME\", \"frontend_http\").\n  AddEnv(\"SERVICE_80_NAME\", \"test_http\").\n  CheckHTTP(\"/health\", 10, 5)\n\napplication.\n  Container.Docker.Container(\"quay.io/gambol99/apache-php:latest\").\n  Bridged().\n  Expose(80).\n  Expose(443)\n\nif _, err := client.CreateApplication(application); err != nil {\n\tlog.Fatalf(\"Failed to create application: %s, error: %s\", application, err)\n} else {\n\tlog.Printf(\"Created the application: %s\", application)\n}\n```\n\nNote: 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.\n\n### Scaling application\n\nChange the number of application instances to 4\n\n```go\nlog.Printf(\"Scale to 4 instances\")\nif err := client.ScaleApplicationInstances(application.ID, 4); err != nil {\n\tlog.Fatalf(\"Failed to delete the application: %s, error: %s\", application, err)\n} else {\n\tclient.WaitOnApplication(application.ID, 30 * time.Second)\n\tlog.Printf(\"Successfully scaled the application\")\n}\n```\n\n### Pods\n\nPods allow you to deploy groups of tasks as a unit. All tasks in a single instance of a pod share networking and storage. View the [Marathon documentation](https://mesosphere.github.io/marathon/docs/pods.html) for more details on this feature.\n\nExamples of their usage can be seen in the `examples/pods` directory, and a smaller snippet is below.\n\n```Go\n// Initialize a single-container pod running nginx\npod := marathon.NewPod()\n\nimage := marathon.NewDockerPodContainerImage().SetID(\"nginx\")\n\ncontainer := marathon.NewPodContainer().\n\tSetName(\"container\", i).\n\tCPUs(0.1).\n\tMemory(128).\n\tSetImage(image)\n\npod.Name(\"mypod\").AddContainer(container)\n\n// Create it and wait for it to start up\npod, err := client.CreatePod(pod)\nerr = client.WaitOnPod(pod.ID, time.Minute*1)\n\n// Scale it\npod.Count(5)\npod, err = client.UpdatePod(pod, true)\n\n// Delete it\nid, err := client.DeletePod(pod.ID, true)\n```\n\n### Subscription & Events\n\nRequest to listen to events related to applications — namely status updates, health checks\nchanges and failures. There are two different event transports controlled by `EventsTransport`\nsetting with the following possible values: `EventsTransportSSE` and `EventsTransportCallback` (default value).\nSee [Event Stream](https://mesosphere.github.io/marathon/docs/rest-api.html#event-stream) and\n[Event Subscriptions](https://mesosphere.github.io/marathon/docs/rest-api.html#event-subscriptions) for details.\n\nEvent subscriptions can also be individually controlled with the `Subscribe` and `Unsubscribe` functions. See [Controlling subscriptions](#controlling-subscriptions) for more details.\n\n#### Event Stream\n\nOnly available in Marathon >= 0.9.0. Does not require any special configuration or prerequisites.\n\n```go\n// Configure client\nconfig := marathon.NewDefaultConfig()\nconfig.URL = marathonURL\nconfig.EventsTransport = marathon.EventsTransportSSE\n\nclient, err := marathon.NewClient(config)\nif err != nil {\n\tlog.Fatalf(\"Failed to create a client for marathon, error: %s\", err)\n}\n\n// Register for events\nevents, err = client.AddEventsListener(marathon.EventIDApplications)\nif err != nil {\n\tlog.Fatalf(\"Failed to register for events, %s\", err)\n}\n\ntimer := time.After(60 * time.Second)\ndone := false\n\n// Receive events from channel for 60 seconds\nfor {\n\tif done {\n\t\tbreak\n\t}\n\tselect {\n\tcase <-timer:\n\t\tlog.Printf(\"Exiting the loop\")\n\t\tdone = true\n\tcase event := <-events:\n\t\tlog.Printf(\"Received event: %s\", event)\n\t}\n}\n\n// Unsubscribe from Marathon events\nclient.RemoveEventsListener(events)\n```\n\n#### Event Subscriptions\n\nRequires to start a built-in web server accessible by Marathon to connect and push events to. Consider the following\nadditional settings:\n\n- `EventsInterface` — the interface we should be listening on for events. Default `\"eth0\"`.\n- `EventsPort` — built-in web server port. Default `10001`.\n- `CallbackURL` — custom callback URL. Default `\"\"`.\n\n```go\n// Configure client\nconfig := marathon.NewDefaultConfig()\nconfig.URL = marathonURL\nconfig.EventsInterface = marathonInterface\nconfig.EventsPort = marathonPort\n\nclient, err := marathon.NewClient(config)\nif err != nil {\n\tlog.Fatalf(\"Failed to create a client for marathon, error: %s\", err)\n}\n\n// Register for events\nevents, err = client.AddEventsListener(marathon.EventIDApplications)\nif err != nil {\n\tlog.Fatalf(\"Failed to register for events, %s\", err)\n}\n\ntimer := time.After(60 * time.Second)\ndone := false\n\n// Receive events from channel for 60 seconds\nfor {\n\tif done {\n\t\tbreak\n\t}\n\tselect {\n\tcase <-timer:\n\t\tlog.Printf(\"Exiting the loop\")\n\t\tdone = true\n\tcase event := <-events:\n\t\tlog.Printf(\"Received event: %s\", event)\n\t}\n}\n\n// Unsubscribe from Marathon events\nclient.RemoveEventsListener(events)\n```\n\nSee [events.go](events.go) for a full list of event IDs.\n\n#### Controlling subscriptions\nIf you simply want to (de)register event subscribers (i.e. without starting an internal web server) you can use the `Subscribe` and `Unsubscribe` methods.\n\n```go\n// Configure client\nconfig := marathon.NewDefaultConfig()\nconfig.URL = marathonURL\n\nclient, err := marathon.NewClient(config)\nif err != nil {\n\tlog.Fatalf(\"Failed to create a client for marathon, error: %s\", err)\n}\n\n// Register an event subscriber via a callback URL\ncallbackURL := \"http://10.241.1.71:9494\"\nif err := client.Subscribe(callbackURL); err != nil {\n\tlog.Fatalf(\"Unable to register the callbackURL [%s], error: %s\", callbackURL, err)\n}\n\n// Deregister the same subscriber\nif err := client.Unsubscribe(callbackURL); err != nil {\n\tlog.Fatalf(\"Unable to deregister the callbackURL [%s], error: %s\", callbackURL, err)\n}\n```\n\n## Contributing\n\nSee the [contribution guidelines](CONTRIBUTING.md).\n\n## Development\n\n### Marathon Fake\n\ngo-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_:\n\n- HTTP URI (without the protocol and the hostname, e.g., `/v2/apps`)\n- HTTP method (e.g., `GET`)\n- response content (i.e., the message returned)\n- scope (see below)\n\n#### Response Content\n\nThe response content can be provided in one of two forms:\n\n- **static:** A pure response message returned on every match, including repeated queries.\n- **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.\n\nAn example for a trivial static response content is\n\n```yaml\n- uri: /v2/apps\n  method: POST\n  content: |\n\t\t{\n\t\t\"app\": {\n\t\t}\n\t\t}\n```\n\nwhich would be returned for every POST request targetting `/v2/apps`.\n\nAn indexed response content would look like:\n\n```yaml\n- uri: /v2/apps\n  method: POST\n  contentSequence:\n\t\t- index: 1\n\t\t- content: |\n\t\t\t{\n\t\t\t\"app\": {\n\t\t\t\t\"id\": \"foo\"\n\t\t\t}\n\t\t\t}\n\t\t- index: 3\n\t\t- content: |\n\t\t\t{\n\t\t\t\"app\": {\n\t\t\t\t\"id\": \"bar\"\n\t\t\t}\n\t\t\t}\n```\n\nWhat 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.\n\nTrying to define both a static and indexed response content constitutes an error and leads to `panic`.\n\n#### Scope\n\nBy 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.\n\nScopes 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\n\n```yaml\n- uri: /v2/apps\n  method: GET\n\t# Note: no scope defined.\n  content: |\n\t\t{\n\t\t\"app\": {\n\t\t\t\"id\": \"foo\"\n\t\t}\n\t\t}\n- uri: /v2/apps\n  method: GET\n\tscope: v1.1.1  # This one does have a scope.\n  contentSequence:\n\t\t- index: 1\n\t\t- content: |\n\t\t\t{\n\t\t\t\"app\": {\n\t\t\t\t\"id\": \"bar\"\n\t\t\t}\n\t\t\t}\n```\n\nand the tests\n\n```go\nfunc TestFoo(t * testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)  // No custom configs given.\n\tdefer endpoint.Close()\n\tapp, err := endpoint.Client.Applications(nil)\n\t// Do something with \"foo\"\n}\n\nfunc TestFoo(t * testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, &configContainer{\n\t\tserver: &serverConfig{\n\t\t\tscope: \"v1.1.1\",\t\t// Matches the message spec's scope.\n\t\t},\n\t})\n\tdefer endpoint.Close()\n\tapp, err := endpoint.Client.Applications(nil)\n\t// Do something with \"bar\"\n}\n```\n\nThe \"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.\n"
  },
  {
    "path": "application.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"time\"\n)\n\nvar (\n\t// ErrNoApplicationContainer is thrown when a container has been specified yet\n\tErrNoApplicationContainer = errors.New(\"you have not specified a docker container yet\")\n)\n\n// Applications is a collection of applications\ntype Applications struct {\n\tApps []Application `json:\"apps\"`\n}\n\n// IPAddressPerTask is used by IP-per-task functionality https://mesosphere.github.io/marathon/docs/ip-per-task.html\ntype IPAddressPerTask struct {\n\tGroups      *[]string          `json:\"groups,omitempty\"`\n\tLabels      *map[string]string `json:\"labels,omitempty\"`\n\tDiscovery   *Discovery         `json:\"discovery,omitempty\"`\n\tNetworkName string             `json:\"networkName,omitempty\"`\n}\n\n// Discovery provides info about ports expose by IP-per-task functionality\ntype Discovery struct {\n\tPorts *[]Port `json:\"ports,omitempty\"`\n}\n\n// Port provides info about ports used by IP-per-task\ntype Port struct {\n\tNumber   int    `json:\"number,omitempty\"`\n\tName     string `json:\"name,omitempty\"`\n\tProtocol string `json:\"protocol,omitempty\"`\n}\n\n// Application is the definition for an application in marathon\ntype Application struct {\n\tID          string        `json:\"id,omitempty\"`\n\tCmd         *string       `json:\"cmd,omitempty\"`\n\tArgs        *[]string     `json:\"args,omitempty\"`\n\tConstraints *[][]string   `json:\"constraints,omitempty\"`\n\tContainer   *Container    `json:\"container,omitempty\"`\n\tCPUs        float64       `json:\"cpus,omitempty\"`\n\tGPUs        *float64      `json:\"gpus,omitempty\"`\n\tDisk        *float64      `json:\"disk,omitempty\"`\n\tNetworks    *[]PodNetwork `json:\"networks,omitempty\"`\n\n\t// Contains non-secret environment variables. Secrets environment variables are part of the Secrets map.\n\tEnv                        *map[string]string  `json:\"-\"`\n\tExecutor                   *string             `json:\"executor,omitempty\"`\n\tHealthChecks               *[]HealthCheck      `json:\"healthChecks,omitempty\"`\n\tReadinessChecks            *[]ReadinessCheck   `json:\"readinessChecks,omitempty\"`\n\tInstances                  *int                `json:\"instances,omitempty\"`\n\tMem                        *float64            `json:\"mem,omitempty\"`\n\tTasks                      []*Task             `json:\"tasks,omitempty\"`\n\tPorts                      []int               `json:\"ports\"`\n\tPortDefinitions            *[]PortDefinition   `json:\"portDefinitions,omitempty\"`\n\tRequirePorts               *bool               `json:\"requirePorts,omitempty\"`\n\tBackoffSeconds             *float64            `json:\"backoffSeconds,omitempty\"`\n\tBackoffFactor              *float64            `json:\"backoffFactor,omitempty\"`\n\tMaxLaunchDelaySeconds      *float64            `json:\"maxLaunchDelaySeconds,omitempty\"`\n\tTaskKillGracePeriodSeconds *float64            `json:\"taskKillGracePeriodSeconds,omitempty\"`\n\tDeployments                []map[string]string `json:\"deployments,omitempty\"`\n\t// Available when embedding readiness information through query parameter.\n\tReadinessCheckResults *[]ReadinessCheckResult `json:\"readinessCheckResults,omitempty\"`\n\tDependencies          []string                `json:\"dependencies\"`\n\tTasksRunning          int                     `json:\"tasksRunning,omitempty\"`\n\tTasksStaged           int                     `json:\"tasksStaged,omitempty\"`\n\tTasksHealthy          int                     `json:\"tasksHealthy,omitempty\"`\n\tTasksUnhealthy        int                     `json:\"tasksUnhealthy,omitempty\"`\n\tTaskStats             map[string]TaskStats    `json:\"taskStats,omitempty\"`\n\tUser                  string                  `json:\"user,omitempty\"`\n\tUpgradeStrategy       *UpgradeStrategy        `json:\"upgradeStrategy,omitempty\"`\n\tUnreachableStrategy   *UnreachableStrategy    `json:\"unreachableStrategy,omitempty\"`\n\tKillSelection         string                  `json:\"killSelection,omitempty\"`\n\tUris                  *[]string               `json:\"uris,omitempty\"`\n\tVersion               string                  `json:\"version,omitempty\"`\n\tVersionInfo           *VersionInfo            `json:\"versionInfo,omitempty\"`\n\tLabels                *map[string]string      `json:\"labels,omitempty\"`\n\tAcceptedResourceRoles []string                `json:\"acceptedResourceRoles,omitempty\"`\n\tLastTaskFailure       *LastTaskFailure        `json:\"lastTaskFailure,omitempty\"`\n\tFetch                 *[]Fetch                `json:\"fetch,omitempty\"`\n\tIPAddressPerTask      *IPAddressPerTask       `json:\"ipAddress,omitempty\"`\n\tResidency             *Residency              `json:\"residency,omitempty\"`\n\tSecrets               *map[string]Secret      `json:\"-\"`\n\tRole                  *string                 `json:\"role,omitempty\"`\n}\n\n// ApplicationVersions is a collection of application versions for a specific app in marathon\ntype ApplicationVersions struct {\n\tVersions []string `json:\"versions\"`\n}\n\n// ApplicationVersion is the application version response from marathon\ntype ApplicationVersion struct {\n\tVersion string `json:\"version\"`\n}\n\n// VersionInfo is the application versioning details from marathon\ntype VersionInfo struct {\n\tLastScalingAt      string `json:\"lastScalingAt,omitempty\"`\n\tLastConfigChangeAt string `json:\"lastConfigChangeAt,omitempty\"`\n}\n\n// Fetch will download URI before task starts\ntype Fetch struct {\n\tURI        string `json:\"uri\"`\n\tExecutable bool   `json:\"executable\"`\n\tExtract    bool   `json:\"extract\"`\n\tCache      bool   `json:\"cache\"`\n}\n\n// GetAppOpts contains a payload for Application method\n//\t\tembed:\tEmbeds nested resources that match the supplied path.\n// \t\t\t\tYou can specify this parameter multiple times with different values\ntype GetAppOpts struct {\n\tEmbed []string `url:\"embed,omitempty\"`\n}\n\n// DeleteAppOpts contains a payload for DeleteApplication method\n//\t\tforce:\t\toverrides a currently running deployment.\ntype DeleteAppOpts struct {\n\tForce bool `url:\"force,omitempty\"`\n}\n\n// TaskStats is a container for Stats\ntype TaskStats struct {\n\tStats Stats `json:\"stats\"`\n}\n\n// Stats is a collection of aggregate statistics about an application's tasks\ntype Stats struct {\n\tCounts   map[string]int     `json:\"counts\"`\n\tLifeTime map[string]float64 `json:\"lifeTime\"`\n}\n\n// Secret is the environment variable and secret store path associated with a secret.\n// The value for EnvVar is populated from the env field, and Source is populated from\n// the secrets field of the application json.\ntype Secret struct {\n\tEnvVar string\n\tSource string\n}\n\n// SetIPAddressPerTask defines that the application will have a IP address defines by a external agent.\n// This configuration is not allowed to be used with Port or PortDefinitions. Thus, the implementation\n// clears both.\nfunc (r *Application) SetIPAddressPerTask(ipAddressPerTask IPAddressPerTask) *Application {\n\tr.Ports = make([]int, 0)\n\tr.EmptyPortDefinitions()\n\tr.IPAddressPerTask = &ipAddressPerTask\n\n\treturn r\n}\n\n// NewDockerApplication creates a default docker application\nfunc NewDockerApplication() *Application {\n\tapplication := new(Application)\n\tapplication.Container = NewDockerContainer()\n\treturn application\n}\n\n// Name sets the name / ID of the application i.e. the identifier for this application\nfunc (r *Application) Name(id string) *Application {\n\tr.ID = validateID(id)\n\treturn r\n}\n\n// Command sets the cmd of the application\nfunc (r *Application) Command(cmd string) *Application {\n\tr.Cmd = &cmd\n\treturn r\n}\n\n// CPU set the amount of CPU shares per instance which is assigned to the application\n//\t\tcpu:\tthe CPU shared (check Docker docs) per instance\nfunc (r *Application) CPU(cpu float64) *Application {\n\tr.CPUs = cpu\n\treturn r\n}\n\n// SetGPUs set the amount of GPU per instance which is assigned to the application\n//\t\tgpu:\tthe GPU (check MESOS docs) per instance\nfunc (r *Application) SetGPUs(gpu float64) *Application {\n\tr.GPUs = &gpu\n\treturn r\n}\n\n// EmptyGPUs explicitly empties GPUs -- use this if you need to empty\n// gpus of an application that already has gpus set (setting port definitions to nil will\n// keep the current value)\nfunc (r *Application) EmptyGPUs() *Application {\n\tg := 0.0\n\tr.GPUs = &g\n\treturn r\n}\n\n// Storage sets the amount of disk space the application is assigned, which for docker\n// application I don't believe is relevant\n//\t\tdisk:\tthe disk space in MB\nfunc (r *Application) Storage(disk float64) *Application {\n\tr.Disk = &disk\n\treturn r\n}\n\n// AllTaskRunning checks to see if all the application tasks are running, i.e. the instances is equal\n// to the number of running tasks\nfunc (r *Application) AllTaskRunning() bool {\n\tif r.Instances == nil || *r.Instances == 0 {\n\t\treturn true\n\t}\n\tif r.Tasks == nil {\n\t\treturn false\n\t}\n\tif r.TasksRunning == *r.Instances {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// DependsOn adds one or more dependencies for this application. Note, if you want to wait for\n// an application dependency to actually be UP, i.e. not just deployed, you need a health check\n// on the dependant app.\n//\t\tnames:\tthe application id(s) this application depends on\nfunc (r *Application) DependsOn(names ...string) *Application {\n\tif r.Dependencies == nil {\n\t\tr.Dependencies = make([]string, 0)\n\t}\n\tr.Dependencies = append(r.Dependencies, names...)\n\n\treturn r\n}\n\n// Memory sets he amount of memory the application can consume per instance\n//\t\tmemory:\tthe amount of MB to assign\nfunc (r *Application) Memory(memory float64) *Application {\n\tr.Mem = &memory\n\n\treturn r\n}\n\n// AddPortDefinition adds a port definition. Port definitions are used to define ports that\n// should be considered part of a resource. They are necessary when you are using HOST\n// networking and no port mappings are specified.\nfunc (r *Application) AddPortDefinition(portDefinition PortDefinition) *Application {\n\tif r.PortDefinitions == nil {\n\t\tr.EmptyPortDefinitions()\n\t}\n\n\tportDefinitions := *r.PortDefinitions\n\tportDefinitions = append(portDefinitions, portDefinition)\n\tr.PortDefinitions = &portDefinitions\n\treturn r\n}\n\n// EmptyPortDefinitions explicitly empties port definitions -- use this if you need to empty\n// port definitions of an application that already has port definitions set (setting port definitions to nil will\n// keep the current value)\nfunc (r *Application) EmptyPortDefinitions() *Application {\n\tr.PortDefinitions = &[]PortDefinition{}\n\n\treturn r\n}\n\n// Count sets the number of instances of the application to run\n//\t\tcount:\tthe number of instances to run\nfunc (r *Application) Count(count int) *Application {\n\tr.Instances = &count\n\n\treturn r\n}\n\n// SetTaskKillGracePeriod sets the number of seconds between escalating from SIGTERM to SIGKILL\n// when signalling tasks to terminate. Using this grace period, tasks should perform orderly shut down\n// immediately upon receiving SIGTERM.\n//\t\tseconds:\tthe number of seconds\nfunc (r *Application) SetTaskKillGracePeriod(seconds float64) *Application {\n\tr.TaskKillGracePeriodSeconds = &seconds\n\n\treturn r\n}\n\n// AddArgs adds one or more arguments to the applications\n//\t\targuments:\tthe argument(s) you are adding\nfunc (r *Application) AddArgs(arguments ...string) *Application {\n\tif r.Args == nil {\n\t\tr.EmptyArgs()\n\t}\n\n\targs := *r.Args\n\targs = append(args, arguments...)\n\tr.Args = &args\n\n\treturn r\n}\n\n// EmptyArgs explicitly empties arguments -- use this if you need to empty\n// arguments of an application that already has arguments set (setting args to nil will\n// keep the current value)\nfunc (r *Application) EmptyArgs() *Application {\n\tr.Args = &[]string{}\n\n\treturn r\n}\n\n// AddConstraint adds a new constraint\n//\t\tconstraints:\tthe constraint definition, one constraint per array element\nfunc (r *Application) AddConstraint(constraints ...string) *Application {\n\tif r.Constraints == nil {\n\t\tr.EmptyConstraints()\n\t}\n\n\tc := *r.Constraints\n\tc = append(c, constraints)\n\tr.Constraints = &c\n\n\treturn r\n}\n\n// EmptyConstraints explicitly empties constraints -- use this if you need to empty\n// constraints of an application that already has constraints set (setting constraints to nil will\n// keep the current value)\nfunc (r *Application) EmptyConstraints() *Application {\n\tr.Constraints = &[][]string{}\n\n\treturn r\n}\n\n// AddLabel adds a label to the application\n//\t\tname:\tthe name of the label\n//\t\tvalue: value for this label\nfunc (r *Application) AddLabel(name, value string) *Application {\n\tif r.Labels == nil {\n\t\tr.EmptyLabels()\n\t}\n\t(*r.Labels)[name] = value\n\n\treturn r\n}\n\n// EmptyLabels explicitly empties the labels -- use this if you need to empty\n// the labels of an application that already has labels set (setting labels to nil will\n// keep the current value)\nfunc (r *Application) EmptyLabels() *Application {\n\tr.Labels = &map[string]string{}\n\n\treturn r\n}\n\n// AddEnv adds an environment variable to the application\n// name:\tthe name of the variable\n// value:\tgo figure, the value associated to the above\nfunc (r *Application) AddEnv(name, value string) *Application {\n\tif r.Env == nil {\n\t\tr.EmptyEnvs()\n\t}\n\t(*r.Env)[name] = value\n\n\treturn r\n}\n\n// EmptyEnvs explicitly empties the envs -- use this if you need to empty\n// the environments of an application that already has environments set (setting env to nil will\n// keep the current value)\nfunc (r *Application) EmptyEnvs() *Application {\n\tr.Env = &map[string]string{}\n\n\treturn r\n}\n\n// AddSecret adds a secret declaration\n// envVar: the name of the environment variable\n// name:\tthe name of the secret\n// source:\tthe source ID of the secret\nfunc (r *Application) AddSecret(envVar, name, source string) *Application {\n\tif r.Secrets == nil {\n\t\tr.EmptySecrets()\n\t}\n\t(*r.Secrets)[name] = Secret{EnvVar: envVar, Source: source}\n\n\treturn r\n}\n\n// EmptySecrets explicitly empties the secrets -- use this if you need to empty\n// the secrets of an application that already has secrets set (setting secrets to nil will\n// keep the current value)\nfunc (r *Application) EmptySecrets() *Application {\n\tr.Secrets = &map[string]Secret{}\n\n\treturn r\n}\n\n// SetExecutor sets the executor\nfunc (r *Application) SetExecutor(executor string) *Application {\n\tr.Executor = &executor\n\n\treturn r\n}\n\n// AddHealthCheck adds a health check\n// \thealthCheck the health check that should be added\nfunc (r *Application) AddHealthCheck(healthCheck HealthCheck) *Application {\n\tif r.HealthChecks == nil {\n\t\tr.EmptyHealthChecks()\n\t}\n\n\thealthChecks := *r.HealthChecks\n\thealthChecks = append(healthChecks, healthCheck)\n\tr.HealthChecks = &healthChecks\n\n\treturn r\n}\n\n// EmptyHealthChecks explicitly empties health checks -- use this if you need to empty\n// health checks of an application that already has health checks set (setting health checks to nil will\n// keep the current value)\nfunc (r *Application) EmptyHealthChecks() *Application {\n\tr.HealthChecks = &[]HealthCheck{}\n\n\treturn r\n}\n\n// HasHealthChecks is a helper method, used to check if an application has health checks\nfunc (r *Application) HasHealthChecks() bool {\n\treturn r.HealthChecks != nil && len(*r.HealthChecks) > 0\n}\n\n// AddReadinessCheck adds a readiness check.\nfunc (r *Application) AddReadinessCheck(readinessCheck ReadinessCheck) *Application {\n\tif r.ReadinessChecks == nil {\n\t\tr.EmptyReadinessChecks()\n\t}\n\n\treadinessChecks := *r.ReadinessChecks\n\treadinessChecks = append(readinessChecks, readinessCheck)\n\tr.ReadinessChecks = &readinessChecks\n\n\treturn r\n}\n\n// EmptyReadinessChecks empties the readiness checks.\nfunc (r *Application) EmptyReadinessChecks() *Application {\n\tr.ReadinessChecks = &[]ReadinessCheck{}\n\n\treturn r\n}\n\n// DeploymentIDs retrieves the application deployments IDs\nfunc (r *Application) DeploymentIDs() []*DeploymentID {\n\tvar deployments []*DeploymentID\n\n\tif r.Deployments == nil {\n\t\treturn deployments\n\t}\n\n\t// step: extract the deployment id from the result\n\tfor _, deploy := range r.Deployments {\n\t\tif id, found := deploy[\"id\"]; found {\n\t\t\tdeployment := &DeploymentID{\n\t\t\t\tVersion:      r.Version,\n\t\t\t\tDeploymentID: id,\n\t\t\t}\n\t\t\tdeployments = append(deployments, deployment)\n\t\t}\n\t}\n\n\treturn deployments\n}\n\n// CheckHTTP adds a HTTP check to an application\n//\t\tport: \t\tthe port the check should be checking\n// \t\tinterval:\tthe interval in seconds the check should be performed\nfunc (r *Application) CheckHTTP(path string, port, interval int) (*Application, error) {\n\tif r.Container == nil || r.Container.Docker == nil {\n\t\treturn nil, ErrNoApplicationContainer\n\t}\n\t// step: get the port index\n\tportIndex, err := r.Container.Docker.ServicePortIndex(port)\n\tif err != nil {\n\t\tportIndex, err = r.Container.ServicePortIndex(port)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\thealth := NewDefaultHealthCheck()\n\thealth.IntervalSeconds = interval\n\t*health.Path = path\n\t*health.PortIndex = portIndex\n\t// step: add to the checks\n\tr.AddHealthCheck(*health)\n\n\treturn r, nil\n}\n\n// CheckTCP adds a TCP check to an application; note the port mapping must already exist, or an\n// error will thrown\n//\t\tport: \t\tthe port the check should, err, check\n// \t\tinterval:\tthe interval in seconds the check should be performed\nfunc (r *Application) CheckTCP(port, interval int) (*Application, error) {\n\tif r.Container == nil || r.Container.Docker == nil {\n\t\treturn nil, ErrNoApplicationContainer\n\t}\n\t// step: get the port index\n\tportIndex, err := r.Container.Docker.ServicePortIndex(port)\n\tif err != nil {\n\t\tportIndex, err = r.Container.ServicePortIndex(port)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\thealth := NewDefaultHealthCheck()\n\thealth.Protocol = \"TCP\"\n\thealth.IntervalSeconds = interval\n\t*health.PortIndex = portIndex\n\t// step: add to the checks\n\tr.AddHealthCheck(*health)\n\n\treturn r, nil\n}\n\n// AddUris adds one or more uris to the applications\n//\t\targuments:\tthe uri(s) you are adding\nfunc (r *Application) AddUris(newUris ...string) *Application {\n\tif r.Uris == nil {\n\t\tr.EmptyUris()\n\t}\n\n\turis := *r.Uris\n\turis = append(uris, newUris...)\n\tr.Uris = &uris\n\n\treturn r\n}\n\n// EmptyUris explicitly empties uris -- use this if you need to empty\n// uris of an application that already has uris set (setting uris to nil will\n// keep the current value)\nfunc (r *Application) EmptyUris() *Application {\n\tr.Uris = &[]string{}\n\n\treturn r\n}\n\n// AddFetchURIs adds one or more fetch URIs to the application.\n//\t\tfetchURIs:\tthe fetch URI(s) to add.\nfunc (r *Application) AddFetchURIs(fetchURIs ...Fetch) *Application {\n\tif r.Fetch == nil {\n\t\tr.EmptyFetchURIs()\n\t}\n\n\tfetch := *r.Fetch\n\tfetch = append(fetch, fetchURIs...)\n\tr.Fetch = &fetch\n\n\treturn r\n}\n\n// EmptyFetchURIs explicitly empties fetch URIs -- use this if you need to empty\n// fetch URIs of an application that already has fetch URIs set.\n// Setting fetch URIs to nil will keep the current value.\nfunc (r *Application) EmptyFetchURIs() *Application {\n\tr.Fetch = &[]Fetch{}\n\n\treturn r\n}\n\n// SetUpgradeStrategy sets the upgrade strategy.\nfunc (r *Application) SetUpgradeStrategy(us UpgradeStrategy) *Application {\n\tr.UpgradeStrategy = &us\n\treturn r\n}\n\n// EmptyUpgradeStrategy explicitly empties the upgrade strategy -- use this if\n// you need to empty the upgrade strategy of an application that already has\n// the upgrade strategy set (setting it to nil will keep the current value).\nfunc (r *Application) EmptyUpgradeStrategy() *Application {\n\tr.UpgradeStrategy = &UpgradeStrategy{}\n\treturn r\n}\n\n// SetUnreachableStrategy sets the unreachable strategy.\nfunc (r *Application) SetUnreachableStrategy(us UnreachableStrategy) *Application {\n\tr.UnreachableStrategy = &us\n\treturn r\n}\n\n// EmptyUnreachableStrategy explicitly empties the unreachable strategy -- use this if\n// you need to empty the unreachable strategy of an application that already has\n// the unreachable strategy set (setting it to nil will keep the current value).\nfunc (r *Application) EmptyUnreachableStrategy() *Application {\n\tr.UnreachableStrategy = &UnreachableStrategy{}\n\treturn r\n}\n\n// SetResidency sets behavior for resident applications, an application is resident when\n// it has local persistent volumes set\nfunc (r *Application) SetResidency(whenLost TaskLostBehaviorType) *Application {\n\tr.Residency = &Residency{\n\t\tTaskLostBehavior: whenLost,\n\t}\n\treturn r\n}\n\n// EmptyResidency explicitly empties the residency -- use this if\n// you need to empty the residency of an application that already has\n// the residency set (setting it to nil will keep the current value).\nfunc (r *Application) EmptyResidency() *Application {\n\tr.Residency = &Residency{}\n\treturn r\n}\n\n// String returns the json representation of this application\nfunc (r *Application) String() string {\n\ts, err := json.MarshalIndent(r, \"\", \"  \")\n\tif err != nil {\n\t\treturn fmt.Sprintf(`{\"error\": \"error decoding type into json: %s\"}`, err)\n\t}\n\n\treturn string(s)\n}\n\n// Applications retrieves an array of all the applications which are running in marathon\nfunc (r *marathonClient) Applications(v url.Values) (*Applications, error) {\n\tquery := v.Encode()\n\tif query != \"\" {\n\t\tquery = \"?\" + query\n\t}\n\n\tapplications := new(Applications)\n\terr := r.apiGet(marathonAPIApps+query, nil, applications)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn applications, nil\n}\n\n// ListApplications retrieves an array of the application names currently running in marathon\nfunc (r *marathonClient) ListApplications(v url.Values) ([]string, error) {\n\tapplications, err := r.Applications(v)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar list []string\n\tfor _, application := range applications.Apps {\n\t\tlist = append(list, application.ID)\n\t}\n\n\treturn list, nil\n}\n\n// HasApplicationVersion checks to see if the application version exists in Marathon\n// \t\tname: \t\tthe id used to identify the application\n//\t\tversion: \tthe version (normally a timestamp) your looking for\nfunc (r *marathonClient) HasApplicationVersion(name, version string) (bool, error) {\n\tid := trimRootPath(name)\n\tversions, err := r.ApplicationVersions(id)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\treturn contains(versions.Versions, version), nil\n}\n\n// ApplicationVersions is a list of versions which has been deployed with marathon for a specific application\n//\t\tname:\t\tthe id used to identify the application\nfunc (r *marathonClient) ApplicationVersions(name string) (*ApplicationVersions, error) {\n\tpath := fmt.Sprintf(\"%s/versions\", buildPath(name))\n\tversions := new(ApplicationVersions)\n\tif err := r.apiGet(path, nil, versions); err != nil {\n\t\treturn nil, err\n\t}\n\treturn versions, nil\n}\n\n// SetApplicationVersion changes the version of the application\n// \t\tname: \t\tthe id used to identify the application\n//\t\tversion: \tthe version (normally a timestamp) you wish to change to\nfunc (r *marathonClient) SetApplicationVersion(name string, version *ApplicationVersion) (*DeploymentID, error) {\n\tpath := buildPath(name)\n\tdeploymentID := new(DeploymentID)\n\tif err := r.apiPut(path, version, deploymentID); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn deploymentID, nil\n}\n\n// Application retrieves the application configuration from marathon\n// \t\tname: \t\tthe id used to identify the application\nfunc (r *marathonClient) Application(name string) (*Application, error) {\n\tvar wrapper struct {\n\t\tApplication *Application `json:\"app\"`\n\t}\n\n\tif err := r.apiGet(buildPath(name), nil, &wrapper); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn wrapper.Application, nil\n}\n\n// ApplicationBy retrieves the application configuration from marathon\n// \t\tname: \t\tthe id used to identify the application\n//\t\topts:\t\tGetAppOpts request payload\nfunc (r *marathonClient) ApplicationBy(name string, opts *GetAppOpts) (*Application, error) {\n\tpath, err := addOptions(buildPath(name), opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar wrapper struct {\n\t\tApplication *Application `json:\"app\"`\n\t}\n\n\tif err := r.apiGet(path, nil, &wrapper); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn wrapper.Application, nil\n}\n\n// ApplicationByVersion retrieves the application configuration from marathon\n// \t\tname: \t\tthe id used to identify the application\n// \t\tversion:  the version of the configuration you would like to receive\nfunc (r *marathonClient) ApplicationByVersion(name, version string) (*Application, error) {\n\tapp := new(Application)\n\n\tpath := fmt.Sprintf(\"%s/versions/%s\", buildPath(name), version)\n\tif err := r.apiGet(path, nil, app); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn app, nil\n}\n\n// ApplicationOK validates that the application, or more appropriately it's tasks have passed all the health checks.\n// If no health checks exist, we simply return true\n// \t\tname: \t\tthe id used to identify the application\nfunc (r *marathonClient) ApplicationOK(name string) (bool, error) {\n\t// step: get the application\n\tapplication, err := r.Application(name)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\t// step: check if all the tasks are running?\n\tif !application.AllTaskRunning() {\n\t\treturn false, nil\n\t}\n\n\t// step: if the application has not health checks, just return true\n\tif application.HealthChecks == nil || len(*application.HealthChecks) == 0 {\n\t\treturn true, nil\n\t}\n\n\t// step: iterate the application checks and look for false\n\tfor _, task := range application.Tasks {\n\t\t// Health check results may not be available immediately. Assume\n\t\t// non-healthiness if they are missing for any task.\n\t\tif task.HealthCheckResults == nil {\n\t\t\treturn false, nil\n\t\t}\n\n\t\tfor _, check := range task.HealthCheckResults {\n\t\t\t//When a task is flapping in Marathon, this is sometimes nil\n\t\t\tif check == nil || !check.Alive {\n\t\t\t\treturn false, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn true, nil\n}\n\n// ApplicationDeployments retrieves an array of Deployment IDs for an application\n//       name:       the id used to identify the application\nfunc (r *marathonClient) ApplicationDeployments(name string) ([]*DeploymentID, error) {\n\tapplication, err := r.Application(name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn application.DeploymentIDs(), nil\n}\n\n// CreateApplication creates a new application in Marathon\n// \t\tapplication:\t\tthe structure holding the application configuration\nfunc (r *marathonClient) CreateApplication(application *Application) (*Application, error) {\n\tresult := new(Application)\n\tif err := r.ApiPost(marathonAPIApps, application, result); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// WaitOnApplication waits for an application to be deployed\n//\t\tname:\t\tthe id of the application\n//\t\ttimeout:\ta duration of time to wait for an application to deploy\nfunc (r *marathonClient) WaitOnApplication(name string, timeout time.Duration) error {\n\treturn r.wait(name, timeout, r.appExistAndRunning)\n}\n\nfunc (r *marathonClient) appExistAndRunning(name string) bool {\n\tapp, err := r.Application(name)\n\tif apiErr, ok := err.(*APIError); ok && apiErr.ErrCode == ErrCodeNotFound {\n\t\treturn false\n\t}\n\tif err == nil && app.AllTaskRunning() {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// DeleteApplication deletes an application from marathon\n// \t\tname: \t\tthe id used to identify the application\n//\t\tforce:\t\tused to force the delete operation in case of blocked deployment\nfunc (r *marathonClient) DeleteApplication(name string, force bool) (*DeploymentID, error) {\n\tpath := buildPathWithForceParam(name, force)\n\t// step: check of the application already exists\n\tdeployID := new(DeploymentID)\n\tif err := r.apiDelete(path, nil, deployID); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn deployID, nil\n}\n\n// RestartApplication performs a rolling restart of marathon application\n// \t\tname: \t\tthe id used to identify the application\nfunc (r *marathonClient) RestartApplication(name string, force bool) (*DeploymentID, error) {\n\tdeployment := new(DeploymentID)\n\tvar options struct{}\n\tpath := buildPathWithForceParam(fmt.Sprintf(\"%s/restart\", name), force)\n\tif err := r.ApiPost(path, &options, deployment); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn deployment, nil\n}\n\n// ScaleApplicationInstances changes the number of instance an application is running\n// \t\tname: \t\tthe id used to identify the application\n// \t\tinstances:\tthe number of instances you wish to change to\n//    force: used to force the scale operation in case of blocked deployment\nfunc (r *marathonClient) ScaleApplicationInstances(name string, instances int, force bool) (*DeploymentID, error) {\n\tchanges := new(Application)\n\tchanges.ID = validateID(name)\n\tchanges.Instances = &instances\n\tpath := buildPathWithForceParam(name, force)\n\tdeployID := new(DeploymentID)\n\tif err := r.apiPut(path, changes, deployID); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn deployID, nil\n}\n\n// UpdateApplication updates an application in Marathon\n// \t\tapplication:\t\tthe structure holding the application configuration\nfunc (r *marathonClient) UpdateApplication(application *Application, force bool) (*DeploymentID, error) {\n\tresult := new(DeploymentID)\n\tpath := buildPathWithForceParam(application.ID, force)\n\tif err := r.apiPut(path, application, result); err != nil {\n\t\treturn nil, err\n\t}\n\treturn result, nil\n}\n\nfunc buildPathWithForceParam(rootPath string, force bool) string {\n\tpath := buildPath(rootPath)\n\tif force {\n\t\tpath += \"?force=true\"\n\t}\n\treturn path\n}\n\nfunc buildPath(path string) string {\n\treturn fmt.Sprintf(\"%s/%s\", marathonAPIApps, trimRootPath(path))\n}\n\n// EmptyLabels explicitly empties labels -- use this if you need to empty\n// labels of an application that already has IP per task with labels defined\nfunc (i *IPAddressPerTask) EmptyLabels() *IPAddressPerTask {\n\ti.Labels = &map[string]string{}\n\treturn i\n}\n\n// AddLabel adds a label to an IPAddressPerTask\n//    name: The label name\n//   value: The label value\nfunc (i *IPAddressPerTask) AddLabel(name, value string) *IPAddressPerTask {\n\tif i.Labels == nil {\n\t\ti.EmptyLabels()\n\t}\n\t(*i.Labels)[name] = value\n\treturn i\n}\n\n// EmptyGroups explicitly empties groups -- use this if you need to empty\n// groups of an application that already has IP per task with groups defined\nfunc (i *IPAddressPerTask) EmptyGroups() *IPAddressPerTask {\n\ti.Groups = &[]string{}\n\treturn i\n}\n\n// AddGroup adds a group to an IPAddressPerTask\n//  group: The group name\nfunc (i *IPAddressPerTask) AddGroup(group string) *IPAddressPerTask {\n\tif i.Groups == nil {\n\t\ti.EmptyGroups()\n\t}\n\n\tgroups := *i.Groups\n\tgroups = append(groups, group)\n\ti.Groups = &groups\n\n\treturn i\n}\n\n// SetDiscovery define the discovery to an IPAddressPerTask\n//  discovery: The discovery struct\nfunc (i *IPAddressPerTask) SetDiscovery(discovery Discovery) *IPAddressPerTask {\n\ti.Discovery = &discovery\n\treturn i\n}\n\n// EmptyPorts explicitly empties discovey port -- use this if you need to empty\n// discovey port of an application that already has IP per task with discovey ports\n// defined\nfunc (d *Discovery) EmptyPorts() *Discovery {\n\td.Ports = &[]Port{}\n\treturn d\n}\n\n// AddPort adds a port to the discovery info of a IP per task applicable\n//   port: The discovery port\nfunc (d *Discovery) AddPort(port Port) *Discovery {\n\tif d.Ports == nil {\n\t\td.EmptyPorts()\n\t}\n\tports := *d.Ports\n\tports = append(ports, port)\n\td.Ports = &ports\n\treturn d\n}\n\n// EmptyNetworks explicitly empties networks\nfunc (r *Application) EmptyNetworks() *Application {\n\tr.Networks = &[]PodNetwork{}\n\treturn r\n}\n\n// SetNetwork sets the networking mode\nfunc (r *Application) SetNetwork(name string, mode PodNetworkMode) *Application {\n\tif r.Networks == nil {\n\t\tr.EmptyNetworks()\n\t}\n\n\tnetwork := PodNetwork{Name: name, Mode: mode}\n\tnetworks := *r.Networks\n\tnetworks = append(networks, network)\n\tr.Networks = &networks\n\treturn r\n}\n"
  },
  {
    "path": "application_marshalling.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n)\n\n// Alias aliases the Application struct so that it will be marshaled/unmarshaled automatically\ntype Alias Application\n\n// TmpEnvSecret holds the secret values deserialized from the environment variables field\ntype TmpEnvSecret struct {\n\tSecret string `json:\"secret,omitempty\"`\n}\n\n// TmpSecret holds the deserialized secrets field in a Marathon application configuration\ntype TmpSecret struct {\n\tSource string `json:\"source,omitempty\"`\n}\n\n// UnmarshalJSON unmarshals the given Application JSON as expected except for environment variables and secrets.\n// Environment varialbes are stored in the Env field. Secrets, including the environment variable part,\n// are stored in the Secrets field.\nfunc (app *Application) UnmarshalJSON(b []byte) error {\n\taux := &struct {\n\t\t*Alias\n\t\tEnv     map[string]interface{} `json:\"env\"`\n\t\tSecrets map[string]TmpSecret   `json:\"secrets\"`\n\t}{\n\t\tAlias: (*Alias)(app),\n\t}\n\tif err := json.Unmarshal(b, aux); err != nil {\n\t\treturn fmt.Errorf(\"malformed application definition %v\", err)\n\t}\n\tenv := &map[string]string{}\n\tsecrets := &map[string]Secret{}\n\n\tfor envName, genericEnvValue := range aux.Env {\n\t\tswitch envValOrSecret := genericEnvValue.(type) {\n\t\tcase string:\n\t\t\t(*env)[envName] = envValOrSecret\n\t\tcase map[string]interface{}:\n\t\t\tfor secret, secretStore := range envValOrSecret {\n\t\t\t\tif secStore, ok := secretStore.(string); ok && secret == \"secret\" {\n\t\t\t\t\t(*secrets)[secStore] = Secret{EnvVar: envName}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\treturn fmt.Errorf(\"unexpected secret field %v of value type %T\", secret, envValOrSecret[secret])\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"unexpected environment variable type %T\", envValOrSecret)\n\t\t}\n\t}\n\tapp.Env = env\n\tfor k, v := range aux.Secrets {\n\t\ttmp := (*secrets)[k]\n\t\ttmp.Source = v.Source\n\t\t(*secrets)[k] = tmp\n\t}\n\tapp.Secrets = secrets\n\treturn nil\n}\n\n// MarshalJSON marshals the given Application as expected except for environment variables and secrets,\n// which are marshaled from specialized structs.  The environment variable piece of the secrets and other\n// normal environment variables are combined and marshaled to the env field.  The secrets and the related\n// source are marshaled into the secrets field.\nfunc (app *Application) MarshalJSON() ([]byte, error) {\n\tenv := make(map[string]interface{})\n\tsecrets := make(map[string]TmpSecret)\n\n\tif app.Env != nil {\n\t\tfor k, v := range *app.Env {\n\t\t\tenv[string(k)] = string(v)\n\t\t}\n\t}\n\tif app.Secrets != nil {\n\t\tfor k, v := range *app.Secrets {\n\t\t\tenv[v.EnvVar] = TmpEnvSecret{Secret: k}\n\t\t\tsecrets[k] = TmpSecret{v.Source}\n\t\t}\n\t}\n\taux := &struct {\n\t\t*Alias\n\t\tEnv     map[string]interface{} `json:\"env,omitempty\"`\n\t\tSecrets map[string]TmpSecret   `json:\"secrets,omitempty\"`\n\t}{Alias: (*Alias)(app), Env: env, Secrets: secrets}\n\n\treturn json.Marshal(aux)\n}\n"
  },
  {
    "path": "application_marshalling_test.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestEnvironmentVariableUnmarshal(t *testing.T) {\n\tdefaultConfig := NewDefaultConfig()\n\tconfigs := &configContainer{\n\t\tclient: &defaultConfig,\n\t\tserver: &serverConfig{\n\t\t\tscope: \"environment-variables\",\n\t\t},\n\t}\n\n\tendpoint := newFakeMarathonEndpoint(t, configs)\n\tdefer endpoint.Close()\n\n\tapplication, err := endpoint.Client.Application(fakeAppName)\n\trequire.NoError(t, err)\n\n\tenv := application.Env\n\tsecrets := application.Secrets\n\n\trequire.NotNil(t, env)\n\tassert.Equal(t, \"bar\", (*env)[\"FOO\"])\n\tassert.Equal(t, \"TOP\", (*secrets)[\"secret\"].EnvVar)\n\tassert.Equal(t, \"/path/to/secret\", (*secrets)[\"secret\"].Source)\n}\n\nfunc TestMalformedPayloadUnmarshal(t *testing.T) {\n\tvar tests = []struct {\n\t\texpected    string\n\t\tgiven       []byte\n\t\tdescription string\n\t}{\n\t\t{\n\t\t\texpected:    \"unexpected secret field\",\n\t\t\tgiven:       []byte(`{\"env\": {\"FOO\": \"bar\", \"SECRET\": {\"not_secret\": \"secret1\"}}, \"secrets\": {\"secret1\": {\"source\": \"/path/to/secret\"}}}`),\n\t\t\tdescription: \"Field in environment secret not equal to secret.\",\n\t\t},\n\t\t{\n\t\t\texpected:    \"unexpected secret field\",\n\t\t\tgiven:       []byte(`{\"env\": {\"FOO\": \"bar\", \"SECRET\": {\"secret\": 1}}, \"secrets\": {\"secret1\": {\"source\": \"/path/to/secret\"}}}`),\n\t\t\tdescription: \"Invalid value in environment secret.\",\n\t\t},\n\t\t{\n\t\t\texpected:    \"unexpected environment variable type\",\n\t\t\tgiven:       []byte(`{\"env\": {\"FOO\": 1, \"SECRET\": {\"secret\": \"secret1\"}}, \"secrets\": {\"secret1\": {\"source\": \"/path/to/secret\"}}}`),\n\t\t\tdescription: \"Invalid environment variable type.\",\n\t\t},\n\t\t{\n\t\t\texpected:    \"malformed application definition\",\n\t\t\tgiven:       []byte(`{\"env\": \"value\"}`),\n\t\t\tdescription: \"Bad application definition.\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\ttmpApp := new(Application)\n\n\t\terr := json.Unmarshal(test.given, &tmpApp)\n\t\tif assert.Error(t, err, test.description) {\n\t\t\tassert.True(t, strings.HasPrefix(err.Error(), test.expected), test.description)\n\t\t}\n\t}\n}\n\nfunc TestEnvironmentVariableMarshal(t *testing.T) {\n\ttestApp := new(Application)\n\ttargetString := []byte(`{\"ports\":null,\"dependencies\":null,\"env\":{\"FOO\":\"bar\",\"TOP\":{\"secret\":\"secret1\"}},\"secrets\":{\"secret1\":{\"source\":\"/path/to/secret\"}}}`)\n\ttestApp.AddEnv(\"FOO\", \"bar\")\n\ttestApp.AddSecret(\"TOP\", \"secret1\", \"/path/to/secret\")\n\n\tapp, err := json.Marshal(testApp)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, targetString, app)\n\t}\n}\n"
  },
  {
    "path": "application_test.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestApplicationDependsOn(t *testing.T) {\n\tapp := NewDockerApplication()\n\tapp.DependsOn(\"fake-app\")\n\tapp.DependsOn(\"fake-app1\", \"fake-app2\")\n\tassert.Equal(t, 3, len(app.Dependencies))\n}\n\nfunc TestApplicationMemory(t *testing.T) {\n\tapp := NewDockerApplication()\n\tapp.Memory(50.0)\n\tassert.Equal(t, 50.0, *app.Mem)\n}\n\nfunc TestApplicationString(t *testing.T) {\n\ttype test struct {\n\t\tname                string\n\t\tapp                 *Application\n\t\texpectedAppJSONPath string\n\t\tsetup               func(*Application)\n\t}\n\n\ttests := []test{\n\t\t{\n\t\t\tname: \"marathon < 1.5\",\n\t\t\tapp: NewDockerApplication().\n\t\t\t\tName(\"my-app\").\n\t\t\t\tCPU(0.1).\n\t\t\t\tMemory(64).\n\t\t\t\tStorage(0.0).\n\t\t\t\tCount(2).\n\t\t\t\tAddArgs(\"/usr/sbin/apache2ctl\", \"-D\", \"FOREGROUND\").\n\t\t\t\tAddEnv(\"NAME\", \"frontend_http\").\n\t\t\t\tAddEnv(\"SERVICE_80_NAME\", \"test_http\"),\n\t\t\texpectedAppJSONPath: \"tests/app-definitions/TestApplicationString-output.json\",\n\t\t\tsetup: func(app *Application) {\n\t\t\t\tapp.\n\t\t\t\t\tContainer.Docker.Container(\"quay.io/gambol99/apache-php:latest\").\n\t\t\t\t\tBridged().\n\t\t\t\t\tExpose(80).\n\t\t\t\t\tExpose(443)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"marathon > 1.5\",\n\t\t\tapp: NewDockerApplication().\n\t\t\t\tName(\"my-app\").\n\t\t\t\tCPU(0.1).\n\t\t\t\tMemory(64).\n\t\t\t\tStorage(0.0).\n\t\t\t\tCount(2).\n\t\t\t\tSetNetwork(\"\", \"container/bridge\").\n\t\t\t\tAddArgs(\"/usr/sbin/apache2ctl\", \"-D\", \"FOREGROUND\").\n\t\t\t\tAddEnv(\"NAME\", \"frontend_http\").\n\t\t\t\tAddEnv(\"SERVICE_80_NAME\", \"test_http\"),\n\t\t\texpectedAppJSONPath: \"tests/app-definitions/TestApplicationString-1.5-output.json\",\n\t\t\tsetup: func(app *Application) {\n\t\t\t\tapp.\n\t\t\t\t\tContainer.Expose(80).Expose(443).\n\t\t\t\t\tDocker.Container(\"quay.io/gambol99/apache-php:latest\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tlabel := fmt.Sprintf(\"test: %s\", test.name)\n\n\t\ttest.setup(test.app)\n\t\t_, err := test.app.CheckHTTP(\"/health\", 80, 5)\n\t\tassert.Nil(t, err)\n\n\t\texpectedAppJSONBytes, err := ioutil.ReadFile(test.expectedAppJSONPath)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\texpectedAppJSON := strings.TrimSpace(string(expectedAppJSONBytes))\n\t\tassert.Equal(t, expectedAppJSON, test.app.String(), label)\n\t}\n}\nfunc TestApplicationCount(t *testing.T) {\n\tapp := NewDockerApplication()\n\tassert.Nil(t, app.Instances)\n\tapp.Count(1)\n\tassert.Equal(t, 1, *app.Instances)\n}\n\nfunc TestApplicationStorage(t *testing.T) {\n\tapp := NewDockerApplication()\n\tassert.Nil(t, app.Disk)\n\tapp.Storage(0.10)\n\tassert.Equal(t, 0.10, *app.Disk)\n}\n\nfunc TestApplicationAllTaskRunning(t *testing.T) {\n\tapp := NewDockerApplication()\n\n\tapp.Instances = nil\n\tapp.Tasks = nil\n\n\tassert.True(t, app.AllTaskRunning())\n\n\tvar cnt int\n\tapp.Instances = &cnt\n\n\tcnt = 0\n\tassert.True(t, app.AllTaskRunning())\n\n\tcnt = 1\n\tassert.False(t, app.AllTaskRunning())\n\n\tapp.Tasks = []*Task{}\n\tapp.TasksRunning = 1\n\tassert.True(t, app.AllTaskRunning())\n\n\tcnt = 2\n\tapp.TasksRunning = 1\n\tassert.False(t, app.AllTaskRunning())\n}\n\nfunc TestApplicationName(t *testing.T) {\n\tapp := NewDockerApplication()\n\tassert.Equal(t, \"\", app.ID)\n\tapp.Name(fakeAppName)\n\tassert.Equal(t, fakeAppName, app.ID)\n}\n\nfunc TestApplicationCommand(t *testing.T) {\n\tapp := NewDockerApplication()\n\tassert.Equal(t, \"\", app.ID)\n\tapp.Command(\"format C:\")\n\tassert.Equal(t, \"format C:\", *app.Cmd)\n}\n\nfunc TestApplicationCPU(t *testing.T) {\n\tapp := NewDockerApplication()\n\tassert.Equal(t, 0.0, app.CPUs)\n\tapp.CPU(0.1)\n\tassert.Equal(t, 0.1, app.CPUs)\n}\n\nfunc TestApplicationSetGPUs(t *testing.T) {\n\tapp := NewDockerApplication()\n\tassert.Nil(t, app.GPUs)\n\tapp.SetGPUs(0.1)\n\tassert.Equal(t, 0.1, *app.GPUs)\n}\n\nfunc TestApplicationEmptyGPUs(t *testing.T) {\n\tapp := NewDockerApplication()\n\tassert.Nil(t, app.GPUs)\n\tapp.EmptyGPUs()\n\tassert.Equal(t, 0.0, *app.GPUs)\n}\n\nfunc TestApplicationArgs(t *testing.T) {\n\tapp := NewDockerApplication()\n\tassert.Nil(t, app.Args)\n\tapp.AddArgs(\"-p\").AddArgs(\"option\", \"-v\")\n\tassert.Equal(t, 3, len(*app.Args))\n\tassert.Equal(t, \"-p\", (*app.Args)[0])\n\tassert.Equal(t, \"option\", (*app.Args)[1])\n\tassert.Equal(t, \"-v\", (*app.Args)[2])\n\n\tapp.EmptyArgs()\n\tassert.NotNil(t, app.Args)\n\tassert.Equal(t, 0, len(*app.Args))\n}\n\nfunc ExampleApplication_AddConstraint() {\n\tapp := NewDockerApplication()\n\n\t// add two constraints\n\tapp.AddConstraint(\"hostname\", \"UNIQUE\").\n\t\tAddConstraint(\"rack_id\", \"CLUSTER\", \"rack-1\")\n}\n\nfunc TestApplicationConstraints(t *testing.T) {\n\tapp := NewDockerApplication()\n\tassert.Nil(t, app.Constraints)\n\tapp.AddConstraint(\"hostname\", \"UNIQUE\").\n\t\tAddConstraint(\"rack_id\", \"CLUSTER\", \"rack-1\")\n\n\tassert.Equal(t, 2, len(*app.Constraints))\n\tassert.Equal(t, []string{\"hostname\", \"UNIQUE\"}, (*app.Constraints)[0])\n\tassert.Equal(t, []string{\"rack_id\", \"CLUSTER\", \"rack-1\"}, (*app.Constraints)[1])\n\n\tapp.EmptyConstraints()\n\tassert.NotNil(t, app.Constraints)\n\tassert.Equal(t, 0, len(*app.Constraints))\n}\n\nfunc TestApplicationLabels(t *testing.T) {\n\tapp := NewDockerApplication()\n\tassert.Nil(t, app.Labels)\n\n\tapp.AddLabel(\"hello\", \"world\").AddLabel(\"foo\", \"bar\")\n\tassert.Equal(t, 2, len(*app.Labels))\n\tassert.Equal(t, \"world\", (*app.Labels)[\"hello\"])\n\tassert.Equal(t, \"bar\", (*app.Labels)[\"foo\"])\n\n\tapp.EmptyLabels()\n\tassert.NotNil(t, app.Labels)\n\tassert.Equal(t, 0, len(*app.Labels))\n}\n\nfunc TestApplicationEnvs(t *testing.T) {\n\tapp := NewDockerApplication()\n\tassert.Nil(t, app.Env)\n\n\tapp.AddEnv(\"hello\", \"world\").AddEnv(\"foo\", \"bar\")\n\tif assert.Equal(t, 2, len((*app.Env))) {\n\t\tassert.Equal(t, \"world\", (*app.Env)[\"hello\"])\n\t\tassert.Equal(t, \"bar\", (*app.Env)[\"foo\"])\n\t}\n\tapp.EmptyEnvs()\n\tassert.NotNil(t, app.Env)\n\tassert.Equal(t, 0, len(*app.Env))\n}\n\nfunc TestApplicationSecrets(t *testing.T) {\n\tapp := NewDockerApplication()\n\tassert.Nil(t, app.Env)\n\n\tapp.AddSecret(\"MY_FIRST_SECRET\", \"secret0\", \"path/to/my/secret\")\n\tapp.AddSecret(\"MY_SECOND_SECRET\", \"secret1\", \"path/to/my/other/secret\")\n\tif assert.Equal(t, 2, len(*app.Secrets)) {\n\t\tassert.Equal(t, Secret{EnvVar: \"MY_FIRST_SECRET\", Source: \"path/to/my/secret\"}, (*app.Secrets)[\"secret0\"])\n\t\tassert.Equal(t, Secret{EnvVar: \"MY_SECOND_SECRET\", Source: \"path/to/my/other/secret\"}, (*app.Secrets)[\"secret1\"])\n\t}\n\tapp.EmptySecrets()\n\tassert.NotNil(t, app.Secrets)\n\tassert.Equal(t, 0, len(*app.Secrets))\n}\n\nfunc TestApplicationSetExecutor(t *testing.T) {\n\tapp := NewDockerApplication()\n\tassert.Nil(t, app.Executor)\n\n\tapp.SetExecutor(\"executor\")\n\tassert.Equal(t, \"executor\", *app.Executor)\n\n\tapp.SetExecutor(\"\")\n\tassert.Equal(t, \"\", *app.Executor)\n}\n\nfunc TestApplicationHealthChecks(t *testing.T) {\n\tapp := NewDockerApplication()\n\tassert.Nil(t, app.HealthChecks)\n\thc1 := NewDefaultHealthCheck()\n\thc2 := NewDefaultHealthCheck()\n\tapp.AddHealthCheck(*hc1).AddHealthCheck(*hc2)\n\n\tassert.Equal(t, 2, len(*app.HealthChecks))\n\tassert.Equal(t, *hc1, (*app.HealthChecks)[0])\n\tassert.Equal(t, *hc2, (*app.HealthChecks)[1])\n\n\tapp.EmptyHealthChecks()\n\tassert.NotNil(t, app.HealthChecks)\n\tassert.Equal(t, 0, len(*app.HealthChecks))\n}\n\nfunc TestApplicationReadinessChecks(t *testing.T) {\n\tapp := NewDockerApplication()\n\trequire.Nil(t, app.HealthChecks)\n\trc := ReadinessCheck{}\n\trc.SetName(\"/readiness\")\n\tapp.AddReadinessCheck(rc)\n\n\trequire.Equal(t, 1, len(*app.ReadinessChecks))\n\tassert.Equal(t, \"/readiness\", *((*app.ReadinessChecks)[0].Name))\n\n\tapp.EmptyReadinessChecks()\n\trequire.NotNil(t, app.ReadinessChecks)\n\tassert.Equal(t, 0, len(*app.ReadinessChecks))\n}\n\nfunc TestApplicationPortDefinitions(t *testing.T) {\n\tapp := NewDockerApplication()\n\tassert.Nil(t, app.PortDefinitions)\n\tpd1 := new(PortDefinition)\n\tpd1.SetProtocol(\"tcp\").SetName(\"es\").SetPort(9092).AddLabel(\"foo\", \"bar\")\n\tpd2 := new(PortDefinition)\n\tpd2.SetProtocol(\"udp,tcp\").SetName(\"syslog\").SetPort(514)\n\tapp.AddPortDefinition(*pd1).AddPortDefinition(*pd2)\n\n\tassert.Equal(t, 2, len(*app.PortDefinitions))\n\tassert.Equal(t, *pd1, (*app.PortDefinitions)[0])\n\tassert.Equal(t, 1, len(*(*app.PortDefinitions)[0].Labels))\n\tassert.Equal(t, *pd2, (*app.PortDefinitions)[1])\n\tassert.Nil(t, (*app.PortDefinitions)[1].Labels)\n\n\t(*app.PortDefinitions)[0].EmptyLabels()\n\tassert.NotNil(t, (*app.PortDefinitions)[0].Labels)\n\tassert.Equal(t, 0, len(*(*app.PortDefinitions)[0].Labels))\n\n\tapp.EmptyPortDefinitions()\n\tassert.NotNil(t, app.PortDefinitions)\n\tassert.Equal(t, 0, len(*app.PortDefinitions))\n}\n\nfunc TestHasHealthChecks(t *testing.T) {\n\tapps := []*Application{\n\t\tNewDockerApplication(),\n\t\tNewDockerApplication(),\n\t}\n\n\tfor i := range apps {\n\t\tassert.False(t, apps[i].HasHealthChecks())\n\t}\n\n\t// Marathon < 1.5\n\tapps[0].Container.Docker.Container(\"quay.io/gambol99/apache-php:latest\").Expose(80)\n\n\t// Marathon >= 1.5\n\tapps[1].Container.Expose(80).Docker.Container(\"quay.io/gambol99/apache-php:latest\")\n\n\tfor i := range apps {\n\t\t_, err := apps[i].CheckTCP(80, 10)\n\t\tassert.NoError(t, err)\n\t\tassert.True(t, apps[i].HasHealthChecks())\n\t}\n}\n\nfunc TestApplicationCheckTCP(t *testing.T) {\n\tapps := []*Application{\n\t\tNewDockerApplication(),\n\t\tNewDockerApplication(),\n\t}\n\n\tfor i := range apps {\n\t\tassert.False(t, apps[i].HasHealthChecks())\n\t\t_, err := apps[i].CheckTCP(80, 10)\n\t\tassert.Error(t, err)\n\t\tassert.False(t, apps[i].HasHealthChecks())\n\t}\n\n\t// Marathon < 1.5\n\tapps[0].Container.Docker.Container(\"quay.io/gambol99/apache-php:latest\").Expose(80)\n\n\t// Marathon >= 1.5\n\tapps[1].Container.Expose(80).Docker.Container(\"quay.io/gambol99/apache-php:latest\")\n\n\tfor i := range apps {\n\t\t_, err := apps[i].CheckTCP(80, 10)\n\t\tassert.NoError(t, err)\n\t\tassert.True(t, apps[i].HasHealthChecks())\n\t\tcheck := (*apps[i].HealthChecks)[0]\n\t\tassert.Equal(t, \"TCP\", check.Protocol)\n\t\tassert.Equal(t, 10, check.IntervalSeconds)\n\t\tassert.Equal(t, 0, *check.PortIndex)\n\t}\n}\n\nfunc TestApplicationCheckHTTP(t *testing.T) {\n\tapps := []*Application{\n\t\tNewDockerApplication(),\n\t\tNewDockerApplication(),\n\t}\n\n\tfor i := range apps {\n\t\tassert.False(t, apps[i].HasHealthChecks())\n\t\t_, err := apps[i].CheckHTTP(\"/\", 80, 10)\n\t\tassert.Error(t, err)\n\t\tassert.False(t, apps[i].HasHealthChecks())\n\t}\n\n\t// Marathon < 1.5\n\tapps[0].Container.Docker.Container(\"quay.io/gambol99/apache-php:latest\").Expose(80)\n\n\t// Marathon >= 1.5\n\tapps[1].Container.Expose(80).Docker.Container(\"quay.io/gambol99/apache-php:latest\")\n\n\tfor i := range apps {\n\t\t_, err := apps[i].CheckHTTP(\"/health\", 80, 10)\n\t\tassert.NoError(t, err)\n\t\tassert.True(t, apps[i].HasHealthChecks())\n\t\tcheck := (*apps[i].HealthChecks)[0]\n\t\tassert.Equal(t, \"HTTP\", check.Protocol)\n\t\tassert.Equal(t, 10, check.IntervalSeconds)\n\t\tassert.Equal(t, \"/health\", *check.Path)\n\t\tassert.Equal(t, 0, *check.PortIndex)\n\t}\n}\n\nfunc TestCreateApplication(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tapplication := NewDockerApplication()\n\tapplication.Name(fakeAppName)\n\tapp, err := endpoint.Client.CreateApplication(application)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, app)\n\tassert.Equal(t, application.ID, fakeAppName)\n\tassert.Equal(t, app.Deployments[0][\"id\"], \"f44fd4fc-4330-4600-a68b-99c7bd33014a\")\n}\n\nfunc TestUpdateApplication(t *testing.T) {\n\tfor _, force := range []bool{false, true} {\n\t\tendpoint := newFakeMarathonEndpoint(t, nil)\n\t\tdefer endpoint.Close()\n\n\t\tapplication := NewDockerApplication()\n\t\tapplication.Name(fakeAppName)\n\t\tid, err := endpoint.Client.UpdateApplication(application, force)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, id.DeploymentID, \"83b215a6-4e26-4e44-9333-5c385eda6438\")\n\t\tassert.Equal(t, id.Version, \"2014-08-26T07:37:50.462Z\")\n\t}\n}\n\nfunc TestApplications(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tapplications, err := endpoint.Client.Applications(nil)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, applications)\n\tassert.Equal(t, len(applications.Apps), 2)\n\tassert.Equal(t, (*applications.Apps[0].Secrets)[\"secret0\"].EnvVar, \"SECRET1\")\n\tassert.Equal(t, (*applications.Apps[0].Secrets)[\"secret0\"].Source, \"secret/definition/id\")\n\n\tv := url.Values{}\n\tv.Set(\"cmd\", \"nginx\")\n\tapplications, err = endpoint.Client.Applications(v)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, applications)\n\tassert.Equal(t, len(applications.Apps), 1)\n}\n\nfunc TestApplicationsEmbedTaskStats(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tv := url.Values{}\n\tv.Set(\"embed\", \"apps.taskStats\")\n\tapplications, err := endpoint.Client.Applications(v)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, applications)\n\tassert.Equal(t, len(applications.Apps), 1)\n\tassert.NotNil(t, applications.Apps[0].TaskStats)\n\tassert.Equal(t, applications.Apps[0].TaskStats[\"startedAfterLastScaling\"].Stats.Counts[\"healthy\"], 1)\n\tassert.Equal(t, applications.Apps[0].TaskStats[\"startedAfterLastScaling\"].Stats.LifeTime[\"averageSeconds\"], 17024.575)\n}\n\nfunc TestApplicationsEmbedReadiness(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tv := url.Values{}\n\tv.Set(\"embed\", \"apps.readiness\")\n\tapplications, err := endpoint.Client.Applications(v)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, applications)\n\trequire.Equal(t, len(applications.Apps), 1)\n\trequire.NotNil(t, applications.Apps[0].ReadinessCheckResults)\n\trequire.True(t, len(*applications.Apps[0].ReadinessCheckResults) > 0)\n\tactualRes := (*applications.Apps[0].ReadinessCheckResults)[0]\n\texpectedRes := ReadinessCheckResult{\n\t\tName:   \"myReadyCheck\",\n\t\tTaskID: \"test_frontend_app1.c9de6033\",\n\t\tReady:  false,\n\t\tLastResponse: ReadinessLastResponse{\n\t\t\tBody:        \"{}\",\n\t\t\tContentType: \"application/json\",\n\t\t\tStatus:      500,\n\t\t},\n\t}\n\tassert.Equal(t, expectedRes, actualRes)\n}\n\nfunc TestListApplications(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tapplications, err := endpoint.Client.ListApplications(nil)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, applications)\n\tassert.Equal(t, len(applications), 2)\n\tassert.Equal(t, applications[0], fakeAppName)\n\tassert.Equal(t, applications[1], fakeAppNameBroken)\n}\n\nfunc TestApplicationVersions(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tversions, err := endpoint.Client.ApplicationVersions(fakeAppName)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, versions)\n\tassert.NotNil(t, versions.Versions)\n\tassert.Equal(t, len(versions.Versions), 1)\n\tassert.Equal(t, versions.Versions[0], \"2014-04-04T06:25:31.399Z\")\n\t/* check we get an error on app not there */\n\t_, err = endpoint.Client.ApplicationVersions(\"/not/there\")\n\tassert.Error(t, err)\n}\n\nfunc TestRestartApplication(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tid, err := endpoint.Client.RestartApplication(fakeAppName, false)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, id)\n\tassert.Equal(t, \"83b215a6-4e26-4e44-9333-5c385eda6438\", id.DeploymentID)\n\tassert.Equal(t, \"2014-08-26T07:37:50.462Z\", id.Version)\n\tid, err = endpoint.Client.RestartApplication(\"/not/there\", false)\n\tassert.Error(t, err)\n\tassert.Nil(t, id)\n}\n\nfunc TestApplicationUris(t *testing.T) {\n\tapp := NewDockerApplication()\n\tassert.Nil(t, app.Uris)\n\tapp.AddUris(\"file://uri1.tar.gz\").AddUris(\"file://uri2.tar.gz\", \"file://uri3.tar.gz\")\n\tassert.Equal(t, 3, len(*app.Uris))\n\tassert.Equal(t, \"file://uri1.tar.gz\", (*app.Uris)[0])\n\tassert.Equal(t, \"file://uri2.tar.gz\", (*app.Uris)[1])\n\tassert.Equal(t, \"file://uri3.tar.gz\", (*app.Uris)[2])\n\n\tapp.EmptyUris()\n\tassert.NotNil(t, app.Uris)\n\tassert.Equal(t, 0, len(*app.Uris))\n}\n\nfunc TestApplicationFetchURIs(t *testing.T) {\n\tapp := NewDockerApplication()\n\tassert.Nil(t, app.Fetch)\n\tapp.AddFetchURIs(Fetch{URI: \"file://uri1.tar.gz\"}).\n\t\tAddFetchURIs(Fetch{URI: \"file://uri2.tar.gz\"}, Fetch{URI: \"file://uri3.tar.gz\"})\n\tassert.Equal(t, 3, len(*app.Fetch))\n\tassert.Equal(t, Fetch{URI: \"file://uri1.tar.gz\"}, (*app.Fetch)[0])\n\tassert.Equal(t, Fetch{URI: \"file://uri2.tar.gz\"}, (*app.Fetch)[1])\n\tassert.Equal(t, Fetch{URI: \"file://uri3.tar.gz\"}, (*app.Fetch)[2])\n\n\tapp.EmptyFetchURIs()\n\tassert.NotNil(t, app.Fetch)\n\tassert.Equal(t, 0, len(*app.Fetch))\n}\n\nfunc TestSetApplicationVersion(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tdeployment, err := endpoint.Client.SetApplicationVersion(fakeAppName, &ApplicationVersion{Version: \"2014-08-26T07:37:50.462Z\"})\n\tassert.NoError(t, err)\n\tassert.NotNil(t, deployment)\n\tassert.NotNil(t, deployment.Version)\n\tassert.NotNil(t, deployment.DeploymentID)\n\tassert.Equal(t, deployment.Version, \"2014-08-26T07:37:50.462Z\")\n\tassert.Equal(t, deployment.DeploymentID, \"83b215a6-4e26-4e44-9333-5c385eda6438\")\n\t_, err = endpoint.Client.SetApplicationVersion(\"/not/there\", &ApplicationVersion{Version: \"2014-04-04T06:25:31.399Z\"})\n\tassert.Error(t, err)\n}\n\nfunc TestHasApplicationVersion(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tfound, err := endpoint.Client.HasApplicationVersion(fakeAppName, \"2014-04-04T06:25:31.399Z\")\n\tassert.NoError(t, err)\n\tassert.True(t, found)\n\tfound, err = endpoint.Client.HasApplicationVersion(fakeAppName, \"###2015-04-04T06:25:31.399Z\")\n\tassert.NoError(t, err)\n\tassert.False(t, found)\n}\n\nfunc TestDeleteApplication(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tfor _, force := range []bool{false, true} {\n\t\tid, err := endpoint.Client.DeleteApplication(fakeAppName, force)\n\t\tassert.NoError(t, err)\n\t\tassert.NotNil(t, id)\n\t\tassert.Equal(t, \"83b215a6-4e26-4e44-9333-5c385eda6438\", id.DeploymentID)\n\t\tassert.Equal(t, \"2014-08-26T07:37:50.462Z\", id.Version)\n\t\t_, err = endpoint.Client.DeleteApplication(\"no_such_app\", force)\n\t\tassert.Error(t, err)\n\t}\n}\n\nfunc TestApplicationOK(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tok, err := endpoint.Client.ApplicationOK(fakeAppName)\n\tassert.NoError(t, err)\n\tassert.True(t, ok)\n\tok, err = endpoint.Client.ApplicationOK(fakeAppNameBroken)\n\tassert.NoError(t, err)\n\tassert.False(t, ok)\n\tok, err = endpoint.Client.ApplicationOK(fakeAppNameUnhealthy)\n\tassert.NoError(t, err)\n\tassert.False(t, ok)\n}\n\nfunc verifyApplication(application *Application, t *testing.T) {\n\tassert.NotNil(t, application)\n\tassert.Equal(t, application.ID, fakeAppName)\n\tassert.NotNil(t, application.HealthChecks)\n\tassert.NotNil(t, application.Tasks)\n\tassert.Equal(t, len(*application.HealthChecks), 1)\n\tassert.Equal(t, len(application.Tasks), 2)\n\tassert.Equal(t, application.Residency, &Residency{\n\t\tTaskLostBehavior:                 TaskLostBehaviorTypeRelaunchAfterTimeout,\n\t\tRelaunchEscalationTimeoutSeconds: 60,\n\t})\n}\n\nfunc TestApplication(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tapplication, err := endpoint.Client.Application(fakeAppName)\n\tassert.NoError(t, err)\n\tverifyApplication(application, t)\n\n\t_, err = endpoint.Client.Application(\"no_such_app\")\n\tassert.Error(t, err)\n\tapiErr, ok := err.(*APIError)\n\tassert.True(t, ok)\n\tassert.Equal(t, ErrCodeNotFound, apiErr.ErrCode)\n\n\tconfig := NewDefaultConfig()\n\tconfig.URL = \"http://non-existing-marathon-host.local:5555\"\n\t// Reduce timeout to speed up test execution time.\n\tconfig.HTTPClient = &http.Client{\n\t\tTimeout: 100 * time.Millisecond,\n\t}\n\tendpoint = newFakeMarathonEndpoint(t, &configContainer{\n\t\tclient: &config,\n\t})\n\tdefer endpoint.Close()\n\n\t_, err = endpoint.Client.Application(fakeAppName)\n\tassert.Error(t, err)\n\t_, ok = err.(*APIError)\n\tassert.False(t, ok)\n}\n\nfunc TestApplicationConfiguration(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tapplication, err := endpoint.Client.ApplicationByVersion(fakeAppName, \"2014-09-12T23:28:21.737Z\")\n\tassert.NoError(t, err)\n\tverifyApplication(application, t)\n\n\t_, err = endpoint.Client.ApplicationByVersion(fakeAppName, \"no_such_version\")\n\tassert.Error(t, err)\n\tapiErr, ok := err.(*APIError)\n\tassert.True(t, ok)\n\tassert.Equal(t, ErrCodeNotFound, apiErr.ErrCode)\n\n\t_, err = endpoint.Client.ApplicationByVersion(\"no_such_app\", \"latest\")\n\tassert.Error(t, err)\n\tapiErr, ok = err.(*APIError)\n\tassert.True(t, ok)\n\tassert.Equal(t, ErrCodeNotFound, apiErr.ErrCode)\n}\n\nfunc TestWaitOnApplication(t *testing.T) {\n\twaitTime := 100 * time.Millisecond\n\n\ttests := []struct {\n\t\tdesc          string\n\t\ttimeout       time.Duration\n\t\tappName       string\n\t\ttestScope     string\n\t\tshouldSucceed bool\n\t}{\n\t\t{\n\t\t\tdesc:          \"initially existing app\",\n\t\t\ttimeout:       0,\n\t\t\tappName:       fakeAppName,\n\t\t\tshouldSucceed: true,\n\t\t},\n\n\t\t{\n\t\t\tdesc:          \"delayed existing app | timeout > ticker\",\n\t\t\ttimeout:       200 * time.Millisecond,\n\t\t\tappName:       fakeAppName,\n\t\t\ttestScope:     \"wait-on-app\",\n\t\t\tshouldSucceed: true,\n\t\t},\n\n\t\t{\n\t\t\tdesc:          \"delayed existing app | timeout < ticker\",\n\t\t\ttimeout:       50 * time.Millisecond,\n\t\t\tappName:       fakeAppName,\n\t\t\ttestScope:     \"wait-on-app\",\n\t\t\tshouldSucceed: false,\n\t\t},\n\t\t{\n\t\t\tdesc:          \"missing app | timeout > ticker\",\n\t\t\ttimeout:       200 * time.Millisecond,\n\t\t\tappName:       \"no_such_app\",\n\t\t\tshouldSucceed: false,\n\t\t},\n\t\t{\n\t\t\tdesc:          \"missing app | timeout < ticker\",\n\t\t\ttimeout:       50 * time.Millisecond,\n\t\t\tappName:       \"no_such_app\",\n\t\t\tshouldSucceed: false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tdefaultConfig := NewDefaultConfig()\n\t\tdefaultConfig.PollingWaitTime = waitTime\n\t\tconfigs := &configContainer{\n\t\t\tclient: &defaultConfig,\n\t\t\tserver: &serverConfig{\n\t\t\t\tscope: test.testScope,\n\t\t\t},\n\t\t}\n\n\t\tendpoint := newFakeMarathonEndpoint(t, configs)\n\t\tdefer endpoint.Close()\n\n\t\terrCh := make(chan error)\n\t\tgo func() {\n\t\t\terrCh <- endpoint.Client.WaitOnApplication(test.appName, test.timeout)\n\t\t}()\n\n\t\tselect {\n\t\tcase <-time.After(400 * time.Millisecond):\n\t\t\tassert.Fail(t, fmt.Sprintf(\"%s: WaitOnApplication did not complete in time\", test.desc))\n\t\tcase err := <-errCh:\n\t\t\tif test.shouldSucceed {\n\t\t\t\tassert.NoError(t, err, test.desc)\n\t\t\t} else {\n\t\t\t\tassert.IsType(t, err, ErrTimeoutError, test.desc)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestAppExistAndRunning(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\tclient := endpoint.Client.(*marathonClient)\n\tassert.True(t, client.appExistAndRunning(fakeAppName))\n\tassert.False(t, client.appExistAndRunning(\"no_such_app\"))\n}\n\nfunc TestSetIPPerTask(t *testing.T) {\n\tapp := Application{}\n\tapp.Ports = append(app.Ports, 10)\n\tapp.AddPortDefinition(PortDefinition{})\n\tassert.Nil(t, app.IPAddressPerTask)\n\tassert.Equal(t, 1, len(app.Ports))\n\tassert.Equal(t, 1, len(*app.PortDefinitions))\n\n\tapp.SetIPAddressPerTask(IPAddressPerTask{})\n\tassert.NotNil(t, app.IPAddressPerTask)\n\tassert.Equal(t, 0, len(app.Ports))\n\tassert.Equal(t, 0, len(*app.PortDefinitions))\n}\n\nfunc TestIPAddressPerTask(t *testing.T) {\n\tipPerTask := IPAddressPerTask{}\n\tassert.Nil(t, ipPerTask.Groups)\n\tassert.Nil(t, ipPerTask.Labels)\n\tassert.Nil(t, ipPerTask.Discovery)\n\n\tipPerTask.\n\t\tAddGroup(\"label\").\n\t\tAddLabel(\"key\", \"value\").\n\t\tSetDiscovery(Discovery{\n\t\t\tPorts: &[]Port{},\n\t\t})\n\n\tassert.Equal(t, 1, len(*ipPerTask.Groups))\n\tassert.Equal(t, \"label\", (*ipPerTask.Groups)[0])\n\tassert.Equal(t, \"value\", (*ipPerTask.Labels)[\"key\"])\n\tassert.NotEmpty(t, *ipPerTask.Discovery)\n\n\tipPerTask.EmptyGroups()\n\tassert.Equal(t, 0, len(*ipPerTask.Groups))\n\n\tipPerTask.EmptyLabels()\n\tassert.Equal(t, 0, len(*ipPerTask.Labels))\n}\n\nfunc TestIPAddressPerTaskDiscovery(t *testing.T) {\n\tdisc := Discovery{}\n\tassert.Nil(t, disc.Ports)\n\n\tdisc.AddPort(Port{})\n\tassert.NotNil(t, disc.Ports)\n\tassert.Equal(t, 1, len(*disc.Ports))\n\n\tdisc.EmptyPorts()\n\tassert.NotNil(t, disc.Ports)\n\tassert.Equal(t, 0, len(*disc.Ports))\n\n}\n\nfunc TestUpgradeStrategy(t *testing.T) {\n\tapp := Application{}\n\tassert.Nil(t, app.UpgradeStrategy)\n\tus := new(UpgradeStrategy)\n\tus.SetMinimumHealthCapacity(1.0).SetMaximumOverCapacity(0.0)\n\tapp.SetUpgradeStrategy(*us)\n\ttestUs := app.UpgradeStrategy\n\tassert.Equal(t, 1.0, *testUs.MinimumHealthCapacity)\n\tassert.Equal(t, 0.0, *testUs.MaximumOverCapacity)\n\n\tapp.EmptyUpgradeStrategy()\n\tus = app.UpgradeStrategy\n\tassert.NotNil(t, us)\n\tassert.Nil(t, us.MinimumHealthCapacity)\n\tassert.Nil(t, us.MaximumOverCapacity)\n}\n\nfunc TestBridgedNetworking(t *testing.T) {\n\tapp := NewDockerApplication().SetNetwork(\"test\", \"container/bridge\")\n\tnetworks := *app.Networks\n\n\tassert.Equal(t, networks[0].Mode, BridgeNetworkMode)\n}\n\nfunc TestContainerNetworking(t *testing.T) {\n\tapp := NewDockerApplication().SetNetwork(\"test\", \"container\")\n\tnetworks := *app.Networks\n\n\tassert.Equal(t, networks[0].Mode, ContainerNetworkMode)\n}\n\nfunc TestHostNetworking(t *testing.T) {\n\tapp := NewDockerApplication().SetNetwork(\"test\", \"host\")\n\tnetworks := *app.Networks\n\n\tassert.Equal(t, networks[0].Mode, HostNetworkMode)\n}\n"
  },
  {
    "path": "client.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n)\n\n// Marathon is the interface to the marathon API\ntype Marathon interface {\n\t// -- GENERIC API ACCESS ---\n\n\tApiPost(path string, post, result interface{}) error\n\n\t// -- APPLICATIONS ---\n\n\t// get a listing of the application ids\n\tListApplications(url.Values) ([]string, error)\n\t// a list of application versions\n\tApplicationVersions(name string) (*ApplicationVersions, error)\n\t// check a application version exists\n\tHasApplicationVersion(name, version string) (bool, error)\n\t// change an application to a different version\n\tSetApplicationVersion(name string, version *ApplicationVersion) (*DeploymentID, error)\n\t// check if an application is ok\n\tApplicationOK(name string) (bool, error)\n\t// create an application in marathon\n\tCreateApplication(application *Application) (*Application, error)\n\t// delete an application\n\tDeleteApplication(name string, force bool) (*DeploymentID, error)\n\t// update an application in marathon\n\tUpdateApplication(application *Application, force bool) (*DeploymentID, error)\n\t// a list of deployments on a application\n\tApplicationDeployments(name string) ([]*DeploymentID, error)\n\t// scale a application\n\tScaleApplicationInstances(name string, instances int, force bool) (*DeploymentID, error)\n\t// restart an application\n\tRestartApplication(name string, force bool) (*DeploymentID, error)\n\t// get a list of applications from marathon\n\tApplications(url.Values) (*Applications, error)\n\t// get an application by name\n\tApplication(name string) (*Application, error)\n\t// get an application by options\n\tApplicationBy(name string, opts *GetAppOpts) (*Application, error)\n\t// get an application by name and version\n\tApplicationByVersion(name, version string) (*Application, error)\n\t// wait of application\n\tWaitOnApplication(name string, timeout time.Duration) error\n\n\t// -- PODS ---\n\t// whether this version of Marathon supports pods\n\tSupportsPods() (bool, error)\n\n\t// get pod status\n\tPodStatus(name string) (*PodStatus, error)\n\t// get all pod statuses\n\tPodStatuses() ([]*PodStatus, error)\n\n\t// get pod\n\tPod(name string) (*Pod, error)\n\t// get all pods\n\tPods() ([]Pod, error)\n\t// create pod\n\tCreatePod(pod *Pod) (*Pod, error)\n\t// update pod\n\tUpdatePod(pod *Pod, force bool) (*Pod, error)\n\t// delete pod\n\tDeletePod(name string, force bool) (*DeploymentID, error)\n\t// wait on pod to be deployed\n\tWaitOnPod(name string, timeout time.Duration) error\n\t// check if a pod is running\n\tPodIsRunning(name string) bool\n\n\t// get versions of a pod\n\tPodVersions(name string) ([]string, error)\n\t// get pod by version\n\tPodByVersion(name, version string) (*Pod, error)\n\n\t// delete instances of a pod\n\tDeletePodInstances(name string, instances []string) ([]*PodInstance, error)\n\t// delete pod instance\n\tDeletePodInstance(name, instance string) (*PodInstance, error)\n\n\t// -- TASKS ---\n\n\t// get a list of tasks for a specific application\n\tTasks(application string) (*Tasks, error)\n\t// get a list of all tasks\n\tAllTasks(opts *AllTasksOpts) (*Tasks, error)\n\t// get the endpoints for a service on a application\n\tTaskEndpoints(name string, port int, healthCheck bool) ([]string, error)\n\t// kill all the tasks for any application\n\tKillApplicationTasks(applicationID string, opts *KillApplicationTasksOpts) (*Tasks, error)\n\t// kill a single task\n\tKillTask(taskID string, opts *KillTaskOpts) (*Task, error)\n\t// kill the given array of tasks\n\tKillTasks(taskIDs []string, opts *KillTaskOpts) error\n\n\t// --- GROUPS ---\n\n\t// list all the groups in the system\n\tGroups() (*Groups, error)\n\t// retrieve a specific group from marathon\n\tGroup(name string) (*Group, error)\n\t// list all groups in marathon by options\n\tGroupsBy(opts *GetGroupOpts) (*Groups, error)\n\t// retrieve a specific group from marathon by options\n\tGroupBy(name string, opts *GetGroupOpts) (*Group, error)\n\t// create a group deployment\n\tCreateGroup(group *Group) error\n\t// delete a group\n\tDeleteGroup(name string, force bool) (*DeploymentID, error)\n\t// update a groups\n\tUpdateGroup(id string, group *Group, force bool) (*DeploymentID, error)\n\t// check if a group exists\n\tHasGroup(name string) (bool, error)\n\t// wait for an group to be deployed\n\tWaitOnGroup(name string, timeout time.Duration) error\n\n\t// --- DEPLOYMENTS ---\n\n\t// get a list of the deployments\n\tDeployments() ([]*Deployment, error)\n\t// delete a deployment\n\tDeleteDeployment(id string, force bool) (*DeploymentID, error)\n\t// check to see if a deployment exists\n\tHasDeployment(id string) (bool, error)\n\t// wait of a deployment to finish\n\tWaitOnDeployment(id string, timeout time.Duration) error\n\n\t// --- SUBSCRIPTIONS ---\n\n\t// a list of current subscriptions\n\tSubscriptions() (*Subscriptions, error)\n\t// add a events listener\n\tAddEventsListener(filter int) (EventsChannel, error)\n\t// remove a events listener\n\tRemoveEventsListener(channel EventsChannel)\n\t// Subscribe a callback URL\n\tSubscribe(string) error\n\t// Unsubscribe a callback URL\n\tUnsubscribe(string) error\n\n\t// --- QUEUE ---\n\t// get marathon launch queue\n\tQueue() (*Queue, error)\n\t// resets task launch delay of the specific application\n\tDeleteQueueDelay(appID string) error\n\n\t// --- MISC ---\n\n\t// get the marathon url\n\tGetMarathonURL() string\n\t// ping the marathon\n\tPing() (bool, error)\n\t// grab the marathon server info\n\tInfo() (*Info, error)\n\t// retrieve the leader info\n\tLeader() (string, error)\n\t// cause the current leader to abdicate\n\tAbdicateLeader() (string, error)\n}\n\nvar (\n\t// ErrMarathonDown is thrown when all the marathon endpoints are down\n\tErrMarathonDown = errors.New(\"all the Marathon hosts are presently down\")\n\t// ErrTimeoutError is thrown when the operation has timed out\n\tErrTimeoutError = errors.New(\"the operation has timed out\")\n\n\t// Default HTTP client used for SSE subscription requests\n\t// It is invalid to set client.Timeout because it includes time to read response so\n\t// set dial, tls handshake and response header timeouts instead\n\tdefaultHTTPSSEClient = &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tDial: (&net.Dialer{\n\t\t\t\tTimeout: 5 * time.Second,\n\t\t\t}).Dial,\n\t\t\tResponseHeaderTimeout: 10 * time.Second,\n\t\t\tTLSHandshakeTimeout:   5 * time.Second,\n\t\t},\n\t}\n\n\t// Default HTTP client used for non SSE requests\n\tdefaultHTTPClient = &http.Client{\n\t\tTimeout: 10 * time.Second,\n\t}\n)\n\n// EventsChannelContext holds contextual data for an EventsChannel.\ntype EventsChannelContext struct {\n\tfilter     int\n\tdone       chan struct{}\n\tcompletion *sync.WaitGroup\n}\n\ntype marathonClient struct {\n\tsync.RWMutex\n\t// the configuration for the client\n\tconfig Config\n\t// the flag used to prevent multiple SSE subscriptions\n\tsubscribedToSSE bool\n\t// the ip address of the client\n\tipAddress string\n\t// the http server\n\teventsHTTP *http.Server\n\t// the marathon hosts\n\thosts *cluster\n\t// a map of service you wish to listen to\n\tlisteners map[EventsChannel]EventsChannelContext\n\t// a custom log function for debug messages\n\tdebugLog func(format string, v ...interface{})\n\t// the marathon HTTP client to ensure consistency in requests\n\tclient *httpClient\n}\n\ntype httpClient struct {\n\t// the configuration for the marathon HTTP client\n\tconfig Config\n}\n\n// newRequestError signals that creating a new http.Request failed\ntype newRequestError struct {\n\terror\n}\n\n// NewClient creates a new marathon client\n//\t\tconfig:\t\t\tthe configuration to use\nfunc NewClient(config Config) (Marathon, error) {\n\t// step: if the SSE HTTP client is missing, prefer a configured regular\n\t// client, and otherwise use the default SSE HTTP client.\n\tif config.HTTPSSEClient == nil {\n\t\tconfig.HTTPSSEClient = defaultHTTPSSEClient\n\t\tif config.HTTPClient != nil {\n\t\t\tconfig.HTTPSSEClient = config.HTTPClient\n\t\t}\n\t}\n\n\t// step: if a regular HTTP client is missing, use the default one.\n\tif config.HTTPClient == nil {\n\t\tconfig.HTTPClient = defaultHTTPClient\n\t}\n\n\t// step: if no polling wait time is set, default to 500 milliseconds.\n\tif config.PollingWaitTime == 0 {\n\t\tconfig.PollingWaitTime = defaultPollingWaitTime\n\t}\n\n\t// step: setup shared client\n\tclient := &httpClient{config: config}\n\n\t// step: create a new cluster\n\thosts, err := newCluster(client, config.URL, config.DCOSToken != \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdebugLog := func(string, ...interface{}) {}\n\tif config.LogOutput != nil {\n\t\tlogger := log.New(config.LogOutput, \"\", 0)\n\t\tdebugLog = func(format string, v ...interface{}) {\n\t\t\tlogger.Printf(format, v...)\n\t\t}\n\t}\n\n\treturn &marathonClient{\n\t\tconfig:    config,\n\t\tlisteners: make(map[EventsChannel]EventsChannelContext),\n\t\thosts:     hosts,\n\t\tdebugLog:  debugLog,\n\t\tclient:    client,\n\t}, nil\n}\n\n// GetMarathonURL retrieves the marathon url\nfunc (r *marathonClient) GetMarathonURL() string {\n\treturn r.config.URL\n}\n\n// Ping pings the current marathon endpoint (note, this is not a ICMP ping, but a rest api call)\nfunc (r *marathonClient) Ping() (bool, error) {\n\tif err := r.apiGet(marathonAPIPing, nil, nil); err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\nfunc (r *marathonClient) apiHead(path string, result interface{}) error {\n\treturn r.apiCall(\"HEAD\", path, nil, result)\n}\n\nfunc (r *marathonClient) apiGet(path string, post, result interface{}) error {\n\treturn r.apiCall(\"GET\", path, post, result)\n}\n\nfunc (r *marathonClient) apiPut(path string, post, result interface{}) error {\n\treturn r.apiCall(\"PUT\", path, post, result)\n}\n\nfunc (r *marathonClient) ApiPost(path string, post, result interface{}) error {\n\treturn r.apiCall(\"POST\", path, post, result)\n}\n\nfunc (r *marathonClient) apiDelete(path string, post, result interface{}) error {\n\treturn r.apiCall(\"DELETE\", path, post, result)\n}\n\nfunc (r *marathonClient) apiCall(method, path string, body, result interface{}) error {\n\tconst deploymentHeader = \"Marathon-Deployment-Id\"\n\n\tfor {\n\t\t// step: marshall the request to json\n\t\tvar requestBody []byte\n\t\tvar err error\n\t\tif body != nil {\n\t\t\tif requestBody, err = json.Marshal(body); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// step: create the API request\n\t\trequest, member, err := r.buildAPIRequest(method, path, bytes.NewReader(requestBody))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// step: perform the API request\n\t\tresponse, err := r.client.Do(request)\n\t\tif err != nil {\n\t\t\tr.hosts.markDown(member)\n\t\t\t// step: attempt the request on another member\n\t\t\tr.debugLog(\"apiCall(): request failed on host: %s, error: %s, trying another\", member, err)\n\t\t\tcontinue\n\t\t}\n\t\tdefer response.Body.Close()\n\n\t\t// step: read the response body\n\t\trespBody, err := ioutil.ReadAll(response.Body)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif len(requestBody) > 0 {\n\t\t\tr.debugLog(\"apiCall(): %v %v %s returned %v %s\", request.Method, request.URL.String(), requestBody, response.Status, oneLogLine(respBody))\n\t\t} else {\n\t\t\tr.debugLog(\"apiCall(): %v %v returned %v %s\", request.Method, request.URL.String(), response.Status, oneLogLine(respBody))\n\t\t}\n\n\t\t// step: check for a successful response\n\t\tif response.StatusCode >= 200 && response.StatusCode <= 299 {\n\t\t\tif result != nil {\n\t\t\t\t// If we have a deployment ID header and no response body, give them that\n\t\t\t\t// This specifically handles the use case of a DELETE on an app/pod\n\t\t\t\t// We need a way to retrieve the deployment ID\n\t\t\t\tdeploymentID := response.Header.Get(deploymentHeader)\n\t\t\t\tif len(respBody) == 0 && deploymentID != \"\" {\n\t\t\t\t\td := DeploymentID{\n\t\t\t\t\t\tDeploymentID: deploymentID,\n\t\t\t\t\t}\n\t\t\t\t\tif deployID, ok := result.(*DeploymentID); ok {\n\t\t\t\t\t\t*deployID = d\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif err := json.Unmarshal(respBody, result); err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to unmarshal response from Marathon: %s\", err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\n\t\t// step: if the member node returns a >= 500 && <= 599 we should try another node?\n\t\tif response.StatusCode >= 500 && response.StatusCode <= 599 {\n\t\t\t// step: mark the host as down\n\t\t\tr.hosts.markDown(member)\n\t\t\tr.debugLog(\"apiCall(): request failed, host: %s, status: %d, trying another\", member, response.StatusCode)\n\t\t\tcontinue\n\t\t}\n\n\t\treturn NewAPIError(response.StatusCode, respBody)\n\t}\n}\n\n// wait waits until the provided function returns true (or times out)\nfunc (r *marathonClient) wait(name string, timeout time.Duration, fn func(string) bool) error {\n\ttimer := time.NewTimer(timeout)\n\tdefer timer.Stop()\n\n\tticker := time.NewTicker(r.config.PollingWaitTime)\n\tdefer ticker.Stop()\n\tfor {\n\t\tif fn(name) {\n\t\t\treturn nil\n\t\t}\n\n\t\tselect {\n\t\tcase <-timer.C:\n\t\t\treturn ErrTimeoutError\n\t\tcase <-ticker.C:\n\t\t\tcontinue\n\t\t}\n\t}\n}\n\n// buildAPIRequest creates a default API request.\n// It fails when there is no available member in the cluster anymore or when the request can not be built.\nfunc (r *marathonClient) buildAPIRequest(method, path string, reader io.Reader) (request *http.Request, member string, err error) {\n\t// Grab a member from the cluster\n\tmember, err = r.hosts.getMember()\n\tif err != nil {\n\t\treturn nil, \"\", ErrMarathonDown\n\t}\n\n\t// Build the HTTP request to Marathon\n\trequest, err = r.client.buildMarathonJSONRequest(method, member, path, reader)\n\tif err != nil {\n\t\treturn nil, member, newRequestError{err}\n\t}\n\treturn request, member, nil\n}\n\n// buildMarathonJSONRequest is like buildMarathonRequest but sets the\n// Content-Type and Accept headers to application/json.\nfunc (rc *httpClient) buildMarathonJSONRequest(method, member, path string, reader io.Reader) (request *http.Request, err error) {\n\treq, err := rc.buildMarathonRequest(method, member, path, reader)\n\tif err == nil {\n\t\treq.Header.Add(\"Content-Type\", \"application/json\")\n\t\treq.Header.Add(\"Accept\", \"application/json\")\n\t}\n\n\treturn req, err\n}\n\n// buildMarathonRequest creates a new HTTP request and configures it according to the *httpClient configuration.\n// The path must not contain a leading \"/\", otherwise buildMarathonRequest will panic.\nfunc (rc *httpClient) buildMarathonRequest(method, member, path string, reader io.Reader) (request *http.Request, err error) {\n\tif strings.HasPrefix(path, \"/\") {\n\t\tpanic(fmt.Sprintf(\"Path '%s' must not start with a leading slash\", path))\n\t}\n\n\t// Create the endpoint URL\n\turl := fmt.Sprintf(\"%s/%s\", member, path)\n\n\t// Instantiate an HTTP request\n\trequest, err = http.NewRequest(method, url, reader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Add any basic auth and the content headers\n\tif rc.config.HTTPBasicAuthUser != \"\" && rc.config.HTTPBasicPassword != \"\" {\n\t\trequest.SetBasicAuth(rc.config.HTTPBasicAuthUser, rc.config.HTTPBasicPassword)\n\t}\n\n\tif rc.config.DCOSToken != \"\" {\n\t\trequest.Header.Add(\"Authorization\", \"token=\"+rc.config.DCOSToken)\n\t}\n\n\treturn request, nil\n}\n\nfunc (rc *httpClient) Do(request *http.Request) (response *http.Response, err error) {\n\treturn rc.config.HTTPClient.Do(request)\n}\n\nvar oneLogLineRegex = regexp.MustCompile(`(?m)^\\s*`)\n\n// oneLogLine removes indentation at the beginning of each line and\n// escapes new line characters.\nfunc oneLogLine(in []byte) []byte {\n\treturn bytes.Replace(oneLogLineRegex.ReplaceAll(in, nil), []byte(\"\\n\"), []byte(\"\\\\n \"), -1)\n}\n"
  },
  {
    "path": "client_test.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"net/http\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewClient(t *testing.T) {\n\tconfig := Config{\n\t\tURL: \"http://marathon\",\n\t}\n\tcl, err := NewClient(config)\n\n\tif !assert.Nil(t, err) {\n\t\treturn\n\t}\n\n\tconf := cl.(*marathonClient).config\n\n\tassert.Equal(t, conf.HTTPClient, defaultHTTPClient)\n\tassert.Equal(t, conf.HTTPSSEClient, defaultHTTPSSEClient)\n\tassert.Zero(t, conf.HTTPSSEClient.Timeout)\n\tassert.Equal(t, conf.PollingWaitTime, defaultPollingWaitTime)\n}\n\nfunc TestHTTPClientDefaults(t *testing.T) {\n\tcustomHTTPRegularClient := http.DefaultClient\n\n\ttests := []struct {\n\t\tname                  string\n\t\thttpRegularClient     *http.Client\n\t\thttpSSEClient         *http.Client\n\t\twantHTTPRegularClient *http.Client\n\t\twantHTTPSSEClient     *http.Client\n\t}{\n\t\t{\n\t\t\tname:                  \"regular HTTP client missing\",\n\t\t\thttpRegularClient:     nil,\n\t\t\twantHTTPRegularClient: defaultHTTPClient,\n\t\t},\n\t\t{\n\t\t\tname:              \"SSE and regular HTTP clients missing\",\n\t\t\thttpSSEClient:     nil,\n\t\t\twantHTTPSSEClient: defaultHTTPSSEClient,\n\t\t},\n\t\t{\n\t\t\tname:              \"SSE HTTP client missing, regular HTTP client available\",\n\t\t\thttpSSEClient:     nil,\n\t\t\thttpRegularClient: customHTTPRegularClient,\n\t\t\twantHTTPSSEClient: customHTTPRegularClient,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tconfig := NewDefaultConfig()\n\t\tconfig.HTTPClient = test.httpRegularClient\n\t\tconfig.HTTPSSEClient = test.httpSSEClient\n\n\t\tclient, err := NewClient(config)\n\t\tif !assert.NoError(t, err, test.name) {\n\t\t\tcontinue\n\t\t}\n\n\t\tmaraClient := client.(*marathonClient)\n\t\tif test.wantHTTPRegularClient != nil {\n\t\t\tif !assert.Equal(t, test.wantHTTPRegularClient, maraClient.config.HTTPClient, test.name) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif test.wantHTTPSSEClient != nil {\n\t\t\tif !assert.Equal(t, test.wantHTTPSSEClient, maraClient.config.HTTPSSEClient, test.name) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestLogOutput(t *testing.T) {\n\tbuf := bytes.NewBuffer(nil)\n\tconfig := Config{\n\t\tURL:       \"http://marathon\",\n\t\tLogOutput: buf,\n\t}\n\n\tcl, err := NewClient(config)\n\trequire.Nil(t, err)\n\n\tcl.(*marathonClient).debugLog(\"this is a %s\", \"test\")\n\n\tassert.Equal(t, \"this is a test\\n\", buf.String())\n}\n\nfunc TestInvalidConfig(t *testing.T) {\n\tconfig := Config{\n\t\tURL: \"\",\n\t}\n\t_, err := NewClient(config)\n\tassert.Error(t, err)\n}\n\nfunc TestPing(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tpong, err := endpoint.Client.Ping()\n\tassert.NoError(t, err)\n\tassert.True(t, pong)\n}\n\nfunc TestGetMarathonURL(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tassert.Equal(t, endpoint.Client.GetMarathonURL(), endpoint.URL)\n}\n\nfunc TestAPIRequest(t *testing.T) {\n\tcases := []struct {\n\t\tUsername       string\n\t\tPassword       string\n\t\tServerUsername string\n\t\tServerPassword string\n\t\tOk             bool\n\t}{\n\t\t{\n\t\t\tUsername:       \"should_pass\",\n\t\t\tPassword:       \"\",\n\t\t\tServerUsername: \"\",\n\t\t\tServerPassword: \"\",\n\t\t\tOk:             true,\n\t\t},\n\t\t{\n\t\t\tUsername:       \"bad_username\",\n\t\t\tPassword:       \"\",\n\t\t\tServerUsername: \"test\",\n\t\t\tServerPassword: \"password\",\n\t\t\tOk:             false,\n\t\t},\n\t\t{\n\t\t\tUsername:       \"test\",\n\t\t\tPassword:       \"bad_password\",\n\t\t\tServerUsername: \"test\",\n\t\t\tServerPassword: \"password\",\n\t\t\tOk:             false,\n\t\t},\n\t\t{\n\t\t\tUsername:       \"\",\n\t\t\tPassword:       \"\",\n\t\t\tServerUsername: \"test\",\n\t\t\tServerPassword: \"password\",\n\t\t\tOk:             false,\n\t\t},\n\t\t{\n\t\t\tUsername:       \"test\",\n\t\t\tPassword:       \"password\",\n\t\t\tServerUsername: \"test\",\n\t\t\tServerPassword: \"password\",\n\t\t\tOk:             true,\n\t\t},\n\t}\n\tfor i, x := range cases {\n\t\tvar endpoint *endpoint\n\n\t\tconfig := NewDefaultConfig()\n\t\tconfig.HTTPBasicAuthUser = x.Username\n\t\tconfig.HTTPBasicPassword = x.Password\n\n\t\tendpoint = newFakeMarathonEndpoint(t, &configContainer{\n\t\t\tclient: &config,\n\t\t\tserver: &serverConfig{\n\t\t\t\tusername: x.ServerUsername,\n\t\t\t\tpassword: x.ServerPassword,\n\t\t\t},\n\t\t})\n\n\t\t_, err := endpoint.Client.Applications(nil)\n\n\t\tif x.Ok && err != nil {\n\t\t\tt.Errorf(\"case %d, did not expect an error: %s\", i, err)\n\t\t}\n\t\tif !x.Ok && err == nil {\n\t\t\tt.Errorf(\"case %d, expected to received an error\", i)\n\t\t}\n\n\t\tendpoint.Close()\n\t}\n}\n\nfunc TestBuildApiRequestFailure(t *testing.T) {\n\ttests := []struct {\n\t\tname              string\n\t\texpectedError     error\n\t\texpectedErrorType interface{}\n\t\tpath              string\n\t\tclusterDown       bool\n\t}{\n\t\t{\n\t\t\tname:          \"cluster down\",\n\t\t\texpectedError: ErrMarathonDown,\n\t\t\tclusterDown:   true,\n\t\t},\n\t\t{\n\t\t\tname:              \"invalid request parameter\",\n\t\t\texpectedErrorType: newRequestError{},\n\t\t\tpath:              \"%zzzzz\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tif test.expectedError == nil && test.expectedErrorType == nil {\n\t\t\tpanic(\"Testcase requires at least one of 'expectedError' or 'expectedErrorType'\")\n\t\t}\n\n\t\tclientCfg := NewDefaultConfig()\n\t\tconfig := configContainer{client: &clientCfg}\n\t\tendpoint := newFakeMarathonEndpoint(t, &config)\n\n\t\tclient := endpoint.Client.(*marathonClient)\n\n\t\tif test.clusterDown {\n\t\t\tfor _, member := range client.hosts.members {\n\t\t\t\tmember.status = memberStatusDown\n\t\t\t}\n\t\t}\n\n\t\t_, _, err := client.buildAPIRequest(\"GET\", test.path, nil)\n\n\t\tif test.expectedError != nil {\n\t\t\tassert.Equal(t, test.expectedError, err)\n\t\t}\n\t\tif test.expectedErrorType != nil {\n\t\t\tassert.IsType(t, test.expectedErrorType, err)\n\t\t}\n\n\t\tendpoint.Close()\n\t}\n}\n\nfunc TestOneLogLine(t *testing.T) {\n\tin := `\n\ta\n\tb    c\n\td\\n\n\t  efgh\n\ti\\r\\n\n\tj\\t\n\t{\"json\":  \"works\",\n\t\t\"f o o\": \"ba    r\"\n\t}\n\t`\n\tassert.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))))\n}\n\nfunc TestAPIRequestDCOS(t *testing.T) {\n\tcases := []struct {\n\t\tDCOSToken       string\n\t\tServerDCOSToken string\n\t\tServerUsername  string\n\t\tServerPassword  string\n\t\tOk              bool\n\t}{\n\t\t{\n\t\t\tDCOSToken:       \"should_pass\",\n\t\t\tServerDCOSToken: \"should_pass\",\n\t\t\tServerUsername:  \"\",\n\t\t\tServerPassword:  \"\",\n\t\t\tOk:              true,\n\t\t},\n\t\t{\n\t\t\tDCOSToken:       \"should_pass\",\n\t\t\tServerDCOSToken: \"\",\n\t\t\tServerUsername:  \"\",\n\t\t\tServerPassword:  \"\",\n\t\t\tOk:              true,\n\t\t},\n\t\t{\n\t\t\tDCOSToken:       \"should_not_pass\",\n\t\t\tServerDCOSToken: \"different_token\",\n\t\t\tServerUsername:  \"\",\n\t\t\tServerPassword:  \"\",\n\t\t\tOk:              false,\n\t\t},\n\t}\n\tfor i, x := range cases {\n\t\tvar endpoint *endpoint\n\n\t\tconfig := NewDefaultConfig()\n\t\tconfig.DCOSToken = x.DCOSToken\n\n\t\tendpoint = newFakeMarathonEndpoint(t, &configContainer{\n\t\t\tclient: &config,\n\t\t\tserver: &serverConfig{\n\t\t\t\tdcosToken: x.ServerDCOSToken,\n\t\t\t\tusername:  x.ServerUsername,\n\t\t\t\tpassword:  x.ServerPassword,\n\t\t\t},\n\t\t})\n\n\t\t_, err := endpoint.Client.Applications(nil)\n\n\t\tif x.Ok && err != nil {\n\t\t\tt.Errorf(\"case %d, did not expect an error: %s\", i, err)\n\t\t}\n\t\tif !x.Ok && err == nil {\n\t\t\tt.Errorf(\"case %d, expected to received an error\", i)\n\t\t}\n\n\t\tendpoint.Close()\n\t}\n}\n"
  },
  {
    "path": "cluster.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n)\n\nconst (\n\tmemberStatusUp   = 0\n\tmemberStatusDown = 1\n)\n\n// the status of a member node\ntype memberStatus int\n\n// cluster is a collection of marathon nodes\ntype cluster struct {\n\tsync.RWMutex\n\t// a collection of nodes\n\tmembers []*member\n\t// the marathon HTTP client to ensure consistency in requests\n\tclient *httpClient\n\t// healthCheckInterval is the interval by which we probe down nodes for\n\t// availability again.\n\thealthCheckInterval time.Duration\n}\n\n// member represents an individual endpoint\ntype member struct {\n\t// the name / ip address of the host\n\tendpoint string\n\t// the status of the host\n\tstatus memberStatus\n}\n\n// newCluster returns a new marathon cluster\nfunc newCluster(client *httpClient, marathonURL string, isDCOS bool) (*cluster, error) {\n\t// step: extract and basic validate the endpoints\n\tvar members []*member\n\tvar defaultProto string\n\n\tfor _, endpoint := range strings.Split(marathonURL, \",\") {\n\t\t// step: check for nothing\n\t\tif endpoint == \"\" {\n\t\t\treturn nil, newInvalidEndpointError(\"endpoint is blank\")\n\t\t}\n\t\t// step: prepend scheme if missing on (non-initial) endpoint.\n\t\tif !strings.HasPrefix(endpoint, \"http://\") && !strings.HasPrefix(endpoint, \"https://\") {\n\t\t\tif defaultProto == \"\" {\n\t\t\t\treturn nil, newInvalidEndpointError(\"missing scheme on (first) endpoint\")\n\t\t\t}\n\n\t\t\tendpoint = fmt.Sprintf(\"%s://%s\", defaultProto, endpoint)\n\t\t}\n\t\t// step: parse the url\n\t\tu, err := url.Parse(endpoint)\n\t\tif err != nil {\n\t\t\treturn nil, newInvalidEndpointError(\"invalid endpoint '%s': %s\", endpoint, err)\n\t\t}\n\t\tif defaultProto == \"\" {\n\t\t\tdefaultProto = u.Scheme\n\t\t}\n\n\t\t// step: check for empty hosts\n\t\tif u.Host == \"\" {\n\t\t\treturn nil, newInvalidEndpointError(\"endpoint: %s must have a host\", endpoint)\n\t\t}\n\n\t\t// step: if DCOS is set and no path is given, set the default DCOS path.\n\t\t// done in order to maintain compatibility with automatic addition of the\n\t\t// default DCOS path.\n\t\tif isDCOS && strings.TrimLeft(u.Path, \"/\") == \"\" {\n\t\t\tu.Path = defaultDCOSPath\n\t\t}\n\n\t\t// step: create a new node for this endpoint\n\t\tmembers = append(members, &member{endpoint: u.String()})\n\t}\n\n\treturn &cluster{\n\t\tclient:              client,\n\t\tmembers:             members,\n\t\thealthCheckInterval: 5 * time.Second,\n\t}, nil\n}\n\n// retrieve the current member, i.e. the current endpoint in use\nfunc (c *cluster) getMember() (string, error) {\n\tc.RLock()\n\tdefer c.RUnlock()\n\tfor _, n := range c.members {\n\t\tif n.status == memberStatusUp {\n\t\t\treturn n.endpoint, nil\n\t\t}\n\t}\n\n\treturn \"\", ErrMarathonDown\n}\n\n// markDown marks down the current endpoint\nfunc (c *cluster) markDown(endpoint string) {\n\tc.Lock()\n\tdefer c.Unlock()\n\tfor _, n := range c.members {\n\t\t// step: check if this is the node and it's marked as up - The double  checking on the\n\t\t// nodes status ensures the multiple calls don't create multiple checks\n\t\tif n.status == memberStatusUp && n.endpoint == endpoint {\n\t\t\tn.status = memberStatusDown\n\t\t\tgo c.healthCheckNode(n)\n\t\t\tbreak\n\t\t}\n\t}\n}\n\n// healthCheckNode performs a health check on the node and when active updates the status\nfunc (c *cluster) healthCheckNode(node *member) {\n\t// step: wait for the node to become active ... we are assuming a /ping is enough here\n\tticker := time.NewTicker(c.healthCheckInterval)\n\tdefer ticker.Stop()\n\tfor range ticker.C {\n\t\treq, err := c.client.buildMarathonRequest(\"GET\", node.endpoint, \"ping\", nil)\n\t\tif err == nil {\n\t\t\tres, err := c.client.Do(req)\n\t\t\tif err == nil && res.StatusCode == 200 {\n\t\t\t\t// step: mark the node as active again\n\t\t\t\tc.Lock()\n\t\t\t\tnode.status = memberStatusUp\n\t\t\t\tc.Unlock()\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n\n// activeMembers returns a list of active members\nfunc (c *cluster) activeMembers() []string {\n\treturn c.membersList(memberStatusUp)\n}\n\n// nonActiveMembers returns a list of non-active members in the cluster\nfunc (c *cluster) nonActiveMembers() []string {\n\treturn c.membersList(memberStatusDown)\n}\n\n// memberList returns a list of members of a specified status\nfunc (c *cluster) membersList(status memberStatus) []string {\n\tc.RLock()\n\tdefer c.RUnlock()\n\tvar list []string\n\tfor _, m := range c.members {\n\t\tif m.status == status {\n\t\t\tlist = append(list, m.endpoint)\n\t\t}\n\t}\n\n\treturn list\n}\n\n// size returns the size of the cluster\nfunc (c *cluster) size() int {\n\treturn len(c.members)\n}\n\n// String returns a string representation\nfunc (m member) String() string {\n\tstatus := \"UP\"\n\tif m.status == memberStatusDown {\n\t\tstatus = \"DOWN\"\n\t}\n\n\treturn fmt.Sprintf(\"member: %s:%s\", m.endpoint, status)\n}\n"
  },
  {
    "path": "cluster_test.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSize(t *testing.T) {\n\tcluster, err := newStandardCluster(fakeMarathonURL)\n\tassert.NoError(t, err)\n\tassert.Equal(t, cluster.size(), 3)\n}\n\nfunc TestActive(t *testing.T) {\n\tcluster, err := newStandardCluster(fakeMarathonURL)\n\tassert.NoError(t, err)\n\tassert.Equal(t, len(cluster.activeMembers()), 3)\n}\n\nfunc TestNonActive(t *testing.T) {\n\tcluster, err := newStandardCluster(fakeMarathonURL)\n\tassert.NoError(t, err)\n\tassert.Equal(t, len(cluster.nonActiveMembers()), 0)\n}\n\nfunc TestGetMember(t *testing.T) {\n\tcases := []struct {\n\t\tisDCOS      bool\n\t\tMarathonURL string\n\t\tmember      string\n\t}{\n\t\t{\n\t\t\tisDCOS:      false,\n\t\t\tMarathonURL: fakeMarathonURL,\n\t\t\tmember:      \"http://127.0.0.1:3000\",\n\t\t},\n\t\t{\n\t\t\tisDCOS:      false,\n\t\t\tMarathonURL: fakeMarathonURLWithPath,\n\t\t\tmember:      \"http://127.0.0.1:3000/path\",\n\t\t},\n\t\t{\n\t\t\tisDCOS:      true,\n\t\t\tMarathonURL: fakeMarathonURL,\n\t\t\tmember:      \"http://127.0.0.1:3000/marathon\",\n\t\t},\n\t\t{\n\t\t\tisDCOS:      true,\n\t\t\tMarathonURL: fakeMarathonURLWithPath,\n\t\t\tmember:      \"http://127.0.0.1:3000/path\",\n\t\t},\n\t}\n\tfor _, x := range cases {\n\t\tcluster, err := newCluster(&httpClient{config: Config{HTTPClient: defaultHTTPClient}}, x.MarathonURL, x.isDCOS)\n\t\tassert.NoError(t, err)\n\t\tmember, err := cluster.getMember()\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, member, x.member)\n\t}\n}\n\nfunc TestMarkDown(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\tcluster, err := newStandardCluster(endpoint.URL)\n\trequire.NoError(t, err)\n\trequire.Equal(t, len(cluster.activeMembers()), 3)\n\tcluster.healthCheckInterval = 2500 * time.Millisecond\n\n\tmembers := cluster.activeMembers()\n\tcluster.markDown(members[0])\n\tcluster.markDown(members[1])\n\trequire.Equal(t, len(cluster.activeMembers()), 1)\n\n\tticker := time.NewTicker(250 * time.Millisecond)\n\tdefer ticker.Stop()\n\ttimeout := time.NewTimer(5 * time.Second)\n\tdefer timeout.Stop()\n\tvar numFoundMembers int\n\tfor {\n\t\tnumFoundMembers = len(cluster.activeMembers())\n\t\tif numFoundMembers == 3 {\n\t\t\tbreak\n\t\t}\n\t\tselect {\n\t\tcase <-ticker.C:\n\t\t\tcontinue\n\t\tcase <-timeout.C:\n\t\t\tt.Fatalf(\"found %d active member(s), want 3\", numFoundMembers)\n\t\t}\n\t}\n}\n\nfunc TestValidClusterHosts(t *testing.T) {\n\tcs := []struct {\n\t\tURL    string\n\t\tExpect []string\n\t}{\n\t\t{\n\t\t\tURL:    \"http://127.0.0.1\",\n\t\t\tExpect: []string{\"http://127.0.0.1\"},\n\t\t},\n\t\t{\n\t\t\tURL:    \"http://127.0.0.1:8080\",\n\t\t\tExpect: []string{\"http://127.0.0.1:8080\"},\n\t\t},\n\t\t{\n\t\t\tURL:    \"http://127.0.0.1:8080,http://127.0.0.2:8081\",\n\t\t\tExpect: []string{\"http://127.0.0.1:8080\", \"http://127.0.0.2:8081\"},\n\t\t},\n\t\t{\n\t\t\tURL:    \"https://127.0.0.1:8080,http://127.0.0.2:8081\",\n\t\t\tExpect: []string{\"https://127.0.0.1:8080\", \"http://127.0.0.2:8081\"},\n\t\t},\n\t\t{\n\t\t\tURL:    \"http://127.0.0.1:8080,127.0.0.2\",\n\t\t\tExpect: []string{\"http://127.0.0.1:8080\", \"http://127.0.0.2\"},\n\t\t},\n\t\t{\n\t\t\tURL:    \"https://127.0.0.1:8080,127.0.0.2\",\n\t\t\tExpect: []string{\"https://127.0.0.1:8080\", \"https://127.0.0.2\"},\n\t\t},\n\t\t{\n\t\t\tURL:    \"http://127.0.0.1:8080,127.0.0.2:8080\",\n\t\t\tExpect: []string{\"http://127.0.0.1:8080\", \"http://127.0.0.2:8080\"},\n\t\t},\n\t\t{\n\t\t\tURL:    \"http://127.0.0.1:8080,https://127.0.0.2\",\n\t\t\tExpect: []string{\"http://127.0.0.1:8080\", \"https://127.0.0.2\"},\n\t\t},\n\t\t{\n\t\t\tURL:    \"http://127.0.0.1:8080,https://127.0.0.2:8080\",\n\t\t\tExpect: []string{\"http://127.0.0.1:8080\", \"https://127.0.0.2:8080\"},\n\t\t},\n\t\t{\n\t\t\tURL:    \"http://127.0.0.1:8080/path1,127.0.0.2/path2\",\n\t\t\tExpect: []string{\"http://127.0.0.1:8080/path1\", \"http://127.0.0.2/path2\"},\n\t\t},\n\t}\n\tfor _, x := range cs {\n\t\tc, err := newStandardCluster(x.URL)\n\t\tif !assert.NoError(t, err, \"URL '%s' should not have thrown an error: %s\", x.URL, err) {\n\t\t\tcontinue\n\t\t}\n\t\tassert.Equal(t, x.Expect, c.activeMembers(), \"URL '%s', expected: %v, got: %s\", x.URL, x.Expect, c.activeMembers())\n\t}\n}\n\nfunc TestInvalidClusterHosts(t *testing.T) {\n\tfor _, invalidHost := range []string{\n\t\t\"\",\n\t\t\"://\",\n\t\t\"http://\",\n\t\t\"http://,,\",\n\t\t\"http://%42\",\n\t\t\"http://,127.0.0.1:3000,127.0.0.1:3000\",\n\t\t\"http://127.0.0.1:3000,,127.0.0.1:3000\",\n\t\t\"http://127.0.0.1:3000,127.0.0.1:3000,\",\n\t\t\"foo://127.0.0.1:3000\",\n\t} {\n\t\t_, err := newStandardCluster(invalidHost)\n\t\tif !assert.Error(t, err) {\n\t\t\tt.Errorf(\"undetected invalid host: %s\", invalidHost)\n\t\t}\n\t}\n}\n\nfunc newStandardCluster(url string) (*cluster, error) {\n\treturn newCluster(&httpClient{config: Config{HTTPClient: defaultHTTPClient}}, url, false)\n}\n"
  },
  {
    "path": "config.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"io\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"time\"\n)\n\nconst defaultPollingWaitTime = 500 * time.Millisecond\n\nconst defaultDCOSPath = \"marathon\"\n\n// EventsTransport describes which transport should be used to deliver Marathon events\ntype EventsTransport int\n\n// Config holds the settings and options for the client\ntype Config struct {\n\t// URL is the url for marathon\n\tURL string\n\t// EventsTransport is the events transport: EventsTransportCallback or EventsTransportSSE\n\tEventsTransport EventsTransport\n\t// EventsPort is the event handler port\n\tEventsPort int\n\t// the interface we should be listening on for events\n\tEventsInterface string\n\t// HTTPBasicAuthUser is the http basic auth\n\tHTTPBasicAuthUser string\n\t// HTTPBasicPassword is the http basic password\n\tHTTPBasicPassword string\n\t// CallbackURL custom callback url\n\tCallbackURL string\n\t// DCOSToken for DCOS environment, This will override the Authorization header\n\tDCOSToken string\n\t// LogOutput the output for debug log messages\n\tLogOutput io.Writer\n\t// HTTPClient is the HTTP client\n\tHTTPClient *http.Client\n\t// HTTPSSEClient is the HTTP client used for SSE subscriptions, can't have client.Timeout set\n\tHTTPSSEClient *http.Client\n\t// wait time (in milliseconds) between repetitive requests to the API during polling\n\tPollingWaitTime time.Duration\n}\n\n// NewDefaultConfig create a default client config\nfunc NewDefaultConfig() Config {\n\treturn Config{\n\t\tURL:             \"http://127.0.0.1:8080\",\n\t\tEventsTransport: EventsTransportCallback,\n\t\tEventsPort:      10001,\n\t\tEventsInterface: \"eth0\",\n\t\tLogOutput:       ioutil.Discard,\n\t\tPollingWaitTime: defaultPollingWaitTime,\n\t}\n}\n"
  },
  {
    "path": "const.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nconst (\n\tdefaultEventsURL = \"/event\"\n\n\t/* --- api related constants --- */\n\tmarathonAPIVersion      = \"v2\"\n\tmarathonAPIEventStream  = marathonAPIVersion + \"/events\"\n\tmarathonAPISubscription = marathonAPIVersion + \"/eventSubscriptions\"\n\tmarathonAPIApps         = marathonAPIVersion + \"/apps\"\n\tmarathonAPIPods         = marathonAPIVersion + \"/pods\"\n\tmarathonAPITasks        = marathonAPIVersion + \"/tasks\"\n\tmarathonAPIDeployments  = marathonAPIVersion + \"/deployments\"\n\tmarathonAPIGroups       = marathonAPIVersion + \"/groups\"\n\tmarathonAPIQueue        = marathonAPIVersion + \"/queue\"\n\tmarathonAPIInfo         = marathonAPIVersion + \"/info\"\n\tmarathonAPILeader       = marathonAPIVersion + \"/leader\"\n\tmarathonAPIPing         = \"ping\"\n)\n\nconst (\n\t// EventsTransportCallback activates callback events transport\n\tEventsTransportCallback EventsTransport = 1 << iota\n\n\t// EventsTransportSSE activates stream events transport\n\tEventsTransportSSE\n)\n"
  },
  {
    "path": "deployment.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n)\n\n// Deployment is a marathon deployment definition\ntype Deployment struct {\n\tID             string              `json:\"id\"`\n\tVersion        string              `json:\"version\"`\n\tCurrentStep    int                 `json:\"currentStep\"`\n\tTotalSteps     int                 `json:\"totalSteps\"`\n\tAffectedApps   []string            `json:\"affectedApps\"`\n\tAffectedPods   []string            `json:\"affectedPods\"`\n\tSteps          [][]*DeploymentStep `json:\"-\"`\n\tXXStepsRaw     json.RawMessage     `json:\"steps\"` // Holds raw steps JSON to unmarshal later\n\tCurrentActions []*DeploymentStep   `json:\"currentActions\"`\n}\n\n// DeploymentID is the identifier for a application deployment\ntype DeploymentID struct {\n\tDeploymentID string `json:\"deploymentId\"`\n\tVersion      string `json:\"version\"`\n}\n\n// DeploymentStep is a step in the application deployment plan\ntype DeploymentStep struct {\n\tAction                string                  `json:\"action\"`\n\tApp                   string                  `json:\"app\"`\n\tReadinessCheckResults *[]ReadinessCheckResult `json:\"readinessCheckResults,omitempty\"`\n}\n\n// StepActions is a series of deployment steps\ntype StepActions struct {\n\tActions []struct {\n\t\tAction string `json:\"action\"` // 1.1.2 and after\n\t\tType   string `json:\"type\"`   // 1.1.1 and before\n\t\tApp    string `json:\"app\"`\n\t}\n}\n\n// DeploymentPlan is a collection of steps for application deployment\ntype DeploymentPlan struct {\n\tID       string         `json:\"id\"`\n\tVersion  string         `json:\"version\"`\n\tOriginal *Group         `json:\"original\"`\n\tTarget   *Group         `json:\"target\"`\n\tSteps    []*StepActions `json:\"steps\"`\n}\n\n// Deployments retrieves a list of current deployments\nfunc (r *marathonClient) Deployments() ([]*Deployment, error) {\n\tvar deployments []*Deployment\n\terr := r.apiGet(marathonAPIDeployments, nil, &deployments)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// Allows loading of deployment steps from the Marathon v1.X API\n\t// Implements a fix for issue https://github.com/gambol99/go-marathon/issues/153\n\tfor _, deployment := range deployments {\n\t\t// Unmarshal pre-v1.X step\n\t\tif err := json.Unmarshal(deployment.XXStepsRaw, &deployment.Steps); err != nil {\n\t\t\tdeployment.Steps = make([][]*DeploymentStep, 0)\n\t\t\tvar steps []*StepActions\n\t\t\t// Unmarshal v1.X Marathon step\n\t\t\tif err := json.Unmarshal(deployment.XXStepsRaw, &steps); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tfor stepIndex, step := range steps {\n\t\t\t\tdeployment.Steps = append(deployment.Steps, make([]*DeploymentStep, len(step.Actions)))\n\t\t\t\tfor actionIndex, action := range step.Actions {\n\t\t\t\t\tvar stepAction string\n\t\t\t\t\tif action.Type != \"\" {\n\t\t\t\t\t\tstepAction = action.Type\n\t\t\t\t\t} else {\n\t\t\t\t\t\tstepAction = action.Action\n\t\t\t\t\t}\n\t\t\t\t\tdeployment.Steps[stepIndex][actionIndex] = &DeploymentStep{\n\t\t\t\t\t\tAction: stepAction,\n\t\t\t\t\t\tApp:    action.App,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn deployments, nil\n}\n\n// DeleteDeployment delete a current deployment from marathon\n// \tid:\t\tthe deployment id you wish to delete\n// \tforce:\twhether or not to force the deletion\nfunc (r *marathonClient) DeleteDeployment(id string, force bool) (*DeploymentID, error) {\n\tpath := fmt.Sprintf(\"%s/%s\", marathonAPIDeployments, id)\n\n\t// if force=true, no body is returned\n\tif force {\n\t\tpath += \"?force=true\"\n\t\treturn nil, r.apiDelete(path, nil, nil)\n\t}\n\n\tdeployment := new(DeploymentID)\n\terr := r.apiDelete(path, nil, deployment)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn deployment, nil\n}\n\n// HasDeployment checks to see if a deployment exists\n// \tid:\t\tthe deployment id you are looking for\nfunc (r *marathonClient) HasDeployment(id string) (bool, error) {\n\tdeployments, err := r.Deployments()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tfor _, deployment := range deployments {\n\t\tif deployment.ID == id {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\treturn false, nil\n}\n\n// WaitOnDeployment waits on a deployment to finish\n//  version:\t\tthe version of the application\n// \ttimeout:\t\tthe timeout to wait for the deployment to take, otherwise return an error\nfunc (r *marathonClient) WaitOnDeployment(id string, timeout time.Duration) error {\n\tif found, err := r.HasDeployment(id); err != nil {\n\t\treturn err\n\t} else if !found {\n\t\treturn nil\n\t}\n\n\tnowTime := time.Now()\n\tstopTime := nowTime.Add(timeout)\n\tif timeout <= 0 {\n\t\tstopTime = nowTime.Add(time.Duration(900) * time.Second)\n\t}\n\n\t// step: a somewhat naive implementation, but it will work\n\tfor {\n\t\tif time.Now().After(stopTime) {\n\t\t\treturn ErrTimeoutError\n\t\t}\n\t\tfound, err := r.HasDeployment(id)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !found {\n\t\t\treturn nil\n\t\t}\n\t\ttime.Sleep(r.config.PollingWaitTime)\n\t}\n}\n"
  },
  {
    "path": "deployment_test.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDeployments(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tdeployments, err := endpoint.Client.Deployments()\n\trequire.NoError(t, err)\n\trequire.NotNil(t, deployments)\n\trequire.Equal(t, len(deployments), 1)\n\tdeployment := deployments[0]\n\trequire.NotNil(t, deployment)\n\tassert.Equal(t, deployment.ID, \"867ed450-f6a8-4d33-9b0e-e11c5513990b\")\n\trequire.NotNil(t, deployment.Steps)\n\tassert.Equal(t, len(deployment.Steps), 1)\n}\n\nfunc TestDeploymentsV1(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, &configContainer{\n\t\tserver: &serverConfig{\n\t\t\tscope: \"v1.1.1\",\n\t\t},\n\t})\n\tdefer endpoint.Close()\n\tdeployments, err := endpoint.Client.Deployments()\n\tassert.NoError(t, err)\n\tassert.NotNil(t, deployments)\n\tassert.Equal(t, len(deployments), 1)\n\tdeployment := deployments[0]\n\tassert.NotNil(t, deployment)\n\tassert.Equal(t, deployment.ID, \"2620aa06-1001-4eea-8861-a51957d4fd80\")\n\tassert.NotNil(t, deployment.Steps)\n\tassert.Equal(t, len(deployment.Steps), 2)\n\n\trequire.Equal(t, len(deployment.CurrentActions), 1)\n\tcurAction := deployment.CurrentActions[0]\n\trequire.NotNil(t, curAction)\n\trequire.NotNil(t, curAction.ReadinessCheckResults)\n\trequire.True(t, len(*curAction.ReadinessCheckResults) > 0)\n\tactualRes := (*curAction.ReadinessCheckResults)[0]\n\texpectedRes := ReadinessCheckResult{\n\t\tName:   \"myReadyCheck\",\n\t\tTaskID: \"test_frontend_app1.c9de6033\",\n\t\tReady:  false,\n\t\tLastResponse: ReadinessLastResponse{\n\t\t\tBody:        \"{}\",\n\t\t\tContentType: \"application/json\",\n\t\t\tStatus:      500,\n\t\t},\n\t}\n\tassert.Equal(t, expectedRes, actualRes)\n}\n\nfunc TestDeleteDeployment(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\tid, err := endpoint.Client.DeleteDeployment(fakeDeploymentID, false)\n\trequire.NoError(t, err)\n\tassert.Equal(t, id.DeploymentID, \"0b1467fc-d5cd-4bbc-bac2-2805351cee1e\")\n\tassert.Equal(t, id.Version, \"2014-08-26T08:20:26.171Z\")\n}\n\nfunc TestDeleteDeploymentForce(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\tresp, err := endpoint.Client.DeleteDeployment(fakeDeploymentID, true)\n\trequire.NoError(t, err)\n\tassert.Nil(t, resp)\n}\n"
  },
  {
    "path": "docker.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n)\n\n// Container is the definition for a container type in marathon\ntype Container struct {\n\tType         string         `json:\"type,omitempty\"`\n\tDocker       *Docker        `json:\"docker,omitempty\"`\n\tVolumes      *[]Volume      `json:\"volumes,omitempty\"`\n\tPortMappings *[]PortMapping `json:\"portMappings,omitempty\"`\n}\n\n// PortMapping is the portmapping structure between container and mesos\ntype PortMapping struct {\n\tContainerPort int                `json:\"containerPort,omitempty\"`\n\tHostPort      int                `json:\"hostPort\"`\n\tLabels        *map[string]string `json:\"labels,omitempty\"`\n\tName          string             `json:\"name,omitempty\"`\n\tServicePort   int                `json:\"servicePort,omitempty\"`\n\tProtocol      string             `json:\"protocol,omitempty\"`\n\tNetworkNames  *[]string          `json:\"networkNames,omitempty\"`\n}\n\n// Parameters is the parameters to pass to the docker client when creating the container\ntype Parameters struct {\n\tKey   string `json:\"key,omitempty\"`\n\tValue string `json:\"value,omitempty\"`\n}\n\n// Volume is the docker volume details associated to the container\ntype Volume struct {\n\tContainerPath string            `json:\"containerPath,omitempty\"`\n\tHostPath      string            `json:\"hostPath,omitempty\"`\n\tExternal      *ExternalVolume   `json:\"external,omitempty\"`\n\tMode          string            `json:\"mode,omitempty\"`\n\tPersistent    *PersistentVolume `json:\"persistent,omitempty\"`\n\tSecret        string            `json:\"secret,omitempty\"`\n}\n\n// PersistentVolumeType is the a persistent docker volume to be mounted\ntype PersistentVolumeType string\n\nconst (\n\t// PersistentVolumeTypeRoot is the root path of the persistent volume\n\tPersistentVolumeTypeRoot PersistentVolumeType = \"root\"\n\t// PersistentVolumeTypePath is the mount path of the persistent volume\n\tPersistentVolumeTypePath PersistentVolumeType = \"path\"\n\t// PersistentVolumeTypeMount is the mount type of the persistent volume\n\tPersistentVolumeTypeMount PersistentVolumeType = \"mount\"\n)\n\n// PersistentVolume declares a Volume to be Persistent, and sets\n// the size (in MiB) and optional type, max size (MiB) and constraints for the Volume.\ntype PersistentVolume struct {\n\tType        PersistentVolumeType `json:\"type,omitempty\"`\n\tSize        int                  `json:\"size\"`\n\tMaxSize     int                  `json:\"maxSize,omitempty\"`\n\tConstraints *[][]string          `json:\"constraints,omitempty\"`\n}\n\n// SetType sets the type of mesos disk resource to use\n//\t\ttype:\t       PersistentVolumeType enum\nfunc (p *PersistentVolume) SetType(tp PersistentVolumeType) *PersistentVolume {\n\tp.Type = tp\n\treturn p\n}\n\n// SetSize sets size of the persistent volume\n//\t\tsize:\t        size in MiB\nfunc (p *PersistentVolume) SetSize(size int) *PersistentVolume {\n\tp.Size = size\n\treturn p\n}\n\n// SetMaxSize sets maximum size of an exclusive mount-disk resource to consider;\n// does not apply to root or path disk resource types\n//\t\tmaxSize:\tsize in MiB\nfunc (p *PersistentVolume) SetMaxSize(maxSize int) *PersistentVolume {\n\tp.MaxSize = maxSize\n\treturn p\n}\n\n// AddConstraint adds a new constraint\n//\t\tconstraints:\tthe constraint definition, one constraint per array element\nfunc (p *PersistentVolume) AddConstraint(constraints ...string) *PersistentVolume {\n\tif p.Constraints == nil {\n\t\tp.EmptyConstraints()\n\t}\n\n\tc := *p.Constraints\n\tc = append(c, constraints)\n\tp.Constraints = &c\n\treturn p\n}\n\n// EmptyConstraints explicitly empties constraints -- use this if you need to empty\n// constraints of an application that already has constraints set (setting constraints to nil will\n// keep the current value)\nfunc (p *PersistentVolume) EmptyConstraints() *PersistentVolume {\n\tp.Constraints = &[][]string{}\n\treturn p\n}\n\n// ExternalVolume is an external volume definition\ntype ExternalVolume struct {\n\tName     string             `json:\"name,omitempty\"`\n\tProvider string             `json:\"provider,omitempty\"`\n\tOptions  *map[string]string `json:\"options,omitempty\"`\n}\n\n// PullConfig specifies a secret for authentication with a private Docker registry\ntype PullConfig struct {\n\tSecret string `json:\"secret,omitempty\"`\n}\n\n// NewPullConfig creats a *PullConfig based on a given secret\nfunc NewPullConfig(secret string) *PullConfig {\n\treturn &PullConfig{\n\t\tSecret: secret,\n\t}\n}\n\n// Docker is the docker definition from a marathon application\ntype Docker struct {\n\tForcePullImage *bool          `json:\"forcePullImage,omitempty\"`\n\tImage          string         `json:\"image,omitempty\"`\n\tNetwork        string         `json:\"network,omitempty\"`\n\tParameters     *[]Parameters  `json:\"parameters,omitempty\"`\n\tPortMappings   *[]PortMapping `json:\"portMappings,omitempty\"`\n\tPrivileged     *bool          `json:\"privileged,omitempty\"`\n\tPullConfig     *PullConfig    `json:\"pullConfig,omitempty\"`\n}\n\n// Volume attachs a volume to the container\n//\t\thost_path:\t\t\tthe path on the docker host to map\n//\t\tcontainer_path:\t\tthe path inside the container to map the host volume\n//\t\tmode:\t\t\t\tthe mode to map the container\nfunc (container *Container) Volume(hostPath, containerPath, mode string) *Container {\n\tif container.Volumes == nil {\n\t\tcontainer.EmptyVolumes()\n\t}\n\n\tvolumes := *container.Volumes\n\tvolumes = append(volumes, Volume{\n\t\tContainerPath: containerPath,\n\t\tHostPath:      hostPath,\n\t\tMode:          mode,\n\t})\n\n\tcontainer.Volumes = &volumes\n\n\treturn container\n}\n\n// EmptyVolumes explicitly empties the volumes -- use this if you need to empty\n// volumes of an application that already has volumes set (setting volumes to nil will\n// keep the current value)\nfunc (container *Container) EmptyVolumes() *Container {\n\tcontainer.Volumes = &[]Volume{}\n\treturn container\n}\n\n// SetPersistentVolume defines persistent properties for volume\nfunc (v *Volume) SetPersistentVolume() *PersistentVolume {\n\tev := &PersistentVolume{}\n\tv.Persistent = ev\n\treturn ev\n}\n\n// SetSecretVolume defines secret and containerPath for volume\nfunc (v *Volume) SetSecretVolume(containerPath, secret string) *Volume {\n\tv.ContainerPath = containerPath\n\tv.Secret = secret\n\treturn v\n}\n\n// EmptyPersistentVolume empties the persistent volume definition\nfunc (v *Volume) EmptyPersistentVolume() *Volume {\n\tv.Persistent = &PersistentVolume{}\n\treturn v\n}\n\n// SetExternalVolume define external elements for a volume\n//      name: the name of the volume\n//      provider: the provider of the volume (e.g. dvdi)\nfunc (v *Volume) SetExternalVolume(name, provider string) *ExternalVolume {\n\tev := &ExternalVolume{\n\t\tName:     name,\n\t\tProvider: provider,\n\t}\n\tv.External = ev\n\treturn ev\n}\n\n// EmptyExternalVolume emptys the external volume definition\nfunc (v *Volume) EmptyExternalVolume() *Volume {\n\tv.External = &ExternalVolume{}\n\treturn v\n}\n\n// AddOption adds an option to an ExternalVolume\n//\t\tname:  the name of the option\n//\t\tvalue: value for the option\nfunc (ev *ExternalVolume) AddOption(name, value string) *ExternalVolume {\n\tif ev.Options == nil {\n\t\tev.EmptyOptions()\n\t}\n\t(*ev.Options)[name] = value\n\n\treturn ev\n}\n\n// EmptyOptions explicitly empties the options\nfunc (ev *ExternalVolume) EmptyOptions() *ExternalVolume {\n\tev.Options = &map[string]string{}\n\n\treturn ev\n}\n\n// NewDockerContainer creates a default docker container for you\nfunc NewDockerContainer() *Container {\n\tcontainer := &Container{}\n\tcontainer.Type = \"DOCKER\"\n\tcontainer.Docker = &Docker{}\n\n\treturn container\n}\n\n// SetForcePullImage sets whether the docker image should always be force pulled before\n// starting an instance\n//\t\tforcePull:\t\t\ttrue / false\nfunc (docker *Docker) SetForcePullImage(forcePull bool) *Docker {\n\tdocker.ForcePullImage = &forcePull\n\n\treturn docker\n}\n\n// SetPrivileged sets whether the docker image should be started\n// with privilege turned on\n//\t\tpriv:\t\t\ttrue / false\nfunc (docker *Docker) SetPrivileged(priv bool) *Docker {\n\tdocker.Privileged = &priv\n\n\treturn docker\n}\n\n// Container sets the image of the container\n//\t\timage:\t\t\tthe image name you are using\nfunc (docker *Docker) Container(image string) *Docker {\n\tdocker.Image = image\n\treturn docker\n}\n\n// Bridged sets the networking mode to bridged\nfunc (docker *Docker) Bridged() *Docker {\n\tdocker.Network = \"BRIDGE\"\n\treturn docker\n}\n\n// Host sets the networking mode to host\nfunc (docker *Docker) Host() *Docker {\n\tdocker.Network = \"HOST\"\n\treturn docker\n}\n\n// Expose sets the container to expose the following TCP ports\n//\t\tports:\t\t\tthe TCP ports the container is exposing\nfunc (container *Container) Expose(ports ...int) *Container {\n\tfor _, port := range ports {\n\t\tcontainer.ExposePort(PortMapping{\n\t\t\tContainerPort: port,\n\t\t\tHostPort:      0,\n\t\t\tServicePort:   0,\n\t\t\tProtocol:      \"tcp\"})\n\t}\n\treturn container\n}\n\n// Expose sets the container to expose the following TCP ports\n//\t\tports:\t\t\tthe TCP ports the container is exposing\nfunc (docker *Docker) Expose(ports ...int) *Docker {\n\tfor _, port := range ports {\n\t\tdocker.ExposePort(PortMapping{\n\t\t\tContainerPort: port,\n\t\t\tHostPort:      0,\n\t\t\tServicePort:   0,\n\t\t\tProtocol:      \"tcp\"})\n\t}\n\treturn docker\n}\n\n// ExposeUDP sets the container to expose the following UDP ports\n//\t\tports:\t\t\tthe UDP ports the container is exposing\nfunc (container *Container) ExposeUDP(ports ...int) *Container {\n\tfor _, port := range ports {\n\t\tcontainer.ExposePort(PortMapping{\n\t\t\tContainerPort: port,\n\t\t\tHostPort:      0,\n\t\t\tServicePort:   0,\n\t\t\tProtocol:      \"udp\"})\n\t}\n\treturn container\n}\n\n// ExposeUDP sets the container to expose the following UDP ports\n//\t\tports:\t\t\tthe UDP ports the container is exposing\nfunc (docker *Docker) ExposeUDP(ports ...int) *Docker {\n\tfor _, port := range ports {\n\t\tdocker.ExposePort(PortMapping{\n\t\t\tContainerPort: port,\n\t\t\tHostPort:      0,\n\t\t\tServicePort:   0,\n\t\t\tProtocol:      \"udp\"})\n\t}\n\treturn docker\n}\n\n// ExposePort exposes an port in the container\nfunc (container *Container) ExposePort(portMapping PortMapping) *Container {\n\tif container.PortMappings == nil {\n\t\tcontainer.EmptyPortMappings()\n\t}\n\n\tportMappings := *container.PortMappings\n\tportMappings = append(portMappings, portMapping)\n\tcontainer.PortMappings = &portMappings\n\n\treturn container\n}\n\n// ExposePort exposes an port in the container\nfunc (docker *Docker) ExposePort(portMapping PortMapping) *Docker {\n\tif docker.PortMappings == nil {\n\t\tdocker.EmptyPortMappings()\n\t}\n\n\tportMappings := *docker.PortMappings\n\tportMappings = append(portMappings, portMapping)\n\tdocker.PortMappings = &portMappings\n\n\treturn docker\n}\n\n// EmptyPortMappings explicitly empties the port mappings -- use this if you need to empty\n// port mappings of an application that already has port mappings set (setting port mappings to nil will\n// keep the current value)\nfunc (container *Container) EmptyPortMappings() *Container {\n\tcontainer.PortMappings = &[]PortMapping{}\n\treturn container\n}\n\n// EmptyPortMappings explicitly empties the port mappings -- use this if you need to empty\n// port mappings of an application that already has port mappings set (setting port mappings to nil will\n// keep the current value)\nfunc (docker *Docker) EmptyPortMappings() *Docker {\n\tdocker.PortMappings = &[]PortMapping{}\n\treturn docker\n}\n\n// AddLabel adds a label to a PortMapping\n//\t\tname:\tthe name of the label\n//\t\tvalue: value for this label\nfunc (p *PortMapping) AddLabel(name, value string) *PortMapping {\n\tif p.Labels == nil {\n\t\tp.EmptyLabels()\n\t}\n\t(*p.Labels)[name] = value\n\n\treturn p\n}\n\n// EmptyLabels explicitly empties the labels -- use this if you need to empty\n// the labels of a port mapping that already has labels set (setting labels to\n// nil will keep the current value)\nfunc (p *PortMapping) EmptyLabels() *PortMapping {\n\tp.Labels = &map[string]string{}\n\n\treturn p\n}\n\n// AddParameter adds a parameter to the docker execution line when creating the container\n//\t\tkey:\t\t\tthe name of the option to add\n//\t\tvalue:\t\tthe value of the option\nfunc (docker *Docker) AddParameter(key string, value string) *Docker {\n\tif docker.Parameters == nil {\n\t\tdocker.EmptyParameters()\n\t}\n\n\tparameters := *docker.Parameters\n\tparameters = append(parameters, Parameters{\n\t\tKey:   key,\n\t\tValue: value})\n\n\tdocker.Parameters = &parameters\n\n\treturn docker\n}\n\n// EmptyParameters explicitly empties the parameters -- use this if you need to empty\n// parameters of an application that already has parameters set (setting parameters to nil will\n// keep the current value)\nfunc (docker *Docker) EmptyParameters() *Docker {\n\tdocker.Parameters = &[]Parameters{}\n\treturn docker\n}\n\n// ServicePortIndex finds the service port index of the exposed port\n//\t\tport:\t\t\tthe port you are looking for\nfunc (container *Container) ServicePortIndex(port int) (int, error) {\n\tif container.PortMappings == nil || len(*container.PortMappings) == 0 {\n\t\treturn 0, errors.New(\"The container does not contain any port mappings to search\")\n\t}\n\n\t// step: iterate and find the port\n\tfor index, containerPort := range *container.PortMappings {\n\t\tif containerPort.ContainerPort == port {\n\t\t\treturn index, nil\n\t\t}\n\t}\n\n\t// step: we didn't find the port in the mappings\n\treturn 0, fmt.Errorf(\"The container port %d was not found in the container port mappings\", port)\n}\n\n// ServicePortIndex finds the service port index of the exposed port\n//\t\tport:\t\t\tthe port you are looking for\nfunc (docker *Docker) ServicePortIndex(port int) (int, error) {\n\tif docker.PortMappings == nil || len(*docker.PortMappings) == 0 {\n\t\treturn 0, errors.New(\"The docker does not contain any port mappings to search\")\n\t}\n\n\t// step: iterate and find the port\n\tfor index, containerPort := range *docker.PortMappings {\n\t\tif containerPort.ContainerPort == port {\n\t\t\treturn index, nil\n\t\t}\n\t}\n\n\t// step: we didn't find the port in the mappings\n\treturn 0, fmt.Errorf(\"The docker port %d was not found in the container port mappings\", port)\n}\n\n// SetPullConfig adds *PullConfig to Docker\nfunc (docker *Docker) SetPullConfig(pullConfig *PullConfig) *Docker {\n\tdocker.PullConfig = pullConfig\n\n\treturn docker\n}\n\n// AddNetwork adds a network name to a PortMapping\n//\t\tname:\tthe name of the network\nfunc (p *PortMapping) AddNetwork(name string) *PortMapping {\n\tif p.NetworkNames == nil {\n\t\tp.EmptyNetworkNames()\n\t}\n\tnetworks := *p.NetworkNames\n\tnetworks = append(networks, name)\n\tp.NetworkNames = &networks\n\treturn p\n}\n\n// EmptyNetworkNames explicitly empties the network names -- use this if you need to empty\n// the network names of a port mapping that already has network names set\nfunc (p *PortMapping) EmptyNetworkNames() *PortMapping {\n\tp.NetworkNames = &[]string{}\n\n\treturn p\n}\n"
  },
  {
    "path": "docker_test.go",
    "content": "/*\nCopyright 2015 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc createPortMapping(containerPort int, protocol string) *PortMapping {\n\treturn &PortMapping{\n\t\tContainerPort: containerPort,\n\t\tHostPort:      0,\n\t\tServicePort:   0,\n\t\tProtocol:      protocol,\n\t}\n}\n\nfunc TestDockerAddParameter(t *testing.T) {\n\tdocker := NewDockerApplication().Container.Docker\n\tdocker.AddParameter(\"k1\", \"v1\").AddParameter(\"k2\", \"v2\")\n\n\tassert.Equal(t, 2, len(*docker.Parameters))\n\tassert.Equal(t, (*docker.Parameters)[0].Key, \"k1\")\n\tassert.Equal(t, (*docker.Parameters)[0].Value, \"v1\")\n\tassert.Equal(t, (*docker.Parameters)[1].Key, \"k2\")\n\tassert.Equal(t, (*docker.Parameters)[1].Value, \"v2\")\n\n\tdocker.EmptyParameters()\n\tassert.NotNil(t, docker.Parameters)\n\tassert.Equal(t, 0, len(*docker.Parameters))\n}\nfunc TestDockerExpose(t *testing.T) {\n\tapps := []*Application{\n\t\tNewDockerApplication(),\n\t\tNewDockerApplication(),\n\t}\n\n\t// Marathon < 1.5\n\tapps[0].Container.Docker.Expose(8080).Expose(80, 443)\n\n\t// Marathon >= 1.5\n\tapps[1].Container.Expose(8080).Expose(80, 443)\n\n\tportMappings := []*[]PortMapping{\n\t\tapps[0].Container.Docker.PortMappings,\n\t\tapps[1].Container.PortMappings,\n\t}\n\n\tfor _, portMapping := range portMappings {\n\t\tassert.Equal(t, 3, len(*portMapping))\n\n\t\tassert.Equal(t, *createPortMapping(8080, \"tcp\"), (*portMapping)[0])\n\t\tassert.Equal(t, *createPortMapping(80, \"tcp\"), (*portMapping)[1])\n\t\tassert.Equal(t, *createPortMapping(443, \"tcp\"), (*portMapping)[2])\n\t}\n}\n\nfunc TestDockerExposeUDP(t *testing.T) {\n\tapps := []*Application{\n\t\tNewDockerApplication(),\n\t\tNewDockerApplication(),\n\t}\n\n\t// Marathon < 1.5\n\tapps[0].Container.Docker.ExposeUDP(53).ExposeUDP(5060, 6881)\n\n\t// Marathon >= 1.5\n\tapps[1].Container.ExposeUDP(53).ExposeUDP(5060, 6881)\n\n\tportMappings := []*[]PortMapping{\n\t\tapps[0].Container.Docker.PortMappings,\n\t\tapps[1].Container.PortMappings,\n\t}\n\n\tfor _, portMapping := range portMappings {\n\t\tassert.Equal(t, 3, len(*portMapping))\n\t\tassert.Equal(t, *createPortMapping(53, \"udp\"), (*portMapping)[0])\n\t\tassert.Equal(t, *createPortMapping(5060, \"udp\"), (*portMapping)[1])\n\t\tassert.Equal(t, *createPortMapping(6881, \"udp\"), (*portMapping)[2])\n\t}\n}\n\nfunc TestPortMappingLabels(t *testing.T) {\n\tpm := createPortMapping(80, \"tcp\")\n\n\tpm.AddLabel(\"hello\", \"world\").AddLabel(\"foo\", \"bar\")\n\n\tassert.Equal(t, 2, len(*pm.Labels))\n\tassert.Equal(t, \"world\", (*pm.Labels)[\"hello\"])\n\tassert.Equal(t, \"bar\", (*pm.Labels)[\"foo\"])\n\n\tpm.EmptyLabels()\n\n\tassert.NotNil(t, pm.Labels)\n\tassert.Equal(t, 0, len(*pm.Labels))\n}\n\nfunc TestPortMappingNetworkNames(t *testing.T) {\n\tpm := createPortMapping(80, \"tcp\")\n\n\tpm.AddNetwork(\"test\")\n\n\tassert.Equal(t, 1, len(*pm.NetworkNames))\n\tassert.Equal(t, \"test\", (*pm.NetworkNames)[0])\n\n\tpm.EmptyNetworkNames()\n\n\tassert.NotNil(t, pm.NetworkNames)\n\tassert.Equal(t, 0, len(*pm.NetworkNames))\n}\n\nfunc TestVolume(t *testing.T) {\n\tcontainer := NewDockerApplication().Container\n\n\tcontainer.Volume(\"hp1\", \"cp1\", \"RW\")\n\tcontainer.Volume(\"hp2\", \"cp2\", \"R\")\n\n\tassert.Equal(t, 2, len(*container.Volumes))\n\tassert.Equal(t, (*container.Volumes)[0].HostPath, \"hp1\")\n\tassert.Equal(t, (*container.Volumes)[0].ContainerPath, \"cp1\")\n\tassert.Equal(t, (*container.Volumes)[0].Mode, \"RW\")\n\tassert.Equal(t, (*container.Volumes)[1].HostPath, \"hp2\")\n\tassert.Equal(t, (*container.Volumes)[1].ContainerPath, \"cp2\")\n\tassert.Equal(t, (*container.Volumes)[1].Mode, \"R\")\n}\n\nfunc TestSecretVolume(t *testing.T) {\n\tcontainer := NewDockerApplication().Container\n\n\tcontainer.Volume(\"\", \"oldPath\", \"\")\n\n\tsv1 := (*container.Volumes)[0]\n\tassert.Equal(t, sv1.ContainerPath, \"oldPath\")\n\n\tsv1.SetSecretVolume(\"newPath\", \"some-secret\")\n\tassert.Equal(t, sv1.ContainerPath, \"newPath\")\n\tassert.Equal(t, sv1.Secret, \"some-secret\")\n}\n\nfunc TestExternalVolume(t *testing.T) {\n\tcontainer := NewDockerApplication().Container\n\n\tcontainer.Volume(\"\", \"cp\", \"RW\")\n\tev := (*container.Volumes)[0].SetExternalVolume(\"myVolume\", \"dvdi\")\n\n\tev.AddOption(\"prop\", \"pval\")\n\tev.AddOption(\"dvdi\", \"rexray\")\n\n\tev1 := (*container.Volumes)[0].External\n\tassert.Equal(t, ev1.Name, \"myVolume\")\n\tassert.Equal(t, ev1.Provider, \"dvdi\")\n\tif assert.Equal(t, len(*ev1.Options), 2) {\n\t\tassert.Equal(t, (*ev1.Options)[\"dvdi\"], \"rexray\")\n\t\tassert.Equal(t, (*ev1.Options)[\"prop\"], \"pval\")\n\t}\n\n\t// empty the external volume again\n\t(*container.Volumes)[0].EmptyExternalVolume()\n\tev2 := (*container.Volumes)[0].External\n\tassert.Equal(t, ev2.Name, \"\")\n\tassert.Equal(t, ev2.Provider, \"\")\n}\n\nfunc TestDockerPersistentVolume(t *testing.T) {\n\tdocker := NewDockerApplication()\n\tcontainer := docker.Container.Volume(\"/host\", \"/container\", \"RW\")\n\trequire.Equal(t, 1, len(*docker.Container.Volumes))\n\n\tpVol := (*container.Volumes)[0].SetPersistentVolume()\n\tpVol.SetType(PersistentVolumeTypeMount)\n\tpVol.SetSize(256)\n\tpVol.SetMaxSize(128)\n\tpVol.AddConstraint(\"cons1\", \"EQUAL\", \"tag1\")\n\tpVol.AddConstraint(\"cons2\", \"UNIQUE\")\n\n\tassert.Equal(t, 256, pVol.Size)\n\tassert.Equal(t, PersistentVolumeTypeMount, pVol.Type)\n\tassert.Equal(t, 128, pVol.MaxSize)\n\n\tif assert.NotNil(t, pVol.Constraints) {\n\t\tconstraints := *pVol.Constraints\n\t\trequire.Equal(t, 2, len(constraints))\n\t\tassert.Equal(t, []string{\"cons1\", \"EQUAL\", \"tag1\"}, constraints[0])\n\t\tassert.Equal(t, []string{\"cons2\", \"UNIQUE\"}, constraints[1])\n\t}\n\n\tpVol.EmptyConstraints()\n\tif assert.NotNil(t, pVol.Constraints) {\n\t\tassert.Empty(t, len(*pVol.Constraints))\n\t}\n}\n\nfunc TestDockerPullConfig(t *testing.T) {\n\tsecretName := \"mysecret1\"\n\tapp := NewDockerApplication()\n\tpullConfig := NewPullConfig(secretName)\n\tapp.Container.Docker.SetPullConfig(pullConfig)\n\n\tif assert.NotNil(t, app.Container.Docker.PullConfig) {\n\t\tassert.Equal(t, secretName, app.Container.Docker.PullConfig.Secret)\n\t}\n}\n"
  },
  {
    "path": "error.go",
    "content": "/*\nCopyright 2015 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n)\n\nconst (\n\t// ErrCodeBadRequest specifies a 400 Bad Request error.\n\tErrCodeBadRequest = iota\n\t// ErrCodeUnauthorized specifies a 401 Unauthorized error.\n\tErrCodeUnauthorized\n\t// ErrCodeForbidden specifies a 403 Forbidden error.\n\tErrCodeForbidden\n\t// ErrCodeNotFound specifies a 404 Not Found error.\n\tErrCodeNotFound\n\t// ErrCodeDuplicateID specifies a PUT 409 Conflict error.\n\tErrCodeDuplicateID\n\t// ErrCodeAppLocked specifies a POST 409 Conflict error.\n\tErrCodeAppLocked\n\t// ErrCodeInvalidBean specifies a 422 UnprocessableEntity error.\n\tErrCodeInvalidBean\n\t// ErrCodeServer specifies a 500+ Server error.\n\tErrCodeServer\n\t// ErrCodeUnknown specifies an unknown error.\n\tErrCodeUnknown\n\t// ErrCodeMethodNotAllowed specifies a 405 Method Not Allowed.\n\tErrCodeMethodNotAllowed\n)\n\n// InvalidEndpointError indicates a endpoint error in the marathon urls\ntype InvalidEndpointError struct {\n\tmessage string\n}\n\n// Error returns the string message\nfunc (e *InvalidEndpointError) Error() string {\n\treturn e.message\n}\n\n// newInvalidEndpointError creates a new error\nfunc newInvalidEndpointError(message string, args ...interface{}) error {\n\treturn &InvalidEndpointError{message: fmt.Sprintf(message, args...)}\n}\n\n// APIError represents a generic API error.\ntype APIError struct {\n\t// ErrCode specifies the nature of the error.\n\tErrCode int\n\tmessage string\n}\n\nfunc (e *APIError) Error() string {\n\treturn fmt.Sprintf(\"Marathon API error: %s\", e.message)\n}\n\n// NewAPIError creates a new APIError instance from the given response code and content.\nfunc NewAPIError(code int, content []byte) error {\n\tvar errDef errorDefinition\n\tswitch {\n\tcase code == http.StatusBadRequest:\n\t\terrDef = &badRequestDef{}\n\tcase code == http.StatusUnauthorized:\n\t\terrDef = &simpleErrDef{code: ErrCodeUnauthorized}\n\tcase code == http.StatusForbidden:\n\t\terrDef = &simpleErrDef{code: ErrCodeForbidden}\n\tcase code == http.StatusNotFound:\n\t\terrDef = &simpleErrDef{code: ErrCodeNotFound}\n\tcase code == http.StatusMethodNotAllowed:\n\t\terrDef = &simpleErrDef{code: ErrCodeMethodNotAllowed}\n\tcase code == http.StatusConflict:\n\t\terrDef = &conflictDef{}\n\tcase code == 422:\n\t\terrDef = &unprocessableEntityDef{}\n\tcase code >= http.StatusInternalServerError:\n\t\terrDef = &simpleErrDef{code: ErrCodeServer}\n\tdefault:\n\t\terrDef = &simpleErrDef{code: ErrCodeUnknown}\n\t}\n\n\treturn parseContent(errDef, content)\n}\n\ntype errorDefinition interface {\n\tmessage() string\n\terrCode() int\n}\n\nfunc parseContent(errDef errorDefinition, content []byte) error {\n\t// If the content cannot be JSON-unmarshalled, we assume that it's not JSON\n\t// and encode it into the APIError instance as-is.\n\terrMessage := string(content)\n\tif err := json.Unmarshal(content, errDef); err == nil {\n\t\terrMessage = errDef.message()\n\t}\n\n\treturn &APIError{message: errMessage, ErrCode: errDef.errCode()}\n}\n\ntype simpleErrDef struct {\n\tMessage string `json:\"message\"`\n\tcode    int\n}\n\nfunc (def *simpleErrDef) message() string {\n\treturn def.Message\n}\n\nfunc (def *simpleErrDef) errCode() int {\n\treturn def.code\n}\n\ntype detailDescription struct {\n\tPath   string   `json:\"path\"`\n\tErrors []string `json:\"errors\"`\n}\n\nfunc (d detailDescription) String() string {\n\treturn fmt.Sprintf(\"path: '%s' errors: %s\", d.Path, strings.Join(d.Errors, \", \"))\n}\n\ntype badRequestDef struct {\n\tMessage string              `json:\"message\"`\n\tDetails []detailDescription `json:\"details\"`\n}\n\nfunc (def *badRequestDef) message() string {\n\tvar details []string\n\tfor _, detail := range def.Details {\n\t\tdetails = append(details, detail.String())\n\t}\n\n\treturn fmt.Sprintf(\"%s (%s)\", def.Message, strings.Join(details, \"; \"))\n}\n\nfunc (def *badRequestDef) errCode() int {\n\treturn ErrCodeBadRequest\n}\n\ntype conflictDef struct {\n\tMessage     string `json:\"message\"`\n\tDeployments []struct {\n\t\tID string `json:\"id\"`\n\t} `json:\"deployments\"`\n}\n\nfunc (def *conflictDef) message() string {\n\tif len(def.Deployments) == 0 {\n\t\t// 409 Conflict response to \"POST /v2/apps\".\n\t\treturn def.Message\n\t}\n\n\t// 409 Conflict response to \"PUT /v2/apps/{appId}\".\n\tvar ids []string\n\tfor _, deployment := range def.Deployments {\n\t\tids = append(ids, deployment.ID)\n\t}\n\treturn fmt.Sprintf(\"%s (locking deployment IDs: %s)\", def.Message, strings.Join(ids, \", \"))\n}\n\nfunc (def *conflictDef) errCode() int {\n\tif len(def.Deployments) == 0 {\n\t\treturn ErrCodeDuplicateID\n\t}\n\n\treturn ErrCodeAppLocked\n}\n\ntype unprocessableEntityDetails []struct {\n\t// Used in Marathon >= 1.0.0-RC1.\n\tdetailDescription\n\t// Used in Marathon < 1.0.0-RC1.\n\tAttribute string `json:\"attribute\"`\n\tError     string `json:\"error\"`\n}\n\ntype unprocessableEntityDef struct {\n\tMessage string `json:\"message\"`\n\t// Name used in Marathon >= 0.15.0.\n\tDetails unprocessableEntityDetails `json:\"details\"`\n\t// Name used in Marathon < 0.15.0.\n\tErrors unprocessableEntityDetails `json:\"errors\"`\n}\n\nfunc (def *unprocessableEntityDef) message() string {\n\tjoinDetails := func(details unprocessableEntityDetails) []string {\n\t\tvar res []string\n\t\tfor _, detail := range details {\n\t\t\tres = append(res, fmt.Sprintf(\"attribute '%s': %s\", detail.Attribute, detail.Error))\n\t\t}\n\t\treturn res\n\t}\n\n\tvar details []string\n\tswitch {\n\tcase len(def.Errors) > 0:\n\t\tdetails = joinDetails(def.Errors)\n\tcase len(def.Details) > 0 && len(def.Details[0].Attribute) > 0:\n\t\tdetails = joinDetails(def.Details)\n\tdefault:\n\t\tfor _, detail := range def.Details {\n\t\t\tdetails = append(details, detail.detailDescription.String())\n\t\t}\n\t}\n\n\treturn fmt.Sprintf(\"%s (%s)\", def.Message, strings.Join(details, \"; \"))\n}\n\nfunc (def *unprocessableEntityDef) errCode() int {\n\treturn ErrCodeInvalidBean\n}\n"
  },
  {
    "path": "error_test.go",
    "content": "/*\nCopyright 2015 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestErrors(t *testing.T) {\n\ttests := []struct {\n\t\thttpCode   int\n\t\tnameSuffix string\n\t\terrCode    int\n\t\terrText    string\n\t\tcontent    string\n\t}{\n\t\t// 400\n\t\t{\n\t\t\thttpCode: http.StatusBadRequest,\n\t\t\terrCode:  ErrCodeBadRequest,\n\t\t\terrText:  \"Invalid JSON (path: '/id' errors: error.expected.jsstring, error.something.else; path: '/name' errors: error.not.inventive)\",\n\t\t\tcontent:  content400(),\n\t\t},\n\t\t// 401\n\t\t{\n\t\t\thttpCode: http.StatusUnauthorized,\n\t\t\terrCode:  ErrCodeUnauthorized,\n\t\t\terrText:  \"invalid username or password.\",\n\t\t\tcontent:  `{\"message\": \"invalid username or password.\"}`,\n\t\t},\n\t\t// 403\n\t\t{\n\t\t\thttpCode: http.StatusForbidden,\n\t\t\terrCode:  ErrCodeForbidden,\n\t\t\terrText:  \"Not Authorized to perform this action!\",\n\t\t\tcontent:  `{\"message\": \"Not Authorized to perform this action!\"}`,\n\t\t},\n\t\t// 404\n\t\t{\n\t\t\thttpCode: http.StatusNotFound,\n\t\t\terrCode:  ErrCodeNotFound,\n\t\t\terrText:  \"App '/not_existent' does not exist\",\n\t\t\tcontent:  `{\"message\": \"App '/not_existent' does not exist\"}`,\n\t\t},\n\t\t// 405\n\t\t{\n\t\t\thttpCode: http.StatusMethodNotAllowed,\n\t\t\terrCode:  ErrCodeMethodNotAllowed,\n\t\t\terrText:  \"\",\n\t\t\tcontent:  `{\"message\": null}`,\n\t\t},\n\t\t// 409 POST\n\t\t{\n\t\t\thttpCode:   http.StatusConflict,\n\t\t\tnameSuffix: \"POST\",\n\t\t\terrCode:    ErrCodeDuplicateID,\n\t\t\terrText:    \"An app with id [/existing_app] already exists.\",\n\t\t\tcontent:    `{\"message\": \"An app with id [/existing_app] already exists.\"}`,\n\t\t},\n\t\t// 409 PUT\n\t\t{\n\t\t\thttpCode:   http.StatusConflict,\n\t\t\tnameSuffix: \"PUT\",\n\t\t\terrCode:    ErrCodeAppLocked,\n\t\t\terrText:    \"App is locked (locking deployment IDs: 97c136bf-5a28-4821-9d94-480d9fbb01c8)\",\n\t\t\tcontent:    `{\"message\":\"App is locked\", \"deployments\": [ { \"id\": \"97c136bf-5a28-4821-9d94-480d9fbb01c8\" } ] }`,\n\t\t},\n\t\t// 422 pre-1.0 \"details\" key\n\t\t{\n\t\t\thttpCode:   422,\n\t\t\tnameSuffix: \"pre-1.0 details key\",\n\t\t\terrCode:    ErrCodeInvalidBean,\n\t\t\terrText:    \"Something is not valid (attribute 'upgradeStrategy.minimumHealthCapacity': is greater than 1; attribute 'foobar': foo does not have enough bar)\",\n\t\t\tcontent:    content422(\"details\"),\n\t\t},\n\t\t// 422 pre-1.0 \"errors\" key\n\t\t{\n\t\t\thttpCode:   422,\n\t\t\tnameSuffix: \"pre-1.0 errors key\",\n\t\t\terrCode:    ErrCodeInvalidBean,\n\t\t\terrText:    \"Something is not valid (attribute 'upgradeStrategy.minimumHealthCapacity': is greater than 1; attribute 'foobar': foo does not have enough bar)\",\n\t\t\tcontent:    content422(\"errors\"),\n\t\t},\n\t\t// 422 1.0 \"invalid object\"\n\t\t{\n\t\t\thttpCode:   422,\n\t\t\tnameSuffix: \"invalid object\",\n\t\t\terrCode:    ErrCodeInvalidBean,\n\t\t\terrText:    \"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)\",\n\t\t\tcontent:    content422V1(),\n\t\t},\n\t\t// 499 unknown error\n\t\t{\n\t\t\thttpCode:   499,\n\t\t\tnameSuffix: \"unknown error\",\n\t\t\terrCode:    ErrCodeUnknown,\n\t\t\terrText:    \"unknown error\",\n\t\t\tcontent:    `{\"message\": \"unknown error\"}`,\n\t\t},\n\t\t// 500\n\t\t{\n\t\t\thttpCode: http.StatusInternalServerError,\n\t\t\terrCode:  ErrCodeServer,\n\t\t\terrText:  \"internal server error\",\n\t\t\tcontent:  `{\"message\": \"internal server error\"}`,\n\t\t},\n\t\t// 503 (no JSON)\n\t\t{\n\t\t\thttpCode:   http.StatusServiceUnavailable,\n\t\t\tnameSuffix: \"no JSON\",\n\t\t\terrCode:    ErrCodeServer,\n\t\t\terrText:    \"No server is available to handle this request.\",\n\t\t\tcontent:    `No server is available to handle this request.`,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tname := fmt.Sprintf(\"%d\", test.httpCode)\n\t\tif len(test.nameSuffix) > 0 {\n\t\t\tname = fmt.Sprintf(\"%s (%s)\", name, test.nameSuffix)\n\t\t}\n\t\tapiErr := NewAPIError(test.httpCode, []byte(test.content))\n\t\tgotErrCode := apiErr.(*APIError).ErrCode\n\t\tassert.Equal(t, test.errCode, gotErrCode, fmt.Sprintf(\"HTTP code %s (error code): got %d, want %d\", name, gotErrCode, test.errCode))\n\t\tpureErrText := strings.TrimPrefix(apiErr.Error(), \"Marathon API error: \")\n\t\tassert.Equal(t, pureErrText, test.errText, fmt.Sprintf(\"HTTP code %s (error text)\", name))\n\t}\n}\n\nfunc content400() string {\n\treturn `{\n\t\"message\": \"Invalid JSON\",\n\t\"details\": [\n\t\t{\n\t\t\t\"path\": \"/id\",\n\t\t\t\"errors\": [\"error.expected.jsstring\", \"error.something.else\"]\n\t\t},\n\t\t{\n\t\t\t\"path\": \"/name\",\n\t\t\t\"errors\": [\"error.not.inventive\"]\n\t\t}\n\t]\n}`\n}\n\nfunc content422(detailsPropKey string) string {\n\treturn fmt.Sprintf(`{\n\t\"message\": \"Something is not valid\",\n\t\"%s\": [\n\t\t{\n\t\t\t\"attribute\": \"upgradeStrategy.minimumHealthCapacity\",\n\t\t\t\"error\": \"is greater than 1\"\n\t\t},\n\t\t{\n\t\t\t\"attribute\": \"foobar\",\n\t\t\t\"error\": \"foo does not have enough bar\"\n\t\t}\n\t]\n}`, detailsPropKey)\n}\n\nfunc content422V1() string {\n\treturn `{\n\t\"message\": \"Object is not valid\",\n\t\"details\": [\n\t\t{\n\t\t\t\"path\": \"upgradeStrategy.minimumHealthCapacity\",\n\t\t\t\"errors\": [\"is greater than 1\"]\n\t\t},\n\t\t{\n\t\t\t\"path\": \"/value\",\n\t\t\t\"errors\": [\"service port conflict app /app1\", \"service port conflict app /app2\"]\n\t\t}\n\t]\n}`\n}\n"
  },
  {
    "path": "events.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport \"fmt\"\n\n// EventType is a wrapper for a marathon event\ntype EventType struct {\n\tEventType string `json:\"eventType\"`\n}\n\nconst (\n\t// EventIDAPIRequest is the event listener ID for the corresponding event.\n\tEventIDAPIRequest = 1 << iota\n\t// EventIDStatusUpdate is the event listener ID for the corresponding event.\n\tEventIDStatusUpdate\n\t// EventIDFrameworkMessage is the event listener ID for the corresponding event.\n\tEventIDFrameworkMessage\n\t// EventIDSubscription is the event listener ID for the corresponding event.\n\tEventIDSubscription\n\t// EventIDUnsubscribed is the event listener ID for the corresponding event.\n\tEventIDUnsubscribed\n\t// EventIDStreamAttached is the event listener ID for the corresponding event.\n\tEventIDStreamAttached\n\t// EventIDStreamDetached is the event listener ID for the corresponding event.\n\tEventIDStreamDetached\n\t// EventIDAddHealthCheck is the event listener ID for the corresponding event.\n\tEventIDAddHealthCheck\n\t// EventIDRemoveHealthCheck is the event listener ID for the corresponding event.\n\tEventIDRemoveHealthCheck\n\t// EventIDFailedHealthCheck is the event listener ID for the corresponding event.\n\tEventIDFailedHealthCheck\n\t// EventIDChangedHealthCheck is the event listener ID for the corresponding event.\n\tEventIDChangedHealthCheck\n\t// EventIDGroupChangeSuccess is the event listener ID for the corresponding event.\n\tEventIDGroupChangeSuccess\n\t// EventIDGroupChangeFailed is the event listener ID for the corresponding event.\n\tEventIDGroupChangeFailed\n\t// EventIDDeploymentSuccess is the event listener ID for the corresponding event.\n\tEventIDDeploymentSuccess\n\t// EventIDDeploymentFailed is the event listener ID for the corresponding event.\n\tEventIDDeploymentFailed\n\t// EventIDDeploymentInfo is the event listener ID for the corresponding event.\n\tEventIDDeploymentInfo\n\t// EventIDDeploymentStepSuccess is the event listener ID for the corresponding event.\n\tEventIDDeploymentStepSuccess\n\t// EventIDDeploymentStepFailed is the event listener ID for the corresponding event.\n\tEventIDDeploymentStepFailed\n\t// EventIDAppTerminated is the event listener ID for the corresponding event.\n\tEventIDAppTerminated\n\t//EventIDApplications comprises all listener IDs for application events.\n\tEventIDApplications = EventIDStatusUpdate | EventIDChangedHealthCheck | EventIDFailedHealthCheck | EventIDAppTerminated\n\t//EventIDSubscriptions comprises all listener IDs for subscription events.\n\tEventIDSubscriptions = EventIDSubscription | EventIDUnsubscribed | EventIDStreamAttached | EventIDStreamDetached\n)\n\nvar (\n\teventTypesMap map[string]int\n)\n\nfunc init() {\n\teventTypesMap = map[string]int{\n\t\t\"api_post_event\":              EventIDAPIRequest,\n\t\t\"status_update_event\":         EventIDStatusUpdate,\n\t\t\"framework_message_event\":     EventIDFrameworkMessage,\n\t\t\"subscribe_event\":             EventIDSubscription,\n\t\t\"unsubscribe_event\":           EventIDUnsubscribed,\n\t\t\"event_stream_attached\":       EventIDStreamAttached,\n\t\t\"event_stream_detached\":       EventIDStreamDetached,\n\t\t\"add_health_check_event\":      EventIDAddHealthCheck,\n\t\t\"remove_health_check_event\":   EventIDRemoveHealthCheck,\n\t\t\"failed_health_check_event\":   EventIDFailedHealthCheck,\n\t\t\"health_status_changed_event\": EventIDChangedHealthCheck,\n\t\t\"group_change_success\":        EventIDGroupChangeSuccess,\n\t\t\"group_change_failed\":         EventIDGroupChangeFailed,\n\t\t\"deployment_success\":          EventIDDeploymentSuccess,\n\t\t\"deployment_failed\":           EventIDDeploymentFailed,\n\t\t\"deployment_info\":             EventIDDeploymentInfo,\n\t\t\"deployment_step_success\":     EventIDDeploymentStepSuccess,\n\t\t\"deployment_step_failure\":     EventIDDeploymentStepFailed,\n\t\t\"app_terminated_event\":        EventIDAppTerminated,\n\t}\n}\n\n//\n//  Events taken from: https://mesosphere.github.io/marathon/docs/event-bus.html\n//\n\n// Event is the definition for a event in marathon\ntype Event struct {\n\tID    int\n\tName  string\n\tEvent interface{}\n}\n\nfunc (event *Event) String() string {\n\treturn fmt.Sprintf(\"type: %s, event: %s\", event.Name, event.Event)\n}\n\n// EventsChannel is a channel to receive events upon\ntype EventsChannel chan *Event\n\n/* --- API Request --- */\n\n// EventAPIRequest describes an 'api_post_event' event.\ntype EventAPIRequest struct {\n\tEventType     string       `json:\"eventType\"`\n\tClientIP      string       `json:\"clientIp\"`\n\tTimestamp     string       `json:\"timestamp\"`\n\tURI           string       `json:\"uri\"`\n\tAppDefinition *Application `json:\"appDefinition\"`\n}\n\n/* --- Status Update --- */\n\n// EventStatusUpdate describes a 'status_update_event' event.\ntype EventStatusUpdate struct {\n\tEventType   string       `json:\"eventType\"`\n\tTimestamp   string       `json:\"timestamp,omitempty\"`\n\tSlaveID     string       `json:\"slaveId,omitempty\"`\n\tTaskID      string       `json:\"taskId\"`\n\tTaskStatus  string       `json:\"taskStatus\"`\n\tMessage     string       `json:\"message,omitempty\"`\n\tAppID       string       `json:\"appId\"`\n\tHost        string       `json:\"host\"`\n\tPorts       []int        `json:\"ports,omitempty\"`\n\tIPAddresses []*IPAddress `json:\"ipAddresses\"`\n\tVersion     string       `json:\"version,omitempty\"`\n}\n\n// EventAppTerminated describes an 'app_terminated_event' event.\ntype EventAppTerminated struct {\n\tEventType string `json:\"eventType\"`\n\tTimestamp string `json:\"timestamp,omitempty\"`\n\tAppID     string `json:\"appId\"`\n}\n\n/* --- Framework Message --- */\n\n// EventFrameworkMessage describes a 'framework_message_event' event.\ntype EventFrameworkMessage struct {\n\tEventType  string `json:\"eventType\"`\n\tExecutorID string `json:\"executorId\"`\n\tMessage    string `json:\"message\"`\n\tSlaveID    string `json:\"slaveId\"`\n\tTimestamp  string `json:\"timestamp\"`\n}\n\n/* --- Event Subscription --- */\n\n// EventSubscription describes a 'subscribe_event' event.\ntype EventSubscription struct {\n\tCallbackURL string `json:\"callbackUrl\"`\n\tClientIP    string `json:\"clientIp\"`\n\tEventType   string `json:\"eventType\"`\n\tTimestamp   string `json:\"timestamp\"`\n}\n\n// EventUnsubscription describes an 'unsubscribe_event' event.\ntype EventUnsubscription struct {\n\tCallbackURL string `json:\"callbackUrl\"`\n\tClientIP    string `json:\"clientIp\"`\n\tEventType   string `json:\"eventType\"`\n\tTimestamp   string `json:\"timestamp\"`\n}\n\n// EventStreamAttached describes an 'event_stream_attached' event.\ntype EventStreamAttached struct {\n\tRemoteAddress string `json:\"remoteAddress\"`\n\tEventType     string `json:\"eventType\"`\n\tTimestamp     string `json:\"timestamp\"`\n}\n\n// EventStreamDetached describes an 'event_stream_detached' event.\ntype EventStreamDetached struct {\n\tRemoteAddress string `json:\"remoteAddress\"`\n\tEventType     string `json:\"eventType\"`\n\tTimestamp     string `json:\"timestamp\"`\n}\n\n/* --- Health Checks --- */\n\n// EventAddHealthCheck describes an 'add_health_check_event' event.\ntype EventAddHealthCheck struct {\n\tAppID       string `json:\"appId\"`\n\tEventType   string `json:\"eventType\"`\n\tHealthCheck struct {\n\t\tGracePeriodSeconds     float64 `json:\"gracePeriodSeconds\"`\n\t\tIntervalSeconds        float64 `json:\"intervalSeconds\"`\n\t\tMaxConsecutiveFailures float64 `json:\"maxConsecutiveFailures\"`\n\t\tPath                   string  `json:\"path\"`\n\t\tPortIndex              float64 `json:\"portIndex\"`\n\t\tProtocol               string  `json:\"protocol\"`\n\t\tTimeoutSeconds         float64 `json:\"timeoutSeconds\"`\n\t} `json:\"healthCheck\"`\n\tTimestamp string `json:\"timestamp\"`\n}\n\n// EventRemoveHealthCheck describes a 'remove_health_check_event' event.\ntype EventRemoveHealthCheck struct {\n\tAppID       string `json:\"appId\"`\n\tEventType   string `json:\"eventType\"`\n\tHealthCheck struct {\n\t\tGracePeriodSeconds     float64 `json:\"gracePeriodSeconds\"`\n\t\tIntervalSeconds        float64 `json:\"intervalSeconds\"`\n\t\tMaxConsecutiveFailures float64 `json:\"maxConsecutiveFailures\"`\n\t\tPath                   string  `json:\"path\"`\n\t\tPortIndex              float64 `json:\"portIndex\"`\n\t\tProtocol               string  `json:\"protocol\"`\n\t\tTimeoutSeconds         float64 `json:\"timeoutSeconds\"`\n\t} `json:\"healthCheck\"`\n\tTimestamp string `json:\"timestamp\"`\n}\n\n// EventFailedHealthCheck describes a 'failed_health_check_event' event.\ntype EventFailedHealthCheck struct {\n\tAppID       string `json:\"appId\"`\n\tEventType   string `json:\"eventType\"`\n\tHealthCheck struct {\n\t\tGracePeriodSeconds     float64 `json:\"gracePeriodSeconds\"`\n\t\tIntervalSeconds        float64 `json:\"intervalSeconds\"`\n\t\tMaxConsecutiveFailures float64 `json:\"maxConsecutiveFailures\"`\n\t\tPath                   string  `json:\"path\"`\n\t\tPortIndex              float64 `json:\"portIndex\"`\n\t\tProtocol               string  `json:\"protocol\"`\n\t\tTimeoutSeconds         float64 `json:\"timeoutSeconds\"`\n\t} `json:\"healthCheck\"`\n\tTimestamp string `json:\"timestamp\"`\n}\n\n// EventHealthCheckChanged describes a 'health_status_changed_event' event.\ntype EventHealthCheckChanged struct {\n\tEventType string `json:\"eventType\"`\n\tTimestamp string `json:\"timestamp,omitempty\"`\n\tAppID     string `json:\"appId\"`\n\tTaskID    string `json:\"taskId\"`\n\tVersion   string `json:\"version,omitempty\"`\n\tAlive     bool   `json:\"alive\"`\n}\n\n/* --- Deployments --- */\n\n// EventGroupChangeSuccess describes a 'group_change_success' event.\ntype EventGroupChangeSuccess struct {\n\tEventType string `json:\"eventType\"`\n\tGroupID   string `json:\"groupId\"`\n\tTimestamp string `json:\"timestamp\"`\n\tVersion   string `json:\"version\"`\n}\n\n// EventGroupChangeFailed describes a 'group_change_failed' event.\ntype EventGroupChangeFailed struct {\n\tEventType string `json:\"eventType\"`\n\tGroupID   string `json:\"groupId\"`\n\tTimestamp string `json:\"timestamp\"`\n\tVersion   string `json:\"version\"`\n\tReason    string `json:\"reason\"`\n}\n\n// EventDeploymentSuccess describes a 'deployment_success' event.\ntype EventDeploymentSuccess struct {\n\tID        string          `json:\"id\"`\n\tEventType string          `json:\"eventType\"`\n\tTimestamp string          `json:\"timestamp\"`\n\tPlan      *DeploymentPlan `json:\"plan\"`\n}\n\n// EventDeploymentFailed describes a 'deployment_failed' event.\ntype EventDeploymentFailed struct {\n\tID        string `json:\"id\"`\n\tEventType string `json:\"eventType\"`\n\tTimestamp string `json:\"timestamp\"`\n}\n\n// EventDeploymentInfo describes a 'deployment_info' event.\ntype EventDeploymentInfo struct {\n\tEventType   string          `json:\"eventType\"`\n\tCurrentStep *StepActions    `json:\"currentStep\"`\n\tTimestamp   string          `json:\"timestamp\"`\n\tPlan        *DeploymentPlan `json:\"plan\"`\n}\n\n// EventDeploymentStepSuccess describes a 'deployment_step_success' event.\ntype EventDeploymentStepSuccess struct {\n\tEventType   string          `json:\"eventType\"`\n\tCurrentStep *StepActions    `json:\"currentStep\"`\n\tTimestamp   string          `json:\"timestamp\"`\n\tPlan        *DeploymentPlan `json:\"plan\"`\n}\n\n// EventDeploymentStepFailure describes a 'deployment_step_failure' event.\ntype EventDeploymentStepFailure struct {\n\tEventType   string          `json:\"eventType\"`\n\tCurrentStep *StepActions    `json:\"currentStep\"`\n\tTimestamp   string          `json:\"timestamp\"`\n\tPlan        *DeploymentPlan `json:\"plan\"`\n}\n\n// GetEvent returns allocated empty event object which corresponds to provided event type\n//\t\teventType:\t\t\tthe type of Marathon event\nfunc GetEvent(eventType string) (*Event, error) {\n\t// step: check it's supported\n\tid, found := eventTypesMap[eventType]\n\tif found {\n\t\tevent := new(Event)\n\t\tevent.ID = id\n\t\tevent.Name = eventType\n\t\tswitch eventType {\n\t\tcase \"api_post_event\":\n\t\t\tevent.Event = new(EventAPIRequest)\n\t\tcase \"status_update_event\":\n\t\t\tevent.Event = new(EventStatusUpdate)\n\t\tcase \"framework_message_event\":\n\t\t\tevent.Event = new(EventFrameworkMessage)\n\t\tcase \"subscribe_event\":\n\t\t\tevent.Event = new(EventSubscription)\n\t\tcase \"unsubscribe_event\":\n\t\t\tevent.Event = new(EventUnsubscription)\n\t\tcase \"event_stream_attached\":\n\t\t\tevent.Event = new(EventStreamAttached)\n\t\tcase \"event_stream_detached\":\n\t\t\tevent.Event = new(EventStreamDetached)\n\t\tcase \"add_health_check_event\":\n\t\t\tevent.Event = new(EventAddHealthCheck)\n\t\tcase \"remove_health_check_event\":\n\t\t\tevent.Event = new(EventRemoveHealthCheck)\n\t\tcase \"failed_health_check_event\":\n\t\t\tevent.Event = new(EventFailedHealthCheck)\n\t\tcase \"health_status_changed_event\":\n\t\t\tevent.Event = new(EventHealthCheckChanged)\n\t\tcase \"group_change_success\":\n\t\t\tevent.Event = new(EventGroupChangeSuccess)\n\t\tcase \"group_change_failed\":\n\t\t\tevent.Event = new(EventGroupChangeFailed)\n\t\tcase \"deployment_success\":\n\t\t\tevent.Event = new(EventDeploymentSuccess)\n\t\tcase \"deployment_failed\":\n\t\t\tevent.Event = new(EventDeploymentFailed)\n\t\tcase \"deployment_info\":\n\t\t\tevent.Event = new(EventDeploymentInfo)\n\t\tcase \"deployment_step_success\":\n\t\t\tevent.Event = new(EventDeploymentStepSuccess)\n\t\tcase \"deployment_step_failure\":\n\t\t\tevent.Event = new(EventDeploymentStepFailure)\n\t\tcase \"app_terminated_event\":\n\t\t\tevent.Event = new(EventAppTerminated)\n\t\t}\n\t\treturn event, nil\n\t}\n\n\treturn nil, fmt.Errorf(\"the event type: %s was not found or supported\", eventType)\n}\n"
  },
  {
    "path": "examples/Makefile",
    "content": "all:\n\tfind * -type d -exec bash -exc \"cd {}; go build . || kill $${PPID}\" \\;\n"
  },
  {
    "path": "examples/applications/main.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage main\n\nimport (\n\t\"flag\"\n\t\"log\"\n\t\"time\"\n\n\tmarathon \"github.com/gambol99/go-marathon\"\n)\n\nvar marathonURL string\n\nfunc init() {\n\tflag.StringVar(&marathonURL, \"url\", \"http://127.0.0.1:8080\", \"the url for the marathon endpoint\")\n}\n\nfunc assert(err error) {\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed, error: %s\", err)\n\t}\n}\n\nfunc waitOnDeployment(client marathon.Marathon, id *marathon.DeploymentID) {\n\tassert(client.WaitOnDeployment(id.DeploymentID, 0))\n}\n\nfunc main() {\n\tflag.Parse()\n\tconfig := marathon.NewDefaultConfig()\n\tconfig.URL = marathonURL\n\tclient, err := marathon.NewClient(config)\n\tassert(err)\n\tapplications, err := client.Applications(nil)\n\tassert(err)\n\n\tlog.Printf(\"Found %d application running\", len(applications.Apps))\n\tfor _, application := range applications.Apps {\n\t\tlog.Printf(\"Application: %v\", application)\n\t\tdetails, err := client.Application(application.ID)\n\t\tassert(err)\n\t\tif details.Tasks != nil && len(details.Tasks) > 0 {\n\t\t\tfor _, task := range details.Tasks {\n\t\t\t\tlog.Printf(\"task: %v\", task)\n\t\t\t}\n\t\t\thealth, err := client.ApplicationOK(details.ID)\n\t\t\tassert(err)\n\t\t\tlog.Printf(\"Application: %s, healthy: %t\", details.ID, health)\n\t\t}\n\t}\n\n\tapplicationName := \"/my/product\"\n\n\tif _, err := client.Application(applicationName); err == nil {\n\t\tdeployID, err := client.DeleteApplication(applicationName, false)\n\t\tassert(err)\n\t\twaitOnDeployment(client, deployID)\n\t}\n\n\tlog.Printf(\"Deploying a new application\")\n\tapplication := marathon.NewDockerApplication().\n\t\tName(applicationName).\n\t\tCPU(0.1).\n\t\tMemory(64).\n\t\tStorage(0.0).\n\t\tCount(2).\n\t\tAddArgs(\"/usr/sbin/apache2ctl\", \"-D\", \"FOREGROUND\").\n\t\tAddEnv(\"NAME\", \"frontend_http\").\n\t\tAddEnv(\"SERVICE_80_NAME\", \"test_http\")\n\n\tapplication.\n\t\tContainer.Docker.Container(\"quay.io/gambol99/apache-php:latest\").\n\t\tBridged().\n\t\tExpose(80).\n\t\tExpose(443)\n\n\t*application.RequirePorts = true\n\t_, err = client.CreateApplication(application)\n\tassert(err)\n\tclient.WaitOnApplication(application.ID, 30*time.Second)\n\n\tlog.Printf(\"Scaling the application to 4 instances\")\n\tdeployID, err := client.ScaleApplicationInstances(application.ID, 4, false)\n\tassert(err)\n\tclient.WaitOnApplication(application.ID, 30*time.Second)\n\tlog.Printf(\"Successfully scaled the application, deployID: %s\", deployID.DeploymentID)\n\n\tlog.Printf(\"Deleting the application: %s\", applicationName)\n\tdeployID, err = client.DeleteApplication(application.ID, true)\n\tassert(err)\n\twaitOnDeployment(client, deployID)\n\tlog.Printf(\"Successfully deleted the application\")\n\n\tlog.Printf(\"Starting the application again\")\n\t_, err = client.CreateApplication(application)\n\tassert(err)\n\tlog.Printf(\"Created the application: %s\", application.ID)\n\n\tlog.Printf(\"Delete all the tasks\")\n\t_, err = client.KillApplicationTasks(application.ID, nil)\n\tassert(err)\n}\n"
  },
  {
    "path": "examples/docker-compose.yml",
    "content": "# Based on https://github.com/meltwater/docker-mesos\n\nzookeeper:\n  image: mesoscloud/zookeeper:3.4.6-centos-7\n  ports:\n    - \"2181:2181\"\n    - \"2888:2888\"\n    - \"3888:3888\"\n  environment:\n    SERVERS: server.1=127.0.0.1\n    MYID: 1\n\nmesosmaster:\n  image: mesoscloud/mesos-master:0.24.1-centos-7\n  net: host\n  environment:\n    MESOS_ZK: zk://localhost:2181/mesos\n    MESOS_QUORUM: 1\n    MESOS_CLUSTER: local\n    MESOS_HOSTNAME: localhost\n\nmesosslave:\n  image: mesoscloud/mesos-slave:0.24.1-centos-7\n  net: host\n  privileged: true\n  volumes:\n    - /sys:/sys\n# /cgroup is needed on some older Linux versions\n#    - /cgroup:/cgroup\n# /usr/bin/docker is needed if you're running an older docker version\n#    - /usr/local/bin/docker:/usr/bin/docker:r\n    - /var/run/docker.sock:/var/run/docker.sock:rw\n  environment:\n    MESOS_MASTER: zk://localhost:2181/mesos\n    MESOS_EXECUTOR_SHUTDOWN_GRACE_PERIOD: 90secs\n    MESOS_DOCKER_STOP_TIMEOUT: 60secs\n# If your workstation doesn't have a resolvable hostname/FQDN then $MESOS_HOSTNAME needs to be set to its IP-address\n#    MESOS_HOSTNAME: 192.168.178.39\n\nmarathon:\n  image: mesoscloud/marathon:0.11.0-centos-7\n  net: host\n  environment:\n    MARATHON_ZK: zk://localhost:2181/marathon\n    MARATHON_MASTER: zk://localhost:2181/mesos\n    MARATHON_EVENT_SUBSCRIBER: http_callback\n    MARATHON_TASK_LAUNCH_TIMEOUT: 300000\n"
  },
  {
    "path": "examples/events_callback_transport/main.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage main\n\nimport (\n\t\"flag\"\n\t\"log\"\n\t\"time\"\n\n\tmarathon \"github.com/gambol99/go-marathon\"\n)\n\nvar marathonURL string\nvar marathonInterface string\nvar marathonPort int\nvar timeout int\n\nfunc init() {\n\tflag.StringVar(&marathonURL, \"url\", \"http://127.0.0.1:8080\", \"the url for the Marathon endpoint\")\n\tflag.StringVar(&marathonInterface, \"interface\", \"eth0\", \"the interface we should use for events\")\n\tflag.IntVar(&marathonPort, \"port\", 19999, \"the port the events service should run on\")\n\tflag.IntVar(&timeout, \"timeout\", 60, \"listen to events for x seconds\")\n}\n\nfunc assert(err error) {\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed, error: %s\", err)\n\t}\n}\n\nfunc main() {\n\tflag.Parse()\n\tconfig := marathon.NewDefaultConfig()\n\tconfig.URL = marathonURL\n\tconfig.EventsInterface = marathonInterface\n\tconfig.EventsPort = marathonPort\n\tlog.Printf(\"Creating a client, Marathon: %s\", marathonURL)\n\n\tclient, err := marathon.NewClient(config)\n\tassert(err)\n\n\t// Register for events\n\tevents, err := client.AddEventsListener(marathon.EventIDApplications)\n\tassert(err)\n\tdeployments, err := client.AddEventsListener(marathon.EventIDDeploymentStepSuccess)\n\tassert(err)\n\n\t// Listen for x seconds and then split\n\ttimer := time.After(time.Duration(timeout) * time.Second)\n\tdone := false\n\tfor {\n\t\tif done {\n\t\t\tbreak\n\t\t}\n\t\tselect {\n\t\tcase <-timer:\n\t\t\tlog.Printf(\"Exiting the loop\")\n\t\t\tdone = true\n\t\tcase event := <-events:\n\t\t\tlog.Printf(\"Received application event: %s\", event)\n\t\tcase event := <-deployments:\n\t\t\tlog.Printf(\"Received deployment event: %v\", event)\n\t\t\tvar deployment *marathon.EventDeploymentStepSuccess\n\t\t\tdeployment = event.Event.(*marathon.EventDeploymentStepSuccess)\n\t\t\tlog.Printf(\"deployment step: %v\", deployment.CurrentStep)\n\t\t}\n\t}\n\n\tlog.Printf(\"Removing our subscription\")\n\tclient.RemoveEventsListener(events)\n\tclient.RemoveEventsListener(deployments)\n}\n"
  },
  {
    "path": "examples/events_sse_transport/main.go",
    "content": "/*\nCopyright 2015 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage main\n\nimport (\n\t\"flag\"\n\t\"log\"\n\t\"time\"\n\n\tmarathon \"github.com/gambol99/go-marathon\"\n)\n\nvar marathonURL string\nvar timeout int\n\nfunc init() {\n\tflag.StringVar(&marathonURL, \"url\", \"http://127.0.0.1:8080\", \"the url for the Marathon endpoint\")\n\tflag.IntVar(&timeout, \"timeout\", 60, \"listen to events for x seconds\")\n}\n\nfunc assert(err error) {\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed, error: %s\", err)\n\t}\n}\n\nfunc main() {\n\tflag.Parse()\n\tconfig := marathon.NewDefaultConfig()\n\tconfig.URL = marathonURL\n\tconfig.EventsTransport = marathon.EventsTransportSSE\n\tlog.Printf(\"Creating a client, Marathon: %s\", marathonURL)\n\n\tclient, err := marathon.NewClient(config)\n\tassert(err)\n\n\t// Register for events\n\tevents, err := client.AddEventsListener(marathon.EventIDApplications)\n\tassert(err)\n\tdeployments, err := client.AddEventsListener(marathon.EventIDDeploymentStepSuccess)\n\tassert(err)\n\n\t// Listen for x seconds and then split\n\ttimer := time.After(time.Duration(timeout) * time.Second)\n\tdone := false\n\tfor {\n\t\tif done {\n\t\t\tbreak\n\t\t}\n\t\tselect {\n\t\tcase <-timer:\n\t\t\tlog.Printf(\"Exiting the loop\")\n\t\t\tdone = true\n\t\tcase event := <-events:\n\t\t\tlog.Printf(\"Received application event: %s\", event)\n\t\tcase event := <-deployments:\n\t\t\tlog.Printf(\"Received deployment event: %v\", event)\n\t\t\tvar deployment *marathon.EventDeploymentStepSuccess\n\t\t\tdeployment = event.Event.(*marathon.EventDeploymentStepSuccess)\n\t\t\tlog.Printf(\"deployment step: %v\", deployment.CurrentStep)\n\t\t}\n\t}\n\n\tlog.Printf(\"Removing our subscription\")\n\tclient.RemoveEventsListener(events)\n\tclient.RemoveEventsListener(deployments)\n}\n"
  },
  {
    "path": "examples/glog/main.go",
    "content": "/*\nCopyright 2015 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// go run main.go -logtostderr\n\npackage main\n\nimport (\n\t\"flag\"\n\n\tmarathon \"github.com/gambol99/go-marathon\"\n\t\"github.com/golang/glog\"\n)\n\nvar marathonURL string\n\ntype logBridge struct{}\n\nfunc (l *logBridge) Write(b []byte) (n int, err error) {\n\tglog.InfoDepth(3, \"go-marathon: \"+string(b))\n\treturn len(b), nil\n}\n\nfunc init() {\n\tflag.StringVar(&marathonURL, \"url\", \"http://127.0.0.1:8080\", \"the url for the marathon endpoint\")\n}\n\nfunc main() {\n\tflag.Parse()\n\tconfig := marathon.NewDefaultConfig()\n\tconfig.URL = marathonURL\n\tconfig.LogOutput = new(logBridge)\n\tclient, err := marathon.NewClient(config)\n\tif err != nil {\n\t\tglog.Exitln(err)\n\t}\n\n\tapplications, err := client.Applications(nil)\n\tif err != nil {\n\t\tglog.Exitln(err)\n\t}\n\n\tfor _, a := range applications.Apps {\n\t\tglog.Infof(\"App ID: %v\\n\", a.ID)\n\t}\n}\n"
  },
  {
    "path": "examples/groups/main.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage main\n\nimport (\n\t\"flag\"\n\t\"log\"\n\t\"time\"\n\n\tmarathon \"github.com/gambol99/go-marathon\"\n)\n\nvar marathonURL string\n\nfunc init() {\n\tflag.StringVar(&marathonURL, \"url\", \"http://127.0.0.1:8080\", \"the url for the marathon endpoint\")\n}\n\nfunc assert(err error) {\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed, error: %s\", err)\n\t}\n}\n\nfunc main() {\n\tflag.Parse()\n\tconfig := marathon.NewDefaultConfig()\n\tconfig.URL = marathonURL\n\tclient, err := marathon.NewClient(config)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to create a client for marathon, error: %s\", err)\n\t}\n\n\tlog.Printf(\"Retrieving a list of groups\")\n\tif groups, err := client.Groups(); err != nil {\n\t\tlog.Fatalf(\"Failed to retrieve the groups from maratho, error: %s\", err)\n\t} else {\n\t\tfor _, group := range groups.Groups {\n\t\t\tlog.Printf(\"Found group: %s\", group.ID)\n\t\t}\n\t}\n\n\tgroupName := \"/product/group\"\n\n\tfound, err := client.HasGroup(groupName)\n\tassert(err)\n\tif found {\n\t\tlog.Printf(\"Deleting the group: %s, as it already exists\", groupName)\n\t\tid, err := client.DeleteGroup(groupName, true)\n\t\tassert(err)\n\t\terr = client.WaitOnDeployment(id.DeploymentID, 0)\n\t\tassert(err)\n\t}\n\n\t/* step: the frontend app */\n\tfrontend := marathon.NewDockerApplication()\n\tfrontend.Name(\"/product/group/frontend\")\n\tfrontend.CPU(0.1).Memory(64).Storage(0.0).Count(2)\n\tfrontend.AddArgs(\"/usr/sbin/apache2ctl\", \"-D\", \"FOREGROUND\")\n\tfrontend.AddEnv(\"NAME\", \"frontend_http\")\n\tfrontend.AddEnv(\"SERVICE_80_NAME\", \"frontend_http\")\n\tfrontend.AddEnv(\"SERVICE_443_NAME\", \"frontend_https\")\n\tfrontend.AddEnv(\"BACKEND_MYSQL\", \"/product/group/mysql/3306;3306\")\n\tfrontend.AddEnv(\"BACKEND_CACHE\", \"/product/group/cache/6379;6379\")\n\tfrontend.DependsOn(\"/product/group/cache\")\n\tfrontend.DependsOn(\"/product/group/mysql\")\n\tfrontend.Container.Docker.Container(\"quay.io/gambol99/apache-php:latest\").Expose(80).Expose(443)\n\t_, err = frontend.CheckHTTP(\"/hostname.php\", 80, 10)\n\tassert(err)\n\n\tmysql := marathon.NewDockerApplication()\n\tmysql.Name(\"/product/group/mysql\")\n\tmysql.CPU(0.1).Memory(128).Storage(0.0).Count(1)\n\tmysql.AddEnv(\"NAME\", \"group_cache\")\n\tmysql.AddEnv(\"SERVICE_3306_NAME\", \"mysql\")\n\tmysql.AddEnv(\"MYSQL_PASS\", \"mysql\")\n\tmysql.Container.Docker.Container(\"tutum/mysql\").Expose(3306)\n\t_, err = mysql.CheckTCP(3306, 10)\n\tassert(err)\n\n\tredis := marathon.NewDockerApplication()\n\tredis.Name(\"/product/group/cache\")\n\tredis.CPU(0.1).Memory(64).Storage(0.0).Count(2)\n\tredis.AddEnv(\"NAME\", \"group_cache\")\n\tredis.AddEnv(\"SERVICE_6379_NAME\", \"redis\")\n\tredis.Container.Docker.Container(\"redis:latest\").Expose(6379)\n\t_, err = redis.CheckTCP(6379, 10)\n\tassert(err)\n\n\tgroup := marathon.NewApplicationGroup(groupName)\n\tgroup.App(frontend).App(redis).App(mysql)\n\n\tassert(client.CreateGroup(group))\n\tlog.Printf(\"Successfully created the group: %s\", group.ID)\n\n\tlog.Printf(\"Updating the group parameters\")\n\tfrontend.Count(4)\n\n\tid, err := client.UpdateGroup(groupName, group, true)\n\tassert(err)\n\tlog.Printf(\"Successfully updated the group: %s, version: %s\", group.ID, id.DeploymentID)\n\tassert(client.WaitOnGroup(groupName, 500*time.Second))\n}\n"
  },
  {
    "path": "examples/multiple_endpoints/main.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage main\n\nimport (\n\t\"flag\"\n\t\"log\"\n\t\"time\"\n\n\tmarathon \"github.com/gambol99/go-marathon\"\n)\n\nconst waitTime = 5 * time.Second\n\nvar marathonURL string\n\nfunc init() {\n\tflag.StringVar(&marathonURL, \"url\", \"http://127.0.0.1:8080,127.0.0.1:8080\", \"the url for the marathon endpoint\")\n}\n\nfunc main() {\n\tflag.Parse()\n\tconfig := marathon.NewDefaultConfig()\n\tconfig.URL = marathonURL\n\tclient, err := marathon.NewClient(config)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to create a client for marathon, error: %s\", err)\n\t}\n\tfor {\n\t\tif application, err := client.Applications(nil); err != nil {\n\t\t\tlog.Fatalf(\"Failed to retrieve a list of applications, error: %s\", err)\n\t\t} else {\n\t\t\tlog.Printf(\"Retrieved a list of applications, %v\", application)\n\t\t}\n\t\tlog.Printf(\"Going to sleep for %s\\n\", waitTime)\n\t\ttime.Sleep(waitTime)\n\t}\n}\n"
  },
  {
    "path": "examples/pods/main.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"time\"\n\n\tmarathon \"github.com/gambol99/go-marathon\"\n)\n\nvar marathonURL string\nvar dcosToken string\n\nfunc init() {\n\tflag.StringVar(&marathonURL, \"url\", \"http://127.0.0.1:8080\", \"the url for the marathon endpoint\")\n\tflag.StringVar(&dcosToken, \"token\", \"\", \"DCOS token for auth\")\n}\n\nfunc assert(err error) {\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed, error: %s\", err)\n\t}\n}\n\nfunc waitOnDeployment(client marathon.Marathon, id *marathon.DeploymentID) {\n\tassert(client.WaitOnDeployment(id.DeploymentID, 0))\n}\n\nfunc createRawPod() *marathon.Pod {\n\tvar containers []*marathon.PodContainer\n\tfor i := 0; i < 2; i++ {\n\t\tcontainer := &marathon.PodContainer{\n\t\t\tName: fmt.Sprintf(\"container%d\", i),\n\t\t\tExec: &marathon.PodExec{\n\t\t\t\tCommand: marathon.PodCommand{\n\t\t\t\t\tShell: \"echo Hello World && sleep 600\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tImage: &marathon.PodContainerImage{\n\t\t\t\tKind:      \"DOCKER\",\n\t\t\t\tID:        \"nginx\",\n\t\t\t\tForcePull: marathon.Bool(true),\n\t\t\t},\n\t\t\tVolumeMounts: []*marathon.PodVolumeMount{\n\t\t\t\t&marathon.PodVolumeMount{\n\t\t\t\t\tName:      \"sharedvolume\",\n\t\t\t\t\tMountPath: \"/peers\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tResources: &marathon.Resources{\n\t\t\t\tCpus: 0.1,\n\t\t\t\tMem:  128,\n\t\t\t},\n\t\t\tEnv: map[string]string{\n\t\t\t\t\"key\": \"value\",\n\t\t\t},\n\t\t}\n\n\t\tcontainers = append(containers, container)\n\t}\n\n\tpod := &marathon.Pod{\n\t\tID:         \"/mypod\",\n\t\tContainers: containers,\n\t\tScaling: &marathon.PodScalingPolicy{\n\t\t\tKind:      \"fixed\",\n\t\t\tInstances: 2,\n\t\t},\n\t\tVolumes: []*marathon.PodVolume{\n\t\t\t&marathon.PodVolume{\n\t\t\t\tName: \"sharedvolume\",\n\t\t\t\tHost: \"/tmp\",\n\t\t\t},\n\t\t},\n\t}\n\n\treturn pod\n}\n\nfunc createConveniencePod() *marathon.Pod {\n\tpod := marathon.NewPod()\n\n\tpod.Name(\"mypod\").\n\t\tCount(2).\n\t\tAddVolume(marathon.NewPodVolume(\"sharedvolume\", \"/tmp\"))\n\n\tfor i := 0; i < 2; i++ {\n\t\timage := marathon.NewDockerPodContainerImage().\n\t\t\tSetID(\"nginx\")\n\n\t\tcontainer := marathon.NewPodContainer().\n\t\t\tSetName(fmt.Sprintf(\"container%d\", i)).\n\t\t\tCPUs(0.1).\n\t\t\tMemory(128).\n\t\t\tSetImage(image).\n\t\t\tAddEnv(\"key\", \"value\").\n\t\t\tAddVolumeMount(marathon.NewPodVolumeMount(\"sharedvolume\", \"/peers\")).\n\t\t\tSetCommand(\"echo Hello World && sleep 600\")\n\n\t\tpod.AddContainer(container)\n\t}\n\n\treturn pod\n}\n\nfunc doPlayground(client marathon.Marathon, pod *marathon.Pod) {\n\t// Create a pod\n\tfmt.Println(\"Creating pod...\")\n\tpod, err := client.CreatePod(pod)\n\tassert(err)\n\n\t// Check its status\n\tfmt.Println(\"Waiting on pod...\")\n\terr = client.WaitOnPod(pod.ID, time.Minute*1)\n\tassert(err)\n\n\t// Scale it\n\tfmt.Println(\"Scaling pod...\")\n\tpod.Count(5)\n\tpod, err = client.UpdatePod(pod, true)\n\tassert(err)\n\n\t// Get instances\n\tstatus, err := client.PodStatus(pod.ID)\n\tfmt.Printf(\"Pod status: %s\\n\", status.Status)\n\tassert(err)\n\n\t// Kill an instance\n\tfmt.Println(\"Deleting an instance...\")\n\t_, err = client.DeletePodInstance(pod.ID, status.Instances[0].ID)\n\tassert(err)\n\n\t// Delete it\n\tfmt.Println(\"Deleting the pod\")\n\tid, err := client.DeletePod(pod.ID, true)\n\tassert(err)\n\n\twaitOnDeployment(client, id)\n}\n\nfunc main() {\n\tflag.Parse()\n\tconfig := marathon.NewDefaultConfig()\n\tconfig.URL = marathonURL\n\tconfig.DCOSToken = dcosToken\n\tclient, err := marathon.NewClient(config)\n\tassert(err)\n\n\tfmt.Println(\"Convenience Pods:\")\n\tpodC := createConveniencePod()\n\tdoPlayground(client, podC)\n\n\tfmt.Println(\"Raw Pods:\")\n\tpodR := createRawPod()\n\tdoPlayground(client, podR)\n}\n"
  },
  {
    "path": "examples/queue/main.go",
    "content": "/*\nCopyright 2016 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"time\"\n\n\tmarathon \"github.com/gambol99/go-marathon\"\n)\n\nvar marathonURL string\n\nfunc init() {\n\tflag.StringVar(&marathonURL, \"url\", \"http://127.0.0.1:8080\", \"the url for the marathon endpoint\")\n}\n\nfunc main() {\n\tconfig := marathon.NewDefaultConfig()\n\tconfig.URL = marathonURL\n\tclient, err := marathon.NewClient(config)\n\tif err != nil {\n\t\tlog.Fatalf(\"Make new marathon client error: %v\", err)\n\t}\n\n\tapp := marathon.Application{}\n\tapp.ID = \"queue-test\"\n\tapp.Command(\"sleep 5\")\n\tapp.Count(1)\n\tapp.Memory(32)\n\tfmt.Println(\"Creating/updating app.\")\n\t// Update application will either create or update the app.\n\t_, err = client.UpdateApplication(&app, false)\n\tif err != nil {\n\t\tlog.Fatalf(\"Update application error: %v\", err)\n\t}\n\t// wait until marathon will launch tasks\n\terr = client.WaitOnApplication(app.ID, 10*time.Second)\n\tif err != nil {\n\t\tlog.Fatalln(\"Application deploy failure, timeout.\")\n\t}\n\tfmt.Println(\"Application was deployed.\")\n\n\t// get marathon queue by chance\n\tfor i := 0; i < 30; i++ {\n\t\t// Avoid shadowing err from outer scope.\n\t\tvar queue *marathon.Queue\n\t\tqueue, err = client.Queue()\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Get queue error: %v\\n\", err)\n\t\t}\n\t\tif len(queue.Items) > 0 {\n\t\t\tfmt.Println(queue)\n\t\t\tbreak\n\t\t}\n\t\tfmt.Printf(\"Queue is blank now, retry(%d)...\\n\", 30-i)\n\t\ttime.Sleep(time.Second)\n\t}\n\n\t// delete marathon queue delay\n\terr = client.DeleteQueueDelay(app.ID)\n\tif err != nil {\n\t\tlog.Fatalf(\"Delete queue delay error: %v\\n\", err)\n\t}\n\tfmt.Println(\"Queue delay deleted.\")\n\n\treturn\n}\n"
  },
  {
    "path": "examples/tasks/main.go",
    "content": "/*\nCopyright 2015 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage main\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\tmarathon \"github.com/gambol99/go-marathon\"\n)\n\nconst marathonURL = \"http://127.0.0.1:8080\"\n\nfunc main() {\n\tconfig := marathon.NewDefaultConfig()\n\tconfig.URL = marathonURL\n\tclient, err := marathon.NewClient(config)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tapp := marathon.Application{}\n\tapp.ID = \"tasks-test\"\n\tapp.Command(\"sleep 60\")\n\tapp.Count(3)\n\tfmt.Println(\"Creating app.\")\n\t// Update application will either create or update the app.\n\t_, err = client.UpdateApplication(&app, false)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// wait until marathon will launch tasks\n\tclient.WaitOnApplication(app.ID, 10*time.Second)\n\tfmt.Println(\"Tasks were deployed.\")\n\n\ttasks, err := client.Tasks(app.ID)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\thost := tasks.Tasks[0].Host\n\tfmt.Printf(\"Killing tasks on the host: %s\\n\", host)\n\n\t_, err = client.KillApplicationTasks(app.ID, &marathon.KillApplicationTasksOpts{Scale: true, Host: host})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n"
  },
  {
    "path": "group.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\n// Group is a marathon application group\ntype Group struct {\n\tID           string         `json:\"id\"`\n\tApps         []*Application `json:\"apps\"`\n\tDependencies []string       `json:\"dependencies\"`\n\tGroups       []*Group       `json:\"groups\"`\n}\n\n// Groups is a collection of marathon application groups\ntype Groups struct {\n\tID           string         `json:\"id\"`\n\tApps         []*Application `json:\"apps\"`\n\tDependencies []string       `json:\"dependencies\"`\n\tGroups       []*Group       `json:\"groups\"`\n}\n\n// GetGroupOpts contains a payload for Group and Groups method\n//\t\tembed:\t\tEmbeds nested resources that match the supplied path.\n// \t\t\t\t\tYou can specify this parameter multiple times with different values\ntype GetGroupOpts struct {\n\tEmbed []string `url:\"embed,omitempty\"`\n}\n\n// DeleteGroupOpts contains a payload for DeleteGroup method\n//\t\tforce:\t\toverrides a currently running deployment.\ntype DeleteGroupOpts struct {\n\tForce bool `url:\"force,omitempty\"`\n}\n\n// UpdateGroupOpts contains a payload for UpdateGroup method\n//\t\tforce:\t\toverrides a currently running deployment.\ntype UpdateGroupOpts struct {\n\tForce bool `url:\"force,omitempty\"`\n}\n\n// NewApplicationGroup create a new application group\n//\t\tname:\t\t\tthe name of the group\nfunc NewApplicationGroup(name string) *Group {\n\treturn &Group{\n\t\tID:           name,\n\t\tApps:         make([]*Application, 0),\n\t\tDependencies: make([]string, 0),\n\t\tGroups:       make([]*Group, 0),\n\t}\n}\n\n// Name sets the name of the group\n// \t\tname:\tthe name of the group\nfunc (r *Group) Name(name string) *Group {\n\tr.ID = validateID(name)\n\treturn r\n}\n\n// App add a application to the group in question\n// \t\tapplication:\ta pointer to the Application\nfunc (r *Group) App(application *Application) *Group {\n\tif r.Apps == nil {\n\t\tr.Apps = make([]*Application, 0)\n\t}\n\tr.Apps = append(r.Apps, application)\n\treturn r\n}\n\n// Groups retrieves a list of all the groups from marathon\nfunc (r *marathonClient) Groups() (*Groups, error) {\n\tgroups := new(Groups)\n\tif err := r.apiGet(marathonAPIGroups, \"\", groups); err != nil {\n\t\treturn nil, err\n\t}\n\treturn groups, nil\n}\n\n// Group retrieves the configuration of a specific group from marathon\n//\t\tname:\t\t\tthe identifier for the group\nfunc (r *marathonClient) Group(name string) (*Group, error) {\n\tgroup := new(Group)\n\tif err := r.apiGet(fmt.Sprintf(\"%s/%s\", marathonAPIGroups, trimRootPath(name)), nil, group); err != nil {\n\t\treturn nil, err\n\t}\n\treturn group, nil\n}\n\n// GroupsBy retrieves a list of all the groups from marathon by embed options\n//\t\topts:\t\tGetGroupOpts request payload\nfunc (r *marathonClient) GroupsBy(opts *GetGroupOpts) (*Groups, error) {\n\tpath, err := addOptions(marathonAPIGroups, opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgroups := new(Groups)\n\tif err := r.apiGet(path, \"\", groups); err != nil {\n\t\treturn nil, err\n\t}\n\treturn groups, nil\n}\n\n// GroupBy retrieves the configuration of a specific group from marathon\n//\t\tname:\t\t\tthe identifier for the group\n//\t\topts:\t\t\tGetGroupOpts request payload\nfunc (r *marathonClient) GroupBy(name string, opts *GetGroupOpts) (*Group, error) {\n\tpath, err := addOptions(fmt.Sprintf(\"%s/%s\", marathonAPIGroups, trimRootPath(name)), opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgroup := new(Group)\n\tif err := r.apiGet(path, nil, group); err != nil {\n\t\treturn nil, err\n\t}\n\treturn group, nil\n}\n\n// HasGroup checks if the group exists in marathon\n// \t\tname:\t\t\tthe identifier for the group\nfunc (r *marathonClient) HasGroup(name string) (bool, error) {\n\tpath := fmt.Sprintf(\"%s/%s\", marathonAPIGroups, trimRootPath(name))\n\terr := r.apiGet(path, \"\", nil)\n\tif err != nil {\n\t\tif apiErr, ok := err.(*APIError); ok && apiErr.ErrCode == ErrCodeNotFound {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// CreateGroup creates a new group in marathon\n//\t\tgroup:\t\t\ta pointer the Group structure defining the group\nfunc (r *marathonClient) CreateGroup(group *Group) error {\n\treturn r.ApiPost(marathonAPIGroups, group, nil)\n}\n\n// WaitOnGroup waits for all the applications in a group to be deployed\n// \t\tgroup:\t\t\tthe identifier for the group\n//\t\ttimeout: \t\ta duration of time to wait before considering it failed (all tasks in all apps running defined as deployed)\nfunc (r *marathonClient) WaitOnGroup(name string, timeout time.Duration) error {\n\terr := deadline(timeout, func(stop_channel chan bool) error {\n\t\tvar flick atomicSwitch\n\t\tgo func() {\n\t\t\t<-stop_channel\n\t\t\tclose(stop_channel)\n\t\t\tflick.SwitchOn()\n\t\t}()\n\t\tfor !flick.IsSwitched() {\n\t\t\tif group, err := r.Group(name); err != nil {\n\t\t\t\tcontinue\n\t\t\t} else {\n\t\t\t\tallRunning := true\n\t\t\t\t// for each of the application, check if the tasks and running\n\t\t\t\tfor _, appID := range group.Apps {\n\t\t\t\t\t// Arrrgghhh!! .. so we can't use application instances from the Application struct like with app wait on as it\n\t\t\t\t\t// appears the instance count is not set straight away!! .. it defaults to zero and changes probably at the\n\t\t\t\t\t// dependencies gets deployed. Which is probably how it internally handles dependencies ..\n\t\t\t\t\t// step: grab the application\n\t\t\t\t\tapplication, err := r.Application(appID.ID)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tallRunning = false\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\n\t\t\t\t\tif application.Tasks == nil {\n\t\t\t\t\t\tallRunning = false\n\t\t\t\t\t} else if len(application.Tasks) != *appID.Instances {\n\t\t\t\t\t\tallRunning = false\n\t\t\t\t\t} else if application.TasksRunning != *appID.Instances {\n\t\t\t\t\t\tallRunning = false\n\t\t\t\t\t} else if len(application.DeploymentIDs()) > 0 {\n\t\t\t\t\t\tallRunning = false\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// has anyone toggle the flag?\n\t\t\t\tif allRunning {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t\ttime.Sleep(r.config.PollingWaitTime)\n\t\t}\n\t\treturn nil\n\t})\n\n\treturn err\n}\n\n// DeleteGroup deletes a group from marathon\n//\t\tname:\t\t\tthe identifier for the group\n//\t\tforce:\t\t\tused to force the delete operation in case of blocked deployment\nfunc (r *marathonClient) DeleteGroup(name string, force bool) (*DeploymentID, error) {\n\tversion := new(DeploymentID)\n\tpath := fmt.Sprintf(\"%s/%s\", marathonAPIGroups, trimRootPath(name))\n\tif force {\n\t\tpath += \"?force=true\"\n\t}\n\tif err := r.apiDelete(path, nil, version); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn version, nil\n}\n\n// UpdateGroup updates the parameters of a groups\n//\t\tname:\t\t\tthe identifier for the group\n//\t\tgroup:  \t\tthe group structure with the new params\n//\t\tforce:\t\t\tused to force the update operation in case of blocked deployment\nfunc (r *marathonClient) UpdateGroup(name string, group *Group, force bool) (*DeploymentID, error) {\n\tdeploymentID := new(DeploymentID)\n\tpath := fmt.Sprintf(\"%s/%s\", marathonAPIGroups, trimRootPath(name))\n\tif force {\n\t\tpath += \"?force=true\"\n\t}\n\tif err := r.apiPut(path, group, deploymentID); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn deploymentID, nil\n}\n"
  },
  {
    "path": "group_test.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGroups(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tgroups, err := endpoint.Client.Groups()\n\tassert.NoError(t, err)\n\tassert.NotNil(t, groups)\n\tassert.Equal(t, 1, len(groups.Groups))\n\tgroup := groups.Groups[0]\n\tassert.Equal(t, fakeGroupName, group.ID)\n}\n\nfunc TestNewGroup(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tgroup, err := endpoint.Client.Group(fakeGroupName)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, group)\n\tassert.Equal(t, 1, len(group.Apps))\n\tassert.Equal(t, fakeGroupName, group.ID)\n\n\tgroup, err = endpoint.Client.Group(fakeGroupName1)\n\n\tassert.NoError(t, err)\n\tassert.NotNil(t, group)\n\tassert.Equal(t, fakeGroupName1, group.ID)\n\tassert.NotNil(t, group.Groups)\n\tassert.Equal(t, 1, len(group.Groups))\n\n\tfrontend := group.Groups[0]\n\tassert.Equal(t, \"frontend\", frontend.ID)\n\tassert.Equal(t, 3, len(frontend.Apps))\n\tfor _, app := range frontend.Apps {\n\t\tassert.NotNil(t, app.Container)\n\t\tassert.NotNil(t, app.Container.Docker)\n\t\tfor _, network := range *app.Networks {\n\t\t\tassert.Equal(t, BridgeNetworkMode, network.Mode)\n\t\t}\n\t\tif len(*app.Container.PortMappings) == 0 {\n\t\t\tt.Fail()\n\t\t}\n\t}\n}\n\n// TODO @kamsz: How to work with old and new endpoints from methods.yml?\n// func TestGroup(t *testing.T) {\n// \tendpoint := newFakeMarathonEndpoint(t, nil)\n// \tdefer endpoint.Close()\n\n// \tgroup, err := endpoint.Client.Group(fakeGroupName)\n// \tassert.NoError(t, err)\n// \tassert.NotNil(t, group)\n// \tassert.Equal(t, 1, len(group.Apps))\n// \tassert.Equal(t, fakeGroupName, group.ID)\n\n// \tgroup, err = endpoint.Client.Group(fakeGroupName1)\n\n// \tassert.NoError(t, err)\n// \tassert.NotNil(t, group)\n// \tassert.Equal(t, fakeGroupName1, group.ID)\n// \tassert.NotNil(t, group.Groups)\n// \tassert.Equal(t, 1, len(group.Groups))\n\n// \tfrontend := group.Groups[0]\n// \tassert.Equal(t, \"frontend\", frontend.ID)\n// \tassert.Equal(t, 3, len(frontend.Apps))\n// \tfor _, app := range frontend.Apps {\n// \t\tassert.NotNil(t, app.Container)\n// \t\tassert.NotNil(t, app.Container.Docker)\n// \t\tassert.Equal(t, \"BRIDGE\", app.Container.Docker.Network)\n// \t\tif len(*app.Container.Docker.PortMappings) == 0 {\n// \t\t\tt.Fail()\n// \t\t}\n// \t}\n// }\n"
  },
  {
    "path": "health.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\n// HealthCheck is the definition for an application health check\ntype HealthCheck struct {\n\tCommand                *Command `json:\"command,omitempty\"`\n\tPortIndex              *int     `json:\"portIndex,omitempty\"`\n\tPort                   *int     `json:\"port,omitempty\"`\n\tPath                   *string  `json:\"path,omitempty\"`\n\tMaxConsecutiveFailures *int     `json:\"maxConsecutiveFailures,omitempty\"`\n\tProtocol               string   `json:\"protocol,omitempty\"`\n\tGracePeriodSeconds     int      `json:\"gracePeriodSeconds,omitempty\"`\n\tIntervalSeconds        int      `json:\"intervalSeconds,omitempty\"`\n\tTimeoutSeconds         int      `json:\"timeoutSeconds,omitempty\"`\n\tIgnoreHTTP1xx          *bool    `json:\"ignoreHttp1xx,omitempty\"`\n}\n\n// HTTPHealthCheck describes an HTTP based health check\ntype HTTPHealthCheck struct {\n\tEndpoint string `json:\"endpoint,omitempty\"`\n\tPath     string `json:\"path,omitempty\"`\n\tScheme   string `json:\"scheme,omitempty\"`\n}\n\n// TCPHealthCheck describes a TCP based health check\ntype TCPHealthCheck struct {\n\tEndpoint string `json:\"endpoint,omitempty\"`\n}\n\n// CommandHealthCheck describes a shell-based health check\ntype CommandHealthCheck struct {\n\tCommand PodCommand `json:\"command,omitempty\"`\n}\n\n// PodHealthCheck describes how to determine a pod's health\ntype PodHealthCheck struct {\n\tHTTP                   *HTTPHealthCheck    `json:\"http,omitempty\"`\n\tTCP                    *TCPHealthCheck     `json:\"tcp,omitempty\"`\n\tExec                   *CommandHealthCheck `json:\"exec,omitempty\"`\n\tGracePeriodSeconds     *int                `json:\"gracePeriodSeconds,omitempty\"`\n\tIntervalSeconds        *int                `json:\"intervalSeconds,omitempty\"`\n\tMaxConsecutiveFailures *int                `json:\"maxConsecutiveFailures,omitempty\"`\n\tTimeoutSeconds         *int                `json:\"timeoutSeconds,omitempty\"`\n\tDelaySeconds           *int                `json:\"delaySeconds,omitempty\"`\n}\n\n// NewPodHealthCheck creates an empty PodHealthCheck\nfunc NewPodHealthCheck() *PodHealthCheck {\n\treturn &PodHealthCheck{}\n}\n\n// NewHTTPHealthCheck creates an empty HTTPHealthCheck\nfunc NewHTTPHealthCheck() *HTTPHealthCheck {\n\treturn &HTTPHealthCheck{}\n}\n\n// NewTCPHealthCheck creates an empty TCPHealthCheck\nfunc NewTCPHealthCheck() *TCPHealthCheck {\n\treturn &TCPHealthCheck{}\n}\n\n// NewCommandHealthCheck creates an empty CommandHealthCheck\nfunc NewCommandHealthCheck() *CommandHealthCheck {\n\treturn &CommandHealthCheck{}\n}\n\n// SetCommand sets the given command on the health check.\nfunc (h *HealthCheck) SetCommand(c Command) *HealthCheck {\n\th.Command = &c\n\treturn h\n}\n\n// SetPortIndex sets the given port index on the health check.\nfunc (h *HealthCheck) SetPortIndex(i int) *HealthCheck {\n\th.PortIndex = &i\n\treturn h\n}\n\n// SetPort sets the given port on the health check.\nfunc (h *HealthCheck) SetPort(i int) *HealthCheck {\n\th.Port = &i\n\treturn h\n}\n\n// SetPath sets the given path on the health check.\nfunc (h *HealthCheck) SetPath(p string) *HealthCheck {\n\th.Path = &p\n\treturn h\n}\n\n// SetMaxConsecutiveFailures sets the maximum consecutive failures on the health check.\nfunc (h *HealthCheck) SetMaxConsecutiveFailures(i int) *HealthCheck {\n\th.MaxConsecutiveFailures = &i\n\treturn h\n}\n\n// SetIgnoreHTTP1xx sets ignore http 1xx on the health check.\nfunc (h *HealthCheck) SetIgnoreHTTP1xx(ignore bool) *HealthCheck {\n\th.IgnoreHTTP1xx = &ignore\n\treturn h\n}\n\n// NewDefaultHealthCheck creates a default application health check\nfunc NewDefaultHealthCheck() *HealthCheck {\n\tportIndex := 0\n\tpath := \"\"\n\tmaxConsecutiveFailures := 3\n\n\treturn &HealthCheck{\n\t\tProtocol:               \"HTTP\",\n\t\tPath:                   &path,\n\t\tPortIndex:              &portIndex,\n\t\tMaxConsecutiveFailures: &maxConsecutiveFailures,\n\t\tGracePeriodSeconds:     30,\n\t\tIntervalSeconds:        10,\n\t\tTimeoutSeconds:         5,\n\t}\n}\n\n// HealthCheckResult is the health check result\ntype HealthCheckResult struct {\n\tAlive               bool   `json:\"alive\"`\n\tConsecutiveFailures int    `json:\"consecutiveFailures\"`\n\tFirstSuccess        string `json:\"firstSuccess\"`\n\tLastFailure         string `json:\"lastFailure\"`\n\tLastFailureCause    string `json:\"lastFailureCause\"`\n\tLastSuccess         string `json:\"lastSuccess\"`\n\tTaskID              string `json:\"taskId\"`\n}\n\n// Command is the command health check type\ntype Command struct {\n\tValue string `json:\"value\"`\n}\n\n// SetHTTPHealthCheck configures the pod's health check for an HTTP endpoint.\n// Note this will erase any configured TCP/Exec health checks.\nfunc (p *PodHealthCheck) SetHTTPHealthCheck(h *HTTPHealthCheck) *PodHealthCheck {\n\tp.HTTP = h\n\tp.TCP = nil\n\tp.Exec = nil\n\treturn p\n}\n\n// SetTCPHealthCheck configures the pod's health check for a TCP endpoint.\n// Note this will erase any configured HTTP/Exec health checks.\nfunc (p *PodHealthCheck) SetTCPHealthCheck(t *TCPHealthCheck) *PodHealthCheck {\n\tp.TCP = t\n\tp.HTTP = nil\n\tp.Exec = nil\n\treturn p\n}\n\n// SetExecHealthCheck configures the pod's health check for a command.\n// Note this will erase any configured HTTP/TCP health checks.\nfunc (p *PodHealthCheck) SetExecHealthCheck(e *CommandHealthCheck) *PodHealthCheck {\n\tp.Exec = e\n\tp.HTTP = nil\n\tp.TCP = nil\n\treturn p\n}\n\n// SetGracePeriod sets the health check initial grace period, in seconds\nfunc (p *PodHealthCheck) SetGracePeriod(gracePeriodSeconds int) *PodHealthCheck {\n\tp.GracePeriodSeconds = &gracePeriodSeconds\n\treturn p\n}\n\n// SetInterval sets the health check polling interval, in seconds\nfunc (p *PodHealthCheck) SetInterval(intervalSeconds int) *PodHealthCheck {\n\tp.IntervalSeconds = &intervalSeconds\n\treturn p\n}\n\n// SetMaxConsecutiveFailures sets the maximum consecutive failures on the health check\nfunc (p *PodHealthCheck) SetMaxConsecutiveFailures(maxFailures int) *PodHealthCheck {\n\tp.MaxConsecutiveFailures = &maxFailures\n\treturn p\n}\n\n// SetTimeout sets the length of time the health check will await a result, in seconds\nfunc (p *PodHealthCheck) SetTimeout(timeoutSeconds int) *PodHealthCheck {\n\tp.TimeoutSeconds = &timeoutSeconds\n\treturn p\n}\n\n// SetDelay sets the length of time a pod will delay running health checks on initial launch, in seconds\nfunc (p *PodHealthCheck) SetDelay(delaySeconds int) *PodHealthCheck {\n\tp.DelaySeconds = &delaySeconds\n\treturn p\n}\n\n// SetEndpoint sets the name of the pod health check endpoint\nfunc (h *HTTPHealthCheck) SetEndpoint(endpoint string) *HTTPHealthCheck {\n\th.Endpoint = endpoint\n\treturn h\n}\n\n// SetPath sets the HTTP path of the pod health check endpoint\nfunc (h *HTTPHealthCheck) SetPath(path string) *HTTPHealthCheck {\n\th.Path = path\n\treturn h\n}\n\n// SetScheme sets the HTTP scheme of the pod health check endpoint\nfunc (h *HTTPHealthCheck) SetScheme(scheme string) *HTTPHealthCheck {\n\th.Scheme = scheme\n\treturn h\n}\n\n// SetEndpoint sets the name of the pod health check endpoint\nfunc (t *TCPHealthCheck) SetEndpoint(endpoint string) *TCPHealthCheck {\n\tt.Endpoint = endpoint\n\treturn t\n}\n\n// SetCommand sets a CommandHealthCheck's underlying PodCommand\nfunc (c *CommandHealthCheck) SetCommand(p PodCommand) *CommandHealthCheck {\n\tc.Command = p\n\treturn c\n}\n"
  },
  {
    "path": "health_test.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCommand(t *testing.T) {\n\thc := new(HealthCheck)\n\tcommand := Command{\"curl localhost:8080\"}\n\thc.SetCommand(command)\n\tassert.Equal(t, command, (*hc.Command))\n}\n\nfunc TestPortIndex(t *testing.T) {\n\thc := new(HealthCheck)\n\thc.SetPortIndex(0)\n\tassert.Equal(t, 0, (*hc.PortIndex))\n}\n\nfunc TestPort(t *testing.T) {\n\thc := new(HealthCheck)\n\thc.SetPort(8000)\n\tassert.Equal(t, 8000, (*hc.Port))\n}\n\nfunc TestPath(t *testing.T) {\n\thc := new(HealthCheck)\n\thc.SetPath(\"/path\")\n\tassert.Equal(t, \"/path\", (*hc.Path))\n}\n\nfunc TestMaxConsecutiveFailures(t *testing.T) {\n\thc := new(HealthCheck)\n\thc.SetMaxConsecutiveFailures(3)\n\tassert.Equal(t, 3, (*hc.MaxConsecutiveFailures))\n}\n\nfunc TestIgnoreHTTP1xx(t *testing.T) {\n\thc := new(HealthCheck)\n\thc.SetIgnoreHTTP1xx(true)\n\tassert.True(t, (*hc.IgnoreHTTP1xx))\n}\n"
  },
  {
    "path": "info.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\n// Info is the detailed stats returned from marathon info\ntype Info struct {\n\tEventSubscriber struct {\n\t\tHTTPEndpoints []string `json:\"http_endpoints\"`\n\t\tType          string   `json:\"type\"`\n\t} `json:\"event_subscriber\"`\n\tFrameworkID string `json:\"frameworkId\"`\n\tHTTPConfig  struct {\n\t\tAssetsPath interface{} `json:\"assets_path\"`\n\t\tHTTPPort   float64     `json:\"http_port\"`\n\t\tHTTPSPort  float64     `json:\"https_port\"`\n\t} `json:\"http_config\"`\n\tLeader         string `json:\"leader\"`\n\tMarathonConfig struct {\n\t\tCheckpoint                     bool    `json:\"checkpoint\"`\n\t\tExecutor                       string  `json:\"executor\"`\n\t\tFailoverTimeout                float64 `json:\"failover_timeout\"`\n\t\tFrameworkName                  string  `json:\"framework_name\"`\n\t\tHa                             bool    `json:\"ha\"`\n\t\tHostname                       string  `json:\"hostname\"`\n\t\tLeaderProxyConnectionTimeoutMs float64 `json:\"leader_proxy_connection_timeout_ms\"`\n\t\tLeaderProxyReadTimeoutMs       float64 `json:\"leader_proxy_read_timeout_ms\"`\n\t\tLocalPortMax                   float64 `json:\"local_port_max\"`\n\t\tLocalPortMin                   float64 `json:\"local_port_min\"`\n\t\tMaster                         string  `json:\"master\"`\n\t\tMesosLeaderUIURL               string  `json:\"mesos_leader_ui_url\"`\n\t\tWebUIURL                       string  `json:\"webui_url\"`\n\t\tMesosRole                      string  `json:\"mesos_role\"`\n\t\tMesosUser                      string  `json:\"mesos_user\"`\n\t\tReconciliationInitialDelay     float64 `json:\"reconciliation_initial_delay\"`\n\t\tReconciliationInterval         float64 `json:\"reconciliation_interval\"`\n\t\tTaskLaunchTimeout              float64 `json:\"task_launch_timeout\"`\n\t\tTaskReservationTimeout         float64 `json:\"task_reservation_timeout\"`\n\t} `json:\"marathon_config\"`\n\tName            string `json:\"name\"`\n\tVersion         string `json:\"version\"`\n\tZookeeperConfig struct {\n\t\tZk              string `json:\"zk\"`\n\t\tZkFutureTimeout struct {\n\t\t\tDuration float64 `json:\"duration\"`\n\t\t} `json:\"zk_future_timeout\"`\n\t\tZkHosts   string  `json:\"zk_hosts\"`\n\t\tZkPath    string  `json:\"zk_path\"`\n\t\tZkState   string  `json:\"zk_state\"`\n\t\tZkTimeout float64 `json:\"zk_timeout\"`\n\t} `json:\"zookeeper_config\"`\n}\n\n// Info retrieves the info stats from marathon\nfunc (r *marathonClient) Info() (*Info, error) {\n\tinfo := new(Info)\n\tif err := r.apiGet(marathonAPIInfo, nil, info); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn info, nil\n}\n\n// Leader retrieves the current marathon leader node\nfunc (r *marathonClient) Leader() (string, error) {\n\tvar leader struct {\n\t\tLeader string `json:\"leader\"`\n\t}\n\tif err := r.apiGet(marathonAPILeader, nil, &leader); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn leader.Leader, nil\n}\n\n// AbdicateLeader abdicates the marathon leadership\nfunc (r *marathonClient) AbdicateLeader() (string, error) {\n\tvar message struct {\n\t\tMessage string `json:\"message\"`\n\t}\n\n\tif err := r.apiDelete(marathonAPILeader, nil, &message); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn message.Message, nil\n}\n"
  },
  {
    "path": "info_test.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestInfo(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tinfo, err := endpoint.Client.Info()\n\tassert.NoError(t, err)\n\tassert.Equal(t, info.FrameworkID, \"20140730-222531-1863654316-5050-10422-0000\")\n\tassert.Equal(t, info.Leader, \"127.0.0.1:8080\")\n\tassert.Equal(t, info.Version, \"0.7.0-SNAPSHOT\")\n}\n\nfunc TestLeader(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tleader, err := endpoint.Client.Leader()\n\tassert.NoError(t, err)\n\tassert.Equal(t, leader, \"127.0.0.1:8080\")\n}\n\nfunc TestAbdicateLeader(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tmessage, err := endpoint.Client.AbdicateLeader()\n\tassert.NoError(t, err)\n\tassert.Equal(t, message, \"Leadership abdicted\")\n}\n"
  },
  {
    "path": "last_task_failure.go",
    "content": "/*\nCopyright 2015 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\n// LastTaskFailure provides details on the last error experienced by an application\ntype LastTaskFailure struct {\n\tAppID     string `json:\"appId,omitempty\"`\n\tHost      string `json:\"host,omitempty\"`\n\tMessage   string `json:\"message,omitempty\"`\n\tSlaveID   string `json:\"slaveId,omitempty\"`\n\tState     string `json:\"state,omitempty\"`\n\tTaskID    string `json:\"taskId,omitempty\"`\n\tTimestamp string `json:\"timestamp,omitempty\"`\n\tVersion   string `json:\"version,omitempty\"`\n}\n"
  },
  {
    "path": "network.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\n// PodNetworkMode is the mode of a network descriptor\ntype PodNetworkMode string\n\nconst (\n\tContainerNetworkMode PodNetworkMode = \"container\"\n\tBridgeNetworkMode    PodNetworkMode = \"container/bridge\"\n\tHostNetworkMode      PodNetworkMode = \"host\"\n)\n\n// PodNetwork contains network descriptors for a pod\ntype PodNetwork struct {\n\tName   string            `json:\"name,omitempty\"`\n\tMode   PodNetworkMode    `json:\"mode,omitempty\"`\n\tLabels map[string]string `json:\"labels,omitempty\"`\n}\n\n// PodEndpoint describes an endpoint for a pod's container\ntype PodEndpoint struct {\n\tName          string            `json:\"name,omitempty\"`\n\tContainerPort int               `json:\"containerPort,omitempty\"`\n\tHostPort      int               `json:\"hostPort,omitempty\"`\n\tProtocol      []string          `json:\"protocol,omitempty\"`\n\tLabels        map[string]string `json:\"labels,omitempty\"`\n}\n\n// NewPodNetwork creates an empty PodNetwork\nfunc NewPodNetwork(name string) *PodNetwork {\n\treturn &PodNetwork{\n\t\tName:   name,\n\t\tLabels: map[string]string{},\n\t}\n}\n\n// NewPodEndpoint creates an empty PodEndpoint\nfunc NewPodEndpoint() *PodEndpoint {\n\treturn &PodEndpoint{\n\t\tProtocol: []string{},\n\t\tLabels:   map[string]string{},\n\t}\n}\n\n// NewBridgePodNetwork creates a PodNetwork for a container in bridge mode\nfunc NewBridgePodNetwork() *PodNetwork {\n\tpn := NewPodNetwork(\"\")\n\treturn pn.SetMode(BridgeNetworkMode)\n}\n\n// NewContainerPodNetwork creates a PodNetwork for a container\nfunc NewContainerPodNetwork(name string) *PodNetwork {\n\tpn := NewPodNetwork(name)\n\treturn pn.SetMode(ContainerNetworkMode)\n}\n\n// NewHostPodNetwork creates a PodNetwork for a container in host mode\nfunc NewHostPodNetwork() *PodNetwork {\n\tpn := NewPodNetwork(\"\")\n\treturn pn.SetMode(HostNetworkMode)\n}\n\n// SetName sets the name of a PodNetwork\nfunc (n *PodNetwork) SetName(name string) *PodNetwork {\n\tn.Name = name\n\treturn n\n}\n\n// SetMode sets the mode of a PodNetwork\nfunc (n *PodNetwork) SetMode(mode PodNetworkMode) *PodNetwork {\n\tn.Mode = mode\n\treturn n\n}\n\n// Label sets a label of a PodNetwork\nfunc (n *PodNetwork) Label(key, value string) *PodNetwork {\n\tn.Labels[key] = value\n\treturn n\n}\n\n// SetName sets the name for a PodEndpoint\nfunc (e *PodEndpoint) SetName(name string) *PodEndpoint {\n\te.Name = name\n\treturn e\n}\n\n// SetContainerPort sets the container port for a PodEndpoint\nfunc (e *PodEndpoint) SetContainerPort(port int) *PodEndpoint {\n\te.ContainerPort = port\n\treturn e\n}\n\n// SetHostPort sets the host port for a PodEndpoint\nfunc (e *PodEndpoint) SetHostPort(port int) *PodEndpoint {\n\te.HostPort = port\n\treturn e\n}\n\n// AddProtocol appends a protocol for a PodEndpoint\nfunc (e *PodEndpoint) AddProtocol(protocol string) *PodEndpoint {\n\te.Protocol = append(e.Protocol, protocol)\n\treturn e\n}\n\n// Label sets a label for a PodEndpoint\nfunc (e *PodEndpoint) Label(key, value string) *PodEndpoint {\n\te.Labels[key] = value\n\treturn e\n}\n"
  },
  {
    "path": "offer.go",
    "content": "/*\nCopyright 2019 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage marathon\n\n// based on https://github.com/mesosphere/marathon/blob/e7b1456ad0cfba23c9fdfa3c5d638a4b9aeb60d0/docs/docs/rest-api/public/api/v2/types/offer.raml\n\n// Offer describes a Mesos offer to a framework\ntype Offer struct {\n\tID         string           `json:\"id\"`\n\tHostname   string           `json:\"hostname\"`\n\tAgentID    string           `json:\"agentId\"`\n\tResources  []OfferResource  `json:\"resources\"`\n\tAttributes []AgentAttribute `json:\"attributes\"`\n}\n\n// OfferResource describes a resource that is part of an offer\ntype OfferResource struct {\n\tName   string        `json:\"name\"`\n\tRole   string        `json:\"role\"`\n\tScalar *float64      `json:\"scalar,omitempty\"`\n\tRanges []NumberRange `json:\"ranges,omitempty\"`\n\tSet    []string      `json:\"set,omitempty\"`\n}\n\n// NumberRange is a range of numbers\ntype NumberRange struct {\n\tBegin int64 `json:\"begin\"`\n\tEnd   int64 `json:\"end\"`\n}\n\n// AgentAttribute describes an attribute of an agent node\ntype AgentAttribute struct {\n\tName   string        `json:\"name\"`\n\tText   *string       `json:\"text,omitempty\"`\n\tScalar *float64      `json:\"scalar,omitempty\"`\n\tRanges []NumberRange `json:\"ranges,omitempty\"`\n\tSet    []string      `json:\"set,omitempty\"`\n}\n"
  },
  {
    "path": "pod.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"fmt\"\n)\n\n// Pod is the definition for an pod in marathon\ntype Pod struct {\n\tID      string            `json:\"id,omitempty\"`\n\tLabels  map[string]string `json:\"labels,omitempty\"`\n\tVersion string            `json:\"version,omitempty\"`\n\tUser    string            `json:\"user,omitempty\"`\n\t// Non-secret environment variables. Actual secrets are stored in Secrets\n\t// Magic happens at marshaling/unmarshaling to get them into the correct schema\n\tEnv               map[string]string    `json:\"-\"`\n\tSecrets           map[string]Secret    `json:\"-\"`\n\tContainers        []*PodContainer      `json:\"containers,omitempty\"`\n\tVolumes           []*PodVolume         `json:\"volumes,omitempty\"`\n\tNetworks          []*PodNetwork        `json:\"networks,omitempty\"`\n\tScaling           *PodScalingPolicy    `json:\"scaling,omitempty\"`\n\tScheduling        *PodSchedulingPolicy `json:\"scheduling,omitempty\"`\n\tExecutorResources *ExecutorResources   `json:\"executorResources,omitempty\"`\n\tRole              *string              `json:\"role,omitempty\"`\n}\n\n// PodScalingPolicy is the scaling policy of the pod\ntype PodScalingPolicy struct {\n\tKind         string `json:\"kind\"`\n\tInstances    int    `json:\"instances\"`\n\tMaxInstances int    `json:\"maxInstances,omitempty\"`\n}\n\n// NewPod create an empty pod\nfunc NewPod() *Pod {\n\treturn &Pod{\n\t\tLabels:     map[string]string{},\n\t\tEnv:        map[string]string{},\n\t\tContainers: []*PodContainer{},\n\t\tSecrets:    map[string]Secret{},\n\t\tVolumes:    []*PodVolume{},\n\t\tNetworks:   []*PodNetwork{},\n\t}\n}\n\n// Name sets the name / ID of the pod i.e. the identifier for this pod\nfunc (p *Pod) Name(id string) *Pod {\n\tp.ID = validateID(id)\n\treturn p\n}\n\n// SetUser sets the user to run the pod as\nfunc (p *Pod) SetUser(user string) *Pod {\n\tp.User = user\n\treturn p\n}\n\n// EmptyLabels empties the labels in a pod\nfunc (p *Pod) EmptyLabels() *Pod {\n\tp.Labels = make(map[string]string)\n\treturn p\n}\n\n// AddLabel adds a label to a pod\nfunc (p *Pod) AddLabel(key, value string) *Pod {\n\tp.Labels[key] = value\n\treturn p\n}\n\n// SetLabels sets the labels for a pod\nfunc (p *Pod) SetLabels(labels map[string]string) *Pod {\n\tp.Labels = labels\n\treturn p\n}\n\n// EmptyEnvs empties the environment variables for a pod\nfunc (p *Pod) EmptyEnvs() *Pod {\n\tp.Env = make(map[string]string)\n\treturn p\n}\n\n// AddEnv adds an environment variable to a pod\nfunc (p *Pod) AddEnv(name, value string) *Pod {\n\tif p.Env == nil {\n\t\tp = p.EmptyEnvs()\n\t}\n\tp.Env[name] = value\n\treturn p\n}\n\n// ExtendEnv extends the environment with the new environment variables\nfunc (p *Pod) ExtendEnv(env map[string]string) *Pod {\n\tif p.Env == nil {\n\t\tp = p.EmptyEnvs()\n\t}\n\n\tfor k, v := range env {\n\t\tp.AddEnv(k, v)\n\t}\n\treturn p\n}\n\n// AddContainer adds a container to a pod\nfunc (p *Pod) AddContainer(container *PodContainer) *Pod {\n\tp.Containers = append(p.Containers, container)\n\treturn p\n}\n\n// EmptySecrets empties the secret sources in a pod\nfunc (p *Pod) EmptySecrets() *Pod {\n\tp.Secrets = make(map[string]Secret)\n\treturn p\n}\n\n// GetSecretSource gets the source of the named secret\nfunc (p *Pod) GetSecretSource(name string) (string, error) {\n\tif val, ok := p.Secrets[name]; ok {\n\t\treturn val.Source, nil\n\t}\n\treturn \"\", fmt.Errorf(\"secret does not exist\")\n}\n\n// AddSecret adds the secret to the pod\nfunc (p *Pod) AddSecret(envVar, secretName, sourceName string) *Pod {\n\tif p.Secrets == nil {\n\t\tp = p.EmptySecrets()\n\t}\n\tp.Secrets[secretName] = Secret{EnvVar: envVar, Source: sourceName}\n\treturn p\n}\n\n// AddVolume adds a volume to a pod\nfunc (p *Pod) AddVolume(vol *PodVolume) *Pod {\n\tp.Volumes = append(p.Volumes, vol)\n\treturn p\n}\n\n// AddNetwork adds a PodNetwork to a pod\nfunc (p *Pod) AddNetwork(net *PodNetwork) *Pod {\n\tp.Networks = append(p.Networks, net)\n\treturn p\n}\n\n// Count sets the count of the pod\nfunc (p *Pod) Count(count int) *Pod {\n\tp.Scaling = &PodScalingPolicy{\n\t\tKind:      \"fixed\",\n\t\tInstances: count,\n\t}\n\treturn p\n}\n\n// SetPodSchedulingPolicy sets the PodSchedulingPolicy of a pod\nfunc (p *Pod) SetPodSchedulingPolicy(policy *PodSchedulingPolicy) *Pod {\n\tp.Scheduling = policy\n\treturn p\n}\n\n// SetExecutorResources sets the resources for the pod executor\nfunc (p *Pod) SetExecutorResources(resources *ExecutorResources) *Pod {\n\tp.ExecutorResources = resources\n\treturn p\n}\n\n// SupportsPods determines if this version of marathon supports pods\n// If HEAD returns 200 it does\nfunc (r *marathonClient) SupportsPods() (bool, error) {\n\tif err := r.apiHead(marathonAPIPods, nil); err != nil {\n\t\t// If we get a 404 we can return a strict false, otherwise it could be\n\t\t// a valid error\n\t\tif apiErr, ok := err.(*APIError); ok && apiErr.ErrCode == ErrCodeNotFound {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\n\treturn true, nil\n}\n\n// Pod gets a pod object from marathon by name\nfunc (r *marathonClient) Pod(name string) (*Pod, error) {\n\turi := buildPodURI(name)\n\tresult := new(Pod)\n\tif err := r.apiGet(uri, nil, result); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// Pods gets all pods from marathon\nfunc (r *marathonClient) Pods() ([]Pod, error) {\n\tvar result []Pod\n\tif err := r.apiGet(marathonAPIPods, nil, &result); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// CreatePod creates a new pod in Marathon\nfunc (r *marathonClient) CreatePod(pod *Pod) (*Pod, error) {\n\tresult := new(Pod)\n\tif err := r.ApiPost(marathonAPIPods, &pod, result); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// DeletePod deletes a pod from marathon\nfunc (r *marathonClient) DeletePod(name string, force bool) (*DeploymentID, error) {\n\turi := fmt.Sprintf(\"%s?force=%v\", buildPodURI(name), force)\n\n\tdeployID := new(DeploymentID)\n\tif err := r.apiDelete(uri, nil, deployID); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn deployID, nil\n}\n\n// UpdatePod creates a new pod in Marathon\nfunc (r *marathonClient) UpdatePod(pod *Pod, force bool) (*Pod, error) {\n\turi := fmt.Sprintf(\"%s?force=%v\", buildPodURI(pod.ID), force)\n\tresult := new(Pod)\n\n\tif err := r.apiPut(uri, pod, result); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// PodVersions gets all the deployed versions of a pod\nfunc (r *marathonClient) PodVersions(name string) ([]string, error) {\n\turi := buildPodVersionURI(name)\n\tvar result []string\n\tif err := r.apiGet(uri, nil, &result); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// PodByVersion gets a pod by a version identifier\nfunc (r *marathonClient) PodByVersion(name, version string) (*Pod, error) {\n\turi := fmt.Sprintf(\"%s/%s\", buildPodVersionURI(name), version)\n\tresult := new(Pod)\n\tif err := r.apiGet(uri, nil, result); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\nfunc buildPodVersionURI(name string) string {\n\treturn fmt.Sprintf(\"%s/%s::versions\", marathonAPIPods, trimRootPath(name))\n}\n\nfunc buildPodURI(path string) string {\n\treturn fmt.Sprintf(\"%s/%s\", marathonAPIPods, trimRootPath(path))\n}\n"
  },
  {
    "path": "pod_container.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\n// PodContainer describes a container in a pod\ntype PodContainer struct {\n\tName         string             `json:\"name,omitempty\"`\n\tExec         *PodExec           `json:\"exec,omitempty\"`\n\tResources    *Resources         `json:\"resources,omitempty\"`\n\tEndpoints    []*PodEndpoint     `json:\"endpoints,omitempty\"`\n\tImage        *PodContainerImage `json:\"image,omitempty\"`\n\tEnv          map[string]string  `json:\"-\"`\n\tSecrets      map[string]Secret  `json:\"-\"`\n\tUser         string             `json:\"user,omitempty\"`\n\tHealthCheck  *PodHealthCheck    `json:\"healthCheck,omitempty\"`\n\tVolumeMounts []*PodVolumeMount  `json:\"volumeMounts,omitempty\"`\n\tArtifacts    []*PodArtifact     `json:\"artifacts,omitempty\"`\n\tLabels       map[string]string  `json:\"labels,omitempty\"`\n\tLifecycle    PodLifecycle       `json:\"lifecycle,omitempty\"`\n}\n\n// PodLifecycle describes the lifecycle of a pod\ntype PodLifecycle struct {\n\tKillGracePeriodSeconds *float64 `json:\"killGracePeriodSeconds,omitempty\"`\n}\n\n// PodCommand is the command to run as the entrypoint of the container\ntype PodCommand struct {\n\tShell string `json:\"shell,omitempty\"`\n}\n\n// PodExec contains the PodCommand\ntype PodExec struct {\n\tCommand PodCommand `json:\"command,omitempty\"`\n}\n\n// PodArtifact describes how to obtain a generic artifact for a pod\ntype PodArtifact struct {\n\tURI        string `json:\"uri,omitempty\"`\n\tExtract    *bool  `json:\"extract,omitempty\"`\n\tExecutable *bool  `json:\"executable,omitempty\"`\n\tCache      *bool  `json:\"cache,omitempty\"`\n\tDestPath   string `json:\"destPath,omitempty\"`\n}\n\n// NewPodContainer creates an empty PodContainer\nfunc NewPodContainer() *PodContainer {\n\treturn &PodContainer{\n\t\tEndpoints:    []*PodEndpoint{},\n\t\tEnv:          map[string]string{},\n\t\tVolumeMounts: []*PodVolumeMount{},\n\t\tArtifacts:    []*PodArtifact{},\n\t\tLabels:       map[string]string{},\n\t\tResources:    NewResources(),\n\t}\n}\n\n// SetName sets the name of a pod container\nfunc (p *PodContainer) SetName(name string) *PodContainer {\n\tp.Name = name\n\treturn p\n}\n\n// SetCommand sets the shell command of a pod container\nfunc (p *PodContainer) SetCommand(name string) *PodContainer {\n\tp.Exec = &PodExec{\n\t\tCommand: PodCommand{\n\t\t\tShell: name,\n\t\t},\n\t}\n\treturn p\n}\n\n// CPUs sets the CPUs of a pod container\nfunc (p *PodContainer) CPUs(cpu float64) *PodContainer {\n\tp.Resources.Cpus = cpu\n\treturn p\n}\n\n// Memory sets the memory of a pod container\nfunc (p *PodContainer) Memory(memory float64) *PodContainer {\n\tp.Resources.Mem = memory\n\treturn p\n}\n\n// Storage sets the storage capacity of a pod container\nfunc (p *PodContainer) Storage(disk float64) *PodContainer {\n\tp.Resources.Disk = disk\n\treturn p\n}\n\n// GPUs sets the GPU requirements of a pod container\nfunc (p *PodContainer) GPUs(gpu int32) *PodContainer {\n\tp.Resources.Gpus = gpu\n\treturn p\n}\n\n// AddEndpoint appends an endpoint for a pod container\nfunc (p *PodContainer) AddEndpoint(endpoint *PodEndpoint) *PodContainer {\n\tp.Endpoints = append(p.Endpoints, endpoint)\n\treturn p\n}\n\n// SetImage sets the image of a pod container\nfunc (p *PodContainer) SetImage(image *PodContainerImage) *PodContainer {\n\tp.Image = image\n\treturn p\n}\n\n// EmptyEnvs initialized env to empty\nfunc (p *PodContainer) EmptyEnvs() *PodContainer {\n\tp.Env = make(map[string]string)\n\treturn p\n}\n\n// AddEnv adds an environment variable for a pod container\nfunc (p *PodContainer) AddEnv(name, value string) *PodContainer {\n\tif p.Env == nil {\n\t\tp = p.EmptyEnvs()\n\t}\n\tp.Env[name] = value\n\treturn p\n}\n\n// ExtendEnv extends the environment for a pod container\nfunc (p *PodContainer) ExtendEnv(env map[string]string) *PodContainer {\n\tif p.Env == nil {\n\t\tp = p.EmptyEnvs()\n\t}\n\tfor k, v := range env {\n\t\tp.AddEnv(k, v)\n\t}\n\treturn p\n}\n\n// AddSecret adds a secret to the environment for a pod container\nfunc (p *PodContainer) AddSecret(name, secretName string) *PodContainer {\n\tif p.Env == nil {\n\t\tp = p.EmptyEnvs()\n\t}\n\tp.Env[name] = secretName\n\treturn p\n}\n\n// SetUser sets the user to run the pod as\nfunc (p *PodContainer) SetUser(user string) *PodContainer {\n\tp.User = user\n\treturn p\n}\n\n// SetHealthCheck sets the health check of a pod container\nfunc (p *PodContainer) SetHealthCheck(healthcheck *PodHealthCheck) *PodContainer {\n\tp.HealthCheck = healthcheck\n\treturn p\n}\n\n// AddVolumeMount appends a volume mount to a pod container\nfunc (p *PodContainer) AddVolumeMount(mount *PodVolumeMount) *PodContainer {\n\tp.VolumeMounts = append(p.VolumeMounts, mount)\n\treturn p\n}\n\n// AddArtifact appends an artifact to a pod container\nfunc (p *PodContainer) AddArtifact(artifact *PodArtifact) *PodContainer {\n\tp.Artifacts = append(p.Artifacts, artifact)\n\treturn p\n}\n\n// AddLabel adds a label to a pod container\nfunc (p *PodContainer) AddLabel(key, value string) *PodContainer {\n\tp.Labels[key] = value\n\treturn p\n}\n\n// SetLifecycle sets the lifecycle of a pod container\nfunc (p *PodContainer) SetLifecycle(lifecycle PodLifecycle) *PodContainer {\n\tp.Lifecycle = lifecycle\n\treturn p\n}\n"
  },
  {
    "path": "pod_container_image.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\n// ImageType represents the image format type\ntype ImageType string\n\nconst (\n\t// ImageTypeDocker is the docker format\n\tImageTypeDocker ImageType = \"DOCKER\"\n\n\t// ImageTypeAppC is the appc format\n\tImageTypeAppC ImageType = \"APPC\"\n)\n\n// PodContainerImage describes how to retrieve the container image\ntype PodContainerImage struct {\n\tKind       ImageType   `json:\"kind,omitempty\"`\n\tID         string      `json:\"id,omitempty\"`\n\tForcePull  *bool       `json:\"forcePull,omitempty\"`\n\tPullConfig *PullConfig `json:\"pullConfig,omitempty\"`\n}\n\n// NewPodContainerImage creates an empty PodContainerImage\nfunc NewPodContainerImage() *PodContainerImage {\n\treturn &PodContainerImage{}\n}\n\n// SetKind sets the Kind of the image\nfunc (i *PodContainerImage) SetKind(typ ImageType) *PodContainerImage {\n\ti.Kind = typ\n\treturn i\n}\n\n// SetID sets the ID of the image\nfunc (i *PodContainerImage) SetID(id string) *PodContainerImage {\n\ti.ID = id\n\treturn i\n}\n\n// SetPullConfig adds *PullConfig to PodContainerImage\nfunc (i *PodContainerImage) SetPullConfig(pullConfig *PullConfig) *PodContainerImage {\n\ti.PullConfig = pullConfig\n\n\treturn i\n}\n\n// NewDockerPodContainerImage creates a docker PodContainerImage\nfunc NewDockerPodContainerImage() *PodContainerImage {\n\treturn NewPodContainerImage().SetKind(ImageTypeDocker)\n}\n"
  },
  {
    "path": "pod_container_marshalling.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n)\n\n// PodContainerAlias aliases the PodContainer struct so that it will be marshaled/unmarshaled automatically\ntype PodContainerAlias PodContainer\n\n// UnmarshalJSON unmarshals the given PodContainer JSON as expected except for environment variables and secrets.\n// Environment variables are stored in the Env field. Secrets, including the environment variable part,\n// are stored in the Secrets field.\nfunc (p *PodContainer) UnmarshalJSON(b []byte) error {\n\taux := &struct {\n\t\t*PodContainerAlias\n\t\tEnv map[string]interface{} `json:\"environment\"`\n\t}{\n\t\tPodContainerAlias: (*PodContainerAlias)(p),\n\t}\n\tif err := json.Unmarshal(b, aux); err != nil {\n\t\treturn fmt.Errorf(\"malformed pod container definition %v\", err)\n\t}\n\tenv := map[string]string{}\n\tsecrets := map[string]Secret{}\n\n\tfor envName, genericEnvValue := range aux.Env {\n\t\tswitch envValOrSecret := genericEnvValue.(type) {\n\t\tcase string:\n\t\t\tenv[envName] = envValOrSecret\n\t\tcase map[string]interface{}:\n\t\t\tfor secret, secretStore := range envValOrSecret {\n\t\t\t\tif secStore, ok := secretStore.(string); ok && secret == \"secret\" {\n\t\t\t\t\tsecrets[secStore] = Secret{EnvVar: envName}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\treturn fmt.Errorf(\"unexpected secret field %v of value type %T\", secret, envValOrSecret[secret])\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"unexpected environment variable type %T\", envValOrSecret)\n\t\t}\n\t}\n\tp.Env = env\n\tfor k, v := range aux.Secrets {\n\t\ttmp := secrets[k]\n\t\ttmp.Source = v.Source\n\t\tsecrets[k] = tmp\n\t}\n\tp.Secrets = secrets\n\treturn nil\n}\n\n// MarshalJSON marshals the given PodContainer as expected except for environment variables and secrets,\n// which are marshaled from specialized structs.  The environment variable piece of the secrets and other\n// normal environment variables are combined and marshaled to the env field.  The secrets and the related\n// source are marshaled into the secrets field.\nfunc (p *PodContainer) MarshalJSON() ([]byte, error) {\n\tenv := make(map[string]interface{})\n\tsecrets := make(map[string]TmpSecret)\n\n\tif p.Env != nil {\n\t\tfor k, v := range p.Env {\n\t\t\tenv[string(k)] = string(v)\n\t\t}\n\t}\n\tif p.Secrets != nil {\n\t\tfor k, v := range p.Secrets {\n\t\t\tenv[v.EnvVar] = TmpEnvSecret{Secret: k}\n\t\t\tsecrets[k] = TmpSecret{v.Source}\n\t\t}\n\t}\n\taux := &struct {\n\t\t*PodContainerAlias\n\t\tEnv map[string]interface{} `json:\"environment,omitempty\"`\n\t}{PodContainerAlias: (*PodContainerAlias)(p), Env: env}\n\n\treturn json.Marshal(aux)\n}\n"
  },
  {
    "path": "pod_instance.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\n// PodInstance is the representation of an instance as returned by deleting an instance\ntype PodInstance struct {\n\tInstanceID          PodInstanceID              `json:\"instanceId\"`\n\tAgentInfo           PodAgentInfo               `json:\"agentInfo\"`\n\tTasksMap            map[string]PodTask         `json:\"tasksMap\"`\n\tRunSpecVersion      time.Time                  `json:\"runSpecVersion\"`\n\tState               PodInstanceStateHistory    `json:\"state\"`\n\tUnreachableStrategy EnabledUnreachableStrategy `json:\"unreachableStrategy\"`\n}\n\n// PodInstanceStateHistory is the pod instance's state\ntype PodInstanceStateHistory struct {\n\tCondition   PodTaskCondition `json:\"condition\"`\n\tSince       time.Time        `json:\"since\"`\n\tActiveSince time.Time        `json:\"activeSince\"`\n}\n\n// PodInstanceID contains the instance ID\ntype PodInstanceID struct {\n\tID string `json:\"idString\"`\n}\n\n// PodAgentInfo contains info about the agent the instance is running on\ntype PodAgentInfo struct {\n\tHost       string   `json:\"host\"`\n\tAgentID    string   `json:\"agentId\"`\n\tAttributes []string `json:\"attributes\"`\n}\n\n// PodTask contains the info about the specific task within the instance\ntype PodTask struct {\n\tTaskID         string        `json:\"taskId\"`\n\tRunSpecVersion time.Time     `json:\"runSpecVersion\"`\n\tStatus         PodTaskStatus `json:\"status\"`\n}\n\n// PodTaskStatus is the current status of the task\ntype PodTaskStatus struct {\n\tStagedAt    time.Time        `json:\"stagedAt\"`\n\tStartedAt   time.Time        `json:\"startedAt\"`\n\tMesosStatus string           `json:\"mesosStatus\"`\n\tCondition   PodTaskCondition `json:\"condition\"`\n\tNetworkInfo PodNetworkInfo   `json:\"networkInfo\"`\n}\n\n// PodTaskCondition contains a string representation of the condition\ntype PodTaskCondition struct {\n\tStr string `json:\"str\"`\n}\n\n// PodNetworkInfo contains the network info for a task\ntype PodNetworkInfo struct {\n\tHostName    string      `json:\"hostName\"`\n\tHostPorts   []int       `json:\"hostPorts\"`\n\tIPAddresses []IPAddress `json:\"ipAddresses\"`\n}\n\n// DeletePodInstances deletes all instances of the named pod\nfunc (r *marathonClient) DeletePodInstances(name string, instances []string) ([]*PodInstance, error) {\n\turi := buildPodInstancesURI(name)\n\tvar result []*PodInstance\n\tif err := r.apiDelete(uri, instances, &result); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// DeletePodInstance deletes a specific instance of a pod\nfunc (r *marathonClient) DeletePodInstance(name, instance string) (*PodInstance, error) {\n\turi := fmt.Sprintf(\"%s/%s\", buildPodInstancesURI(name), instance)\n\tresult := new(PodInstance)\n\tif err := r.apiDelete(uri, nil, result); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\nfunc buildPodInstancesURI(path string) string {\n\treturn fmt.Sprintf(\"%s/%s::instances\", marathonAPIPods, trimRootPath(path))\n}\n"
  },
  {
    "path": "pod_instance_status.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\n// PodInstanceState is the state of a specific pod instance\ntype PodInstanceState string\n\nconst (\n\t// PodInstanceStatePending is when an instance is pending scheduling\n\tPodInstanceStatePending PodInstanceState = \"PENDING\"\n\n\t// PodInstanceStateStaging is when an instance is staged to be scheduled\n\tPodInstanceStateStaging PodInstanceState = \"STAGING\"\n\n\t// PodInstanceStateStable is when an instance is stably running\n\tPodInstanceStateStable PodInstanceState = \"STABLE\"\n\n\t// PodInstanceStateDegraded is when an instance is degraded status\n\tPodInstanceStateDegraded PodInstanceState = \"DEGRADED\"\n\n\t// PodInstanceStateTerminal is when an instance is terminal\n\tPodInstanceStateTerminal PodInstanceState = \"TERMINAL\"\n)\n\n// PodInstanceStatus is the status of a pod instance\ntype PodInstanceStatus struct {\n\tAgentHostname string              `json:\"agentHostname,omitempty\"`\n\tConditions    []*StatusCondition  `json:\"conditions,omitempty\"`\n\tContainers    []*ContainerStatus  `json:\"containers,omitempty\"`\n\tID            string              `json:\"id,omitempty\"`\n\tLastChanged   string              `json:\"lastChanged,omitempty\"`\n\tLastUpdated   string              `json:\"lastUpdated,omitempty\"`\n\tMessage       string              `json:\"message,omitempty\"`\n\tNetworks      []*PodNetworkStatus `json:\"networks,omitempty\"`\n\tResources     *Resources          `json:\"resources,omitempty\"`\n\tSpecReference string              `json:\"specReference,omitempty\"`\n\tStatus        PodInstanceState    `json:\"status,omitempty\"`\n\tStatusSince   string              `json:\"statusSince,omitempty\"`\n}\n\n// PodNetworkStatus is the networks attached to a pod instance\ntype PodNetworkStatus struct {\n\tAddresses []string `json:\"addresses,omitempty\"`\n\tName      string   `json:\"name,omitempty\"`\n}\n\n// StatusCondition describes info about a status change\ntype StatusCondition struct {\n\tName        string `json:\"name,omitempty\"`\n\tValue       string `json:\"value,omitempty\"`\n\tReason      string `json:\"reason,omitempty\"`\n\tLastChanged string `json:\"lastChanged,omitempty\"`\n\tLastUpdated string `json:\"lastUpdated,omitempty\"`\n}\n\n// ContainerStatus contains all status information for a container instance\ntype ContainerStatus struct {\n\tConditions  []*StatusCondition         `json:\"conditions,omitempty\"`\n\tContainerID string                     `json:\"containerId,omitempty\"`\n\tEndpoints   []*PodEndpoint             `json:\"endpoints,omitempty\"`\n\tLastChanged string                     `json:\"lastChanged,omitempty\"`\n\tLastUpdated string                     `json:\"lastUpdated,omitempty\"`\n\tMessage     string                     `json:\"message,omitempty\"`\n\tName        string                     `json:\"name,omitempty\"`\n\tResources   *Resources                 `json:\"resources,omitempty\"`\n\tStatus      string                     `json:\"status,omitempty\"`\n\tStatusSince string                     `json:\"statusSince,omitempty\"`\n\tTermination *ContainerTerminationState `json:\"termination,omitempty\"`\n}\n\n// ContainerTerminationState describes why a container terminated\ntype ContainerTerminationState struct {\n\tExitCode int    `json:\"exitCode,omitempty\"`\n\tMessage  string `json:\"message,omitempty\"`\n}\n"
  },
  {
    "path": "pod_instance_test.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst fakePodInstanceName = \"fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003\"\n\nfunc TestDeletePodInstance(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tpodInstance, err := endpoint.Client.DeletePodInstance(fakePodName, fakePodInstanceName)\n\trequire.NoError(t, err)\n\tassert.Equal(t, podInstance.InstanceID.ID, fakePodInstanceName)\n}\n\nfunc TestDeletePodInstances(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tinstances := []string{fakePodInstanceName}\n\tpodInstances, err := endpoint.Client.DeletePodInstances(fakePodName, instances)\n\trequire.NoError(t, err)\n\tassert.Equal(t, podInstances[0].InstanceID.ID, fakePodInstanceName)\n}\n"
  },
  {
    "path": "pod_marshalling.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n)\n\n// PodAlias aliases the Pod struct so that it will be marshaled/unmarshaled automatically\ntype PodAlias Pod\n\n// UnmarshalJSON unmarshals the given Pod JSON as expected except for environment variables and secrets.\n// Environment variables are stored in the Env field. Secrets, including the environment variable part,\n// are stored in the Secrets field.\nfunc (p *Pod) UnmarshalJSON(b []byte) error {\n\taux := &struct {\n\t\t*PodAlias\n\t\tEnv     map[string]interface{} `json:\"environment\"`\n\t\tSecrets map[string]TmpSecret   `json:\"secrets\"`\n\t}{\n\t\tPodAlias: (*PodAlias)(p),\n\t}\n\tif err := json.Unmarshal(b, aux); err != nil {\n\t\treturn fmt.Errorf(\"malformed pod definition %v\", err)\n\t}\n\tenv := map[string]string{}\n\tsecrets := map[string]Secret{}\n\n\tfor envName, genericEnvValue := range aux.Env {\n\t\tswitch envValOrSecret := genericEnvValue.(type) {\n\t\tcase string:\n\t\t\tenv[envName] = envValOrSecret\n\t\tcase map[string]interface{}:\n\t\t\tfor secret, secretStore := range envValOrSecret {\n\t\t\t\tif secStore, ok := secretStore.(string); ok && secret == \"secret\" {\n\t\t\t\t\tsecrets[secStore] = Secret{EnvVar: envName}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\treturn fmt.Errorf(\"unexpected secret field %v of value type %T\", secret, envValOrSecret[secret])\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"unexpected environment variable type %T\", envValOrSecret)\n\t\t}\n\t}\n\tp.Env = env\n\tfor k, v := range aux.Secrets {\n\t\ttmp := secrets[k]\n\t\ttmp.Source = v.Source\n\t\tsecrets[k] = tmp\n\t}\n\tp.Secrets = secrets\n\treturn nil\n}\n\n// MarshalJSON marshals the given Pod as expected except for environment variables and secrets,\n// which are marshaled from specialized structs.  The environment variable piece of the secrets and other\n// normal environment variables are combined and marshaled to the env field.  The secrets and the related\n// source are marshaled into the secrets field.\nfunc (p *Pod) MarshalJSON() ([]byte, error) {\n\tenv := make(map[string]interface{})\n\tsecrets := make(map[string]TmpSecret)\n\n\tif p.Env != nil {\n\t\tfor k, v := range p.Env {\n\t\t\tenv[string(k)] = string(v)\n\t\t}\n\t}\n\tif p.Secrets != nil {\n\t\tfor k, v := range p.Secrets {\n\t\t\t// Only add it to the root level pod environment if it's used\n\t\t\t// Otherwise it's likely in one of the container environments\n\t\t\tif v.EnvVar != \"\" {\n\t\t\t\tenv[v.EnvVar] = TmpEnvSecret{Secret: k}\n\t\t\t}\n\t\t\tsecrets[k] = TmpSecret{v.Source}\n\t\t}\n\t}\n\taux := &struct {\n\t\t*PodAlias\n\t\tEnv     map[string]interface{} `json:\"environment,omitempty\"`\n\t\tSecrets map[string]TmpSecret   `json:\"secrets,omitempty\"`\n\t}{PodAlias: (*PodAlias)(p), Env: env, Secrets: secrets}\n\n\treturn json.Marshal(aux)\n}\n"
  },
  {
    "path": "pod_marshalling_test.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPodEnvironmentVariableUnmarshal(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tpod, err := endpoint.Client.Pod(fakePodName)\n\trequire.NoError(t, err)\n\n\tenv := pod.Env\n\tsecrets := pod.Secrets\n\n\trequire.NotNil(t, env)\n\tassert.Equal(t, \"value\", env[\"key1\"])\n\tassert.Equal(t, \"key2\", secrets[\"secret0\"].EnvVar)\n\tassert.Equal(t, \"source0\", secrets[\"secret0\"].Source)\n\n\tassert.Equal(t, \"value3\", pod.Containers[0].Env[\"key3\"])\n\tassert.Equal(t, \"key4\", pod.Containers[0].Secrets[\"secret1\"].EnvVar)\n\tassert.Equal(t, \"source1\", secrets[\"secret1\"].Source)\n}\n\nfunc TestPodMalformedPayloadUnmarshal(t *testing.T) {\n\tvar tests = []struct {\n\t\texpected    string\n\t\tgiven       []byte\n\t\tdescription string\n\t}{\n\t\t{\n\t\t\texpected:    \"unexpected secret field\",\n\t\t\tgiven:       []byte(`{\"environment\": {\"FOO\": \"bar\", \"SECRET\": {\"not_secret\": \"secret1\"}}, \"secrets\": {\"secret1\": {\"source\": \"/path/to/secret\"}}}`),\n\t\t\tdescription: \"Field in environment secret not equal to secret.\",\n\t\t},\n\t\t{\n\t\t\texpected:    \"unexpected secret field\",\n\t\t\tgiven:       []byte(`{\"environment\": {\"FOO\": \"bar\", \"SECRET\": {\"secret\": 1}}, \"secrets\": {\"secret1\": {\"source\": \"/path/to/secret\"}}}`),\n\t\t\tdescription: \"Invalid value in environment secret.\",\n\t\t},\n\t\t{\n\t\t\texpected:    \"unexpected environment variable type\",\n\t\t\tgiven:       []byte(`{\"environment\": {\"FOO\": 1, \"SECRET\": {\"secret\": \"secret1\"}}, \"secrets\": {\"secret1\": {\"source\": \"/path/to/secret\"}}}`),\n\t\t\tdescription: \"Invalid environment variable type.\",\n\t\t},\n\t\t{\n\t\t\texpected:    \"malformed pod definition\",\n\t\t\tgiven:       []byte(`{\"environment\": \"value\"}`),\n\t\t\tdescription: \"Bad pod definition.\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\ttmpPod := new(Pod)\n\n\t\terr := json.Unmarshal(test.given, &tmpPod)\n\t\tif assert.Error(t, err, test.description) {\n\t\t\tassert.True(t, strings.HasPrefix(err.Error(), test.expected), test.description)\n\t\t}\n\t}\n}\n\nfunc TestPodEnvironmentVariableMarshal(t *testing.T) {\n\ttestPod := new(Pod)\n\ttargetString := []byte(`{\"containers\":[{\"lifecycle\":{},\"environment\":{\"FOO2\":\"bar2\",\"TOP2\":\"secret1\"}}],\"environment\":{\"FOO\":\"bar\",\"TOP\":{\"secret\":\"secret1\"}},\"secrets\":{\"secret1\":{\"source\":\"/path/to/secret\"}}}`)\n\n\ttestPod.AddEnv(\"FOO\", \"bar\")\n\ttestPod.AddSecret(\"TOP\", \"secret1\", \"/path/to/secret\")\n\n\ttestContainer := new(PodContainer)\n\ttestContainer.AddSecret(\"TOP2\", \"secret1\")\n\ttestContainer.AddEnv(\"FOO2\", \"bar2\")\n\ttestPod.AddContainer(testContainer)\n\n\tpod, err := json.Marshal(testPod)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, targetString, pod)\n\t}\n}\n\nfunc TestPodContainerArtifactBoolMarshal(t *testing.T) {\n\ttargetString := `{\"containers\":[{\"artifacts\":[{\"extract\":false}],\"lifecycle\":{}}]}`\n\n\ttestPod := new(Pod)\n\ttestArtifact := new(PodArtifact)\n\ttestArtifact.Extract = Bool(false)\n\ttestContainer := new(PodContainer)\n\ttestContainer.AddArtifact(testArtifact)\n\ttestPod.AddContainer(testContainer)\n\n\tpod, err := json.Marshal(testPod)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, targetString, string(pod))\n\t}\n}\n"
  },
  {
    "path": "pod_scheduling.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\n// PodBackoff describes the backoff for re-run attempts of a pod\ntype PodBackoff struct {\n\tBackoff        *float64 `json:\"backoff,omitempty\"`\n\tBackoffFactor  *float64 `json:\"backoffFactor,omitempty\"`\n\tMaxLaunchDelay *float64 `json:\"maxLaunchDelay,omitempty\"`\n}\n\n// PodUpgrade describes the policy for upgrading a pod in-place\ntype PodUpgrade struct {\n\tMinimumHealthCapacity *float64 `json:\"minimumHealthCapacity,omitempty\"`\n\tMaximumOverCapacity   *float64 `json:\"maximumOverCapacity,omitempty\"`\n}\n\n// PodPlacement supports constraining which hosts a pod is placed on\ntype PodPlacement struct {\n\tConstraints           *[]Constraint `json:\"constraints\"`\n\tAcceptedResourceRoles []string      `json:\"acceptedResourceRoles,omitempty\"`\n}\n\n// PodSchedulingPolicy is the overarching pod scheduling policy\ntype PodSchedulingPolicy struct {\n\tBackoff             *PodBackoff          `json:\"backoff,omitempty\"`\n\tUpgrade             *PodUpgrade          `json:\"upgrade,omitempty\"`\n\tPlacement           *PodPlacement        `json:\"placement,omitempty\"`\n\tUnreachableStrategy *UnreachableStrategy `json:\"unreachableStrategy,omitempty\"`\n\tKillSelection       string               `json:\"killSelection,omitempty\"`\n}\n\n// Constraint describes the constraint for pod placement\ntype Constraint struct {\n\tFieldName string `json:\"fieldName\"`\n\tOperator  string `json:\"operator\"`\n\tValue     string `json:\"value,omitempty\"`\n}\n\n// NewPodPlacement creates an empty PodPlacement\nfunc NewPodPlacement() *PodPlacement {\n\treturn &PodPlacement{\n\t\tConstraints:           &[]Constraint{},\n\t\tAcceptedResourceRoles: []string{},\n\t}\n}\n\n// AddConstraint adds a new constraint\n//\t\tconstraints:\tthe constraint definition, one constraint per array element\nfunc (p *PodPlacement) AddConstraint(constraint Constraint) *PodPlacement {\n\tc := *p.Constraints\n\tc = append(c, constraint)\n\tp.Constraints = &c\n\n\treturn p\n}\n\n// NewPodSchedulingPolicy creates an empty PodSchedulingPolicy\nfunc NewPodSchedulingPolicy() *PodSchedulingPolicy {\n\treturn &PodSchedulingPolicy{\n\t\tPlacement: NewPodPlacement(),\n\t}\n}\n\n// NewPodBackoff creates an empty PodBackoff\nfunc NewPodBackoff() *PodBackoff {\n\treturn &PodBackoff{}\n}\n\n// NewPodUpgrade creates a new PodUpgrade\nfunc NewPodUpgrade() *PodUpgrade {\n\treturn &PodUpgrade{}\n}\n\n// SetBackoff sets the base backoff interval for failed pod launches, in seconds\nfunc (p *PodBackoff) SetBackoff(backoffSeconds float64) *PodBackoff {\n\tp.Backoff = &backoffSeconds\n\treturn p\n}\n\n// SetBackoffFactor sets the backoff interval growth factor for failed pod launches\nfunc (p *PodBackoff) SetBackoffFactor(backoffFactor float64) *PodBackoff {\n\tp.BackoffFactor = &backoffFactor\n\treturn p\n}\n\n// SetMaxLaunchDelay sets the maximum backoff interval for failed pod launches, in seconds\nfunc (p *PodBackoff) SetMaxLaunchDelay(maxLaunchDelaySeconds float64) *PodBackoff {\n\tp.MaxLaunchDelay = &maxLaunchDelaySeconds\n\treturn p\n}\n\n// SetMinimumHealthCapacity sets the minimum amount of pod instances for healthy operation, expressed as a fraction of instance count\nfunc (p *PodUpgrade) SetMinimumHealthCapacity(capacity float64) *PodUpgrade {\n\tp.MinimumHealthCapacity = &capacity\n\treturn p\n}\n\n// SetMaximumOverCapacity sets the maximum amount of pod instances above the instance count, expressed as a fraction of instance count\nfunc (p *PodUpgrade) SetMaximumOverCapacity(capacity float64) *PodUpgrade {\n\tp.MaximumOverCapacity = &capacity\n\treturn p\n}\n\n// SetBackoff sets the pod's backoff settings\nfunc (p *PodSchedulingPolicy) SetBackoff(backoff *PodBackoff) *PodSchedulingPolicy {\n\tp.Backoff = backoff\n\treturn p\n}\n\n// SetUpgrade sets the pod's upgrade settings\nfunc (p *PodSchedulingPolicy) SetUpgrade(upgrade *PodUpgrade) *PodSchedulingPolicy {\n\tp.Upgrade = upgrade\n\treturn p\n}\n\n// SetPlacement sets the pod's placement settings\nfunc (p *PodSchedulingPolicy) SetPlacement(placement *PodPlacement) *PodSchedulingPolicy {\n\tp.Placement = placement\n\treturn p\n}\n\n// SetKillSelection sets the pod's kill selection criteria when terminating pod instances\nfunc (p *PodSchedulingPolicy) SetKillSelection(killSelection string) *PodSchedulingPolicy {\n\tp.KillSelection = killSelection\n\treturn p\n}\n\n// SetUnreachableStrategy sets the pod's unreachable strategy for lost instances\nfunc (p *PodSchedulingPolicy) SetUnreachableStrategy(strategy EnabledUnreachableStrategy) *PodSchedulingPolicy {\n\tp.UnreachableStrategy = &UnreachableStrategy{\n\t\tEnabledUnreachableStrategy: strategy,\n\t}\n\treturn p\n}\n\n// SetUnreachableStrategyDisabled disables the pod's unreachable strategy\nfunc (p *PodSchedulingPolicy) SetUnreachableStrategyDisabled() *PodSchedulingPolicy {\n\tp.UnreachableStrategy = &UnreachableStrategy{\n\t\tAbsenceReason: UnreachableStrategyAbsenceReasonDisabled,\n\t}\n\treturn p\n}\n"
  },
  {
    "path": "pod_status.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\n// PodState defines the state of a pod\ntype PodState string\n\nconst (\n\t// PodStateDegraded is a degraded pod\n\tPodStateDegraded PodState = \"DEGRADED\"\n\n\t// PodStateStable is a stable pod\n\tPodStateStable PodState = \"STABLE\"\n\n\t// PodStateTerminal is a terminal pod\n\tPodStateTerminal PodState = \"TERMINAL\"\n)\n\n// PodStatus describes the pod status\ntype PodStatus struct {\n\tID                 string                   `json:\"id,omitempty\"`\n\tSpec               *Pod                     `json:\"spec,omitempty\"`\n\tStatus             PodState                 `json:\"status,omitempty\"`\n\tStatusSince        string                   `json:\"statusSince,omitempty\"`\n\tMessage            string                   `json:\"message,omitempty\"`\n\tInstances          []*PodInstanceStatus     `json:\"instances,omitempty\"`\n\tTerminationHistory []*PodTerminationHistory `json:\"terminationHistory,omitempty\"`\n\tLastUpdated        string                   `json:\"lastUpdated,omitempty\"`\n\tLastChanged        string                   `json:\"lastChanged,omitempty\"`\n}\n\n// PodTerminationHistory is the termination history of the pod\ntype PodTerminationHistory struct {\n\tInstanceID   string                         `json:\"instanceId,omitempty\"`\n\tStartedAt    string                         `json:\"startedAt,omitempty\"`\n\tTerminatedAt string                         `json:\"terminatedAt,omitempty\"`\n\tMessage      string                         `json:\"message,omitempty\"`\n\tContainers   []*ContainerTerminationHistory `json:\"containers,omitempty\"`\n}\n\n// ContainerTerminationHistory is the termination history of a container in a pod\ntype ContainerTerminationHistory struct {\n\tContainerID    string                     `json:\"containerId,omitempty\"`\n\tLastKnownState string                     `json:\"lastKnownState,omitempty\"`\n\tTermination    *ContainerTerminationState `json:\"termination,omitempty\"`\n}\n\n// PodStatus retrieves the pod configuration from marathon\nfunc (r *marathonClient) PodStatus(name string) (*PodStatus, error) {\n\tvar podStatus PodStatus\n\n\tif err := r.apiGet(buildPodStatusURI(name), nil, &podStatus); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &podStatus, nil\n}\n\n// PodStatuses retrieves all pod configuration from marathon\nfunc (r *marathonClient) PodStatuses() ([]*PodStatus, error) {\n\tvar podStatuses []*PodStatus\n\n\tif err := r.apiGet(buildPodStatusURI(\"\"), nil, &podStatuses); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn podStatuses, nil\n}\n\n// WaitOnPod blocks until a pod to be deployed\nfunc (r *marathonClient) WaitOnPod(name string, timeout time.Duration) error {\n\treturn r.wait(name, timeout, r.PodIsRunning)\n}\n\n// PodIsRunning returns whether the pod is stably running\nfunc (r *marathonClient) PodIsRunning(name string) bool {\n\tpodStatus, err := r.PodStatus(name)\n\tif apiErr, ok := err.(*APIError); ok && apiErr.ErrCode == ErrCodeNotFound {\n\t\treturn false\n\t}\n\tif err == nil && podStatus.Status == PodStateStable {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc buildPodStatusURI(path string) string {\n\treturn fmt.Sprintf(\"%s/%s::status\", marathonAPIPods, trimRootPath(path))\n}\n"
  },
  {
    "path": "pod_status_test.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetPodStatus(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tpodStatus, err := endpoint.Client.PodStatus(fakePodName)\n\trequire.NoError(t, err)\n\n\tif assert.NotNil(t, podStatus) {\n\t\tassert.Equal(t, podStatus.Spec.ID, fakePodName)\n\t}\n}\n\nfunc TestGetAllPodStatus(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tpodStatuses, err := endpoint.Client.PodStatuses()\n\trequire.NoError(t, err)\n\tassert.Equal(t, podStatuses[0].Spec.ID, fakePodName)\n}\n\nfunc TestWaitOnPod(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\terr := endpoint.Client.WaitOnPod(fakePodName, 1*time.Microsecond)\n\trequire.NoError(t, err)\n}\n\nfunc TestPodIsRunning(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\texists := endpoint.Client.PodIsRunning(fakePodName)\n\tassert.True(t, exists)\n\n\texists = endpoint.Client.PodIsRunning(\"not_existing\")\n\tassert.False(t, exists)\n\n\texists = endpoint.Client.PodIsRunning(secondFakePodName)\n\tassert.False(t, exists)\n}\n"
  },
  {
    "path": "pod_test.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst key = \"testKey\"\nconst val = \"testValue\"\n\nconst fakePodName = \"/fake-pod\"\nconst secondFakePodName = \"/fake-pod2\"\n\nfunc TestPodLabels(t *testing.T) {\n\tpod := NewPod()\n\tpod.AddLabel(key, val)\n\tif assert.Equal(t, len(pod.Labels), 1) {\n\t\tassert.Equal(t, pod.Labels[key], val)\n\t}\n\n\tpod.EmptyLabels()\n\tassert.Equal(t, len(pod.Labels), 0)\n}\n\nfunc TestPodEnvironmentVars(t *testing.T) {\n\tpod := NewPod()\n\tpod.AddEnv(key, val)\n\n\tnewVal, ok := pod.Env[key]\n\tassert.Equal(t, newVal, val)\n\tassert.Equal(t, ok, true)\n\n\tbadVal, ok := pod.Env[\"fakeKey\"]\n\tassert.Equal(t, badVal, \"\")\n\tassert.Equal(t, ok, false)\n\n\tpod.EmptyEnvs()\n\tassert.Equal(t, len(pod.Env), 0)\n}\n\nfunc TestSecrets(t *testing.T) {\n\tpod := NewPod()\n\tpod.AddSecret(\"randomVar\", key, val)\n\n\tnewVal, err := pod.GetSecretSource(key)\n\tassert.Equal(t, newVal, val)\n\tassert.Equal(t, err, nil)\n\n\tbadVal, err := pod.GetSecretSource(\"fakeKey\")\n\tassert.Equal(t, badVal, \"\")\n\tassert.NotNil(t, err)\n\n\tpod.EmptySecrets()\n\tassert.Equal(t, len(pod.Env), 0)\n}\n\nfunc TestSupportsPod(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\n\tsupports, err := endpoint.Client.SupportsPods()\n\tif assert.Nil(t, err) {\n\n\t\tassert.Equal(t, supports, true)\n\t}\n\n\t// Manually closing to test lack of support\n\tendpoint.Close()\n\n\tsupports, err = endpoint.Client.SupportsPods()\n\tassert.NotNil(t, err)\n\tassert.Equal(t, supports, false)\n}\nfunc TestGetPod(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tpod, err := endpoint.Client.Pod(fakePodName)\n\trequire.NoError(t, err)\n\tif assert.NotNil(t, pod) {\n\t\tassert.Equal(t, pod.ID, fakePodName)\n\t}\n}\n\nfunc TestGetAllPods(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tpods, err := endpoint.Client.Pods()\n\trequire.NoError(t, err)\n\tif assert.Equal(t, len(pods), 2) {\n\t\tassert.Equal(t, pods[0].ID, fakePodName)\n\t\tassert.Equal(t, pods[1].ID, secondFakePodName)\n\t}\n}\n\nfunc TestCreatePod(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tpod := NewPod().Name(fakePodName)\n\tpod, err := endpoint.Client.CreatePod(pod)\n\trequire.NoError(t, err)\n\tif assert.NotNil(t, pod) {\n\t\tassert.Equal(t, pod.ID, fakePodName)\n\t}\n}\n\nfunc TestUpdatePod(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tpod := NewPod().Name(fakePodName)\n\tpod, err := endpoint.Client.CreatePod(pod)\n\trequire.NoError(t, err)\n\n\tpod, err = endpoint.Client.UpdatePod(pod, true)\n\trequire.NoError(t, err)\n\n\tif assert.NotNil(t, pod) {\n\t\tassert.Equal(t, pod.ID, fakePodName)\n\t\tassert.Equal(t, pod.Scaling.Instances, 2)\n\t}\n}\n\nfunc TestDeletePod(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tid, err := endpoint.Client.DeletePod(fakePodName, true)\n\trequire.NoError(t, err)\n\n\tif assert.NotNil(t, id) {\n\t\tassert.Equal(t, id.DeploymentID, \"c0e7434c-df47-4d23-99f1-78bd78662231\")\n\t}\n}\n\nfunc TestVersions(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tversions, err := endpoint.Client.PodVersions(fakePodName)\n\trequire.NoError(t, err)\n\tassert.Equal(t, versions[0], \"2014-08-18T22:36:41.451Z\")\n}\n\nfunc TestGetPodByVersion(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tpod, err := endpoint.Client.PodByVersion(fakePodName, \"2014-08-18T22:36:41.451Z\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, pod.ID, fakePodName)\n}\n\nfunc TestAddPodImagePullConfig(t *testing.T) {\n\tcontainer := new(PodContainer)\n\tcontainer.Image = new(PodContainerImage)\n\tpullConfig := NewPullConfig(\"pullConfig-secret\")\n\n\tcontainer.Image.SetPullConfig(pullConfig)\n\n\tif assert.NotNil(t, container.Image.PullConfig) {\n\t\tassert.Equal(t, \"pullConfig-secret\", container.Image.PullConfig.Secret)\n\t}\n}\n"
  },
  {
    "path": "port_definition.go",
    "content": "/*\nCopyright 2016 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\n// PortDefinition is a definition of a port that should be considered\n// part of a resource. Port definitions are necessary when you are\n// using HOST networking and no port mappings are specified.\ntype PortDefinition struct {\n\tPort     *int               `json:\"port,omitempty\"`\n\tProtocol string             `json:\"protocol,omitempty\"`\n\tName     string             `json:\"name,omitempty\"`\n\tLabels   *map[string]string `json:\"labels,omitempty\"`\n}\n\n// SetPort sets the given port for the PortDefinition\nfunc (p *PortDefinition) SetPort(port int) *PortDefinition {\n\tif p.Port == nil {\n\t\tp.EmptyPort()\n\t}\n\tp.Port = &port\n\treturn p\n}\n\n// EmptyPort sets the port to 0 for the PortDefinition\nfunc (p *PortDefinition) EmptyPort() *PortDefinition {\n\tport := 0\n\tp.Port = &port\n\treturn p\n}\n\n// SetProtocol sets the protocol for the PortDefinition\n// protocol: the protocol as a string\nfunc (p *PortDefinition) SetProtocol(protocol string) *PortDefinition {\n\tp.Protocol = protocol\n\treturn p\n}\n\n// SetName sets the name for the PortDefinition\n// name: the name of the PortDefinition\nfunc (p *PortDefinition) SetName(name string) *PortDefinition {\n\tp.Name = name\n\treturn p\n}\n\n// AddLabel adds a label to the PortDefinition\n//\t\tname: the name of the label\n//\t\tvalue: value for this label\nfunc (p *PortDefinition) AddLabel(name, value string) *PortDefinition {\n\tif p.Labels == nil {\n\t\tp.EmptyLabels()\n\t}\n\t(*p.Labels)[name] = value\n\n\treturn p\n}\n\n// EmptyLabels explicitly empties the labels -- use this if you need to empty\n// the labels of a PortDefinition that already has labels set\n// (setting labels to nill will keep the current value)\nfunc (p *PortDefinition) EmptyLabels() *PortDefinition {\n\tp.Labels = &map[string]string{}\n\n\treturn p\n}\n"
  },
  {
    "path": "queue.go",
    "content": "/*\nCopyright 2016 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"fmt\"\n)\n\n// Queue is the definition of marathon queue\ntype Queue struct {\n\tItems []Item `json:\"queue\"`\n}\n\n// Item is the definition of element in the queue\ntype Item struct {\n\tCount                  int                    `json:\"count\"`\n\tDelay                  Delay                  `json:\"delay\"`\n\tApplication            *Application           `json:\"app\"`\n\tPod                    *Pod                   `json:\"pod\"`\n\tRole                   string                 `json:\"role\"`\n\tSince                  string                 `json:\"since\"`\n\tProcessedOffersSummary ProcessedOffersSummary `json:\"processedOffersSummary\"`\n\tLastUnusedOffers       []UnusedOffer          `json:\"lastUnusedOffers,omitempty\"`\n}\n\n// Delay cotains the application postpone information\ntype Delay struct {\n\tOverdue         bool `json:\"overdue\"`\n\tTimeLeftSeconds int  `json:\"timeLeftSeconds\"`\n}\n\n// ProcessedOffersSummary contains statistics for processed offers.\ntype ProcessedOffersSummary struct {\n\tProcessedOffersCount       int32               `json:\"processedOffersCount\"`\n\tUnusedOffersCount          int32               `json:\"unusedOffersCount\"`\n\tLastUnusedOfferAt          *string             `json:\"lastUnusedOfferAt,omitempty\"`\n\tLastUsedOfferAt            *string             `json:\"lastUsedOfferAt,omitempty\"`\n\tRejectSummaryLastOffers    []DeclinedOfferStep `json:\"rejectSummaryLastOffers,omitempty\"`\n\tRejectSummaryLaunchAttempt []DeclinedOfferStep `json:\"rejectSummaryLaunchAttempt,omitempty\"`\n}\n\n// DeclinedOfferStep contains how often an offer was declined for a specific reason\ntype DeclinedOfferStep struct {\n\tReason    string `json:\"reason\"`\n\tDeclined  int32  `json:\"declined\"`\n\tProcessed int32  `json:\"processed\"`\n}\n\n// UnusedOffer contains which offers weren't used and why\ntype UnusedOffer struct {\n\tOffer     Offer    `json:\"offer\"`\n\tReason    []string `json:\"reason\"`\n\tTimestamp string   `json:\"timestamp\"`\n}\n\n// Queue retrieves content of the marathon launch queue\nfunc (r *marathonClient) Queue() (*Queue, error) {\n\tvar queue *Queue\n\terr := r.apiGet(marathonAPIQueue, nil, &queue)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn queue, nil\n}\n\n// DeleteQueueDelay resets task launch delay of the specific application\n//\t\tappID:\t\tthe ID of the application\nfunc (r *marathonClient) DeleteQueueDelay(appID string) error {\n\tpath := fmt.Sprintf(\"%s/%s/delay\", marathonAPIQueue, trimRootPath(appID))\n\treturn r.apiDelete(path, nil, nil)\n}\n"
  },
  {
    "path": "queue_test.go",
    "content": "/*\nCopyright 2016 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestQueue(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tqueue, err := endpoint.Client.Queue()\n\tassert.NoError(t, err)\n\tassert.NotNil(t, queue)\n\n\tassert.Len(t, queue.Items, 1)\n\titem := queue.Items[0]\n\tassert.Equal(t, item.Count, 10)\n\tassert.Equal(t, item.Delay.Overdue, true)\n\tassert.Equal(t, item.Delay.TimeLeftSeconds, 784)\n\tassert.NotEmpty(t, item.Application.ID)\n}\n\nfunc TestDeleteQueueDelay(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\terr := endpoint.Client.DeleteQueueDelay(fakeAppName)\n\tassert.NoError(t, err)\n}\n"
  },
  {
    "path": "readiness.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport \"time\"\n\n// ReadinessCheck represents a readiness check.\ntype ReadinessCheck struct {\n\tName                    *string `json:\"name,omitempty\"`\n\tProtocol                string  `json:\"protocol,omitempty\"`\n\tPath                    string  `json:\"path,omitempty\"`\n\tPortName                string  `json:\"portName,omitempty\"`\n\tIntervalSeconds         int     `json:\"intervalSeconds,omitempty\"`\n\tTimeoutSeconds          int     `json:\"timeoutSeconds,omitempty\"`\n\tHTTPStatusCodesForReady *[]int  `json:\"httpStatusCodesForReady,omitempty\"`\n\tPreserveLastResponse    *bool   `json:\"preserveLastResponse,omitempty\"`\n}\n\n// SetName sets the name on the readiness check.\nfunc (rc *ReadinessCheck) SetName(name string) *ReadinessCheck {\n\trc.Name = &name\n\treturn rc\n}\n\n// SetProtocol sets the protocol on the readiness check.\nfunc (rc *ReadinessCheck) SetProtocol(proto string) *ReadinessCheck {\n\trc.Protocol = proto\n\treturn rc\n}\n\n// SetPath sets the path on the readiness check.\nfunc (rc *ReadinessCheck) SetPath(p string) *ReadinessCheck {\n\trc.Path = p\n\treturn rc\n}\n\n// SetPortName sets the port name on the readiness check.\nfunc (rc *ReadinessCheck) SetPortName(name string) *ReadinessCheck {\n\trc.PortName = name\n\treturn rc\n}\n\n// SetInterval sets the interval on the readiness check.\nfunc (rc *ReadinessCheck) SetInterval(interval time.Duration) *ReadinessCheck {\n\tsecs := int(interval.Seconds())\n\trc.IntervalSeconds = secs\n\treturn rc\n}\n\n// SetTimeout sets the timeout on the readiness check.\nfunc (rc *ReadinessCheck) SetTimeout(timeout time.Duration) *ReadinessCheck {\n\tsecs := int(timeout.Seconds())\n\trc.TimeoutSeconds = secs\n\treturn rc\n}\n\n// SetHTTPStatusCodesForReady sets the HTTP status codes for ready on the\n// readiness check.\nfunc (rc *ReadinessCheck) SetHTTPStatusCodesForReady(codes []int) *ReadinessCheck {\n\trc.HTTPStatusCodesForReady = &codes\n\treturn rc\n}\n\n// SetPreserveLastResponse sets the preserve last response flag on the\n// readiness check.\nfunc (rc *ReadinessCheck) SetPreserveLastResponse(preserve bool) *ReadinessCheck {\n\trc.PreserveLastResponse = &preserve\n\treturn rc\n}\n\n// ReadinessLastResponse holds the result of the last response embedded in a\n// readiness check result.\ntype ReadinessLastResponse struct {\n\tBody        string `json:\"body\"`\n\tContentType string `json:\"contentType\"`\n\tStatus      int    `json:\"status\"`\n}\n\n// ReadinessCheckResult is the result of a readiness check.\ntype ReadinessCheckResult struct {\n\tName         string                `json:\"name\"`\n\tTaskID       string                `json:\"taskId\"`\n\tReady        bool                  `json:\"ready\"`\n\tLastResponse ReadinessLastResponse `json:\"lastResponse,omitempty\"`\n}\n"
  },
  {
    "path": "readiness_test.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestReadinessCheck(t *testing.T) {\n\trc := ReadinessCheck{}\n\trc.SetName(\"readiness\").\n\t\tSetProtocol(\"HTTP\").\n\t\tSetPath(\"/ready\").\n\t\tSetPortName(\"http\").\n\t\tSetInterval(3 * time.Second).\n\t\tSetTimeout(5 * time.Second).\n\t\tSetHTTPStatusCodesForReady([]int{200, 201}).\n\t\tSetPreserveLastResponse(true)\n\n\tif assert.NotNil(t, rc.Name) {\n\t\tassert.Equal(t, \"readiness\", *rc.Name)\n\t}\n\tassert.Equal(t, rc.Protocol, \"HTTP\")\n\tassert.Equal(t, rc.Path, \"/ready\")\n\tassert.Equal(t, rc.PortName, \"http\")\n\tassert.Equal(t, rc.IntervalSeconds, 3)\n\tassert.Equal(t, rc.TimeoutSeconds, 5)\n\tif assert.NotNil(t, rc.HTTPStatusCodesForReady) {\n\t\tassert.Equal(t, *rc.HTTPStatusCodesForReady, []int{200, 201})\n\t}\n\tif assert.NotNil(t, rc.PreserveLastResponse) {\n\t\tassert.True(t, *rc.PreserveLastResponse)\n\t}\n}\n"
  },
  {
    "path": "residency.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport \"time\"\n\n// TaskLostBehaviorType sets action taken when the resident task is lost\ntype TaskLostBehaviorType string\n\nconst (\n\t// TaskLostBehaviorTypeWaitForever indicates to not take any action when the resident task is lost\n\tTaskLostBehaviorTypeWaitForever TaskLostBehaviorType = \"WAIT_FOREVER\"\n\t// TaskLostBehaviorTypeRelaunchAfterTimeout indicates to try relaunching the lost resident task on\n\t// another node after the relaunch escalation timeout has elapsed\n\tTaskLostBehaviorTypeRelaunchAfterTimeout TaskLostBehaviorType = \"RELAUNCH_AFTER_TIMEOUT\"\n)\n\n// Residency defines how terminal states of tasks with local persistent volumes are handled\ntype Residency struct {\n\tTaskLostBehavior                 TaskLostBehaviorType `json:\"taskLostBehavior,omitempty\"`\n\tRelaunchEscalationTimeoutSeconds int                  `json:\"relaunchEscalationTimeoutSeconds,omitempty\"`\n}\n\n// SetTaskLostBehavior sets the residency behavior\nfunc (r *Residency) SetTaskLostBehavior(behavior TaskLostBehaviorType) *Residency {\n\tr.TaskLostBehavior = behavior\n\treturn r\n}\n\n// SetRelaunchEscalationTimeout sets the residency relaunch escalation timeout with seconds precision\nfunc (r *Residency) SetRelaunchEscalationTimeout(timeout time.Duration) *Residency {\n\tr.RelaunchEscalationTimeoutSeconds = int(timeout.Seconds())\n\treturn r\n}\n"
  },
  {
    "path": "residency_test.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestResidency(t *testing.T) {\n\tapp := NewDockerApplication()\n\n\tapp = app.SetResidency(TaskLostBehaviorTypeWaitForever)\n\n\tif assert.NotNil(t, app.Residency) {\n\t\tres := app.Residency\n\n\t\tassert.Equal(t, res.TaskLostBehavior, TaskLostBehaviorTypeWaitForever)\n\n\t\tres.SetRelaunchEscalationTimeout(2525 * time.Millisecond)\n\t\t// should be trimmed to seconds precision\n\t\tassert.Equal(t, app.Residency.RelaunchEscalationTimeoutSeconds, 2)\n\n\t\tres.SetTaskLostBehavior(TaskLostBehaviorTypeRelaunchAfterTimeout)\n\t\tassert.Equal(t, res.TaskLostBehavior, TaskLostBehaviorTypeRelaunchAfterTimeout)\n\t}\n\n\tapp = app.EmptyResidency()\n\n\tif assert.NotNil(t, app.Residency) {\n\t\tassert.Equal(t, app.Residency, &Residency{})\n\t}\n}\n"
  },
  {
    "path": "resources.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\n// ExecutorResources are the resources supported by an executor (a task running a pod)\ntype ExecutorResources struct {\n\tCpus float64 `json:\"cpus,omitempty\"`\n\tMem  float64 `json:\"mem,omitempty\"`\n\tDisk float64 `json:\"disk,omitempty\"`\n}\n\n// Resources are the full set of resources for a task\ntype Resources struct {\n\tCpus float64 `json:\"cpus\"`\n\tMem  float64 `json:\"mem\"`\n\tDisk float64 `json:\"disk,omitempty\"`\n\tGpus int32   `json:\"gpus,omitempty\"`\n}\n\n// NewResources creates an empty Resources\nfunc NewResources() *Resources {\n\treturn &Resources{}\n}\n"
  },
  {
    "path": "subscription.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/donovanhide/eventsource\"\n)\n\n// Subscriptions is a collection to urls that marathon is implementing a callback on\ntype Subscriptions struct {\n\tCallbackURLs []string `json:\"callbackUrls\"`\n}\n\n// Subscriptions retrieves a list of registered subscriptions\nfunc (r *marathonClient) Subscriptions() (*Subscriptions, error) {\n\tsubscriptions := new(Subscriptions)\n\tif err := r.apiGet(marathonAPISubscription, nil, subscriptions); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn subscriptions, nil\n}\n\n// AddEventsListener adds your self as a listener to events from Marathon\n//\t\tchannel:\ta EventsChannel used to receive event on\nfunc (r *marathonClient) AddEventsListener(filter int) (EventsChannel, error) {\n\tr.Lock()\n\tdefer r.Unlock()\n\n\t// step: someone has asked to start listening to event, we need to register for events\n\t// if we haven't done so already\n\tif err := r.registerSubscription(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tchannel := make(EventsChannel)\n\tr.listeners[channel] = EventsChannelContext{\n\t\tfilter:     filter,\n\t\tdone:       make(chan struct{}, 1),\n\t\tcompletion: &sync.WaitGroup{},\n\t}\n\treturn channel, nil\n}\n\n// RemoveEventsListener removes the channel from the events listeners\n//\t\tchannel:\t\t\tthe channel you are removing\nfunc (r *marathonClient) RemoveEventsListener(channel EventsChannel) {\n\tr.Lock()\n\tdefer r.Unlock()\n\n\tif context, found := r.listeners[channel]; found {\n\t\tclose(context.done)\n\t\tdelete(r.listeners, channel)\n\t\t// step: if there is no one else listening, let's remove ourselves\n\t\t// from the events callback\n\t\tif r.config.EventsTransport == EventsTransportCallback && len(r.listeners) == 0 {\n\t\t\tr.Unsubscribe(r.SubscriptionURL())\n\t\t}\n\n\t\t// step: wait for pending goroutines to finish and close channel\n\t\tgo func(completion *sync.WaitGroup) {\n\t\t\tcompletion.Wait()\n\t\t\tclose(channel)\n\t\t}(context.completion)\n\t}\n}\n\n// SubscriptionURL retrieves the subscription callback URL used when registering\nfunc (r *marathonClient) SubscriptionURL() string {\n\tif r.config.CallbackURL != \"\" {\n\t\treturn fmt.Sprintf(\"%s%s\", r.config.CallbackURL, defaultEventsURL)\n\t}\n\n\treturn fmt.Sprintf(\"http://%s:%d%s\", r.ipAddress, r.config.EventsPort, defaultEventsURL)\n}\n\n// registerSubscription registers ourselves with Marathon to receive events from configured transport facility\nfunc (r *marathonClient) registerSubscription() error {\n\tswitch r.config.EventsTransport {\n\tcase EventsTransportCallback:\n\t\treturn r.registerCallbackSubscription()\n\tcase EventsTransportSSE:\n\t\treturn r.registerSSESubscription()\n\tdefault:\n\t\treturn fmt.Errorf(\"the events transport: %d is not supported\", r.config.EventsTransport)\n\t}\n}\n\nfunc (r *marathonClient) registerCallbackSubscription() error {\n\tif r.eventsHTTP == nil {\n\t\tipAddress, err := getInterfaceAddress(r.config.EventsInterface)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Unable to get the ip address from the interface: %s, error: %s\",\n\t\t\t\tr.config.EventsInterface, err)\n\t\t}\n\n\t\t// step: set the ip address\n\t\tr.ipAddress = ipAddress\n\t\tbinding := fmt.Sprintf(\"%s:%d\", ipAddress, r.config.EventsPort)\n\t\t// step: register the handler\n\t\thttp.HandleFunc(defaultEventsURL, r.handleCallbackEvent)\n\t\t// step: create the http server\n\t\tr.eventsHTTP = &http.Server{\n\t\t\tAddr:           binding,\n\t\t\tHandler:        nil,\n\t\t\tReadTimeout:    10 * time.Second,\n\t\t\tWriteTimeout:   10 * time.Second,\n\t\t\tMaxHeaderBytes: 1 << 20,\n\t\t}\n\n\t\t// @todo need to add a timeout value here\n\t\tlistener, err := net.Listen(\"tcp\", binding)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tgo func() {\n\t\t\tfor {\n\t\t\t\tr.eventsHTTP.Serve(listener)\n\t\t\t}\n\t\t}()\n\t}\n\n\t// step: get the callback url\n\tcallback := r.SubscriptionURL()\n\n\t// step: check if the callback is registered\n\tfound, err := r.HasSubscription(callback)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !found {\n\t\t// step: we need to register ourselves\n\t\tif err := r.Subscribe(callback); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// registerSSESubscription starts a go routine that continuously tries to\n// connect to the SSE stream and to process the received events. To establish\n// the connection it tries the active cluster members until no more member is\n// active. When this happens it will retry to get a connection every 5 seconds.\nfunc (r *marathonClient) registerSSESubscription() error {\n\tif r.subscribedToSSE {\n\t\treturn nil\n\t}\n\n\tif r.config.HTTPSSEClient.Timeout != 0 {\n\t\treturn fmt.Errorf(\n\t\t\t\"global timeout must not be set for SSE connections (found %s) -- remove global timeout from HTTP client or provide separate SSE HTTP client without global timeout\",\n\t\t\tr.config.HTTPSSEClient.Timeout,\n\t\t)\n\t}\n\n\tgo func() {\n\t\tfor {\n\t\t\tstream, err := r.connectToSSE()\n\t\t\tif err != nil {\n\t\t\t\tr.debugLog(\"Error connecting SSE subscription: %s\", err)\n\t\t\t\t<-time.After(5 * time.Second)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\terr = r.listenToSSE(stream)\n\t\t\tstream.Close()\n\t\t\tr.debugLog(\"Error on SSE subscription: %s\", err)\n\t\t}\n\t}()\n\n\tr.subscribedToSSE = true\n\treturn nil\n}\n\n// connectToSSE tries to establish an *eventsource.Stream to any of the Marathon cluster members, marking the\n// member as down on connection failure, until there is no more active member in the cluster.\n// Given the http request can not be built, it will panic as this case should never happen.\nfunc (r *marathonClient) connectToSSE() (*eventsource.Stream, error) {\n\tfor {\n\t\trequest, member, err := r.buildAPIRequest(\"GET\", marathonAPIEventStream, nil)\n\t\tif err != nil {\n\t\t\tswitch err.(type) {\n\t\t\tcase newRequestError:\n\t\t\t\tpanic(fmt.Sprintf(\"Requests for SSE subscriptions should never fail to be created: %s\", err.Error()))\n\t\t\tdefault:\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\t// The event source library manipulates the HTTPClient. So we create a new one and copy\n\t\t// its underlying fields for performance reasons. See note that at least the Transport\n\t\t// should be reused here: https://golang.org/pkg/net/http/#Client\n\t\thttpClient := &http.Client{\n\t\t\tTransport:     r.config.HTTPSSEClient.Transport,\n\t\t\tCheckRedirect: r.config.HTTPSSEClient.CheckRedirect,\n\t\t\tJar:           r.config.HTTPSSEClient.Jar,\n\t\t\tTimeout:       r.config.HTTPSSEClient.Timeout,\n\t\t}\n\n\t\tstream, err := eventsource.SubscribeWith(\"\", httpClient, request)\n\t\tif err != nil {\n\t\t\tr.debugLog(\"Error subscribing to Marathon event stream: %s\", err)\n\t\t\tr.hosts.markDown(member)\n\t\t\tcontinue\n\t\t}\n\n\t\treturn stream, nil\n\t}\n}\n\nfunc (r *marathonClient) listenToSSE(stream *eventsource.Stream) error {\n\tfor {\n\t\tselect {\n\t\tcase ev := <-stream.Events:\n\t\t\tif err := r.handleEvent(ev.Data()); err != nil {\n\t\t\t\tr.debugLog(\"listenToSSE(): failed to handle event: %v\", err)\n\t\t\t}\n\t\tcase err := <-stream.Errors:\n\t\t\treturn err\n\n\t\t}\n\t}\n}\n\n// Subscribe adds a URL to Marathon's callback facility\n//\tcallback\t: the URL you wish to subscribe\nfunc (r *marathonClient) Subscribe(callback string) error {\n\tpath := fmt.Sprintf(\"%s?callbackUrl=%s\", marathonAPISubscription, callback)\n\treturn r.ApiPost(path, \"\", nil)\n\n}\n\n// Unsubscribe removes a URL from Marathon's callback facility\n//\tcallback\t: the URL you wish to unsubscribe\nfunc (r *marathonClient) Unsubscribe(callback string) error {\n\t// step: remove from the list of subscriptions\n\treturn r.apiDelete(fmt.Sprintf(\"%s?callbackUrl=%s\", marathonAPISubscription, callback), nil, nil)\n}\n\n// HasSubscription checks to see a subscription already exists with Marathon\n//\t\tcallback:\t\t\tthe url of the callback\nfunc (r *marathonClient) HasSubscription(callback string) (bool, error) {\n\t// step: generate our events callback\n\tsubscriptions, err := r.Subscriptions()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tfor _, subscription := range subscriptions.CallbackURLs {\n\t\tif callback == subscription {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\treturn false, nil\n}\n\nfunc (r *marathonClient) handleEvent(content string) error {\n\t// step: process and decode the event\n\teventType := new(EventType)\n\terr := json.NewDecoder(strings.NewReader(content)).Decode(eventType)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to decode the event type, content: %s, error: %s\", content, err)\n\t}\n\n\t// step: check whether event type is handled\n\tevent, err := GetEvent(eventType.EventType)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to handle event, type: %s, error: %s\", eventType.EventType, err)\n\t}\n\n\t// step: let's decode message\n\terr = json.NewDecoder(strings.NewReader(content)).Decode(event.Event)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to decode the event, id: %d, error: %s\", event.ID, err)\n\t}\n\n\tr.RLock()\n\tdefer r.RUnlock()\n\n\t// step: check if anyone is listen for this event\n\tfor channel, context := range r.listeners {\n\t\t// step: check if this listener wants this event type\n\t\tif event.ID&context.filter != 0 {\n\t\t\tcontext.completion.Add(1)\n\t\t\tgo func(ch EventsChannel, context EventsChannelContext, e *Event) {\n\t\t\t\tdefer context.completion.Done()\n\t\t\t\tselect {\n\t\t\t\tcase ch <- e:\n\t\t\t\tcase <-context.done:\n\t\t\t\t\t// Terminates goroutine.\n\t\t\t\t}\n\t\t\t}(channel, context, event)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (r *marathonClient) handleCallbackEvent(writer http.ResponseWriter, request *http.Request) {\n\tbody, err := ioutil.ReadAll(request.Body)\n\tif err != nil {\n\t\t// TODO should this return a 500?\n\t\tr.debugLog(\"handleCallbackEvent(): failed to read request body, error: %s\", err)\n\t\treturn\n\t}\n\n\tif err := r.handleEvent(string(body[:])); err != nil {\n\t\t// TODO should this return a 500?\n\t\tr.debugLog(\"handleCallbackEvent(): failed to handle event: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "subscription_test.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\teventPublishTimeout time.Duration = 250 * time.Millisecond\n\tSSEConnectWaitTime  time.Duration = 250 * time.Millisecond\n)\n\ntype testCaseList []testCase\n\nfunc (l testCaseList) find(name string) *testCase {\n\tfor _, testCase := range l {\n\t\tif testCase.name == name {\n\t\t\treturn &testCase\n\t\t}\n\t}\n\treturn nil\n}\n\ntype testCase struct {\n\tname        string\n\tsource      string\n\texpectation interface{}\n}\n\nvar testCases = testCaseList{\n\ttestCase{\n\t\tname: \"status_update_event\",\n\t\tsource: `{\n\t\"eventType\": \"status_update_event\",\n\t\"timestamp\": \"2014-03-01T23:29:30.158Z\",\n\t\"slaveId\": \"20140909-054127-177048842-5050-1494-0\",\n\t\"taskId\": \"my-app_0-1396592784349\",\n\t\"taskStatus\": \"TASK_RUNNING\",\n\t\"appId\": \"/my-app\",\n\t\"host\": \"slave-1234.acme.org\",\n\t\"ports\": [31372],\n\t\"version\": \"2014-04-04T06:26:23.051Z\"\n}`,\n\t\texpectation: &EventStatusUpdate{\n\t\t\tEventType:  \"status_update_event\",\n\t\t\tTimestamp:  \"2014-03-01T23:29:30.158Z\",\n\t\t\tSlaveID:    \"20140909-054127-177048842-5050-1494-0\",\n\t\t\tTaskID:     \"my-app_0-1396592784349\",\n\t\t\tTaskStatus: \"TASK_RUNNING\",\n\t\t\tAppID:      \"/my-app\",\n\t\t\tHost:       \"slave-1234.acme.org\",\n\t\t\tPorts:      []int{31372},\n\t\t\tVersion:    \"2014-04-04T06:26:23.051Z\",\n\t\t},\n\t},\n\ttestCase{\n\t\tname: \"health_status_changed_event\",\n\t\tsource: `{\n\t\"eventType\": \"health_status_changed_event\",\n\t\"timestamp\": \"2014-03-01T23:29:30.158Z\",\n\t\"appId\": \"/my-app\",\n\t\"taskId\": \"my-app_0-1396592784349\",\n\t\"version\": \"2014-04-04T06:26:23.051Z\",\n\t\"alive\": true\n}`,\n\t\texpectation: &EventHealthCheckChanged{\n\t\t\tEventType: \"health_status_changed_event\",\n\t\t\tTimestamp: \"2014-03-01T23:29:30.158Z\",\n\t\t\tAppID:     \"/my-app\",\n\t\t\tTaskID:    \"my-app_0-1396592784349\",\n\t\t\tVersion:   \"2014-04-04T06:26:23.051Z\",\n\t\t\tAlive:     true,\n\t\t},\n\t},\n\ttestCase{\n\t\tname: \"failed_health_check_event\",\n\t\tsource: `{\n\t\"eventType\": \"failed_health_check_event\",\n\t\"timestamp\": \"2014-03-01T23:29:30.158Z\",\n\t\"appId\": \"/my-app\",\n\t\"taskId\": \"my-app_0-1396592784349\",\n\t\"healthCheck\": {\n\t\t\"protocol\": \"HTTP\",\n\t\t\"path\": \"/health\",\n\t\t\"portIndex\": 0,\n\t\t\"gracePeriodSeconds\": 5,\n\t\t\"intervalSeconds\": 10,\n\t\t\"timeoutSeconds\": 10,\n\t\t\"maxConsecutiveFailures\": 3\n\t}\n}`,\n\t\texpectation: &EventFailedHealthCheck{\n\t\t\tEventType: \"failed_health_check_event\",\n\t\t\tTimestamp: \"2014-03-01T23:29:30.158Z\",\n\t\t\tAppID:     \"/my-app\",\n\t\t\tHealthCheck: struct {\n\t\t\t\tGracePeriodSeconds     float64 `json:\"gracePeriodSeconds\"`\n\t\t\t\tIntervalSeconds        float64 `json:\"intervalSeconds\"`\n\t\t\t\tMaxConsecutiveFailures float64 `json:\"maxConsecutiveFailures\"`\n\t\t\t\tPath                   string  `json:\"path\"`\n\t\t\t\tPortIndex              float64 `json:\"portIndex\"`\n\t\t\t\tProtocol               string  `json:\"protocol\"`\n\t\t\t\tTimeoutSeconds         float64 `json:\"timeoutSeconds\"`\n\t\t\t}{\n\t\t\t\tGracePeriodSeconds:     5,\n\t\t\t\tIntervalSeconds:        10,\n\t\t\t\tMaxConsecutiveFailures: 3,\n\t\t\t\tPath:           \"/health\",\n\t\t\t\tPortIndex:      0,\n\t\t\t\tProtocol:       \"HTTP\",\n\t\t\t\tTimeoutSeconds: 10,\n\t\t\t},\n\t\t},\n\t},\n\t// For Marathon 1.1.1 and before\n\ttestCase{\n\t\tname: \"deployment_info\",\n\t\tsource: `{\n\t\"eventType\": \"deployment_info\",\n\t\"timestamp\": \"2016-07-29T08:03:52.542Z\",\n\t\"plan\": {\n\t\t\"id\": \"dcf63e4a-ef27-4816-e865-1730fcb26ac3\",\n\t\t\"version\": \"2016-07-29T08:03:52.542Z\",\n\t\t\"original\": {},\n\t\t\"target\": {},\n\t\t\"steps\": [\n\t\t\t{\n\t\t\t\t\"actions\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"type\": \"ScaleApplication\",\n\t\t\t\t\t\t\"app\": \"/my-app\"\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t}\n\t\t]\n\t},\n\t\"currentStep\": {\n\t\t\"actions\": [\n\t\t\t{\n\t\t\t\t\"type\": \"ScaleApplication\",\n\t\t\t\t\"app\": \"/my-app\"\n\t\t\t}\n\t\t]\n\t}\n}`,\n\t\texpectation: &EventDeploymentInfo{\n\t\t\tEventType: \"deployment_info\",\n\t\t\tTimestamp: \"2016-07-29T08:03:52.542Z\",\n\t\t\tPlan: &DeploymentPlan{\n\t\t\t\tID:       \"dcf63e4a-ef27-4816-e865-1730fcb26ac3\",\n\t\t\t\tVersion:  \"2016-07-29T08:03:52.542Z\",\n\t\t\t\tOriginal: &Group{},\n\t\t\t\tTarget:   &Group{},\n\t\t\t\tSteps: []*StepActions{\n\t\t\t\t\t&StepActions{\n\t\t\t\t\t\tActions: []struct {\n\t\t\t\t\t\t\tAction string `json:\"action\"`\n\t\t\t\t\t\t\tType   string `json:\"type\"`\n\t\t\t\t\t\t\tApp    string `json:\"app\"`\n\t\t\t\t\t\t}{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tType: \"ScaleApplication\",\n\t\t\t\t\t\t\t\tApp:  \"/my-app\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tCurrentStep: &StepActions{\n\t\t\t\tActions: []struct {\n\t\t\t\t\tAction string `json:\"action\"`\n\t\t\t\t\tType   string `json:\"type\"`\n\t\t\t\t\tApp    string `json:\"app\"`\n\t\t\t\t}{\n\t\t\t\t\t{\n\t\t\t\t\t\tType: \"ScaleApplication\",\n\t\t\t\t\t\tApp:  \"/my-app\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\t// For Marathon 1.1.2 and after\n\ttestCase{\n\t\tname: \"deployment_step_success\",\n\t\tsource: `{\n\t\"eventType\": \"deployment_step_success\",\n\t\"timestamp\": \"2016-07-29T08:03:52.542Z\",\n\t\"plan\": {\n\t\t\"id\": \"dcf63e4a-ef27-4816-e865-1730fcb26ac3\",\n\t\t\"version\": \"2016-07-29T08:03:52.542Z\",\n\t\t\"original\": {},\n\t\t\"target\": {},\n\t\t\"steps\": [\n\t\t\t{\n\t\t\t\t\"actions\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"action\": \"ScaleApplication\",\n\t\t\t\t\t\t\"app\": \"/my-app\"\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t}\n\t\t]\n\t},\n\t\"currentStep\": {\n\t\t\"actions\": [\n\t\t\t{\n\t\t\t\t\"action\": \"ScaleApplication\",\n\t\t\t\t\"app\": \"/my-app\"\n\t\t\t}\n\t\t]\n\t}\n}`,\n\t\texpectation: &EventDeploymentInfo{\n\t\t\tEventType: \"deployment_info\",\n\t\t\tTimestamp: \"2016-07-29T08:03:52.542Z\",\n\t\t\tPlan: &DeploymentPlan{\n\t\t\t\tID:       \"dcf63e4a-ef27-4816-e865-1730fcb26ac3\",\n\t\t\t\tVersion:  \"2016-07-29T08:03:52.542Z\",\n\t\t\t\tOriginal: &Group{},\n\t\t\t\tTarget:   &Group{},\n\t\t\t\tSteps: []*StepActions{\n\t\t\t\t\t&StepActions{\n\t\t\t\t\t\tActions: []struct {\n\t\t\t\t\t\t\tAction string `json:\"action\"`\n\t\t\t\t\t\t\tType   string `json:\"type\"`\n\t\t\t\t\t\t\tApp    string `json:\"app\"`\n\t\t\t\t\t\t}{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tAction: \"ScaleApplication\",\n\t\t\t\t\t\t\t\tApp:    \"/my-app\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tCurrentStep: &StepActions{\n\t\t\t\tActions: []struct {\n\t\t\t\t\tAction string `json:\"action\"`\n\t\t\t\t\tType   string `json:\"type\"`\n\t\t\t\t\tApp    string `json:\"app\"`\n\t\t\t\t}{\n\t\t\t\t\t{\n\t\t\t\t\t\tAction: \"ScaleApplication\",\n\t\t\t\t\t\tApp:    \"/my-app\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n}\n\nfunc TestSubscriptions(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tsub, err := endpoint.Client.Subscriptions()\n\tassert.NoError(t, err)\n\tassert.NotNil(t, sub)\n\tassert.NotNil(t, sub.CallbackURLs)\n\tassert.Equal(t, len(sub.CallbackURLs), 1)\n}\n\nfunc TestSubscribe(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\terr := endpoint.Client.Subscribe(\"http://localhost:9292/callback\")\n\tassert.NoError(t, err)\n}\n\nfunc TestUnsubscribe(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\terr := endpoint.Client.Unsubscribe(\"http://localhost:9292/callback\")\n\tassert.NoError(t, err)\n}\n\nfunc TestSSEWithGlobalTimeout(t *testing.T) {\n\tclientCfg := NewDefaultConfig()\n\tclientCfg.HTTPSSEClient = &http.Client{\n\t\tTimeout: 1 * time.Second,\n\t}\n\tconfig := configContainer{\n\t\tclient: &clientCfg,\n\t}\n\tconfig.client.EventsTransport = EventsTransportSSE\n\tendpoint := newFakeMarathonEndpoint(t, &config)\n\tdefer endpoint.Close()\n\n\t_, err := endpoint.Client.AddEventsListener(EventIDApplications)\n\tassert.Error(t, err)\n}\n\nfunc TestEventStreamEventsReceived(t *testing.T) {\n\trequire.True(t, len(testCases) > 1, \"must have at least 2 test cases to end prematurely\")\n\n\tclientCfg := NewDefaultConfig()\n\tconfig := configContainer{\n\t\tclient: &clientCfg,\n\t}\n\tconfig.client.EventsTransport = EventsTransportSSE\n\tendpoint := newFakeMarathonEndpoint(t, &config)\n\tdefer endpoint.Close()\n\n\tevents, err := endpoint.Client.AddEventsListener(EventIDApplications | EventIDDeploymentInfo | EventIDDeploymentStepSuccess)\n\tassert.NoError(t, err)\n\n\talmostAllTestCases := testCases[:len(testCases)-1]\n\tfinalTestCase := testCases[len(testCases)-1]\n\n\t// Give it a bit of time so that the subscription can be set up\n\ttime.Sleep(SSEConnectWaitTime)\n\n\t// Publish all but one test event.\n\tfor _, testCase := range almostAllTestCases {\n\t\tendpoint.Server.PublishEvent(testCase.source)\n\t}\n\n\t// Receive test events.\n\tfor i := 0; i < len(almostAllTestCases); i++ {\n\t\tselect {\n\t\tcase event := <-events:\n\t\t\ttc := testCases.find(event.Name)\n\t\t\tif !assert.NotNil(t, tc, \"received unknown event: %s\", event.Name) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectation, event.Event)\n\t\tcase <-time.After(eventPublishTimeout):\n\t\t\tassert.Fail(t, \"did not receive event in time\")\n\t\t}\n\t}\n\n\t// Publish last test event that we do not intend to consume anymore.\n\tendpoint.Server.PublishEvent(finalTestCase.source)\n\n\t// Give event stream some time to buffer another event.\n\ttime.Sleep(eventPublishTimeout)\n\n\t// Trigger done channel closure.\n\tendpoint.Client.RemoveEventsListener(events)\n\n\t// Give pending goroutine time to consume done signal.\n\ttime.Sleep(eventPublishTimeout)\n\n\t// Validate that channel is closed.\n\tselect {\n\tcase _, more := <-events:\n\t\tassert.False(t, more, \"should not have received another event\")\n\tdefault:\n\t\tassert.Fail(t, \"channel was not closed\")\n\t}\n}\n\nfunc TestConnectToSSESuccess(t *testing.T) {\n\tclientCfg := NewDefaultConfig()\n\t// Use non-existent address as first cluster member\n\tclientCfg.URL = \"http://127.0.0.1:11111\"\n\tclientCfg.EventsTransport = EventsTransportSSE\n\tconfig := configContainer{client: &clientCfg}\n\n\tendpoint := newFakeMarathonEndpoint(t, &config)\n\tdefer endpoint.Close()\n\n\tclient := endpoint.Client.(*marathonClient)\n\t// Add real server as member to the cluster\n\tclient.hosts.members = append(client.hosts.members, &member{endpoint: endpoint.Server.httpSrv.URL})\n\n\t// Connection should work as one of the Marathon members is up\n\tstream, err := client.connectToSSE()\n\tif assert.NoError(t, err, \"expected no error in connectToSSE\") {\n\t\tstream.Close()\n\t}\n}\n\nfunc TestConnectToSSEFailure(t *testing.T) {\n\tclientCfg := NewDefaultConfig()\n\tclientCfg.EventsTransport = EventsTransportSSE\n\tconfig := configContainer{client: &clientCfg}\n\n\tendpoint := newFakeMarathonEndpoint(t, &config)\n\tendpoint.Close()\n\n\tclient := endpoint.Client.(*marathonClient)\n\n\t// No Marathon member is up, we should get an error\n\tstream, err := client.connectToSSE()\n\tif !assert.Error(t, err, \"expected error in connectToSSE when all cluster members are down\") {\n\t\tstream.Close()\n\t}\n}\n\nfunc TestRegisterSEESubscriptionReconnectsStreamOnError(t *testing.T) {\n\tclientCfg := NewDefaultConfig()\n\tclientCfg.HTTPSSEClient = &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tDial: (&net.Dialer{\n\t\t\t\t// set timeout to a small fraction of SSEConnectWaitTime to give client enough time\n\t\t\t\t// to detect error and reconnect during sleep\n\t\t\t\tTimeout: SSEConnectWaitTime / 10,\n\t\t\t}).Dial,\n\t\t},\n\t}\n\tclientCfg.EventsTransport = EventsTransportSSE\n\tconfig := configContainer{client: &clientCfg}\n\n\tendpoint1 := newFakeMarathonEndpoint(t, &config)\n\tendpoint2 := newFakeMarathonEndpoint(t, &config)\n\tdefer endpoint2.Close()\n\n\tclient1 := endpoint1.Client.(*marathonClient)\n\t// Add the second server to the cluster members\n\tclient1.hosts.members = append(client1.hosts.members, &member{endpoint: endpoint2.Server.httpSrv.URL})\n\n\tevents, err := endpoint1.Client.AddEventsListener(EventIDApplications)\n\trequire.NoError(t, err)\n\n\t// Give it a bit of time so that the subscription can be set up\n\ttime.Sleep(SSEConnectWaitTime)\n\n\t// This should make the SSE subscription fail and reconnect to another cluster member\n\tendpoint1.Close()\n\n\t// Give it a bit of time so that the subscription can reconnect\n\ttime.Sleep(SSEConnectWaitTime)\n\n\t// Now that our SSE subscription failed over, we can publish on the second server and the message should be consumed\n\tendpoint2.Server.PublishEvent(testCases[0].source)\n\n\tselect {\n\tcase event := <-events:\n\t\ttc := testCases.find(event.Name)\n\t\tassert.NotNil(t, tc, \"received unknown event: %s\", event.Name)\n\tcase <-time.After(eventPublishTimeout):\n\t\tassert.Fail(t, \"did not receive event in time\")\n\t}\n}\n"
  },
  {
    "path": "task.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n// Tasks is a collection of marathon tasks\ntype Tasks struct {\n\tTasks []Task `json:\"tasks\"`\n}\n\n// Task is the definition for a marathon task\ntype Task struct {\n\tID                 string               `json:\"id\"`\n\tAppID              string               `json:\"appId\"`\n\tHost               string               `json:\"host\"`\n\tHealthCheckResults []*HealthCheckResult `json:\"healthCheckResults\"`\n\tPorts              []int                `json:\"ports\"`\n\tServicePorts       []int                `json:\"servicePorts\"`\n\tSlaveID            string               `json:\"slaveId\"`\n\tStagedAt           string               `json:\"stagedAt\"`\n\tStartedAt          string               `json:\"startedAt\"`\n\tState              string               `json:\"state\"`\n\tIPAddresses        []*IPAddress         `json:\"ipAddresses\"`\n\tVersion            string               `json:\"version\"`\n}\n\n// IPAddress represents a task's IP address and protocol.\ntype IPAddress struct {\n\tIPAddress string `json:\"ipAddress\"`\n\tProtocol  string `json:\"protocol\"`\n}\n\n// AllTasksOpts contains a payload for AllTasks method\n//\t\tstatus:\t\tReturn only those tasks whose status matches this parameter.\n//\t\t\t\tIf not specified, all tasks are returned. Possible values: running, staging. Default: none.\ntype AllTasksOpts struct {\n\tStatus string `url:\"status,omitempty\"`\n}\n\n// KillApplicationTasksOpts contains a payload for KillApplicationTasks method\n//\t\thost:\t\tkill only those tasks on a specific host (optional)\n//\t\tscale:\t\tScale the app down (i.e. decrement its instances setting by the number of tasks killed) after killing the specified tasks\ntype KillApplicationTasksOpts struct {\n\tHost  string `url:\"host,omitempty\"`\n\tScale bool   `url:\"scale,omitempty\"`\n\tForce bool   `url:\"force,omitempty\"`\n}\n\n// KillTaskOpts contains a payload for task killing methods\n//\t\tscale:\t\tScale the app down\ntype KillTaskOpts struct {\n\tScale bool `url:\"scale,omitempty\"`\n\tForce bool `url:\"force,omitempty\"`\n\tWipe  bool `url:\"wipe,omitempty\"`\n}\n\n// HasHealthCheckResults checks if the task has any health checks\nfunc (r *Task) HasHealthCheckResults() bool {\n\treturn r.HealthCheckResults != nil && len(r.HealthCheckResults) > 0\n}\n\n// AllTasks lists tasks of all applications.\n//\t\topts: \t\tAllTasksOpts request payload\nfunc (r *marathonClient) AllTasks(opts *AllTasksOpts) (*Tasks, error) {\n\tpath, err := addOptions(marathonAPITasks, opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttasks := new(Tasks)\n\tif err := r.apiGet(path, nil, tasks); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn tasks, nil\n}\n\n// Tasks retrieves a list of tasks for an application\n//\t\tid:\t\tthe id of the application\nfunc (r *marathonClient) Tasks(id string) (*Tasks, error) {\n\ttasks := new(Tasks)\n\tif err := r.apiGet(fmt.Sprintf(\"%s/%s/tasks\", marathonAPIApps, trimRootPath(id)), nil, tasks); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn tasks, nil\n}\n\n// KillApplicationTasks kills all tasks relating to an application\n//\t\tid:\t\tthe id of the application\n//\t\topts: \t\tKillApplicationTasksOpts request payload\nfunc (r *marathonClient) KillApplicationTasks(id string, opts *KillApplicationTasksOpts) (*Tasks, error) {\n\tpath := fmt.Sprintf(\"%s/%s/tasks\", marathonAPIApps, trimRootPath(id))\n\tpath, err := addOptions(path, opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttasks := new(Tasks)\n\tif err := r.apiDelete(path, nil, tasks); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn tasks, nil\n}\n\n// KillTask kills the task associated with a given ID\n// \ttaskID:\t\tthe id for the task\n//\topts:\t\tKillTaskOpts request payload\nfunc (r *marathonClient) KillTask(taskID string, opts *KillTaskOpts) (*Task, error) {\n\tappName := taskID[0:strings.LastIndex(taskID, \".\")]\n\tappName = strings.Replace(appName, \"_\", \"/\", -1)\n\ttaskID = strings.Replace(taskID, \"/\", \"_\", -1)\n\n\tpath := fmt.Sprintf(\"%s/%s/tasks/%s\", marathonAPIApps, appName, taskID)\n\tpath, err := addOptions(path, opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\twrappedTask := new(struct {\n\t\tTask Task `json:\"task\"`\n\t})\n\n\tif err := r.apiDelete(path, nil, wrappedTask); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &wrappedTask.Task, nil\n}\n\n// KillTasks kills tasks associated with given array of ids\n//\ttasks:\t\tthe array of task ids\n//\topts:\t\tKillTaskOpts request payload\nfunc (r *marathonClient) KillTasks(tasks []string, opts *KillTaskOpts) error {\n\tpath := fmt.Sprintf(\"%s/delete\", marathonAPITasks)\n\tpath, err := addOptions(path, opts)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tvar post struct {\n\t\tIDs []string `json:\"ids\"`\n\t}\n\tpost.IDs = tasks\n\n\treturn r.ApiPost(path, &post, nil)\n}\n\n// TaskEndpoints gets the endpoints i.e. HOST_IP:DYNAMIC_PORT for a specific application service\n// I.e. a container running apache, might have ports 80/443 (translated to X dynamic ports), but i want\n// port 80 only and i only want those whom have passed the health check\n//\n// Note: I've NO IDEA how to associate the health_check_result to the actual port, I don't think it's\n// possible at the moment, however, given marathon will fail and restart an application even if one of x ports of a task is\n// down, the per port check is redundant??? .. personally, I like it anyhow, but hey\n//\n\n//\t\tname:\t\tthe identifier for the application\n//\t\tport:\t\tthe container port you are interested in\n//\t\thealth: \twhether to check the health or not\nfunc (r *marathonClient) TaskEndpoints(name string, port int, healthCheck bool) ([]string, error) {\n\t// step: get the application details\n\tapplication, err := r.Application(name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// step: we need to get the port index of the service we are interested in\n\tportIndex, err := application.Container.Docker.ServicePortIndex(port)\n\tif err != nil {\n\t\tportIndex, err = application.Container.ServicePortIndex(port)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// step: do we have any tasks?\n\tif application.Tasks == nil || len(application.Tasks) == 0 {\n\t\treturn nil, nil\n\t}\n\n\t// step: if we are checking health the 'service' has a health check?\n\thealthCheck = healthCheck && application.HasHealthChecks()\n\n\t// step: iterate the tasks and extract the dynamic ports\n\tvar list []string\n\tfor _, task := range application.Tasks {\n\t\tif !healthCheck || task.allHealthChecksAlive() {\n\t\t\tendpoint := fmt.Sprintf(\"%s:%d\", task.Host, task.Ports[portIndex])\n\t\t\tlist = append(list, endpoint)\n\t\t}\n\t}\n\n\treturn list, nil\n}\n\nfunc (r *Task) allHealthChecksAlive() bool {\n\t// check: does the task have a health check result, if NOT, it's because the\n\t// health of the task hasn't yet been performed, hence we assume it as DOWN\n\tif !r.HasHealthCheckResults() {\n\t\treturn false\n\t}\n\t// step: check the health results then\n\tfor _, check := range r.HealthCheckResults {\n\t\tif !check.Alive {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n"
  },
  {
    "path": "task_test.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestHasHealthCheckResults(t *testing.T) {\n\ttask := Task{}\n\tassert.False(t, task.HasHealthCheckResults())\n\ttask.HealthCheckResults = append(task.HealthCheckResults, &HealthCheckResult{})\n\tassert.True(t, task.HasHealthCheckResults())\n}\n\nfunc TestAllTasks(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\ttasks, err := endpoint.Client.AllTasks(nil)\n\tassert.NoError(t, err)\n\tif assert.NotNil(t, tasks) {\n\t\tassert.Equal(t, len(tasks.Tasks), 2)\n\t}\n\n\ttasks, err = endpoint.Client.AllTasks(&AllTasksOpts{Status: \"staging\"})\n\tassert.Nil(t, err)\n\tif assert.NotNil(t, tasks) {\n\t\tassert.Equal(t, len(tasks.Tasks), 0)\n\t}\n}\n\nfunc TestTasks(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\ttasks, err := endpoint.Client.Tasks(fakeAppName)\n\tassert.NoError(t, err)\n\tif assert.NotNil(t, tasks) {\n\t\tassert.Equal(t, len(tasks.Tasks), 2)\n\t}\n}\n\nfunc TestKillApplicationTasks(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\ttasks, err := endpoint.Client.KillApplicationTasks(fakeAppName, nil)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, tasks)\n}\n\nfunc TestKillTask(t *testing.T) {\n\tcases := map[string]struct {\n\t\tTaskID string\n\t\tResult string\n\t}{\n\t\t\"CommonApp\":           {fakeTaskID, fakeTaskID},\n\t\t\"GroupApp\":            {\"fake-group_fake-app.fake-task\", \"fake-group_fake-app.fake-task\"},\n\t\t\"GroupAppWithSlashes\": {\"fake-group/fake-app.fake-task\", \"fake-group_fake-app.fake-task\"},\n\t}\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tfor k, tc := range cases {\n\t\ttask, err := endpoint.Client.KillTask(tc.TaskID, nil)\n\t\tassert.NoError(t, err, \"TestCase: %s\", k)\n\t\tassert.Equal(t, tc.Result, task.ID, \"TestCase: %s\", k)\n\t}\n}\n\nfunc TestKillTasks(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\terr := endpoint.Client.KillTasks([]string{fakeTaskID}, nil)\n\tassert.NoError(t, err)\n}\n\nfunc TestTaskEndpoints(t *testing.T) {\n\tendpoint := newFakeMarathonEndpoint(t, nil)\n\tdefer endpoint.Close()\n\n\tendpoints, err := endpoint.Client.TaskEndpoints(fakeAppNameBroken, 8080, true)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, endpoints)\n\tassert.Equal(t, len(endpoints), 1, t)\n\tassert.Equal(t, endpoints[0], \"10.141.141.10:31045\", t)\n\n\tendpoints, err = endpoint.Client.TaskEndpoints(fakeAppNameBroken, 8080, false)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, endpoints)\n\tassert.Equal(t, len(endpoints), 2, t)\n\tassert.Equal(t, endpoints[0], \"10.141.141.10:31045\", t)\n\tassert.Equal(t, endpoints[1], \"10.141.141.10:31234\", t)\n\n\t_, err = endpoint.Client.TaskEndpoints(fakeAppNameBroken, 80, true)\n\tassert.Error(t, err)\n}\n"
  },
  {
    "path": "testing_utils_test.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/donovanhide/eventsource\"\n\tyaml \"gopkg.in/yaml.v2\"\n)\n\nconst (\n\tfakeMarathonURL         = \"http://127.0.0.1:3000,127.0.0.1:3000,127.0.0.1:3000\"\n\tfakeMarathonURLWithPath = \"http://127.0.0.1:3000/path,127.0.0.1:3000/path,127.0.0.1:3000/path\"\n\tfakeGroupName           = \"/test\"\n\tfakeGroupName1          = \"/qa/product/1\"\n\tfakeAppName             = \"/fake-app\"\n\tfakeTaskID              = \"fake-app.fake-task\"\n\tfakeAppNameBroken       = \"/fake-app-broken\"\n\tfakeDeploymentID        = \"867ed450-f6a8-4d33-9b0e-e11c5513990b\"\n\tfakeAppNameUnhealthy    = \"/no-health-check-results-app\"\n)\n\nvar (\n\tfakeResponses map[string][]indexedResponse\n\tonce          sync.Once\n)\n\ntype indexedResponse struct {\n\tIndex   int               `yaml:\"index,omitempty\"`\n\tContent string            `yaml:\"content,omitempty\"`\n\tHeaders map[string]string `yaml:\"headers,omitempty\"`\n}\n\ntype responseIndices struct {\n\tsync.Mutex\n\tm map[string]int\n}\n\nfunc newResponseIndices() *responseIndices {\n\treturn &responseIndices{m: map[string]int{}}\n}\n\n// restMethod represents an expected HTTP method and an associated fake response\ntype restMethod struct {\n\t// the uri of the method\n\tURI string `yaml:\"uri,omitempty\"`\n\t// the http method type (GET|PUT etc)\n\tMethod string `yaml:\"method,omitempty\"`\n\t// the content i.e. response\n\tContent string `yaml:\"content,omitempty\"`\n\t// ContentSequence is a sequence of responses that are returned in order.\n\tContentSequence []indexedResponse `yaml:\"contentSequence,omitempty\"`\n\t// the test scope\n\tScope string `yaml:\"scope,omitempty\"`\n\t// headers in the response\n\tHeaders map[string]string `yaml:\"headers,omitempty\"`\n}\n\n// serverConfig holds the Marathon server configuration\ntype serverConfig struct {\n\t// Username for basic auth\n\tusername string\n\t// Password for basic auth\n\tpassword string\n\t// Token for authorization in case of DCOS environment\n\tdcosToken string\n\t// scope is an arbitrary test scope to distinguish fake responses from\n\t// otherwise equal HTTP methods and query strings.\n\tscope string\n}\n\n// configContainer holds both server and client Marathon configuration\ntype configContainer struct {\n\tclient *Config\n\tserver *serverConfig\n}\n\ntype fakeServer struct {\n\tio.Closer\n\n\teventSrv        *eventsource.Server\n\thttpSrv         *httptest.Server\n\tfakeRespIndices *responseIndices\n}\n\ntype endpoint struct {\n\tio.Closer\n\n\tServer fakeServer\n\tClient Marathon\n\tURL    string\n}\n\ntype fakeEvent struct {\n\tdata string\n}\n\nfunc getTestURL(urlString string) string {\n\tparsedURL, err := url.Parse(urlString)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"failed to parse URL '%s': %s\", urlString, err))\n\t}\n\treturn fmt.Sprintf(\"%s://%s\", parsedURL.Scheme, strings.Join([]string{parsedURL.Host, parsedURL.Host, parsedURL.Host}, \",\"))\n}\n\nfunc newFakeMarathonEndpoint(t *testing.T, configs *configContainer) *endpoint {\n\t// step: read in the fake responses if required\n\tinitFakeMarathonResponses(t)\n\n\t// step: create a fake SSE event service\n\teventSrv := eventsource.NewServer()\n\n\t// step: fill in the default if required\n\tdefaultConfig := NewDefaultConfig()\n\tif configs == nil {\n\t\tconfigs = &configContainer{}\n\t}\n\tif configs.client == nil {\n\t\tconfigs.client = &defaultConfig\n\t}\n\tif configs.server == nil {\n\t\tconfigs.server = &serverConfig{}\n\t}\n\n\tfakeRespIndices := newResponseIndices()\n\n\t// step: create the HTTP router\n\tmux := http.NewServeMux()\n\tmux.HandleFunc(\"/v2/events\", authMiddleware(configs.server, eventSrv.Handler(\"event\")))\n\tmux.HandleFunc(\"/\", authMiddleware(configs.server, func(writer http.ResponseWriter, reader *http.Request) {\n\t\trespKey := fakeResponseMapKey(reader.Method, reader.RequestURI, configs.server.scope)\n\t\tfakeRespIndices.Lock()\n\t\tfakeRespIndex := fakeRespIndices.m[respKey]\n\t\tfakeRespIndices.m[respKey]++\n\t\tresponses, found := fakeResponses[respKey]\n\t\tfakeRespIndices.Unlock()\n\t\tif found {\n\t\t\tfor _, response := range responses {\n\t\t\t\t// Index < 0 indicates a static response.\n\t\t\t\tif response.Index < 0 || response.Index == fakeRespIndex {\n\t\t\t\t\twriter.Header().Add(\"Content-Type\", \"application/json\")\n\t\t\t\t\tfor k, v := range response.Headers {\n\t\t\t\t\t\twriter.Header().Add(k, v)\n\t\t\t\t\t}\n\n\t\t\t\t\twriter.Write([]byte(response.Content))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\thttp.Error(writer, `{\"message\": \"not found\"}`, 404)\n\t}))\n\n\t// step: create HTTP test server\n\thttpSrv := httptest.NewServer(mux)\n\n\tif configs.client.URL == defaultConfig.URL {\n\t\tconfigs.client.URL = getTestURL(httpSrv.URL)\n\t}\n\n\t// step: create the client for the service\n\tclient, err := NewClient(*configs.client)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create the fake client, %s, error: %s\", configs.client.URL, err)\n\t}\n\n\treturn &endpoint{\n\t\tServer: fakeServer{\n\t\t\teventSrv:        eventSrv,\n\t\t\thttpSrv:         httpSrv,\n\t\t\tfakeRespIndices: fakeRespIndices,\n\t\t},\n\t\tClient: client,\n\t\tURL:    configs.client.URL,\n\t}\n}\n\n// basicAuthMiddleware handles basic auth\nfunc basicAuthMiddleware(server *serverConfig, next http.HandlerFunc) func(http.ResponseWriter, *http.Request) {\n\tunauthorized := `{\"message\": \"invalid username or password\"}`\n\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\t// step: is authentication required?\n\t\tif server.username != \"\" && server.password != \"\" {\n\t\t\tu, p, found := r.BasicAuth()\n\t\t\t// step: if no auth found, error it\n\t\t\tif !found {\n\t\t\t\thttp.Error(w, unauthorized, 401)\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// step: if username and password don't match, error it\n\t\t\tif server.username != u || server.password != p {\n\t\t\t\thttp.Error(w, unauthorized, 401)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tnext(w, r)\n\t}\n}\n\n// authMiddleware handles basic auth and dcos_acs_token\nfunc authMiddleware(server *serverConfig, next http.HandlerFunc) func(http.ResponseWriter, *http.Request) {\n\tunauthorized := `{\"message\": \"invalid username or password\"}`\n\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\t// step: is authentication required?\n\n\t\tif server.dcosToken != \"\" {\n\t\t\theaderValue := r.Header.Get(\"Authorization\")\n\t\t\t// step: if no auth found, error it\n\t\t\tif headerValue == \"\" {\n\t\t\t\thttp.Error(w, unauthorized, 401)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ts := strings.Split(headerValue, \"=\")\n\n\t\t\tif s[1] != server.dcosToken {\n\t\t\t\thttp.Error(w, unauthorized, 401)\n\t\t\t\treturn\n\t\t\t}\n\t\t} else if server.username != \"\" && server.password != \"\" {\n\t\t\tu, p, found := r.BasicAuth()\n\t\t\t// step: if no auth found, error it\n\t\t\tif !found {\n\t\t\t\thttp.Error(w, unauthorized, 401)\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// step: if username and password don't match, error it\n\t\t\tif server.username != u || server.password != p {\n\t\t\t\thttp.Error(w, unauthorized, 401)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tnext(w, r)\n\t}\n}\n\n// initFakeMarathonResponses reads in the marathon fake responses from the yaml file\nfunc initFakeMarathonResponses(t *testing.T) {\n\tonce.Do(func() {\n\t\tfakeResponses = make(map[string][]indexedResponse)\n\t\tvar methods []*restMethod\n\n\t\t// step: read in the test method specification\n\t\tmethodSpec, err := ioutil.ReadFile(\"./tests/rest-api/methods.yml\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to read in the fake yaml responses: %s\", err)\n\t\t}\n\n\t\tif err = yaml.Unmarshal([]byte(methodSpec), &methods); err != nil {\n\t\t\tt.Fatalf(\"failed to unmarshal the response: %s\", err)\n\t\t}\n\t\tfor _, method := range methods {\n\t\t\tkey := fakeResponseMapKey(method.Method, method.URI, method.Scope)\n\t\t\tswitch {\n\t\t\tcase method.Content != \"\" && len(method.ContentSequence) > 0:\n\t\t\t\tpanic(\"content and contentSequence must not be provided simultaneously\")\n\t\t\tcase len(method.ContentSequence) > 0:\n\t\t\t\tfakeResponses[key] = method.ContentSequence\n\t\t\tdefault:\n\t\t\t\t// This combines the cases where static content was defined or not. The\n\t\t\t\t// latter models an empty response (via an empty content) that should\n\t\t\t\t// not result into a 404.\n\t\t\t\tfakeResponses[key] = []indexedResponse{\n\t\t\t\t\tindexedResponse{\n\t\t\t\t\t\t// Index -1 indicates a static response.\n\t\t\t\t\t\tIndex:   -1,\n\t\t\t\t\t\tContent: method.Content,\n\t\t\t\t\t\tHeaders: method.Headers,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc fakeResponseMapKey(method, uri, scope string) string {\n\treturn fmt.Sprintf(\"%s:%s:%s\", method, uri, scope)\n}\n\nfunc (t fakeEvent) Id() string {\n\treturn \"0\"\n}\n\nfunc (t fakeEvent) Event() string {\n\treturn \"MarathonEvent\"\n}\n\nfunc (t fakeEvent) Data() string {\n\treturn t.data\n}\n\nfunc (s *fakeServer) PublishEvent(event string) {\n\ts.eventSrv.Publish([]string{\"event\"}, fakeEvent{event})\n}\n\nfunc (s *fakeServer) Close() {\n\ts.eventSrv.Close()\n\ts.httpSrv.Close()\n}\n\nfunc (e *endpoint) Close() {\n\te.Server.Close()\n}\n"
  },
  {
    "path": "tests/app-definitions/TestApplicationString-1.5-output.json",
    "content": "{\n  \"id\": \"/my-app\",\n  \"args\": [\n    \"/usr/sbin/apache2ctl\",\n    \"-D\",\n    \"FOREGROUND\"\n  ],\n  \"container\": {\n    \"type\": \"DOCKER\",\n    \"docker\": {\n      \"image\": \"quay.io/gambol99/apache-php:latest\"\n    },\n    \"portMappings\": [\n      {\n        \"containerPort\": 80,\n        \"hostPort\": 0,\n        \"protocol\": \"tcp\"\n      },\n      {\n        \"containerPort\": 443,\n        \"hostPort\": 0,\n        \"protocol\": \"tcp\"\n      }\n    ]\n  },\n  \"cpus\": 0.1,\n  \"disk\": 0,\n  \"networks\": [\n    {\n      \"mode\": \"container/bridge\"\n    }\n  ],\n  \"healthChecks\": [\n    {\n      \"portIndex\": 0,\n      \"path\": \"/health\",\n      \"maxConsecutiveFailures\": 3,\n      \"protocol\": \"HTTP\",\n      \"gracePeriodSeconds\": 30,\n      \"intervalSeconds\": 5,\n      \"timeoutSeconds\": 5\n    }\n  ],\n  \"instances\": 2,\n  \"mem\": 64,\n  \"ports\": null,\n  \"dependencies\": null,\n  \"env\": {\n    \"NAME\": \"frontend_http\",\n    \"SERVICE_80_NAME\": \"test_http\"\n  }\n}\n"
  },
  {
    "path": "tests/app-definitions/TestApplicationString-output.json",
    "content": "{\n  \"id\": \"/my-app\",\n  \"args\": [\n    \"/usr/sbin/apache2ctl\",\n    \"-D\",\n    \"FOREGROUND\"\n  ],\n  \"container\": {\n    \"type\": \"DOCKER\",\n    \"docker\": {\n      \"image\": \"quay.io/gambol99/apache-php:latest\",\n      \"network\": \"BRIDGE\",\n      \"portMappings\": [\n        {\n          \"containerPort\": 80,\n          \"hostPort\": 0,\n          \"protocol\": \"tcp\"\n        },\n        {\n          \"containerPort\": 443,\n          \"hostPort\": 0,\n          \"protocol\": \"tcp\"\n        }\n      ]\n    }\n  },\n  \"cpus\": 0.1,\n  \"disk\": 0,\n  \"healthChecks\": [\n    {\n      \"portIndex\": 0,\n      \"path\": \"/health\",\n      \"maxConsecutiveFailures\": 3,\n      \"protocol\": \"HTTP\",\n      \"gracePeriodSeconds\": 30,\n      \"intervalSeconds\": 5,\n      \"timeoutSeconds\": 5\n    }\n  ],\n  \"instances\": 2,\n  \"mem\": 64,\n  \"ports\": null,\n  \"dependencies\": null,\n  \"env\": {\n    \"NAME\": \"frontend_http\",\n    \"SERVICE_80_NAME\": \"test_http\"\n  }\n}\n"
  },
  {
    "path": "tests/rest-api/methods.yml",
    "content": "- uri: /ping\n  method: GET\n  content: |\n    pong\n- uri: /v2/apps/fake-app/versions\n  method: GET\n  content: |\n    {\n        \"versions\": [\n            \"2014-04-04T06:25:31.399Z\"\n        ]\n    }\n- uri: /v2/apps\n  method: POST\n  content: |\n    {\n      \"args\": null,\n      \"backoffFactor\": 1.15,\n      \"backoffSeconds\": 1,\n      \"maxLaunchDelaySeconds\": 3600,\n      \"cmd\": \"env && python3 -m http.server $PORT0\",\n      \"constraints\": [\n          [\n              \"hostname\",\n              \"UNIQUE\"\n          ]\n      ],\n      \"container\": {\n          \"docker\": {\n              \"image\": \"python:3\"\n          },\n          \"type\": \"DOCKER\",\n          \"volumes\": []\n      },\n      \"cpus\": 0.25,\n      \"dependencies\": [],\n      \"deployments\": [\n          {\n              \"id\": \"f44fd4fc-4330-4600-a68b-99c7bd33014a\"\n          }\n      ],\n      \"disk\": 0.0,\n      \"env\": {},\n      \"executor\": \"\",\n      \"healthChecks\": [\n          {\n              \"command\": null,\n              \"gracePeriodSeconds\": 3,\n              \"intervalSeconds\": 10,\n              \"maxConsecutiveFailures\": 3,\n              \"path\": \"/\",\n              \"portIndex\": 0,\n              \"protocol\": \"HTTP\",\n              \"timeoutSeconds\": 5\n          }\n      ],\n      \"id\": \"/fake-app\",\n      \"instances\": 2,\n      \"mem\": 50.0,\n      \"requirePorts\": false,\n      \"residency\" : {\n          \"taskLostBehavior\" : \"RELAUNCH_AFTER_TIMEOUT\",\n          \"relaunchEscalationTimeoutSeconds\" : 60\n      },\n      \"storeUrls\": [],\n      \"upgradeStrategy\": {\n          \"minimumHealthCapacity\": 0.5,\n          \"maximumOverCapacity\": 0.5\n      },\n      \"user\": null,\n      \"version\": \"2014-08-18T22:36:41.451Z\"\n    }\n- uri: /v2/apps\n  method: GET\n  content: |\n    {\n    \"apps\": [\n        {\n            \"args\": null,\n            \"backoffFactor\": 1.15,\n            \"backoffSeconds\": 1,\n            \"cmd\": \"python3 -m http.server 8080\",\n            \"constraints\": [],\n            \"container\": {\n                \"docker\": {\n                    \"image\": \"python:3\"\n                },\n                \"portMappings\": [\n                    {\n                        \"containerPort\": 8080,\n                        \"hostPort\": 0,\n                        \"servicePort\": 9000,\n                        \"protocol\": \"tcp\"\n                    },\n                    {\n                        \"containerPort\": 161,\n                        \"hostPort\": 0,\n                        \"protocol\": \"udp\"\n                    }\n                ],\n                \"type\": \"DOCKER\",\n                \"volumes\": []\n            },\n            \"cpus\": 0.5,\n            \"dependencies\": [],\n            \"deployments\": [],\n            \"disk\": 0.0,\n            \"networks\": [\n                {\n                    \"mode\": \"container/bridge\"\n                }\n            ],\n            \"env\": {\n                \"VAR\": \"VALUE\",\n                \"SECRET1\": {\n                    \"secret\": \"secret0\"\n                }\n            },\n            \"executor\": \"\",\n            \"healthChecks\": [\n                {\n                    \"command\": null,\n                    \"gracePeriodSeconds\": 5,\n                    \"intervalSeconds\": 20,\n                    \"maxConsecutiveFailures\": 3,\n                    \"path\": \"/\",\n                    \"portIndex\": 0,\n                    \"protocol\": \"HTTP\",\n                    \"timeoutSeconds\": 20\n                }\n            ],\n            \"id\": \"/fake-app\",\n            \"instances\": 2,\n            \"mem\": 64.0,\n            \"requirePorts\": false,\n            \"secrets\": {\n                \"secret0\": {\n                  \"source\": \"secret/definition/id\"\n                }\n            },\n            \"storeUrls\": [],\n            \"tasksRunning\": 2,\n            \"tasksStaged\": 0,\n            \"upgradeStrategy\": {\n                \"minimumHealthCapacity\": 1.0\n            },\n            \"user\": null,\n            \"version\": \"2014-09-25T02:26:59.256Z\"\n        },\n        {\n            \"args\": null,\n            \"backoffFactor\": 1.15,\n            \"backoffSeconds\": 1,\n            \"cmd\": \"python3 -m http.server 8080\",\n            \"constraints\": [],\n            \"container\": {\n                \"docker\": {\n                    \"image\": \"python:3\"\n                },\n                \"portMappings\": [\n                    {\n                        \"containerPort\": 8080,\n                        \"hostPort\": 0,\n                        \"servicePort\": 9000,\n                        \"protocol\": \"tcp\"\n                    },\n                    {\n                        \"containerPort\": 161,\n                        \"hostPort\": 0,\n                        \"protocol\": \"udp\"\n                    }\n                ],\n                \"type\": \"DOCKER\",\n                \"volumes\": []\n            },\n            \"cpus\": 0.5,\n            \"dependencies\": [],\n            \"deployments\": [],\n            \"disk\": 0.0,\n            \"networks\": [\n                {\n                    \"mode\": \"container/bridge\"\n                }\n            ],\n            \"env\": {},\n            \"executor\": \"\",\n            \"healthChecks\": [\n                {\n                    \"command\": null,\n                    \"gracePeriodSeconds\": 5,\n                    \"intervalSeconds\": 20,\n                    \"maxConsecutiveFailures\": 3,\n                    \"path\": \"/\",\n                    \"portIndex\": 0,\n                    \"protocol\": \"HTTP\",\n                    \"timeoutSeconds\": 20\n                }\n            ],\n            \"id\": \"/fake-app-broken\",\n            \"instances\": 2,\n            \"mem\": 64.0,\n            \"requirePorts\": false,\n            \"residency\" : {\n                \"taskLostBehavior\" : \"RELAUNCH_AFTER_TIMEOUT\",\n                \"relaunchEscalationTimeoutSeconds\" : 60\n            },\n            \"storeUrls\": [],\n            \"tasksRunning\": 2,\n            \"tasksStaged\": 0,\n            \"upgradeStrategy\": {\n                \"minimumHealthCapacity\": 1.0\n            },\n            \"user\": null,\n            \"version\": \"2014-09-25T02:26:59.256Z\"\n        }\n    ]\n    }\n- uri: /v2/apps?embed=apps.taskStats\n  method: GET\n  content: |\n    {\n    \"apps\": [\n        {\n            \"taskStats\": {\n                \"startedAfterLastScaling\": {\n                    \"stats\": {\n                        \"counts\": {\n                            \"staged\": 0,\n                            \"running\": 1,\n                            \"healthy\": 1,\n                            \"unhealthy\": 0\n                        },\n                        \"lifeTime\": {\n                            \"averageSeconds\": 17024.575,\n                            \"medianSeconds\": 17024.575\n                        }\n                    }\n                }\n            }\n        }\n    ]\n    }\n- uri: /v2/apps?cmd=nginx\n  method: GET\n  content: |\n    {\n    \"apps\": [\n        {\n            \"args\": null,\n            \"cmd\": \"nginx\"\n        }\n    ]\n    }\n- uri: /v2/apps/fake-app/restart\n  method: POST\n  content: |\n    {\n      \"deploymentId\": \"83b215a6-4e26-4e44-9333-5c385eda6438\",\n      \"version\": \"2014-08-26T07:37:50.462Z\"\n    }\n- uri: /v2/apps/fake-app\n  method: DELETE\n  content: |\n    {\n      \"deploymentId\": \"83b215a6-4e26-4e44-9333-5c385eda6438\",\n      \"version\": \"2014-08-26T07:37:50.462Z\"\n    }\n- uri: /v2/apps/fake-app?force=true\n  method: DELETE\n  content: |\n    {\n      \"deploymentId\": \"83b215a6-4e26-4e44-9333-5c385eda6438\",\n      \"version\": \"2014-08-26T07:37:50.462Z\"\n    }\n- uri: /v2/apps/fake-app/tasks\n  method: GET\n  content: |\n    {\n        \"tasks\": [{\"id\": \"1\"},{\"id\": \"2\"}]\n    }\n- uri: /v2/apps/fake-app/tasks\n  method: DELETE\n  content: |\n    {\n        \"tasks\": []\n    }\n- uri: /v2/apps/fake-app/tasks/fake-app.fake-task\n  method: DELETE\n  content: |\n    {\n        \"task\": {\"id\": \"fake-app.fake-task\"}\n    }\n- uri: /v2/apps/fake-group/fake-app/tasks/fake-group_fake-app.fake-task\n  method: DELETE\n  content: |\n    {\n        \"task\": {\"id\": \"fake-group_fake-app.fake-task\"}\n    }\n- uri: /v2/apps/fake-app/versions/2014-09-12T23:28:21.737Z\n  method: GET\n  content: |\n    {\n        \"args\": null,\n        \"backoffFactor\": 1.15,\n        \"backoffSeconds\": 1,\n        \"cmd\": \"python toggle.py $PORT0\",\n        \"constraints\": [],\n        \"container\": {\n            \"docker\": {\n                \"image\": \"python:3\"\n            },\n            \"portMappings\": [\n                {\n                    \"containerPort\": 8080,\n                    \"hostPort\": 0,\n                    \"servicePort\": 9000,\n                    \"protocol\": \"tcp\"\n                }\n            ],\n            \"type\": \"DOCKER\",\n            \"volumes\": []\n        },\n        \"cpus\": 0.2,\n        \"dependencies\": [],\n        \"deployments\": [],\n        \"disk\": 0.0,\n        \"networks\": [\n            {\n                \"mode\": \"container/bridge\"\n            }\n        ],\n        \"env\": {},\n        \"executor\": \"\",\n        \"healthChecks\": [\n            {\n                \"command\": null,\n                \"gracePeriodSeconds\": 5,\n                \"intervalSeconds\": 10,\n                \"maxConsecutiveFailures\": 3,\n                \"path\": \"/health\",\n                \"portIndex\": 0,\n                \"protocol\": \"HTTP\",\n                \"timeoutSeconds\": 10\n            }\n        ],\n        \"id\": \"/fake-app\",\n        \"instances\": 2,\n        \"lastTaskFailure\": {\n            \"appId\": \"/toggle\",\n            \"host\": \"10.141.141.10\",\n            \"message\": \"Abnormal executor termination\",\n            \"slaveId\": \"14ac45bf-9a40-42cf-94ec-695130865592-S0\",\n            \"state\": \"TASK_FAILED\",\n            \"taskId\": \"toggle.cc427e60-5046-11e4-9e34-56847afe9799\",\n            \"timestamp\": \"2014-09-12T23:23:41.711Z\",\n            \"version\": \"2014-09-12T23:28:21.737Z\"\n        },\n        \"mem\": 32.0,\n        \"requirePorts\": false,\n        \"residency\" : {\n            \"taskLostBehavior\" : \"RELAUNCH_AFTER_TIMEOUT\",\n            \"relaunchEscalationTimeoutSeconds\" : 60\n        },\n        \"storeUrls\": [],\n        \"tasks\": [\n            {\n                \"appId\": \"/toggle\",\n                \"healthCheckResults\": [\n                    {\n                        \"alive\": true,\n                        \"consecutiveFailures\": 0,\n                        \"firstSuccess\": \"2014-09-13T00:20:28.101Z\",\n                        \"lastFailure\": null,\n                        \"lastSuccess\": \"2014-09-13T00:25:07.506Z\",\n                        \"taskId\": \"toggle.802df2ae-3ad4-11e4-a400-56847afe9799\"\n                    }\n                ],\n                \"host\": \"10.141.141.10\",\n                \"id\": \"toggle.802df2ae-3ad4-11e4-a400-56847afe9799\",\n                \"ports\": [\n                    31045\n                ],\n                \"stagedAt\": \"2014-09-12T23:28:28.594Z\",\n                \"startedAt\": \"2014-09-13T00:24:46.959Z\",\n                \"version\": \"2014-09-12T23:28:21.737Z\"\n            },\n            {\n                \"appId\": \"/toggle\",\n                \"healthCheckResults\": [\n                    {\n                        \"alive\": true,\n                        \"consecutiveFailures\": 0,\n                        \"firstSuccess\": \"2014-09-13T00:20:28.101Z\",\n                        \"lastFailure\": null,\n                        \"lastSuccess\": \"2014-09-13T00:25:07.508Z\",\n                        \"taskId\": \"toggle.7c99814d-3ad4-11e4-a400-56847afe9799\"\n                    }\n                ],\n                \"host\": \"10.141.141.10\",\n                \"id\": \"toggle.7c99814d-3ad4-11e4-a400-56847afe9799\",\n                \"ports\": [\n                    31234\n                ],\n                \"stagedAt\": \"2014-09-12T23:28:22.587Z\",\n                \"startedAt\": \"2014-09-13T00:24:46.965Z\",\n                \"version\": \"2014-09-12T23:28:21.737Z\"\n            }\n        ],\n        \"tasksRunning\": 2,\n        \"tasksStaged\": 0,\n        \"upgradeStrategy\": {\n            \"minimumHealthCapacity\": 1.0\n        },\n        \"user\": null,\n        \"version\": \"2014-09-12T23:28:21.737Z\"\n    }\n- uri: /v2/apps/fake-app\n  method: GET\n  content: |\n    {\n    \"app\": {\n        \"args\": null,\n        \"backoffFactor\": 1.15,\n        \"backoffSeconds\": 1,\n        \"cmd\": \"python toggle.py $PORT0\",\n        \"constraints\": [],\n        \"container\": {\n            \"docker\": {\n                \"image\": \"python:3\"\n            },\n             \"portMappings\": [\n                {\n                    \"containerPort\": 8080,\n                    \"hostPort\": 0,\n                    \"servicePort\": 9000,\n                    \"protocol\": \"tcp\"\n                }\n            ],\n            \"type\": \"DOCKER\",\n            \"volumes\": []\n        },\n        \"cpus\": 0.2,\n        \"dependencies\": [],\n        \"deployments\": [],\n        \"disk\": 0.0,\n        \"networks\": [\n            {\n                \"mode\": \"container/bridge\"\n            }\n        ],\n        \"env\": {},\n        \"executor\": \"\",\n        \"healthChecks\": [\n            {\n                \"command\": null,\n                \"gracePeriodSeconds\": 5,\n                \"intervalSeconds\": 10,\n                \"maxConsecutiveFailures\": 3,\n                \"path\": \"/health\",\n                \"portIndex\": 0,\n                \"protocol\": \"HTTP\",\n                \"timeoutSeconds\": 10\n            }\n        ],\n        \"id\": \"/fake-app\",\n        \"instances\": 2,\n        \"lastTaskFailure\": {\n            \"appId\": \"/toggle\",\n            \"host\": \"10.141.141.10\",\n            \"message\": \"Abnormal executor termination\",\n            \"slaveId\": \"14ac45bf-9a40-42cf-94ec-695130865592-S0\",\n            \"state\": \"TASK_FAILED\",\n            \"taskId\": \"toggle.cc427e60-5046-11e4-9e34-56847afe9799\",\n            \"timestamp\": \"2014-09-12T23:23:41.711Z\",\n            \"version\": \"2014-09-12T23:28:21.737Z\"\n        },\n        \"mem\": 32.0,\n        \"requirePorts\": false,\n        \"residency\" : {\n            \"taskLostBehavior\" : \"RELAUNCH_AFTER_TIMEOUT\",\n            \"relaunchEscalationTimeoutSeconds\" : 60\n        },\n        \"storeUrls\": [],\n        \"tasks\": [\n            {\n                \"appId\": \"/toggle\",\n                \"healthCheckResults\": [\n                    {\n                        \"alive\": true,\n                        \"consecutiveFailures\": 0,\n                        \"firstSuccess\": \"2014-09-13T00:20:28.101Z\",\n                        \"lastFailure\": null,\n                        \"lastSuccess\": \"2014-09-13T00:25:07.506Z\",\n                        \"taskId\": \"toggle.802df2ae-3ad4-11e4-a400-56847afe9799\"\n                    }\n                ],\n                \"host\": \"10.141.141.10\",\n                \"id\": \"toggle.802df2ae-3ad4-11e4-a400-56847afe9799\",\n                \"ports\": [\n                    31045\n                ],\n                \"stagedAt\": \"2014-09-12T23:28:28.594Z\",\n                \"startedAt\": \"2014-09-13T00:24:46.959Z\",\n                \"version\": \"2014-09-12T23:28:21.737Z\"\n            },\n            {\n                \"appId\": \"/toggle\",\n                \"healthCheckResults\": [\n                    {\n                        \"alive\": true,\n                        \"consecutiveFailures\": 0,\n                        \"firstSuccess\": \"2014-09-13T00:20:28.101Z\",\n                        \"lastFailure\": null,\n                        \"lastSuccess\": \"2014-09-13T00:25:07.508Z\",\n                        \"taskId\": \"toggle.7c99814d-3ad4-11e4-a400-56847afe9799\"\n                    }\n                ],\n                \"host\": \"10.141.141.10\",\n                \"id\": \"toggle.7c99814d-3ad4-11e4-a400-56847afe9799\",\n                \"ports\": [\n                    31234\n                ],\n                \"stagedAt\": \"2014-09-12T23:28:22.587Z\",\n                \"startedAt\": \"2014-09-13T00:24:46.965Z\",\n                \"version\": \"2014-09-12T23:28:21.737Z\"\n            }\n        ],\n        \"tasksRunning\": 2,\n        \"tasksStaged\": 0,\n        \"upgradeStrategy\": {\n            \"minimumHealthCapacity\": 1.0\n        },\n        \"user\": null,\n        \"version\": \"2014-09-12T23:28:21.737Z\"\n    }\n    }\n\n- uri: /v2/apps/fake-app\n  method: GET\n  scope: wait-on-app\n  contentSequence:\n    - index: 1\n      content: |\n        {\n        \"app\": {\n        }\n        }\n\n- uri: /v2/apps/fake-app-broken\n  method: GET\n  content: |\n    {\n    \"app\": {\n        \"args\": null,\n        \"backoffFactor\": 1.15,\n        \"backoffSeconds\": 1,\n        \"cmd\": \"python toggle.py $PORT0\",\n        \"constraints\": [],\n        \"container\": {\n            \"docker\": {\n                \"image\": \"python:3\"\n            },\n            \"portMappings\": [\n                {\n                    \"containerPort\": 8080,\n                    \"hostPort\": 0,\n                    \"servicePort\": 9000,\n                    \"protocol\": \"tcp\"\n                }\n            ],\n            \"type\": \"DOCKER\",\n            \"volumes\": []\n        },\n        \"cpus\": 0.2,\n        \"dependencies\": [],\n        \"deployments\": [],\n        \"disk\": 0.0,\n        \"networks\": [\n            {\n                \"mode\": \"container/bridge\"\n            }\n        ],\n        \"env\": {},\n        \"executor\": \"\",\n        \"healthChecks\": [\n            {\n                \"command\": null,\n                \"gracePeriodSeconds\": 5,\n                \"intervalSeconds\": 10,\n                \"maxConsecutiveFailures\": 3,\n                \"path\": \"/health\",\n                \"portIndex\": 0,\n                \"protocol\": \"HTTP\",\n                \"timeoutSeconds\": 10\n            }\n        ],\n        \"id\": \"/fake-app-broken\",\n        \"instances\": 2,\n        \"lastTaskFailure\": {\n            \"appId\": \"/toggle\",\n            \"host\": \"10.141.141.10\",\n            \"message\": \"Abnormal executor termination\",\n            \"state\": \"TASK_FAILED\",\n            \"taskId\": \"toggle.cc427e60-5046-11e4-9e34-56847afe9799\",\n            \"timestamp\": \"2014-09-12T23:23:41.711Z\",\n            \"version\": \"2014-09-12T23:28:21.737Z\"\n        },\n        \"mem\": 32.0,\n        \"requirePorts\": false,\n        \"residency\" : {\n            \"taskLostBehavior\" : \"RELAUNCH_AFTER_TIMEOUT\",\n            \"relaunchEscalationTimeoutSeconds\" : 60\n        },\n        \"storeUrls\": [],\n        \"tasks\": [\n            {\n                \"appId\": \"/toggle\",\n                \"healthCheckResults\": [\n                    {\n                        \"alive\": true,\n                        \"consecutiveFailures\": 0,\n                        \"firstSuccess\": \"2014-09-13T00:20:28.101Z\",\n                        \"lastFailure\": null,\n                        \"lastSuccess\": \"2014-09-13T00:25:07.506Z\",\n                        \"taskId\": \"toggle.802df2ae-3ad4-11e4-a400-56847afe9799\"\n                    }\n                ],\n                \"host\": \"10.141.141.10\",\n                \"id\": \"toggle.802df2ae-3ad4-11e4-a400-56847afe9799\",\n                \"ports\": [\n                    31045\n                ],\n                \"stagedAt\": \"2014-09-12T23:28:28.594Z\",\n                \"startedAt\": \"2014-09-13T00:24:46.959Z\",\n                \"version\": \"2014-09-12T23:28:21.737Z\"\n            },\n            {\n                \"appId\": \"/toggle\",\n                \"healthCheckResults\": [\n                    {\n                        \"alive\": false,\n                        \"consecutiveFailures\": 0,\n                        \"firstSuccess\": \"2014-09-13T00:20:28.101Z\",\n                        \"lastFailure\": null,\n                        \"lastSuccess\": \"2014-09-13T00:25:07.508Z\",\n                        \"taskId\": \"toggle.7c99814d-3ad4-11e4-a400-56847afe9799\"\n                    }\n                ],\n                \"host\": \"10.141.141.10\",\n                \"id\": \"toggle.7c99814d-3ad4-11e4-a400-56847afe9799\",\n                \"ports\": [\n                    31234\n                ],\n                \"stagedAt\": \"2014-09-12T23:28:22.587Z\",\n                \"startedAt\": \"2014-09-13T00:24:46.965Z\",\n                \"version\": \"2014-09-12T23:28:21.737Z\"\n            }\n        ],\n        \"tasksRunning\": 2,\n        \"tasksStaged\": 0,\n        \"upgradeStrategy\": {\n            \"minimumHealthCapacity\": 1.0\n        },\n        \"user\": null,\n        \"version\": \"2014-09-12T23:28:21.737Z\"\n    }\n    }\n- uri: /v2/apps/fake-app/versions\n  method: GET\n  content: |\n    {\n        \"versions\": [\n            \"2014-04-04T06:25:31.399Z\"\n        ]\n    }\n- uri: /v2/apps/fake-app\n  method: PUT\n  content: |\n    {\n      \"deploymentId\": \"83b215a6-4e26-4e44-9333-5c385eda6438\",\n      \"version\": \"2014-08-26T07:37:50.462Z\"\n    }\n- uri: /v2/apps/fake-app?force=true\n  method: PUT\n  content: |\n    {\n      \"deploymentId\": \"83b215a6-4e26-4e44-9333-5c385eda6438\",\n      \"version\": \"2014-08-26T07:37:50.462Z\"\n    }\n- uri: /v2/pods\n  method: HEAD\n- uri: /v2/pods/fake-pod::status\n  method: GET\n  content: |\n    {\n      \"id\": \"/fake-pod\",\n      \"spec\": {\n        \"id\": \"/fake-pod\",\n        \"labels\": {\n          \"key\": \"value\"\n        },\n        \"version\": \"2014-08-18T22:36:41.451Z\",\n        \"user\": \"nobody\",\n        \"environment\": {\n          \"key\": {\n            \"secret\": \"secret0\"\n          }\n        },\n        \"containers\": [],\n        \"secrets\": {\n          \"secret0\": {\n            \"source\": \"source0\"\n          }\n        },\n        \"volumes\": [],\n        \"networks\": [],\n        \"scaling\": {\n          \"kind\": \"fixed\",\n          \"instances\": 1\n        },\n        \"scheduling\": {}\n      },\n      \"status\": \"STABLE\",\n      \"statusSince\": \"2017-07-13T21:33:17.349Z\",\n      \"instances\": [\n        {\n          \"id\": \"fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003\",\n          \"status\": \"STABLE\",\n          \"statusSince\": \"2017-07-13T21:33:17.349Z\",\n          \"conditions\": [],\n          \"agentHostname\": \"192.168.99.100\",\n          \"resources\": {\n            \"cpus\": 0.1,\n            \"mem\": 64,\n            \"disk\": 0,\n            \"gpus\": 0\n          },\n          \"networks\": [],\n          \"containers\": [\n            {\n              \"name\": \"container1\",\n              \"status\": \"TASK_RUNNING\",\n              \"statusSince\": \"2017-07-13T21:33:17.349Z\",\n              \"conditions\": [],\n              \"containerId\": \"fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003.container1\",\n              \"endpoints\": [],\n              \"resources\": {\n                \"cpus\": 0.1,\n                \"mem\": 32,\n                \"disk\": 0,\n                \"gpus\": 0\n              },\n              \"lastUpdated\": \"2017-07-13T21:33:17.349Z\",\n              \"lastChanged\": \"2017-07-13T21:33:17.349Z\"\n            }\n          ],\n          \"specReference\": \"/v2/pods/fake-pod::versions/2017-07-13T21:33:07.389Z\",\n          \"lastUpdated\": \"2017-07-13T21:33:17.349Z\",\n          \"lastChanged\": \"2017-07-13T21:33:17.349Z\"\n        }\n      ],\n      \"terminationHistory\": [],\n      \"lastUpdated\": \"2017-07-13T22:13:46.635Z\",\n      \"lastChanged\": \"2017-07-13T21:33:17.349Z\"\n    }\n- uri: /v2/pods/fake-pod2::status\n  method: GET\n  content: |\n    {\n      \"id\": \"/fake-pod2\",\n      \"spec\": {\n        \"id\": \"/fake-pod2\",\n        \"labels\": {\n          \"key\": \"value\"\n        },\n        \"version\": \"2014-08-18T22:36:41.451Z\",\n        \"user\": \"nobody\",\n        \"environment\": {\n          \"key\": {\n            \"secret\": \"secret0\"\n          }\n        },\n        \"containers\": [],\n        \"secrets\": {\n          \"secret0\": {\n            \"source\": \"source0\"\n          }\n        },\n        \"volumes\": [],\n        \"networks\": [],\n        \"scaling\": {\n          \"kind\": \"fixed\",\n          \"instances\": 1\n        },\n        \"scheduling\": {}\n      },\n      \"status\": \"DEGRADED\",\n      \"statusSince\": \"2017-07-13T21:33:17.349Z\",\n      \"instances\": [\n        {\n          \"id\": \"fake-pod2.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003\",\n          \"status\": \"DEGRADED\",\n          \"statusSince\": \"2017-07-13T21:33:17.349Z\",\n          \"conditions\": [],\n          \"agentHostname\": \"192.168.99.100\",\n          \"resources\": {\n            \"cpus\": 0.1,\n            \"mem\": 64,\n            \"disk\": 0,\n            \"gpus\": 0\n          },\n          \"networks\": [],\n          \"containers\": [\n            {\n              \"name\": \"container1\",\n              \"status\": \"TASK_RUNNING\",\n              \"statusSince\": \"2017-07-13T21:33:17.349Z\",\n              \"conditions\": [],\n              \"containerId\": \"fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003.container1\",\n              \"endpoints\": [],\n              \"resources\": {\n                \"cpus\": 0.1,\n                \"mem\": 32,\n                \"disk\": 0,\n                \"gpus\": 0\n              },\n              \"lastUpdated\": \"2017-07-13T21:33:17.349Z\",\n              \"lastChanged\": \"2017-07-13T21:33:17.349Z\"\n            }\n          ],\n          \"specReference\": \"/v2/pods/fake-pod::versions/2017-07-13T21:33:07.389Z\",\n          \"lastUpdated\": \"2017-07-13T21:33:17.349Z\",\n          \"lastChanged\": \"2017-07-13T21:33:17.349Z\"\n        }\n      ],\n      \"terminationHistory\": [],\n      \"lastUpdated\": \"2017-07-13T22:13:46.635Z\",\n      \"lastChanged\": \"2017-07-13T21:33:17.349Z\"\n    }\n- uri: /v2/pods/::status\n  method: GET\n  content: |\n    [\n      {\n        \"id\": \"/fake-pod\",\n        \"spec\": {\n          \"id\": \"/fake-pod\",\n          \"labels\": {\n            \"key\": \"value\"\n          },\n          \"version\": \"2014-08-18T22:36:41.451Z\",\n          \"user\": \"nobody\",\n          \"environment\": {\n            \"key\": {\n              \"secret\": \"secret0\"\n            }\n          },\n          \"containers\": [],\n          \"secrets\": {\n            \"secret0\": {\n              \"source\": \"source0\"\n            }\n          },\n          \"volumes\": [],\n          \"networks\": [],\n          \"scaling\": {\n            \"kind\": \"fixed\",\n            \"instances\": 1\n          },\n          \"scheduling\": {}\n        },\n        \"status\": \"STABLE\",\n        \"statusSince\": \"2017-07-13T21:33:17.349Z\",\n        \"instances\": [\n          {\n            \"id\": \"fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003\",\n            \"status\": \"STABLE\",\n            \"statusSince\": \"2017-07-13T21:33:17.349Z\",\n            \"conditions\": [],\n            \"agentHostname\": \"192.168.99.100\",\n            \"resources\": {\n              \"cpus\": 0.1,\n              \"mem\": 64,\n              \"disk\": 0,\n              \"gpus\": 0\n            },\n            \"networks\": [],\n            \"containers\": [\n              {\n                \"name\": \"container1\",\n                \"status\": \"TASK_RUNNING\",\n                \"statusSince\": \"2017-07-13T21:33:17.349Z\",\n                \"conditions\": [],\n                \"containerId\": \"fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003.container1\",\n                \"endpoints\": [],\n                \"resources\": {\n                  \"cpus\": 0.1,\n                  \"mem\": 32,\n                  \"disk\": 0,\n                  \"gpus\": 0\n                },\n                \"lastUpdated\": \"2017-07-13T21:33:17.349Z\",\n                \"lastChanged\": \"2017-07-13T21:33:17.349Z\"\n              }\n            ],\n            \"specReference\": \"/v2/pods/fake-pod::versions/2017-07-13T21:33:07.389Z\",\n            \"lastUpdated\": \"2017-07-13T21:33:17.349Z\",\n            \"lastChanged\": \"2017-07-13T21:33:17.349Z\"\n          }\n        ],\n        \"terminationHistory\": [],\n        \"lastUpdated\": \"2017-07-13T22:13:46.635Z\",\n        \"lastChanged\": \"2017-07-13T21:33:17.349Z\"\n      }\n    ]\n- uri: /v2/pods/fake-pod\n  method: GET\n  content: |\n    {\n      \"id\": \"/fake-pod\",\n      \"labels\": {\n          \"key\": \"value\"\n      },\n      \"version\": \"2014-08-18T22:36:41.451Z\",\n      \"user\": \"nobody\",\n      \"environment\": {\n          \"key1\": \"value\",\n          \"key2\": {\n              \"secret\": \"secret0\"\n          }\n      },\n      \"containers\": [\n          {\n              \"environment\": {\n                  \"key3\": \"value3\",\n                  \"key4\": {\n                      \"secret\": \"secret1\"\n                  }\n              }\n          }\n      ],\n      \"secrets\": {\n          \"secret0\": {\n              \"source\": \"source0\"\n          },\n          \"secret1\": {\n              \"source\": \"source1\"\n          }\n      },\n      \"volumes\": [\n\n      ],\n      \"networks\": [\n\n      ],\n      \"scaling\": {\n          \"kind\": \"fixed\",\n          \"instances\": 1\n      },\n      \"scheduling\": {\n\n      }\n    }\n- uri: /v2/pods\n  method: GET\n  content: |\n    [\n      {\n        \"id\": \"/fake-pod\",\n        \"labels\": {\n            \"key\": \"value\"\n        },\n        \"version\": \"2014-08-18T22:36:41.451Z\",\n        \"user\": \"nobody\",\n        \"environment\": {\n            \"key1\": \"value\",\n            \"key2\": {\n                \"secret\": \"secret0\"\n            }\n        },\n        \"containers\": [\n        ],\n        \"secrets\": {\n            \"secret0\": {\n                \"source\": \"source0\"\n            }\n        },\n        \"volumes\": [\n\n        ],\n        \"networks\": [\n\n        ],\n        \"scaling\": {\n            \"kind\": \"fixed\",\n            \"instances\": 1\n        },\n        \"scheduling\": {\n\n        }\n      },\n      {\n        \"id\": \"/fake-pod2\",\n        \"labels\": {\n            \"key\": \"value\"\n        },\n        \"version\": \"2014-08-18T22:36:41.451Z\",\n        \"user\": \"nobody\",\n        \"environment\": {\n            \"key\": \"value\",\n            \"key\": {\n                \"secret\": \"secret0\"\n            }\n        },\n        \"containers\": [\n        ],\n        \"secrets\": {\n            \"secret0\": {\n                \"source\": \"source0\"\n            }\n        },\n        \"volumes\": [\n\n        ],\n        \"networks\": [\n\n        ],\n        \"scaling\": {\n            \"kind\": \"fixed\",\n            \"instances\": 1\n        },\n        \"scheduling\": {\n\n        }\n      }\n    ]\n- uri: /v2/pods\n  method: POST\n  content: |\n    {\n      \"id\": \"/fake-pod\",\n      \"labels\": {\n          \"key\": \"value\"\n      },\n      \"version\": \"2014-08-18T22:36:41.451Z\",\n      \"user\": \"nobody\",\n      \"environment\": {\n          \"key\": \"value\",\n          \"key\": {\n              \"secret\": \"secret0\"\n          }\n      },\n      \"containers\": [\n      ],\n      \"secrets\": {\n          \"secret0\": {\n              \"source\": \"source0\"\n          }\n      },\n      \"volumes\": [\n\n      ],\n      \"networks\": [\n\n      ],\n      \"scaling\": {\n          \"kind\": \"fixed\",\n          \"instances\": 1\n      },\n      \"scheduling\": {\n\n      }\n    }\n- uri: /v2/pods/fake-pod?force=true\n  method: PUT\n  content: |\n    {\n      \"id\": \"/fake-pod\",\n      \"labels\": {\n          \"key\": \"value\"\n      },\n      \"version\": \"2014-08-18T22:36:41.451Z\",\n      \"user\": \"nobody\",\n      \"environment\": {\n          \"key\": \"value\",\n          \"key\": {\n              \"secret\": \"secret0\"\n          }\n      },\n      \"containers\": [\n      ],\n      \"secrets\": {\n          \"secret0\": {\n              \"source\": \"source0\"\n          }\n      },\n      \"volumes\": [\n\n      ],\n      \"networks\": [\n\n      ],\n      \"scaling\": {\n          \"kind\": \"fixed\",\n          \"instances\": 2\n      },\n      \"scheduling\": {\n\n      }\n    }\n- uri: /v2/pods/fake-pod?force=true\n  method: DELETE\n  headers:\n    \"Marathon-Deployment-Id\": \"c0e7434c-df47-4d23-99f1-78bd78662231\"\n- uri: /v2/pods/fake-pod::versions\n  method: GET\n  content: |\n    [\n        \"2014-08-18T22:36:41.451Z\"\n    ]\n- uri: /v2/pods/fake-pod::versions/2014-08-18T22:36:41.451Z\n  method: GET\n  content: |\n    {\n      \"id\": \"/fake-pod\",\n      \"labels\": {\n          \"key\": \"value\"\n      },\n      \"version\": \"2014-08-18T22:36:41.451Z\",\n      \"user\": \"nobody\",\n      \"environment\": {\n          \"key\": \"value\",\n          \"key\": {\n              \"secret\": \"secret0\"\n          }\n      },\n      \"containers\": [\n      ],\n      \"secrets\": {\n          \"secret0\": {\n              \"source\": \"source0\"\n          }\n      },\n      \"volumes\": [\n\n      ],\n      \"networks\": [\n\n      ],\n      \"scaling\": {\n          \"kind\": \"fixed\",\n          \"instances\": 1\n      },\n      \"scheduling\": {\n\n      }\n    }\n- uri: /v2/pods/fake-pod::instances/fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003\n  method: DELETE\n  content: |\n    {\n      \"instanceId\": {\n        \"idString\": \"fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003\"\n      },\n      \"agentInfo\": {\n        \"host\": \"192.168.99.100\",\n        \"agentId\": \"c3856950-ab1a-4aea-b1a8-168397c0fb33-S9\",\n        \"attributes\": [\n          \"CgR6b25lEAMqBgoEcGR4Mw==\",\n          \"CgZyZWdpb24QABoJCQAAAAAAAAhA\",\n          \"Cg1pbnN0YW5jZV90eXBlEAMqCQoHY29tcHV0ZQ==\",\n          \"CgNzZG4QAyoKCghjb250cmFpbA==\",\n          \"CgJvcxADKgkKB2NlbnRvczc=\"\n        ]\n      },\n      \"tasksMap\": {\n        \"fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003.container1\": {\n          \"taskId\": \"fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003.container1\",\n          \"runSpecVersion\": \"2017-07-13T23:04:05.835Z\",\n          \"status\": {\n            \"stagedAt\": \"2017-07-13T23:04:05.939Z\",\n            \"startedAt\": \"2017-07-13T23:04:07.767Z\",\n            \"mesosStatus\": \"Ck0KS3JjbHVzdGVyX2Rjb3Mtc2lkZWNhci5pbnN0YW5jZS05MWU5YzE1OC02ODFmLTExZTctYTE4ZS03MGIzZDU4MDAwMDMuc2lkZWNhchABKikKJ2MzODU2OTUwLWFiMWEtNGFlYS1iMWE4LTE2ODM5N2MwZmIzMy1TOTGxw/AZ/1nWQTpFCkNpbnN0YW5jZS1yY2x1c3Rlcl9kY29zLXNpZGVjYXIuOTFlOWMxNTgtNjgxZi0xMWU3LWExOGUtNzBiM2Q1ODAwMDAzSAJaEOVRUUkSVEbRi5mOLzx0bnxqkgIKvAEimAEKLgoRcmNsdXN0ZXIubG9jYXRpb24SGWdsb2JhbHJpb3QucGR4Mi5yY2x1c3RlcjEKGgoOcmNsdXN0ZXIuZ3JvdXASCHJjbHVzdGVyCiQKFHJjbHVzdGVyLmFwcGxpY2F0aW9uEgxkY29zLXNpZGVjYXIKJAoKRENPU19TUEFDRRIWL3JjbHVzdGVyL2Rjb3Mtc2lkZWNhcioRCAESDTEwLjQwLjI1My4xMDAyDHJjbHVzdGVyLWNuaRiiWyJOCiRiYjgyOWJiZS02OWExLTRiMmQtYWE4Zi0zNGY5MDQ4YjNiMjUSJgokY2UyZDFjOWMtZGQ5MC00MDYyLWI1M2QtYzdkNTRiZGI2ODI1\",\n            \"condition\": {\n              \"str\": \"running\"\n            },\n            \"networkInfo\": {\n              \"hostName\": \"192.168.99.100\",\n              \"hostPorts\": [],\n              \"ipAddresses\": [\n                {\n                  \"ipAddress\": \"192.168.99.100\",\n                  \"protocol\": \"IPv4\"\n                }\n              ]\n            }\n          }\n        }\n      },\n      \"runSpecVersion\": \"2017-07-13T23:04:05.835Z\",\n      \"state\": {\n        \"condition\": {\n          \"str\": \"running\"\n        },\n        \"since\": \"2017-07-13T23:04:07.767Z\",\n        \"activeSince\": \"2017-07-13T23:04:07.767Z\"\n      },\n      \"unreachableStrategy\": {\n        \"inactiveAfterSeconds\": 300,\n        \"expungeAfterSeconds\": 600\n      }\n    }\n- uri: /v2/pods/fake-pod::instances\n  method: DELETE\n  content: |\n    [\n      {\n        \"instanceId\": {\n          \"idString\": \"fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003\"\n        },\n        \"agentInfo\": {\n          \"host\": \"192.168.99.100\",\n          \"agentId\": \"c3856950-ab1a-4aea-b1a8-168397c0fb33-S9\",\n          \"attributes\": [\n            \"CgR6b25lEAMqBgoEcGR4Mw==\",\n            \"CgZyZWdpb24QABoJCQAAAAAAAAhA\",\n            \"Cg1pbnN0YW5jZV90eXBlEAMqCQoHY29tcHV0ZQ==\",\n            \"CgNzZG4QAyoKCghjb250cmFpbA==\",\n            \"CgJvcxADKgkKB2NlbnRvczc=\"\n          ]\n        },\n        \"tasksMap\": {\n          \"fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003.container1\": {\n            \"taskId\": \"fake-pod.instance-dc6cfe60-6812-11e7-a18e-70b3d5800003.container1\",\n            \"runSpecVersion\": \"2017-07-13T23:04:05.835Z\",\n            \"status\": {\n              \"stagedAt\": \"2017-07-13T23:04:05.939Z\",\n              \"startedAt\": \"2017-07-13T23:04:07.767Z\",\n              \"mesosStatus\": \"Ck0KS3JjbHVzdGVyX2Rjb3Mtc2lkZWNhci5pbnN0YW5jZS05MWU5YzE1OC02ODFmLTExZTctYTE4ZS03MGIzZDU4MDAwMDMuc2lkZWNhchABKikKJ2MzODU2OTUwLWFiMWEtNGFlYS1iMWE4LTE2ODM5N2MwZmIzMy1TOTGxw/AZ/1nWQTpFCkNpbnN0YW5jZS1yY2x1c3Rlcl9kY29zLXNpZGVjYXIuOTFlOWMxNTgtNjgxZi0xMWU3LWExOGUtNzBiM2Q1ODAwMDAzSAJaEOVRUUkSVEbRi5mOLzx0bnxqkgIKvAEimAEKLgoRcmNsdXN0ZXIubG9jYXRpb24SGWdsb2JhbHJpb3QucGR4Mi5yY2x1c3RlcjEKGgoOcmNsdXN0ZXIuZ3JvdXASCHJjbHVzdGVyCiQKFHJjbHVzdGVyLmFwcGxpY2F0aW9uEgxkY29zLXNpZGVjYXIKJAoKRENPU19TUEFDRRIWL3JjbHVzdGVyL2Rjb3Mtc2lkZWNhcioRCAESDTEwLjQwLjI1My4xMDAyDHJjbHVzdGVyLWNuaRiiWyJOCiRiYjgyOWJiZS02OWExLTRiMmQtYWE4Zi0zNGY5MDQ4YjNiMjUSJgokY2UyZDFjOWMtZGQ5MC00MDYyLWI1M2QtYzdkNTRiZGI2ODI1\",\n              \"condition\": {\n                \"str\": \"running\"\n              },\n              \"networkInfo\": {\n                \"hostName\": \"192.168.99.100\",\n                \"hostPorts\": [],\n                \"ipAddresses\": [\n                  {\n                    \"ipAddress\": \"192.168.99.100\",\n                    \"protocol\": \"IPv4\"\n                  }\n                ]\n              }\n            }\n          }\n        },\n        \"runSpecVersion\": \"2017-07-13T23:04:05.835Z\",\n        \"state\": {\n          \"condition\": {\n            \"str\": \"running\"\n          },\n          \"since\": \"2017-07-13T23:04:07.767Z\",\n          \"activeSince\": \"2017-07-13T23:04:07.767Z\"\n        },\n        \"unreachableStrategy\": {\n          \"inactiveAfterSeconds\": 300,\n          \"expungeAfterSeconds\": 600\n        }\n      }\n    ]\n- uri: /v2/groups\n  method: GET\n  content: |\n    {\n        \"apps\": [],\n        \"dependencies\": [],\n        \"groups\": [\n            {\n                \"apps\": [\n                    {\n                        \"args\": null,\n                        \"backoffFactor\": 1.15,\n                        \"backoffSeconds\": 1,\n                        \"maxLaunchDelaySeconds\": 3600,\n                        \"cmd\": \"sleep 30\",\n                        \"constraints\": [],\n                        \"container\": null,\n                        \"cpus\": 1.0,\n                        \"dependencies\": [],\n                        \"disk\": 0.0,\n                        \"env\": {},\n                        \"executor\": \"\",\n                        \"healthChecks\": [],\n                        \"id\": \"/test/app\",\n                        \"instances\": 1,\n                        \"mem\": 128.0,\n                        \"requirePorts\": false,\n                        \"storeUrls\": [],\n                        \"upgradeStrategy\": {\n                            \"minimumHealthCapacity\": 1.0\n                        },\n                        \"user\": null,\n                        \"version\": \"2014-08-28T01:05:40.586Z\"\n                    }\n                ],\n                \"dependencies\": [],\n                \"groups\": [],\n                \"id\": \"/test\",\n                \"version\": \"2014-08-28T01:09:46.212Z\"\n            }\n        ],\n        \"id\": \"/\",\n        \"version\": \"2014-08-28T01:09:46.212Z\"\n    }\n- uri: /v2/groups/test\n  method: GET\n  content: |\n    {\n        \"apps\": [\n            {\n                \"args\": null,\n                \"backoffFactor\": 1.15,\n                \"backoffSeconds\": 1,\n                \"maxLaunchDelaySeconds\": 3600,\n                \"cmd\": \"sleep 30\",\n                \"constraints\": [],\n                \"container\": null,\n                \"cpus\": 1.0,\n                \"dependencies\": [],\n                \"disk\": 0.0,\n                \"env\": {},\n                \"executor\": \"\",\n                \"healthChecks\": [],\n                \"id\": \"/test/app\",\n                \"instances\": 1,\n                \"mem\": 128.0,\n                \"requirePorts\": false,\n                \"storeUrls\": [],\n                \"upgradeStrategy\": {\n                    \"minimumHealthCapacity\": 1.0\n                },\n                \"user\": null,\n                \"version\": \"2014-08-28T01:05:40.586Z\"\n            }\n        ],\n        \"dependencies\": [],\n        \"groups\": [],\n        \"id\": \"/test\",\n        \"version\": \"2014-08-28T01:09:46.212Z\"\n    }\n- uri: /v2/groups/qa/product/1\n  method: GET\n  content: |\n    {\n      \"id\": \"/qa/product/1\",\n      \"groups\": [\n        {\n          \"id\": \"frontend\",\n          \"apps\": [\n            {\n              \"cmd\": \"\",\n              \"container\": {\n                \"type\": \"DOCKER\",\n                \"docker\": {\n                  \"image\": \"quay.io/gambol99/apache-php:latest\"\n                },\n                \"portMappings\": [\n                    {\n                      \"containerPort\": 80,\n                      \"hostPort\": 0,\n                      \"protocol\": \"tcp\"\n                    },\n                    {\n                      \"containerPort\": 443,\n                      \"hostPort\": 0,\n                      \"protocol\": \"tcp\"\n                    }\n                ]\n              },\n              \"healthChecks\": [\n                {\n                  \"protocol\": \"HTTP\",\n                  \"path\": \"/hostname.php\",\n                  \"gracePeriodSeconds\": 3,\n                  \"intervalSeconds\": 10,\n                  \"portIndex\": 0,\n                  \"timeoutSeconds\": 10,\n                  \"maxConsecutiveFailures\": 3\n                }\n              ],\n              \"id\": \"apache\",\n              \"mem\": 64,\n              \"args\": [],\n              \"networks\": [\n                {\n                  \"mode\": \"container/bridge\"\n                }\n              ],\n              \"env\": {\n                \"ENVIRONMENT\": \"qa\",\n                \"SERVICE_80_NAME\": \"apache_http-qa-1\",\n                \"SERVICE_443_NAME\": \"apache_https-qa-1\",\n                \"NAME\": \"frontend\",\n                \"BACKEND_MYSQL_MASTER\": \"mysql-qa-1;3306\",\n                \"BACKEND_CACHE\": \"redis-qa-1;6379\"\n              },\n              \"dependencies\": [\n                \"/qa/product/1/database\",\n                \"/qa/product/1/caching\"\n              ]\n            },{\n              \"id\": \"database\",\n              \"container\": {\n                \"type\": \"DOCKER\",\n                \"docker\": {\n                  \"image\": \"tutum/mysql\"\n                },\n                \"portMappings\": [\n                  { \"containerPort\": 3306, \"hostPort\": 0, \"protocol\": \"tcp\" }\n                ]\n              },\n              \"healthChecks\": [\n                {\n                  \"portIndex\": 0,\n                  \"protocol\": \"TCP\",\n                  \"gracePeriodSeconds\": 10,\n                  \"intervalSeconds\": 10,\n                  \"timeoutSeconds\": 5,\n                  \"maxConsecutiveFailures\": 2\n                }\n              ],\n              \"id\": \"mysql\",\n              \"mem\": 1024,\n              \"cmd\": \"\",\n              \"networks\": [\n                {\n                  \"mode\": \"container/bridge\"\n                }\n              ],\n              \"env\": {\n                \"ENVIRONMENT\": \"qa\",\n                \"SERVICE_NAME\": \"dbmaster\",\n                \"SERVICE_3306_NAME\": \"mysql-qa-1\",\n                \"MYSQL_PASS\": \"mysql\",\n                \"REPLICATION_MASTER\": \"true\",\n                \"REPLICATION_USER\": \"replication\",\n                \"REPLICATION_PASS\": \"8d67as9f7sjhsdfsd\"\n              }\n            },{\n              \"container\": {\n                \"type\": \"DOCKER\",\n                \"docker\": {\n                  \"image\": \"redis\"\n                },\n                \"portMappings\": [\n                    {\n                      \"containerPort\": 6379,\n                      \"hostPort\": 0,\n                      \"protocol\": \"tcp\"\n                    }\n                ]\n              },\n              \"healthChecks\": [\n                {\n                  \"portIndex\": 0,\n                  \"protocol\": \"TCP\",\n                  \"gracePeriodSeconds\": 10,\n                  \"intervalSeconds\": 10,\n                  \"timeoutSeconds\": 5,\n                  \"maxConsecutiveFailures\": 2\n                }\n              ],\n              \"id\": \"caching\",\n              \"cmd\": \"\",\n              \"mem\": 128,\n              \"networks\": [\n                {\n                  \"mode\": \"container/bridge\"\n                }\n              ],\n              \"env\": {\n                \"ENVIRONMENT\": \"qa\",\n                \"SERVICE_6379_NAME\": \"redis-qa-1\"\n              }\n            }\n          ]\n        }\n      ]\n    }\n- uri: /v2/groups/:groupId\n  method: PUT\n  content: |\n    {\n        \"deploymentId\": \"c0e7434c-df47-4d23-99f1-78bd78662231\",\n        \"version\": \"2014-08-28T16:45:41.063Z\"\n    }\n- uri: /v2/groups/:groupId\n  method: DELETE\n  content: |\n    {\"version\":\"2014-07-01T10:20:50.196Z\"}\n- uri: /v2/tasks\n  method: GET\n  content: |\n    {\n        \"tasks\": [\n            {\n                \"appId\": \"/bridged-webapp\",\n                \"healthCheckResults\": [\n                    {\n                        \"alive\": true,\n                        \"consecutiveFailures\": 0,\n                        \"firstSuccess\": \"2014-10-03T22:57:02.246Z\",\n                        \"lastFailure\": null,\n                        \"lastSuccess\": \"2014-10-03T22:57:41.643Z\",\n                        \"taskId\": \"bridged-webapp.eb76c51f-4b4a-11e4-ae49-56847afe9799\"\n                    }\n                ],\n                \"host\": \"10.141.141.10\",\n                \"id\": \"bridged-webapp.eb76c51f-4b4a-11e4-ae49-56847afe9799\",\n                \"ports\": [\n                    31000\n                ],\n                \"servicePorts\": [\n                    9000\n                ],\n                \"stagedAt\": \"2014-10-03T22:16:27.811Z\",\n                \"startedAt\": \"2014-10-03T22:57:41.587Z\",\n                \"version\": \"2014-10-03T22:16:23.634Z\"\n            },\n            {\n                \"appId\": \"/bridged-webapp\",\n                \"healthCheckResults\": [\n                    {\n                        \"alive\": true,\n                        \"consecutiveFailures\": 0,\n                        \"firstSuccess\": \"2014-10-03T22:57:02.246Z\",\n                        \"lastFailure\": null,\n                        \"lastSuccess\": \"2014-10-03T22:57:41.649Z\",\n                        \"taskId\": \"bridged-webapp.ef0b5d91-4b4a-11e4-ae49-56847afe9799\"\n                    }\n                ],\n                \"host\": \"10.141.141.10\",\n                \"id\": \"bridged-webapp.ef0b5d91-4b4a-11e4-ae49-56847afe9799\",\n                \"ports\": [\n                    31001\n                ],\n                \"servicePorts\": [\n                    9000\n                ],\n                \"stagedAt\": \"2014-10-03T22:16:33.814Z\",\n                \"startedAt\": \"2014-10-03T22:57:41.593Z\",\n                \"version\": \"2014-10-03T22:16:23.634Z\"\n            }\n        ]\n    }\n- uri: /v2/tasks?status=staging\n  method: GET\n  content: |\n    {\"tasks\":[]}\n- uri: /v2/tasks/delete\n  method: POST\n- uri: /v2/deployments\n  method: GET\n  content: |\n    [\n        {\n            \"affectedApps\": [\n                \"/test\"\n            ],\n            \"id\": \"867ed450-f6a8-4d33-9b0e-e11c5513990b\",\n            \"steps\": [\n                [\n                    {\n                        \"action\": \"ScaleApplication\",\n                        \"app\": \"/test\"\n                    }\n                ]\n            ],\n            \"currentActions\": [\n              {\n                \"action\": \"ScaleApplication\",\n                \"app\": \"/test\"\n              }\n            ],\n            \"version\": \"2014-08-26T08:18:03.595Z\",\n            \"currentStep\": 1,\n            \"totalSteps\": 1\n        }\n    ]\n- uri: /v2/deployments\n  method: GET\n  scope: v1.1.1\n  content: |\n    [\n      {\n        \"id\": \"2620aa06-1001-4eea-8861-a51957d4fd80\",\n        \"version\": \"2016-06-06T16:06:11.612Z\",\n        \"affectedApps\": [\n          \"/test-app-v1\"\n        ],\n        \"steps\": [\n          {\n            \"actions\": [\n              {\n                \"type\": \"StartApplication\",\n                \"app\": \"/test-app-v1\"\n              }\n            ]\n          },\n          {\n            \"actions\": [\n              {\n                \"type\": \"ScaleApplication\",\n                \"app\": \"/test-app-v1\"\n              }\n            ]\n          }\n        ],\n        \"currentActions\": [\n          {\n            \"action\": \"ScaleApplication\",\n            \"app\": \"/test-app-v1\",\n            \"readinessCheckResults\": [\n              {\n                \"name\": \"myReadyCheck\",\n                \"taskId\": \"test_frontend_app1.c9de6033\",\n                \"ready\": false,\n                \"lastResponse\": {\n                  \"body\": \"{}\",\n                  \"contentType\": \"application/json\",\n                  \"status\": 500\n                }\n              }\n            ]\n          }\n        ],\n        \"currentStep\": 2,\n        \"totalSteps\": 2\n      }\n    ]\n\n- uri: /v2/deployments/867ed450-f6a8-4d33-9b0e-e11c5513990b\n  method: DELETE\n  content: |\n    {\n        \"deploymentId\": \"0b1467fc-d5cd-4bbc-bac2-2805351cee1e\",\n        \"version\": \"2014-08-26T08:20:26.171Z\"\n    }\n- uri: /v2/deployments/867ed450-f6a8-4d33-9b0e-e11c5513990b?force=true\n  method: DELETE\n  content: |\n    {\n    }\n- uri: /v2/eventSubscriptions?callbackUrl=http://localhost:9292/callback\n  method: POST\n  content: |\n    {\n            \"callbackUrl\": \"http://localhost:9292/callback\",\n            \"clientIp\": \"0:0:0:0:0:0:0:1\",\n            \"eventType\": \"subscribe_event\"\n        }\n- uri: /v2/eventSubscriptions\n  method: POST\n  content: |\n    {\n        \"callbackUrl\": \"http://localhost:9292/callback\",\n        \"clientIp\": \"0:0:0:0:0:0:0:1\",\n        \"eventType\": \"subscribe_event\"\n    }\n- uri: /v2/eventSubscriptions\n  method: GET\n  content: |\n    {\n        \"callbackUrls\": [\n            \"http://localhost:9292/callback\"\n        ]\n    }\n- uri: /v2/eventSubscriptions?callbackUrl=http://localhost:9292/callback\n  method: DELETE\n  content: |\n    {\n        \"callbackUrl\": \"http://localhost:9292/callback\",\n        \"clientIp\": \"0:0:0:0:0:0:0:1\",\n        \"eventType\": \"unsubscribe_event\"\n    }\n- uri: /v2/queue\n  method: GET\n  content: |\n    {\n        \"queue\": [\n            {\n                \"count\": 10,\n                \"delay\": {\n                  \"overdue\": true,\n                  \"timeLeftSeconds\": 784\n                },\n                \"app\": {\n                    \"args\": null,\n                    \"backoffFactor\": 1.15,\n                    \"backoffSeconds\": 1,\n                    \"cmd\": \"python toggle.py $PORT0\",\n                    \"constraints\": [],\n                    \"container\": null,\n                    \"cpus\": 0.2,\n                    \"dependencies\": [],\n                    \"disk\": 0.0,\n                    \"env\": {},\n                    \"executor\": \"\",\n                    \"healthChecks\": [],\n                    \"id\": \"/test\",\n                    \"instances\": 3,\n                    \"mem\": 32.0,\n                    \"requirePorts\": false,\n                    \"storeUrls\": [],\n                    \"upgradeStrategy\": {\n                        \"minimumHealthCapacity\": 1.0\n                    },\n                    \"user\": null,\n                    \"version\": \"2014-08-26T05:04:49.766Z\"\n                }\n            }\n        ]\n    }\n\n- uri: /v2/queue/fake-app/delay\n  method: DELETE\n\n- uri: /v2/leader\n  method: GET\n  content: |\n    {\n       \"leader\": \"127.0.0.1:8080\"\n    }\n- uri: /v2/leader\n  method: DELETE\n  content: |\n    {\n      \"message\": \"Leadership abdicted\"\n    }\n- uri: /v2/info\n  method: GET\n  content: |\n    {\n        \"frameworkId\": \"20140730-222531-1863654316-5050-10422-0000\",\n        \"leader\": \"127.0.0.1:8080\",\n        \"http_config\": {\n            \"assets_path\": null,\n            \"http_port\": 8080,\n            \"https_port\": 8443\n        },\n        \"event_subscriber\": {\n            \"type\": \"http_callback\",\n            \"http_endpoints\": [\n                \"localhost:9999/events\"\n            ]\n        },\n        \"marathon_config\": {\n            \"checkpoint\": false,\n            \"executor\": \"//cmd\",\n            \"failover_timeout\": 604800,\n            \"ha\": true,\n            \"hostname\": \"127.0.0.1\",\n            \"local_port_max\": 49151,\n            \"local_port_min\": 32767,\n            \"master\": \"zk://localhost:2181/mesos\",\n            \"mesos_role\": null,\n            \"mesos_user\": \"root\",\n            \"reconciliation_initial_delay\": 30000,\n            \"reconciliation_interval\": 30000,\n            \"task_launch_timeout\": 60000\n        },\n        \"name\": \"marathon\",\n        \"version\": \"0.7.0-SNAPSHOT\",\n        \"zookeeper_config\": {\n            \"zk\": \"zk://localhost:2181/marathon\",\n            \"zk_future_timeout\": {\n                \"duration\": 10\n            },\n            \"zk_hosts\": \"localhost:2181\",\n            \"zk_path\": \"/marathon\",\n            \"zk_state\": \"/marathon\",\n            \"zk_timeout\": 10\n        }\n    }\n- uri: /ping\n  method: GET\n  content: |\n    pong\n- uri: /marathon/v2/apps\n  method: GET\n  content: |\n    {\n    \"apps\": [\n        {\n            \"args\": null,\n            \"backoffFactor\": 1.15,\n            \"backoffSeconds\": 1,\n            \"cmd\": \"python3 -m http.server 8080\",\n            \"constraints\": [],\n            \"container\": {\n                \"docker\": {\n                    \"image\": \"python:3\",\n                    \"network\": \"BRIDGE\",\n                    \"portMappings\": [\n                        {\n                            \"containerPort\": 8080,\n                            \"hostPort\": 0,\n                            \"servicePort\": 9000,\n                            \"protocol\": \"tcp\"\n                        },\n                        {\n                            \"containerPort\": 161,\n                            \"hostPort\": 0,\n                            \"protocol\": \"udp\"\n                        }\n                    ]\n                },\n                \"type\": \"DOCKER\",\n                \"volumes\": []\n            },\n            \"cpus\": 0.5,\n            \"dependencies\": [],\n            \"deployments\": [],\n            \"disk\": 0.0,\n            \"env\": {},\n            \"executor\": \"\",\n            \"healthChecks\": [\n                {\n                    \"command\": null,\n                    \"gracePeriodSeconds\": 5,\n                    \"intervalSeconds\": 20,\n                    \"maxConsecutiveFailures\": 3,\n                    \"path\": \"/\",\n                    \"portIndex\": 0,\n                    \"protocol\": \"HTTP\",\n                    \"timeoutSeconds\": 20\n                }\n            ],\n            \"id\": \"/fake-app\",\n            \"instances\": 2,\n            \"mem\": 64.0,\n            \"requirePorts\": false,\n            \"residency\" : {\n                \"taskLostBehavior\" : \"RELAUNCH_AFTER_TIMEOUT\",\n                \"relaunchEscalationTimeoutSeconds\" : 60\n            },\n            \"storeUrls\": [],\n            \"tasksRunning\": 2,\n            \"tasksStaged\": 0,\n            \"upgradeStrategy\": {\n                \"minimumHealthCapacity\": 1.0\n            },\n            \"user\": null,\n            \"version\": \"2014-09-25T02:26:59.256Z\"\n        },\n        {\n            \"args\": null,\n            \"backoffFactor\": 1.15,\n            \"backoffSeconds\": 1,\n            \"cmd\": \"python3 -m http.server 8080\",\n            \"constraints\": [],\n            \"container\": {\n                \"docker\": {\n                    \"image\": \"python:3\",\n                    \"network\": \"BRIDGE\",\n                    \"portMappings\": [\n                        {\n                            \"containerPort\": 8080,\n                            \"hostPort\": 0,\n                            \"servicePort\": 9000,\n                            \"protocol\": \"tcp\"\n                        },\n                        {\n                            \"containerPort\": 161,\n                            \"hostPort\": 0,\n                            \"protocol\": \"udp\"\n                        }\n                    ]\n                },\n                \"type\": \"DOCKER\",\n                \"volumes\": []\n            },\n            \"cpus\": 0.5,\n            \"dependencies\": [],\n            \"deployments\": [],\n            \"disk\": 0.0,\n            \"env\": {},\n            \"executor\": \"\",\n            \"healthChecks\": [\n                {\n                    \"command\": null,\n                    \"gracePeriodSeconds\": 5,\n                    \"intervalSeconds\": 20,\n                    \"maxConsecutiveFailures\": 3,\n                    \"path\": \"/\",\n                    \"portIndex\": 0,\n                    \"protocol\": \"HTTP\",\n                    \"timeoutSeconds\": 20\n                }\n            ],\n            \"id\": \"/fake-app-broken\",\n            \"instances\": 2,\n            \"mem\": 64.0,\n            \"requirePorts\": false,\n            \"storeUrls\": [],\n            \"tasksRunning\": 2,\n            \"tasksStaged\": 0,\n            \"upgradeStrategy\": {\n                \"minimumHealthCapacity\": 1.0\n            },\n            \"user\": null,\n            \"version\": \"2014-09-25T02:26:59.256Z\"\n        }\n    ]\n    }\n- uri: /v2/apps/no-health-check-results-app\n  method: GET\n  content: |\n    {\n    \"app\": {\n        \"healthChecks\": [\n            {\n                \"command\": null,\n                \"gracePeriodSeconds\": 5,\n                \"intervalSeconds\": 10,\n                \"maxConsecutiveFailures\": 3,\n                \"path\": \"/health\",\n                \"portIndex\": 0,\n                \"protocol\": \"HTTP\",\n                \"timeoutSeconds\": 10\n            }\n        ],\n        \"id\": \"/no-health-check-results-app\",\n        \"instances\": 2,\n        \"mem\": 32.0,\n        \"tasks\": [\n            {\n                \"appId\": \"/no-health-check-results-app\",\n                \"healthCheckResults\": [\n                    {\n                        \"alive\": true,\n                        \"consecutiveFailures\": 0,\n                        \"firstSuccess\": \"2014-09-13T00:20:28.101Z\",\n                        \"lastFailure\": null,\n                        \"lastSuccess\": \"2014-09-13T00:25:07.506Z\",\n                        \"taskId\": \"toggle.802df2ae-3ad4-11e4-a400-56847afe9799\"\n                    }\n                ],\n                \"id\": \"task1.802df2ae-3ad4-11e4-a400-56847afe9799\"\n            },\n            {\n                \"appId\": \"/no-health-check-results-app\",\n                \"id\": \"task2.7c99814d-3ad4-11e4-a400-56847afe9799\"\n            }\n        ],\n        \"tasksRunning\": 2,\n        \"tasksStaged\": 0,\n        \"version\": \"2014-09-12T23:28:21.737Z\"\n    }\n    }\n- uri: /v2/apps?embed=apps.readiness\n  method: GET\n  content: |\n    {\n    \"apps\": [\n      {\n        \"id\": \"/fake-app\",\n        \"readinessCheckResults\": [\n          {\n            \"name\": \"myReadyCheck\",\n            \"taskId\": \"test_frontend_app1.c9de6033\",\n            \"ready\": false,\n            \"lastResponse\": {\n              \"body\": \"{}\",\n              \"contentType\": \"application/json\",\n              \"status\": 500\n            }\n          }\n        ]\n      }\n    ]\n    }\n- uri: /v2/apps/fake-app\n  method: GET\n  scope: unreachablestrategy-present\n  content: |\n    {\n    \"app\": {\n          \"id\": \"/fake-app\",\n          \"unreachableStrategy\": {\n            \"inactiveAfterSeconds\": 3.0,\n            \"expungeAfterSeconds\": 4.0\n          }\n      }\n    }\n- uri: /v2/apps/fake-app\n  method: GET\n  scope: unreachablestrategy-absent\n  content: |\n    {\n    \"app\": {\n          \"id\": \"/fake-app\",\n          \"unreachableStrategy\": \"disabled\"\n      }\n    }\n\n- uri: /v2/apps/fake-app\n  method: GET\n  scope: environment-variables\n  content: |\n    {\n    \"app\": {\n          \"id\": \"/fake-app\",\n          \"env\": {\n            \"FOO\": \"bar\",\n            \"TOP\": {\n              \"secret\": \"secret\"\n              }\n            },\n          \"secrets\":{\n            \"secret\": {\n              \"source\": \"/path/to/secret\"\n              }\n            }\n      }\n    }\n"
  },
  {
    "path": "unreachable_strategy.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n)\n\n// UnreachableStrategyAbsenceReasonDisabled signifies the reason of disabled unreachable strategy\nconst UnreachableStrategyAbsenceReasonDisabled = \"disabled\"\n\n// UnreachableStrategy is the unreachable strategy applied to an application.\ntype UnreachableStrategy struct {\n\tEnabledUnreachableStrategy\n\tAbsenceReason string\n}\n\n// EnabledUnreachableStrategy covers parameters pertaining to present unreachable strategies.\ntype EnabledUnreachableStrategy struct {\n\tInactiveAfterSeconds *float64 `json:\"inactiveAfterSeconds,omitempty\"`\n\tExpungeAfterSeconds  *float64 `json:\"expungeAfterSeconds,omitempty\"`\n}\n\ntype unreachableStrategy UnreachableStrategy\n\n// UnmarshalJSON unmarshals the given JSON into an UnreachableStrategy. It\n// populates parameters for present strategies, and otherwise only sets the\n// absence reason.\nfunc (us *UnreachableStrategy) UnmarshalJSON(b []byte) error {\n\tvar u unreachableStrategy\n\tvar errEnabledUS, errNonEnabledUS error\n\tif errEnabledUS = json.Unmarshal(b, &u); errEnabledUS == nil {\n\t\t*us = UnreachableStrategy(u)\n\t\treturn nil\n\t}\n\n\tif errNonEnabledUS = json.Unmarshal(b, &us.AbsenceReason); errNonEnabledUS == nil {\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"failed to unmarshal unreachable strategy: unmarshaling into enabled returned error '%s'; unmarshaling into non-enabled returned error '%s'\", errEnabledUS, errNonEnabledUS)\n}\n\n// MarshalJSON marshals the unreachable strategy.\nfunc (us *UnreachableStrategy) MarshalJSON() ([]byte, error) {\n\tif us.AbsenceReason == \"\" {\n\t\treturn json.Marshal(us.EnabledUnreachableStrategy)\n\t}\n\n\treturn json.Marshal(us.AbsenceReason)\n}\n\n// SetInactiveAfterSeconds sets the period after which instance will be marked as inactive.\nfunc (us *UnreachableStrategy) SetInactiveAfterSeconds(cap float64) *UnreachableStrategy {\n\tus.InactiveAfterSeconds = &cap\n\treturn us\n}\n\n// SetExpungeAfterSeconds sets the period after which instance will be expunged.\nfunc (us *UnreachableStrategy) SetExpungeAfterSeconds(cap float64) *UnreachableStrategy {\n\tus.ExpungeAfterSeconds = &cap\n\treturn us\n}\n"
  },
  {
    "path": "unreachable_strategy_test.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestUnreachableStrategyAPI(t *testing.T) {\n\tapp := Application{}\n\trequire.Nil(t, app.UnreachableStrategy)\n\tus := new(UnreachableStrategy)\n\tus.SetExpungeAfterSeconds(30.0).SetInactiveAfterSeconds(5.0)\n\tapp.SetUnreachableStrategy(*us)\n\ttestUs := app.UnreachableStrategy\n\tassert.Equal(t, 30.0, *testUs.ExpungeAfterSeconds)\n\tassert.Equal(t, 5.0, *testUs.InactiveAfterSeconds)\n\n\tapp.EmptyUnreachableStrategy()\n\tus = app.UnreachableStrategy\n\trequire.NotNil(t, us)\n\tassert.Nil(t, us.ExpungeAfterSeconds)\n\tassert.Nil(t, us.InactiveAfterSeconds)\n}\n\nfunc TestUnreachableStrategyUnmarshalEnabled(t *testing.T) {\n\tdefaultConfig := NewDefaultConfig()\n\tconfigs := &configContainer{\n\t\tclient: &defaultConfig,\n\t\tserver: &serverConfig{\n\t\t\tscope: \"unreachablestrategy-present\",\n\t\t},\n\t}\n\n\tendpoint := newFakeMarathonEndpoint(t, configs)\n\tdefer endpoint.Close()\n\n\tapplication, err := endpoint.Client.Application(fakeAppName)\n\trequire.NoError(t, err)\n\n\tus := application.UnreachableStrategy\n\trequire.NotNil(t, us)\n\tassert.Empty(t, us.AbsenceReason)\n\tif assert.NotNil(t, us.InactiveAfterSeconds) {\n\t\tassert.Equal(t, 3.0, *us.InactiveAfterSeconds)\n\t}\n\tif assert.NotNil(t, us.ExpungeAfterSeconds) {\n\t\tassert.Equal(t, 4.0, *us.ExpungeAfterSeconds)\n\t}\n}\n\nfunc TestUnreachableStrategyUnmarshalNonEnabled(t *testing.T) {\n\tdefaultConfig := NewDefaultConfig()\n\tconfigs := &configContainer{\n\t\tclient: &defaultConfig,\n\t\tserver: &serverConfig{\n\t\t\tscope: \"unreachablestrategy-absent\",\n\t\t},\n\t}\n\n\tendpoint := newFakeMarathonEndpoint(t, configs)\n\tdefer endpoint.Close()\n\n\tapplication, err := endpoint.Client.Application(fakeAppName)\n\trequire.NoError(t, err)\n\n\tus := application.UnreachableStrategy\n\trequire.NotNil(t, us)\n\tassert.Equal(t, UnreachableStrategyAbsenceReasonDisabled, us.AbsenceReason)\n}\n\nfunc TestUnreachableStrategyUnmarshalIllegal(t *testing.T) {\n\tj := []byte(`{false}`)\n\tus := UnreachableStrategy{}\n\tassert.Error(t, us.UnmarshalJSON(j))\n}\n\nfunc TestUnreachableStrategyMarshal(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tus       UnreachableStrategy\n\t\twantJSON string\n\t}{\n\t\t{\n\t\t\tname: \"present\",\n\t\t\tus: UnreachableStrategy{\n\t\t\t\tEnabledUnreachableStrategy: EnabledUnreachableStrategy{\n\t\t\t\t\tInactiveAfterSeconds: float64p(3.5),\n\t\t\t\t\tExpungeAfterSeconds:  float64p(4.5),\n\t\t\t\t},\n\t\t\t\tAbsenceReason: \"\",\n\t\t\t},\n\t\t\twantJSON: `{\"inactiveAfterSeconds\":3.5,\"expungeAfterSeconds\":4.5}`,\n\t\t},\n\t\t{\n\t\t\tname: \"absent\",\n\t\t\tus: UnreachableStrategy{\n\t\t\t\tAbsenceReason: UnreachableStrategyAbsenceReasonDisabled,\n\t\t\t},\n\t\t\twantJSON: fmt.Sprintf(`\"%s\"`, UnreachableStrategyAbsenceReasonDisabled),\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tlabel := fmt.Sprintf(\"test: %s\", test.name)\n\t\tj, err := test.us.MarshalJSON()\n\t\tif assert.NoError(t, err, label) {\n\t\t\tassert.Equal(t, test.wantJSON, string(j), label)\n\t\t}\n\t}\n}\n\nfunc float64p(f float64) *float64 {\n\treturn &f\n}\n"
  },
  {
    "path": "upgrade_strategy.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\n// UpgradeStrategy is the upgrade strategy applied to an application.\ntype UpgradeStrategy struct {\n\tMinimumHealthCapacity *float64 `json:\"minimumHealthCapacity,omitempty\"`\n\tMaximumOverCapacity   *float64 `json:\"maximumOverCapacity,omitempty\"`\n}\n\n// SetMinimumHealthCapacity sets the minimum health capacity.\nfunc (us *UpgradeStrategy) SetMinimumHealthCapacity(cap float64) *UpgradeStrategy {\n\tus.MinimumHealthCapacity = &cap\n\treturn us\n}\n\n// SetMaximumOverCapacity sets the maximum over capacity.\nfunc (us *UpgradeStrategy) SetMaximumOverCapacity(cap float64) *UpgradeStrategy {\n\tus.MaximumOverCapacity = &cap\n\treturn us\n}\n"
  },
  {
    "path": "utils.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/google/go-querystring/query\"\n)\n\ntype atomicSwitch int64\n\nfunc (r *atomicSwitch) IsSwitched() bool {\n\treturn atomic.LoadInt64((*int64)(r)) != 0\n}\n\nfunc (r *atomicSwitch) SwitchOn() {\n\tatomic.StoreInt64((*int64)(r), 1)\n}\n\nfunc (r *atomicSwitch) SwitchedOff() {\n\tatomic.StoreInt64((*int64)(r), 0)\n}\n\nfunc validateID(id string) string {\n\tif !strings.HasPrefix(id, \"/\") {\n\t\treturn fmt.Sprintf(\"/%s\", id)\n\t}\n\treturn id\n}\n\nfunc trimRootPath(id string) string {\n\tif strings.HasPrefix(id, \"/\") {\n\t\treturn strings.TrimPrefix(id, \"/\")\n\t}\n\treturn id\n}\n\nfunc deadline(timeout time.Duration, work func(chan bool) error) error {\n\tresult := make(chan error)\n\ttimer := time.After(timeout)\n\tstopChannel := make(chan bool, 1)\n\n\t// allow the method to attempt\n\tgo func() {\n\t\tresult <- work(stopChannel)\n\t}()\n\tfor {\n\t\tselect {\n\t\tcase err := <-result:\n\t\t\treturn err\n\t\tcase <-timer:\n\t\t\tstopChannel <- true\n\t\t\treturn ErrTimeoutError\n\t\t}\n\t}\n}\n\nfunc getInterfaceAddress(name string) (string, error) {\n\tinterfaces, err := net.Interfaces()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tfor _, iface := range interfaces {\n\t\t// step: get only the interface we're interested in\n\t\tif iface.Name == name {\n\t\t\taddrs, err := iface.Addrs()\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\t// step: return the first address\n\t\t\tif len(addrs) > 0 {\n\t\t\t\treturn parseIPAddr(addrs[0]), nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn \"\", errors.New(\"Unable to determine or find the interface\")\n}\n\nfunc contains(elements []string, value string) bool {\n\tfor _, element := range elements {\n\t\tif element == value {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc parseIPAddr(addr net.Addr) string {\n\treturn strings.SplitN(addr.String(), \"/\", 2)[0]\n}\n\n// addOptions adds the parameters in opt as URL query parameters to s.\n// opt must be a struct whose fields may contain \"url\" tags.\nfunc addOptions(s string, opt interface{}) (string, error) {\n\tv := reflect.ValueOf(opt)\n\tif v.Kind() == reflect.Ptr && v.IsNil() {\n\t\treturn s, nil\n\t}\n\n\tu, err := url.Parse(s)\n\tif err != nil {\n\t\treturn s, err\n\t}\n\n\tqs, err := query.Values(opt)\n\tif err != nil {\n\t\treturn s, err\n\t}\n\n\tu.RawQuery = qs.Encode()\n\treturn u.String(), nil\n}\n\n// Bool returns a pointer to the passed in bool value\nfunc Bool(b bool) *bool {\n\treturn &b\n}\n"
  },
  {
    "path": "utils_test.go",
    "content": "/*\nCopyright 2014 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\nimport (\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype stubAddr struct {\n\taddr string\n}\n\nfunc (sa stubAddr) Network() string {\n\treturn \"network\"\n}\n\nfunc (sa stubAddr) String() string {\n\treturn sa.addr + \"/8\"\n}\n\nfunc TestUtilsAtomicIsSwitched(t *testing.T) {\n\tvar sw atomicSwitch\n\tassert.False(t, sw.IsSwitched())\n\tsw.SwitchOn()\n\tassert.True(t, sw.IsSwitched())\n}\n\nfunc TestUtilsAtomicIsSwitchedOff(t *testing.T) {\n\tvar sw atomicSwitch\n\tassert.False(t, sw.IsSwitched())\n\tsw.SwitchOn()\n\tassert.True(t, sw.IsSwitched())\n\tsw.SwitchedOff()\n\tassert.False(t, sw.IsSwitched())\n}\n\nfunc TestUtilsDeadline(t *testing.T) {\n\terr := deadline(time.Duration(5)*time.Millisecond, func(chan bool) error {\n\t\t<-time.After(time.Duration(1) * time.Second)\n\t\treturn nil\n\t})\n\tassert.Error(t, err)\n\tassert.Equal(t, ErrTimeoutError, err)\n\n\terr = deadline(time.Duration(5)*time.Second, func(chan bool) error {\n\t\t<-time.After(time.Duration(5) * time.Millisecond)\n\t\treturn nil\n\t})\n\n\tassert.NoError(t, err)\n}\n\nfunc TestUtilsContains(t *testing.T) {\n\tlist := []string{\"1\", \"2\", \"3\"}\n\tassert.True(t, contains(list, \"2\"))\n\tassert.False(t, contains(list, \"12\"))\n}\n\nfunc TestUtilsValidateID(t *testing.T) {\n\tpath := \"test/path\"\n\tassert.Equal(t, validateID(path), \"/test/path\")\n\tpath = \"/test/path\"\n\tassert.Equal(t, validateID(path), \"/test/path\")\n}\n\nfunc TestUtilsGetInterfaceAddress(t *testing.T) {\n\t// Find actual IP address we can test against.\n\tinterfaces, err := net.Interfaces()\n\tassert.NoError(t, err)\n\tassert.NotEqual(t, 0, len(interfaces))\n\tiface := interfaces[0]\n\texpectedName := iface.Name\n\taddresses, err := iface.Addrs()\n\tassert.NoError(t, err)\n\texpectedIPAddress := parseIPAddr(addresses[0])\n\n\t// Execute test.\n\taddress, err := getInterfaceAddress(expectedName)\n\tassert.NoError(t, err)\n\tassert.Equal(t, expectedIPAddress, address)\n\taddress, err = getInterfaceAddress(\"nothing\")\n\tassert.Error(t, err)\n\tassert.Equal(t, \"\", address)\n}\n\nfunc TestUtilsTrimRootPath(t *testing.T) {\n\tpath := \"/test/path\"\n\tassert.Equal(t, trimRootPath(path), \"test/path\")\n\tpath = \"test/path\"\n\tassert.Equal(t, trimRootPath(path), \"test/path\")\n}\n\nfunc TestParseIPAddr(t *testing.T) {\n\tipAddr := \"127.0.0.1\"\n\taddr := stubAddr{ipAddr}\n\tassert.Equal(t, ipAddr, parseIPAddr(addr))\n}\n"
  },
  {
    "path": "volume.go",
    "content": "/*\nCopyright 2017 The go-marathon Authors All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage marathon\n\n// PodVolume describes a volume on the host\ntype PodVolume struct {\n\tName       string            `json:\"name,omitempty\"`\n\tHost       string            `json:\"host,omitempty\"`\n\tSecret     string            `json:\"secret,omitempty\"`\n\tPersistent *PersistentVolume `json:\"persistent,omitempty\"`\n}\n\n// PodVolumeMount describes how to mount a volume into a task\ntype PodVolumeMount struct {\n\tName      string `json:\"name,omitempty\"`\n\tMountPath string `json:\"mountPath,omitempty\"`\n\tReadOnly  *bool  `json:\"readOnly,omitempty\"`\n}\n\n// NewPodVolume creates a new PodVolume\nfunc NewPodVolume(name, path string) *PodVolume {\n\treturn &PodVolume{\n\t\tName: name,\n\t\tHost: path,\n\t}\n}\n\n// NewPodVolume creates a new PodVolume for file based secrets\nfunc NewPodVolumeSecret(name, secretPath string) *PodVolume {\n\treturn &PodVolume{\n\t\tName:   name,\n\t\tSecret: secretPath,\n\t}\n}\n\n// NewPodVolumeMount creates a new PodVolumeMount\nfunc NewPodVolumeMount(name, mount string) *PodVolumeMount {\n\treturn &PodVolumeMount{\n\t\tName:      name,\n\t\tMountPath: mount,\n\t}\n}\n\n// SetPersistentVolume sets the persistence settings of a PodVolume\nfunc (pv *PodVolume) SetPersistentVolume(p *PersistentVolume) *PodVolume {\n\tpv.Persistent = p\n\treturn pv\n}\n"
  }
]