marathon-api 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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