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 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: