marathon-scooter 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +1 -0
- data/LICENSE +22 -0
- data/README.md +94 -0
- data/bin/scooter +22 -0
- data/lib/extensions/hash.rb +9 -0
- data/lib/extensions/marathon/app.rb +83 -0
- data/lib/scooter.rb +37 -0
- data/lib/scooter/command.rb +26 -0
- data/lib/scooter/commands/app.rb +29 -0
- data/lib/scooter/commands/clean.rb +52 -0
- data/lib/scooter/commands/delete.rb +31 -0
- data/lib/scooter/commands/export.rb +53 -0
- data/lib/scooter/commands/info.rb +33 -0
- data/lib/scooter/commands/scale.rb +27 -0
- data/lib/scooter/commands/sync.rb +73 -0
- data/lib/scooter/commands/tidy.rb +38 -0
- data/lib/scooter/exceptions.rb +1 -0
- data/lib/scooter/gli.rb +267 -0
- data/lib/scooter/ui.rb +88 -0
- data/lib/scooter/version.rb +28 -0
- data/spec/scooter_spec.rb +7 -0
- data/spec/spec_helper.rb +2 -0
- metadata +210 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8a9e6da40cf705b812b2eda984cb2d7f11c7c7fe
|
4
|
+
data.tar.gz: 60b3b689dbded24a6133867de8c2382a9d57e798
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b92136c7742d8935ce94dcd5b9a111926bffc2309041e2cada4580a636dbb7e69748a477b0013e3676b1ca2c07fe5b51799a1031426ad6b16879664458999056
|
7
|
+
data.tar.gz: 8806ead8554859080f7b660bda1c583548badfa3b5144590fa4db162200cbd7e0ca580ede6a96f46961cb7257211dfafee22d6531143fe09c2a7ee45a830e279
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
## 0.1.0 -- Initial Public Release
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Yieldbot
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
22
|
+
|
data/README.md
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# Scooter
|
2
|
+
|
3
|
+
A CLI for the Marathon Rest API, with some opinionated configuration management of Marathon jobs.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
````
|
8
|
+
gem install marathon-scooter
|
9
|
+
````
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
To see the a complete set of global and command specific command line arguments you can use the following:
|
14
|
+
|
15
|
+
`scooter --help`
|
16
|
+
|
17
|
+
and
|
18
|
+
|
19
|
+
`scooter COMMAND --help`
|
20
|
+
|
21
|
+
## Commands
|
22
|
+
|
23
|
+
### app
|
24
|
+
|
25
|
+
This command retrieves the configuration for a given application id, including the option of a specific application version, and output as JSON.
|
26
|
+
|
27
|
+
### clean
|
28
|
+
|
29
|
+
This command will remove job configurations *FROM* Marathon that do not exist within a given directory.
|
30
|
+
|
31
|
+
*Note: This command is destructive and requires an additional flag to execute the clean*
|
32
|
+
|
33
|
+
### delete
|
34
|
+
|
35
|
+
This command will delete the job configuration *FROM* Marathon for a given application id.
|
36
|
+
|
37
|
+
*Note: This command is destructive and requires an additional flag to execute the delete*
|
38
|
+
|
39
|
+
### export
|
40
|
+
|
41
|
+
This command provides a method of exporting Marathon job configurations to given directory. By default all jobs are exported, however, a regex can be provided to export a certain subset as needed.
|
42
|
+
|
43
|
+
### help
|
44
|
+
|
45
|
+
This command provides general usage information.
|
46
|
+
|
47
|
+
### info
|
48
|
+
|
49
|
+
This command retrieves basic Marathon and job configuration data and presents it to the user.
|
50
|
+
|
51
|
+
### scale
|
52
|
+
|
53
|
+
This command scales the number of instances of a given application. Setting instances to 0 (zero) will suspend the application.
|
54
|
+
|
55
|
+
### sync
|
56
|
+
|
57
|
+
This command will sync the given application file or directory with the Marathon. Marathon will update the application configuration if the application already exists, otherwise, it will createa a new application.
|
58
|
+
|
59
|
+
### tidy
|
60
|
+
|
61
|
+
This command will clean up the JSON for a given file or directory of files, removing any unnecessary configuration, and sorting the keys to reduce file differences when storing job configuration in Git.
|
62
|
+
|
63
|
+
## Environment Variables
|
64
|
+
|
65
|
+
Scooter provides the ability to set global options via environment variables for the following:
|
66
|
+
|
67
|
+
SCOOTER_COLOR
|
68
|
+
SCOOTER_MARATHON_HOST
|
69
|
+
SCOOTER_MARATHON_USER
|
70
|
+
SCOOTER_MARATHON_PASS
|
71
|
+
SCOOTER_MARATHON_PROXY_HOST
|
72
|
+
SCOOTER_MARATHON_PROXY_PORT
|
73
|
+
SCOOTER_MARATHON_PROXY_USER
|
74
|
+
SCOOTER_MARATHON_PROXY_PASS
|
75
|
+
SCOOTER_VERBOSE
|
76
|
+
|
77
|
+
## Examples
|
78
|
+
|
79
|
+
### General
|
80
|
+
|
81
|
+
The following command will retrieve general Marathon information.
|
82
|
+
|
83
|
+
````
|
84
|
+
scooter info
|
85
|
+
````
|
86
|
+
|
87
|
+
### Specific Marathon Host
|
88
|
+
|
89
|
+
By default Scooter looks for Marathon on localhost and provides an option to specify what Marathon host to target:
|
90
|
+
|
91
|
+
````
|
92
|
+
scooter --marathon=https://somecluster.marathon.service.consul info
|
93
|
+
````
|
94
|
+
|
data/bin/scooter
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
|
4
|
+
# Exit from interupt
|
5
|
+
Signal.trap('INT') { exit 1 }
|
6
|
+
|
7
|
+
begin
|
8
|
+
# Load the library path
|
9
|
+
lib_path = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
|
10
|
+
$LOAD_PATH.unshift(lib_path)
|
11
|
+
|
12
|
+
# Bring in the app
|
13
|
+
require 'scooter'
|
14
|
+
|
15
|
+
# Let the GLI do its thing
|
16
|
+
Kernel.exit!(Scooter::GLI.new.execute!)
|
17
|
+
rescue => e
|
18
|
+
$stderr.puts e.inspect
|
19
|
+
$stderr.puts e.message
|
20
|
+
$stderr.puts e.backtrace.join("\n")
|
21
|
+
exit e.respond_to?(:status_code) ? e.status_code : 999
|
22
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'marathon/app'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
::Marathon::App.class_eval do
|
5
|
+
|
6
|
+
# `jq 'keys'` used on a full configuration to quickly extract keys
|
7
|
+
CONFIG_KEYS = [
|
8
|
+
'acceptedResourceRoles',
|
9
|
+
'args',
|
10
|
+
'backoffFactor',
|
11
|
+
'backoffSeconds',
|
12
|
+
'cmd',
|
13
|
+
'constraints',
|
14
|
+
'container',
|
15
|
+
'cpus',
|
16
|
+
'dependencies',
|
17
|
+
'env',
|
18
|
+
'executor',
|
19
|
+
'healthChecks',
|
20
|
+
'id',
|
21
|
+
'instances',
|
22
|
+
'labels',
|
23
|
+
'maxLaunchDelaySeconds',
|
24
|
+
'mem',
|
25
|
+
'ports',
|
26
|
+
'requirePorts',
|
27
|
+
'upgradeStrategy',
|
28
|
+
'uris'
|
29
|
+
]
|
30
|
+
|
31
|
+
def filtered_info
|
32
|
+
# Remove the all non-configuration keys
|
33
|
+
sort_hash(deep_stringify_keys(info).reject { |key, value| !CONFIG_KEYS.include?(key) })
|
34
|
+
end
|
35
|
+
|
36
|
+
def filtered_info_to_json
|
37
|
+
JSON.pretty_generate(filtered_info)
|
38
|
+
end
|
39
|
+
|
40
|
+
def info_to_json
|
41
|
+
JSON.pretty_generate(sort_hash(info))
|
42
|
+
end
|
43
|
+
|
44
|
+
def write_to_file(file)
|
45
|
+
File.open(file, 'w') do |f|
|
46
|
+
f.write(filtered_info_to_json)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def deep_stringify_keys(hash)
|
53
|
+
transform_hash(hash, :deep => true) {|hash, key, value|
|
54
|
+
hash[key.to_s] = value
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def slice_hash(hash, *keys)
|
59
|
+
transform_hash(hash, :deep => false) {|hash, key, value|
|
60
|
+
hash[key] = value if keys.include?(key)
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def sort_hash(h)
|
65
|
+
{}.tap do |h2|
|
66
|
+
h.sort.each do |k,v|
|
67
|
+
h2[k] = v.is_a?(Hash) ? sort_hash(v) : v
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def transform_hash(original, options={}, &block)
|
73
|
+
original.inject({}){|result, (key,value)|
|
74
|
+
value = if (options[:deep] && Hash === value)
|
75
|
+
transform_hash(value, options, &block)
|
76
|
+
else
|
77
|
+
value
|
78
|
+
end
|
79
|
+
block.call(result,key,value)
|
80
|
+
result
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
data/lib/scooter.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# Third Party
|
4
|
+
require 'marathon'
|
5
|
+
|
6
|
+
# Internal
|
7
|
+
|
8
|
+
require 'scooter/command'
|
9
|
+
require 'scooter/exceptions'
|
10
|
+
require 'scooter/gli'
|
11
|
+
require 'scooter/ui'
|
12
|
+
require 'scooter/version'
|
13
|
+
|
14
|
+
# Commands
|
15
|
+
|
16
|
+
require 'scooter/commands/app'
|
17
|
+
require 'scooter/commands/clean'
|
18
|
+
require 'scooter/commands/delete'
|
19
|
+
require 'scooter/commands/export'
|
20
|
+
require 'scooter/commands/info'
|
21
|
+
require 'scooter/commands/scale'
|
22
|
+
require 'scooter/commands/sync'
|
23
|
+
require 'scooter/commands/tidy'
|
24
|
+
|
25
|
+
# Extensions
|
26
|
+
require 'extensions/hash'
|
27
|
+
require 'extensions/marathon/app'
|
28
|
+
|
29
|
+
module Scooter
|
30
|
+
class << self
|
31
|
+
attr_writer :ui
|
32
|
+
end
|
33
|
+
|
34
|
+
class << self
|
35
|
+
attr_reader :ui
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Scooter
|
2
|
+
class Command
|
3
|
+
attr_reader :global_options, :options
|
4
|
+
|
5
|
+
def initialize(global_options, options)
|
6
|
+
@global_options, @options = global_options, options
|
7
|
+
end
|
8
|
+
|
9
|
+
def run
|
10
|
+
fail Scooter::Exception, 'Base command has no implementation of `run`'
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
self.class.name.split('::').last.downcase
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
#
|
20
|
+
# Class Methods
|
21
|
+
#
|
22
|
+
end
|
23
|
+
|
24
|
+
# Create the commands namespace
|
25
|
+
module Commands; end
|
26
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'scooter/command'
|
2
|
+
|
3
|
+
module Scooter
|
4
|
+
module Commands
|
5
|
+
class App < Scooter::Command
|
6
|
+
def run
|
7
|
+
Scooter.ui.verbose("Executing the `#{name}` command.")
|
8
|
+
|
9
|
+
begin
|
10
|
+
if options['version']
|
11
|
+
app = ::Marathon::App.version(options['id'], options['name'])
|
12
|
+
else
|
13
|
+
app = ::Marathon::App.get(options['id'])
|
14
|
+
end
|
15
|
+
|
16
|
+
if options['json']
|
17
|
+
Scooter.ui.info(app.info_to_json)
|
18
|
+
else
|
19
|
+
Scooter.ui.info(app.to_pretty_s)
|
20
|
+
end
|
21
|
+
rescue ::Marathon::Error::NotFoundError => e
|
22
|
+
Scooter.ui.warn(e)
|
23
|
+
end
|
24
|
+
|
25
|
+
Scooter.ui.verbose("Execution of `#{name}` command has completed.")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'scooter/command'
|
2
|
+
|
3
|
+
module Scooter
|
4
|
+
module Commands
|
5
|
+
class Clean < Scooter::Command
|
6
|
+
def run
|
7
|
+
Scooter.ui.verbose("Executing the `#{name}` command.")
|
8
|
+
|
9
|
+
local_ids = []
|
10
|
+
|
11
|
+
# Build the list of files
|
12
|
+
|
13
|
+
files = Dir.glob("#{options['dir']}/*.json*")
|
14
|
+
|
15
|
+
# Build a list of app ids from the local configuration files
|
16
|
+
files.each do |f|
|
17
|
+
ext = File.extname(f).delete('.')
|
18
|
+
|
19
|
+
# Load the job configuration
|
20
|
+
begin
|
21
|
+
config = JSON.parse(IO.read(f))
|
22
|
+
rescue Exception => e
|
23
|
+
Scooter.ui.warning("Error parsing #{f}. #{e}")
|
24
|
+
next
|
25
|
+
end
|
26
|
+
|
27
|
+
local_ids << config['id']
|
28
|
+
end
|
29
|
+
|
30
|
+
# Iterate a
|
31
|
+
apps = ::Marathon::App.list
|
32
|
+
::Marathon::App.list.each do |app|
|
33
|
+
|
34
|
+
next if local_ids.include? app.id
|
35
|
+
|
36
|
+
# If delete is flagged do the actual delete
|
37
|
+
if options['delete']
|
38
|
+
|
39
|
+
# Delete the app
|
40
|
+
::Marathon::App.delete(app.id)
|
41
|
+
|
42
|
+
Scooter.ui.info("Job '#{app.id}' removed.")
|
43
|
+
else
|
44
|
+
Scooter.ui.info("[DRYRUN] Job '#{app.id}' removed.")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
Scooter.ui.verbose("Execution of `#{name}` command has completed.")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'scooter/command'
|
2
|
+
|
3
|
+
module Scooter
|
4
|
+
module Commands
|
5
|
+
class Delete < Scooter::Command
|
6
|
+
def run
|
7
|
+
Scooter.ui.verbose("Executing the `#{name}` command.")
|
8
|
+
|
9
|
+
begin
|
10
|
+
|
11
|
+
app = ::Marathon::App.get(options['id'])
|
12
|
+
|
13
|
+
# If delete is flagged do the actual delete
|
14
|
+
if options['delete']
|
15
|
+
|
16
|
+
# Delete the app
|
17
|
+
::Marathon::App.delete(app.id)
|
18
|
+
|
19
|
+
Scooter.ui.info("Job '#{app.id}' removed.")
|
20
|
+
else
|
21
|
+
Scooter.ui.info("[DRYRUN] Job '#{app.id}' removed.")
|
22
|
+
end
|
23
|
+
rescue ::Marathon::Error::NotFoundError => e
|
24
|
+
Scooter.ui.warn(e)
|
25
|
+
end
|
26
|
+
|
27
|
+
Scooter.ui.verbose("Execution of `#{name}` command has completed.")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'scooter/command'
|
2
|
+
|
3
|
+
module Scooter
|
4
|
+
module Commands
|
5
|
+
class Export < Scooter::Command
|
6
|
+
def run
|
7
|
+
Scooter.ui.verbose("Executing the `#{name}` command.")
|
8
|
+
|
9
|
+
# Convert the argument to a regex
|
10
|
+
app_regex = Regexp.new options['regex']
|
11
|
+
|
12
|
+
begin
|
13
|
+
# Iterate each of the configured apps
|
14
|
+
::Marathon::App.list.each do |app|
|
15
|
+
if app.id =~ app_regex
|
16
|
+
|
17
|
+
# Deterine the suffix
|
18
|
+
suffix = '.json'
|
19
|
+
action_suffix = ''
|
20
|
+
action_suffix += '.suspend' if app.instances == 0
|
21
|
+
|
22
|
+
# Determine the destination file
|
23
|
+
dest_file = ::File.join(options['dir'], "#{app.id}#{suffix}")
|
24
|
+
action_dest_file = dest_file + action_suffix
|
25
|
+
|
26
|
+
Scooter.ui.info("Exporting `#{app.id}` to #{action_dest_file}")
|
27
|
+
|
28
|
+
# Delete any files that are for this application
|
29
|
+
Dir.glob(dest_file + '*').each do |filename|
|
30
|
+
# Do not delete the file we are going to write to
|
31
|
+
next if filename == action_dest_file
|
32
|
+
|
33
|
+
Scooter.ui.warn(" Removing stale configuration: #{filename}")
|
34
|
+
|
35
|
+
# Delete the file
|
36
|
+
File.delete(filename)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Write the file
|
40
|
+
app.write_to_file(action_dest_file)
|
41
|
+
else
|
42
|
+
Scooter.ui.info("`#{app.id}` excluded by regex.")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
rescue ::Marathon::Error::NotFoundError => e
|
46
|
+
Scooter.ui.warn(e)
|
47
|
+
end
|
48
|
+
|
49
|
+
Scooter.ui.verbose("Execution of `#{name}` command has completed.")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'scooter/command'
|
2
|
+
require 'colorize'
|
3
|
+
|
4
|
+
module Scooter
|
5
|
+
module Commands
|
6
|
+
class Info < Scooter::Command
|
7
|
+
def run
|
8
|
+
Scooter.ui.verbose("Executing the `#{name}` command.")
|
9
|
+
Scooter.ui.announce('---- Marathon Configuration ----')
|
10
|
+
Scooter.ui.out("Name: #{::Marathon.info['name']}")
|
11
|
+
Scooter.ui.out("Checkpoint: #{::Marathon.info['marathon_config']['checkpoint']}")
|
12
|
+
Scooter.ui.out("High Availability: #{::Marathon.info['marathon_config']['ha'].to_s.green}")
|
13
|
+
Scooter.ui.out("Version: #{::Marathon.info['version']}")
|
14
|
+
Scooter.ui.out
|
15
|
+
|
16
|
+
apps = ::Marathon::App.list
|
17
|
+
if apps.length > 0
|
18
|
+
Scooter.ui.announce('---- Application Configuration ----')
|
19
|
+
::Marathon::App.list.each do |app|
|
20
|
+
# Derive the colors for the various output
|
21
|
+
read_only_color = app.read_only ? :red : :light_green
|
22
|
+
|
23
|
+
Scooter.ui.out("#{printf('%-25s', app.id)} I:#{app.instances} C:#{app.cpus} M:#{app.mem} D:#{app.disk} RO:#{Scooter.ui.color(app.read_only.to_s,read_only_color)}")
|
24
|
+
end
|
25
|
+
else
|
26
|
+
Scooter.ui.warn('There are no applications configured.')
|
27
|
+
end
|
28
|
+
|
29
|
+
Scooter.ui.verbose("Execution of `#{name}` command has completed.")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'scooter/command'
|
2
|
+
|
3
|
+
module Scooter
|
4
|
+
module Commands
|
5
|
+
class Scale < Scooter::Command
|
6
|
+
def run
|
7
|
+
Scooter.ui.verbose("Executing the `#{name}` command.")
|
8
|
+
|
9
|
+
begin
|
10
|
+
app = ::Marathon::App.get(options['id'])
|
11
|
+
|
12
|
+
if app.instances != options['instances']
|
13
|
+
Scooter.ui.info("Scaling '#{options['id']}' from #{app.instances} to #{options['instances']}...")
|
14
|
+
app.scale!(options['instances'], global_options['force'])
|
15
|
+
else
|
16
|
+
Scooter.ui.info("'#{options['id']}' instances already set to #{options['instances']}.")
|
17
|
+
end
|
18
|
+
|
19
|
+
rescue ::Marathon::Error::NotFoundError => e
|
20
|
+
Scooter.ui.warn(e)
|
21
|
+
end
|
22
|
+
|
23
|
+
Scooter.ui.verbose("Execution of `#{name}` command has completed.")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'scooter/command'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Scooter
|
5
|
+
module Commands
|
6
|
+
class Sync < Scooter::Command
|
7
|
+
def run
|
8
|
+
Scooter.ui.verbose("Executing the `#{name}` command.")
|
9
|
+
|
10
|
+
# Build the list of files
|
11
|
+
|
12
|
+
files = options['dir'].nil? ? [options['file']]: Dir.glob("#{options['dir']}/*.json*")
|
13
|
+
|
14
|
+
# Process each of the files based on file extension
|
15
|
+
files.each do |f|
|
16
|
+
ext = File.extname(f).delete('.')
|
17
|
+
|
18
|
+
# Load the job configuration
|
19
|
+
begin
|
20
|
+
config = JSON.parse(IO.read(f))
|
21
|
+
rescue Exception => e
|
22
|
+
Scooter.ui.warning("Error parsing #{f}. #{e}")
|
23
|
+
next
|
24
|
+
end
|
25
|
+
|
26
|
+
app_id = config['id']
|
27
|
+
|
28
|
+
case ext
|
29
|
+
when 'json'
|
30
|
+
begin
|
31
|
+
# Attempt to get the app from the server
|
32
|
+
app = ::Marathon::App.get(app_id)
|
33
|
+
#### TODO: Need a way to compare the config from disk to config from marathon to see if an udpate is required
|
34
|
+
Scooter.ui.info("Updating job '#{app_id}'...")
|
35
|
+
app.change!(config, global_options['force'])
|
36
|
+
rescue ::Marathon::Error::NotFoundError => e
|
37
|
+
Scooter.ui.info("Creating new job '#{app_id}'...")
|
38
|
+
# If the app isnt found, this is a new app so create the app
|
39
|
+
::Marathon::App.create(config)
|
40
|
+
end
|
41
|
+
when 'delete'
|
42
|
+
begin
|
43
|
+
# Attempt to get the app from the server
|
44
|
+
::Marathon::App.get(app_id)
|
45
|
+
Scooter.ui.info("Removing job '#{app_id}'...")
|
46
|
+
::Marathon::App.delete(app_id)
|
47
|
+
rescue ::Marathon::Error::NotFoundError => e
|
48
|
+
# This is a NO-OP
|
49
|
+
end
|
50
|
+
when 'suspend'
|
51
|
+
begin
|
52
|
+
# Attempt to get the app from the server
|
53
|
+
app = ::Marathon::App.get(app_id)
|
54
|
+
|
55
|
+
# Do nothing if instances is already zero
|
56
|
+
next if app.instances == 0
|
57
|
+
|
58
|
+
Scooter.ui.info("Suspending job '#{app_id}'...")
|
59
|
+
|
60
|
+
app.suspend!(global_options['force'])
|
61
|
+
rescue ::Marathon::Error::NotFoundError => e
|
62
|
+
# This is a NO-OP
|
63
|
+
end
|
64
|
+
else
|
65
|
+
Scooter.ui.warning("Unknown file extension for #{f}. Ignorning file.")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
Scooter.ui.verbose("Execution of `#{name}` command has completed.")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'scooter/command'
|
2
|
+
|
3
|
+
module Scooter
|
4
|
+
module Commands
|
5
|
+
class Tidy < Scooter::Command
|
6
|
+
def run
|
7
|
+
Scooter.ui.verbose("Executing the `#{name}` command.")
|
8
|
+
|
9
|
+
# Build the list of files
|
10
|
+
files = options['dir'].nil? ? [options['file']]: Dir.glob("#{options['dir']}/*.json*")
|
11
|
+
|
12
|
+
# Process each of the files
|
13
|
+
files.each do |f|
|
14
|
+
ext = File.extname(f).delete('.')
|
15
|
+
|
16
|
+
Scooter.ui.info("Tidying configuration: #{f}")
|
17
|
+
|
18
|
+
begin
|
19
|
+
# Load the job configuration
|
20
|
+
config = JSON.parse(IO.read(f))
|
21
|
+
|
22
|
+
# Create a local app object
|
23
|
+
app = Marathon::App.new(config)
|
24
|
+
|
25
|
+
# Write out the app
|
26
|
+
app.write_to_file(f)
|
27
|
+
|
28
|
+
rescue Exception => e
|
29
|
+
Scooter.ui.warning("Error parsing #{f}. #{e}")
|
30
|
+
next
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
Scooter.ui.verbose("Execution of `#{name}` command has completed.")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
class Scooter::Exception < RuntimeError; end
|
data/lib/scooter/gli.rb
ADDED
@@ -0,0 +1,267 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'gli'
|
3
|
+
require 'scooter'
|
4
|
+
|
5
|
+
module Scooter
|
6
|
+
# The GLI is the primary entry point into the app
|
7
|
+
class GLI
|
8
|
+
include ::GLI::App
|
9
|
+
|
10
|
+
APP_ID_REGEX = /^[a-zA-z0-9\.\-\/]+$/
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
program_desc 'Opinionated synchronization of Marathon jobs from JSON files.'
|
14
|
+
version Scooter::Version::STRING.dup
|
15
|
+
|
16
|
+
# Global Accepts
|
17
|
+
|
18
|
+
accept(Fixnum) do |string|
|
19
|
+
string.to_i
|
20
|
+
end
|
21
|
+
|
22
|
+
# Global Options
|
23
|
+
|
24
|
+
desc 'Enable colorized output'
|
25
|
+
switch 'color', default_value: ENV['SCOOTER_COLOR'] || true
|
26
|
+
|
27
|
+
desc 'Marathon Host'
|
28
|
+
flag 'marathon', default_value: ENV['SCOOTER_MARATHON_HOST'] || 'http://localhost:8080'
|
29
|
+
|
30
|
+
desc 'Marathon HTTP User'
|
31
|
+
flag 'user', default_value: ENV['SCOOTER_MARATHON_USER'] || nil
|
32
|
+
|
33
|
+
desc 'Marathon HTTP Password'
|
34
|
+
flag 'pass', default_value: ENV['SCOOTER_MARATHON_PASS'] || nil
|
35
|
+
|
36
|
+
desc 'HTTP Proxy Host'
|
37
|
+
flag 'proxy-host', default_value: ENV['SCOOTER_MARATHON_PROXY_HOST'] || nil
|
38
|
+
|
39
|
+
desc 'HTTP Proxy Port'
|
40
|
+
flag 'proxy-port', default_value: ENV['SCOOTER_MARATHON_PROXY_PORT'] || nil
|
41
|
+
|
42
|
+
desc 'HTTP Proxy User'
|
43
|
+
flag 'proxy-user', default_value: ENV['SCOOTER_MARATHON_PROXY_USER'] || nil
|
44
|
+
|
45
|
+
desc 'HTTP Proxy Password'
|
46
|
+
flag 'proxy-pass', default_value: ENV['SCOOTER_MARATHON_PROXY_PASS'] || nil
|
47
|
+
|
48
|
+
desc 'Enable verbose output'
|
49
|
+
switch 'verbose', default_value: ENV['SCOOTER_VERBOSE'] || false
|
50
|
+
|
51
|
+
desc 'Attempt to force the desired operation'
|
52
|
+
switch 'force', default_value: false
|
53
|
+
|
54
|
+
# Pre command execution
|
55
|
+
|
56
|
+
pre do |global_options, _command, _options, _args|
|
57
|
+
|
58
|
+
config = {}
|
59
|
+
|
60
|
+
# Basic HTTP Information
|
61
|
+
config[:username] = global_options[:user]
|
62
|
+
config[:password] = global_options[:pass]
|
63
|
+
|
64
|
+
# Basic SSL information
|
65
|
+
config[:verify] = false
|
66
|
+
|
67
|
+
# Basic Proxy information
|
68
|
+
config[:http_proxyaddr] = global_options['proxy-host']
|
69
|
+
config[:http_proxyport] = global_options['proxy-port']
|
70
|
+
config[:http_proxyuser] = global_options['proxy-user']
|
71
|
+
config[:http_proxypass] = global_options['proxy-pass']
|
72
|
+
|
73
|
+
# Set the Marathon credentials if given
|
74
|
+
::Marathon.options = config
|
75
|
+
|
76
|
+
# Set the Marathon URL
|
77
|
+
::Marathon.url = global_options[:marathon]
|
78
|
+
|
79
|
+
# Setup the UI for the app -- let it use global_options
|
80
|
+
Scooter.ui = Scooter::UI.new($stdout, $stderr, $stdin, global_options)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Post command execution
|
84
|
+
|
85
|
+
post do |_global_options, _command, _options, _args|
|
86
|
+
# Flush the output streams just in case there is anything left
|
87
|
+
$stdout.flush
|
88
|
+
$stderr.flush
|
89
|
+
end
|
90
|
+
|
91
|
+
# Error handling
|
92
|
+
|
93
|
+
on_error do |e|
|
94
|
+
#$stderr.puts e.inspect
|
95
|
+
$stderr.puts e.message
|
96
|
+
$stderr.puts e.backtrace
|
97
|
+
true
|
98
|
+
end
|
99
|
+
|
100
|
+
# Commands
|
101
|
+
|
102
|
+
desc 'Retrieve the configuration for the given app.'
|
103
|
+
command :app do |c|
|
104
|
+
|
105
|
+
c.desc 'Application ID'
|
106
|
+
c.flag [:id], required: true, must_match: APP_ID_REGEX
|
107
|
+
|
108
|
+
c.desc 'Application Version'
|
109
|
+
c.flag [:version], default: nil, type: Fixnum
|
110
|
+
|
111
|
+
c.desc 'Enable JSON output'
|
112
|
+
c.switch [:json], default_value: false
|
113
|
+
|
114
|
+
c.action do |global_options, options, _args|
|
115
|
+
Scooter::Commands::App.new(global_options, options).run
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
desc 'Remove job configurations from Marathon that do not exist in the given directory.'
|
120
|
+
command :clean do |c|
|
121
|
+
|
122
|
+
c.desc 'A job configuration directory'
|
123
|
+
c.flag [:dir, :directory]
|
124
|
+
|
125
|
+
c.desc 'Perform the actual deletion of the jobs in Marathon'
|
126
|
+
c.switch [:delete], default_value: false
|
127
|
+
|
128
|
+
c.action do |global_options, options, _args|
|
129
|
+
|
130
|
+
# Verify the source directory exists
|
131
|
+
if options['dir'] && !File.directory?(options['dir'])
|
132
|
+
Scooter.ui.error("Directory #{options['dir']} does not exist. Exiting.")
|
133
|
+
Scooter.ui.exit(1)
|
134
|
+
end
|
135
|
+
|
136
|
+
Scooter::Commands::Clean.new(global_options, options).run
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
desc 'Delete the job configuration from Marathon for the given app.'
|
141
|
+
command :delete do |c|
|
142
|
+
|
143
|
+
c.desc 'Application ID'
|
144
|
+
c.flag [:id], required: true, must_match: APP_ID_REGEX
|
145
|
+
|
146
|
+
c.desc 'Perform the actual deletion of the jobs in Marathon'
|
147
|
+
c.switch [:delete], default_value: false
|
148
|
+
|
149
|
+
c.action do |global_options, options, _args|
|
150
|
+
Scooter::Commands::Delete.new(global_options, options).run
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
desc 'Export the jobs configurations whose name matches the given regex.'
|
155
|
+
command :export do |c|
|
156
|
+
|
157
|
+
c.desc 'A job configuration directory'
|
158
|
+
c.flag [:dir, :directory]
|
159
|
+
|
160
|
+
c.desc 'Marathon Application Regex. (Note: nil implies `.*`)'
|
161
|
+
c.flag [:regex], default_value: /.*/
|
162
|
+
|
163
|
+
c.action do |global_options, options, _args|
|
164
|
+
|
165
|
+
# Verify the target directory exists
|
166
|
+
if options['dir'] && !File.directory?(options['dir'])
|
167
|
+
Scooter.ui.error("Directory #{options['dir']} does not exist. Exiting.")
|
168
|
+
Scooter.ui.exit(1)
|
169
|
+
end
|
170
|
+
|
171
|
+
Scooter::Commands::Export.new(global_options, options).run
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
desc 'Retrieve Marathon configuration information for the given credentials.'
|
176
|
+
command :info do |c|
|
177
|
+
c.action do |global_options, options, _args|
|
178
|
+
Scooter::Commands::Info.new(global_options, options).run
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
desc 'Scale the number of instances of a given app.'
|
183
|
+
command :scale do |c|
|
184
|
+
|
185
|
+
c.desc 'Application ID'
|
186
|
+
c.flag [:id], required: true, must_match: APP_ID_REGEX
|
187
|
+
|
188
|
+
c.desc 'Specify the number of instances of the application'
|
189
|
+
c.flag [:instances], required: true, type: Fixnum
|
190
|
+
|
191
|
+
c.action do |global_options, options, _args|
|
192
|
+
Scooter::Commands::Scale.new(global_options, options).run
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
desc 'Synchronize the given job configuration(s) with Marathon.'
|
197
|
+
command :sync do |c|
|
198
|
+
|
199
|
+
c.desc 'A job configuration directory'
|
200
|
+
c.flag [:dir, :directory]
|
201
|
+
|
202
|
+
c.desc 'A job configuration file'
|
203
|
+
c.flag [:file]
|
204
|
+
|
205
|
+
c.action do |global_options, options, _args|
|
206
|
+
Scooter.ui.warn('Both `dir` and `file` options given. Ignoring `file`.') if !options['dir'].nil? && !options['file'].nil?
|
207
|
+
|
208
|
+
if options['dir'].nil? && options['file'].nil?
|
209
|
+
Scooter.ui.error('Either `dir` and `file` must be used. Exiting.')
|
210
|
+
Scooter.ui.exit(1)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Verify the file exists
|
214
|
+
if options['file'] && !File.exists?(options['file'])
|
215
|
+
Scooter.ui.error("File #{options['file']} does not exist. Exiting.")
|
216
|
+
Scooter.ui.exit(1)
|
217
|
+
end
|
218
|
+
|
219
|
+
# Verify the directory exists
|
220
|
+
if options['dir'] && !File.directory?(options['dir'])
|
221
|
+
Scooter.ui.error("Directory #{options['dir']} does not exist. Exiting.")
|
222
|
+
Scooter.ui.exit(1)
|
223
|
+
end
|
224
|
+
|
225
|
+
Scooter::Commands::Sync.new(global_options, options).run
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
desc 'Tidy the JSON format for the given job configuration(s).'
|
230
|
+
command :tidy do |c|
|
231
|
+
|
232
|
+
c.desc 'A job configuration directory'
|
233
|
+
c.flag [:dir, :directory]
|
234
|
+
|
235
|
+
c.desc 'A job configuration file'
|
236
|
+
c.flag [:file]
|
237
|
+
|
238
|
+
c.action do |global_options, options, _args|
|
239
|
+
Scooter.ui.warn('Both `dir` and `file` options given. Ignoring `file`.') if !options['dir'].nil? && !options['file'].nil?
|
240
|
+
|
241
|
+
if options['dir'].nil? && options['file'].nil?
|
242
|
+
Scooter.ui.error('Either `dir` and `file` must be used. Exiting.')
|
243
|
+
Scooter.ui.exit(1)
|
244
|
+
end
|
245
|
+
|
246
|
+
# Verify the file exists
|
247
|
+
if options['file'] && !File.exists?(options['file'])
|
248
|
+
Scooter.ui.error("File #{options['file']} does not exist. Exiting.")
|
249
|
+
Scooter.ui.exit(1)
|
250
|
+
end
|
251
|
+
|
252
|
+
# Verify the directory exists
|
253
|
+
if options['dir'] && !File.directory?(options['dir'])
|
254
|
+
Scooter.ui.error("Directory #{options['dir']} does not exist. Exiting.")
|
255
|
+
Scooter.ui.exit(1)
|
256
|
+
end
|
257
|
+
|
258
|
+
Scooter::Commands::Tidy.new(global_options, options).run
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def execute!
|
264
|
+
run(ARGV)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
data/lib/scooter/ui.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'rbconfig'
|
2
|
+
require 'colorize'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
module Scooter
|
6
|
+
class UI
|
7
|
+
attr_reader :stdout
|
8
|
+
attr_reader :stderr
|
9
|
+
attr_reader :stdin
|
10
|
+
attr_reader :options
|
11
|
+
|
12
|
+
def initialize(stdout, stderr, stdin, options = {})
|
13
|
+
@stdout, @stderr, @stdin, @options = stdout, stderr, stdin, options
|
14
|
+
|
15
|
+
# Flush output immediately
|
16
|
+
stdout.sync = true
|
17
|
+
stderr.sync = true
|
18
|
+
end
|
19
|
+
|
20
|
+
def announce(message)
|
21
|
+
out("#{color(message, :green, :bold)} ")
|
22
|
+
end
|
23
|
+
|
24
|
+
def color(string, color = :light_green, style = :default)
|
25
|
+
if color?
|
26
|
+
string.colorize(color: color, mode: style)
|
27
|
+
else
|
28
|
+
string
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def err(message)
|
33
|
+
stderr.puts("#{message}") unless @options[:quiet]
|
34
|
+
end
|
35
|
+
|
36
|
+
def error(message)
|
37
|
+
err("#{color('[ERROR]', :red, :bold)} #{message}")
|
38
|
+
end
|
39
|
+
|
40
|
+
def exit(code)
|
41
|
+
Kernel.exit!(code)
|
42
|
+
end
|
43
|
+
|
44
|
+
def fatal(message)
|
45
|
+
err("#{color('[FATAL]', :red, :bold)} #{message}")
|
46
|
+
end
|
47
|
+
|
48
|
+
def info(message='',color = :light_green)
|
49
|
+
out(color(message,color))
|
50
|
+
end
|
51
|
+
|
52
|
+
def out(message='')
|
53
|
+
stdout.puts("#{message}") unless @options[:quiet]
|
54
|
+
end
|
55
|
+
|
56
|
+
def verbose(message)
|
57
|
+
out(color(message)) if @options[:verbose]
|
58
|
+
end
|
59
|
+
|
60
|
+
def warn(message)
|
61
|
+
err("#{color('[WARNING]', :yellow, :bold)} #{message}")
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def color?
|
67
|
+
@options[:color] && stdout.tty? && os != :windows
|
68
|
+
end
|
69
|
+
|
70
|
+
def os
|
71
|
+
@os ||= (
|
72
|
+
host_os = RbConfig::CONFIG['host_os']
|
73
|
+
case host_os
|
74
|
+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
75
|
+
:windows
|
76
|
+
when /darwin|mac os/
|
77
|
+
:macosx
|
78
|
+
when /linux/
|
79
|
+
:linux
|
80
|
+
when /solaris|bsd/
|
81
|
+
:unix
|
82
|
+
else
|
83
|
+
:other
|
84
|
+
end
|
85
|
+
)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
# encoding: utf-8
|
4
|
+
module Scooter
|
5
|
+
# This defines the version of the gem
|
6
|
+
module Version
|
7
|
+
MAJOR = 0
|
8
|
+
MINOR = 1
|
9
|
+
PATCH = 0
|
10
|
+
|
11
|
+
STRING = [MAJOR, MINOR, PATCH].compact.join('.')
|
12
|
+
|
13
|
+
NAME = 'Scooter'
|
14
|
+
BANNER = "#{NAME} v%s"
|
15
|
+
|
16
|
+
module_function
|
17
|
+
|
18
|
+
def version
|
19
|
+
format(BANNER, STRING)
|
20
|
+
end
|
21
|
+
|
22
|
+
def json_version
|
23
|
+
{
|
24
|
+
'version' => STRING
|
25
|
+
}.to_json
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: marathon-scooter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Yieldbot
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-08-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: colorize
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.7'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: gli
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.12'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.12'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: log4r
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.1.10
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.1.10
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: marathon-api
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.2.5
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.2.5
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bundler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.7'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.7'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ~>
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '10.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ~>
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '10.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec-mocks
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rubocop
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ~>
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 0.26.1
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ~>
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 0.26.1
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: yard
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ~>
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0.8'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ~>
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0.8'
|
153
|
+
description: Opinionated synchronization of Marathon jobs from JSON files.
|
154
|
+
email:
|
155
|
+
- devops@yieldbot.com
|
156
|
+
executables:
|
157
|
+
- scooter
|
158
|
+
extensions: []
|
159
|
+
extra_rdoc_files: []
|
160
|
+
files:
|
161
|
+
- bin/scooter
|
162
|
+
- lib/extensions/hash.rb
|
163
|
+
- lib/extensions/marathon/app.rb
|
164
|
+
- lib/scooter/command.rb
|
165
|
+
- lib/scooter/commands/app.rb
|
166
|
+
- lib/scooter/commands/clean.rb
|
167
|
+
- lib/scooter/commands/delete.rb
|
168
|
+
- lib/scooter/commands/export.rb
|
169
|
+
- lib/scooter/commands/info.rb
|
170
|
+
- lib/scooter/commands/scale.rb
|
171
|
+
- lib/scooter/commands/sync.rb
|
172
|
+
- lib/scooter/commands/tidy.rb
|
173
|
+
- lib/scooter/exceptions.rb
|
174
|
+
- lib/scooter/gli.rb
|
175
|
+
- lib/scooter/ui.rb
|
176
|
+
- lib/scooter/version.rb
|
177
|
+
- lib/scooter.rb
|
178
|
+
- spec/scooter_spec.rb
|
179
|
+
- spec/spec_helper.rb
|
180
|
+
- LICENSE
|
181
|
+
- CHANGELOG.md
|
182
|
+
- README.md
|
183
|
+
homepage: https://github.com/yieldbot/scooter
|
184
|
+
licenses:
|
185
|
+
- MIT
|
186
|
+
metadata: {}
|
187
|
+
post_install_message:
|
188
|
+
rdoc_options: []
|
189
|
+
require_paths:
|
190
|
+
- lib
|
191
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
192
|
+
requirements:
|
193
|
+
- - '>='
|
194
|
+
- !ruby/object:Gem::Version
|
195
|
+
version: '2.0'
|
196
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
197
|
+
requirements:
|
198
|
+
- - '>='
|
199
|
+
- !ruby/object:Gem::Version
|
200
|
+
version: '0'
|
201
|
+
requirements: []
|
202
|
+
rubyforge_project:
|
203
|
+
rubygems_version: 2.0.14
|
204
|
+
signing_key:
|
205
|
+
specification_version: 4
|
206
|
+
summary: Opinionated synchronization of Marathon jobs from JSON files.
|
207
|
+
test_files:
|
208
|
+
- spec/scooter_spec.rb
|
209
|
+
- spec/spec_helper.rb
|
210
|
+
has_rdoc:
|