diff --git a/.gitignore b/.gitignore index 99e9b12..2947fe4 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,4 @@ packer_cache /gh-pages itests/marathon-version +.pytest_cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..80e93db --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +- repo: https://github.com/asottile/pyupgrade + rev: v1.25.1 + hooks: + - id: pyupgrade + args: [--py36-plus] \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 168f551..542dd88 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,31 +1,39 @@ env: - - MARATHONVERSION: 1.4.7 - - MARATHONVERSION: 1.3.0 - - MARATHONVERSION: 1.1.2 - - MARATHONVERSION: 0.15.3 - - MARATHONVERSION: 0.14.1 - - MARATHONVERSION: 0.13.1 - - MARATHONVERSION: 0.11.1 - - MARATHONVERSION: 0.10.1 + - MARATHONVERSION: v1.10.19 + - MARATHONVERSION: v1.9.109 + - MARATHONVERSION: v1.6.322 + - MARATHONVERSION: v1.4.11 + - MARATHONVERSION: v1.3.0 + - MARATHONVERSION: v1.1.2 language: python +services: + - docker python: - - 2.7 + - 3.6 + - 3.7 +before_install: + - docker pull "missingcharacter/marathon-python:${MARATHONVERSION}" + - docker run --name marathon-python -d -p 8080:8080 -p 5050:5050 "missingcharacter/marathon-python:${MARATHONVERSION}" install: - pip install tox script: - - make test - - ./itests/install-marathon.sh - - ./itests/start-marathon.sh & - - make itests + - make test-py${TRAVIS_PYTHON_VERSION/./} + - make itests-py${TRAVIS_PYTHON_VERSION/./} # Work around travis-ci/travis-ci#5227 addons: hostname: localhost - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - libstdc++6-4.7-dev -sudo: required # make it explicit: it was by default only because this repo was set up before 2015 (new forks need it) +os: linux +dist: xenial + +deploy: + - provider: pypi + user: yelplabs + password: + secure: "Wl8GWxsfPy4KoORYH26N3FllvMeWrifzeCbEx2Af4corcBQl43heeiFRRTlUOcSX0TIasER21PUvQ0R0cAgCjfknDb3SOROcRtcSBe16+cMmvwysfxcAx2OcF1UYBPY8e/qOsGge2Zyzx2PAPNEmJoWKbIT3vUJ4WvlLVeGYdJ0=" + on: + tags: true + condition: $MARATHONVERSION == "v1.6.322" + repo: thefactory/marathon-python diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b979c5..dc2946f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,99 @@ -# Change Log +# Changelog + +## [0.13.0](https://github.com/thefactory/marathon-python/tree/0.13.0) (2020-08-21) + +[Full Changelog](https://github.com/thefactory/marathon-python/compare/0.12.0...0.13.0) + +**Closed issues:** + +- about view one instance app logs real time [\#277](https://github.com/thefactory/marathon-python/issues/277) + +**Merged pull requests:** + +- Fix deserialization for Deploment model [\#276](https://github.com/thefactory/marathon-python/pull/276) ([missingcharacter](https://github.com/missingcharacter)) +- Local tests no longer need docker-compose and rely on "mini-marathon" [\#274](https://github.com/thefactory/marathon-python/pull/274) ([missingcharacter](https://github.com/missingcharacter)) +- Adding support for v1.10.19 [\#273](https://github.com/thefactory/marathon-python/pull/273) ([missingcharacter](https://github.com/missingcharacter)) +- Moving marathon away from ubuntu and starting from mesosphere dockerhub [\#272](https://github.com/thefactory/marathon-python/pull/272) ([missingcharacter](https://github.com/missingcharacter)) +- Updates for Marathon 1.9.109 [\#270](https://github.com/thefactory/marathon-python/pull/270) ([missingcharacter](https://github.com/missingcharacter)) +- Fix return value for kill\_given\_tasks. [\#268](https://github.com/thefactory/marathon-python/pull/268) ([Tilian](https://github.com/Tilian)) +- Bump requests from 2.11.1 to 2.20.0 [\#266](https://github.com/thefactory/marathon-python/pull/266) ([dependabot[bot]](https://github.com/apps/dependabot)) + +## [0.12.0](https://github.com/thefactory/marathon-python/tree/0.12.0) (2019-11-14) + +[Full Changelog](https://github.com/thefactory/marathon-python/compare/0.11.0...0.12.0) + +**Closed issues:** + +- Downloading Log For App [\#265](https://github.com/thefactory/marathon-python/issues/265) + +**Merged pull requests:** + +- Always create TZ-aware datetime objects. \(also drop support for python 2\) [\#267](https://github.com/thefactory/marathon-python/pull/267) ([EvanKrall](https://github.com/EvanKrall)) +- Compatible with event stream redirect behavior. [\#262](https://github.com/thefactory/marathon-python/pull/262) ([fengyehong](https://github.com/fengyehong)) + +## [0.11.0](https://github.com/thefactory/marathon-python/tree/0.11.0) (2019-01-15) + +[Full Changelog](https://github.com/thefactory/marathon-python/compare/0.10.0...0.11.0) + +**Merged pull requests:** + +- Added region and zone members to task model. [\#260](https://github.com/thefactory/marathon-python/pull/260) ([gisjedi](https://github.com/gisjedi)) +- Exception [\#259](https://github.com/thefactory/marathon-python/pull/259) ([fengyehong](https://github.com/fengyehong)) +- New release 0.10.0 [\#256](https://github.com/thefactory/marathon-python/pull/256) ([solarkennedy](https://github.com/solarkennedy)) + +## [0.10.0](https://github.com/thefactory/marathon-python/tree/0.10.0) (2018-08-08) + +[Full Changelog](https://github.com/thefactory/marathon-python/compare/0.9.3...0.10.0) + +**Closed issues:** + +- Travis tests are broken [\#249](https://github.com/thefactory/marathon-python/issues/249) +- SSE SSL authentication not supported [\#247](https://github.com/thefactory/marathon-python/issues/247) +- Lack of support for container.networks [\#243](https://github.com/thefactory/marathon-python/issues/243) +- \_\_init\_\_\(\) got an unexpected keyword argument 'port\_mappings' [\#237](https://github.com/thefactory/marathon-python/issues/237) +- Wrong health check object generated for COMMAND protocol [\#222](https://github.com/thefactory/marathon-python/issues/222) + +**Merged pull requests:** + +- Add support for mesos 1.6 [\#255](https://github.com/thefactory/marathon-python/pull/255) ([jdewinne](https://github.com/jdewinne)) +- Possibility for send the full json object on create [\#252](https://github.com/thefactory/marathon-python/pull/252) ([kkorekk](https://github.com/kkorekk)) +- events: add a few attributes [\#251](https://github.com/thefactory/marathon-python/pull/251) ([iksaif](https://github.com/iksaif)) +- install-marathon.sh: do not remove oracle-java7-installer [\#250](https://github.com/thefactory/marathon-python/pull/250) ([iksaif](https://github.com/iksaif)) +- MarathonClient: set verify when using sse\_session [\#248](https://github.com/thefactory/marathon-python/pull/248) ([iksaif](https://github.com/iksaif)) +- add reset delay api [\#246](https://github.com/thefactory/marathon-python/pull/246) ([iandyh](https://github.com/iandyh)) +- fixes for issue 244 [\#245](https://github.com/thefactory/marathon-python/pull/245) ([mikekatica](https://github.com/mikekatica)) +- Test against 1.4.11 [\#240](https://github.com/thefactory/marathon-python/pull/240) ([nhandler](https://github.com/nhandler)) +- fix isuuse-238 [\#239](https://github.com/thefactory/marathon-python/pull/239) ([yudong2015](https://github.com/yudong2015)) +- Test against 1.4.10 instead of 1.4.9 [\#236](https://github.com/thefactory/marathon-python/pull/236) ([nhandler](https://github.com/nhandler)) +- make models.info compatible with 1.4.9 [\#233](https://github.com/thefactory/marathon-python/pull/233) ([iandyh](https://github.com/iandyh)) +- Fix health check 'command' [\#231](https://github.com/thefactory/marathon-python/pull/231) ([protetore](https://github.com/protetore)) +- Feature/marathon constraint model improvements [\#229](https://github.com/thefactory/marathon-python/pull/229) ([diogommartins](https://github.com/diogommartins)) +- Removes id validation from MarathonGroup\(\) [\#228](https://github.com/thefactory/marathon-python/pull/228) ([daltonmatos](https://github.com/daltonmatos)) + +## [0.9.3](https://github.com/thefactory/marathon-python/tree/0.9.3) (2017-10-16) + +[Full Changelog](https://github.com/thefactory/marathon-python/compare/0.9.2...0.9.3) + +**Closed issues:** + +- `list\_queue` doesn't like the `embed\_last\_unused\_offers` option [\#220](https://github.com/thefactory/marathon-python/issues/220) + +**Merged pull requests:** + +- Release 0.9.3 [\#224](https://github.com/thefactory/marathon-python/pull/224) ([solarkennedy](https://github.com/solarkennedy)) +- Make travis automatically upload to pypi on new tags [\#223](https://github.com/thefactory/marathon-python/pull/223) ([solarkennedy](https://github.com/solarkennedy)) +- Fix MarathonQueueItem to know about the possible last\_unused\_offers arg [\#221](https://github.com/thefactory/marathon-python/pull/221) ([matthewbentley](https://github.com/matthewbentley)) +- support more datetime formats in MarathonAppVersionInfo [\#219](https://github.com/thefactory/marathon-python/pull/219) ([somic](https://github.com/somic)) +- Remove default container.docker.network [\#218](https://github.com/thefactory/marathon-python/pull/218) ([protetore](https://github.com/protetore)) +- Make MarathonZooKeeperConfig compatible with maraton 1.5 [\#216](https://github.com/thefactory/marathon-python/pull/216) ([fengyehong](https://github.com/fengyehong)) ## [0.9.2](https://github.com/thefactory/marathon-python/tree/0.9.2) (2017-09-13) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.9.1...0.9.2) **Closed issues:** +- Failed to import marathon in python3 [\#217](https://github.com/thefactory/marathon-python/issues/217) - No support for "USER" network mode. [\#173](https://github.com/thefactory/marathon-python/issues/173) - YAML support for marathon-cli [\#74](https://github.com/thefactory/marathon-python/issues/74) @@ -14,6 +103,7 @@ - Fix events [\#214](https://github.com/thefactory/marathon-python/pull/214) ([fengyehong](https://github.com/fengyehong)) ## [0.9.1](https://github.com/thefactory/marathon-python/tree/0.9.1) (2017-09-06) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.9.0...0.9.1) **Closed issues:** @@ -28,11 +118,12 @@ - Support filtering applications by labels [\#211](https://github.com/thefactory/marathon-python/pull/211) ([iandyh](https://github.com/iandyh)) - add embed option for /v2/queue [\#210](https://github.com/thefactory/marathon-python/pull/210) ([Rob-Johnson](https://github.com/Rob-Johnson)) - Enable TCP keepalive for sse requests [\#209](https://github.com/thefactory/marathon-python/pull/209) ([fengyehong](https://github.com/fengyehong)) -- Add "udp,tcp" to authorized protocols for containers [\#208](https://github.com/thefactory/marathon-python/pull/208) ([fuegowolf](https://github.com/fuegowolf)) +- Add "udp,tcp" to authorized protocols for containers [\#208](https://github.com/thefactory/marathon-python/pull/208) ([fuegoio](https://github.com/fuegoio)) - Allow event type filter on event stream [\#207](https://github.com/thefactory/marathon-python/pull/207) ([fengyehong](https://github.com/fengyehong)) - Fix MarathonResource hash as well [\#205](https://github.com/thefactory/marathon-python/pull/205) ([jolynch](https://github.com/jolynch)) ## [0.9.0](https://github.com/thefactory/marathon-python/tree/0.9.0) (2017-06-21) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.8.14...0.9.0) **Closed issues:** @@ -57,8 +148,13 @@ - Remove out of date constraint validation of operator. [\#190](https://github.com/thefactory/marathon-python/pull/190) ([akatrevorjay](https://github.com/akatrevorjay)) - Add raw\_data option for event\_stream method [\#189](https://github.com/thefactory/marathon-python/pull/189) ([fengyehong](https://github.com/fengyehong)) - handle case when non-ascii char are logged [\#188](https://github.com/thefactory/marathon-python/pull/188) ([tgermain](https://github.com/tgermain)) +- \[fix\] util.to\_camel\_case doesn't handle digits [\#184](https://github.com/thefactory/marathon-python/pull/184) ([hlerebours](https://github.com/hlerebours)) +- \[fix\] broken build: glibc++ not found [\#183](https://github.com/thefactory/marathon-python/pull/183) ([hlerebours](https://github.com/hlerebours)) +- Support for "disabled" unreachableStrategy. [\#182](https://github.com/thefactory/marathon-python/pull/182) ([nihn](https://github.com/nihn)) +- \[fix\] Handle non-JSON errors from Marathon [\#178](https://github.com/thefactory/marathon-python/pull/178) ([hlerebours](https://github.com/hlerebours)) ## [0.8.14](https://github.com/thefactory/marathon-python/tree/0.8.14) (2017-03-24) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.8.13...0.8.14) **Closed issues:** @@ -68,14 +164,8 @@ - ignoreHttp1xx or ignoreHttp1Xx [\#125](https://github.com/thefactory/marathon-python/issues/125) - ValueError when 401 Unauthorized is received [\#22](https://github.com/thefactory/marathon-python/issues/22) -**Merged pull requests:** - -- \[fix\] util.to\_camel\_case doesn't handle digits [\#184](https://github.com/thefactory/marathon-python/pull/184) ([hlerebours](https://github.com/hlerebours)) -- \[fix\] broken build: glibc++ not found [\#183](https://github.com/thefactory/marathon-python/pull/183) ([hlerebours](https://github.com/hlerebours)) -- Support for "disabled" unreachableStrategy. [\#182](https://github.com/thefactory/marathon-python/pull/182) ([nihn](https://github.com/nihn)) -- \[fix\] Handle non-JSON errors from Marathon [\#178](https://github.com/thefactory/marathon-python/pull/178) ([hlerebours](https://github.com/hlerebours)) - ## [0.8.13](https://github.com/thefactory/marathon-python/tree/0.8.13) (2017-03-17) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.8.12...0.8.13) **Merged pull requests:** @@ -83,6 +173,7 @@ - Support processed\_offers\_summary attribute [\#177](https://github.com/thefactory/marathon-python/pull/177) ([nhandler](https://github.com/nhandler)) ## [0.8.12](https://github.com/thefactory/marathon-python/tree/0.8.12) (2017-03-17) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.8.11...0.8.12) **Closed issues:** @@ -97,6 +188,7 @@ - Updated event.py to handle app\_terminated\_event. [\#171](https://github.com/thefactory/marathon-python/pull/171) ([Jbrownstone](https://github.com/Jbrownstone)) ## [0.8.11](https://github.com/thefactory/marathon-python/tree/0.8.11) (2017-02-22) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.8.10...0.8.11) **Merged pull requests:** @@ -106,6 +198,7 @@ - Adds MarathonApp.add\_env\(\) method [\#166](https://github.com/thefactory/marathon-python/pull/166) ([daltonmatos](https://github.com/daltonmatos)) ## [0.8.10](https://github.com/thefactory/marathon-python/tree/0.8.10) (2017-01-07) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.8.9...0.8.10) **Closed issues:** @@ -119,6 +212,7 @@ - Add new Marathon 1.4 API keywords [\#162](https://github.com/thefactory/marathon-python/pull/162) ([stj](https://github.com/stj)) ## [0.8.9](https://github.com/thefactory/marathon-python/tree/0.8.9) (2016-12-15) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.8.8...0.8.9) **Closed issues:** @@ -130,6 +224,7 @@ - Added more unimplemented Marathon 1.4 API keywords [\#161](https://github.com/thefactory/marathon-python/pull/161) ([solarkennedy](https://github.com/solarkennedy)) ## [0.8.8](https://github.com/thefactory/marathon-python/tree/0.8.8) (2016-12-09) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.8.7...0.8.8) **Closed issues:** @@ -142,6 +237,7 @@ - Expose error details from response object MarathonHttpError [\#157](https://github.com/thefactory/marathon-python/pull/157) ([moonkev](https://github.com/moonkev)) ## [0.8.7](https://github.com/thefactory/marathon-python/tree/0.8.7) (2016-10-24) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.8.6...0.8.7) **Closed issues:** @@ -159,6 +255,7 @@ - Add external volume support [\#146](https://github.com/thefactory/marathon-python/pull/146) ([drewrobb](https://github.com/drewrobb)) ## [0.8.6](https://github.com/thefactory/marathon-python/tree/0.8.6) (2016-08-29) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.8.5...0.8.6) **Closed issues:** @@ -176,6 +273,7 @@ - Add support for unhealthy\_task\_kill\_event [\#137](https://github.com/thefactory/marathon-python/pull/137) ([nuclon](https://github.com/nuclon)) ## [0.8.5](https://github.com/thefactory/marathon-python/tree/0.8.5) (2016-08-10) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.8.4...0.8.5) **Closed issues:** @@ -190,6 +288,7 @@ - Marathon 1.1.2 and Mesos 1.0.\* [\#134](https://github.com/thefactory/marathon-python/pull/134) ([nhandler](https://github.com/nhandler)) ## [0.8.4](https://github.com/thefactory/marathon-python/tree/0.8.4) (2016-07-20) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.8.3...0.8.4) **Closed issues:** @@ -202,6 +301,7 @@ - Expose id query param in list\_apps [\#129](https://github.com/thefactory/marathon-python/pull/129) ([moonkev](https://github.com/moonkev)) ## [0.8.3](https://github.com/thefactory/marathon-python/tree/0.8.3) (2016-07-19) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.8.2...0.8.3) **Closed issues:** @@ -231,6 +331,7 @@ - Add message field for MarathonStatusUpdateEvent. [\#109](https://github.com/thefactory/marathon-python/pull/109) ([oilbeater](https://github.com/oilbeater)) ## [0.8.2](https://github.com/thefactory/marathon-python/tree/0.8.2) (2016-06-14) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.8.1...0.8.2) **Closed issues:** @@ -250,6 +351,7 @@ - add name attribute to port mapping [\#101](https://github.com/thefactory/marathon-python/pull/101) ([Rob-Johnson](https://github.com/Rob-Johnson)) ## [0.8.1](https://github.com/thefactory/marathon-python/tree/0.8.1) (2016-04-21) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.8.0...0.8.1) **Closed issues:** @@ -264,6 +366,7 @@ - Support the deployments endpoint correctly in marathon 1.1.1 [\#95](https://github.com/thefactory/marathon-python/pull/95) ([solarkennedy](https://github.com/solarkennedy)) ## [0.8.0](https://github.com/thefactory/marathon-python/tree/0.8.0) (2016-04-18) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.7.7...0.8.0) **Closed issues:** @@ -278,6 +381,7 @@ - update for v2/queue and v2/apps?embed=apps.taskStats [\#89](https://github.com/thefactory/marathon-python/pull/89) ([bergerx](https://github.com/bergerx)) ## [0.7.7](https://github.com/thefactory/marathon-python/tree/0.7.7) (2016-02-29) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.7.6...0.7.7) **Merged pull requests:** @@ -286,6 +390,7 @@ - a small fix for fetching apps for marathon v0.15 [\#87](https://github.com/thefactory/marathon-python/pull/87) ([burakbostancioglu](https://github.com/burakbostancioglu)) ## [0.7.6](https://github.com/thefactory/marathon-python/tree/0.7.6) (2016-02-12) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.7.5...0.7.6) **Closed issues:** @@ -305,23 +410,26 @@ - Use the /v2/tasks/delete endpoint for taskkill [\#67](https://github.com/thefactory/marathon-python/pull/67) ([fengyehong](https://github.com/fengyehong)) ## [0.7.5](https://github.com/thefactory/marathon-python/tree/0.7.5) (2015-12-09) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.7.4...0.7.5) **Merged pull requests:** - Release 0.7.5 for official Marathon 11 support [\#73](https://github.com/thefactory/marathon-python/pull/73) ([solarkennedy](https://github.com/solarkennedy)) - Added tests for killing tasks on an app [\#72](https://github.com/thefactory/marathon-python/pull/72) ([solarkennedy](https://github.com/solarkennedy)) +- Use automatic changelog generation [\#69](https://github.com/thefactory/marathon-python/pull/69) ([solarkennedy](https://github.com/solarkennedy)) - Provide proper compatability support for str/unicode in py3 [\#57](https://github.com/thefactory/marathon-python/pull/57) ([mattrobenolt](https://github.com/mattrobenolt)) ## [0.7.4](https://github.com/thefactory/marathon-python/tree/0.7.4) (2015-11-20) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.7.3...0.7.4) **Merged pull requests:** -- Use automatic changelog generation [\#69](https://github.com/thefactory/marathon-python/pull/69) ([solarkennedy](https://github.com/solarkennedy)) - Marathon 11 Support [\#68](https://github.com/thefactory/marathon-python/pull/68) ([solarkennedy](https://github.com/solarkennedy)) ## [0.7.3](https://github.com/thefactory/marathon-python/tree/0.7.3) (2015-11-12) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.7.2...0.7.3) **Closed issues:** @@ -335,6 +443,7 @@ - Remove call to logging.basicConfig [\#64](https://github.com/thefactory/marathon-python/pull/64) ([itamaro](https://github.com/itamaro)) ## [0.7.2](https://github.com/thefactory/marathon-python/tree/0.7.2) (2015-09-18) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.7.0...0.7.2) **Closed issues:** @@ -346,7 +455,7 @@ **Merged pull requests:** -- Updated to support Marathon 0.9.1 with get\_info\(\) calls [\#59](https://github.com/thefactory/marathon-python/pull/59) ([pyronicide](https://github.com/pyronicide)) +- Updated to support Marathon 0.9.1 with get\_info\(\) calls [\#59](https://github.com/thefactory/marathon-python/pull/59) ([grampelberg](https://github.com/grampelberg)) - Add support for building with a wheel and cleanup setup.py [\#58](https://github.com/thefactory/marathon-python/pull/58) ([mattrobenolt](https://github.com/mattrobenolt)) - travis should run unit tests [\#55](https://github.com/thefactory/marathon-python/pull/55) ([Rob-Johnson](https://github.com/Rob-Johnson)) - implement \_\_eq\_\_ on base models + fix tests to be useful [\#54](https://github.com/thefactory/marathon-python/pull/54) ([Rob-Johnson](https://github.com/Rob-Johnson)) @@ -358,6 +467,7 @@ - First pass at adding an itest framework [\#42](https://github.com/thefactory/marathon-python/pull/42) ([solarkennedy](https://github.com/solarkennedy)) ## [0.7.0](https://github.com/thefactory/marathon-python/tree/0.7.0) (2015-07-06) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.6.15...0.7.0) **Closed issues:** @@ -373,6 +483,7 @@ - Feature/event factory [\#32](https://github.com/thefactory/marathon-python/pull/32) ([kevinschoon](https://github.com/kevinschoon)) ## [0.6.15](https://github.com/thefactory/marathon-python/tree/0.6.15) (2015-06-05) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.6.14...0.6.15) **Merged pull requests:** @@ -380,6 +491,7 @@ - Make `force\_pull\_image` actually work [\#33](https://github.com/thefactory/marathon-python/pull/33) ([mattrobenolt](https://github.com/mattrobenolt)) ## [0.6.14](https://github.com/thefactory/marathon-python/tree/0.6.14) (2015-05-28) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.6.13...0.6.14) **Closed issues:** @@ -392,10 +504,11 @@ **Merged pull requests:** - Added forcePullImage parameter for the container model [\#31](https://github.com/thefactory/marathon-python/pull/31) ([solarkennedy](https://github.com/solarkennedy)) -- Quick fix \#29 - add kwargs to MarathonDockerContainer.\_\_init\_\_ [\#30](https://github.com/thefactory/marathon-python/pull/30) ([g----](https://github.com/g----)) +- Quick fix \#29 - add kwargs to MarathonDockerContainer.\_\_init\_\_ [\#30](https://github.com/thefactory/marathon-python/pull/30) ([ghost](https://github.com/ghost)) - Fixed \#26:Using try/except to get rid of use\_2to3 failing [\#27](https://github.com/thefactory/marathon-python/pull/27) ([vitan](https://github.com/vitan)) ## [0.6.13](https://github.com/thefactory/marathon-python/tree/0.6.13) (2015-03-24) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.6.12...0.6.13) **Merged pull requests:** @@ -406,9 +519,11 @@ - Possibility to send the full object to Marathon on update [\#20](https://github.com/thefactory/marathon-python/pull/20) ([wndhydrnt](https://github.com/wndhydrnt)) ## [0.6.12](https://github.com/thefactory/marathon-python/tree/0.6.12) (2015-03-07) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.6.11...0.6.12) ## [0.6.11](https://github.com/thefactory/marathon-python/tree/0.6.11) (2015-03-06) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.6.10...0.6.11) **Merged pull requests:** @@ -416,6 +531,7 @@ - Small changes to fix compatibility issues with Marathon 0.8.0 [\#19](https://github.com/thefactory/marathon-python/pull/19) ([cloudify](https://github.com/cloudify)) ## [0.6.10](https://github.com/thefactory/marathon-python/tree/0.6.10) (2014-12-17) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.6.8...0.6.10) **Merged pull requests:** @@ -425,9 +541,11 @@ - apparently undocumented API in Marathon [\#16](https://github.com/thefactory/marathon-python/pull/16) ([elyast](https://github.com/elyast)) ## [0.6.8](https://github.com/thefactory/marathon-python/tree/0.6.8) (2014-11-19) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.6.7...0.6.8) ## [0.6.7](https://github.com/thefactory/marathon-python/tree/0.6.7) (2014-11-18) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.6.6...0.6.7) **Closed issues:** @@ -439,6 +557,7 @@ - fixing issues with resources /v2/tasks, v2/info [\#15](https://github.com/thefactory/marathon-python/pull/15) ([elyast](https://github.com/elyast)) ## [0.6.6](https://github.com/thefactory/marathon-python/tree/0.6.6) (2014-11-17) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.6.5...0.6.6) **Closed issues:** @@ -446,9 +565,11 @@ - scale\_app\(...\) calls update\_app\(...\) with only 1 argument [\#13](https://github.com/thefactory/marathon-python/issues/13) ## [0.6.5](https://github.com/thefactory/marathon-python/tree/0.6.5) (2014-11-14) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.6.4...0.6.5) ## [0.6.4](https://github.com/thefactory/marathon-python/tree/0.6.4) (2014-11-14) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.6.3...0.6.4) **Merged pull requests:** @@ -456,6 +577,7 @@ - Add MarathonHealthCheckResult Class to tasks File and Include it in MarathonTask [\#12](https://github.com/thefactory/marathon-python/pull/12) ([JTCunning](https://github.com/JTCunning)) ## [0.6.3](https://github.com/thefactory/marathon-python/tree/0.6.3) (2014-10-10) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.6.2...0.6.3) **Merged pull requests:** @@ -463,6 +585,7 @@ - add service\_port argument [\#11](https://github.com/thefactory/marathon-python/pull/11) ([danielfrg](https://github.com/danielfrg)) ## [0.6.2](https://github.com/thefactory/marathon-python/tree/0.6.2) (2014-10-09) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.6.1...0.6.2) **Merged pull requests:** @@ -470,9 +593,11 @@ - Add `LIKE` and `UNLIKE` constraint [\#10](https://github.com/thefactory/marathon-python/pull/10) ([iven](https://github.com/iven)) ## [0.6.1](https://github.com/thefactory/marathon-python/tree/0.6.1) (2014-09-29) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.6.0...0.6.1) ## [0.6.0](https://github.com/thefactory/marathon-python/tree/0.6.0) (2014-09-29) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.5.1...0.6.0) **Closed issues:** @@ -480,9 +605,11 @@ - Support for HA nodes [\#8](https://github.com/thefactory/marathon-python/issues/8) ## [0.5.1](https://github.com/thefactory/marathon-python/tree/0.5.1) (2014-09-18) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.5.0...0.5.1) ## [0.5.0](https://github.com/thefactory/marathon-python/tree/0.5.0) (2014-09-18) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.4.0...0.5.0) **Merged pull requests:** @@ -490,6 +617,7 @@ - Bug Fix: Cannot define constraints with a tuple of strings [\#6](https://github.com/thefactory/marathon-python/pull/6) ([adgaudio](https://github.com/adgaudio)) ## [0.4.0](https://github.com/thefactory/marathon-python/tree/0.4.0) (2014-08-19) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.3.1...0.4.0) **Merged pull requests:** @@ -498,6 +626,7 @@ - Fix container options not being sent to marathon [\#4](https://github.com/thefactory/marathon-python/pull/4) ([boffbowsh](https://github.com/boffbowsh)) ## [0.3.1](https://github.com/thefactory/marathon-python/tree/0.3.1) (2014-08-05) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.2.9...0.3.1) **Merged pull requests:** @@ -505,12 +634,15 @@ - Raise exceptions instead of swallowing them silently [\#3](https://github.com/thefactory/marathon-python/pull/3) ([StephanErb](https://github.com/StephanErb)) ## [0.2.9](https://github.com/thefactory/marathon-python/tree/0.2.9) (2014-08-04) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.2.7...0.2.9) ## [0.2.7](https://github.com/thefactory/marathon-python/tree/0.2.7) (2014-07-24) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.2.6...0.2.7) ## [0.2.6](https://github.com/thefactory/marathon-python/tree/0.2.6) (2014-07-24) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.2.5...0.2.6) **Merged pull requests:** @@ -518,6 +650,7 @@ - Updated README.md with correction to create\_app args [\#2](https://github.com/thefactory/marathon-python/pull/2) ([rasathus](https://github.com/rasathus)) ## [0.2.5](https://github.com/thefactory/marathon-python/tree/0.2.5) (2014-07-02) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.2.3...0.2.5) **Merged pull requests:** @@ -525,15 +658,21 @@ - allowing stagedAt and startedAt keys to be null [\#1](https://github.com/thefactory/marathon-python/pull/1) ([Codeacious](https://github.com/Codeacious)) ## [0.2.3](https://github.com/thefactory/marathon-python/tree/0.2.3) (2014-06-02) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.2.0...0.2.3) ## [0.2.0](https://github.com/thefactory/marathon-python/tree/0.2.0) (2014-04-28) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.1.1...0.2.0) ## [0.1.1](https://github.com/thefactory/marathon-python/tree/0.1.1) (2014-04-23) + [Full Changelog](https://github.com/thefactory/marathon-python/compare/0.1.0...0.1.1) ## [0.1.0](https://github.com/thefactory/marathon-python/tree/0.1.0) (2014-04-23) +[Full Changelog](https://github.com/thefactory/marathon-python/compare/8060b138250686d1fe2f79d4d5118fef39aa553e...0.1.0) + + -\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* \ No newline at end of file +\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/Makefile b/Makefile index 6c3bbe8..1d94d86 100644 --- a/Makefile +++ b/Makefile @@ -1,22 +1,30 @@ -itests: - tox -e itest-py27 - tox -e itest-py33 +itests: itests-py36 itests-py37 -test: +itests-py36: + tox -e itest-py36 + +itests-py37: + tox -e itest-py37 + +test: test-py36 test-py37 + +test-py36: tox -e pep8 - tox -e test-py27 - tox -e test-py33 + tox -e test-py36 + +test-py37: + tox -e test-py37 clean: rm -rf dist/ build/ package: clean + github_changelog_generator --user=thefactory --project=marathon-python --future-release=0.13.0 pip install wheel python setup.py sdist bdist_wheel publish: package pip install twine twine upload dist/* - github_changelog_generator .PHONY: itests test clean package publish diff --git a/README.md b/README.md index 0822d97..d4e38cf 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,10 @@ This is a Python library for interfacing with [Marathon](https://github.com/meso #### Compatibility +* For Marathon 1.9.x and 1.10.x, use at least 0.13.0 +* For Marathon 1.6.x, use at least 0.10.0 * For Marathon 1.4.1, use at least 0.8.13 -* For Marathon 1.1.1 and 0.15.x, use at least 0.8.1 -* For Marathon 0.14.x, use at least 0.7.6 -* For Marathon 0.8.x-0.11.x, use at least marathon-python 0.7.5 -* For Marathon 0.8.x-0.9.x, use as least marathon-python 0.6.11 - 0.7.4 -* For Marathon 0.7.x, use at least marathon-python 0.6.10 +* For Marathon 1.1.1, use at least 0.8.1 * For all version changes, please see `CHANGELOG.md` If you find a feature that is broken, please submit a PR that adds a test for @@ -54,7 +52,7 @@ make itests ### Running The Tests Against a Specific Version of Marathon ```bash -MARATHONVERSION=0.9.0 make itests +MARATHONVERSION=v1.6.322 make itests ``` ## Documentation diff --git a/docs/conf.py b/docs/conf.py index e3a0e64..b5611ef 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # marathon-python documentation build configuration file, created by # sphinx-quickstart on Tue Apr 22 11:36:23 2014. @@ -46,8 +45,8 @@ master_doc = 'index' # General information about the project. -project = u'marathon-python' -copyright = u'2014, The Factory' +project = 'marathon-python' +copyright = '2014, The Factory' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -203,8 +202,8 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'marathon-python.tex', u'marathon-python Documentation', - u'Mike Babineau', 'manual'), + ('index', 'marathon-python.tex', 'marathon-python Documentation', + 'Mike Babineau', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -233,8 +232,8 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'marathon-python', u'marathon-python Documentation', - [u'Mike Babineau'], 1) + ('index', 'marathon-python', 'marathon-python Documentation', + ['Mike Babineau'], 1) ] # If true, show URL addresses after external links. @@ -247,8 +246,8 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'marathon-python', u'marathon-python Documentation', - u'Mike Babineau', 'marathon-python', 'One line description of project.', + ('index', 'marathon-python', 'marathon-python Documentation', + 'Mike Babineau', 'marathon-python', 'One line description of project.', 'Miscellaneous'), ] @@ -268,10 +267,10 @@ # -- Options for Epub output ---------------------------------------------- # Bibliographic Dublin Core info. -epub_title = u'marathon-python' -epub_author = u'Mike Babineau' -epub_publisher = u'Mike Babineau' -epub_copyright = u'2014, The Factory' +epub_title = 'marathon-python' +epub_author = 'Mike Babineau' +epub_publisher = 'Mike Babineau' +epub_copyright = '2014, The Factory' # The basename for the epub file. It defaults to the project name. #epub_basename = u'marathon-python' diff --git a/itests/Dockerfile b/itests/Dockerfile deleted file mode 100644 index c43b783..0000000 --- a/itests/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM ubuntu:14.04 - -RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y install \ - software-properties-common -RUN add-apt-repository ppa:webupd8team/java -RUN echo "debconf shared/accepted-oracle-license-v1-1 select true" | debconf-set-selections -RUN echo "debconf shared/accepted-oracle-license-v1-1 seen true" | debconf-set-selections -RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y -q install \ - lsb-release \ - oracle-java8-installer - -# Setup -ADD ./marathon-version /root/marathon-version -ADD ./install-marathon.sh /root/install-marathon.sh -RUN /root/install-marathon.sh - -EXPOSE 8080 -ADD ./start-marathon.sh /root/start-marathon.sh -CMD /etc/init.d/zookeeper start && /root/start-marathon.sh diff --git a/itests/docker-compose.yml b/itests/docker-compose.yml deleted file mode 100644 index 13a0062..0000000 --- a/itests/docker-compose.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -marathon: - build: . - ports: - - 8080 diff --git a/itests/docker/.dockerignore b/itests/docker/.dockerignore new file mode 100644 index 0000000..2947fe4 --- /dev/null +++ b/itests/docker/.dockerignore @@ -0,0 +1,66 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +# Sphinx documentation +docs/_build/ + +.DS_Store + +# IntelliJ +.idea +*.iml + +# Packer http://packer.io +packer_cache + +/gh-pages +itests/marathon-version +.pytest_cache/ diff --git a/itests/docker/Dockerfile b/itests/docker/Dockerfile new file mode 100644 index 0000000..511fbbf --- /dev/null +++ b/itests/docker/Dockerfile @@ -0,0 +1,14 @@ +ARG MARATHONVERSION=v1.6.322 +FROM mesosphere/marathon:$MARATHONVERSION +ARG MARATHONVERSION +USER root + +# Setup +ADD ./install-marathon.sh /root/install-marathon.sh +RUN echo "MARATHONVERSION=${MARATHONVERSION}" > /root/marathon-version \ + && /root/install-marathon.sh + +EXPOSE 8080 5050 +ADD ./start-marathon.sh /root/start-marathon.sh +ENTRYPOINT [] +CMD ["/root/start-marathon.sh"] diff --git a/itests/docker/README.md b/itests/docker/README.md new file mode 100644 index 0000000..807726e --- /dev/null +++ b/itests/docker/README.md @@ -0,0 +1,9 @@ +# mini-marathon + +**Note:** We currently only support the marathon versions listed in [.travis.yml](https://github.com/thefactory/marathon-python/blob/acffecd307c38c3512b77487e2e83806963c7a8d/.travis.yml#L2-L7) + +## How to build + +``` +docker build --build-arg "MARATHONVERSION=v1.6.322" . +``` diff --git a/itests/docker/install-marathon.sh b/itests/docker/install-marathon.sh new file mode 100755 index 0000000..5a4ff52 --- /dev/null +++ b/itests/docker/install-marathon.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +# Default version of marathon to test against if not set by the user +[[ -f /root/marathon-version ]] && source /root/marathon-version +MARATHONVERSION="${MARATHONVERSION:-v1.6.322}" + +export DEBIAN_FRONTEND=noninteractive + +shopt -s extglob + +case "${MARATHONVERSION}" in + @(v1.10.19|v1.9.109)) + echo "Marathon version ${MARATHONVERSION} needs no specific changes" + apt update + ;; + v1.6.322) + sed -i 's!deb http://ftp.debian.org/debian jessie-backports main!!g' /etc/apt/sources.list + apt update + apt install -y mesos=1.6.* + ;; + v1.4.11) + sed -i 's!deb http://ftp.debian.org/debian jessie-backports main!!g' /etc/apt/sources.list + apt update + ;; + @(v1.3.0|v1.1.2)) + rm /etc/apt/sources.list.d/jessie-backports.list + apt update + ;; + *) + echo "Marathon version ${MARATHONVERSION} is not supported" + exit 1 + ;; +esac + +apt install -y --force-yes zookeeperd curl lsof +rm -rf /var/log/apt/* /var/log/alternatives.log /var/log/bootstrap.log /var/log/dpkg.log diff --git a/itests/docker/start-marathon.sh b/itests/docker/start-marathon.sh new file mode 100755 index 0000000..116e15c --- /dev/null +++ b/itests/docker/start-marathon.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +LOGGER="--logging_level info" +# Default version of marathon to test against if not set by the user +[[ -f /root/marathon-version ]] && source /root/marathon-version +MARATHONVERSION="${MARATHONVERSION:-v1.6.322}" + +shopt -s extglob + +case "${MARATHONVERSION}" in + @(v1.4.11|v1.3.0|v1.1.2)) + ln -sf /marathon/bin/start /marathon/bin/marathon + ;; + *) + echo "Marathon version ${MARATHONVERSION} needs no specific changes" + ;; +esac + +java -version +export MESOS_WORK_DIR='/tmp/mesos' +export ZK_HOST=$(cat /etc/mesos/zk) + +mkdir -p "${MESOS_WORK_DIR}" +/etc/init.d/zookeeper start +nohup mesos-master --work_dir=/tmp/mesosmaster --zk=${ZK_HOST} --quorum=1 &> mesos-master.log & +nohup /usr/bin/env MESOS_SYSTEMD_ENABLE_SUPPORT=false mesos-slave --master=${ZK_HOST} --work_dir=/tmp/mesosagent --launcher=posix &> mesos-agent.log & +eval "bin/marathon --master ${ZK_HOST} ${LOGGER}" diff --git a/itests/install-marathon.sh b/itests/install-marathon.sh deleted file mode 100755 index 3ba2a2b..0000000 --- a/itests/install-marathon.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -set -vxeu - -# Default version of marathon to test against if not set by the user -[[ -f /root/marathon-version ]] && source /root/marathon-version -MARATHONVERSION="${MARATHONVERSION:-1.4.0}" - -# Setup -sudo apt-key adv --keyserver keyserver.ubuntu.com --recv 81026D0004C44CF7EF55ADF8DF7D54CBE56151BF -DISTRO=$(lsb_release -is | tr '[:upper:]' '[:lower:]') -CODENAME=$(lsb_release -cs) - -# Add the repository -echo "deb http://repos.mesosphere.com/${DISTRO} ${CODENAME} main" | sudo tee /etc/apt/sources.list.d/mesosphere.list -sudo apt-get update - -# Install packages -sudo DEBIAN_FRONTEND=noninteractive apt-get -y install oracle-java8-installer -sudo apt-get -y purge oracle-java7-installer -sudo update-java-alternatives -s java-8-oracle -sudo DEBIAN_FRONTEND=noninteractive apt-get install oracle-java8-set-default - -sudo DEBIAN_FRONTEND=noninteractive apt-get -y --force-yes install mesos=1.1.* marathon=$MARATHONVERSION* - -# WTF MARATHON? -# Why does the precise version have java7 hardcoded if it requires java8? -sudo mkdir -p /usr/lib/jvm/java-7-oracle/bin/ -sudo ln -s /usr/lib/jvm/java-8-oracle/bin/java /usr/lib/jvm/java-7-oracle/bin/java diff --git a/itests/itest.sh b/itests/itest.sh index bbc8b81..6867ef6 100755 --- a/itests/itest.sh +++ b/itests/itest.sh @@ -2,10 +2,7 @@ set -e -[[ -n $TRAVIS ]] || echo MARATHONVERSION=$MARATHONVERSION > marathon-version -[[ -n $TRAVIS ]] || docker-compose build -[[ -n $TRAVIS ]] || docker-compose pull -[[ -n $TRAVIS ]] || docker-compose up -d +[[ -n $TRAVIS ]] || docker pull "missingcharacter/marathon-python:${MARATHONVERSION}" +[[ -n $TRAVIS ]] || docker run --rm --name marathon-python -d -p 18080:8080 -p 15050:5050 "missingcharacter/marathon-python:${MARATHONVERSION}" behave "$@" -[[ -n $TRAVIS ]] || docker-compose stop -[[ -n $TRAVIS ]] || docker-compose rm --force +[[ -n $TRAVIS ]] || docker kill marathon-python diff --git a/itests/itest_utils.py b/itests/itest_utils.py index 6b4d17c..0ec865e 100644 --- a/itests/itest_utils.py +++ b/itests/itest_utils.py @@ -5,7 +5,6 @@ import time import requests -import compose.cli.command class TimeoutError(Exception): @@ -51,23 +50,9 @@ def wait_for_marathon(): break -def get_compose_service(service_name): - """Returns a compose object for the service""" - project = compose.cli.command.get_project(os.path.dirname(os.path.realpath(__file__))) - return project.get_service(service_name) - - def get_marathon_connection_string(): # only reliable way I can detect travis.. if '/travis/' in os.environ.get('PATH'): return 'localhost:8080' else: - service_port = get_service_internal_port('marathon') - local_port = get_compose_service('marathon').get_container().get_local_port(service_port) - return local_port - - -def get_service_internal_port(service_name): - """Gets the exposed port for service_name from docker-compose.yml. If there are - multiple ports. It returns the first one.""" - return get_compose_service(service_name).options['ports'][0] + return "localhost:18080" diff --git a/itests/start-marathon.sh b/itests/start-marathon.sh deleted file mode 100755 index 6b18743..0000000 --- a/itests/start-marathon.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -if [[ $MARATHONVERSION != '0.8.1' ]]; then - LOGGER="--no-logger" -else - LOGGER="" -fi - -java -version -export MESOS_WORK_DIR='/tmp/mesos' -mkdir -p "$MESOS_WORK_DIR" -exec /usr/bin/marathon --master local $LOGGER --hostname localhost diff --git a/itests/steps/marathon_steps.py b/itests/steps/marathon_steps.py index 434bc04..131c199 100644 --- a/itests/steps/marathon_steps.py +++ b/itests/steps/marathon_steps.py @@ -20,18 +20,18 @@ def working_marathon(context): context.client = marathon.MarathonClient(marathon_connection_string) -@then(u'we get the marathon instance\'s info') +@then('we get the marathon instance\'s info') def get_marathon_info(context): assert context.client.get_info() -@when(u'we create a trivial new app') +@when('we create a trivial new app') def create_trivial_new_app(context): context.client.create_app('test-trivial-app', marathon.MarathonApp( cmd='sleep 3600', mem=16, cpus=0.1, instances=5)) -@then(u'we should be able to kill the tasks') +@then('we should be able to kill the tasks') def kill_a_task(context): time.sleep(5) app = context.client.get_app('test-trivial-app') @@ -40,7 +40,7 @@ def kill_a_task(context): app_id='test-trivial-app', task_id=tasks[0].id, scale=True) -@when(u'we create a complex new app') +@when('we create a complex new app') def create_complex_new_app_with_unicode(context): app_config = { 'container': { @@ -51,13 +51,13 @@ def create_complex_new_app_with_unicode(context): 'name': 'myport', 'containerPort': 8888, 'hostPort': 0}], - 'image': u'localhost/fake_docker_url', + 'image': 'localhost/fake_docker_url', 'network': 'BRIDGE', 'parameters': [{'key': 'add-host', 'value': 'google-public-dns-a.google.com:8.8.8.8'}], }, 'volumes': - [{'hostPath': u'/etc/stuff', - 'containerPath': u'/etc/stuff', + [{'hostPath': '/etc/stuff', + 'containerPath': '/etc/stuff', 'mode': 'RO'}], }, 'instances': 1, @@ -68,7 +68,7 @@ def create_complex_new_app_with_unicode(context): 'uris': ['file:///root/.dockercfg'], 'backoff_seconds': 1, 'constraints': None, - 'cmd': u'/bin/true', + 'cmd': '/bin/true', 'health_checks': [ { 'protocol': 'HTTP', @@ -85,13 +85,13 @@ def create_complex_new_app_with_unicode(context): 'test-complex-app', marathon.MarathonApp(**app_config)) -@then(u'we should see the {which} app running via the marathon api') +@then('we should see the {which} app running via the marathon api') def see_complext_app_running(context, which): print(context.client.list_apps()) assert context.client.get_app('test-%s-app' % which) -@when(u'we wait the {which} app deployment finish') +@when('we wait the {which} app deployment finish') def wait_deployment_finish(context, which): while True: time.sleep(1) @@ -100,7 +100,7 @@ def wait_deployment_finish(context, which): break -@then(u'we should be able to kill the #{to_kill} tasks of the {which} app') +@then('we should be able to kill the #{to_kill} tasks of the {which} app') def kill_tasks(context, to_kill, which): app_tasks = context.client.get_app( 'test-%s-app' % which, embed_tasks=True).tasks @@ -111,11 +111,11 @@ def kill_tasks(context, to_kill, which): context.client.kill_given_tasks(task_to_kill) -@then(u'we should be able to list tasks of the {which} app') +@then('we should be able to list tasks of the {which} app') def list_tasks(context, which): app = context.client.get_app('test-%s-app' % which) tasks = context.client.list_tasks('test-%s-app' % which) - assert len(tasks) == app.instances, "we defined %s tasks, got %s tasks" % (app.instances, len(tasks)) + assert len(tasks) == app.instances, "we defined {} tasks, got {} tasks".format(app.instances, len(tasks)) def listen_for_events(client, events): @@ -123,14 +123,14 @@ def listen_for_events(client, events): events.append(msg) -@when(u'marathon version is greater than {version}') +@when('marathon version is greater than {version}') def marathon_version_chech(context, version): info = context.client.get_info() if LooseVersion(info.version) < LooseVersion(version): context.scenario.skip(reason='Marathon version is too low for this scenario') -@when(u'we start listening for events') +@when('we start listening for events') def start_listening_stream(context): manager = multiprocessing.Manager() mlist = manager.list() @@ -141,7 +141,7 @@ def start_listening_stream(context): context.p = p -@then(u'we should see list of events') +@then('we should see list of events') def stop_listening_stream(context): time.sleep(10) context.p.terminate() diff --git a/marathon/__init__.py b/marathon/__init__.py index 8d2975c..fdb6e47 100644 --- a/marathon/__init__.py +++ b/marathon/__init__.py @@ -1,7 +1,6 @@ -import logging - from .client import MarathonClient from .models import MarathonResource, MarathonApp, MarathonTask, MarathonConstraint from .exceptions import MarathonError, MarathonHttpError, NotFoundError, InvalidChoiceError +from .util import get_log -log = logging.getLogger(__name__) +log = get_log() diff --git a/marathon/_compat.py b/marathon/_compat.py deleted file mode 100644 index b2c19ff..0000000 --- a/marathon/_compat.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -Support for python 2 & 3, ripped pieces from six.py -""" -import sys - -PY3 = sys.version_info[0] == 3 - -if PY3: - string_types = str, -else: - string_types = basestring, diff --git a/marathon/client.py b/marathon/client.py index 2616711..26c7914 100644 --- a/marathon/client.py +++ b/marathon/client.py @@ -12,12 +12,13 @@ import marathon from .models import MarathonApp, MarathonDeployment, MarathonGroup, MarathonInfo, MarathonTask, MarathonEndpoint, MarathonQueueItem -from .exceptions import InternalServerError, NotFoundError, MarathonHttpError, MarathonError +from .exceptions import ConflictError, InternalServerError, NotFoundError, MarathonHttpError, MarathonError, NoResponseError +from .models.base import assert_valid_path from .models.events import EventFactory, MarathonEvent from .util import MarathonJsonEncoder, MarathonMinimalJsonEncoder -class MarathonClient(object): +class MarathonClient: """Client interface for the Marathon REST API.""" @@ -79,7 +80,7 @@ def _do_request(self, method, path, params=None, data=None): 'Content-Type': 'application/json', 'Accept': 'application/json'} if self.auth_token: - headers['Authorization'] = "token={}".format(self.auth_token) + headers['Authorization'] = f"token={self.auth_token}" response = None servers = list(self.servers) @@ -96,7 +97,7 @@ def _do_request(self, method, path, params=None, data=None): 'Error while calling %s: %s', url, str(e)) if response is None: - raise MarathonError('No remaining Marathon servers to try') + raise NoResponseError('No remaining Marathon servers to try') if response.status_code >= 500: marathon.log.error('Got HTTP {code}: {body}'.format( @@ -107,6 +108,8 @@ def _do_request(self, method, path, params=None, data=None): code=response.status_code, body=response.text.encode('utf-8'))) if response.status_code == 404: raise NotFoundError(response) + elif response.status_code == 409: + raise ConflictError(response) else: raise MarathonHttpError(response) elif response.status_code >= 300: @@ -120,21 +123,29 @@ def _do_request(self, method, path, params=None, data=None): def _do_sse_request(self, path, params=None): """Query Marathon server for events.""" - for server in list(self.servers): - url = ''.join([server.rstrip('/'), path]) + urls = [''.join([server.rstrip('/'), path]) for server in self.servers] + while urls: + url = urls.pop() try: + # Requests does not set the original Authorization header on cross origin + # redirects. If set allow_redirects=True we may get a 401 response. response = self.sse_session.get( url, params=params, stream=True, headers={'Accept': 'text/event-stream'}, - auth=self.auth + auth=self.auth, + verify=self.verify, + allow_redirects=False ) except Exception as e: marathon.log.error( 'Error while calling %s: %s', url, e.message) else: - if response.ok: + if response.is_redirect and response.next: + urls.append(response.next.url) + marathon.log.debug(f"Got redirect to {response.next.url}") + elif response.ok: return response.iter_lines() raise MarathonError('No remaining Marathon servers to try') @@ -147,17 +158,19 @@ def list_endpoints(self): """ return MarathonEndpoint.from_tasks(self.list_tasks()) - def create_app(self, app_id, app): + def create_app(self, app_id, app, minimal=True): """Create and start an app. :param str app_id: application ID :param :class:`marathon.models.app.MarathonApp` app: the application to create + :param bool minimal: ignore nulls and empty collections :returns: the created app (on success) :rtype: :class:`marathon.models.app.MarathonApp` or False """ - app.id = app_id - data = app.to_json() + app.id = assert_valid_path(app_id) + data = app.to_json(minimal=minimal) + marathon.log.debug('create app JSON sent: {}'.format(data)) response = self._do_request('POST', '/v2/apps', data=data) if response.status_code == 201: return self._parse_response(response, MarathonApp) @@ -246,7 +259,7 @@ def get_app(self, app_id, embed_tasks=False, embed_counts=False, params['embed'] = filtered_embed_params response = self._do_request( - 'GET', '/v2/apps/{app_id}'.format(app_id=app_id), params=params) + 'GET', f'/v2/apps/{app_id}', params=params) return self._parse_response(response, MarathonApp, resource_name='app') def restart_app(self, app_id, force=False): @@ -259,7 +272,7 @@ def restart_app(self, app_id, force=False): """ params = {'force': force} response = self._do_request( - 'POST', '/v2/apps/{app_id}/restart'.format(app_id=app_id), params=params) + 'POST', f'/v2/apps/{app_id}/restart', params=params) return response.json() def update_app(self, app_id, app, force=False, minimal=True): @@ -284,7 +297,7 @@ def update_app(self, app_id, app, force=False, minimal=True): data = app.to_json(minimal=minimal) response = self._do_request( - 'PUT', '/v2/apps/{app_id}'.format(app_id=app_id), params=params, data=data) + 'PUT', f'/v2/apps/{app_id}', params=params, data=data) return response.json() def update_apps(self, apps, force=False, minimal=True): @@ -326,7 +339,7 @@ def rollback_app(self, app_id, version, force=False): params = {'force': force} data = json.dumps({'version': version}) response = self._do_request( - 'PUT', '/v2/apps/{app_id}'.format(app_id=app_id), params=params, data=data) + 'PUT', f'/v2/apps/{app_id}', params=params, data=data) return response.json() def delete_app(self, app_id, force=False): @@ -340,7 +353,7 @@ def delete_app(self, app_id, force=False): """ params = {'force': force} response = self._do_request( - 'DELETE', '/v2/apps/{app_id}'.format(app_id=app_id), params=params) + 'DELETE', f'/v2/apps/{app_id}', params=params) return response.json() def scale_app(self, app_id, instances=None, delta=None, force=False): @@ -367,7 +380,7 @@ def scale_app(self, app_id, instances=None, delta=None, force=False): try: app = self.get_app(app_id) except NotFoundError: - marathon.log.error('App "{app}" not found'.format(app=app_id)) + marathon.log.error(f'App "{app_id}" not found') return desired = instances if instances is not None else ( @@ -410,7 +423,7 @@ def get_group(self, group_id): :rtype: :class:`marathon.models.group.MarathonGroup` """ response = self._do_request( - 'GET', '/v2/groups/{group_id}'.format(group_id=group_id)) + 'GET', f'/v2/groups/{group_id}') return self._parse_response(response, MarathonGroup) def update_group(self, group_id, group, force=False, minimal=True): @@ -435,7 +448,7 @@ def update_group(self, group_id, group, force=False, minimal=True): data = group.to_json(minimal=minimal) response = self._do_request( - 'PUT', '/v2/groups/{group_id}'.format(group_id=group_id), data=data, params=params) + 'PUT', f'/v2/groups/{group_id}', data=data, params=params) return response.json() def rollback_group(self, group_id, version, force=False): @@ -467,7 +480,7 @@ def delete_group(self, group_id, force=False): """ params = {'force': force} response = self._do_request( - 'DELETE', '/v2/groups/{group_id}'.format(group_id=group_id), params=params) + 'DELETE', f'/v2/groups/{group_id}', params=params) return response.json() def scale_group(self, group_id, scale_by): @@ -479,9 +492,9 @@ def scale_group(self, group_id, scale_by): :returns: a dict containing the deployment id and version :rtype: dict """ - params = {'scaleBy': scale_by} + data = {'scaleBy': scale_by} response = self._do_request( - 'PUT', '/v2/groups/{group_id}'.format(group_id=group_id), params=params) + 'PUT', f'/v2/groups/{group_id}', data=json.dumps(data)) return response.json() def list_tasks(self, app_id=None, **kwargs): @@ -520,7 +533,7 @@ def kill_given_tasks(self, task_ids, scale=False, force=None): data = json.dumps({"ids": task_ids}) response = self._do_request( 'POST', '/v2/tasks/delete', params=params, data=data) - return response == 200 + return response.status_code == 200 def kill_tasks(self, app_id, scale=False, wipe=False, host=None, batch_size=0, batch_delay=0): @@ -547,7 +560,7 @@ def batch(iterable, size): if host: params['host'] = host response = self._do_request( - 'DELETE', '/v2/apps/{app_id}/tasks'.format(app_id=app_id), params) + 'DELETE', f'/v2/apps/{app_id}/tasks', params) # Marathon is inconsistent about what type of object it returns on the multi # task deletion endpoint, depending on the version of Marathon. See: # https://github.com/mesosphere/marathon/blob/06a6f763a75fb6d652b4f1660685ae234bd15387/src/main/scala/mesosphere/marathon/api/v2/AppTasksResource.scala#L88-L95 @@ -565,12 +578,12 @@ def batch(iterable, size): # Pause until the tasks have been killed to avoid race # conditions - killed_task_ids = set(t.id for t in killed_tasks) + killed_task_ids = {t.id for t in killed_tasks} running_task_ids = killed_task_ids while killed_task_ids.intersection(running_task_ids): time.sleep(1) - running_task_ids = set( - t.id for t in self.get_app(app_id).tasks) + running_task_ids = { + t.id for t in self.get_app(app_id).tasks} if batch_delay == 0: # Pause until the replacement tasks are healthy @@ -615,7 +628,7 @@ def list_versions(self, app_id): :rtype: list[str] """ response = self._do_request( - 'GET', '/v2/apps/{app_id}/versions'.format(app_id=app_id)) + 'GET', f'/v2/apps/{app_id}/versions') return [version for version in response.json()['versions']] def get_version(self, app_id, version): @@ -704,9 +717,14 @@ def delete_deployment(self, deployment_id, force=False): return {} else: response = self._do_request( - 'DELETE', '/v2/deployments/{deployment}'.format(deployment=deployment_id)) + 'DELETE', f'/v2/deployments/{deployment_id}') return response.json() + def reset_delay(self, app_id): + self._do_request( + "DELETE", f'/v2/queue/{app_id}/delay' + ) + def get_info(self): """Get server configuration information. diff --git a/marathon/exceptions.py b/marathon/exceptions.py index 8d2d249..0889cb9 100644 --- a/marathon/exceptions.py +++ b/marathon/exceptions.py @@ -14,7 +14,7 @@ def __init__(self, response): self.error_message = content.get('message', self.error_message) self.error_details = content.get('details') self.status_code = response.status_code - super(MarathonHttpError, self).__init__(self.__str__()) + super().__init__(self.__str__()) def __repr__(self): return 'MarathonHttpError: HTTP %s returned with message, "%s"' % \ @@ -32,11 +32,19 @@ class InternalServerError(MarathonHttpError): pass +class ConflictError(MarathonHttpError): + pass + + class InvalidChoiceError(MarathonError): def __init__(self, param, value, options): - super(InvalidChoiceError, self).__init__( + super().__init__( 'Invalid choice "{value}" for param "{param}". Must be one of {options}'.format( param=param, value=value, options=options ) ) + + +class NoResponseError(MarathonError): + pass diff --git a/marathon/models/app.py b/marathon/models/app.py index 0a8eafc..e290fd9 100644 --- a/marathon/models/app.py +++ b/marathon/models/app.py @@ -1,11 +1,13 @@ -from datetime import datetime - from ..exceptions import InvalidChoiceError from .base import MarathonResource, MarathonObject, assert_valid_path from .constraint import MarathonConstraint from .container import MarathonContainer from .deployment import MarathonDeployment from .task import MarathonTask +from ..util import get_log +from ..util import to_datetime + +log = get_log() class MarathonApp(MarathonResource): @@ -36,6 +38,7 @@ class MarathonApp(MarathonResource): :param health_checks: health checks :type health_checks: list[:class:`marathon.models.MarathonHealthCheck`] or list[dict] :param str id: app id + :param str role: mesos role :param int instances: instances :param last_task_failure: last task failure :type last_task_failure: :class:`marathon.models.app.MarathonTaskFailure` or dict @@ -73,7 +76,7 @@ class MarathonApp(MarathonResource): 'args', 'backoff_factor', 'backoff_seconds', 'cmd', 'constraints', 'container', 'cpus', 'dependencies', 'disk', 'env', 'executor', 'gpus', 'health_checks', 'instances', 'kill_selection', 'labels', 'max_launch_delay_seconds', 'mem', 'ports', 'require_ports', 'store_urls', 'task_rate_limit', 'upgrade_strategy', 'unreachable_strategy', - 'uris', 'user', 'version' + 'uris', 'user', 'version', 'role' ] """List of attributes which may be updated/changed after app creation""" @@ -88,7 +91,7 @@ class MarathonApp(MarathonResource): def __init__(self, accepted_resource_roles=None, args=None, backoff_factor=None, backoff_seconds=None, cmd=None, constraints=None, container=None, cpus=None, dependencies=None, deployments=None, disk=None, env=None, - executor=None, health_checks=None, id=None, instances=None, kill_selection=None, labels=None, + executor=None, health_checks=None, id=None, role=None, instances=None, kill_selection=None, labels=None, last_task_failure=None, max_launch_delay_seconds=None, mem=None, ports=None, require_ports=None, store_urls=None, task_rate_limit=None, tasks=None, tasks_running=None, tasks_staged=None, tasks_healthy=None, task_kill_grace_period_seconds=None, tasks_unhealthy=None, upgrade_strategy=None, @@ -129,6 +132,7 @@ def __init__(self, accepted_resource_roles=None, args=None, backoff_factor=None, for hc in (health_checks or []) ] self.id = assert_valid_path(id) + self.role = role self.instances = instances if kill_selection and kill_selection not in self.KILL_SELECTIONS: raise InvalidChoiceError( @@ -210,7 +214,21 @@ class MarathonHealthCheck(MarathonObject): def __init__(self, command=None, grace_period_seconds=None, interval_seconds=None, max_consecutive_failures=None, path=None, port_index=None, protocol=None, timeout_seconds=None, ignore_http1xx=None, **kwargs): - self.command = command + + if command is None: + self.command = None + elif isinstance(command, str): + self.command = { + "value": command + } + elif type(command) is dict and 'value' in command: + log.warn('Deprecated: Using command as dict instead of string is deprecated') + self.command = { + "value": command['value'] + } + else: + raise ValueError(f'Invalid command format: {command}') + self.grace_period_seconds = grace_period_seconds self.interval_seconds = interval_seconds self.max_consecutive_failures = max_consecutive_failures @@ -239,8 +257,6 @@ class MarathonTaskFailure(MarathonObject): :param str version: app version with which this task was started """ - DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' - def __init__(self, app_id=None, host=None, message=None, task_id=None, instance_id=None, slave_id=None, state=None, timestamp=None, version=None): self.app_id = app_id @@ -250,8 +266,7 @@ def __init__(self, app_id=None, host=None, message=None, task_id=None, instance_ self.instance_id = instance_id self.slave_id = slave_id self.state = state - self.timestamp = timestamp if (timestamp is None or isinstance(timestamp, datetime)) \ - else datetime.strptime(timestamp, self.DATETIME_FORMAT) + self.timestamp = to_datetime(timestamp) self.version = version @@ -303,7 +318,7 @@ def __init__(self, unreachable_inactive_after_seconds=None, def from_json(cls, attributes): if attributes == cls.DISABLED: return cls.DISABLED - return super(MarathonUnreachableStrategy, cls).from_json(attributes) + return super().from_json(attributes) class MarathonAppVersionInfo(MarathonObject): @@ -317,17 +332,9 @@ class MarathonAppVersionInfo(MarathonObject): :param str host: mesos slave running the task """ - DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' - def __init__(self, last_scaling_at=None, last_config_change_at=None): - self.last_scaling_at = self._to_datetime(last_scaling_at) - self.last_config_change_at = self._to_datetime(last_config_change_at) - - def _to_datetime(self, timestamp): - if (timestamp is None or isinstance(timestamp, datetime)): - return timestamp - else: - return datetime.strptime(timestamp, self.DATETIME_FORMAT) + self.last_scaling_at = to_datetime(last_scaling_at) + self.last_config_change_at = to_datetime(last_config_change_at) class MarathonTaskStats(MarathonObject): diff --git a/marathon/models/base.py b/marathon/models/base.py index ecc8030..39aba17 100644 --- a/marathon/models/base.py +++ b/marathon/models/base.py @@ -4,7 +4,7 @@ from marathon.util import to_camel_case, to_snake_case, MarathonJsonEncoder, MarathonMinimalJsonEncoder -class MarathonObject(object): +class MarathonObject: """Base Marathon object.""" def __repr__(self): @@ -13,7 +13,7 @@ def __repr__(self): def __eq__(self, other): try: return self.__dict__ == other.__dict__ - except: + except Exception: return False def __hash__(self): @@ -61,14 +61,14 @@ class MarathonResource(MarathonObject): def __repr__(self): if 'id' in list(vars(self).keys()): - return "{clazz}::{id}".format(clazz=self.__class__.__name__, id=self.id) + return f"{self.__class__.__name__}::{self.id}" else: return "{clazz}::{obj}".format(clazz=self.__class__.__name__, obj=self.to_json()) def __eq__(self, other): try: return self.__dict__ == other.__dict__ - except: + except Exception: return False def __hash__(self): @@ -78,7 +78,7 @@ def __hash__(self): return hash(self.to_json()) def __str__(self): - return "{clazz}::".format(clazz=self.__class__.__name__) + str(self.__dict__) + return f"{self.__class__.__name__}::" + str(self.__dict__) # See: diff --git a/marathon/models/constraint.py b/marathon/models/constraint.py index 7b14bf3..cbbde87 100644 --- a/marathon/models/constraint.py +++ b/marathon/models/constraint.py @@ -55,3 +55,19 @@ def from_json(cls, obj): if len(obj) > 2: (field, operator, value) = obj return cls(field, operator, value) + + @classmethod + def from_string(cls, constraint): + """ + :param str constraint: The string representation of a constraint + + :rtype: :class:`MarathonConstraint` + """ + obj = constraint.split(':') + marathon_constraint = cls.from_json(obj) + + if marathon_constraint: + return marathon_constraint + + raise ValueError("Invalid string format. " + "Expected `field:operator:value`") diff --git a/marathon/models/container.py b/marathon/models/container.py index 586bf40..0ec267a 100644 --- a/marathon/models/container.py +++ b/marathon/models/container.py @@ -63,7 +63,7 @@ class MarathonDockerContainer(MarathonObject): NETWORK_MODES = ['BRIDGE', 'HOST', 'USER', 'NONE'] """Valid network modes""" - def __init__(self, image=None, network='HOST', port_mappings=None, parameters=None, privileged=None, + def __init__(self, image=None, network=None, port_mappings=None, parameters=None, privileged=None, force_pull_image=None, **kwargs): self.image = image if network: @@ -97,7 +97,7 @@ class MarathonContainerPortMapping(MarathonObject): PROTOCOLS = ['tcp', 'udp', 'udp,tcp'] """Valid protocols""" - def __init__(self, name=None, container_port=None, host_port=0, service_port=None, protocol='tcp', labels=None): + def __init__(self, name=None, container_port=None, host_port=None, service_port=None, protocol='tcp', labels=None): self.name = name self.container_port = container_port self.host_port = host_port diff --git a/marathon/models/deployment.py b/marathon/models/deployment.py index 97e73ec..7a98c85 100644 --- a/marathon/models/deployment.py +++ b/marathon/models/deployment.py @@ -1,4 +1,4 @@ -from .base import MarathonObject, MarathonResource +from .base import MarathonObject, MarathonResource, assert_valid_path class MarathonDeployment(MarathonResource): @@ -25,7 +25,7 @@ def __init__(self, affected_apps=None, current_actions=None, current_step=None, self.affected_apps = affected_apps self.current_actions = [ a if isinstance( - a, MarathonDeploymentAction) else MarathonDeploymentAction().from_json(a) + a, MarathonDeploymentAction) else MarathonDeploymentAction.from_json(a) for a in (current_actions or []) ] self.current_step = current_step @@ -41,7 +41,7 @@ def parse_deployment_step(self, step): return MarathonDeploymentStep().from_json(step) elif step.__class__ == list: # This is Marathon < 1.0.0 style, a list of actions - return [s if isinstance(s, MarathonDeploymentAction) else MarathonDeploymentAction().from_json(s) for s in step] + return [s if isinstance(s, MarathonDeploymentAction) else MarathonDeploymentAction.from_json(s) for s in step] else: return step @@ -60,8 +60,8 @@ class MarathonDeploymentAction(MarathonObject): def __init__(self, action=None, app=None, apps=None, type=None, readiness_check_results=None, pod=None): self.action = action - self.app = app - self.apps = apps + self.app = assert_valid_path(app) + self.apps = assert_valid_path(apps) self.pod = pod self.type = type # TODO: Remove builtin shadow self.readiness_check_results = readiness_check_results # TODO: The docs say this is called just "readinessChecks?" diff --git a/marathon/models/events.py b/marathon/models/events.py index bd64b16..a0b3d4d 100644 --- a/marathon/models/events.py +++ b/marathon/models/events.py @@ -1,10 +1,13 @@ """ This module is used to translate Events from Marathon's EventBus system. -See: https://mesosphere.github.io/marathon/docs/event-bus.html +See: +* https://mesosphere.github.io/marathon/docs/event-bus.html +* https://github.com/mesosphere/marathon/blob/master/src/main/scala/mesosphere/marathon/core/event/Events.scala """ from marathon.models.base import MarathonObject from marathon.models.app import MarathonHealthCheck +from marathon.models.task import MarathonIpAddress from marathon.models.deployment import MarathonDeploymentPlan from marathon.exceptions import MarathonError @@ -19,7 +22,11 @@ class MarathonEvent(MarathonObject): KNOWN_ATTRIBUTES = [] attribute_name_to_marathon_object = { # Allows embedding of MarathonObjects inside events. 'health_check': MarathonHealthCheck, - 'plan': MarathonDeploymentPlan + 'plan': MarathonDeploymentPlan, + 'ip_address': MarathonIpAddress, + } + seq_name_to_singular = { + 'ip_addresses': 'ip_address', } def __init__(self, event_type, timestamp, **kwargs): @@ -28,13 +35,25 @@ def __init__(self, event_type, timestamp, **kwargs): for attribute in self.KNOWN_ATTRIBUTES: self._set(attribute, kwargs.get(attribute)) + def __to_marathon_object(self, attribute_name, attribute): + if attribute_name in self.attribute_name_to_marathon_object: + clazz = self.attribute_name_to_marathon_object[attribute_name] + # If this attribute already has a Marathon object instantiate it. + attribute = clazz.from_json(attribute) + return attribute + def _set(self, attribute_name, attribute): if not attribute: return - if attribute_name in self.attribute_name_to_marathon_object: - clazz = self.attribute_name_to_marathon_object[attribute_name] - attribute = clazz.from_json( - attribute) # If this attribute already has a Marathon object instantiate it. + # Special handling for lists... + if isinstance(attribute, list): + name = self.seq_name_to_singular.get(attribute_name) + attribute = [ + self.__to_marathon_object(name, v) + for v in attribute + ] + else: + attribute = self.__to_marathon_object(attribute_name, attribute) setattr(self, attribute_name, attribute) @@ -44,7 +63,7 @@ class MarathonApiPostEvent(MarathonEvent): class MarathonStatusUpdateEvent(MarathonEvent): KNOWN_ATTRIBUTES = [ - 'slave_id', 'task_id', 'task_status', 'app_id', 'host', 'ports', 'version', 'message'] + 'slave_id', 'task_id', 'task_status', 'app_id', 'host', 'ports', 'version', 'message', 'ip_addresses'] class MarathonFrameworkMessageEvent(MarathonEvent): @@ -68,11 +87,11 @@ class MarathonRemoveHealthCheckEvent(MarathonEvent): class MarathonFailedHealthCheckEvent(MarathonEvent): - KNOWN_ATTRIBUTES = ['app_id', 'health_check', 'task_id'] + KNOWN_ATTRIBUTES = ['app_id', 'health_check', 'task_id', 'instance_id'] class MarathonHealthStatusChangedEvent(MarathonEvent): - KNOWN_ATTRIBUTES = ['app_id', 'health_check', 'task_id', 'alive'] + KNOWN_ATTRIBUTES = ['app_id', 'health_check', 'task_id', 'instance_id', 'alive'] class MarathonGroupChangeSuccess(MarathonEvent): @@ -92,7 +111,7 @@ class MarathonDeploymentFailed(MarathonEvent): class MarathonDeploymentInfo(MarathonEvent): - KNOWN_ATTRIBUTES = ['plan'] + KNOWN_ATTRIBUTES = ['plan', 'current_step'] class MarathonDeploymentStepSuccess(MarathonEvent): @@ -112,7 +131,7 @@ class MarathonEventStreamDetached(MarathonEvent): class MarathonUnhealthyTaskKillEvent(MarathonEvent): - KNOWN_ATTRIBUTES = ['app_id', 'task_id', 'version', 'reason'] + KNOWN_ATTRIBUTES = ['app_id', 'task_id', 'instance_id', 'version', 'reason'] class MarathonAppTerminatedEvent(MarathonEvent): @@ -187,7 +206,7 @@ def __init__(self): 'pod_deleted_event': MarathonPodDeletedEvent, } - class_to_event = dict((v, k) for k, v in event_to_class.items()) + class_to_event = {v: k for k, v in event_to_class.items()} def process(self, event): event_type = event['eventType'] @@ -195,4 +214,4 @@ def process(self, event): clazz = self.event_to_class[event_type] return clazz.from_json(event) else: - raise MarathonError('Unknown event_type: {}, data: {}'.format(event_type, event)) + raise MarathonError(f'Unknown event_type: {event_type}, data: {event}') diff --git a/marathon/models/group.py b/marathon/models/group.py index c7c0339..f68e230 100644 --- a/marathon/models/group.py +++ b/marathon/models/group.py @@ -1,4 +1,4 @@ -from .base import MarathonResource, assert_valid_id +from .base import MarathonResource from .app import MarathonApp @@ -20,7 +20,7 @@ class MarathonGroup(MarathonResource): """ def __init__(self, apps=None, dependencies=None, - groups=None, id=None, pods=None, version=None): + groups=None, id=None, pods=None, version=None, enforce_role=None): self.apps = [ a if isinstance(a, MarathonApp) else MarathonApp().from_json(a) for a in (apps or []) @@ -36,5 +36,6 @@ def __init__(self, apps=None, dependencies=None, # p if isinstance(p, MarathonPod) else MarathonPod().from_json(p) # for p in (pods or []) # ] - self.id = assert_valid_id(id) + self.id = id self.version = version + self.enforce_role = enforce_role diff --git a/marathon/models/info.py b/marathon/models/info.py index 40d4626..68622a3 100644 --- a/marathon/models/info.py +++ b/marathon/models/info.py @@ -64,23 +64,60 @@ class MarathonConfig(MarathonObject): :param int leader_proxy_read_timeout_ms: :param int local_port_min: :param int local_port_max: + :param bool maintenance_mode: :param str master: :param str mesos_leader_ui_url: :param str mesos_role: :param str mesos_user: + :param str new_group_enforce_role: :param str webui_url: :param int reconciliation_initial_delay: :param int reconciliation_interval: :param int task_launch_timeout: :param int task_reservation_timeout: :param int marathon_store_timeout: + :param str access_control_allow_origin: + :param int decline_offer_duration: + :param str default_network_name: + :param str env_vars_prefix: + :param int launch_token: + :param int launch_token_refresh_interval: + :param int max_instances_per_offer: + :param str mesos_bridge_name: + :param int mesos_heartbeat_failure_threshold: + :param int mesos_heartbeat_interval: + :param int min_revive_offers_interval: + :param int offer_matching_timeout: + :param int on_elected_prepare_timeout: + :param bool revive_offers_for_new_apps: + :param int revive_offers_repetitions: + :param int scale_apps_initial_delay: + :param int scale_apps_interval: + :param bool store_cache: + :param int task_launch_confirm_timeout: + :param int task_lost_expunge_initial_delay: + :param int task_lost_expunge_interval: """ def __init__(self, checkpoint=None, executor=None, failover_timeout=None, framework_name=None, ha=None, hostname=None, leader_proxy_connection_timeout_ms=None, leader_proxy_read_timeout_ms=None, - local_port_min=None, local_port_max=None, master=None, mesos_leader_ui_url=None, mesos_role=None, mesos_user=None, - webui_url=None, reconciliation_initial_delay=None, reconciliation_interval=None, - task_launch_timeout=None, marathon_store_timeout=None, task_reservation_timeout=None, features=None): + local_port_min=None, local_port_max=None, maintenance_mode=None, master=None, mesos_leader_ui_url=None, + mesos_role=None, mesos_user=None, new_group_enforce_role=None, webui_url=None, + reconciliation_initial_delay=None, reconciliation_interval=None, task_launch_timeout=None, + marathon_store_timeout=None, task_reservation_timeout=None, features=None, + access_control_allow_origin=None, decline_offer_duration=None, default_network_name=None, + env_vars_prefix=None, launch_token=None, launch_token_refresh_interval=None, + max_instances_per_offer=None, mesos_bridge_name=None, + mesos_heartbeat_failure_threshold=None, + mesos_heartbeat_interval=None, min_revive_offers_interval=None, + offer_matching_timeout=None, on_elected_prepare_timeout=None, + revive_offers_for_new_apps=None, + revive_offers_repetitions=None, scale_apps_initial_delay=None, + scale_apps_interval=None, store_cache=None, + task_launch_confirm_timeout=None, + task_lost_expunge_initial_delay=None, + task_lost_expunge_interval=None + ): self.checkpoint = checkpoint self.executor = executor self.failover_timeout = failover_timeout @@ -89,16 +126,39 @@ def __init__(self, checkpoint=None, executor=None, failover_timeout=None, framew self.hostname = hostname self.local_port_min = local_port_min self.local_port_max = local_port_max + self.maintenance_mode = maintenance_mode self.master = master self.mesos_leader_ui_url = mesos_leader_ui_url self.mesos_role = mesos_role self.mesos_user = mesos_user + self.new_group_enforce_role = new_group_enforce_role self.webui_url = webui_url self.reconciliation_initial_delay = reconciliation_initial_delay self.reconciliation_interval = reconciliation_interval self.task_launch_timeout = task_launch_timeout self.task_reservation_timeout = task_reservation_timeout self.marathon_store_timeout = marathon_store_timeout + self.access_control_allow_origin = access_control_allow_origin + self.decline_offer_duration = decline_offer_duration + self.default_network_name = default_network_name + self.env_vars_prefix = env_vars_prefix + self.launch_token = launch_token + self.launch_token_refresh_interval = launch_token_refresh_interval + self.max_instances_per_offer = max_instances_per_offer + self.mesos_bridge_name = mesos_bridge_name + self.mesos_heartbeat_failure_threshold = mesos_heartbeat_failure_threshold + self.mesos_heartbeat_interval = mesos_heartbeat_interval + self.min_revive_offers_interval = min_revive_offers_interval + self.offer_matching_timeout = offer_matching_timeout + self.on_elected_prepare_timeout = on_elected_prepare_timeout + self.revive_offers_for_new_apps = revive_offers_for_new_apps + self.revive_offers_repetitions = revive_offers_repetitions + self.scale_apps_initial_delay = scale_apps_initial_delay + self.scale_apps_interval = scale_apps_interval + self.store_cache = store_cache + self.task_launch_confirm_timeout = task_launch_confirm_timeout + self.task_lost_expunge_initial_delay = task_lost_expunge_initial_delay + self.task_lost_expunge_interval = task_lost_expunge_interval class MarathonZooKeeperConfig(MarathonObject): @@ -116,10 +176,15 @@ class MarathonZooKeeperConfig(MarathonObject): :param str zk_state: :param int zk_timeout: :param int zk_connection_timeout: + :param bool zk_compression: + :param int zk_compression_threshold: + :param int zk_max_node_size: """ def __init__(self, zk=None, zk_future_timeout=None, zk_hosts=None, zk_max_versions=None, zk_path=None, - zk_session_timeout=None, zk_state=None, zk_timeout=None, zk_connection_timeout=None): + zk_session_timeout=None, zk_state=None, zk_timeout=None, zk_connection_timeout=None, + zk_compression=None, zk_compression_threshold=None, + zk_max_node_size=None): self.zk = zk self.zk_hosts = zk_hosts self.zk_path = zk_path @@ -129,6 +194,9 @@ def __init__(self, zk=None, zk_future_timeout=None, zk_hosts=None, zk_max_versio self.zk_connection_timeout = zk_connection_timeout self.zk_future_timeout = zk_future_timeout self.zk_session_timeout = zk_session_timeout + self.zk_compression = zk_compression + self.zk_compression_threshold = zk_compression_threshold + self.zk_max_node_size = zk_max_node_size class MarathonHttpConfig(MarathonObject): diff --git a/marathon/models/queue.py b/marathon/models/queue.py index 33c0228..d53fd70 100644 --- a/marathon/models/queue.py +++ b/marathon/models/queue.py @@ -26,7 +26,7 @@ class MarathonQueueItem(MarathonResource): """ def __init__(self, app=None, overdue=None, count=None, delay=None, since=None, - processed_offers_summary=None): + processed_offers_summary=None, last_unused_offers=None): self.app = app if isinstance( app, MarathonApp) else MarathonApp().from_json(app) self.overdue = overdue @@ -35,6 +35,7 @@ def __init__(self, app=None, overdue=None, count=None, delay=None, since=None, delay, MarathonQueueItemDelay) else MarathonQueueItemDelay().from_json(delay) self.since = since self.processed_offers_summary = processed_offers_summary + self.last_unused_offers = last_unused_offers class MarathonQueueItemDelay(MarathonResource): diff --git a/marathon/models/task.py b/marathon/models/task.py index 6b81f74..0348c6a 100644 --- a/marathon/models/task.py +++ b/marathon/models/task.py @@ -1,6 +1,5 @@ -from datetime import datetime - from .base import MarathonResource, MarathonObject +from ..util import to_datetime class MarathonTask(MarathonResource): @@ -21,12 +20,19 @@ class MarathonTask(MarathonResource): :param started_at: when this task was started :type started_at: datetime or str :param str version: app version with which this task was started + :type region: str + :param region: fault domain region support in DCOS EE + :type zone: str + :param zone: fault domain zone support in DCOS EE + :type role: str + :param role: mesos role """ DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' def __init__(self, app_id=None, health_check_results=None, host=None, id=None, ports=None, service_ports=None, - slave_id=None, staged_at=None, started_at=None, version=None, ip_addresses=[], state=None, local_volumes=None): + slave_id=None, staged_at=None, started_at=None, version=None, ip_addresses=[], state=None, local_volumes=None, + region=None, zone=None, role=None): self.app_id = app_id self.health_check_results = health_check_results or [] self.health_check_results = [ @@ -39,10 +45,8 @@ def __init__(self, app_id=None, health_check_results=None, host=None, id=None, p self.ports = ports or [] self.service_ports = service_ports or [] self.slave_id = slave_id - self.staged_at = staged_at if (staged_at is None or isinstance(staged_at, datetime)) \ - else datetime.strptime(staged_at, self.DATETIME_FORMAT) - self.started_at = started_at if (started_at is None or isinstance(started_at, datetime)) \ - else datetime.strptime(started_at, self.DATETIME_FORMAT) + self.staged_at = to_datetime(staged_at) + self.started_at = to_datetime(started_at) self.state = state self.version = version self.ip_addresses = [ @@ -50,6 +54,9 @@ def __init__(self, app_id=None, health_check_results=None, host=None, id=None, p ip_addresses, MarathonIpAddress) else MarathonIpAddress().from_json(ipaddr) for ipaddr in (ip_addresses or [])] self.local_volumes = local_volumes or [] + self.region = region + self.zone = zone + self.role = role class MarathonIpAddress(MarathonObject): @@ -83,12 +90,9 @@ def __init__(self, alive=None, consecutive_failures=None, first_success=None, last_failure_cause=None, instance_id=None): self.alive = alive self.consecutive_failures = consecutive_failures - self.first_success = first_success if (first_success is None or isinstance(first_success, datetime)) \ - else datetime.strptime(first_success, self.DATETIME_FORMAT) - self.last_failure = last_failure if (last_failure is None or isinstance(last_failure, datetime)) \ - else datetime.strptime(last_failure, self.DATETIME_FORMAT) - self.last_success = last_success if (last_success is None or isinstance(last_success, datetime)) \ - else datetime.strptime(last_success, self.DATETIME_FORMAT) + self.first_success = to_datetime(first_success) + self.last_failure = to_datetime(last_failure) + self.last_success = to_datetime(last_success) self.task_id = task_id self.last_failure_cause = last_failure_cause self.instance_id = instance_id diff --git a/marathon/util.py b/marathon/util.py index e999333..af2932e 100644 --- a/marathon/util.py +++ b/marathon/util.py @@ -1,5 +1,12 @@ -import collections +# collections.abc new as of 3.3, and collections is deprecated. collections +# will be unavailable in 3.9 +try: + import collections.abc as collections +except ImportError: + import collections + import datetime +import logging try: import json @@ -7,11 +14,9 @@ import simplejson as json import re -from ._compat import string_types - -def is_stringy(obj): - return isinstance(obj, string_types) +def get_log(): + return logging.getLogger(__name__.split('.')[0]) class MarathonJsonEncoder(json.JSONEncoder): @@ -25,7 +30,7 @@ def default(self, obj): if isinstance(obj, datetime.datetime): return obj.strftime('%Y-%m-%dT%H:%M:%S.%fZ') - if isinstance(obj, collections.Iterable) and not is_stringy(obj): + if isinstance(obj, collections.Iterable) and not isinstance(obj, str): try: return {k: self.default(v) for k, v in obj.items()} except AttributeError: @@ -45,7 +50,7 @@ def default(self, obj): if isinstance(obj, datetime.datetime): return obj.strftime('%Y-%m-%dT%H:%M:%S.%fZ') - if isinstance(obj, collections.Iterable) and not is_stringy(obj): + if isinstance(obj, collections.Iterable) and not isinstance(obj, str): try: return {k: self.default(v) for k, v in obj.items() if (v or v in (False, 0))} except AttributeError: @@ -62,3 +67,21 @@ def to_camel_case(snake_str): def to_snake_case(camel_str): s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', camel_str) return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + + +DATETIME_FORMATS = [ + '%Y-%m-%dT%H:%M:%S.%fZ', + '%Y-%m-%dT%H:%M:%SZ', # Marathon omits milliseconds when they would be .000 +] + + +def to_datetime(timestamp): + if (timestamp is None or isinstance(timestamp, datetime.datetime)): + return timestamp + else: + for fmt in DATETIME_FORMATS: + try: + return datetime.datetime.strptime(timestamp, fmt).replace(tzinfo=datetime.timezone.utc) + except ValueError: + pass + raise ValueError(f'Unrecognized datetime format: {timestamp}') diff --git a/requirements.txt b/requirements.txt index 4661030..c20f36f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -requests==2.11.1 +requests==2.20.0 diff --git a/setup.py b/setup.py index d111518..fa25a98 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name='marathon', - version='0.9.2', + version='0.13.0', description='Marathon Client Library', long_description="""Python interface to the Mesos Marathon REST API.""", author='Mike Babineau', @@ -24,10 +24,9 @@ 'Intended Audience :: System Administrators', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Programming Language :: Python', 'Topic :: Software Development :: Libraries :: Python Modules' ], diff --git a/tests/test_api.py b/tests/test_api.py index cee709e..3b0ec7d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -34,7 +34,7 @@ def test_get_deployments_pre_1_0(): mock_client = MarathonClient(servers='http://fake_server') actual_deployments = mock_client.list_deployments() expected_deployments = [models.MarathonDeployment( - id=u"fakeid", + id="fakeid", steps=[ [models.MarathonDeploymentAction( action="ScaleApplication", app="/test")]], @@ -42,8 +42,8 @@ def test_get_deployments_pre_1_0(): action="ScaleApplication", app="/test")], current_step=1, total_steps=1, - affected_apps=[u"/test"], - version=u"fakeversion" + affected_apps=["/test"], + version="fakeversion" )] assert expected_deployments == actual_deployments @@ -90,7 +90,7 @@ def test_get_deployments_post_1_0(): mock_client = MarathonClient(servers='http://fake_server') actual_deployments = mock_client.list_deployments() expected_deployments = [models.MarathonDeployment( - id=u"4d2ff4d8-fbe5-4239-a886-f0831ed68d20", + id="4d2ff4d8-fbe5-4239-a886-f0831ed68d20", steps=[ models.MarathonDeploymentStep( actions=[models.MarathonDeploymentAction( @@ -106,8 +106,8 @@ def test_get_deployments_post_1_0(): ], current_step=2, total_steps=2, - affected_apps=[u"/test-trivial-app"], - version=u"2016-04-20T18:00:20.084Z" + affected_apps=["/test-trivial-app"], + version="2016-04-20T18:00:20.084Z" )] # Helpful for tox to see the diff assert expected_deployments[0].__dict__ == actual_deployments[0].__dict__ diff --git a/tests/test_model_app.py b/tests/test_model_app.py index adddb4d..b211efe 100644 --- a/tests/test_model_app.py +++ b/tests/test_model_app.py @@ -1,5 +1,3 @@ -# encoding: utf-8 - from marathon.models.app import MarathonApp import unittest diff --git a/tests/test_model_constraint.py b/tests/test_model_constraint.py new file mode 100644 index 0000000..e8ad1d5 --- /dev/null +++ b/tests/test_model_constraint.py @@ -0,0 +1,54 @@ +from marathon.models.app import MarathonConstraint +import unittest + + +class MarathonConstraintTests(unittest.TestCase): + def test_repr_with_value(self): + constraint = MarathonConstraint('a_field', 'OPERATOR', 'a_value') + representation = repr(constraint) + self.assertEqual(representation, + "MarathonConstraint::a_field:OPERATOR:a_value") + + def test_repr_without_value(self): + constraint = MarathonConstraint('a_field', 'OPERATOR') + representation = repr(constraint) + self.assertEqual(representation, + "MarathonConstraint::a_field:OPERATOR") + + def test_json_repr_with_value(self): + constraint = MarathonConstraint('a_field', 'OPERATOR', 'a_value') + json_repr = constraint.json_repr() + self.assertEqual(json_repr, ['a_field', 'OPERATOR', 'a_value']) + + def test_json_repr_without_value(self): + constraint = MarathonConstraint('a_field', 'OPERATOR') + json_repr = constraint.json_repr() + self.assertEqual(json_repr, ['a_field', 'OPERATOR']) + + def test_from_json_with_value(self): + constraint = MarathonConstraint.from_json(['a_field', 'OPERATOR', 'a_value']) + self.assertEqual(constraint, + MarathonConstraint('a_field', 'OPERATOR', 'a_value')) + + def test_from_json_without_value(self): + constraint = MarathonConstraint.from_json(['a_field', 'OPERATOR']) + self.assertEqual(constraint, MarathonConstraint('a_field', 'OPERATOR')) + + def test_from_string_with_value(self): + constraint = MarathonConstraint.from_string('a_field:OPERATOR:a_value') + self.assertEqual(constraint, + MarathonConstraint('a_field', 'OPERATOR', 'a_value')) + + def test_from_string_without_value(self): + constraint = MarathonConstraint.from_string('a_field:OPERATOR') + self.assertEqual(constraint, MarathonConstraint('a_field', 'OPERATOR')) + + def test_from_string_raises_an_error_for_invalid_format(self): + with self.assertRaises(ValueError): + MarathonConstraint.from_string('a_field:OPERATOR:a_value:') + + with self.assertRaises(ValueError): + MarathonConstraint.from_string('a_field') + + with self.assertRaises(ValueError): + MarathonConstraint.from_string('a_field:OPERATOR:a_value:something') diff --git a/tests/test_model_deployment.py b/tests/test_model_deployment.py new file mode 100644 index 0000000..9e9723a --- /dev/null +++ b/tests/test_model_deployment.py @@ -0,0 +1,33 @@ +from marathon.models.deployment import MarathonDeployment +import unittest + + +class MarathonDeploymentTest(unittest.TestCase): + + def test_env_defaults_to_empty_dict(self): + """ + é testé + """ + deployment_json = { + "id": "ID", + "version": "2020-05-30T07:35:04.695Z", + "affectedApps": ["/app"], + "affectedPods": [], + "steps": [{ + "actions": [{ + "action": "RestartApplication", + "app": "/app" + }] + }], + "currentActions": [{ + "action": "RestartApplication", + "app": "/app", + "readinessCheckResults": [] + }], + "currentStep": 1, + "totalSteps": 1 + } + + deployment = MarathonDeployment.from_json(deployment_json) + self.assertEquals(deployment.id, "ID") + self.assertEquals(deployment.current_actions[0].app, "/app") diff --git a/tests/test_model_event.py b/tests/test_model_event.py index ad1efef..22453e3 100644 --- a/tests/test_model_event.py +++ b/tests/test_model_event.py @@ -1,10 +1,52 @@ -# encoding: utf-8 - -from marathon.models.events import EventFactory +from marathon.models.events import EventFactory, MarathonStatusUpdateEvent +from marathon.models.task import MarathonIpAddress import unittest class MarathonEventTest(unittest.TestCase): def test_event_factory(self): - self.assertEqual(set(EventFactory.event_to_class.keys()), set(EventFactory.class_to_event.values())) + self.assertEqual( + set(EventFactory.event_to_class.keys()), + set(EventFactory.class_to_event.values()), + ) + + def test_marathon_event(self): + """Test that we can process at least one kind of event.""" + payload = { + "eventType": "status_update_event", + "slaveId": "slave-01", + "taskId": "task-01", + "taskStatus": "TASK_RUNNING", + "message": "Some message", + "appId": "/foo/bar", + "host": "host-01", + "ipAddresses": [ + {"ip_address": "127.0.0.1", "protocol": "tcp"}, + {"ip_address": "127.0.0.1", "protocol": "udp"}, + ], + "ports": [0, 1], + "version": "1234", + "timestamp": 12345, + } + factory = EventFactory() + event = factory.process(payload) + + expected_event = MarathonStatusUpdateEvent( + event_type="status_update_event", + timestamp=12345, + slave_id="slave-01", + task_id="task-01", + task_status="TASK_RUNNING", + message="Some message", + app_id="/foo/bar", + host="host-01", + ports=[0, 1], + version="1234", + ) + expected_event.ip_addresses = [ + MarathonIpAddress(ip_address="127.0.0.1", protocol="tcp"), + MarathonIpAddress(ip_address="127.0.0.1", protocol="udp"), + ] + + self.assertEqual(event.to_json(), expected_event.to_json()) diff --git a/tests/test_model_group.py b/tests/test_model_group.py new file mode 100644 index 0000000..45b20a3 --- /dev/null +++ b/tests/test_model_group.py @@ -0,0 +1,30 @@ +from marathon.models.group import MarathonGroup +import unittest + + +class MarathonGroupTest(unittest.TestCase): + + def test_from_json_parses_root_group(self): + data = { + "id": "/", + "groups": [ + {"id": "/foo", "apps": []}, + {"id": "/bla", "apps": []}, + ], + "apps": [] + } + group = MarathonGroup().from_json(data) + self.assertEqual("/", group.id) + + def test_from_json_parses_group_with_enforce_role(self): + data = { + "id": "/mygroup/works", + "groups": [ + {"id": "/foo", "apps": []}, + ], + "apps": [], + "enforceRole": False, + + } + group = MarathonGroup().from_json(data) + self.assertEqual("/mygroup/works", group.id) diff --git a/tests/test_model_object.py b/tests/test_model_object.py index 89795fb..05c6ee4 100644 --- a/tests/test_model_object.py +++ b/tests/test_model_object.py @@ -1,5 +1,3 @@ -# encoding: utf-8 - from marathon.models.base import MarathonObject from marathon.models.base import MarathonResource import unittest diff --git a/tests/test_util.py b/tests/test_util.py index 8956051..2a967ba 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,4 +1,5 @@ -from marathon.util import to_camel_case, to_snake_case +from datetime import datetime, timezone +from marathon.util import to_camel_case, to_snake_case, to_datetime def _apply_on_pairs(f): @@ -27,3 +28,10 @@ def test(camel, snake): assert to_snake_case(camel) == snake _apply_on_pairs(test) + + +def test_version_info_datetime(): + assert to_datetime("2017-09-28T00:31:55Z") == datetime(2017, 9, 28, 0, 31, 55, tzinfo=timezone.utc) + assert to_datetime("2017-09-28T00:31:55.4Z") == datetime(2017, 9, 28, 0, 31, 55, 400000, tzinfo=timezone.utc) + assert to_datetime("2017-09-28T00:31:55.004Z") == datetime(2017, 9, 28, 0, 31, 55, 4000, tzinfo=timezone.utc) + assert to_datetime("2017-09-28T00:31:55.00042Z") == datetime(2017, 9, 28, 0, 31, 55, 420, tzinfo=timezone.utc) diff --git a/tox.ini b/tox.ini index 7841c6e..300705a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,13 @@ [tox] passenv = TRAVIS usedevelop=True -envlist={test,itest}-{py27,py33},pep8 +envlist={test,itest}-{py36,py37},pep8 [testenv] passenv = TRAVIS MARATHONVERSION DOCKER_HOST DOCKER_TLS_VERIFY DOCKER_CERT_PATH DOCKER_MACHINE_NAME basepython = - py27: python2.7 - py33: python3 + py36: python3.6 + py37: python3.7 whitelist_externals=/bin/bash skipsdist=True changedir = @@ -25,10 +25,18 @@ commands = itest: ./itest.sh {posargs} [testenv:pep8] -basepython = python2.7 +basepython = python3.6 deps = flake8 commands = flake8 . [flake8] exclude = .tox,*.egg,docs,build,__init__.py max-line-length = 160 + +[testenv:pre-commit] +basepython = python3.7 +deps = + pre-commit>=1.20.0 +commands = + pre-commit install -f --install-hooks + pre-commit run --all-files \ No newline at end of file