marathon-scooter 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|