├── .gitignore ├── .jscs.json ├── .jshintrc ├── .npmignore ├── .rock.yml ├── LICENSE ├── NOTICE ├── README.md ├── Vagrantfile ├── lib ├── chronos.js ├── chronos │ ├── job.js │ └── task.js ├── index.js ├── marathon.js ├── marathon │ ├── app.js │ └── event_subscription.js └── utils.js ├── package.json └── test ├── chronos.js ├── helper.js ├── helper └── marathon.js └── marathon.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *swp 3 | .DS_Store 4 | .vagrant 5 | coverage 6 | node_modules 7 | npm-shrinkwrap.json 8 | -------------------------------------------------------------------------------- /.jscs.json: -------------------------------------------------------------------------------- 1 | { 2 | "requireSpaceAfterKeywords": [ 3 | "if", 4 | "else", 5 | "for", 6 | "while", 7 | "do", 8 | "switch", 9 | "return", 10 | "try", 11 | "catch" 12 | ], 13 | "requireSpacesInFunctionExpression": { 14 | "beforeOpeningCurlyBrace": true 15 | }, 16 | "disallowSpacesInFunctionExpression": { 17 | "beforeOpeningRoundBrace": true 18 | }, 19 | "disallowEmptyBlocks": true, 20 | "requireSpacesInsideObjectBrackets": "all", 21 | "disallowSpacesInsideArrayBrackets": true, 22 | "disallowSpacesInsideParentheses": true, 23 | "disallowQuotedKeysInObjects": "allButReserved", 24 | "disallowSpaceAfterObjectKeys": true, 25 | "requireCommaBeforeLineBreak": true, 26 | "requireOperatorBeforeLineBreak": true, 27 | "requireSpaceAfterBinaryOperators": [ 28 | "?", 29 | "+", 30 | "/", 31 | "*", 32 | ":", 33 | "=", 34 | "==", 35 | "===", 36 | "!=", 37 | "!==", 38 | ">", 39 | ">=", 40 | "<", 41 | "<=" 42 | ], 43 | "requireSpacesInConditionalExpression": true, 44 | "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], 45 | "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], 46 | "requireSpaceBeforeBinaryOperators": [ 47 | "+", 48 | "-", 49 | "/", 50 | "*", 51 | "=", 52 | "==", 53 | "===", 54 | "!=", 55 | "!==" 56 | ], 57 | "requireSpaceAfterBinaryOperators": [ 58 | "+", 59 | "-", 60 | "/", 61 | "*", 62 | "=", 63 | "==", 64 | "===", 65 | "!=", 66 | "!==" 67 | ], 68 | "disallowKeywords": ["with"], 69 | "disallowMultipleLineStrings": true, 70 | "disallowMultipleLineBreaks": true, 71 | "validateLineBreaks": "LF", 72 | "validateQuoteMarks": true, 73 | "disallowTrailingWhitespace": true, 74 | "disallowKeywordsOnNewLine": ["else"], 75 | "requireCapitalizedConstructors": true, 76 | "safeContextKeyword": "self", 77 | "requireDotNotation": true, 78 | "excludeFiles": ["node_modules/**"] 79 | } 80 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | /* 3 | * ENVIRONMENTS 4 | * ================= 5 | */ 6 | 7 | // Define globals exposed by modern browsers. 8 | "browser": true, 9 | 10 | // Define globals exposed by jQuery. 11 | "jquery": true, 12 | 13 | // Define globals exposed by Node.js. 14 | "node": true, 15 | 16 | // Define globals exposed by other 17 | "globals": { 18 | // Mocha 19 | "describe": false, 20 | "it": false, 21 | "before": false, 22 | "beforeEach": false, 23 | "after": false, 24 | "afterEach": false 25 | }, 26 | 27 | /* 28 | * ENFORCING OPTIONS 29 | * ================= 30 | */ 31 | 32 | // Force all variable names to use either camelCase style or UPPER_CASE 33 | // with underscores. 34 | "camelcase": true, 35 | 36 | // Prohibit use of == and != in favor of === and !==. 37 | "eqeqeq": true, 38 | 39 | // Suppress warnings about == null comparisons. 40 | "eqnull": true, 41 | 42 | // Enforce tab width of 2 spaces. 43 | "indent": 2, 44 | 45 | // Prohibit use of a variable before it is defined. 46 | "latedef": true, 47 | 48 | // Require capitalized names for constructor functions. 49 | "newcap": true, 50 | 51 | // Enforce use of single quotation marks for strings. 52 | "quotmark": "single", 53 | 54 | // Prohibit trailing whitespace. 55 | "trailing": true, 56 | 57 | // Prohibit use of explicitly undeclared variables. 58 | "undef": true, 59 | 60 | // Warn when variables are defined but never used. 61 | "unused": true, 62 | 63 | // Enforce line length to 80 characters 64 | "maxlen": 80, 65 | 66 | // Enforce placing 'use strict' at the top function scope 67 | "strict": true 68 | } 69 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .env 2 | .jscs.json 3 | .jshintrc 4 | .npmignore 5 | .rock.yml 6 | .vagrant 7 | Vagrantfile 8 | config 9 | coverage 10 | test 11 | tmp 12 | -------------------------------------------------------------------------------- /.rock.yml: -------------------------------------------------------------------------------- 1 | runtime: node010 2 | 3 | env: 4 | VM_IP: "10.141.141.10" 5 | 6 | test: | 7 | if [[ -n "$ROCK_ARGV" ]]; then 8 | exec mocha --recursive --timeout 15000 --reporter spec "${ARGV[@]}" 9 | else 10 | {{ parent }} 11 | fi 12 | 13 | coverage: | 14 | istanbul cover _mocha -- --recursive --timeout 15000 15 | if type -f open &>/dev/null; then 16 | open coverage/lcov-report/index.html 17 | fi 18 | 19 | marathon: open "http://${VM_IP}:8080" 20 | chronos: open "http://${VM_IP}:4400" 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Silas Sewell 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Documentation derived from Marathon project. 2 | 3 | https://github.com/mesosphere/marathon 4 | 5 | Apache License 6 | Version 2.0, January 2004 7 | http://www.apache.org/licenses/ 8 | 9 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 10 | 11 | 1. Definitions. 12 | 13 | "License" shall mean the terms and conditions for use, reproduction, 14 | and distribution as defined by Sections 1 through 9 of this document. 15 | 16 | "Licensor" shall mean the copyright owner or entity authorized by 17 | the copyright owner that is granting the License. 18 | 19 | "Legal Entity" shall mean the union of the acting entity and all 20 | other entities that control, are controlled by, or are under common 21 | control with that entity. For the purposes of this definition, 22 | "control" means (i) the power, direct or indirect, to cause the 23 | direction or management of such entity, whether by contract or 24 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 25 | outstanding shares, or (iii) beneficial ownership of such entity. 26 | 27 | "You" (or "Your") shall mean an individual or Legal Entity 28 | exercising permissions granted by this License. 29 | 30 | "Source" form shall mean the preferred form for making modifications, 31 | including but not limited to software source code, documentation 32 | source, and configuration files. 33 | 34 | "Object" form shall mean any form resulting from mechanical 35 | transformation or translation of a Source form, including but 36 | not limited to compiled object code, generated documentation, 37 | and conversions to other media types. 38 | 39 | "Work" shall mean the work of authorship, whether in Source or 40 | Object form, made available under the License, as indicated by a 41 | copyright notice that is included in or attached to the work 42 | (an example is provided in the Appendix below). 43 | 44 | "Derivative Works" shall mean any work, whether in Source or Object 45 | form, that is based on (or derived from) the Work and for which the 46 | editorial revisions, annotations, elaborations, or other modifications 47 | represent, as a whole, an original work of authorship. For the purposes 48 | of this License, Derivative Works shall not include works that remain 49 | separable from, or merely link (or bind by name) to the interfaces of, 50 | the Work and Derivative Works thereof. 51 | 52 | "Contribution" shall mean any work of authorship, including 53 | the original version of the Work and any modifications or additions 54 | to that Work or Derivative Works thereof, that is intentionally 55 | submitted to Licensor for inclusion in the Work by the copyright owner 56 | or by an individual or Legal Entity authorized to submit on behalf of 57 | the copyright owner. For the purposes of this definition, "submitted" 58 | means any form of electronic, verbal, or written communication sent 59 | to the Licensor or its representatives, including but not limited to 60 | communication on electronic mailing lists, source code control systems, 61 | and issue tracking systems that are managed by, or on behalf of, the 62 | Licensor for the purpose of discussing and improving the Work, but 63 | excluding communication that is conspicuously marked or otherwise 64 | designated in writing by the copyright owner as "Not a Contribution." 65 | 66 | "Contributor" shall mean Licensor and any individual or Legal Entity 67 | on behalf of whom a Contribution has been received by Licensor and 68 | subsequently incorporated within the Work. 69 | 70 | 2. Grant of Copyright License. Subject to the terms and conditions of 71 | this License, each Contributor hereby grants to You a perpetual, 72 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 73 | copyright license to reproduce, prepare Derivative Works of, 74 | publicly display, publicly perform, sublicense, and distribute the 75 | Work and such Derivative Works in Source or Object form. 76 | 77 | 3. Grant of Patent License. Subject to the terms and conditions of 78 | this License, each Contributor hereby grants to You a perpetual, 79 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 80 | (except as stated in this section) patent license to make, have made, 81 | use, offer to sell, sell, import, and otherwise transfer the Work, 82 | where such license applies only to those patent claims licensable 83 | by such Contributor that are necessarily infringed by their 84 | Contribution(s) alone or by combination of their Contribution(s) 85 | with the Work to which such Contribution(s) was submitted. If You 86 | institute patent litigation against any entity (including a 87 | cross-claim or counterclaim in a lawsuit) alleging that the Work 88 | or a Contribution incorporated within the Work constitutes direct 89 | or contributory patent infringement, then any patent licenses 90 | granted to You under this License for that Work shall terminate 91 | as of the date such litigation is filed. 92 | 93 | 4. Redistribution. You may reproduce and distribute copies of the 94 | Work or Derivative Works thereof in any medium, with or without 95 | modifications, and in Source or Object form, provided that You 96 | meet the following conditions: 97 | 98 | (a) You must give any other recipients of the Work or 99 | Derivative Works a copy of this License; and 100 | 101 | (b) You must cause any modified files to carry prominent notices 102 | stating that You changed the files; and 103 | 104 | (c) You must retain, in the Source form of any Derivative Works 105 | that You distribute, all copyright, patent, trademark, and 106 | attribution notices from the Source form of the Work, 107 | excluding those notices that do not pertain to any part of 108 | the Derivative Works; and 109 | 110 | (d) If the Work includes a "NOTICE" text file as part of its 111 | distribution, then any Derivative Works that You distribute must 112 | include a readable copy of the attribution notices contained 113 | within such NOTICE file, excluding those notices that do not 114 | pertain to any part of the Derivative Works, in at least one 115 | of the following places: within a NOTICE text file distributed 116 | as part of the Derivative Works; within the Source form or 117 | documentation, if provided along with the Derivative Works; or, 118 | within a display generated by the Derivative Works, if and 119 | wherever such third-party notices normally appear. The contents 120 | of the NOTICE file are for informational purposes only and 121 | do not modify the License. You may add Your own attribution 122 | notices within Derivative Works that You distribute, alongside 123 | or as an addendum to the NOTICE text from the Work, provided 124 | that such additional attribution notices cannot be construed 125 | as modifying the License. 126 | 127 | You may add Your own copyright statement to Your modifications and 128 | may provide additional or different license terms and conditions 129 | for use, reproduction, or distribution of Your modifications, or 130 | for any such Derivative Works as a whole, provided Your use, 131 | reproduction, and distribution of the Work otherwise complies with 132 | the conditions stated in this License. 133 | 134 | 5. Submission of Contributions. Unless You explicitly state otherwise, 135 | any Contribution intentionally submitted for inclusion in the Work 136 | by You to the Licensor shall be under the terms and conditions of 137 | this License, without any additional terms or conditions. 138 | Notwithstanding the above, nothing herein shall supersede or modify 139 | the terms of any separate license agreement you may have executed 140 | with Licensor regarding such Contributions. 141 | 142 | 6. Trademarks. This License does not grant permission to use the trade 143 | names, trademarks, service marks, or product names of the Licensor, 144 | except as required for reasonable and customary use in describing the 145 | origin of the Work and reproducing the content of the NOTICE file. 146 | 147 | 7. Disclaimer of Warranty. Unless required by applicable law or 148 | agreed to in writing, Licensor provides the Work (and each 149 | Contributor provides its Contributions) on an "AS IS" BASIS, 150 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 151 | implied, including, without limitation, any warranties or conditions 152 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 153 | PARTICULAR PURPOSE. You are solely responsible for determining the 154 | appropriateness of using or redistributing the Work and assume any 155 | risks associated with Your exercise of permissions under this License. 156 | 157 | 8. Limitation of Liability. In no event and under no legal theory, 158 | whether in tort (including negligence), contract, or otherwise, 159 | unless required by applicable law (such as deliberate and grossly 160 | negligent acts) or agreed to in writing, shall any Contributor be 161 | liable to You for damages, including any direct, indirect, special, 162 | incidental, or consequential damages of any character arising as a 163 | result of this License or out of the use or inability to use the 164 | Work (including but not limited to damages for loss of goodwill, 165 | work stoppage, computer failure or malfunction, or any and all 166 | other commercial damages or losses), even if such Contributor 167 | has been advised of the possibility of such damages. 168 | 169 | 9. Accepting Warranty or Additional Liability. While redistributing 170 | the Work or Derivative Works thereof, You may choose to offer, 171 | and charge a fee for, acceptance of support, warranty, indemnity, 172 | or other liability obligations and/or rights consistent with this 173 | License. However, in accepting such obligations, You may act only 174 | on Your own behalf and on Your sole responsibility, not on behalf 175 | of any other Contributor, and only if You agree to indemnify, 176 | defend, and hold each Contributor harmless for any liability 177 | incurred by, or claims asserted against, such Contributor by reason 178 | of your accepting any such warranty or additional liability. 179 | 180 | END OF TERMS AND CONDITIONS 181 | 182 | APPENDIX: How to apply the Apache License to your work. 183 | 184 | To apply the Apache License to your work, attach the following 185 | boilerplate notice, with the fields enclosed by brackets "[]" 186 | replaced with your own identifying information. (Don't include 187 | the brackets!) The text should be enclosed in the appropriate 188 | comment syntax for the file format. We also recommend that a 189 | file or class name and description of purpose be included on the 190 | same "printed page" as the copyright notice for easier 191 | identification within third-party archives. 192 | 193 | Copyright 2013 Mesosphere 194 | 195 | Licensed under the Apache License, Version 2.0 (the "License"); 196 | you may not use this file except in compliance with the License. 197 | You may obtain a copy of the License at 198 | 199 | http://www.apache.org/licenses/LICENSE-2.0 200 | 201 | Unless required by applicable law or agreed to in writing, software 202 | distributed under the License is distributed on an "AS IS" BASIS, 203 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 204 | See the License for the specific language governing permissions and 205 | limitations under the License. 206 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mesos 2 | 3 | Mesos clients. 4 | 5 | * [Documentation](#documentation) 6 | * [Development](#development) 7 | * [License](#license) 8 | 9 | ## Documentation 10 | 11 | * [Chronos](#chronos) 12 | * [Marathon](#marathon) 13 | 14 | 15 | 16 | ### mesos.Chronos([opts]) 17 | 18 | Initialize a new Chronos client. 19 | 20 | Options 21 | 22 | * host (String, default: 127.0.0.1): Chronos address 23 | * port (String, default: 4400): Chronos HTTP port 24 | * secure (Boolean, default: false): enable HTTPS 25 | 26 | Usage 27 | 28 | ``` javascript 29 | var mesos = require('mesos'); 30 | 31 | var chronos = mesos.Chronos({ host: '10.141.141.10' }); 32 | ``` 33 | 34 | 35 | 36 | ### chronos.job.create(opts, callback) 37 | 38 | Create job. 39 | 40 | Options 41 | 42 | * name (String): job name 43 | * schedule (String): [ISO-8601][iso-8601] recurring series time 44 | * command (String): command to execute 45 | * epsilon (String): run if missed within this time period ([ISO-8601][iso-8601] duration) 46 | * owner (String): email address of job owner 47 | * async (Boolean, default: false): run job asynchronously 48 | 49 | ### chronos.job.destroy(opts, callback) 50 | 51 | Delete job. 52 | 53 | Options 54 | 55 | * name (String): job name 56 | 57 | 58 | 59 | ### chronos.job.list(callback) 60 | 61 | List jobs. 62 | 63 | ### chronos.job.search(opts, callback) 64 | 65 | Search jobs. 66 | 67 | Options 68 | 69 | * name (String, optional): query on name 70 | * command (String, optional): query on command 71 | * any (String, optional): query on any field 72 | * limit (Number, default: 10): limit the number of results 73 | * offset (Number, default: 0): offset results by number 74 | 75 | ### chronos.job.start(opts, callback) 76 | 77 | Manually start job. 78 | 79 | Options 80 | 81 | * name (String): job name 82 | 83 | ### chronos.job.stats(opts, callback) 84 | 85 | Get jobs statistics. 86 | 87 | Options 88 | 89 | * name (String, optional): job name 90 | * percentile (String, optional): statistic type 91 | 92 | If you specify the job name you'll get all the statistics for that job, otherwise if you specify a percentile you'll get that statistic for all jobs. 93 | 94 | You must specify either a job name or a percentile. 95 | 96 | ### chronos.task.update(opts, callback) 97 | 98 | Update task. 99 | 100 | Options 101 | 102 | * id (String): task id 103 | * statusCode (Integer, supports: 0, 1): task succeeded (0) or fail (1) 104 | 105 | ### chronos.task.kill(opts, callback) 106 | 107 | Kill tasks. 108 | 109 | Options 110 | 111 | * job (String): job name 112 | 113 | 114 | 115 | ### mesos.Marathon([opts]) 116 | 117 | Initialize a new Marathon client. 118 | 119 | Options 120 | 121 | * host (String, default: 127.0.0.1): Marathon address 122 | * port (String, default: 8080): Marathon HTTP port 123 | * secure (Boolean, default: false): enable HTTPS 124 | 125 | Usage 126 | 127 | ``` javascript 128 | var mesos = require('mesos'); 129 | 130 | var marathon = mesos.Marathon({ host: '10.141.141.10' }); 131 | ``` 132 | 133 | See [Marathon REST][marathon-rest] documentation for more information. 134 | 135 | 136 | 137 | ### marathon.app.create(opts, callback) 138 | 139 | Create and start a new application. 140 | 141 | Options 142 | 143 | * id (String): app ID 144 | * cpus (Number): number of CPUs for each instance 145 | * mem (Number): amount of memory for each instance 146 | * instances (Number): number of instances 147 | * cmd (String, optional): command to execute 148 | 149 | And more, see [docs](https://github.com/mesosphere/marathon/blob/master/REST.md#post-v2apps). 150 | 151 | 152 | 153 | ### marathon.app.list([opts], callback) 154 | 155 | List all running applications. 156 | 157 | Options 158 | 159 | * cmd (String, optional): filter apps by command 160 | 161 | 162 | 163 | ### marathon.app.get(opts, callback) 164 | 165 | Get application with by ID. 166 | 167 | Options 168 | 169 | * id (String): app ID 170 | 171 | 172 | 173 | ### marathon.app.versions(opts, callback) 174 | 175 | List the versions of an application by ID. 176 | 177 | Options 178 | 179 | * id (String): app ID 180 | 181 | 182 | 183 | ### marathon.app.version(opts, callback) 184 | 185 | List the configuration of an application by ID at a specified version. 186 | 187 | Options 188 | 189 | * id (String): app ID 190 | * version (String): app version 191 | 192 | 193 | 194 | ### marathon.app.update(opts, callback) 195 | 196 | Change parameters of a running application. The new application parameters 197 | apply only to subsequently created tasks, and currently running tasks are 198 | not pre-emptively restarted. 199 | 200 | Options 201 | 202 | * id (String): app ID 203 | * cpus (Number): number of CPUs for each instance 204 | * mem (Number): amount of memory for each instance 205 | * instances (Number): number of instances 206 | * cmd (String, optional): command to execute 207 | 208 | And more, see [docs](https://github.com/mesosphere/marathon/blob/master/REST.md#put-v2appsappid). 209 | 210 | 211 | 212 | ### marathon.app.destroy(opts, callback) 213 | 214 | Destroy an applicationb by ID. 215 | 216 | Options 217 | 218 | * id (String): app ID 219 | 220 | 221 | 222 | ### marathon.app.tasks(opts, callback) 223 | 224 | List all running tasks for an application by ID. 225 | 226 | Options 227 | 228 | * id (String): app ID 229 | 230 | 231 | 232 | ### marathon.app.kill(opts, callback) 233 | 234 | Kill tasks that belong to an application. 235 | 236 | Options 237 | 238 | * id (String): app ID 239 | * task (String, optional): kill by task ID 240 | * host (String, optional): restrict to tasks on specified slave (can't use with task) 241 | * scale (Boolean, optional): scale application down by one 242 | 243 | 244 | 245 | ### marathon.eventSubscription.register(opts, callback) 246 | 247 | Register a callback URL as an event subscriber. 248 | 249 | Options 250 | 251 | * url (String): callback URL 252 | 253 | 254 | 255 | ### marathon.eventSubscription.list(callback) 256 | 257 | List all event subscriber callback URLs. 258 | 259 | 260 | 261 | ### marathon.eventSubscription.unregister(opts, callback) 262 | 263 | Unregister a callback URL. 264 | 265 | Options 266 | 267 | * url (String): callback URL 268 | 269 | 270 | 271 | ### marathon.tasks(callback) 272 | 273 | List all running tasks. 274 | 275 | ## Development 276 | 277 | 1. Install [Vagrant][vagrant] 278 | 279 | 1. Clone repository 280 | 281 | ``` console 282 | $ git clone https://github.com/silas/node-mesos.git 283 | ``` 284 | 285 | 1. Switch to project directory 286 | 287 | ``` console 288 | $ cd node-mesos 289 | ``` 290 | 291 | 1. Start VM 292 | 293 | ``` console 294 | $ vagrant up 295 | ``` 296 | 297 | 1. Install client dependencies 298 | 299 | ``` console 300 | $ npm install 301 | ``` 302 | 303 | 1. Run tests 304 | 305 | ``` console 306 | $ npm test 307 | ``` 308 | 309 | ## License 310 | 311 | This work is licensed under the MIT License (see the LICENSE file). 312 | 313 | [iso-8601]: https://github.com/cylc/cylc/wiki/ISO-8601 314 | [marathon-rest]: https://github.com/mesosphere/marathon/blob/master/REST.md 315 | [vagrant]: http://www.vagrantup.com/ 316 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # vi: set ft=ruby 2 | 3 | Vagrant.configure('2') do |config| 4 | config.vm.box = 'playa_mesos_ubuntu_14.04' 5 | config.vm.box_url = "http://downloads.mesosphere.io/playa-mesos/#{config.vm.box}.box" 6 | 7 | config.vm.network :private_network, ip: ENV['VM_IP'] || '10.141.141.10' 8 | 9 | config.vm.provision :shell, inline: <<-eof.gsub(/^\s*/, '') 10 | set -o errexit 11 | 12 | apt-get update 13 | apt-get install chronos marathon -y 14 | 15 | # enable event subscriptions 16 | mkdir -p /etc/marathon/conf 17 | echo http_callback > /etc/marathon/conf/event_subscriber 18 | 19 | # restart 20 | service chronos restart 21 | service marathon restart 22 | eof 23 | 24 | config.vm.provider :virtualbox do |v| 25 | v.customize ['modifyvm', :id, '--cpus', ENV['VM_CPUS'] || '2'] 26 | v.customize ['modifyvm', :id, '--memory', ENV['VM_MEMORY'] || 2048] 27 | v.customize ['modifyvm', :id, '--natdnshostresolver1', 'on'] 28 | v.customize ['modifyvm', :id, '--natdnsproxy1', 'on'] 29 | v.customize ['setextradata', :id, 'VBoxInternal2/SharedFoldersEnableSymlinksCreate/v-root', '1'] 30 | end 31 | 32 | config.ssh.forward_agent = true 33 | end 34 | -------------------------------------------------------------------------------- /lib/chronos.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Chronos client. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var papi = require('papi'); 12 | var util = require('util'); 13 | 14 | var Job = require('./chronos/job').Job; 15 | var Task = require('./chronos/task').Task; 16 | 17 | /** 18 | * Initialize a new `Chronos` client. 19 | * 20 | * @param {Object} opts 21 | */ 22 | 23 | function Chronos(opts) { 24 | if (!(this instanceof Chronos)) { 25 | return new Chronos(opts); 26 | } 27 | 28 | opts = opts || {}; 29 | 30 | if (!opts.baseUrl) { 31 | opts.baseUrl = (opts.secure ? 'https:' : 'http:') + '//' + 32 | (opts.host || '127.0.0.1') + ':' + 33 | (opts.port || '4400') + '/scheduler'; 34 | } 35 | opts.name = 'chronos'; 36 | opts.type = 'json'; 37 | 38 | papi.Client.call(this, opts); 39 | 40 | this.job = new Job(this); 41 | this.task = new Task(this); 42 | } 43 | 44 | util.inherits(Chronos, papi.Client); 45 | 46 | /** 47 | * Module Exports. 48 | */ 49 | 50 | exports.Chronos = Chronos; 51 | -------------------------------------------------------------------------------- /lib/chronos/job.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Job client. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var utils = require('../utils'); 12 | 13 | /** 14 | * Initialize a new `Job` client. 15 | */ 16 | 17 | function Job(chronos) { 18 | this.chronos = chronos; 19 | } 20 | 21 | /** 22 | * List jobs. 23 | */ 24 | 25 | Job.prototype.list = function(opts, callback) { 26 | if (typeof opts === 'function') { 27 | callback = opts; 28 | opts = {}; 29 | } 30 | 31 | var req = { 32 | name: 'job.list', 33 | path: '/jobs', 34 | }; 35 | 36 | this.chronos._get(req, utils.bodyDefault([]), callback); 37 | }; 38 | 39 | /** 40 | * Create job. 41 | */ 42 | 43 | Job.prototype.create = function(opts, callback) { 44 | opts = opts || {}; 45 | 46 | var req = { 47 | name: 'job.create', 48 | path: '/iso8601', 49 | body: { 50 | name: opts.name, 51 | schedule: opts.schedule, 52 | command: opts.command, 53 | epsilon: opts.epsilon, 54 | owner: opts.owner, 55 | }, 56 | }; 57 | 58 | if (!opts.hasOwnProperty('async')) req.body.async = false; 59 | 60 | try { 61 | if (!opts.name) throw new Error('name required'); 62 | if (!opts.schedule) throw new Error('schedule required'); 63 | if (!opts.command) throw new Error('command required'); 64 | if (!opts.epsilon) throw new Error('epsilon required'); 65 | if (!opts.owner) throw new Error('owner required'); 66 | } catch (err) { 67 | return callback(this.chronos._err(err, req)); 68 | } 69 | 70 | this.chronos._post(req, utils.empty, callback); 71 | }; 72 | 73 | /** 74 | * Delete job. 75 | */ 76 | 77 | Job.prototype.destroy = function(opts, callback) { 78 | if (typeof opts === 'string') { 79 | opts = { name: opts }; 80 | } else { 81 | opts = opts || {}; 82 | } 83 | 84 | var req = { 85 | name: 'job.destroy', 86 | path: '/job/{name}', 87 | params: { name: opts.name }, 88 | }; 89 | 90 | if (!opts.name) return callback(this.chronos._err('name required', req)); 91 | 92 | this.chronos._delete(req, utils.empty, callback); 93 | }; 94 | 95 | /** 96 | * Start job. 97 | */ 98 | 99 | Job.prototype.start = function(opts, callback) { 100 | if (typeof opts === 'string') { 101 | opts = { name: opts }; 102 | } else { 103 | opts = opts || {}; 104 | } 105 | 106 | var req = { 107 | name: 'job.start', 108 | path: '/job/{name}', 109 | params: { name: opts.name }, 110 | }; 111 | 112 | if (!opts.name) return callback(this.chronos._err('name required', req)); 113 | 114 | this.chronos._put(req, utils.empty, callback); 115 | }; 116 | 117 | /** 118 | * Stats. 119 | */ 120 | 121 | Job.prototype.stats = function(opts, callback) { 122 | if (typeof opts === 'string') { 123 | opts = { name: opts }; 124 | } else { 125 | opts = opts || {}; 126 | } 127 | 128 | var req = { 129 | name: 'job.stats', 130 | params: {}, 131 | }; 132 | 133 | if (opts.name) { 134 | req.path = '/job/stat/{name}'; 135 | req.params.name = opts.name; 136 | } else if (opts.percentile) { 137 | req.path = '/stats/{percentile}'; 138 | req.params.percentile = opts.percentile; 139 | } else { 140 | return callback(this.chronos._err('name or percentile required', req)); 141 | } 142 | 143 | this.chronos._get(req, utils.body, callback); 144 | }; 145 | 146 | /** 147 | * Search jobs. 148 | */ 149 | 150 | Job.prototype.search = function(opts, callback) { 151 | if (typeof opts === 'string') { 152 | opts = { name: opts }; 153 | } else if (typeof opts === 'function') { 154 | callback = opts; 155 | opts = {}; 156 | } else { 157 | opts = opts || {}; 158 | } 159 | 160 | var req = { 161 | name: 'job.search', 162 | path: '/jobs/search', 163 | query: {}, 164 | }; 165 | 166 | if (opts.any) req.query.any = opts.any; 167 | if (opts.name) req.query.name = opts.name; 168 | if (opts.command) req.query.command = opts.command; 169 | if (opts.limit) req.query.limit = opts.limit; 170 | if (opts.offset) req.query.offset = opts.offset; 171 | 172 | this.chronos._get(req, utils.body, callback); 173 | }; 174 | 175 | /** 176 | * Module Exports. 177 | */ 178 | 179 | exports.Job = Job; 180 | -------------------------------------------------------------------------------- /lib/chronos/task.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Task client. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var utils = require('../utils'); 12 | 13 | /** 14 | * Initialize a new `Task` client. 15 | */ 16 | 17 | function Task(chronos) { 18 | this.chronos = chronos; 19 | } 20 | 21 | /** 22 | * Update task. 23 | */ 24 | 25 | Task.prototype.update = function(opts, callback) { 26 | opts = opts || {}; 27 | 28 | var req = { 29 | name: 'task.update', 30 | path: '/task/{id}', 31 | params: { id: opts.id }, 32 | body: { statusCode: opts.statusCode }, 33 | }; 34 | 35 | try { 36 | if (!opts.id) throw new Error('id required'); 37 | if (!opts.hasOwnProperty('statusCode')) { 38 | throw new Error('statusCode required'); 39 | } 40 | } catch (err) { 41 | return callback(this.chronos._err(err, req)); 42 | } 43 | 44 | this.chronos._put(req, utils.empty, callback); 45 | }; 46 | 47 | /** 48 | * Kill tasks. 49 | */ 50 | 51 | Task.prototype.kill = function(opts, callback) { 52 | opts = opts || {}; 53 | 54 | var req = { 55 | name: 'task.kill', 56 | path: '/task/kill/{job}', 57 | params: { job: opts.job }, 58 | }; 59 | 60 | if (!opts.job) return callback(this.chronos._err('job required', req)); 61 | 62 | this.chronos._delete(req, utils.empty, callback); 63 | }; 64 | 65 | /** 66 | * Module Exports. 67 | */ 68 | 69 | exports.Task = Task; 70 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Supported clients. 5 | */ 6 | 7 | exports.Chronos = require('./chronos').Chronos; 8 | exports.Marathon = require('./marathon').Marathon; 9 | -------------------------------------------------------------------------------- /lib/marathon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Marathon client. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var papi = require('papi'); 12 | var util = require('util'); 13 | 14 | var App = require('./marathon/app').App; 15 | var EventSubscription = require('./marathon/event_subscription') 16 | .EventSubscription; 17 | var utils = require('./utils'); 18 | 19 | /** 20 | * Initialize a new `Marathon` client. 21 | * 22 | * @param {Object} opts 23 | */ 24 | 25 | function Marathon(opts) { 26 | if (!(this instanceof Marathon)) { 27 | return new Marathon(opts); 28 | } 29 | 30 | opts = opts || {}; 31 | 32 | if (!opts.baseUrl) { 33 | opts.baseUrl = (opts.secure ? 'https:' : 'http:') + '//' + 34 | (opts.host || '127.0.0.1') + ':' + 35 | (opts.port || '8080') + '/v2'; 36 | } 37 | opts.name = 'marathon'; 38 | opts.type = 'json'; 39 | 40 | papi.Client.call(this, opts); 41 | 42 | this.app = new App(this); 43 | this.eventSubscription = new EventSubscription(this); 44 | } 45 | 46 | util.inherits(Marathon, papi.Client); 47 | 48 | /** 49 | * List tasks of all running applications. 50 | */ 51 | 52 | Marathon.prototype.tasks = function(opts, callback) { 53 | if (typeof opts === 'function') { 54 | callback = opts; 55 | opts = {}; 56 | } 57 | 58 | var req = { 59 | name: 'tasks', 60 | path: '/tasks', 61 | }; 62 | 63 | this._get(req, utils.bodyItem('tasks'), callback); 64 | }; 65 | 66 | /** 67 | * Module Exports. 68 | */ 69 | 70 | exports.Marathon = Marathon; 71 | -------------------------------------------------------------------------------- /lib/marathon/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * App information 3 | */ 4 | 5 | 'use strict'; 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var utils = require('../utils'); 12 | 13 | /** 14 | * Initialize a new `App` client. 15 | */ 16 | 17 | function App(marathon) { 18 | this.marathon = marathon; 19 | } 20 | 21 | /** 22 | * Create and start a new application. 23 | */ 24 | 25 | App.prototype.create = function(opts, callback) { 26 | opts = opts || {}; 27 | 28 | var req = { 29 | name: 'app.create', 30 | path: '/apps', 31 | }; 32 | 33 | try { 34 | if (!opts.id) throw new Error('id required'); 35 | if (!opts.cpus) throw new Error('cpus required'); 36 | if (!opts.mem) throw new Error('mem required'); 37 | if (!opts.instances) throw new Error('instances required'); 38 | 39 | if (!opts.cmd && !opts.executor && !opts.image) { 40 | throw new Error('cmd, executor or image required'); 41 | } 42 | } catch (err) { 43 | return callback(this.marathon._err(err, req)); 44 | } 45 | 46 | req.body = opts; 47 | 48 | this.marathon._post(req, utils.empty, callback); 49 | }; 50 | 51 | /** 52 | * List all running applications. 53 | */ 54 | 55 | App.prototype.list = function(opts, callback) { 56 | if (typeof opts === 'function') { 57 | callback = opts; 58 | opts = {}; 59 | } 60 | 61 | var req = { 62 | name: 'app.list', 63 | path: '/apps', 64 | query: {}, 65 | }; 66 | 67 | if (opts.cmd) req.query.cmd = opts.cmd; 68 | 69 | this.marathon._get(req, utils.bodyItem('apps'), callback); 70 | }; 71 | 72 | /** 73 | * List the application with application id. 74 | */ 75 | 76 | App.prototype.get = function(opts, callback) { 77 | if (typeof opts === 'string') { 78 | opts = { id: opts }; 79 | } 80 | 81 | var req = { 82 | name: 'app.get', 83 | path: '/apps/{id}', 84 | params: { id: opts.id }, 85 | }; 86 | 87 | if (!opts.id) return callback(this.marathon._err('id required', req)); 88 | 89 | this.marathon._get(req, utils.bodyItem('app'), callback); 90 | }; 91 | 92 | /** 93 | * List the versions of the application with id. 94 | */ 95 | 96 | App.prototype.versions = function(opts, callback) { 97 | if (typeof opts === 'string') { 98 | opts = { id: opts }; 99 | } 100 | 101 | var req = { 102 | name: 'app.versions', 103 | path: '/apps/{id}/versions', 104 | params: { id: opts.id }, 105 | }; 106 | 107 | if (!opts.id) return callback(this.marathon._err('id required', req)); 108 | 109 | this.marathon._get(req, utils.bodyItem('versions'), callback); 110 | }; 111 | 112 | /** 113 | * List the configuration of the application with id at version. 114 | */ 115 | 116 | App.prototype.version = function(opts, callback) { 117 | opts = opts || {}; 118 | 119 | var req = { 120 | name: 'app.version', 121 | path: '/apps/{id}/versions/{version}', 122 | params: { id: opts.id, version: opts.version }, 123 | }; 124 | 125 | try { 126 | if (!opts.id) throw new Error('id required'); 127 | if (!opts.version) throw new Error('version required'); 128 | } catch (err) { 129 | return callback(this.marathon._err(err, req)); 130 | } 131 | 132 | this.marathon._get(req, utils.body, callback); 133 | }; 134 | 135 | /** 136 | * Change parameters of a running application. The new application parameters 137 | * apply only to subsequently created tasks, and currently running tasks are 138 | * not pre-emptively restarted. 139 | */ 140 | 141 | App.prototype.update = function(opts, callback) { 142 | opts = opts || {}; 143 | 144 | var req = { 145 | name: 'app.update', 146 | path: '/apps/{id}', 147 | params: { id: opts.id }, 148 | body: opts, 149 | }; 150 | 151 | if (!opts.id) return callback(this.marathon._err('id required', req)); 152 | 153 | this.marathon._put(req, utils.empty, callback); 154 | }; 155 | 156 | /** 157 | * Destroy an application. All data about that application will be deleted. 158 | */ 159 | 160 | App.prototype.destroy = function(opts, callback) { 161 | if (typeof opts === 'string') { 162 | opts = { id: opts }; 163 | } 164 | 165 | var req = { 166 | name: 'app.destroy', 167 | path: '/apps/{id}', 168 | params: { id: opts.id }, 169 | }; 170 | 171 | if (!opts.id) return callback(this.marathon._err('id required', req)); 172 | 173 | this.marathon._delete(req, utils.empty, callback); 174 | }; 175 | 176 | /** 177 | * List all running tasks for application id. 178 | */ 179 | 180 | App.prototype.tasks = function(opts, callback) { 181 | if (typeof opts === 'string') { 182 | opts = { id: opts }; 183 | } 184 | 185 | var req = { 186 | name: 'app.tasks', 187 | path: '/apps/{id}/tasks', 188 | params: { id: opts.id }, 189 | }; 190 | 191 | if (!opts.id) return callback(this.marathon._err('id required', req)); 192 | 193 | this.marathon._get(req, utils.bodyItem('tasks'), callback); 194 | }; 195 | 196 | /** 197 | * Kill tasks that belong to the application id. 198 | */ 199 | 200 | App.prototype.kill = function(opts, callback) { 201 | if (typeof opts === 'string') { 202 | opts = { id: opts }; 203 | } 204 | 205 | var req = { 206 | name: 'app.kill', 207 | path: '/apps/{id}/tasks', 208 | params: { id: opts.id }, 209 | query: {}, 210 | }; 211 | 212 | try { 213 | if (!opts.id) throw new Error('id required'); 214 | 215 | if (opts.task) { 216 | req.path += '/{task}'; 217 | req.params.task = opts.task; 218 | if (opts.host) throw new Error('host invalid with task'); 219 | } 220 | 221 | if (opts.host) req.query.host = opts.host; 222 | if (opts.scale) req.query.scale = opts.scale ? 'true' : 'false'; 223 | } catch (err) { 224 | return callback(this.marathon._err(err, req)); 225 | } 226 | 227 | this.marathon._delete(req, utils.bodyItem('tasks'), callback); 228 | }; 229 | 230 | /** 231 | * Module Exports. 232 | */ 233 | 234 | exports.App = App; 235 | -------------------------------------------------------------------------------- /lib/marathon/event_subscription.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Event subscription client 3 | */ 4 | 5 | 'use strict'; 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var utils = require('../utils'); 12 | 13 | /** 14 | * Initialize a new `EventSubscription` client. 15 | */ 16 | 17 | function EventSubscription(marathon) { 18 | this.marathon = marathon; 19 | } 20 | 21 | /** 22 | * Register a callback URL as an event subscriber. 23 | */ 24 | 25 | EventSubscription.prototype.register = function(opts, callback) { 26 | if (typeof opts === 'string') { 27 | opts = { url: opts }; 28 | } 29 | 30 | var req = { 31 | name: 'eventSubscription.register', 32 | path: '/eventSubscriptions', 33 | query: { callbackUrl: opts.url }, 34 | }; 35 | 36 | if (!opts.url) return callback(this.marathon._err('url required', req)); 37 | 38 | this.marathon._post(req, utils.empty, callback); 39 | }; 40 | 41 | /** 42 | * List all event subscriber callback URLs. 43 | */ 44 | 45 | EventSubscription.prototype.list = function(opts, callback) { 46 | if (typeof opts === 'function') { 47 | callback = opts; 48 | opts = {}; 49 | } 50 | 51 | var req = { 52 | name: 'eventSubscription.list', 53 | path: '/eventSubscriptions', 54 | }; 55 | 56 | this.marathon._get(req, utils.bodyItem('callbackUrls'), callback); 57 | }; 58 | 59 | /** 60 | * Unregister a callback URL from the event subscribers list. 61 | */ 62 | 63 | EventSubscription.prototype.unregister = function(opts, callback) { 64 | if (typeof opts === 'string') { 65 | opts = { url: opts }; 66 | } 67 | 68 | var req = { 69 | name: 'eventSubscription.unregister', 70 | path: '/eventSubscriptions', 71 | query: { callbackUrl: opts.url }, 72 | }; 73 | 74 | if (!opts.url) return callback(this.marathon._err('url required', req)); 75 | 76 | this.marathon._delete(req, utils.empty, callback); 77 | }; 78 | 79 | /** 80 | * Module Exports. 81 | */ 82 | 83 | exports.EventSubscription = EventSubscription; 84 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Body 5 | */ 6 | 7 | function body(ctx, next) { 8 | if (ctx.err) return next(ctx.err); 9 | 10 | next(false, null, ctx.res.body); 11 | } 12 | 13 | /** 14 | * Body default 15 | */ 16 | 17 | function bodyDefault(value) { 18 | return function(ctx, next) { 19 | if (ctx.err) return next(ctx.err); 20 | 21 | next(false, null, ctx.res.body || value); 22 | }; 23 | } 24 | 25 | /** 26 | * Body item 27 | */ 28 | 29 | function bodyItem(key) { 30 | return function(ctx, next) { 31 | if (ctx.err) return next(ctx.err); 32 | 33 | next(false, null, ctx.res.body[key]); 34 | }; 35 | } 36 | 37 | /** 38 | * Empty 39 | */ 40 | 41 | function empty(ctx, next) { 42 | if (ctx.err) return next(ctx.err); 43 | 44 | next(false); 45 | } 46 | 47 | /** 48 | * Module exports 49 | */ 50 | 51 | exports.body = body; 52 | exports.bodyDefault = bodyDefault; 53 | exports.bodyItem = bodyItem; 54 | exports.empty = empty; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mesos", 3 | "version": "0.7.0", 4 | "description": "Mesos clients", 5 | "main": "lib/index.js", 6 | "dependencies": { 7 | "papi": "^0.15.0" 8 | }, 9 | "devDependencies": { 10 | "async": "^0.9.0", 11 | "debug": "^2.0.0", 12 | "istanbul": "^0.3.2", 13 | "jscs": "^1.6.2", 14 | "jshint": "^2.5.5", 15 | "lodash": "^2.4.1", 16 | "mocha": "^1.21.0", 17 | "node-uuid": "^1.4.1", 18 | "should": "^4.0.4" 19 | }, 20 | "scripts": { 21 | "cover": "./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- --recursive --check-leaks --timeout 15000 && open coverage/lcov-report/index.html", 22 | "test": "./node_modules/.bin/jshint lib test && ./node_modules/.bin/jscs lib test && ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- --recursive --check-leaks --timeout 15000" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/silas/node-mesos.git" 27 | }, 28 | "keywords": [ 29 | "chronos", 30 | "client", 31 | "marathon", 32 | "mesos" 33 | ], 34 | "author": "Silas Sewell ", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/silas/node-mesos/issues" 38 | }, 39 | "homepage": "https://github.com/silas/node-mesos" 40 | } 41 | -------------------------------------------------------------------------------- /test/chronos.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var async = require('async'); 8 | var lodash = require('lodash'); 9 | var should = require('should'); 10 | var uuid = require('node-uuid'); 11 | 12 | var mesos = require('../lib'); 13 | 14 | var helper = require('./helper'); 15 | 16 | /** 17 | * Tests 18 | */ 19 | 20 | describe('Chronos', function() { 21 | before(function() { 22 | var self = this; 23 | 24 | self.chronos = mesos.Chronos({ 25 | host: process.env.VM_HOST || '10.141.141.10', 26 | }); 27 | self.chronos.on('log', helper.debug('mesos:chronos')); 28 | 29 | self.exists = function(name, cb) { 30 | self.chronos.job.list(function(err, data) { 31 | if (err) return cb(err); 32 | 33 | var exists = data.some(function(job) { 34 | return job.name === name; 35 | }); 36 | 37 | cb(null, exists); 38 | }); 39 | }; 40 | }); 41 | 42 | beforeEach(function(done) { 43 | var self = this; 44 | 45 | self.name = 'test-' + uuid.v4(); 46 | self.owner = 'owner@example.org'; 47 | 48 | var jobs = {}; 49 | 50 | jobs.create = function(cb) { 51 | var opts = { 52 | schedule: 'R10/2012-10-01T05:52:00Z/PT2S', 53 | name: self.name, 54 | epsilon: 'PT15M', 55 | command: 'true', 56 | owner: self.owner, 57 | async: false, 58 | }; 59 | 60 | self.chronos.job.create(opts, cb); 61 | }; 62 | 63 | async.auto(jobs, done); 64 | }); 65 | 66 | afterEach(function(done) { 67 | var self = this; 68 | 69 | self.chronos.job.list(function(err, data) { 70 | var names = data.map(function(job) { 71 | return job.name; 72 | }).filter(function(name) { 73 | return name.match(/^test-.*/); 74 | }); 75 | 76 | async.map(names, self.chronos.job.destroy.bind(self.chronos.job), done); 77 | }); 78 | }); 79 | 80 | it('should return jobs', function(done) { 81 | var self = this; 82 | 83 | self.chronos.job.list(function(err, data) { 84 | should.not.exist(err); 85 | 86 | should(data).be.instanceof(Array); 87 | 88 | var job = lodash.find(data, function(job) { 89 | return self.name === job.name; 90 | }); 91 | 92 | should.exist(job); 93 | 94 | job.name.should.eql(self.name); 95 | job.owner.should.eql(self.owner); 96 | job.disabled.should.eql(false); 97 | 98 | done(); 99 | }); 100 | }); 101 | 102 | it('should create job', function(done) { 103 | var opts = { 104 | schedule: 'R10/2012-10-01T05:52:00Z/PT2S', 105 | name: 'test-' + uuid.v4(), 106 | epsilon: 'PT15M', 107 | command: 'true', 108 | owner: 'owner@example.org', 109 | async: false, 110 | }; 111 | 112 | this.chronos.job.create(opts, function(err) { 113 | should.not.exist(err); 114 | 115 | done(); 116 | }); 117 | }); 118 | 119 | it('should delete job', function(done) { 120 | var self = this; 121 | 122 | var jobs = {}; 123 | 124 | jobs.before = function(cb) { 125 | self.exists(self.name, cb); 126 | }; 127 | 128 | jobs.destroy = ['before', function(cb) { 129 | self.chronos.job.destroy(self.name, cb); 130 | }]; 131 | 132 | jobs.after = ['destroy', function(cb) { 133 | self.exists(self.name, cb); 134 | }]; 135 | 136 | async.auto(jobs, function(err, results) { 137 | if (err) return done(err); 138 | 139 | delete results.destroy; 140 | 141 | results.should.eql({ 142 | before: true, 143 | after: false, 144 | }); 145 | 146 | done(); 147 | }); 148 | }); 149 | 150 | it('should start job', function(done) { 151 | var self = this; 152 | 153 | self.chronos.job.start(self.name, function(err) { 154 | should.not.exist(err); 155 | 156 | done(); 157 | }); 158 | }); 159 | 160 | it('should return job stats', function(done) { 161 | var self = this; 162 | 163 | self.chronos.job.stats(self.name, function(err, data) { 164 | should.not.exist(err); 165 | 166 | should.exist(data); 167 | 168 | data.should.have.keys( 169 | '75thPercentile', 170 | '95thPercentile', 171 | '98thPercentile', 172 | '99thPercentile', 173 | 'median', 174 | 'mean', 175 | 'count' 176 | ); 177 | 178 | done(); 179 | }); 180 | }); 181 | 182 | it('should return stat for all jobs', function(done) { 183 | var self = this; 184 | 185 | var opts = { 186 | percentile: 'mean', 187 | }; 188 | 189 | self.chronos.job.stats(opts, function(err, data) { 190 | should.not.exist(err); 191 | 192 | should.exist(data); 193 | 194 | var count = 0; 195 | 196 | data.forEach(function(job) { 197 | job.should.have.keys( 198 | 'jobNameLabel', 199 | 'time' 200 | ); 201 | 202 | if (job.jobNameLabel === self.name) count++; 203 | }); 204 | 205 | count.should.be.above(0); 206 | 207 | done(); 208 | }); 209 | }); 210 | 211 | it('should return jobs with search restrictions', function(done) { 212 | var self = this; 213 | 214 | self.chronos.job.search(self.name, function(err, data) { 215 | should.not.exist(err); 216 | 217 | should(data).be.instanceof(Array); 218 | 219 | data.length.should.equal(1); 220 | 221 | data[0].name.should.eql(self.name); 222 | data[0].owner.should.eql(self.owner); 223 | data[0].disabled.should.eql(false); 224 | 225 | done(); 226 | }); 227 | }); 228 | 229 | it('should update task', function(done) { 230 | var self = this; 231 | 232 | var opts = { 233 | id: '123', 234 | statusCode: 0, 235 | }; 236 | 237 | self.chronos.task.update(opts, function(err) { 238 | should.not.exist(err); 239 | 240 | done(); 241 | }); 242 | }); 243 | 244 | it('should kill task', function(done) { 245 | var self = this; 246 | 247 | var opts = { job: self.name }; 248 | 249 | self.chronos.task.kill(opts, function(err) { 250 | should.not.exist(err); 251 | 252 | done(); 253 | }); 254 | }); 255 | }); 256 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | require('should'); 8 | 9 | var marathon = require('./helper/marathon'); 10 | 11 | /** 12 | * Buffer to string 13 | */ 14 | 15 | function bufferToString(value) { 16 | if (!value) return value; 17 | 18 | if (Buffer.isBuffer(value)) return value.toString(); 19 | 20 | if (Array.isArray(value)) { 21 | return value.map(bufferToString); 22 | } 23 | 24 | if (typeof value === 'object') { 25 | Object.keys(value).forEach(function(key) { 26 | value[key] = bufferToString(value[key]); 27 | }); 28 | } 29 | 30 | return value; 31 | } 32 | 33 | /** 34 | * Debug (convert buffers to strings) 35 | */ 36 | 37 | function debugBuffer(name) { 38 | var debug = require('debug')(name); 39 | 40 | return function() { 41 | debug.apply(debug, bufferToString(Array.prototype.slice.call(arguments))); 42 | }; 43 | } 44 | 45 | /** 46 | * Module Exports. 47 | */ 48 | 49 | exports.debug = debugBuffer; 50 | exports.marathon = marathon; 51 | -------------------------------------------------------------------------------- /test/helper/marathon.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | require('should'); 8 | 9 | var async = require('async'); 10 | 11 | /** 12 | * Wait on task 13 | */ 14 | 15 | function waitOnTask(marathon, id, wantExists, callback) { 16 | async.retry( 17 | 100, 18 | function(cb) { 19 | marathon.app.tasks(id, function(err, data) { 20 | var dataExists = !!(data && data.length); 21 | 22 | if (err || dataExists !== wantExists) { 23 | if (!err) { 24 | err = new Error('Task ' + (wantExists ? 'not found' : 'exists')); 25 | } 26 | return setTimeout(function() { cb(err); }, 100); 27 | } 28 | 29 | cb(null, data); 30 | }); 31 | }, 32 | callback 33 | ); 34 | } 35 | 36 | /** 37 | * Clean 38 | */ 39 | 40 | function clean(test, callback) { 41 | var jobs = {}; 42 | 43 | jobs.eventSubscriptions = function(cb) { 44 | test.marathon.eventSubscription.unregister(test.callbackUrl, cb); 45 | }; 46 | 47 | jobs.list = test.marathon.app.list.bind(test); 48 | 49 | jobs.destroy = ['list', function(cb, results) { 50 | var ids = results.list.map(function(app) { 51 | return app.id; 52 | }).filter(function(id) { 53 | return id.match(/^test-/); 54 | }); 55 | 56 | if (!ids.length) return cb(); 57 | 58 | async.map(ids, test.marathon.app.destroy.bind(test), cb); 59 | }]; 60 | 61 | async.auto(jobs, callback); 62 | } 63 | 64 | /** 65 | * Module Exports. 66 | */ 67 | 68 | exports.waitOnTask = waitOnTask; 69 | exports.clean = clean; 70 | -------------------------------------------------------------------------------- /test/marathon.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var async = require('async'); 8 | var http = require('http'); 9 | var should = require('should'); 10 | var uuid = require('node-uuid'); 11 | 12 | var mesos = require('../lib'); 13 | 14 | var helper = require('./helper'); 15 | 16 | /** 17 | * Tests 18 | */ 19 | 20 | describe('Marathon', function() { 21 | before(function(done) { 22 | var self = this; 23 | 24 | self.marathon = mesos.Marathon({ 25 | host: process.env.VM_HOST || '10.141.141.10', 26 | }); 27 | self.marathon.on('log', helper.debug('mesos:marathon')); 28 | 29 | self.id = 'test-' + uuid.v4(); 30 | 31 | var opts = { 32 | id: self.id, 33 | cmd: 'sleep 300', 34 | cpus: 1, 35 | mem: 16, 36 | instances: 1, 37 | }; 38 | 39 | self.events = []; 40 | 41 | self.http = http.createServer(function(req, res) { 42 | var chunks = []; 43 | var bodyLength = 0; 44 | 45 | req.on('data', function(chunk) { 46 | chunks.push(chunk); 47 | bodyLength += chunk.length; 48 | }); 49 | 50 | req.on('end', function() { 51 | var body = Buffer.concat(chunks, bodyLength).toString(); 52 | 53 | self.events.push({ 54 | path: req.url, 55 | body: body, 56 | }); 57 | 58 | res.writeHead(200); 59 | res.end(); 60 | }); 61 | }); 62 | 63 | var eventHost = process.env.EVENT_HOST || '10.141.141.1'; 64 | var eventPort = process.env.EVENT_PORT || 8088; 65 | 66 | self.callbackUrl = 'http://' + eventHost + ':' + eventPort + '/callback'; 67 | 68 | var jobs = {}; 69 | 70 | jobs.httpListen = function(cb) { 71 | self.http.listen(eventPort, eventHost, cb); 72 | }; 73 | 74 | jobs.clean = function(cb) { 75 | helper.marathon.clean(self, cb); 76 | }; 77 | 78 | jobs.app = ['clean', function(cb) { 79 | self.marathon.app.create(opts, function(err) { 80 | should.not.exist(err); 81 | 82 | cb(); 83 | }); 84 | }]; 85 | 86 | async.auto(jobs, done); 87 | }); 88 | 89 | after(function(done) { 90 | this.http.close(); 91 | 92 | helper.marathon.clean(this, done); 93 | }); 94 | 95 | it('should handle event subscription', function(done) { 96 | var self = this; 97 | 98 | var id = 'test-' + uuid.v4(); 99 | 100 | var jobs = []; 101 | 102 | jobs.push(function(cb) { 103 | self.marathon.eventSubscription.list(function(err, data) { 104 | should.not.exist(err); 105 | 106 | should(data).be.instanceof(Array); 107 | 108 | data.should.not.containEql(self.callbackUrl); 109 | 110 | cb(); 111 | }); 112 | }); 113 | 114 | jobs.push(function(cb) { 115 | self.marathon.eventSubscription.register(self.callbackUrl, cb); 116 | }); 117 | 118 | jobs.push(function(cb) { 119 | self.marathon.eventSubscription.list(function(err, data) { 120 | should.not.exist(err); 121 | 122 | should(data).be.instanceof(Array); 123 | 124 | data.should.containEql(self.callbackUrl); 125 | 126 | cb(); 127 | }); 128 | }); 129 | 130 | jobs.push(function(cb) { 131 | var opts = { 132 | id: id, 133 | cmd: 'sleep 123', 134 | cpus: 1, 135 | mem: 16, 136 | instances: 1, 137 | }; 138 | 139 | self.marathon.app.create(opts, function(err) { 140 | should.not.exist(err); 141 | 142 | cb(); 143 | }); 144 | }); 145 | 146 | jobs.push(function(cb) { 147 | async.retry( 148 | 100, 149 | function(cb) { 150 | if (!self.events.length) { 151 | var err = new Error('No events found'); 152 | return setTimeout(function() { cb(err); }, 100); 153 | } 154 | 155 | cb(); 156 | }, 157 | cb 158 | ); 159 | }); 160 | 161 | async.series(jobs, function(err) { 162 | should.not.exist(err); 163 | 164 | var events = self.events; 165 | 166 | events.should.not.eql([]); 167 | 168 | events = events.filter(function(res) { 169 | var data = res && res.body && JSON.parse(res.body); 170 | 171 | return data.eventType === 'api_post_event' && data.appDefinition && 172 | data.appDefinition.id === id; 173 | }); 174 | 175 | events.should.not.eql([]); 176 | 177 | done(); 178 | }); 179 | }); 180 | 181 | it('should return all tasks', function(done) { 182 | var self = this; 183 | 184 | var jobs = []; 185 | 186 | jobs.push(function(cb) { 187 | helper.marathon.waitOnTask(self.marathon, self.id, true, cb); 188 | }); 189 | 190 | jobs.push(function(cb) { 191 | self.marathon.tasks(function(err, data) { 192 | should.not.exist(err); 193 | 194 | should(data).be.instanceof(Array); 195 | 196 | var task = data.filter(function(task) { 197 | return task.appId === self.id; 198 | })[0]; 199 | 200 | should.exist(task); 201 | 202 | task.should.have.properties('appId', 'id'); 203 | 204 | task.appId.should.eql(self.id); 205 | 206 | cb(); 207 | }); 208 | }); 209 | 210 | async.series(jobs, done); 211 | }); 212 | 213 | it('should create app', function(done) { 214 | var self = this; 215 | 216 | var id = 'test-' + uuid.v4(); 217 | 218 | var jobs = []; 219 | 220 | jobs.push(function(cb) { 221 | var opts = { 222 | id: id, 223 | cmd: 'sleep 300', 224 | cpus: 1, 225 | mem: 16, 226 | instances: 1, 227 | }; 228 | 229 | self.marathon.app.create(opts, function(err) { 230 | should.not.exist(err); 231 | 232 | cb(); 233 | }); 234 | }); 235 | 236 | jobs.push(function(cb) { 237 | self.marathon.app.get(id, function(err, data) { 238 | should.not.exist(err); 239 | 240 | should.exist(data); 241 | data.id.should.eql(id); 242 | 243 | cb(); 244 | }); 245 | }); 246 | 247 | async.series(jobs, done); 248 | }); 249 | 250 | it('should return running apps', function(done) { 251 | var self = this; 252 | 253 | self.marathon.app.list(function(err, data) { 254 | should.not.exist(err); 255 | 256 | should(data).be.instanceof(Array); 257 | data.length.should.be.above(0); 258 | 259 | data.map(function(app) { 260 | return app.id; 261 | }).should.containEql(self.id); 262 | 263 | done(); 264 | }); 265 | }); 266 | 267 | it('should return running apps filtered by cmd', function(done) { 268 | var self = this; 269 | 270 | this.marathon.app.list({ cmd: 'sleep' }, function(err, data) { 271 | should.not.exist(err); 272 | 273 | should(data).be.instanceof(Array); 274 | data.length.should.be.above(0); 275 | 276 | data.map(function(app) { 277 | return app.id; 278 | }).should.containEql(self.id); 279 | 280 | done(); 281 | }); 282 | }); 283 | 284 | it('should not return running apps filtered by cmd', function(done) { 285 | this.marathon.app.list({ cmd: 'notfound' }, function(err, data) { 286 | should.not.exist(err); 287 | 288 | data.should.eql([]); 289 | 290 | done(); 291 | }); 292 | }); 293 | 294 | it('should return running app', function(done) { 295 | var self = this; 296 | 297 | self.marathon.app.get(self.id, function(err, data) { 298 | should.not.exist(err); 299 | 300 | should.exist(data); 301 | 302 | data.should.have.properties('id', 'cmd'); 303 | 304 | data.id.should.eql(self.id); 305 | 306 | done(); 307 | }); 308 | }); 309 | 310 | it('should return app versions', function(done) { 311 | this.marathon.app.versions(this.id, function(err, data) { 312 | should.not.exist(err); 313 | 314 | should(data).be.instanceof(Array); 315 | data.length.should.be.above(0); 316 | 317 | done(); 318 | }); 319 | }); 320 | 321 | it('should return app version', function(done) { 322 | var self = this; 323 | 324 | var jobs = []; 325 | 326 | var opts = { id: self.id }; 327 | 328 | jobs.push(function(cb) { 329 | self.marathon.app.versions(self.id, function(err, data) { 330 | should.not.exist(err); 331 | 332 | should(data).be.instanceof(Array); 333 | data.length.should.be.above(0); 334 | 335 | opts.version = data[0]; 336 | 337 | cb(); 338 | }); 339 | }); 340 | 341 | jobs.push(function(cb) { 342 | self.marathon.app.version(opts, function(err, data) { 343 | should.not.exist(err); 344 | 345 | should.exist(data); 346 | 347 | data.should.have.properties('id', 'cmd'); 348 | 349 | data.id.should.eql(self.id); 350 | 351 | cb(); 352 | }); 353 | }); 354 | 355 | async.series(jobs, done); 356 | }); 357 | 358 | it('should update app', function(done) { 359 | var self = this; 360 | 361 | var jobs = []; 362 | 363 | var opts = { 364 | id: self.id, 365 | cmd: 'sleep 60', 366 | cpus: 2, 367 | instances: 2, 368 | }; 369 | 370 | jobs.push(function(cb) { 371 | self.marathon.app.update(opts, function(err) { 372 | should.not.exist(err); 373 | 374 | cb(); 375 | }); 376 | }); 377 | 378 | jobs.push(function(cb) { 379 | self.marathon.app.get(self.id, function(err, data) { 380 | should.not.exist(err); 381 | 382 | should.exist(data); 383 | 384 | var keys = Object.keys(opts); 385 | 386 | should(data).have.properties(keys); 387 | 388 | keys.forEach(function(key) { 389 | data[key].should.eql(opts[key]); 390 | }); 391 | 392 | cb(); 393 | }); 394 | }); 395 | 396 | async.series(jobs, done); 397 | }); 398 | 399 | it('should destroy app', function(done) { 400 | var self = this; 401 | 402 | var id = 'test-' + uuid.v4(); 403 | 404 | var jobs = []; 405 | 406 | jobs.push(function(cb) { 407 | var opts = { 408 | id: id, 409 | cmd: 'sleep 300', 410 | cpus: 1, 411 | mem: 16, 412 | instances: 1, 413 | }; 414 | 415 | self.marathon.app.create(opts, cb); 416 | }); 417 | 418 | jobs.push(function(cb) { 419 | self.marathon.app.get(id, cb); 420 | }); 421 | 422 | jobs.push(function(cb) { 423 | self.marathon.app.destroy(id, function(err) { 424 | should.not.exist(err); 425 | 426 | cb(); 427 | }); 428 | }); 429 | 430 | jobs.push(function(cb) { 431 | self.marathon.app.get(id, function(err) { 432 | should.exist(err); 433 | err.message.should.eql('marathon: app.get: not found'); 434 | 435 | cb(); 436 | }); 437 | }); 438 | 439 | async.series(jobs, done); 440 | }); 441 | 442 | it('should return tasks', function(done) { 443 | var self = this; 444 | 445 | helper.marathon.waitOnTask(self.marathon, self.id, true, 446 | function(err, data) { 447 | should.not.exist(err); 448 | 449 | should(data).be.instanceof(Array); 450 | data.length.should.be.above(0); 451 | 452 | var task = data[0]; 453 | 454 | task.should.have.properties( 455 | 'appId', 456 | 'id', 457 | 'host', 458 | 'ports', 459 | 'version' 460 | ); 461 | 462 | task.appId.should.eql(self.id); 463 | 464 | done(); 465 | }); 466 | }); 467 | 468 | it('should kill task', function(done) { 469 | var self = this; 470 | 471 | var jobs = []; 472 | 473 | jobs.task = function(cb) { 474 | helper.marathon.waitOnTask(self.marathon, self.id, true, 475 | function(err, data) { 476 | should.not.exist(err); 477 | 478 | should(data).be.instanceof(Array); 479 | data.length.should.be.above(0); 480 | 481 | var task = data[0]; 482 | 483 | task.should.have.property('id'); 484 | 485 | cb(null, task); 486 | }); 487 | }; 488 | 489 | jobs.kill = ['task', function(cb, results) { 490 | var opts = { 491 | id: self.id, 492 | task: results.task.id, 493 | }; 494 | 495 | self.marathon.app.kill(opts, function(err) { 496 | should.not.exist(err); 497 | 498 | cb(); 499 | }); 500 | }]; 501 | 502 | jobs.wait = ['kill', function(cb) { 503 | helper.marathon.waitOnTask(self.marathon, self.id, false, cb); 504 | }]; 505 | 506 | jobs.check = ['wait', 'task', function(cb, results) { 507 | self.marathon.app.tasks(self.id, function(err, data) { 508 | should.not.exist(err); 509 | 510 | should(data).be.instanceof(Array); 511 | 512 | var tasks = data.filter(function(task) { 513 | return task.id === results.task.id; 514 | }); 515 | 516 | tasks.should.eql([]); 517 | 518 | cb(); 519 | }); 520 | }]; 521 | 522 | async.auto(jobs, done); 523 | }); 524 | }); 525 | --------------------------------------------------------------------------------