marathon-api 0.9.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.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.cane +3 -0
  3. data/.gitignore +7 -0
  4. data/.simplecov +5 -0
  5. data/.travis.yml +10 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE +22 -0
  8. data/README.md +91 -0
  9. data/Rakefile +34 -0
  10. data/TESTING.md +49 -0
  11. data/fixtures/marathon_docker_sample.json +14 -0
  12. data/fixtures/marathon_docker_sample_2.json +14 -0
  13. data/fixtures/vcr/Marathon/_info/returns_the_info_hash.yml +30 -0
  14. data/fixtures/vcr/Marathon/_ping/ping/.yml +35 -0
  15. data/fixtures/vcr/Marathon_App/_changes/changes_the_app.yml +57 -0
  16. data/fixtures/vcr/Marathon_App/_changes/fails_with_stange_attributes.yml +32 -0
  17. data/fixtures/vcr/Marathon_App/_delete/deletes_the_app.yml +30 -0
  18. data/fixtures/vcr/Marathon_App/_delete/fails_deleting_not_existing_app.yml +30 -0
  19. data/fixtures/vcr/Marathon_App/_get/fails_getting_not_existing_app.yml +30 -0
  20. data/fixtures/vcr/Marathon_App/_get/gets_the_app.yml +30 -0
  21. data/fixtures/vcr/Marathon_App/_list/lists_apps.yml +32 -0
  22. data/fixtures/vcr/Marathon_App/_restart/fails_restarting_not_existing_app.yml +30 -0
  23. data/fixtures/vcr/Marathon_App/_start/fails_getting_not_existing_app.yml +30 -0
  24. data/fixtures/vcr/Marathon_App/_start/starts_the_app.yml +32 -0
  25. data/fixtures/vcr/Marathon_App/_tasks/has_tasks.yml +30 -0
  26. data/fixtures/vcr/Marathon_App/_version/gets_a_version.yml +61 -0
  27. data/fixtures/vcr/Marathon_App/_versions/gets_versions.yml +32 -0
  28. data/fixtures/vcr/Marathon_Deployment/_delete/deletes_deployments.yml +61 -0
  29. data/fixtures/vcr/Marathon_Deployment/_list/lists_deployments.yml +90 -0
  30. data/fixtures/vcr/Marathon_EventSubscriptions/_list/lists_callbacks.yml +30 -0
  31. data/fixtures/vcr/Marathon_EventSubscriptions/_register/registers_callback.yml +30 -0
  32. data/fixtures/vcr/Marathon_EventSubscriptions/_unregister/unregisters_callback.yml +30 -0
  33. data/fixtures/vcr/Marathon_Leader/_delete/delete/.yml +30 -0
  34. data/fixtures/vcr/Marathon_Leader/_get/get/.yml +30 -0
  35. data/fixtures/vcr/Marathon_Queue/_list/lists_queue.yml +33 -0
  36. data/fixtures/vcr/Marathon_Task/_delete/kills_a_tasks_of_an_app.yml +57 -0
  37. data/fixtures/vcr/Marathon_Task/_delete_all/kills_all_tasks_of_an_app.yml +30 -0
  38. data/fixtures/vcr/Marathon_Task/_get/gets_tasks_of_an_app.yml +30 -0
  39. data/fixtures/vcr/Marathon_Task/_list/lists_running_tasks.yml +30 -0
  40. data/fixtures/vcr/Marathon_Task/_list/lists_tasks.yml +30 -0
  41. data/lib/marathon.rb +65 -0
  42. data/lib/marathon/app.rb +200 -0
  43. data/lib/marathon/connection.rb +97 -0
  44. data/lib/marathon/deployment.rb +60 -0
  45. data/lib/marathon/error.rb +62 -0
  46. data/lib/marathon/event_subscriptions.rb +33 -0
  47. data/lib/marathon/leader.rb +19 -0
  48. data/lib/marathon/queue.rb +36 -0
  49. data/lib/marathon/task.rb +85 -0
  50. data/lib/marathon/util.rb +35 -0
  51. data/lib/marathon/version.rb +3 -0
  52. data/marathon-api.gemspec +31 -0
  53. data/spec/marathon/app_spec.rb +334 -0
  54. data/spec/marathon/connection_spec.rb +40 -0
  55. data/spec/marathon/deployment_spec.rb +95 -0
  56. data/spec/marathon/error_spec.rb +40 -0
  57. data/spec/marathon/event_subscriptions_spec.rb +37 -0
  58. data/spec/marathon/leader_spec.rb +21 -0
  59. data/spec/marathon/marathon_spec.rb +47 -0
  60. data/spec/marathon/queue_spec.rb +62 -0
  61. data/spec/marathon/task_spec.rb +100 -0
  62. data/spec/marathon/util_spec.rb +44 -0
  63. data/spec/spec_helper.rb +34 -0
  64. metadata +271 -0
@@ -0,0 +1,97 @@
1
+ # This class represents a Marathon API Connection.
2
+ class Marathon::Connection
3
+
4
+ include Marathon::Error
5
+ include HTTParty
6
+
7
+ headers(
8
+ 'Content-Type' => 'application/json',
9
+ 'Accept' => 'application/json',
10
+ 'User-Agent' => "ub0r/Marathon-API #{Marathon::VERSION}"
11
+ )
12
+
13
+ default_timeout 5
14
+ maintain_method_across_redirects
15
+
16
+ attr_reader :url
17
+
18
+ # Create a new API connection.
19
+ # ++url++: URL of the marathon API.
20
+ def initialize(url)
21
+ @url = url
22
+ end
23
+
24
+ # Delegate all HTTP methods to the #request.
25
+ [:get, :put, :post, :delete].each do |method|
26
+ define_method(method) { |*args, &block| request(method, *args) }
27
+ end
28
+
29
+ def to_s
30
+ "Marathon::Connection { :url => #{url} }"
31
+ end
32
+
33
+ private
34
+
35
+ # Create URL suffix for a hash of query parameters.
36
+ # URL escaping is done internally.
37
+ # ++query++: Hash of query parameters.
38
+ def query_params(query)
39
+ query = query.select { |k,v| !v.nil? }
40
+ URI.escape(query.map { |k,v| "#{k}=#{v}" }.join('&'))
41
+ end
42
+
43
+ # Create request object.
44
+ # ++http_method++: GET/POST/PUT/DELETE.
45
+ # ++path++: Relative path to connection's URL.
46
+ # ++query++: Optional query parameters.
47
+ # ++opts++: Optional options. Ex. opts[:body] is used for PUT/POST request.
48
+ def compile_request_params(http_method, path, query = nil, opts = nil)
49
+ query ||= {}
50
+ opts ||= {}
51
+ headers = opts.delete(:headers) || {}
52
+ opts[:body] = opts[:body].to_json unless opts[:body].nil?
53
+ {
54
+ :method => http_method,
55
+ :url => "#{@url}#{path}",
56
+ :query => query
57
+ }.merge(opts).reject { |_, v| v.nil? }
58
+ end
59
+
60
+ # Create full URL with query parameters.
61
+ # ++url++: Base URL.
62
+ # ++query++: Hash of query parameters.
63
+ def build_url(url, query)
64
+ url = URI.escape(url)
65
+ if query.size > 0
66
+ url += '?' + query_params(query)
67
+ end
68
+ url
69
+ end
70
+
71
+ # Send a request to the server and parse response.
72
+ # ++http_method++: GET/POST/PUT/DELETE.
73
+ # ++path++: Relative path to connection's URL.
74
+ # ++query++: Optional query parameters.
75
+ # ++opts++: Optional options. Ex. opts[:body] is used for PUT/POST request.
76
+ def request(*args)
77
+ request = compile_request_params(*args)
78
+ url = build_url(request[:url], request[:query])
79
+ response = self.class.send(request[:method], url, request)
80
+ if response.success?
81
+ response.parsed_response
82
+ else
83
+ raise Marathon::Error.from_response(response)
84
+ end
85
+ rescue MarathonError => e
86
+ raise e
87
+ rescue SocketError => e
88
+ raise IOError, "HTTP call failed: #{e.message}"
89
+ rescue SystemCallError => e
90
+ if e.class.name.start_with?('Errno::')
91
+ raise IOError, "HTTP call failed: #{e.message}"
92
+ else
93
+ raise e
94
+ end
95
+ end
96
+
97
+ end
@@ -0,0 +1,60 @@
1
+ # This class represents a Marathon Deployment.
2
+ # See https://mesosphere.github.io/marathon/docs/rest-api.html#deployments for full list of API's methods.
3
+ class Marathon::Deployment
4
+
5
+ attr_reader :info
6
+
7
+ # Create a new deployment object.
8
+ # ++hash++: Hash including all attributes.
9
+ # See https://mesosphere.github.io/marathon/docs/rest-api.html#get-/v2/deployments for full details.
10
+ def initialize(hash = {})
11
+ @info = hash
12
+ end
13
+
14
+ # Shortcuts for reaching attributes
15
+ %w[ id affectedApps steps currentActions version currentStep totalSteps ].each do |method|
16
+ define_method(method) { |*args, &block| info[method] }
17
+ end
18
+
19
+ # Cancel the deployment.
20
+ # ++force++: If set to false (the default) then the deployment is canceled and a new deployment
21
+ # is created to restore the previous configuration. If set to true, then the deployment
22
+ # is still canceled but no rollback deployment is created.
23
+ def delete(force = false)
24
+ self.class.delete(id, force)
25
+ end
26
+ alias :cancel :delete
27
+
28
+ def to_s
29
+ "Marathon::Deployment { " \
30
+ + ":id => #{id} :affectedApps => #{affectedApps} :currentStep => #{currentStep} :totalSteps => #{totalSteps} }"
31
+ end
32
+
33
+ # Return deployment as JSON formatted string.
34
+ def to_json
35
+ info.to_json
36
+ end
37
+
38
+ class << self
39
+
40
+ # List running deployments.
41
+ def list
42
+ json = Marathon.connection.get('/v2/deployments')
43
+ json.map { |j| new(j) }
44
+ end
45
+
46
+ # Cancel the deployment with id.
47
+ # ++id++: Deployment's id
48
+ # ++force++: If set to false (the default) then the deployment is canceled and a new deployment
49
+ # is created to restore the previous configuration. If set to true, then the deployment
50
+ # is still canceled but no rollback deployment is created.
51
+ def delete(id, force = false)
52
+ query = {}
53
+ query[:force] = true if force
54
+ json = Marathon.connection.delete("/v2/deployments/#{id}")
55
+ # TODO parse deploymentId + version
56
+ end
57
+ alias :cancel :delete
58
+ alias :remove :delete
59
+ end
60
+ end
@@ -0,0 +1,62 @@
1
+ # This module holds the Errors for the gem.
2
+ module Marathon::Error
3
+ # The default error. It's never actually raised, but can be used to catch all
4
+ # gem-specific errors that are thrown as they all subclass from this.
5
+ class MarathonError < StandardError; end
6
+
7
+ # Raised when invalid arguments are passed to a method.
8
+ class ArgumentError < MarathonError; end
9
+
10
+ # Raised when a request returns a 400.
11
+ class ClientError < MarathonError; end
12
+
13
+ # Raised when a request returns a 404.
14
+ class NotFoundError < MarathonError; end
15
+
16
+ # Raised when there is an unexpected response code / body.
17
+ class UnexpectedResponseError < MarathonError; end
18
+
19
+ # Raised when a request times out.
20
+ class TimeoutError < MarathonError; end
21
+
22
+ # Raised when login fails.
23
+ class AuthenticationError < MarathonError; end
24
+
25
+ # Raised when an IO action fails.
26
+ class IOError < MarathonError; end
27
+
28
+ # Raise error specific to http response.
29
+ # ++response++: HTTParty response object.
30
+ def from_response(response)
31
+ error_class(response).new(error_message(response))
32
+ end
33
+
34
+ private
35
+
36
+ # Get reponse code specific error class.
37
+ # ++response++: HTTParty response object.
38
+ def error_class(response)
39
+ case response.code
40
+ when 400
41
+ ClientError
42
+ when 404
43
+ NotFoundError
44
+ else
45
+ UnexpectedResponseError
46
+ end
47
+ end
48
+
49
+ # Get response code from http response.
50
+ # ++response++: HTTParty response object.
51
+ def error_message(response)
52
+ body = response.parsed_response
53
+ if body.is_a?(Hash)
54
+ body['message']
55
+ else
56
+ body
57
+ end
58
+ end
59
+
60
+ module_function :error_class, :error_message, :from_response
61
+
62
+ end
@@ -0,0 +1,33 @@
1
+ # This class represents a Marathon Event Subscriptions.
2
+ # See https://mesosphere.github.io/marathon/docs/rest-api.html#event-subscriptions for full list of API's methods.
3
+ class Marathon::EventSubscriptions
4
+
5
+ class << self
6
+ # List all event subscriber callback URLs.
7
+ # Returns a list of strings/URLs.
8
+ def list
9
+ json = Marathon.connection.get('/v2/eventSubscriptions')
10
+ json['callbackUrls']
11
+ end
12
+
13
+ # Register a callback URL as an event subscriber.
14
+ # ++callbackUrl++: URL to which events should be posted.
15
+ # Returns an event as hash.
16
+ def register(callbackUrl)
17
+ query = {}
18
+ query[:callbackUrl] = callbackUrl
19
+ json = Marathon.connection.post('/v2/eventSubscriptions', query)
20
+ json
21
+ end
22
+
23
+ # Unregister a callback URL from the event subscribers list.
24
+ # ++callbackUrl++: URL passed when the event subscription was created.
25
+ # Returns an event as hash.
26
+ def unregister(callbackUrl)
27
+ query = {}
28
+ query[:callbackUrl] = callbackUrl
29
+ json = Marathon.connection.delete('/v2/eventSubscriptions', query)
30
+ json
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,19 @@
1
+ # This class represents a Marathon Leader.
2
+ # See https://mesosphere.github.io/marathon/docs/rest-api.html#get-/v2/leader for full list of API's methods.
3
+ class Marathon::Leader
4
+
5
+ class << self
6
+ # Returns the current leader. If no leader exists, raises NotFoundError.
7
+ def get
8
+ json = Marathon.connection.get('/v2/leader')
9
+ json['leader']
10
+ end
11
+
12
+ # Causes the current leader to abdicate, triggering a new election.
13
+ # If no leader exists, raises NotFoundError.
14
+ def delete
15
+ json = Marathon.connection.delete('/v2/leader')
16
+ json['message']
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,36 @@
1
+ # This class represents a Marathon Queue element.
2
+ # See https://mesosphere.github.io/marathon/docs/rest-api.html#queue for full list of API's methods.
3
+ class Marathon::Queue
4
+
5
+ attr_reader :app
6
+ attr_reader :delay
7
+
8
+ # Create a new queue element object.
9
+ # ++hash++: Hash returned by API, including 'app' and 'delay'
10
+ def initialize(hash = {})
11
+ @app = Marathon::App.new(hash['app'], true)
12
+ @delay = hash['delay']
13
+ end
14
+
15
+ def to_s
16
+ "Marathon::Queue { :appId => #{app.id} :delay => #{delay} }"
17
+ end
18
+
19
+ # Return queue element as JSON formatted string.
20
+ def to_json
21
+ {
22
+ 'app' => @app.info,
23
+ 'delay' => @delay
24
+ }.to_json
25
+ end
26
+
27
+ class << self
28
+
29
+ # Show content of the task queue.
30
+ # Returns Array of Queue objects.
31
+ def list
32
+ json = Marathon.connection.get('/v2/queue')['queue']
33
+ json.map { |j| new(j) }
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,85 @@
1
+ # This class represents a Marathon Task.
2
+ # See https://mesosphere.github.io/marathon/docs/rest-api.html#get-/v2/tasks for full list of API's methods.
3
+ class Marathon::Task
4
+
5
+ attr_reader :info
6
+
7
+ # Create a new task object.
8
+ # ++hash++: Hash including all attributes
9
+ def initialize(hash = {})
10
+ @info = hash
11
+ end
12
+
13
+ # Shortcuts for reaching attributes
14
+ %w[ id appId host ports servicePorts version stagedAt startedAt ].each do |method|
15
+ define_method(method) { |*args, &block| info[method] }
16
+ end
17
+
18
+ # Kill the task that belongs to an application.
19
+ # ++scale++: Scale the app down (i.e. decrement its instances setting by the number of tasks killed)
20
+ # after killing the specified tasks.
21
+ def delete!(scale = false)
22
+ new_task = self.class.delete(appId, id, scale)
23
+ @info = new_task.info
24
+ end
25
+ alias :kill! :delete!
26
+
27
+ def to_s
28
+ "Marathon::Task { :id => #{self.id} :appId => #{appId} :host => #{host} }"
29
+ end
30
+
31
+ # Return task as JSON formatted string.
32
+ def to_json
33
+ info.to_json
34
+ end
35
+
36
+ class << self
37
+
38
+ # List tasks of all applications.
39
+ # ++status++: Return only those tasks whose status matches this parameter.
40
+ # If not specified, all tasks are returned. Possible values: running, staging.
41
+ def list(status = nil)
42
+ query = {}
43
+ Marathon::Util.add_choice(query, :status, status, %w[running staging])
44
+ json = Marathon.connection.get('/v2/tasks', query)['tasks']
45
+ json.map { |j| new(j) }
46
+ end
47
+
48
+ # List all running tasks for application appId.
49
+ # ++appId++: Application's id
50
+ def get(appId)
51
+ json = Marathon.connection.get("/v2/apps/#{appId}/tasks")['tasks']
52
+ json.map { |j| new(j) }
53
+ end
54
+
55
+ # Kill the task that belongs to the application appId.
56
+ # ++appId++: Application's id
57
+ # ++id++: Id of target task.
58
+ # ++scale++: Scale the app down (i.e. decrement its instances setting by the number of tasks killed)
59
+ # after killing the specified tasks.
60
+ def delete(appId, id, scale = false)
61
+ query = {}
62
+ query[:scale] = true if scale
63
+ json = Marathon.connection.delete("/v2/apps/#{appId}/tasks/#{id}", query)
64
+ new(json)
65
+ end
66
+ alias :remove :delete
67
+ alias :kill :delete
68
+
69
+ # Kill tasks that belong to the application appId.
70
+ # ++appId++: Application's id
71
+ # ++host++: Kill only those tasks running on host host.
72
+ # ++scale++: Scale the app down (i.e. decrement its instances setting by the number of tasks killed)
73
+ # after killing the specified tasks.
74
+ def delete_all(appId, host = nil, scale = false)
75
+ query = {}
76
+ query[:host] = host if host
77
+ query[:scale] = true if scale
78
+ json = Marathon.connection.delete("/v2/apps/#{appId}/tasks", query)['tasks']
79
+ json.map { |j| new(j) }
80
+ end
81
+ alias :remove_all :delete_all
82
+ alias :kill_all :delete_all
83
+ end
84
+
85
+ end
@@ -0,0 +1,35 @@
1
+ # Some helper things.
2
+ class Marathon::Util
3
+ class << self
4
+
5
+ # Checks if parameter is of allowed value.
6
+ # ++name++: parameter's name
7
+ # ++value++: parameter's value
8
+ # ++allowed++: array of allowd values
9
+ # ++nil_allowed++: allow nil values
10
+ def validate_choice(name, value, allowed, nil_allowed = true)
11
+ if value.nil?
12
+ unless nil_allowed
13
+ raise Marathon::Error::ArgumentError, "#{name} must not be nil"
14
+ end
15
+ else
16
+ # value is not nil
17
+ unless allowed.include?(value)
18
+ msg = nil_allowed ? "#{name} must be one of #{allowed}, or nil" : "#{name} must be one of #{allowed}"
19
+ raise Marathon::Error::ArgumentError, msg
20
+ end
21
+ end
22
+ end
23
+
24
+ # Check parameter and add it to hash if not nil.
25
+ # ++opts++: hash of parameters
26
+ # ++name++: parameter's name
27
+ # ++value++: parameter's value
28
+ # ++allowed++: array of allowd values
29
+ # ++nil_allowed++: allow nil values
30
+ def add_choice(opts, name, value, allowed, nil_allowed = true)
31
+ validate_choice(name, value, allowed, nil_allowed)
32
+ opts[name] = value if value
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,3 @@
1
+ module Marathon
2
+ VERSION = '0.9.0'
3
+ end
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/marathon/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = "marathon-api"
6
+ gem.version = Marathon::VERSION
7
+ gem.authors = ["Felix Bechstein"]
8
+ gem.email = %w{f@ub0r.de}
9
+ gem.description = %q{A simple REST client for the Marathon Remote API}
10
+ gem.summary = %q{A simple REST client for the Marathon Remote API}
11
+ gem.homepage = 'https://github.com/felixb/marathon-api'
12
+ gem.license = 'MIT'
13
+
14
+ gem.files = `git ls-files`.split($\)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.require_paths = %w{lib}
18
+
19
+ gem.add_dependency 'json'
20
+ gem.add_dependency "httparty", ">= 0.11"
21
+
22
+ gem.add_development_dependency 'rake'
23
+ gem.add_development_dependency 'rspec', '~> 3.0'
24
+ gem.add_development_dependency 'rspec-its'
25
+ gem.add_development_dependency 'vcr', '>= 2.7.0'
26
+ gem.add_development_dependency 'webmock'
27
+ gem.add_development_dependency 'pry'
28
+ gem.add_development_dependency 'cane'
29
+ gem.add_development_dependency 'simplecov'
30
+ gem.add_development_dependency 'codeclimate-test-reporter'
31
+ end