├── .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 |
--------------------------------------------------------------------------------