marathon-scooter 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,9 @@
1
+ class Hash
2
+ def except(*blacklist)
3
+ self.reject {|key, value| blacklist.include?(key.to_s) }
4
+ end
5
+
6
+ def only(*whitelist)
7
+ self.reject {|key, value| !whitelist.include?(key.to_s) }
8
+ end
9
+ 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
@@ -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
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe Scooter do
4
+ it 'has a version number' do
5
+ expect(Scooter::Version::STRING.dup).not_to be nil
6
+ end
7
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'scooter'
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: