├── .ruby-gemset ├── .cane ├── Gemfile ├── lib ├── marathon │ ├── version.rb │ ├── deployment_action.rb │ ├── deployment_step.rb │ ├── container_volume.rb │ ├── container.rb │ ├── leader.rb │ ├── container_docker.rb │ ├── constraint.rb │ ├── base.rb │ ├── deployment_info.rb │ ├── queue.rb │ ├── container_docker_port_mapping.rb │ ├── health_check.rb │ ├── event_subscriptions.rb │ ├── error.rb │ ├── deployment.rb │ ├── util.rb │ ├── connection.rb │ ├── task.rb │ ├── group.rb │ └── app.rb └── marathon.rb ├── .gitignore ├── .simplecov ├── .travis.yml ├── fixtures ├── marathon_docker_sample.json ├── marathon_docker_sample_2.json └── vcr │ ├── Marathon_Queue │ └── _list │ │ └── lists_queue.yml │ ├── Marathon_Leader │ ├── _get │ │ └── get │ │ │ ├── 1_1_1_1.yml │ │ │ └── .yml │ └── _delete │ │ └── delete │ │ ├── .yml │ │ └── 1_2_1_1.yml │ ├── Marathon_Group │ ├── _changes │ │ ├── previews_changes.yml │ │ └── changes_the_group.yml │ ├── _get │ │ ├── fails_getting_not_existing_app.yml │ │ └── gets_the_group.yml │ ├── _delete │ │ ├── fails_deleting_not_existing_app.yml │ │ └── deletes_the_group.yml │ ├── _start │ │ ├── fails_getting_not_existing_group.yml │ │ └── starts_the_group.yml │ └── _list │ │ └── lists_apps.yml │ ├── Marathon_App │ ├── _get │ │ ├── fails_getting_not_existing_app.yml │ │ └── gets_the_app.yml │ ├── _start │ │ ├── fails_getting_not_existing_app.yml │ │ └── starts_the_app.yml │ ├── _delete │ │ ├── fails_deleting_not_existing_app.yml │ │ └── deletes_the_app.yml │ ├── _restart │ │ ├── fails_restarting_not_existing_app.yml │ │ └── restarts_an_app.yml │ ├── _changes │ │ ├── fails_with_stange_attributes.yml │ │ └── changes_the_app.yml │ ├── _list │ │ └── lists_apps.yml │ ├── _versions │ │ └── gets_versions.yml │ └── _version │ │ └── gets_a_version.yml │ ├── Marathon_Deployment │ ├── _delete │ │ ├── cleans_app_from_marathon.yml │ │ └── deletes_deployments.yml │ └── _list │ │ └── lists_deployments.yml │ ├── Marathon_EventSubscriptions │ ├── _list │ │ └── lists_callbacks.yml │ ├── _register │ │ └── registers_callback.yml │ └── _unregister │ │ └── unregisters_callback.yml │ ├── Marathon │ ├── _ping │ │ ├── returns_pong.yml │ │ └── handles_incorrect_content_type.yml │ └── _info │ │ └── returns_the_info_hash.yml │ └── Marathon_Task │ ├── _delete_all │ └── kills_all_tasks_of_an_app.yml │ ├── _get │ └── gets_tasks_of_an_app.yml │ ├── _list │ ├── lists_tasks.yml │ └── lists_running_tasks.yml │ └── _delete │ └── kills_a_tasks_of_an_app.yml ├── spec ├── marathon │ ├── leader_spec.rb │ ├── deployment_action_spec.rb │ ├── constraint_spec.rb │ ├── deployment_step_spec.rb │ ├── event_subscriptions_spec.rb │ ├── connection_spec.rb │ ├── container_volume_spec.rb │ ├── health_check_spec.rb │ ├── container_docker_spec.rb │ ├── deployment_info_spec.rb │ ├── error_spec.rb │ ├── base_spec.rb │ ├── queue_spec.rb │ ├── container_docker_port_mapping_spec.rb │ ├── marathon_spec.rb │ ├── container_spec.rb │ ├── util_spec.rb │ ├── deployment_spec.rb │ ├── task_spec.rb │ ├── group_spec.rb │ └── app_spec.rb └── spec_helper.rb ├── publish.sh ├── Rakefile ├── LICENSE ├── marathon-api.gemspec ├── TESTING.md ├── README.md └── bin └── marathon /.ruby-gemset: -------------------------------------------------------------------------------- 1 | marathon-api -------------------------------------------------------------------------------- /.cane: -------------------------------------------------------------------------------- 1 | --style-measure 120 2 | --color 3 | --parallel 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /lib/marathon/version.rb: -------------------------------------------------------------------------------- 1 | module Marathon 2 | VERSION = '2.2.0' 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | .DS_Store 3 | *.swp 4 | *.gem 5 | Gemfile.lock 6 | vendor 7 | coverage/ 8 | .idea 9 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | SimpleCov.start do 2 | add_group 'Library', 'lib' 3 | add_filter '/vendor' 4 | add_filter '/spec' 5 | end 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0.0 4 | - 2.1.5 5 | - 2.2.0 6 | - 2.3.0 7 | - 2.4.0 8 | script: bundle exec rake 9 | -------------------------------------------------------------------------------- /fixtures/marathon_docker_sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "container": { 3 | "type": "DOCKER", 4 | "docker": { 5 | "image": "libmesos/ubuntu" 6 | } 7 | }, 8 | "id": "/ubuntu", 9 | "instances": 1, 10 | "cpus": 0.1, 11 | "mem": 64, 12 | "uris": [], 13 | "cmd": "while sleep 10; do date -u +%T; done" 14 | } 15 | -------------------------------------------------------------------------------- /fixtures/marathon_docker_sample_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "container": { 3 | "type": "DOCKER", 4 | "docker": { 5 | "image": "libmesos/ubuntu" 6 | } 7 | }, 8 | "id": "/ubuntu2", 9 | "instances": 1, 10 | "cpus": 0.1, 11 | "mem": 64, 12 | "uris": [], 13 | "cmd": "while sleep 10; do date -u +%T; done", 14 | "constraints": [["hostname", "GROUP_BY"]] 15 | } 16 | -------------------------------------------------------------------------------- /spec/marathon/leader_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Marathon::Leader do 4 | 5 | describe '.get', :vcr do 6 | subject { described_class } 7 | 8 | its(:get) { is_expected.to be_instance_of(String) } 9 | end 10 | 11 | describe '.delete', :vcr do 12 | subject { described_class } 13 | 14 | let(:expected_string) { 'Leadership abdicted' } 15 | 16 | its(:delete) { should == expected_string } 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | set -e 4 | 5 | if [ $# -ne 1 ] ; then 6 | echo "usage: $0 " 7 | exit 1 8 | fi 9 | 10 | VERSION="${1}" 11 | 12 | sed -e "s:VERSION.*:VERSION = '${VERSION}':" -i lib/marathon/version.rb 13 | 14 | bundle exec rake spec 15 | 16 | git commit -m "prepare release ${VERSION}" lib/marathon/version.rb 17 | 18 | gem build "marathon-api.gemspec" 19 | git tag "${VERSION}" 20 | 21 | echo "upload?" 22 | read ok 23 | 24 | gem push "marathon-api-${VERSION}.gem" 25 | 26 | echo "push?" 27 | read ok 28 | 29 | git push 30 | git push --tags 31 | -------------------------------------------------------------------------------- /lib/marathon/deployment_action.rb: -------------------------------------------------------------------------------- 1 | # This class represents a Marathon Deployment action. 2 | class Marathon::DeploymentAction < Marathon::Base 3 | 4 | # Create a new deployment action object. 5 | # ++hash++: Hash returned by API, including 'app' and 'type' 6 | def initialize(hash) 7 | super(hash, %w[app]) 8 | end 9 | 10 | def type 11 | info[:type] || info[:action] 12 | end 13 | 14 | alias :action :type 15 | 16 | def to_pretty_s 17 | "#{app}/#{type}" 18 | end 19 | 20 | def to_s 21 | "Marathon::DeploymentAction { :app => #{app} :type => #{type} }" 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /lib/marathon/deployment_step.rb: -------------------------------------------------------------------------------- 1 | # This class represents a Marathon Deployment step. 2 | class Marathon::DeploymentStep < Marathon::Base 3 | 4 | attr_reader :actions 5 | 6 | # Create a new deployment step object. 7 | # ++hash++: Hash returned by API, including 'actions' 8 | def initialize(hash) 9 | super(hash) 10 | if hash.is_a?(Array) 11 | @actions = info.map { |e| Marathon::DeploymentAction.new(e) } 12 | else 13 | @actions = (info[:actions] || []).map { |e| Marathon::DeploymentAction.new(e) } 14 | end 15 | end 16 | 17 | def to_s 18 | "Marathon::DeploymentStep { :actions => #{actions.map { |e| e.to_pretty_s }.join(',')} }" 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /spec/marathon/deployment_action_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | DEPLOYMENT_ACTION_EXAMPLE = {"app" => "app1", "type" => "StartApplication"} 4 | 5 | describe Marathon::DeploymentAction do 6 | 7 | describe '#attributes' do 8 | subject { described_class.new(DEPLOYMENT_ACTION_EXAMPLE) } 9 | 10 | its(:app) { should == 'app1' } 11 | its(:type) { should == 'StartApplication' } 12 | end 13 | 14 | describe '#to_s' do 15 | subject { described_class.new(DEPLOYMENT_ACTION_EXAMPLE) } 16 | 17 | let(:expected_string) do 18 | 'Marathon::DeploymentAction { :app => app1 :type => StartApplication }' 19 | end 20 | 21 | its(:to_s) { should == expected_string } 22 | its(:to_pretty_s) { should == 'app1/StartApplication' } 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /spec/marathon/constraint_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Marathon::Constraint do 4 | 5 | describe '#to_s w/o parameter' do 6 | subject { described_class.new(['hostname', 'UNIQUE']) } 7 | 8 | let(:expected_string) do 9 | 'Marathon::Constraint { :attribute => hostname :operator => UNIQUE }' 10 | end 11 | 12 | its(:to_s) { should == expected_string } 13 | its(:to_pretty_s) { should == 'hostname:UNIQUE' } 14 | end 15 | 16 | describe '#to_s with parameter' do 17 | subject { described_class.new(['hostname', 'LIKE', 'foo-host']) } 18 | 19 | let(:expected_string) do 20 | 'Marathon::Constraint { :attribute => hostname :operator => LIKE :parameter => foo-host }' 21 | end 22 | 23 | its(:to_s) { should == expected_string } 24 | its(:to_pretty_s) { should == 'hostname:LIKE:foo-host' } 25 | end 26 | 27 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | 3 | require 'rspec' 4 | require 'rspec/its' 5 | require 'simplecov' 6 | require 'vcr' 7 | require 'webmock' 8 | require 'marathon' 9 | 10 | %w[MARATHON_URL MARATHON_USER MARATHON_PASSWORD].each do |key| 11 | ENV.delete(key) 12 | end 13 | 14 | VCR.configure do |c| 15 | c.allow_http_connections_when_no_cassette = false 16 | c.cassette_library_dir = "fixtures/vcr" 17 | c.hook_into :webmock 18 | c.configure_rspec_metadata! 19 | end 20 | 21 | RSpec.shared_context "local paths" do 22 | def project_dir 23 | File.expand_path(File.join(File.dirname(__FILE__), '..')) 24 | end 25 | end 26 | 27 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 28 | 29 | RSpec.configure do |c| 30 | c.mock_with :rspec 31 | c.color = true 32 | c.formatter = :documentation 33 | c.tty = true 34 | end 35 | -------------------------------------------------------------------------------- /lib/marathon/container_volume.rb: -------------------------------------------------------------------------------- 1 | # This class represents a Marathon Container Volume information. 2 | # See https://mesosphere.github.io/marathon/docs/native-docker.html for full details. 3 | class Marathon::ContainerVolume < Marathon::Base 4 | 5 | ACCESSORS = %w[ containerPath hostPath mode ] 6 | DEFAULTS = { 7 | :mode => 'RW' 8 | } 9 | 10 | # Create a new container volume object. 11 | # ++hash++: Hash returned by API. 12 | def initialize(hash) 13 | super(Marathon::Util.merge_keywordized_hash(DEFAULTS, hash), ACCESSORS) 14 | Marathon::Util.validate_choice('mode', mode, %w[RW RO]) 15 | raise Marathon::Error::ArgumentError, 'containerPath must not be nil' unless containerPath 16 | end 17 | 18 | def to_pretty_s 19 | "#{containerPath}:#{hostPath}:#{mode}" 20 | end 21 | 22 | def to_s 23 | "Marathon::ContainerVolume { :containerPath => #{containerPath} :hostPath => #{hostPath} :mode => #{mode} }" 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Queue/_list/lists_queue.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://localhost:8080/v2/queue 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"queue":[]}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:06:09 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /spec/marathon/deployment_step_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | DEPLOYMENT_STEP_EXAMPLE = { 4 | "actions" => [ 5 | {"app" => "app1", "type" => "StartApplication"}, 6 | {"app" => "app2", "type" => "StartApplication"} 7 | ] 8 | } 9 | 10 | describe Marathon::DeploymentStep do 11 | 12 | describe '#attributes' do 13 | subject { described_class.new(DEPLOYMENT_STEP_EXAMPLE) } 14 | 15 | it 'has actions' do 16 | expect(subject.actions).to be_instance_of(Array) 17 | expect(subject.actions.size).to eq(2) 18 | expect(subject.actions.first).to be_instance_of(Marathon::DeploymentAction) 19 | end 20 | end 21 | 22 | describe '#to_s' do 23 | subject { described_class.new(DEPLOYMENT_STEP_EXAMPLE) } 24 | 25 | let(:expected_string) do 26 | 'Marathon::DeploymentStep { :actions => app1/StartApplication,app2/StartApplication }' 27 | end 28 | 29 | its(:to_s) { should == expected_string } 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Leader/_get/get/1_1_1_1.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://localhost:8080/v2/leader 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.3.2 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"leader":"mesos:8080"}' 36 | http_version: 37 | recorded_at: Wed, 06 Jan 2016 07:55:12 GMT 38 | recorded_with: VCR 3.0.1 39 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Leader/_delete/delete/.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: delete 5 | uri: http://localhost:8080/v2/leader 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"message":"Leadership abdicted"}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:06:09 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Leader/_delete/delete/1_2_1_1.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: delete 5 | uri: http://localhost:8080/v2/leader 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.3.2 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"message":"Leadership abdicted"}' 36 | http_version: 37 | recorded_at: Wed, 06 Jan 2016 07:55:12 GMT 38 | recorded_with: VCR 3.0.1 39 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Leader/_get/get/.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://localhost:8080/v2/leader 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"leader":"mesos-dev-f999998.lhotse.ov.otto.de:8080"}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:06:09 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Group/_changes/previews_changes.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: put 5 | uri: http://localhost:8080/v2/groups//test-group?dryRun=true 6 | body: 7 | encoding: UTF-8 8 | string: '{"instances":20}' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"steps":[]}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:25:39 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_App/_get/fails_getting_not_existing_app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://localhost:8080/v2/apps/fooo%20app 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 404 19 | message: Not Found 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"message":"App ''/fooo app'' does not exist"}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:05:39 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_App/_start/fails_getting_not_existing_app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://localhost:8080/v2/apps/fooo%20app 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 404 19 | message: Not Found 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"message":"App ''/fooo app'' does not exist"}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:05:39 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_App/_delete/fails_deleting_not_existing_app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: delete 5 | uri: http://localhost:8080/v2/apps/fooo%20app 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 404 19 | message: Not Found 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"message":"App ''/fooo app'' does not exist"}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:05:39 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_App/_restart/fails_restarting_not_existing_app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: http://localhost:8080/v2/apps/fooo%20app/restart 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 404 19 | message: Not Found 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"message":"App ''/fooo app'' does not exist"}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:05:39 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Group/_get/fails_getting_not_existing_app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://localhost:8080/v2/groups/fooo%20group 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 404 19 | message: Not Found 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"message":"Group ''/fooo group'' does not exist"}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:06:08 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Group/_delete/fails_deleting_not_existing_app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: delete 5 | uri: http://localhost:8080/v2/groups/fooo%20group 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 404 19 | message: Not Found 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"message":"Group ''/fooo group'' does not exist"}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:06:09 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Group/_start/fails_getting_not_existing_group.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://localhost:8080/v2/groups/fooo%20group 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 404 19 | message: Not Found 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"message":"Group ''/fooo group'' does not exist"}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:06:08 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_App/_delete/deletes_the_app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: delete 5 | uri: http://localhost:8080/v2/apps//test-app 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"version":"2015-03-17T13:05:39.285Z","deploymentId":"e5985d99-2106-40e2-962d-18cb18126269"}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:05:39 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_App/_restart/restarts_an_app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: http://localhost:8080/v2/apps//ubuntu2/restart 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"version":"2015-03-17T13:05:39.490Z","deploymentId":"f31c4fdf-ba97-4e45-a69b-48cc79f19706"}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:05:39 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Deployment/_delete/cleans_app_from_marathon.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: delete 5 | uri: http://localhost:8080/v2/apps//test-app 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"version":"2015-03-17T13:06:03.873Z","deploymentId":"8f5983da-4a3f-4d29-a7e9-dd90cd399815"}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:06:07 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Group/_delete/deletes_the_group.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: delete 5 | uri: http://localhost:8080/v2/groups//test-group?force=true 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"version":"2015-03-17T13:06:08.992Z","deploymentId":"dc1bea59-c598-4d50-91db-6d50d675f073"}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:06:09 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib')) 2 | 3 | require 'rake' 4 | require 'json' 5 | require 'marathon' 6 | require 'rspec/core/rake_task' 7 | require 'vcr' 8 | require 'cane/rake_task' 9 | 10 | task :default => [:spec, :quality] 11 | 12 | RSpec::Core::RakeTask.new do |t| 13 | t.pattern = 'spec/**/*_spec.rb' 14 | end 15 | 16 | Cane::RakeTask.new(:quality) do |cane| 17 | cane.canefile = '.cane' 18 | end 19 | 20 | dir = File.expand_path(File.dirname(__FILE__)) 21 | namespace :vcr do 22 | desc 'Remove VCR files' 23 | task :clean do 24 | FileUtils.remove_dir("#{dir}/fixtures/vcr", true) 25 | end 26 | 27 | desc 'Preapre Marathon by deploying some tasks' 28 | task :prepare do 29 | json = JSON.parse(File.read("#{dir}/fixtures/marathon_docker_sample.json")) 30 | Marathon::App.new(json).start! 31 | 32 | json = JSON.parse(File.read("#{dir}/fixtures/marathon_docker_sample_2.json")) 33 | Marathon::App.new(json).start! 34 | end 35 | 36 | desc 'Run spec tests and record VCR cassettes' 37 | task :record => [:clean, :prepare, :spec] 38 | 39 | end 40 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_EventSubscriptions/_list/lists_callbacks.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://localhost:8080/v2/eventSubscriptions 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"callbackUrls":["http://localhost:8083/marathon/event","http://127.0.0.1:8083/marathon/event","http://localhost/events/foo"]}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:06:07 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /lib/marathon/container.rb: -------------------------------------------------------------------------------- 1 | # This class represents a Marathon Container information. 2 | # It is included in App's definition. 3 | # See https://mesosphere.github.io/marathon/docs/native-docker.html for full details. 4 | class Marathon::Container < Marathon::Base 5 | 6 | SUPPERTED_TYPES = %w[ DOCKER MESOS] 7 | ACCESSORS = %w[ type ] 8 | DEFAULTS = { 9 | :type => 'DOCKER', 10 | :volumes => [] 11 | } 12 | 13 | attr_reader :docker, :volumes 14 | 15 | # Create a new container object. 16 | # ++hash++: Hash returned by API. 17 | def initialize(hash) 18 | super(Marathon::Util.merge_keywordized_hash(DEFAULTS, hash), ACCESSORS) 19 | Marathon::Util.validate_choice('type', type, SUPPERTED_TYPES) 20 | @docker = Marathon::ContainerDocker.new(info[:docker]) if info[:docker] 21 | @volumes = info[:volumes].map { |e| Marathon::ContainerVolume.new(e) } 22 | end 23 | 24 | def to_s 25 | "Marathon::Container { :type => #{type} :docker => #{Marathon::Util.items_to_pretty_s(docker)}"\ 26 | + " :volumes => #{Marathon::Util.items_to_pretty_s(volumes)} }" 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon/_ping/returns_pong.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://localhost:8080/ping 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.3.2 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Date: 22 | - Wed, 02 Mar 2016 11:32:08 GMT 23 | - Wed, 02 Mar 2016 11:32:08 GMT 24 | Server: 25 | - Jetty(9.3.z-SNAPSHOT) 26 | Cache-Control: 27 | - must-revalidate,no-cache,no-store 28 | Access-Control-Allow-Credentials: 29 | - 'true' 30 | Expires: 31 | - '0' 32 | Pragma: 33 | - no-cache 34 | Content-Type: 35 | - text/plain;charset=iso-8859-1 36 | Content-Length: 37 | - '5' 38 | body: 39 | encoding: UTF-8 40 | string: | 41 | pong 42 | http_version: 43 | recorded_at: Wed, 02 Mar 2016 11:32:28 GMT 44 | recorded_with: VCR 2.9.3 45 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_App/_changes/fails_with_stange_attributes.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: put 5 | uri: http://localhost:8080/v2/apps//ubuntu2 6 | body: 7 | encoding: UTF-8 8 | string: '{"instances":"foo"}' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 400 19 | message: Bad Request 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"message":"Invalid JSON: JsResultException(errors:List((/instances,List(ValidationError(error.expected.jsnumber,WrappedArray())))))"}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:05:49 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon/_ping/handles_incorrect_content_type.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://localhost:8080/ping 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.3.2 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Date: 22 | - Wed, 02 Mar 2016 11:32:08 GMT 23 | - Wed, 02 Mar 2016 11:32:08 GMT 24 | Server: 25 | - Jetty(9.3.z-SNAPSHOT) 26 | Cache-Control: 27 | - must-revalidate,no-cache,no-store 28 | Access-Control-Allow-Credentials: 29 | - 'true' 30 | Expires: 31 | - '0' 32 | Pragma: 33 | - no-cache 34 | Content-Type: 35 | - application/json; qs=2 36 | Content-Length: 37 | - '5' 38 | body: 39 | encoding: UTF-8 40 | string: | 41 | pong 42 | http_version: 43 | recorded_at: Wed, 02 Mar 2016 11:32:28 GMT 44 | recorded_with: VCR 2.9.3 45 | -------------------------------------------------------------------------------- /lib/marathon/leader.rb: -------------------------------------------------------------------------------- 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 | def initialize(marathon_instance = Marathon.singleton) 6 | @connection = marathon_instance.connection 7 | end 8 | 9 | # Returns the current leader. If no leader exists, raises NotFoundError. 10 | def get 11 | json = @connection.get('/v2/leader') 12 | json['leader'] 13 | end 14 | 15 | # Causes the current leader to abdicate, triggering a new election. 16 | # If no leader exists, raises NotFoundError. 17 | def delete 18 | json = @connection.delete('/v2/leader') 19 | json['message'] 20 | end 21 | 22 | class << self 23 | # Returns the current leader. If no leader exists, raises NotFoundError. 24 | def get 25 | Marathon.singleton.leaders.get 26 | end 27 | 28 | # Causes the current leader to abdicate, triggering a new election. 29 | # If no leader exists, raises NotFoundError. 30 | def delete 31 | Marathon.singleton.leaders.delete 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/marathon/container_docker.rb: -------------------------------------------------------------------------------- 1 | # This class represents a Marathon Container docker information. 2 | # See https://mesosphere.github.io/marathon/docs/native-docker.html for full details. 3 | class Marathon::ContainerDocker < Marathon::Base 4 | 5 | ACCESSORS = %w[ image network privileged parameters ] 6 | DEFAULTS = { 7 | :network => 'BRIDGE', 8 | :portMappings => [] 9 | } 10 | 11 | attr_reader :portMappings 12 | 13 | # Create a new container docker object. 14 | # ++hash++: Hash returned by API. 15 | def initialize(hash) 16 | super(Marathon::Util.merge_keywordized_hash(DEFAULTS, hash), ACCESSORS) 17 | Marathon::Util.validate_choice('network', network, %w[BRIDGE HOST]) 18 | Marathon::Util.validate_choice('privileged', privileged, ['true', 'false', true, false]) 19 | raise Marathon::Error::ArgumentError, 'image must not be nil' unless image 20 | @portMappings = (info[:portMappings] || []).map { |e| Marathon::ContainerDockerPortMapping.new(e) } 21 | end 22 | 23 | def to_pretty_s 24 | "#{image}" 25 | end 26 | 27 | def to_s 28 | "Marathon::ContainerDocker { :image => #{image} }" 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_EventSubscriptions/_register/registers_callback.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: http://localhost:8080/v2/eventSubscriptions?callbackUrl=http://localhost/events/foo 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"clientIp":"10.0.2.2","callbackUrl":"http://localhost/events/foo","eventType":"subscribe_event","timestamp":"2015-03-17T13:06:07.067Z"}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:06:07 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Otto (GmbH & Co KG) 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 13 | all 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 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_EventSubscriptions/_unregister/unregisters_callback.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: delete 5 | uri: http://localhost:8080/v2/eventSubscriptions?callbackUrl=http://localhost/events/foo 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"clientIp":"10.0.2.2","callbackUrl":"http://localhost/events/foo","eventType":"unsubscribe_event","timestamp":"2015-03-17T13:06:07.744Z"}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:06:07 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /spec/marathon/event_subscriptions_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Marathon::EventSubscriptions do 4 | 5 | describe '.register' do 6 | subject { described_class } 7 | 8 | it 'registers callback', :vcr do 9 | json = subject.register('http://localhost/events/foo') 10 | expect(json).to be_instance_of(Hash) 11 | expect(json['eventType']).to eq('subscribe_event') 12 | expect(json['callbackUrl']).to eq('http://localhost/events/foo') 13 | end 14 | end 15 | 16 | describe '.list' do 17 | subject { described_class } 18 | 19 | it 'lists callbacks', :vcr do 20 | json = subject.list 21 | expect(json).to be_instance_of(Array) 22 | expect(json).to include('http://localhost/events/foo') 23 | end 24 | end 25 | 26 | describe '.unregister' do 27 | subject { described_class } 28 | 29 | it 'unregisters callback', :vcr do 30 | json = subject.unregister('http://localhost/events/foo') 31 | expect(json).to be_instance_of(Hash) 32 | expect(json['eventType']).to eq('unsubscribe_event') 33 | expect(json['callbackUrl']).to eq('http://localhost/events/foo') 34 | end 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /lib/marathon/constraint.rb: -------------------------------------------------------------------------------- 1 | # This class represents a Marathon Constraint. 2 | # See https://mesosphere.github.io/marathon/docs/constraints.html for full details. 3 | class Marathon::Constraint < Marathon::Base 4 | 5 | # Create a new constraint object. 6 | # ++array++: Array returned by API, holds attribute, operator and parameter. 7 | def initialize(array) 8 | raise Marathon::Error::ArgumentError, 'array must be an Array' unless array.is_a?(Array) 9 | raise Marathon::Error::ArgumentError, 10 | 'array must be [attribute, operator, parameter] where only parameter is optional' \ 11 | unless array.size != 2 or array.size != 3 12 | super 13 | end 14 | 15 | def attribute 16 | info[0] 17 | end 18 | 19 | def operator 20 | info[1] 21 | end 22 | 23 | def parameter 24 | info[2] 25 | end 26 | 27 | def to_s 28 | if parameter 29 | "Marathon::Constraint { :attribute => #{attribute} :operator => #{operator} :parameter => #{parameter} }" 30 | else 31 | "Marathon::Constraint { :attribute => #{attribute} :operator => #{operator} }" 32 | end 33 | end 34 | 35 | # Returns a string for listing the constraint. 36 | def to_pretty_s 37 | info.join(':') 38 | end 39 | end -------------------------------------------------------------------------------- /lib/marathon/base.rb: -------------------------------------------------------------------------------- 1 | # Base class for all the API specific classes. 2 | class Marathon::Base 3 | 4 | include Marathon::Error 5 | 6 | attr_reader :info 7 | 8 | # Create the object 9 | # ++hash++: object returned from API. May be Hash or Array. 10 | # ++attr_accessors++: List of attribute accessors. 11 | def initialize(hash, attr_accessors = []) 12 | raise ArgumentError, 'hash must be a Hash' if attr_accessors and attr_accessors.size > 0 and not hash.is_a?(Hash) 13 | raise ArgumentError, 'hash must be Hash or Array' unless hash.is_a?(Hash) or hash.is_a?(Array) 14 | raise ArgumentError, 'attr_accessors must be an Array' unless attr_accessors.is_a?(Array) 15 | @info = Marathon::Util.keywordize_hash!(hash) 16 | attr_accessors.each { |e| add_attr_accessor(e) } 17 | end 18 | 19 | # Return application as JSON formatted string. 20 | def to_json(opts = {}) 21 | info.to_json(opts) 22 | end 23 | 24 | private 25 | 26 | # Create attr_accessor for @info[key]. 27 | # ++key++: key in @info 28 | def add_attr_accessor(key) 29 | sym = key.to_sym 30 | self.class.send(:define_method, sym.id2name) { info[sym] } 31 | self.class.send(:define_method, "#{sym.id2name}=") { |v| info[sym] = v } 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/marathon/connection_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Marathon::Connection do 4 | 5 | describe '#to_s' do 6 | subject { described_class.new('http://foo:8080') } 7 | 8 | let(:expected_string) do 9 | "Marathon::Connection { :url => http://foo:8080 :options => {} }" 10 | end 11 | 12 | its(:to_s) { should == expected_string } 13 | end 14 | 15 | describe '#request' do 16 | subject { described_class.new('http://foo.example.org:8080') } 17 | 18 | it 'raises IOError on SocketError' do 19 | allow(described_class).to receive(:send) { raise SocketError.new } 20 | expect { 21 | subject.get('/v2/some/api/path') 22 | }.to raise_error(Marathon::Error::IOError) 23 | end 24 | 25 | it 'raises IOError on Errno' do 26 | allow(described_class).to receive(:send) { raise Errno::EINTR.new } 27 | expect { 28 | subject.get('/v2/some/api/path') 29 | }.to raise_error(Marathon::Error::IOError) 30 | end 31 | 32 | it 'raises original error when unknown' do 33 | allow(described_class).to receive(:send) { raise RuntimeError.new } 34 | expect { 35 | subject.get('/v2/some/api/path') 36 | }.to raise_error(RuntimeError) 37 | end 38 | end 39 | 40 | end -------------------------------------------------------------------------------- /lib/marathon/deployment_info.rb: -------------------------------------------------------------------------------- 1 | # This class represents a Marathon Deployment information. 2 | # It is returned by asynchronious deployment calls. 3 | class Marathon::DeploymentInfo < Marathon::Base 4 | 5 | RECHECK_INTERVAL = 3 6 | 7 | # Create a new deployment info object. 8 | # ++hash++: Hash returned by API, including 'deploymentId' and 'version' 9 | # ++marathon_instance++: MarathonInstance holding a connection to marathon 10 | def initialize(hash, marathon_instance = Marathon.singleton) 11 | super(hash, %w[deploymentId version]) 12 | raise Marathon::Error::ArgumentError, 'version must not be nil' unless version 13 | @marathon_instance = marathon_instance 14 | end 15 | 16 | # Wait for a deployment to finish. 17 | # ++timeout++: Timeout in seconds. 18 | def wait(timeout = 60) 19 | Timeout::timeout(timeout) do 20 | deployments = nil 21 | while deployments.nil? or deployments.any? { |e| e.id == deploymentId } do 22 | sleep(RECHECK_INTERVAL) 23 | deployments = @marathon_instance.deployments.list 24 | end 25 | end 26 | end 27 | 28 | def to_s 29 | if deploymentId 30 | "Marathon::DeploymentInfo { :version => #{version} :deploymentId => #{deploymentId} }" 31 | else 32 | "Marathon::DeploymentInfo { :version => #{version} }" 33 | end 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /marathon-api.gemspec: -------------------------------------------------------------------------------- 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{felix.bechstein@otto.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/otto-de/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 | gem.add_dependency 'trollop', '>= 2.0' 22 | 23 | gem.add_development_dependency 'rake' 24 | gem.add_development_dependency 'rspec', '~> 3.0' 25 | gem.add_development_dependency 'rspec-its' 26 | gem.add_development_dependency 'vcr', '>= 2.7.0' 27 | gem.add_development_dependency 'webmock', '< 2.0.0' 28 | gem.add_development_dependency 'pry' 29 | gem.add_development_dependency 'cane' 30 | gem.add_development_dependency 'simplecov' 31 | end 32 | -------------------------------------------------------------------------------- /lib/marathon/queue.rb: -------------------------------------------------------------------------------- 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 < Marathon::Base 4 | 5 | attr_reader :app 6 | 7 | # Create a new queue element object. 8 | # ++hash++: Hash returned by API, including 'app' and 'delay' 9 | # ++marathon_instance++: MarathonInstance holding a connection to marathon 10 | def initialize(hash, marathon_instance = Marathon.singleton) 11 | super(hash, %w[delay]) 12 | @app = Marathon::App.new(info[:app], marathon_instance, true) 13 | @marathon_instance = marathon_instance 14 | end 15 | 16 | def to_s 17 | "Marathon::Queue { :appId => #{app.id} :delay => #{delay} }" 18 | end 19 | 20 | class << self 21 | 22 | # Show content of the task queue. 23 | # Returns Array of Queue objects. 24 | def list 25 | Marathon.singleton.queues.list 26 | end 27 | end 28 | end 29 | 30 | # This class represents the Queue with all its elements 31 | class Marathon::Queues 32 | def initialize(marathon_instance) 33 | @connection = marathon_instance.connection 34 | end 35 | 36 | # Show content of the task queue. 37 | # Returns Array of Queue objects. 38 | def list 39 | json = @connection.get('/v2/queue')['queue'] 40 | json.map { |j| Marathon::Queue.new(j) } 41 | end 42 | 43 | end -------------------------------------------------------------------------------- /lib/marathon/container_docker_port_mapping.rb: -------------------------------------------------------------------------------- 1 | # This class represents a Marathon Container docker information. 2 | # See https://mesosphere.github.io/marathon/docs/native-docker.html for full details. 3 | class Marathon::ContainerDockerPortMapping < Marathon::Base 4 | 5 | ACCESSORS = %w[ containerPort hostPort servicePort protocol ] 6 | DEFAULTS = { 7 | :protocol => 'tcp', 8 | :hostPort => 0 9 | } 10 | 11 | # Create a new container docker port mappint object. 12 | # ++hash++: Hash returned by API. 13 | def initialize(hash) 14 | super(Marathon::Util.merge_keywordized_hash(DEFAULTS, hash), ACCESSORS) 15 | Marathon::Util.validate_choice('protocol', protocol, %w[tcp udp]) 16 | raise Marathon::Error::ArgumentError, 'containerPort must not be nil' unless containerPort 17 | raise Marathon::Error::ArgumentError, 'containerPort must be a non negative number' \ 18 | unless containerPort.is_a?(Integer) and containerPort >= 0 19 | raise Marathon::Error::ArgumentError, 'hostPort must be a non negative number' \ 20 | unless hostPort.is_a?(Integer) and hostPort >= 0 21 | end 22 | 23 | def to_pretty_s 24 | "#{protocol}/#{containerPort}:#{hostPort}" 25 | end 26 | 27 | def to_s 28 | "Marathon::ContainerDockerPortMapping { :protocol => #{protocol} " \ 29 | + ":containerPort => #{containerPort} :hostPort => #{hostPort} }" 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /spec/marathon/container_volume_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | CONTAINER_VOLUME_EXAMPLE = { 4 | :containerPath => '/data', 5 | :hostPath => '/var/opt/foo', 6 | :mode => 'RO' 7 | } 8 | 9 | describe Marathon::ContainerVolume do 10 | 11 | describe '#init' do 12 | subject { described_class } 13 | 14 | it 'should fail with invalid mode' do 15 | expect { subject.new(:containerPath => '/', :hostPath => '/', :mode => 'foo') } 16 | .to raise_error(Marathon::Error::ArgumentError, /mode must be one of /) 17 | end 18 | 19 | it 'should fail with invalid path' do 20 | expect { subject.new(:hostPath => '/') } 21 | .to raise_error(Marathon::Error::ArgumentError, /containerPath .* not be nil/) 22 | end 23 | end 24 | 25 | describe '#attributes' do 26 | subject { described_class.new(CONTAINER_VOLUME_EXAMPLE) } 27 | 28 | its(:containerPath) { should == '/data' } 29 | its(:hostPath) { should == '/var/opt/foo' } 30 | its(:mode) { should == 'RO' } 31 | end 32 | 33 | describe '#to_s' do 34 | subject { described_class.new(CONTAINER_VOLUME_EXAMPLE) } 35 | 36 | let(:expected_string) do 37 | 'Marathon::ContainerVolume { :containerPath => /data :hostPath => /var/opt/foo :mode => RO }' 38 | end 39 | 40 | its(:to_s) { should == expected_string } 41 | its(:to_pretty_s) { should == '/data:/var/opt/foo:RO' } 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Task/_delete_all/kills_all_tasks_of_an_app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: delete 5 | uri: http://localhost:8080/v2/apps//ubuntu2/tasks 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"tasks":[{"id":"ubuntu2.52c55930-cca6-11e4-9cfc-080027d9edbf","host":"mesos-dev-f999999.lhotse.ov.otto.de","ports":[],"startedAt":"2015-03-17T13:05:46.755Z","stagedAt":"2015-03-17T13:05:45.326Z","version":"2015-03-17T13:05:39.490Z"},{"id":"ubuntu2.57c93911-cca6-11e4-9cfc-080027d9edbf","host":"mesos-dev-f999999.lhotse.ov.otto.de","ports":[],"startedAt":"2015-03-17T13:05:55.629Z","stagedAt":"2015-03-17T13:05:53.741Z","version":"2015-03-17T13:05:39.559Z"}]}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:06:10 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Task/_get/gets_tasks_of_an_app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://localhost:8080/v2/apps//ubuntu2/tasks 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"tasks":[{"appId":"/ubuntu2","id":"ubuntu2.52c55930-cca6-11e4-9cfc-080027d9edbf","host":"mesos-dev-f999999.lhotse.ov.otto.de","ports":[],"startedAt":"2015-03-17T13:05:46.755Z","stagedAt":"2015-03-17T13:05:45.326Z","version":"2015-03-17T13:05:39.490Z"},{"appId":"/ubuntu2","id":"ubuntu2.57c93911-cca6-11e4-9cfc-080027d9edbf","host":"mesos-dev-f999999.lhotse.ov.otto.de","ports":[],"startedAt":"2015-03-17T13:05:55.629Z","stagedAt":"2015-03-17T13:05:53.741Z","version":"2015-03-17T13:05:39.559Z"}]}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:06:10 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Group/_start/starts_the_group.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: http://localhost:8080/v2/groups 6 | body: 7 | encoding: UTF-8 8 | string: '{"id":"/test-group","apps":[{"backoffFactor":1.15,"backoffSeconds":1,"maxLaunchDelaySeconds":3600,"cmd":"sleep 9 | 30","constraints":[],"cpus":1.0,"dependencies":[],"disk":0.0,"env":{},"executor":"","id":"app","instances":1,"mem":128.0,"ports":[10000],"requirePorts":false,"storeUrls":[],"upgradeStrategy":{"minimumHealthCapacity":1.0},"tasks":[]}],"dependencies":[],"groups":[]}' 10 | headers: 11 | Content-Type: 12 | - application/json 13 | Accept: 14 | - application/json 15 | User-Agent: 16 | - ub0r/Marathon-API 1.1.0 17 | response: 18 | status: 19 | code: 201 20 | message: Created 21 | headers: 22 | Cache-Control: 23 | - no-cache, no-store, must-revalidate 24 | Pragma: 25 | - no-cache 26 | Expires: 27 | - '0' 28 | Location: 29 | - http://localhost:8080/v2/groups/test-group 30 | Content-Type: 31 | - application/json 32 | Transfer-Encoding: 33 | - chunked 34 | Server: 35 | - Jetty(8.y.z-SNAPSHOT) 36 | body: 37 | encoding: UTF-8 38 | string: '{"version":"2015-03-17T13:06:07.888Z","deploymentId":"b8ec8dda-f910-4339-9aff-fde4b7da31d4"}' 39 | http_version: 40 | recorded_at: Tue, 17 Mar 2015 13:06:08 GMT 41 | recorded_with: VCR 2.9.3 42 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Group/_get/gets_the_group.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://localhost:8080/v2/groups//test-group 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"id":"/test-group","apps":[{"id":"/test-group/app","cmd":"sleep 30","args":null,"user":null,"env":{},"instances":1,"cpus":1.0,"mem":128.0,"disk":0.0,"executor":"","constraints":[],"uris":[],"storeUrls":[],"ports":[10000],"requirePorts":false,"backoffSeconds":1,"backoffFactor":1.15,"maxLaunchDelaySeconds":3600,"container":null,"healthChecks":[],"dependencies":[],"upgradeStrategy":{"minimumHealthCapacity":1.0,"maximumOverCapacity":0.0},"labels":{},"version":"2015-03-17T13:06:07.888Z"}],"groups":[],"dependencies":[],"version":"2015-03-17T13:06:07.888Z"}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:06:08 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /lib/marathon/health_check.rb: -------------------------------------------------------------------------------- 1 | # This class represents a Marathon HealthCheck. 2 | # See https://mesosphere.github.io/marathon/docs/health-checks.html for full details. 3 | class Marathon::HealthCheck < Marathon::Base 4 | 5 | DEFAULTS = { 6 | :gracePeriodSeconds => 300, 7 | :intervalSeconds => 60, 8 | :maxConsecutiveFailures => 3, 9 | :path => '/', 10 | :portIndex => 0, 11 | :protocol => 'HTTP', 12 | :timeoutSeconds => 20 13 | } 14 | 15 | ACCESSORS = %w[ command gracePeriodSeconds intervalSeconds maxConsecutiveFailures 16 | path portIndex protocol timeoutSeconds ignoreHttp1xx ] 17 | 18 | # Create a new health check object. 19 | # ++hash++: Hash returned by API. 20 | def initialize(hash) 21 | super(Marathon::Util.merge_keywordized_hash(DEFAULTS, hash), ACCESSORS) 22 | Marathon::Util.validate_choice(:protocol, protocol, %w[HTTP TCP COMMAND HTTPS MESOS_HTTP MESOS_HTTPS MESOS_TCP]) 23 | end 24 | 25 | def to_s 26 | if protocol == 'COMMAND' 27 | "Marathon::HealthCheck { :protocol => #{protocol} :command => #{command} }" 28 | elsif %w[HTTP HTTPS MESOS_HTTP MESOS_HTTPS].include? protocol 29 | "Marathon::HealthCheck { :protocol => #{protocol} :portIndex => #{portIndex} :path => #{path}" + 30 | (%w[HTTP HTTPS].include? protocol and !ignoreHttp1xx.nil? ? " :ignoreHttp1xx => #{ignoreHttp1xx}" : '') + " }" 31 | else 32 | "Marathon::HealthCheck { :protocol => #{protocol} :portIndex => #{portIndex} }" 33 | end 34 | end 35 | 36 | end -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_App/_start/starts_the_app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: http://localhost:8080/v2/apps 6 | body: 7 | encoding: UTF-8 8 | string: '{"id":"/test-app","cmd":"sleep 10","instances":1,"cpus":0.1,"mem":32}' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 201 19 | message: Created 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Location: 28 | - http://localhost:8080/v2/apps/test-app 29 | Content-Type: 30 | - application/json 31 | Transfer-Encoding: 32 | - chunked 33 | Server: 34 | - Jetty(8.y.z-SNAPSHOT) 35 | body: 36 | encoding: UTF-8 37 | string: '{"id":"/test-app","cmd":"sleep 10","args":null,"user":null,"env":{},"instances":1,"cpus":0.1,"mem":32.0,"disk":0.0,"executor":"","constraints":[],"uris":[],"storeUrls":[],"ports":[0],"requirePorts":false,"backoffFactor":1.15,"container":null,"healthChecks":[],"dependencies":[],"upgradeStrategy":{"minimumHealthCapacity":1.0,"maximumOverCapacity":1.0},"labels":{},"version":"2015-03-17T13:05:38.806Z","deployments":[{"id":"92294bd9-b2a5-48be-8d02-c594d4bf3a21"}],"tasks":[],"tasksStaged":0,"tasksRunning":0,"tasksHealthy":0,"tasksUnhealthy":0,"backoffSeconds":1,"maxLaunchDelaySeconds":3600}' 38 | http_version: 39 | recorded_at: Tue, 17 Mar 2015 13:05:38 GMT 40 | recorded_with: VCR 2.9.3 41 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Task/_list/lists_tasks.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://localhost:8080/v2/tasks 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"tasks":[{"appId":"/ubuntu","id":"ubuntu.a6c4d769-cc9c-11e4-9cfc-080027d9edbf","host":"mesos-dev-f999999.lhotse.ov.otto.de","ports":[],"startedAt":"2015-03-17T11:56:36.225Z","stagedAt":"2015-03-17T11:56:31.284Z","version":"2015-03-17T11:56:26.532Z"},{"appId":"/ubuntu2","id":"ubuntu2.52c55930-cca6-11e4-9cfc-080027d9edbf","host":"mesos-dev-f999999.lhotse.ov.otto.de","ports":[],"startedAt":"2015-03-17T13:05:46.755Z","stagedAt":"2015-03-17T13:05:45.326Z","version":"2015-03-17T13:05:39.490Z"},{"appId":"/ubuntu2","id":"ubuntu2.57c93911-cca6-11e4-9cfc-080027d9edbf","host":"mesos-dev-f999999.lhotse.ov.otto.de","ports":[],"startedAt":"2015-03-17T13:05:55.629Z","stagedAt":"2015-03-17T13:05:53.741Z","version":"2015-03-17T13:05:39.559Z"}]}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:06:09 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Task/_list/lists_running_tasks.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://localhost:8080/v2/tasks?status=running 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"tasks":[{"appId":"/ubuntu","id":"ubuntu.a6c4d769-cc9c-11e4-9cfc-080027d9edbf","host":"mesos-dev-f999999.lhotse.ov.otto.de","ports":[],"startedAt":"2015-03-17T11:56:36.225Z","stagedAt":"2015-03-17T11:56:31.284Z","version":"2015-03-17T11:56:26.532Z"},{"appId":"/ubuntu2","id":"ubuntu2.52c55930-cca6-11e4-9cfc-080027d9edbf","host":"mesos-dev-f999999.lhotse.ov.otto.de","ports":[],"startedAt":"2015-03-17T13:05:46.755Z","stagedAt":"2015-03-17T13:05:45.326Z","version":"2015-03-17T13:05:39.490Z"},{"appId":"/ubuntu2","id":"ubuntu2.57c93911-cca6-11e4-9cfc-080027d9edbf","host":"mesos-dev-f999999.lhotse.ov.otto.de","ports":[],"startedAt":"2015-03-17T13:05:55.629Z","stagedAt":"2015-03-17T13:05:53.741Z","version":"2015-03-17T13:05:39.559Z"}]}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:06:09 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /spec/marathon/health_check_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Marathon::HealthCheck do 4 | 5 | describe '#init' do 6 | subject { described_class.new({'protocol' => 'TCP'}) } 7 | 8 | its(:protocol) { should == 'TCP' } 9 | its(:portIndex) { should == 0 } 10 | its(:timeoutSeconds) { should == 20 } 11 | end 12 | 13 | describe '#to_s with protocol==HTTP' do 14 | subject { described_class.new({'protocol' => 'HTTP', 'portIndex' => 0, 'path' => '/ping'}) } 15 | 16 | let(:expected_string) do 17 | 'Marathon::HealthCheck { :protocol => HTTP :portIndex => 0 :path => /ping }' 18 | end 19 | 20 | its(:to_s) { should == expected_string } 21 | end 22 | 23 | describe '#to_s with protocol==TCP' do 24 | subject { described_class.new({'protocol' => 'TCP', 'portIndex' => 0, 'path' => '/ping'}) } 25 | 26 | let(:expected_string) do 27 | 'Marathon::HealthCheck { :protocol => TCP :portIndex => 0 }' 28 | end 29 | 30 | its(:to_s) { should == expected_string } 31 | end 32 | 33 | describe '#to_s with protocol==COMMAND' do 34 | subject { described_class.new({'protocol' => 'COMMAND', 'command' => 'true'}) } 35 | 36 | let(:expected_string) do 37 | 'Marathon::HealthCheck { :protocol => COMMAND :command => true }' 38 | end 39 | 40 | its(:to_s) { should == expected_string } 41 | end 42 | 43 | describe '#to_json' do 44 | subject { described_class.new({'protocol' => 'HTTP', 'portIndex' => 0, 'path' => '/ping'}) } 45 | 46 | its(:to_json) { should == '{"gracePeriodSeconds":300,"intervalSeconds":60,"maxConsecutiveFailures":3,' \ 47 | + '"path":"/ping","portIndex":0,"protocol":"HTTP","timeoutSeconds":20}' } 48 | end 49 | 50 | end -------------------------------------------------------------------------------- /spec/marathon/container_docker_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | CONTAINER_DOCKER_EXAMPLE = { 4 | :network => 'HOST', 5 | :image => 'felixb/yocto-httpd', 6 | :privileged => false 7 | } 8 | 9 | describe Marathon::ContainerDocker do 10 | 11 | describe '#init' do 12 | subject { described_class } 13 | 14 | it 'should fail with invalid network' do 15 | expect { subject.new(:network => 'foo', :image => 'foo') } 16 | .to raise_error(Marathon::Error::ArgumentError, /network must be one of /) 17 | end 18 | 19 | it 'should fail w/o image' do 20 | expect { subject.new({}) } 21 | .to raise_error(Marathon::Error::ArgumentError, /image must not be/) 22 | end 23 | end 24 | 25 | describe '#attributes' do 26 | subject { described_class.new(CONTAINER_DOCKER_EXAMPLE) } 27 | 28 | its(:network) { should == 'HOST' } 29 | its(:image) { should == 'felixb/yocto-httpd' } 30 | its(:portMappings) { should == [] } 31 | its(:privileged) { should == false } 32 | end 33 | describe '#privileged' do 34 | subject { described_class.new({ 35 | :network => 'HOST', 36 | :image => 'felixb/yocto-httpd', 37 | :privileged => true 38 | }) 39 | } 40 | its(:privileged) { should == true } 41 | end 42 | 43 | describe '#to_s' do 44 | subject { described_class.new(CONTAINER_DOCKER_EXAMPLE) } 45 | 46 | let(:expected_string) do 47 | 'Marathon::ContainerDocker { :image => felixb/yocto-httpd }' 48 | end 49 | 50 | its(:to_s) { should == expected_string } 51 | its(:to_pretty_s) { should == 'felixb/yocto-httpd' } 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon/_info/returns_the_info_hash.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://localhost:8080/v2/info 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"name":"marathon","http_config":{"assets_path":null,"http_port":8080,"https_port":8443},"frameworkId":"20150210-083349-1160423616-5050-9698-0000","leader":null,"event_subscriber":{"type":"http_callback","http_endpoints":["http://127.0.0.1:8083/marathon/event"]},"marathon_config":{"local_port_max":20000,"local_port_min":10000,"hostname":"mesos-dev-f999998.lhotse.ov.otto.de","master":"zk://192.168.42.69:2181/develop/mesos","reconciliation_interval":75000,"mesos_role":null,"task_launch_timeout":300000,"reconciliation_initial_delay":15000,"ha":true,"failover_timeout":75,"checkpoint":true,"webui_url":null,"executor":"//cmd","marathon_store_timeout":2000,"mesos_user":"root"},"version":"0.8.1","zookeeper_config":{"zk_path":"/develop/marathon","zk":"zk://192.168.42.69:2181/develop/marathon","zk_timeout":10,"zk_hosts":"192.168.42.69:2181","zk_future_timeout":{"duration":10}},"elected":false}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:06:09 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /spec/marathon/deployment_info_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | DEPLOYMENT_INFO_EXAMPLE = { 4 | 'deploymentId' => 'deployment-123', 5 | 'version' => 'version-456' 6 | } 7 | 8 | describe Marathon::DeploymentInfo do 9 | 10 | describe '#attributes' do 11 | subject { described_class.new(DEPLOYMENT_INFO_EXAMPLE, double(Marathon::MarathonInstance)) } 12 | 13 | its(:deploymentId) { should == 'deployment-123' } 14 | its(:version) { should == 'version-456' } 15 | end 16 | 17 | describe '#wait' do 18 | before(:each) do 19 | @deployments = double(Marathon::Deployments) 20 | @subject = described_class.new(DEPLOYMENT_INFO_EXAMPLE, 21 | double(Marathon::MarathonInstance, :deployments => @deployments)) 22 | end 23 | 24 | it 'waits for the deployment' do 25 | expect(@deployments).to receive(:list) { [] } 26 | @subject.wait 27 | end 28 | end 29 | 30 | describe '#to_s' do 31 | subject { described_class.new(DEPLOYMENT_INFO_EXAMPLE, double(Marathon::MarathonInstance)) } 32 | 33 | let(:expected_string) do 34 | 'Marathon::DeploymentInfo { :version => version-456 :deploymentId => deployment-123 }' 35 | end 36 | 37 | its(:to_s) { should == expected_string } 38 | end 39 | 40 | describe '#to_s w/o deploymentId' do 41 | subject { described_class.new({:version => 'foo-version'}, double(Marathon::MarathonInstance)) } 42 | 43 | let(:expected_string) do 44 | 'Marathon::DeploymentInfo { :version => foo-version }' 45 | end 46 | 47 | its(:to_s) { should == expected_string } 48 | end 49 | 50 | describe '#to_json' do 51 | subject { described_class.new(DEPLOYMENT_INFO_EXAMPLE, double(Marathon::MarathonInstance)) } 52 | 53 | its(:to_json) { should == DEPLOYMENT_INFO_EXAMPLE.to_json } 54 | end 55 | 56 | end 57 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_App/_get/gets_the_app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://localhost:8080/v2/apps//ubuntu 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"app":{"id":"/ubuntu","cmd":"while sleep 10; do date -u +%T; done","args":null,"user":null,"env":{},"instances":1,"cpus":0.1,"mem":64.0,"disk":0.0,"executor":"","constraints":[],"uris":[],"storeUrls":[],"ports":[],"requirePorts":false,"backoffSeconds":1,"backoffFactor":1.15,"maxLaunchDelaySeconds":3600,"container":{"type":"DOCKER","volumes":[],"docker":{"image":"libmesos/ubuntu","privileged":false,"parameters":[]}},"healthChecks":[],"dependencies":[],"upgradeStrategy":{"minimumHealthCapacity":1.0,"maximumOverCapacity":1.0},"labels":{},"version":"2015-03-17T11:56:26.532Z","tasksStaged":0,"tasksRunning":1,"tasksHealthy":0,"tasksUnhealthy":0,"deployments":[],"tasks":[{"id":"ubuntu.a6c4d769-cc9c-11e4-9cfc-080027d9edbf","host":"mesos-dev-f999999.lhotse.ov.otto.de","ports":[],"startedAt":"2015-03-17T11:56:36.225Z","stagedAt":"2015-03-17T11:56:31.284Z","version":"2015-03-17T11:56:26.532Z","appId":"/ubuntu"}],"lastTaskFailure":null}}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:05:39 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /spec/marathon/error_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Marathon::Error do 4 | 5 | describe '.error_class' do 6 | subject { described_class } 7 | 8 | it 'returns ClientError on 400' do 9 | expect(subject.error_class(Net::HTTPResponse.new(1.1, 400, 'Client Error'))) 10 | .to be(Marathon::Error::ClientError) 11 | end 12 | 13 | it 'returns ClientError on 422' do 14 | expect(subject.error_class(Net::HTTPResponse.new(1.1, 422, 'Client Error'))) 15 | .to be(Marathon::Error::ClientError) 16 | end 17 | 18 | it 'returns NotFoundError on 404' do 19 | expect(subject.error_class(Net::HTTPResponse.new(1.1, 404, 'Not Found'))) 20 | .to be(Marathon::Error::NotFoundError) 21 | end 22 | 23 | it 'returns UnexpectedResponseError anything else' do 24 | expect(subject.error_class(Net::HTTPResponse.new(1.1, 599, 'Whatever'))) 25 | .to be(Marathon::Error::UnexpectedResponseError) 26 | end 27 | end 28 | 29 | describe '.error_message' do 30 | subject { described_class } 31 | 32 | it 'returns "message" from respose json' do 33 | r = {'message' => 'fooo'} 34 | expect(r).to receive(:parsed_response) { r } 35 | expect(subject.error_message(r)).to eq('fooo') 36 | end 37 | 38 | it 'returns "errors" from respose json' do 39 | r = {'errors' => 'fooo'} 40 | expect(r).to receive(:parsed_response) { r } 41 | expect(subject.error_message(r)).to eq('fooo') 42 | end 43 | 44 | it 'returns full hash from respose json, if keys are missing' do 45 | r = {'bars' => 'fooo'} 46 | expect(r).to receive(:parsed_response) { r } 47 | expect(subject.error_message(r)).to eq(r) 48 | end 49 | 50 | it 'returns full body if not a hash with "message"' do 51 | r = 'fooo' 52 | expect(r).to receive(:parsed_response) { r } 53 | expect(subject.error_message(r)).to eq('fooo') 54 | end 55 | end 56 | 57 | end -------------------------------------------------------------------------------- /spec/marathon/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Marathon::Base do 4 | 5 | describe '#init' do 6 | subject { described_class } 7 | 8 | it 'fails with strange input' do 9 | expect { subject.new('foo') }.to raise_error(Marathon::Error::ArgumentError) 10 | expect { subject.new({}, 'foo') }.to raise_error(Marathon::Error::ArgumentError) 11 | expect { subject.new(nil) }.to raise_error(Marathon::Error::ArgumentError) 12 | expect { subject.new({}, nil) }.to raise_error(Marathon::Error::ArgumentError) 13 | expect { subject.new([], ['foo']) }.to raise_error(Marathon::Error::ArgumentError) 14 | end 15 | end 16 | 17 | describe '#to_json' do 18 | subject { described_class.new({ 19 | 'app' => {'id' => '/app/foo'}, 20 | :foo => 'blubb', 21 | :bar => 1 22 | }) } 23 | 24 | let(:expected_string) do 25 | '{"foo":"blubb","bar":1,"app":{"id":"/app/foo"}}' 26 | end 27 | 28 | its(:to_json) { should == expected_string } 29 | end 30 | 31 | describe '#attr_readers' do 32 | subject { described_class.new({ 33 | 'foo' => 'blubb', 34 | :bar => 1 35 | }, [:foo, 'bar']) } 36 | 37 | its(:info) { should == {:foo => 'blubb', :bar => 1} } 38 | its(:foo) { should == 'blubb' } 39 | its(:bar) { should == 1 } 40 | end 41 | 42 | describe '#attr_readers, from string array' do 43 | subject { described_class.new({ 44 | 'foo' => 'blubb', 45 | :bar => 1 46 | }, %w[foo bar]) } 47 | 48 | its(:info) { should == {:foo => 'blubb', :bar => 1} } 49 | its(:foo) { should == 'blubb' } 50 | its(:bar) { should == 1 } 51 | end 52 | 53 | end 54 | -------------------------------------------------------------------------------- /spec/marathon/queue_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Marathon::Queue do 4 | 5 | describe '#attributes' do 6 | subject { described_class.new({ 7 | 'app' => {'id' => '/app/foo'}, 8 | 'delay' => {'overdue' => true} 9 | }, double(Marathon::MarathonInstance)) } 10 | 11 | it 'has app' do 12 | expect(subject.app).to be_instance_of(Marathon::App) 13 | expect(subject.app.id).to eq('/app/foo') 14 | end 15 | 16 | it 'has a read only app' do 17 | expect(subject.app.read_only).to be(true) 18 | end 19 | 20 | it 'has delay' do 21 | expect(subject.delay).to be_instance_of(Hash) 22 | expect(subject.delay[:overdue]).to be(true) 23 | end 24 | end 25 | 26 | describe '#to_s' do 27 | subject { described_class.new({ 28 | 'app' => {'id' => '/app/foo'}, 29 | 'delay' => {'overdue' => true} 30 | }, double(Marathon::MarathonInstance)) } 31 | 32 | let(:expected_string) do 33 | 'Marathon::Queue { :appId => /app/foo :delay => {:overdue=>true} }' 34 | end 35 | 36 | its(:to_s) { should == expected_string } 37 | end 38 | 39 | describe '#to_json' do 40 | subject { described_class.new({ 41 | 'app' => {'id' => '/app/foo'}, 42 | 'delay' => {'overdue' => true} 43 | }, double(Marathon::MarathonInstance)) } 44 | 45 | let(:expected_string) do 46 | '{"app":{"id":"/app/foo"},"delay":{"overdue":true}}' 47 | end 48 | 49 | its(:to_json) { should == expected_string } 50 | end 51 | 52 | describe '.list' do 53 | subject { described_class } 54 | 55 | it 'lists queue', :vcr do 56 | queue = described_class.list 57 | expect(queue).to be_instance_of(Array) 58 | end 59 | end 60 | 61 | end 62 | -------------------------------------------------------------------------------- /TESTING.md: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | To develop on this gem, you must the following installed: 3 | * a sane Ruby 1.9+ environment with `bundler` 4 | ```shell 5 | $ gem install bundler 6 | ``` 7 | * Local Marathon v0.8.0 or greater running on Port 8080. Use [Vagrant][1] or [docker][2] to prevent local installation. 8 | 9 | 10 | 11 | # Getting Started 12 | 1. Clone the git repository from Github: 13 | ```shell 14 | $ git clone git@github.com:felixb/marathon-api.git 15 | ``` 16 | 2. Install the dependencies using Bundler 17 | ```shell 18 | $ bundle install 19 | ``` 20 | 3. Create a branch for your changes 21 | ```shell 22 | $ git checkout -b my_bug_fix 23 | ``` 24 | 4. Make any changes 25 | 5. Write tests to support those changes. 26 | 6. Run the tests: 27 | * `bundle exec rake vcr:test` 28 | 7. Assuming the tests pass, open a Pull Request on Github. 29 | 30 | # Using Rakefile Commands 31 | This repository comes with five Rake commands to assist in your testing of the code. 32 | 33 | ## `rake spec` 34 | This command will run Rspec tests normally on your local system. Be careful that VCR will behave "weirdly" if you currently have the Docker daemon running. 35 | 36 | ## `rake quality` 37 | This command runs a code quality threshold checker to hinder bad code. 38 | 39 | ## `rake vcr` 40 | This gem uses [VCR](https://relishapp.com/vcr/vcr) to record and replay HTTP requests made to the Docker API. The `vcr` namespace is used to record and replay spec tests inside of a Docker container. This will allow each developer to run and rerecord VCR cassettes in a consistent environment. 41 | 42 | ### `rake vcr:record` 43 | This is the command you will use to record a new set of VCR cassettes. This command runs the following procedures: 44 | 1. Delete the existing `fixtures/vcr` directory. 45 | 2. Launch some tasks on local Marathon instance 46 | 3. Record new VCR cassettes by running the Rspec test suite against the local Marathon instance. 47 | 48 | [1]: https://github.com/everpeace/vagrant-mesos 49 | [2]: https://registry.hub.docker.com/u/mesosphere/marathon/ -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_App/_changes/changes_the_app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: put 5 | uri: http://localhost:8080/v2/apps//ubuntu2?force=true 6 | body: 7 | encoding: UTF-8 8 | string: '{"instances":2}' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"version":"2015-03-17T13:07:11.009Z","deploymentId":"442a4787-a964-4a16-889e-4fed7944ad7f"}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:07:11 GMT 38 | - request: 39 | method: put 40 | uri: http://localhost:8080/v2/apps//ubuntu2?force=true 41 | body: 42 | encoding: UTF-8 43 | string: '{"instances":1}' 44 | headers: 45 | Content-Type: 46 | - application/json 47 | Accept: 48 | - application/json 49 | User-Agent: 50 | - ub0r/Marathon-API 1.1.0 51 | response: 52 | status: 53 | code: 200 54 | message: OK 55 | headers: 56 | Cache-Control: 57 | - no-cache, no-store, must-revalidate 58 | Pragma: 59 | - no-cache 60 | Expires: 61 | - '0' 62 | Content-Type: 63 | - application/json 64 | Transfer-Encoding: 65 | - chunked 66 | Server: 67 | - Jetty(8.y.z-SNAPSHOT) 68 | body: 69 | encoding: UTF-8 70 | string: '{"version":"2015-03-17T13:07:11.094Z","deploymentId":"25c06868-441f-4af4-95aa-96e1862e8553"}' 71 | http_version: 72 | recorded_at: Tue, 17 Mar 2015 13:07:11 GMT 73 | recorded_with: VCR 2.9.3 74 | -------------------------------------------------------------------------------- /spec/marathon/container_docker_port_mapping_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | CONTAINER_DOCKER_PORT_MAPPING_EXAMPLE = { 4 | :protocol => 'tcp', 5 | :hostPort => 0, 6 | :containerPort => 8080 7 | } 8 | 9 | describe Marathon::ContainerDockerPortMapping do 10 | 11 | describe '#init' do 12 | subject { described_class } 13 | 14 | it 'should fail with invalid protocol' do 15 | expect { subject.new(:protocol => 'foo', :containerPort => 8080) } 16 | .to raise_error(Marathon::Error::ArgumentError, /protocol must be one of /) 17 | end 18 | 19 | it 'should fail with invalid containerPort' do 20 | expect { subject.new(:containerPort => 'foo') } 21 | .to raise_error(Marathon::Error::ArgumentError, /containerPort must be/) 22 | expect { subject.new(:containerPort => 0) } 23 | .not_to raise_error 24 | expect { subject.new(:containerPort => -1) } 25 | .to raise_error(Marathon::Error::ArgumentError, /containerPort must be/) 26 | end 27 | 28 | it 'should fail with invalid hostPort' do 29 | expect { subject.new(:hostPort => 'foo', :containerPort => 8080) } 30 | .to raise_error(Marathon::Error::ArgumentError, /hostPort must be/) 31 | expect { subject.new(:hostPort => -1, :containerPort => 8080) } 32 | .to raise_error(Marathon::Error::ArgumentError, /hostPort must be/) 33 | end 34 | end 35 | 36 | describe '#attributes' do 37 | subject { described_class.new(CONTAINER_DOCKER_PORT_MAPPING_EXAMPLE) } 38 | 39 | its(:protocol) { should == 'tcp' } 40 | its(:hostPort) { should == 0 } 41 | its(:containerPort) { should == 8080 } 42 | end 43 | 44 | describe '#to_s' do 45 | subject { described_class.new(CONTAINER_DOCKER_PORT_MAPPING_EXAMPLE) } 46 | 47 | let(:expected_string) do 48 | 'Marathon::ContainerDockerPortMapping { :protocol => tcp :containerPort => 8080 :hostPort => 0 }' 49 | end 50 | 51 | its(:to_s) { should == expected_string } 52 | its(:to_pretty_s) { should == 'tcp/8080:0' } 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Group/_changes/changes_the_group.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: put 5 | uri: http://localhost:8080/v2/groups//test-group?force=true 6 | body: 7 | encoding: UTF-8 8 | string: '{"instances":2}' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"version":"2015-03-17T13:06:08.503Z","deploymentId":"7eb64062-acb9-43c5-8f0f-0c8e2837c868"}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:06:08 GMT 38 | - request: 39 | method: put 40 | uri: http://localhost:8080/v2/groups//test-group?force=true 41 | body: 42 | encoding: UTF-8 43 | string: '{"instances":1}' 44 | headers: 45 | Content-Type: 46 | - application/json 47 | Accept: 48 | - application/json 49 | User-Agent: 50 | - ub0r/Marathon-API 1.1.0 51 | response: 52 | status: 53 | code: 200 54 | message: OK 55 | headers: 56 | Cache-Control: 57 | - no-cache, no-store, must-revalidate 58 | Pragma: 59 | - no-cache 60 | Expires: 61 | - '0' 62 | Content-Type: 63 | - application/json 64 | Transfer-Encoding: 65 | - chunked 66 | Server: 67 | - Jetty(8.y.z-SNAPSHOT) 68 | body: 69 | encoding: UTF-8 70 | string: '{"version":"2015-03-17T13:06:08.649Z","deploymentId":"e6232eee-4370-4d50-9215-a5c57ac14304"}' 71 | http_version: 72 | recorded_at: Tue, 17 Mar 2015 13:06:08 GMT 73 | recorded_with: VCR 2.9.3 74 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Deployment/_delete/deletes_deployments.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: put 5 | uri: http://localhost:8080/v2/apps//test-app?force=true 6 | body: 7 | encoding: UTF-8 8 | string: '{"instances":1}' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"version":"2015-03-17T13:06:00.450Z","deploymentId":"82213a8e-87fe-48ba-9b1e-2c2d7f68abda"}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:06:01 GMT 38 | - request: 39 | method: delete 40 | uri: http://localhost:8080/v2/deployments/82213a8e-87fe-48ba-9b1e-2c2d7f68abda 41 | body: 42 | encoding: US-ASCII 43 | string: '' 44 | headers: 45 | Content-Type: 46 | - application/json 47 | Accept: 48 | - application/json 49 | User-Agent: 50 | - ub0r/Marathon-API 1.1.0 51 | response: 52 | status: 53 | code: 200 54 | message: OK 55 | headers: 56 | Cache-Control: 57 | - no-cache, no-store, must-revalidate 58 | Pragma: 59 | - no-cache 60 | Expires: 61 | - '0' 62 | Content-Type: 63 | - application/json 64 | Transfer-Encoding: 65 | - chunked 66 | Server: 67 | - Jetty(8.y.z-SNAPSHOT) 68 | body: 69 | encoding: UTF-8 70 | string: '{"version":"2015-03-17T13:06:01.878Z","deploymentId":"7c36a5ea-f928-4387-8206-69b111121210"}' 71 | http_version: 72 | recorded_at: Tue, 17 Mar 2015 13:06:03 GMT 73 | recorded_with: VCR 2.9.3 74 | -------------------------------------------------------------------------------- /lib/marathon/event_subscriptions.rb: -------------------------------------------------------------------------------- 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 | def initialize(marathon_instance = Marathon.singleton) 6 | @connection = marathon_instance.connection 7 | end 8 | 9 | # List all event subscriber callback URLs. 10 | # Returns a list of strings/URLs. 11 | def list 12 | json = @connection.get('/v2/eventSubscriptions') 13 | json['callbackUrls'] 14 | end 15 | 16 | # Register a callback URL as an event subscriber. 17 | # ++callbackUrl++: URL to which events should be posted. 18 | # Returns an event as hash. 19 | def register(callbackUrl) 20 | query = {} 21 | query[:callbackUrl] = callbackUrl 22 | json = @connection.post('/v2/eventSubscriptions', query) 23 | json 24 | end 25 | 26 | # Unregister a callback URL from the event subscribers list. 27 | # ++callbackUrl++: URL passed when the event subscription was created. 28 | # Returns an event as hash. 29 | def unregister(callbackUrl) 30 | query = {} 31 | query[:callbackUrl] = callbackUrl 32 | json = @connection.delete('/v2/eventSubscriptions', query) 33 | json 34 | end 35 | 36 | 37 | class << self 38 | # List all event subscriber callback URLs. 39 | # Returns a list of strings/URLs. 40 | def list 41 | Marathon.singleton.event_subscriptions.list 42 | end 43 | 44 | # Register a callback URL as an event subscriber. 45 | # ++callbackUrl++: URL to which events should be posted. 46 | # Returns an event as hash. 47 | def register(callbackUrl) 48 | Marathon.singleton.event_subscriptions.register(callbackUrl) 49 | end 50 | 51 | # Unregister a callback URL from the event subscribers list. 52 | # ++callbackUrl++: URL passed when the event subscription was created. 53 | # Returns an event as hash. 54 | def unregister(callbackUrl) 55 | Marathon.singleton.event_subscriptions.unregister(callbackUrl) 56 | end 57 | end 58 | end -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_App/_list/lists_apps.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://localhost:8080/v2/apps 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"apps":[{"id":"/ubuntu","cmd":"while sleep 10; do date -u +%T; done","args":null,"user":null,"env":{},"instances":1,"cpus":0.1,"mem":64.0,"disk":0.0,"executor":"","constraints":[],"uris":[],"storeUrls":[],"ports":[],"requirePorts":false,"backoffSeconds":1,"backoffFactor":1.15,"maxLaunchDelaySeconds":3600,"container":{"type":"DOCKER","volumes":[],"docker":{"image":"libmesos/ubuntu","privileged":false,"parameters":[]}},"healthChecks":[],"dependencies":[],"upgradeStrategy":{"minimumHealthCapacity":1.0,"maximumOverCapacity":1.0},"labels":{},"version":"2015-03-17T11:56:26.532Z","tasksStaged":0,"tasksRunning":1,"tasksHealthy":0,"tasksUnhealthy":0,"deployments":[]},{"id":"/ubuntu2","cmd":"while 36 | sleep 10; do date -u +%T; done","args":null,"user":null,"env":{},"instances":1,"cpus":0.1,"mem":64.0,"disk":0.0,"executor":"","constraints":[["hostname","GROUP_BY"]],"uris":[],"storeUrls":[],"ports":[],"requirePorts":false,"backoffSeconds":1,"backoffFactor":1.15,"maxLaunchDelaySeconds":3600,"container":{"type":"DOCKER","volumes":[],"docker":{"image":"libmesos/ubuntu","privileged":false,"parameters":[]}},"healthChecks":[],"dependencies":[],"upgradeStrategy":{"minimumHealthCapacity":1.0,"maximumOverCapacity":1.0},"labels":{},"version":"2015-03-17T13:04:37.200Z","tasksStaged":0,"tasksRunning":1,"tasksHealthy":0,"tasksUnhealthy":0,"deployments":[]}]}' 37 | http_version: 38 | recorded_at: Tue, 17 Mar 2015 13:05:38 GMT 39 | recorded_with: VCR 2.9.3 40 | -------------------------------------------------------------------------------- /lib/marathon/error.rb: -------------------------------------------------------------------------------- 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; 6 | end 7 | 8 | # Raised when invalid arguments are passed to a method. 9 | class ArgumentError < MarathonError; 10 | end 11 | 12 | # Raised when a request returns a 400 or 422. 13 | class ClientError < MarathonError; 14 | end 15 | 16 | # Raised when a request returns a 404. 17 | class NotFoundError < MarathonError; 18 | end 19 | 20 | # Raised when there is an unexpected response code / body. 21 | class UnexpectedResponseError < MarathonError; 22 | attr_accessor :response 23 | end 24 | 25 | # Raised when a request times out. 26 | class TimeoutError < MarathonError; 27 | end 28 | 29 | # Raised when login fails. 30 | class AuthenticationError < MarathonError; 31 | end 32 | 33 | # Raised when an IO action fails. 34 | class IOError < MarathonError; 35 | end 36 | 37 | # Raise error specific to http response. 38 | # ++response++: HTTParty response object. 39 | def from_response(response) 40 | error_class(response).new(error_message(response)).tap do |err| 41 | err.response = response if err.is_a?(UnexpectedResponseError) 42 | end 43 | end 44 | 45 | private 46 | 47 | # Get reponse code specific error class. 48 | # ++response++: HTTParty response object. 49 | def error_class(response) 50 | case response.code 51 | when 400 52 | ClientError 53 | when 422 54 | ClientError 55 | when 404 56 | NotFoundError 57 | else 58 | UnexpectedResponseError 59 | end 60 | end 61 | 62 | # Get response code from http response. 63 | # ++response++: HTTParty response object. 64 | def error_message(response) 65 | body = response.parsed_response 66 | if not body.is_a?(Hash) 67 | body 68 | elsif body['message'] 69 | body['message'] 70 | elsif body['errors'] 71 | body['errors'] 72 | else 73 | body 74 | end 75 | rescue JSON::ParserError 76 | body 77 | end 78 | 79 | module_function :error_class, :error_message, :from_response 80 | 81 | end 82 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_App/_versions/gets_versions.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://localhost:8080/v2/apps//ubuntu2/versions 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"versions":["2015-03-17T13:05:39.559Z","2015-03-17T13:05:39.490Z","2015-03-17T13:04:37.200Z","2015-03-17T13:04:37.060Z","2015-03-17T13:04:36.997Z","2015-03-17T12:06:04.971Z","2015-03-17T12:06:00.843Z","2015-03-17T12:06:00.788Z","2015-03-17T12:04:49.309Z","2015-03-17T12:04:47.711Z","2015-03-17T12:04:47.484Z","2015-03-17T12:04:33.718Z","2015-03-17T12:04:32.686Z","2015-03-17T12:04:32.630Z","2015-03-17T12:04:30.244Z","2015-03-17T12:03:31.426Z","2015-03-17T12:03:26.347Z","2015-03-17T12:03:26.281Z","2015-03-17T12:02:23.385Z","2015-03-17T12:02:15.239Z","2015-03-17T12:02:15.195Z","2015-03-17T12:00:37.036Z","2015-03-17T12:00:28.288Z","2015-03-17T12:00:27.750Z","2015-03-17T12:00:09.006Z","2015-03-17T12:00:08.822Z","2015-03-17T12:00:08.757Z","2015-03-17T11:59:06.274Z","2015-03-17T11:59:06.059Z","2015-03-17T11:59:05.997Z","2015-03-17T11:57:04.811Z","2015-03-17T11:57:02.136Z","2015-03-17T11:57:01.341Z","2015-03-17T11:56:31.250Z","2015-03-17T11:56:28.537Z","2015-03-17T11:56:28.397Z","2015-03-17T11:52:04.570Z","2015-03-17T11:52:04.421Z","2015-03-17T11:52:04.355Z","2015-03-17T11:50:01.156Z","2015-03-17T11:49:57.243Z","2015-03-17T11:49:57.183Z","2015-03-17T11:42:41.281Z","2015-03-17T10:34:05.973Z","2015-03-17T10:33:19.189Z","2015-03-17T10:32:51.153Z","2015-03-17T10:32:34.010Z","2015-03-17T10:31:44.527Z","2015-03-17T10:19:02.815Z","2015-03-17T10:12:14.793Z"]}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:05:49 GMT 38 | recorded_with: VCR 2.9.3 39 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Task/_delete/kills_a_tasks_of_an_app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://localhost:8080/v2/apps//ubuntu2/tasks 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"tasks":[{"appId":"/ubuntu2","id":"ubuntu2.52c55930-cca6-11e4-9cfc-080027d9edbf","host":"mesos-dev-f999999.lhotse.ov.otto.de","ports":[],"startedAt":"2015-03-17T13:05:46.755Z","stagedAt":"2015-03-17T13:05:45.326Z","version":"2015-03-17T13:05:39.490Z"},{"appId":"/ubuntu2","id":"ubuntu2.57c93911-cca6-11e4-9cfc-080027d9edbf","host":"mesos-dev-f999999.lhotse.ov.otto.de","ports":[],"startedAt":"2015-03-17T13:05:55.629Z","stagedAt":"2015-03-17T13:05:53.741Z","version":"2015-03-17T13:05:39.559Z"}]}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:06:10 GMT 38 | - request: 39 | method: post 40 | uri: http://localhost:8080/v2/tasks/delete 41 | body: 42 | encoding: UTF-8 43 | string: '{"ids":["ubuntu2.52c55930-cca6-11e4-9cfc-080027d9edbf"]}' 44 | headers: 45 | Content-Type: 46 | - application/json 47 | Accept: 48 | - application/json 49 | User-Agent: 50 | - ub0r/Marathon-API 1.1.0 51 | response: 52 | status: 53 | code: 200 54 | message: OK 55 | headers: 56 | Cache-Control: 57 | - no-cache, no-store, must-revalidate 58 | Pragma: 59 | - no-cache 60 | Expires: 61 | - '0' 62 | Content-Type: 63 | - application/json 64 | Content-Length: 65 | - '0' 66 | Server: 67 | - Jetty(8.y.z-SNAPSHOT) 68 | body: 69 | encoding: UTF-8 70 | string: '' 71 | http_version: 72 | recorded_at: Tue, 17 Mar 2015 13:06:10 GMT 73 | recorded_with: VCR 2.9.3 74 | -------------------------------------------------------------------------------- /spec/marathon/marathon_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Marathon do 4 | 5 | describe '.url=' do 6 | subject { described_class } 7 | 8 | it 'sets new url' do 9 | described_class.url = 'http://foo' 10 | expect(described_class.url).to eq('http://foo') 11 | 12 | # reset connection after running this spec 13 | described_class.url = nil 14 | end 15 | 16 | it 'resets connection' do 17 | old_connection = described_class.connection 18 | described_class.url = 'http://bar' 19 | 20 | expect(described_class.connection).not_to be(old_connection) 21 | 22 | # reset connection after running this spec 23 | described_class.url = nil 24 | end 25 | end 26 | 27 | describe '.options=' do 28 | subject { described_class } 29 | 30 | it 'sets new options' do 31 | described_class.options = {:foo => 'bar'} 32 | expect(described_class.options).to eq({:foo => 'bar'}) 33 | 34 | # reset connection after running this spec 35 | described_class.options = nil 36 | end 37 | 38 | it 'resets connection' do 39 | old_connection = described_class.connection 40 | described_class.options = {:foo => 'bar'} 41 | 42 | expect(described_class.connection).not_to be(old_connection) 43 | 44 | # reset connection after running this spec 45 | described_class.options = nil 46 | end 47 | 48 | it 'adds :basic_auth options for :username and :password' do 49 | described_class.options = {:username => 'user', :password => 'password'} 50 | expect(described_class.connection.options) 51 | .to eq({:basic_auth => {:username => 'user', :password => 'password'}}) 52 | 53 | # reset connection after running this spec 54 | described_class.options = nil 55 | end 56 | end 57 | 58 | describe '.info' do 59 | subject { described_class } 60 | 61 | let(:info) { subject.info } 62 | let(:keys) do 63 | %w[ elected event_subscriber frameworkId http_config leader 64 | marathon_config name version zookeeper_config ] 65 | end 66 | 67 | it 'returns the info hash', :vcr do 68 | expect(info).to be_a Hash 69 | expect(info.keys.sort).to eq keys 70 | end 71 | end 72 | 73 | describe '.metrics' do 74 | subject { described_class } 75 | 76 | let(:metrics) { subject.metrics } 77 | let(:keys) do 78 | %w[ version gauges counters histograms meters timers ] 79 | end 80 | 81 | it 'returns the metrics hash', :vcr do 82 | expect(metrics).to be_a Hash 83 | expect(metrics.keys.sort).to eq keys.sort 84 | end 85 | end 86 | 87 | describe '.ping', :vcr do 88 | subject { described_class } 89 | let(:ping) { subject.ping } 90 | 91 | it 'returns pong' do 92 | ping.should == "pong\n" 93 | end 94 | 95 | it 'handles incorrect content type' do 96 | ping.should =~ /pong/ 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/marathon/container_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | def container_helper type 4 | return { 5 | :type => type, 6 | :docker => { 7 | :image => 'felixb/yocto-httpd', 8 | :portMappings => [{:containerPort => 8080}] 9 | }, 10 | :volumes => [{ 11 | :containerPath => '/data', 12 | :hostPath => '/var/opt/foo' 13 | }] 14 | } 15 | end 16 | 17 | describe Marathon::Container do 18 | 19 | context 'when type="DOCKER"' do 20 | 21 | describe '#attributes' do 22 | subject { described_class.new(container_helper "DOCKER") } 23 | 24 | its(:type) { should == 'DOCKER' } 25 | its(:docker) { should be_instance_of(Marathon::ContainerDocker) } 26 | its("docker.portMappings") { should be_instance_of(Array) } 27 | its("docker.portMappings.first") { should be_instance_of(Marathon::ContainerDockerPortMapping) } 28 | its("docker.portMappings.first.containerPort") { should == 8080 } 29 | its(:volumes) { should be_instance_of(Array) } 30 | its("volumes.first") { should be_instance_of(Marathon::ContainerVolume) } 31 | its("volumes.first.containerPath") { should == '/data' } 32 | end 33 | 34 | describe '#to_s' do 35 | subject { described_class.new(container_helper "DOCKER") } 36 | 37 | let(:expected_string) do 38 | 'Marathon::Container { :type => DOCKER :docker => felixb/yocto-httpd :volumes => /data:/var/opt/foo:RW }' 39 | end 40 | 41 | its(:to_s) { should == expected_string } 42 | end 43 | 44 | end 45 | 46 | context 'when type="MESOS"' do 47 | 48 | describe '#attributes' do 49 | subject { described_class.new(container_helper "MESOS") } 50 | 51 | its(:type) { should == 'MESOS' } 52 | its(:docker) { should be_instance_of(Marathon::ContainerDocker) } 53 | its("docker.portMappings") { should be_instance_of(Array) } 54 | its("docker.portMappings.first") { should be_instance_of(Marathon::ContainerDockerPortMapping) } 55 | its("docker.portMappings.first.containerPort") { should == 8080 } 56 | its(:volumes) { should be_instance_of(Array) } 57 | its("volumes.first") { should be_instance_of(Marathon::ContainerVolume) } 58 | its("volumes.first.containerPath") { should == '/data' } 59 | end 60 | 61 | describe '#to_s' do 62 | subject { described_class.new(container_helper "MESOS") } 63 | 64 | let(:expected_string) do 65 | 'Marathon::Container { :type => MESOS :docker => felixb/yocto-httpd :volumes => /data:/var/opt/foo:RW }' 66 | end 67 | 68 | its(:to_s) { should == expected_string } 69 | end 70 | 71 | end 72 | 73 | context 'when type="CHUCK_NORRIS"' do 74 | describe "#new" do 75 | it "Should raise Arguement:Error" do 76 | expected = expect do 77 | Marathon::Container.new(container_helper "CHUCK_NORRIS") 78 | end 79 | expected.to raise_error(Marathon::Error::ArgumentError) 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/marathon/util_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Marathon::Util do 4 | 5 | describe '.validate_choice' do 6 | subject { described_class } 7 | 8 | it 'passes with valid value' do 9 | described_class.validate_choice('foo', 'bar', %w[f00 bar], false) 10 | end 11 | 12 | it 'passes with nil value' do 13 | described_class.validate_choice('foo', nil, %w[f00], true) 14 | end 15 | 16 | it 'fails with nil value' do 17 | expect { 18 | described_class.validate_choice('foo', nil, %w[f00], false) 19 | }.to raise_error(Marathon::Error::ArgumentError) 20 | end 21 | 22 | it 'fails with invalid value' do 23 | expect { 24 | described_class.validate_choice('foo', 'bar', %w[f00], false) 25 | }.to raise_error(Marathon::Error::ArgumentError) 26 | end 27 | end 28 | 29 | describe '.add_choice' do 30 | subject { described_class } 31 | 32 | it 'validates choice first' do 33 | expect(described_class).to receive(:validate_choice).with('foo', 'bar', %w[f00 bar], false) 34 | described_class.add_choice({}, 'foo', 'bar', %w[f00 bar], false) 35 | end 36 | 37 | it 'adds choice' do 38 | opts = {} 39 | described_class.add_choice(opts, 'foo', 'bar', %w[f00 bar], false) 40 | expect(opts['foo']).to eq('bar') 41 | end 42 | end 43 | 44 | describe '.keywordize_hash!' do 45 | subject { described_class } 46 | 47 | it 'keywordizes the hash' do 48 | hash = { 49 | 'foo' => 'bar', 50 | 'f00' => {'w00h00' => 'yeah'}, 51 | 'bang' => [{'tricky' => 'one'}], 52 | 'env' => {'foo' => 'bar'}, 53 | 'null' => nil 54 | } 55 | 56 | expect(subject.keywordize_hash!(hash)).to eq({ 57 | :foo => 'bar', 58 | :f00 => {:w00h00 => 'yeah'}, 59 | :bang => [{:tricky => 'one'}], 60 | :env => {'foo' => 'bar'}, 61 | :null => nil 62 | }) 63 | # make sure, it changes the hash w/o creating a new one 64 | expect(hash[:foo]).to eq('bar') 65 | end 66 | end 67 | 68 | describe '.remove_keys' do 69 | subject { described_class } 70 | 71 | it 'removes keys from hash' do 72 | hash = { 73 | :foo => 'bar', 74 | :deleteme => {'w00h00' => 'yeah'}, 75 | :blah => [{:deleteme => :foo}, 1] 76 | } 77 | 78 | expect(subject.remove_keys(hash, [:deleteme])).to eq({ 79 | :foo => 'bar', 80 | :blah => [{}, 1] 81 | }) 82 | # make sure, it does not changes the original hash 83 | expect(hash.size).to eq(3) 84 | end 85 | end 86 | 87 | end 88 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Group/_list/lists_apps.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://localhost:8080/v2/groups 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"id":"/","apps":[{"id":"/ubuntu","cmd":"while sleep 10; do date -u 36 | +%T; done","args":null,"user":null,"env":{},"instances":1,"cpus":0.1,"mem":64.0,"disk":0.0,"executor":"","constraints":[],"uris":[],"storeUrls":[],"ports":[],"requirePorts":false,"backoffSeconds":1,"backoffFactor":1.15,"maxLaunchDelaySeconds":3600,"container":{"type":"DOCKER","volumes":[],"docker":{"image":"libmesos/ubuntu","network":null,"portMappings":null,"privileged":false,"parameters":[]}},"healthChecks":[],"dependencies":[],"upgradeStrategy":{"minimumHealthCapacity":1.0,"maximumOverCapacity":1.0},"labels":{},"version":"2015-03-17T11:56:26.532Z"},{"id":"/ubuntu2","cmd":"while 37 | sleep 10; do date -u +%T; done","args":null,"user":null,"env":{},"instances":2,"cpus":0.1,"mem":64.0,"disk":0.0,"executor":"","constraints":[["hostname","GROUP_BY"]],"uris":[],"storeUrls":[],"ports":[],"requirePorts":false,"backoffSeconds":1,"backoffFactor":1.15,"maxLaunchDelaySeconds":3600,"container":{"type":"DOCKER","volumes":[],"docker":{"image":"libmesos/ubuntu","network":null,"portMappings":null,"privileged":false,"parameters":[]}},"healthChecks":[],"dependencies":[],"upgradeStrategy":{"minimumHealthCapacity":1.0,"maximumOverCapacity":1.0},"labels":{},"version":"2015-03-17T13:05:44.561Z"}],"groups":[{"id":"/test","apps":[],"groups":[],"dependencies":[],"version":"2015-03-17T13:06:07.888Z"},{"id":"/develop-ci","apps":[],"groups":[{"id":"/develop-ci/ops","apps":[],"groups":[],"dependencies":[],"version":"2015-03-17T13:06:07.888Z"}],"dependencies":[],"version":"2015-03-17T13:06:07.888Z"},{"id":"/spark","apps":[],"groups":[],"dependencies":[],"version":"2015-03-17T13:06:07.888Z"},{"id":"/test-group","apps":[{"id":"/test-group/app","cmd":"sleep 38 | 30","args":null,"user":null,"env":{},"instances":1,"cpus":1.0,"mem":128.0,"disk":0.0,"executor":"","constraints":[],"uris":[],"storeUrls":[],"ports":[10000],"requirePorts":false,"backoffSeconds":1,"backoffFactor":1.15,"maxLaunchDelaySeconds":3600,"container":null,"healthChecks":[],"dependencies":[],"upgradeStrategy":{"minimumHealthCapacity":1.0,"maximumOverCapacity":0.0},"labels":{},"version":"2015-03-17T13:06:07.888Z"}],"groups":[],"dependencies":[],"version":"2015-03-17T13:06:07.888Z"}],"dependencies":[],"version":"2015-03-17T13:06:07.888Z"}' 39 | http_version: 40 | recorded_at: Tue, 17 Mar 2015 13:06:08 GMT 41 | recorded_with: VCR 2.9.3 42 | -------------------------------------------------------------------------------- /lib/marathon/deployment.rb: -------------------------------------------------------------------------------- 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 < Marathon::Base 4 | 5 | ACCESSORS = %w[ id affectedApps version currentStep totalSteps ] 6 | attr_reader :steps, :currentActions 7 | 8 | # Create a new deployment object. 9 | # ++hash++: Hash including all attributes. 10 | # See https://mesosphere.github.io/marathon/docs/rest-api.html#get-/v2/deployments for full details. 11 | def initialize(hash, marathon_instance) 12 | super(hash, ACCESSORS) 13 | @marathon_instance = marathon_instance 14 | @currentActions = (info[:currentActions] || []).map { |e| Marathon::DeploymentAction.new(e) } 15 | @steps = (info[:steps] || []).map { |e| Marathon::DeploymentStep.new(e) } 16 | end 17 | 18 | # Cancel the deployment. 19 | # ++force++: If set to false (the default) then the deployment is canceled and a new deployment 20 | # is created to restore the previous configuration. If set to true, then the deployment 21 | # is still canceled but no rollback deployment is created. 22 | def delete(force = false) 23 | @marathon_instance.deployments.delete(id, force) 24 | end 25 | 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 | class << self 34 | 35 | # List running deployments. 36 | def list 37 | Marathon.singleton.deployments.list 38 | end 39 | 40 | # Cancel the deployment with id. 41 | # ++id++: Deployment's id 42 | # ++force++: If set to false (the default) then the deployment is canceled and a new deployment 43 | # is created to restore the previous configuration. If set to true, then the deployment 44 | # is still canceled but no rollback deployment is created. 45 | def delete(id, force = false) 46 | Marathon.singleton.deployments.delete(id, force) 47 | end 48 | 49 | alias :cancel :delete 50 | alias :remove :delete 51 | end 52 | end 53 | 54 | # This class represents a set of Deployments 55 | class Marathon::Deployments 56 | def initialize(marathon_instance) 57 | @marathon_instance = marathon_instance 58 | @connection = @marathon_instance.connection 59 | end 60 | 61 | # List running deployments. 62 | def list 63 | json = @connection.get('/v2/deployments') 64 | json.map { |j| Marathon::Deployment.new(j, @marathon_instance) } 65 | end 66 | 67 | # Cancel the deployment with id. 68 | # ++id++: Deployment's id 69 | # ++force++: If set to false (the default) then the deployment is canceled and a new deployment 70 | # is created to restore the previous configuration. If set to true, then the deployment 71 | # is still canceled but no rollback deployment is created. 72 | def delete(id, force = false) 73 | query = {} 74 | query[:force] = true if force 75 | json = @connection.delete("/v2/deployments/#{id}") 76 | Marathon::DeploymentInfo.new(json, @marathon_instance) 77 | end 78 | 79 | end -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_Deployment/_list/lists_deployments.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: put 5 | uri: http://localhost:8080/v2/apps//test-app?force=true 6 | body: 7 | encoding: UTF-8 8 | string: '{"instances":0,"cmd":"sleep 60"}' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 201 19 | message: Created 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Location: 28 | - http://localhost:8080/v2/apps//test-app/test-app 29 | Content-Type: 30 | - application/json 31 | Transfer-Encoding: 32 | - chunked 33 | Server: 34 | - Jetty(8.y.z-SNAPSHOT) 35 | body: 36 | encoding: UTF-8 37 | string: '{"version":"2015-03-17T13:05:54.871Z","deploymentId":"ce0b301a-4149-47b3-8a5c-96e976ca58c5"}' 38 | http_version: 39 | recorded_at: Tue, 17 Mar 2015 13:05:57 GMT 40 | - request: 41 | method: put 42 | uri: http://localhost:8080/v2/apps//test-app?force=true 43 | body: 44 | encoding: UTF-8 45 | string: '{"instances":2}' 46 | headers: 47 | Content-Type: 48 | - application/json 49 | Accept: 50 | - application/json 51 | User-Agent: 52 | - ub0r/Marathon-API 1.1.0 53 | response: 54 | status: 55 | code: 200 56 | message: OK 57 | headers: 58 | Cache-Control: 59 | - no-cache, no-store, must-revalidate 60 | Pragma: 61 | - no-cache 62 | Expires: 63 | - '0' 64 | Content-Type: 65 | - application/json 66 | Transfer-Encoding: 67 | - chunked 68 | Server: 69 | - Jetty(8.y.z-SNAPSHOT) 70 | body: 71 | encoding: UTF-8 72 | string: '{"version":"2015-03-17T13:05:58.587Z","deploymentId":"0893efb1-8fd7-435b-b7f4-5ba195b635d9"}' 73 | http_version: 74 | recorded_at: Tue, 17 Mar 2015 13:05:59 GMT 75 | - request: 76 | method: get 77 | uri: http://localhost:8080/v2/deployments 78 | body: 79 | encoding: US-ASCII 80 | string: '' 81 | headers: 82 | Content-Type: 83 | - application/json 84 | Accept: 85 | - application/json 86 | User-Agent: 87 | - ub0r/Marathon-API 1.1.0 88 | response: 89 | status: 90 | code: 200 91 | message: OK 92 | headers: 93 | Cache-Control: 94 | - no-cache, no-store, must-revalidate 95 | Pragma: 96 | - no-cache 97 | Expires: 98 | - '0' 99 | Content-Type: 100 | - application/json 101 | Transfer-Encoding: 102 | - chunked 103 | Server: 104 | - Jetty(8.y.z-SNAPSHOT) 105 | body: 106 | encoding: UTF-8 107 | string: '[{"currentStep":1,"currentActions":[{"action":"ScaleApplication","app":"/test-app"}],"affectedApps":["/test-app"],"version":"2015-03-17T13:05:58.587Z","id":"0893efb1-8fd7-435b-b7f4-5ba195b635d9","totalSteps":1,"steps":[[{"action":"ScaleApplication","app":"/test-app"}]]}]' 108 | http_version: 109 | recorded_at: Tue, 17 Mar 2015 13:06:00 GMT 110 | recorded_with: VCR 2.9.3 111 | -------------------------------------------------------------------------------- /spec/marathon/deployment_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | DEPLOYMENT_EXAMPLE = { 4 | :affectedApps => ["/test"], 5 | :id => "867ed450-f6a8-4d33-9b0e-e11c5513990b", 6 | :steps => [ 7 | [ 8 | { 9 | :action => "ScaleApplication", 10 | :app => "/test" 11 | } 12 | ] 13 | ], 14 | :currentActions => [ 15 | { 16 | :action => "ScaleApplication", 17 | :app => "/test" 18 | } 19 | ], 20 | :version => "2014-08-26T08:18:03.595Z", 21 | :currentStep => 1, 22 | :totalSteps => 1 23 | } 24 | 25 | describe Marathon::Deployment do 26 | 27 | describe '#to_s' do 28 | subject { described_class.new(DEPLOYMENT_EXAMPLE, double(Marathon::MarathonInstance)) } 29 | 30 | let(:expected_string) do 31 | 'Marathon::Deployment { ' \ 32 | + ':id => 867ed450-f6a8-4d33-9b0e-e11c5513990b :affectedApps => ["/test"] :currentStep => 1 :totalSteps => 1 }' 33 | end 34 | 35 | its(:to_s) { should == expected_string } 36 | end 37 | 38 | describe '#to_json' do 39 | subject { described_class.new(DEPLOYMENT_EXAMPLE, double(Marathon::MarathonInstance)) } 40 | 41 | its(:to_json) { should == DEPLOYMENT_EXAMPLE.to_json } 42 | end 43 | 44 | describe 'attributes' do 45 | subject { described_class.new(DEPLOYMENT_EXAMPLE, double(Marathon::MarathonInstance)) } 46 | 47 | its(:id) { should == DEPLOYMENT_EXAMPLE[:id] } 48 | its(:affectedApps) { should == DEPLOYMENT_EXAMPLE[:affectedApps] } 49 | its(:version) { should == DEPLOYMENT_EXAMPLE[:version] } 50 | its(:currentStep) { should == DEPLOYMENT_EXAMPLE[:currentStep] } 51 | its(:totalSteps) { should == DEPLOYMENT_EXAMPLE[:totalSteps] } 52 | end 53 | 54 | describe '#delete' do 55 | before(:each) do 56 | @deployments = double(Marathon::Deployments) 57 | @subject = described_class.new(DEPLOYMENT_EXAMPLE, 58 | double(Marathon::MarathonInstance, :deployments => @deployments)) 59 | end 60 | 61 | it 'deletes the deployment' do 62 | expect(@deployments).to receive(:delete).with(DEPLOYMENT_EXAMPLE[:id], false) 63 | @subject.delete 64 | end 65 | 66 | it 'force deletes the deployment' do 67 | expect(@deployments).to receive(:delete).with(DEPLOYMENT_EXAMPLE[:id], true) 68 | @subject.delete(true) 69 | end 70 | end 71 | 72 | describe '.list' do 73 | subject { described_class } 74 | 75 | it 'lists deployments', :vcr do 76 | # start a deployment 77 | Marathon::App.change('/test-app', {:instances => 0, :cmd => 'sleep 60'}, true) 78 | sleep 1 79 | Marathon::App.change('/test-app', {:instances => 2}, true) 80 | sleep 1 81 | 82 | deployments = subject.list 83 | expect(deployments).to be_instance_of(Array) 84 | expect(deployments.first).to be_instance_of(Marathon::Deployment) 85 | end 86 | end 87 | 88 | describe '.delete' do 89 | subject { described_class } 90 | 91 | it 'deletes deployments', :vcr do 92 | # start a deployment 93 | info = Marathon::App.change('/test-app', {:instances => 1}, true) 94 | expect(subject.delete(info.deploymentId)).to be_instance_of(Marathon::DeploymentInfo) 95 | end 96 | 97 | it 'cleans app from marathon', :vcr do 98 | Marathon::App.delete('/test-app') 99 | end 100 | end 101 | 102 | end 103 | -------------------------------------------------------------------------------- /lib/marathon/util.rb: -------------------------------------------------------------------------------- 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 | value = value[name] if value.is_a?(Hash) 12 | if value.nil? 13 | unless nil_allowed 14 | raise Marathon::Error::ArgumentError, "#{name} must not be nil" 15 | end 16 | else 17 | # value is not nil 18 | unless allowed.include?(value) 19 | if nil_allowed 20 | raise Marathon::Error::ArgumentError, 21 | "#{name} must be one of #{allowed.join(', ')} or nil, but is '#{value}'" 22 | else 23 | raise Marathon::Error::ArgumentError, 24 | "#{name} must be one of #{allowed.join(', ')} or nil, but is '#{value}'" 25 | end 26 | end 27 | end 28 | end 29 | 30 | # Check parameter and add it to hash if not nil. 31 | # ++opts++: hash of parameters 32 | # ++name++: parameter's name 33 | # ++value++: parameter's value 34 | # ++allowed++: array of allowd values 35 | # ++nil_allowed++: allow nil values 36 | def add_choice(opts, name, value, allowed, nil_allowed = true) 37 | validate_choice(name, value, allowed, nil_allowed) 38 | opts[name] = value if value 39 | end 40 | 41 | # Swap keys of the hash against their symbols. 42 | # ++hash++: the hash 43 | # ++ignore_keys++: don't keywordize hashes under theses keys 44 | def keywordize_hash!(hash, ignore_keys = [:env]) 45 | if hash.is_a?(Hash) 46 | hmap!(hash) do |k, v| 47 | key = k.to_sym 48 | if ignore_keys.include?(key) and v.is_a?(Hash) 49 | {key => v} 50 | else 51 | {key => keywordize_hash!(v)} 52 | end 53 | end 54 | elsif hash.is_a?(Array) 55 | hash.map! { |e| keywordize_hash!(e) } 56 | end 57 | hash 58 | end 59 | 60 | # Remove keys from hash and all it's sub hashes. 61 | # ++hash++: the hash 62 | # ++keys++: list of keys to remove 63 | def remove_keys(hash, keys) 64 | if hash.is_a?(Hash) 65 | new_hash = {} 66 | hash.each { |k, v| new_hash[k] = remove_keys(v, keys) unless keys.include?(k) } 67 | new_hash 68 | elsif hash.is_a?(Array) 69 | hash.map { |e| remove_keys(e, keys) } 70 | else 71 | hash 72 | end 73 | end 74 | 75 | # Merge two hashes but keywordize both. 76 | def merge_keywordized_hash(h1, h2) 77 | keywordize_hash!(h1).merge(keywordize_hash!(h2)) 78 | end 79 | 80 | # Stringify an item or an array of items. 81 | def items_to_pretty_s(item) 82 | if item.nil? 83 | nil 84 | elsif item.is_a?(Array) 85 | item.map { |e| e.to_pretty_s }.join(',') 86 | else 87 | item.to_pretty_s 88 | end 89 | end 90 | 91 | # Implement map! on a hash 92 | def hmap!(hash, &block) 93 | hash.keys.each do |key| 94 | new_hash = block.call(key, hash[key]) 95 | new_key = new_hash.keys.first 96 | hash[new_key] = new_hash[new_key] 97 | hash.delete(key) unless key == new_key 98 | end 99 | hash 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /spec/marathon/task_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Marathon::Task do 4 | 5 | describe '#to_s' do 6 | subject { described_class.new({ 7 | 'id' => 'task-id-foo', 8 | 'appId' => '/app/foo', 9 | 'host' => 'foo-host', 10 | 'ports' => [31000, 31001], 11 | 'version' => 'foo-version' 12 | }, double(Marathon::MarathonInstance)) } 13 | 14 | let(:expected_string) do 15 | "Marathon::Task { :id => task-id-foo :appId => /app/foo :host => foo-host }" 16 | end 17 | 18 | let(:expected_pretty_string) do 19 | "Task ID: task-id-foo\n" + \ 20 | "App ID: /app/foo\n" + \ 21 | "Host: foo-host\n" + \ 22 | "Ports: 31000,31001\n" + \ 23 | "Staged at: \n" + \ 24 | "Started at: \n" + \ 25 | "Version: foo-version" 26 | end 27 | 28 | its(:to_s) { should == expected_string } 29 | its(:to_pretty_s) { should == expected_pretty_string } 30 | end 31 | 32 | describe '#to_json' do 33 | subject { described_class.new({ 34 | 'id' => 'task-id-foo', 35 | 'appId' => '/app/foo', 36 | 'host' => 'foo-host', 37 | }, double(Marathon::MarathonInstance)) } 38 | 39 | let(:expected_string) do 40 | '{"id":"task-id-foo","appId":"/app/foo","host":"foo-host"}' 41 | end 42 | 43 | its(:to_json) { should == expected_string } 44 | end 45 | 46 | describe '#delete!' do 47 | let(:task) { described_class.new({ 48 | 'id' => 'task_123', 'appId' => '/app/foo' 49 | }, double(Marathon::MarathonInstance)) } 50 | 51 | it 'deletes the task' do 52 | expect(described_class).to receive(:delete).with('task_123', false) 53 | task.delete! 54 | end 55 | end 56 | 57 | describe '.list' do 58 | subject { described_class } 59 | 60 | it 'raises error when run with strange status' do 61 | expect { 62 | subject.list('foo') 63 | }.to raise_error(Marathon::Error::ArgumentError) 64 | end 65 | 66 | it 'lists tasks', :vcr do 67 | tasks = subject.list 68 | expect(tasks.size).to be_within(1).of(2) 69 | expect(tasks.first).to be_instance_of(described_class) 70 | end 71 | 72 | it 'lists running tasks', :vcr do 73 | tasks = subject.list('running') 74 | expect(tasks.size).to be_within(1).of(2) 75 | expect(tasks.first).to be_instance_of(described_class) 76 | end 77 | end 78 | 79 | describe '.get' do 80 | subject { described_class } 81 | 82 | it 'gets tasks of an app', :vcr do 83 | tasks = subject.get('/ubuntu2') 84 | expect(tasks.size).not_to eq(0) 85 | expect(tasks.first).to be_instance_of(described_class) 86 | expect(tasks.first.appId).to eq('/ubuntu2') 87 | end 88 | end 89 | 90 | describe '.delete' do 91 | subject { described_class } 92 | 93 | it 'kills a tasks of an app', :vcr do 94 | tasks = subject.get('/ubuntu2') 95 | subject.delete(tasks.first.id) 96 | end 97 | end 98 | 99 | describe '.delete_all' do 100 | subject { described_class } 101 | 102 | it 'kills all tasks of an app', :vcr do 103 | subject.delete_all('/ubuntu2') 104 | end 105 | end 106 | 107 | end 108 | -------------------------------------------------------------------------------- /fixtures/vcr/Marathon_App/_version/gets_a_version.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://localhost:8080/v2/apps//ubuntu2/versions 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Content-Type: 11 | - application/json 12 | Accept: 13 | - application/json 14 | User-Agent: 15 | - ub0r/Marathon-API 1.1.0 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Cache-Control: 22 | - no-cache, no-store, must-revalidate 23 | Pragma: 24 | - no-cache 25 | Expires: 26 | - '0' 27 | Content-Type: 28 | - application/json 29 | Transfer-Encoding: 30 | - chunked 31 | Server: 32 | - Jetty(8.y.z-SNAPSHOT) 33 | body: 34 | encoding: UTF-8 35 | string: '{"versions":["2015-03-17T13:05:39.559Z","2015-03-17T13:05:39.490Z","2015-03-17T13:04:37.200Z","2015-03-17T13:04:37.060Z","2015-03-17T13:04:36.997Z","2015-03-17T12:06:04.971Z","2015-03-17T12:06:00.843Z","2015-03-17T12:06:00.788Z","2015-03-17T12:04:49.309Z","2015-03-17T12:04:47.711Z","2015-03-17T12:04:47.484Z","2015-03-17T12:04:33.718Z","2015-03-17T12:04:32.686Z","2015-03-17T12:04:32.630Z","2015-03-17T12:04:30.244Z","2015-03-17T12:03:31.426Z","2015-03-17T12:03:26.347Z","2015-03-17T12:03:26.281Z","2015-03-17T12:02:23.385Z","2015-03-17T12:02:15.239Z","2015-03-17T12:02:15.195Z","2015-03-17T12:00:37.036Z","2015-03-17T12:00:28.288Z","2015-03-17T12:00:27.750Z","2015-03-17T12:00:09.006Z","2015-03-17T12:00:08.822Z","2015-03-17T12:00:08.757Z","2015-03-17T11:59:06.274Z","2015-03-17T11:59:06.059Z","2015-03-17T11:59:05.997Z","2015-03-17T11:57:04.811Z","2015-03-17T11:57:02.136Z","2015-03-17T11:57:01.341Z","2015-03-17T11:56:31.250Z","2015-03-17T11:56:28.537Z","2015-03-17T11:56:28.397Z","2015-03-17T11:52:04.570Z","2015-03-17T11:52:04.421Z","2015-03-17T11:52:04.355Z","2015-03-17T11:50:01.156Z","2015-03-17T11:49:57.243Z","2015-03-17T11:49:57.183Z","2015-03-17T11:42:41.281Z","2015-03-17T10:34:05.973Z","2015-03-17T10:33:19.189Z","2015-03-17T10:32:51.153Z","2015-03-17T10:32:34.010Z","2015-03-17T10:31:44.527Z","2015-03-17T10:19:02.815Z","2015-03-17T10:12:14.793Z"]}' 36 | http_version: 37 | recorded_at: Tue, 17 Mar 2015 13:05:49 GMT 38 | - request: 39 | method: get 40 | uri: http://localhost:8080/v2/apps//ubuntu2/versions/2015-03-17T13:05:39.559Z 41 | body: 42 | encoding: US-ASCII 43 | string: '' 44 | headers: 45 | Content-Type: 46 | - application/json 47 | Accept: 48 | - application/json 49 | User-Agent: 50 | - ub0r/Marathon-API 1.1.0 51 | response: 52 | status: 53 | code: 200 54 | message: OK 55 | headers: 56 | Cache-Control: 57 | - no-cache, no-store, must-revalidate 58 | Pragma: 59 | - no-cache 60 | Expires: 61 | - '0' 62 | Content-Type: 63 | - application/json 64 | Transfer-Encoding: 65 | - chunked 66 | Server: 67 | - Jetty(8.y.z-SNAPSHOT) 68 | body: 69 | encoding: UTF-8 70 | string: '{"id":"/ubuntu2","cmd":"while sleep 10; do date -u +%T; done","args":null,"user":null,"env":{},"instances":2,"cpus":0.1,"mem":64.0,"disk":0.0,"executor":"","constraints":[["hostname","GROUP_BY"]],"uris":[],"storeUrls":[],"ports":[],"requirePorts":false,"backoffSeconds":1,"backoffFactor":1.15,"maxLaunchDelaySeconds":3600,"container":{"type":"DOCKER","volumes":[],"docker":{"image":"libmesos/ubuntu","network":null,"portMappings":null,"privileged":false,"parameters":[]}},"healthChecks":[],"dependencies":[],"upgradeStrategy":{"minimumHealthCapacity":1.0,"maximumOverCapacity":1.0},"labels":{},"version":"2015-03-17T13:05:39.559Z"}' 71 | http_version: 72 | recorded_at: Tue, 17 Mar 2015 13:05:49 GMT 73 | recorded_with: VCR 2.9.3 74 | -------------------------------------------------------------------------------- /lib/marathon/connection.rb: -------------------------------------------------------------------------------- 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, :options 17 | 18 | # Create a new API connection. 19 | # ++url++: URL of the marathon API. 20 | # ++options++: Hash with options for marathon API. 21 | def initialize(url, options = {}) 22 | @url = url 23 | @options = options 24 | if @options[:username] and @options[:password] 25 | @options[:basic_auth] = { 26 | :username => @options[:username], 27 | :password => @options[:password] 28 | } 29 | @options.delete(:username) 30 | @options.delete(:password) 31 | end 32 | 33 | # The insecure option allows ignoring bad (or self-signed) SSL 34 | # certificates. 35 | if @options[:insecure] 36 | @options[:verify] = false 37 | @options.delete(:insecure) 38 | end 39 | end 40 | 41 | # Delegate all HTTP methods to the #request. 42 | [:get, :put, :post, :delete].each do |method| 43 | define_method(method) { |*args, &block| request(method, *args) } 44 | end 45 | 46 | def to_s 47 | "Marathon::Connection { :url => #{url} :options => #{options} }" 48 | end 49 | 50 | private 51 | 52 | # Create URL suffix for a hash of query parameters. 53 | # URL escaping is done internally. 54 | # ++query++: Hash of query parameters. 55 | def query_params(query) 56 | query = query.select { |k, v| !v.nil? } 57 | URI.escape(query.map { |k, v| "#{k}=#{v}" }.join('&')) 58 | end 59 | 60 | # Create request object. 61 | # ++http_method++: GET/POST/PUT/DELETE. 62 | # ++path++: Relative path to connection's URL. 63 | # ++query++: Optional query parameters. 64 | # ++opts++: Optional options. Ex. opts[:body] is used for PUT/POST request. 65 | def compile_request_params(http_method, path, query = nil, opts = nil) 66 | query ||= {} 67 | opts ||= {} 68 | headers = opts.delete(:headers) || {} 69 | opts[:body] = opts[:body].to_json unless opts[:body].nil? 70 | { 71 | :method => http_method, 72 | :url => "#{@url}#{path}", 73 | :query => query 74 | }.merge(@options).merge(opts).reject { |_, v| v.nil? } 75 | end 76 | 77 | # Create full URL with query parameters. 78 | # ++request++: hash containing :url and optional :query 79 | def build_url(request) 80 | url = URI.escape(request[:url]) 81 | if request[:query].size > 0 82 | url += '?' + query_params(request[:query]) 83 | end 84 | url 85 | end 86 | 87 | # Parse response or raise error. 88 | # ++response++: response from HTTParty call. 89 | def parse_response(response) 90 | if response.success? 91 | begin 92 | response.parsed_response 93 | rescue => err 94 | raise Marathon::Error.from_response(response) 95 | end 96 | else 97 | raise Marathon::Error.from_response(response) 98 | end 99 | end 100 | 101 | # Send a request to the server and parse response. 102 | # ++http_method++: GET/POST/PUT/DELETE. 103 | # ++path++: Relative path to connection's URL. 104 | # ++query++: Optional query parameters. 105 | # ++opts++: Optional options. Ex. opts[:body] is used for PUT/POST request. 106 | def request(*args) 107 | request = compile_request_params(*args) 108 | url = build_url(request) 109 | parse_response(self.class.send(request[:method], url, request)) 110 | rescue => e 111 | if e.class == SocketError or e.class.name.start_with?('Errno::') 112 | raise IOError, "HTTP call failed: #{e.message}" 113 | else 114 | raise e 115 | end 116 | end 117 | 118 | end 119 | -------------------------------------------------------------------------------- /lib/marathon.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems/package' 2 | require 'httparty' 3 | require 'json' 4 | require 'uri' 5 | require 'timeout' 6 | 7 | # The top-level module for this gem. It's purpose is to hold global 8 | # configuration variables that are used as defaults in other classes. 9 | module Marathon 10 | 11 | attr_accessor :logger 12 | 13 | require 'marathon/version' 14 | require 'marathon/util' 15 | require 'marathon/error' 16 | require 'marathon/connection' 17 | require 'marathon/base' 18 | require 'marathon/constraint' 19 | require 'marathon/container_docker_port_mapping' 20 | require 'marathon/container_docker' 21 | require 'marathon/container_volume' 22 | require 'marathon/container' 23 | require 'marathon/health_check' 24 | require 'marathon/deployment_info' 25 | require 'marathon/deployment_action' 26 | require 'marathon/deployment_step' 27 | require 'marathon/group' 28 | require 'marathon/app' 29 | require 'marathon/deployment' 30 | require 'marathon/event_subscriptions' 31 | require 'marathon/leader' 32 | require 'marathon/queue' 33 | require 'marathon/task' 34 | 35 | # Represents an instance of Marathon 36 | class MarathonInstance 37 | attr_reader :connection 38 | 39 | def initialize(url, options) 40 | @connection = Connection.new(url, options) 41 | end 42 | 43 | def ping 44 | begin 45 | connection.get('/ping') 46 | rescue Marathon::Error::UnexpectedResponseError => err 47 | return err.response.body if err.response.code == 200 48 | raise err 49 | end 50 | end 51 | 52 | # Get information about the marathon server 53 | def info 54 | connection.get('/v2/info') 55 | end 56 | 57 | def metrics 58 | connection.get('/metrics') 59 | end 60 | 61 | def apps 62 | Marathon::Apps.new(self) 63 | end 64 | 65 | def groups 66 | Marathon::Groups.new(self) 67 | end 68 | 69 | def deployments 70 | Marathon::Deployments.new(self) 71 | end 72 | 73 | def tasks 74 | Marathon::Tasks.new(self) 75 | end 76 | 77 | def queues 78 | Marathon::Queues.new(self) 79 | end 80 | 81 | def leaders 82 | Marathon::Leader.new(self) 83 | end 84 | 85 | def event_subscriptions 86 | Marathon::EventSubscriptions.new(self) 87 | end 88 | 89 | end 90 | 91 | 92 | DEFAULT_URL = 'http://localhost:8080' 93 | 94 | attr_reader :singleton 95 | 96 | @singleton = MarathonInstance::new(DEFAULT_URL, {}) 97 | 98 | # Get the marathon url from environment 99 | def env_url 100 | ENV['MARATHON_URL'] 101 | end 102 | 103 | # Get marathon options from environment 104 | def env_options 105 | opts = {} 106 | opts[:username] = ENV['MARATHON_USER'] if ENV['MARATHON_USER'] 107 | opts[:password] = ENV['MARATHON_PASSWORD'] if ENV['MARATHON_PASSWORD'] 108 | opts[:insecure] = ENV['MARATHON_INSECURE'] == 'true' if ENV['MARATHON_INSECURE'] 109 | opts 110 | end 111 | 112 | # Get the marathon API URL 113 | def url 114 | @url ||= env_url || DEFAULT_URL 115 | @url 116 | end 117 | 118 | # Get options for connecting to marathon API 119 | def options 120 | @options ||= env_options 121 | end 122 | 123 | # Set a new url 124 | def url=(new_url) 125 | @url = new_url 126 | reset_singleton! 127 | end 128 | 129 | # Set new options 130 | def options=(new_options) 131 | @options = env_options.merge(new_options || {}) 132 | reset_singleton! 133 | end 134 | 135 | # Set a new connection 136 | def connection 137 | singleton.connection 138 | end 139 | 140 | 141 | def reset_singleton! 142 | @singleton = MarathonInstance.new(url, options) 143 | end 144 | 145 | def reset_connection! 146 | reset_singleton! 147 | end 148 | 149 | # Get information about the marathon server 150 | def info 151 | singleton.info 152 | end 153 | 154 | # Ping marathon 155 | def ping 156 | singleton.ping 157 | end 158 | 159 | def metrics 160 | singleton.metrics 161 | end 162 | 163 | module_function :connection, :env_options, :env_url, :info, :logger, :logger=, :ping, :metrics, 164 | :options, :options=, :url, :url=, :reset_connection!, :reset_singleton!, :singleton 165 | end 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | marathon-api 2 | ============ 3 | 4 | [![Gem Version](https://badge.fury.io/rb/marathon-api.svg)](http://badge.fury.io/rb/marathon-api) [![travis-ci](https://travis-ci.org/otto-de/marathon-api.png?branch=master)](https://travis-ci.org/otto-de/marathon-api) [![Code Climate](https://codeclimate.com/github/otto-de/marathon-api/badges/gpa.svg)](https://codeclimate.com/github/otto-de/marathon-api) 5 | 6 | This gem provides an object oriented interface to the [Marathon Remote API][1]. At the time if this writing, marathon-api is meant to interface with Marathon version 0.10.1. 7 | 8 | Installation 9 | ------------ 10 | 11 | Add this line to your application's Gemfile: 12 | 13 | ```ruby 14 | gem 'marathon-api', :require => 'marathon' 15 | ``` 16 | 17 | And then run: 18 | 19 | ```shell 20 | $ bundle install 21 | ``` 22 | 23 | Alternatively, if you wish to just use the gem in a script, you can run: 24 | 25 | ```shell 26 | $ gem install marathon-api 27 | ``` 28 | 29 | Finally, just add `require 'marathon'` to the top of the file using this gem. 30 | 31 | Usage 32 | ----- 33 | 34 | marathon-api is designed to be very lightweight. Only little state is cached to ensure that each method call's information is up to date. As such, just about every external method represents an API call. 35 | 36 | If you're running Marathon locally on port 8080, there is no setup to do in Ruby. If you're not or change the path or port, you'll have to point the gem to your socket or local/remote port. For example: 37 | 38 | ```ruby 39 | Marathon.url = 'http://example.com:8080' 40 | ``` 41 | 42 | It's possible to use `ENV` variables to configure the endpoint as well: 43 | 44 | ```shell 45 | $ MARATHON_URL=http://remote.marathon.example.com:8080 irb 46 | irb(main):001:0> require 'marathon' 47 | => true 48 | irb(main):002:0> Marathon.url 49 | => "http://remote.marathon.example.com:8080" 50 | ``` 51 | 52 | ## Authentification 53 | 54 | You have two options to set authentification if your Marathon API requires it: 55 | 56 | ```ruby 57 | Marathon.options = {:username => 'your-user-name', :password => 'your-secret-password'} 58 | ``` 59 | 60 | or 61 | 62 | ```shell 63 | $ export MARATHON_USER=your-user-name 64 | $ export MARATHON_PASSWORD=your-secret-password 65 | $ irb 66 | irb(main):001:0> require 'marathon' 67 | => true 68 | irb(main):002:0> Marathon.options 69 | => {:username => "your-user-name", :password => "your-secret-password"} 70 | ``` 71 | 72 | ## Global calls 73 | 74 | ```ruby 75 | require 'marathon' 76 | # => true 77 | 78 | Marathon.info 79 | # => {"name"=>"marathon", "http_config"=>{"assets_path"=>null, "http_port"=>8080, "https_port"=>8443}, "frameworkId"=>"20150228-110436-16842879-5050-2169-0001", "leader"=>null, "event_subscriber"=>null, "marathon_config"=>{"local_port_max"=>20000, "local_port_min"=>10000, "hostname"=>"mesos", "master"=>"zk://localhost:2181/mesos", "reconciliation_interval"=>300000, "mesos_role"=>null, "task_launch_timeout"=>300000, "reconciliation_initial_delay"=>15000, "ha"=>true, "failover_timeout"=>604800, "checkpoint"=>true, "executor"=>"//cmd", "marathon_store_timeout"=>2000, "mesos_user"=>"root"}, "version"=>"0.8.0", "zookeeper_config"=>{"zk_path"=>"/marathon", "zk"=>null, "zk_timeout"=>10, "zk_hosts"=>"localhost:2181", "zk_future_timeout"=>{"duration"=>10}}, "elected"=>false} 80 | 81 | Marathon.ping 82 | # => 'pong' 83 | 84 | ``` 85 | 86 | ## Applications 87 | 88 | You can list, change, delete apps like this: 89 | 90 | ```ruby 91 | require 'marathon' 92 | 93 | # fetch a list of applications 94 | apps = Marathon::App.list 95 | 96 | # scale the first app to 2 instances 97 | apps.first.scale!(2) 98 | 99 | # delete the last app 100 | apps.last.delete! 101 | ``` 102 | 103 | The other Marathon endpoints are available in the same way. 104 | 105 | Contributing 106 | ------------ 107 | 108 | Please fork and send pull request. 109 | Make sure to have test cases for your changes. 110 | 111 | Credits 112 | ------- 113 | 114 | This gem is inspired by mesosphere's abondend [marathon_client][2] and swipelies [docker-api][3]. 115 | 116 | License 117 | ------- 118 | 119 | This program is licensed under the MIT license. See LICENSE for details. 120 | 121 | [1]: https://mesosphere.github.io/marathon/docs/rest-api.html 122 | [2]: https://github.com/mesosphere/marathon_client 123 | [3]: https://github.com/swipely/docker-api 124 | -------------------------------------------------------------------------------- /lib/marathon/task.rb: -------------------------------------------------------------------------------- 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 < Marathon::Base 4 | 5 | ACCESSORS = %w[ id appId host ports servicePorts version stagedAt startedAt ] 6 | 7 | # Create a new task object. 8 | # ++hash++: Hash including all attributes 9 | # ++marathon_instance++: MarathonInstance holding a connection to marathon 10 | def initialize(hash, marathon_instance = Marathon.singleton) 11 | super(hash, ACCESSORS) 12 | @marathon_instance = marathon_instance 13 | end 14 | 15 | # Kill the task that belongs to an application. 16 | # ++scale++: Scale the app down (i.e. decrement its instances setting by the number of tasks killed) 17 | # after killing the specified tasks. 18 | def delete!(scale = false) 19 | new_task = self.class.delete(id, scale) 20 | end 21 | 22 | alias :kill! :delete! 23 | 24 | def to_s 25 | "Marathon::Task { :id => #{self.id} :appId => #{appId} :host => #{host} }" 26 | end 27 | 28 | # Returns a string for listing the task. 29 | def to_pretty_s 30 | %Q[ 31 | Task ID: #{id} 32 | App ID: #{appId} 33 | Host: #{host} 34 | Ports: #{(ports || []).join(',')} 35 | Staged at: #{stagedAt} 36 | Started at: #{startedAt} 37 | Version: #{version} 38 | ].strip 39 | end 40 | 41 | class << self 42 | 43 | # List tasks of all applications. 44 | # ++status++: Return only those tasks whose status matches this parameter. 45 | # If not specified, all tasks are returned. Possible values: running, staging. 46 | def list(status = nil) 47 | Marathon.singleton.tasks.list(status) 48 | end 49 | 50 | # List all running tasks for application appId. 51 | # ++appId++: Application's id 52 | def get(appId) 53 | Marathon.singleton.tasks.get(appId) 54 | end 55 | 56 | # Kill the given list of tasks and scale apps if requested. 57 | # ++ids++: Id or list of ids with target tasks. 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(ids, scale = false) 61 | Marathon.singleton.tasks.delete(ids, scale) 62 | end 63 | 64 | alias :remove :delete 65 | alias :kill :delete 66 | 67 | # Kill tasks that belong to the application appId. 68 | # ++appId++: Application's id 69 | # ++host++: Kill only those tasks running on host host. 70 | # ++scale++: Scale the app down (i.e. decrement its instances setting by the number of tasks killed) 71 | # after killing the specified tasks. 72 | def delete_all(appId, host = nil, scale = false) 73 | Marathon.singleton.tasks.delete_all(appId, host, scale) 74 | end 75 | 76 | alias :remove_all :delete_all 77 | alias :kill_all :delete_all 78 | end 79 | end 80 | 81 | # This class represents a set of Tasks 82 | class Marathon::Tasks 83 | def initialize(marathon_instance) 84 | @marathon_instance = marathon_instance 85 | @connection = marathon_instance.connection 86 | end 87 | 88 | # List tasks of all applications. 89 | # ++status++: Return only those tasks whose status matches this parameter. 90 | # If not specified, all tasks are returned. Possible values: running, staging. 91 | def list(status = nil) 92 | query = {} 93 | Marathon::Util.add_choice(query, :status, status, %w[running staging]) 94 | json = @connection.get('/v2/tasks', query)['tasks'] 95 | json.map { |j| Marathon::Task.new(j, @marathon_instance) } 96 | end 97 | 98 | # List all running tasks for application appId. 99 | # ++appId++: Application's id 100 | def get(appId) 101 | json = @connection.get("/v2/apps/#{appId}/tasks")['tasks'] 102 | json.map { |j| Marathon::Task.new(j, @marathon_instance) } 103 | end 104 | 105 | # Kill the given list of tasks and scale apps if requested. 106 | # ++ids++: Id or list of ids with target tasks. 107 | # ++scale++: Scale the app down (i.e. decrement its instances setting by the number of tasks killed) 108 | # after killing the specified tasks. 109 | def delete(ids, scale = false) 110 | query = {} 111 | query[:scale] = true if scale 112 | ids = [ids] if ids.is_a?(String) 113 | @connection.post("/v2/tasks/delete", query, :body => {:ids => ids}) 114 | end 115 | 116 | # Kill tasks that belong to the application appId. 117 | # ++appId++: Application's id 118 | # ++host++: Kill only those tasks running on host host. 119 | # ++scale++: Scale the app down (i.e. decrement its instances setting by the number of tasks killed) 120 | # after killing the specified tasks. 121 | def delete_all(appId, host = nil, scale = false) 122 | query = {} 123 | query[:host] = host if host 124 | query[:scale] = true if scale 125 | json = @connection.delete("/v2/apps/#{appId}/tasks", query)['tasks'] 126 | json.map { |j| Marathon::Task.new(j, @marathon_instance) } 127 | end 128 | 129 | end 130 | -------------------------------------------------------------------------------- /spec/marathon/group_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | EXAMPLE_GROUP = { 4 | "id" => "/test-group", 5 | "apps" => [ 6 | { 7 | "backoffFactor" => 1.15, 8 | "backoffSeconds" => 1, 9 | "maxLaunchDelaySeconds" => 3600, 10 | "cmd" => "sleep 30", 11 | "constraints" => [], 12 | "cpus" => 1.0, 13 | "dependencies" => [], 14 | "disk" => 0.0, 15 | "env" => {}, 16 | "executor" => "", 17 | "id" => "app", 18 | "instances" => 1, 19 | "mem" => 128.0, 20 | "ports" => [10000], 21 | "requirePorts" => false, 22 | "storeUrls" => [], 23 | "upgradeStrategy" => { 24 | "minimumHealthCapacity" => 1.0 25 | }, 26 | "tasks" => [] 27 | } 28 | ], 29 | "dependencies" => [], 30 | "groups" => [] 31 | } 32 | 33 | describe Marathon::Group do 34 | 35 | describe '#to_s' do 36 | subject { described_class.new(EXAMPLE_GROUP) } 37 | 38 | let(:expected_string) do 39 | "Marathon::Group { :id => /test-group }" 40 | end 41 | 42 | let(:expected_pretty_string) do 43 | "Group ID: /test-group\n" + \ 44 | " App ID: app\n" + \ 45 | " Instances: 0/1\n" + \ 46 | " Command: sleep 30\n" + \ 47 | " CPUs: 1.0\n" + \ 48 | " Memory: 128.0 MB\n" + \ 49 | " Version:\n" + \ 50 | "Version:" 51 | 52 | end 53 | 54 | # its(:to_s) { should == expected_string } 55 | # its(:to_pretty_s) { should == expected_pretty_string } 56 | end 57 | 58 | describe '#start!' do 59 | before(:each) do 60 | @groups = double(Marathon::Groups) 61 | @marathon_instance = double(Marathon::MarathonInstance, :groups => @groups) 62 | @subject = described_class.new({'id' => '/group/foo'}, @marathon_instance) 63 | end 64 | 65 | it 'starts the group' do 66 | expect(@groups).to receive(:start) 67 | .with({:dependencies => [], :id => '/group/foo'}) do 68 | Marathon::DeploymentInfo.new({'version' => 'new-version'}, @marathon_instance) 69 | end 70 | expect(@subject.start!.version).to eq('new-version') 71 | end 72 | end 73 | 74 | describe '#refresh' do 75 | before(:each) do 76 | 77 | @groups = double(Marathon::Groups) 78 | @marathon_instance = double(Marathon::MarathonInstance, :groups => @groups) 79 | @subject = described_class.new({'id' => '/app/foo'}, @marathon_instance) 80 | end 81 | 82 | it 'refreshes the group' do 83 | expect(@groups).to receive(:get).with('/app/foo') do 84 | described_class.new({'id' => '/app/foo', 'refreshed' => true}, @marathon_instance) 85 | end 86 | @subject.refresh 87 | expect(@subject.info[:refreshed]).to be(true) 88 | end 89 | end 90 | 91 | describe '#change!' do 92 | before(:each) do 93 | @groups = double(Marathon::Groups) 94 | @marathon_instance = double(Marathon::MarathonInstance, :groups => @groups) 95 | @subject = described_class.new({'id' => '/app/foo'}, @marathon_instance) 96 | end 97 | 98 | it 'changes the group' do 99 | expect(@groups).to receive(:change).with('/app/foo', {:instances => 9000}, false, false) 100 | @subject.change!('instances' => 9000) 101 | end 102 | 103 | it 'changes the group and strips :version' do 104 | expect(@groups).to receive(:change).with('/app/foo', {:instances => 9000}, false, false) 105 | @subject.change!('instances' => 9000, :version => 'old-version') 106 | end 107 | end 108 | 109 | describe '#roll_back!' do 110 | subject { described_class.new({'id' => '/app/foo', 'instances' => 10}, double(Marathon::MarathonInstance)) } 111 | 112 | it 'changes the group' do 113 | expect(subject).to receive(:change!).with({'version' => 'old_version'}, false) 114 | subject.roll_back!('old_version') 115 | end 116 | 117 | it 'changes the group with force' do 118 | expect(subject).to receive(:change!).with({'version' => 'old_version'}, true) 119 | subject.roll_back!('old_version', true) 120 | end 121 | end 122 | 123 | describe '.start' do 124 | subject { described_class } 125 | 126 | it 'starts the group', :vcr do 127 | expect(subject.start(EXAMPLE_GROUP)).to be_instance_of(Marathon::DeploymentInfo) 128 | end 129 | 130 | it 'fails getting not existing group', :vcr do 131 | expect { 132 | subject.get('fooo group') 133 | }.to raise_error(Marathon::Error::NotFoundError) 134 | end 135 | end 136 | 137 | describe '.list' do 138 | subject { described_class } 139 | 140 | it 'lists apps', :vcr do 141 | groups = subject.list 142 | expect(groups).to be_instance_of(described_class) 143 | expect(groups.groups.size).not_to eq(0) 144 | expect(groups.groups.first).to be_instance_of(described_class) 145 | end 146 | end 147 | 148 | describe '.get' do 149 | subject { described_class } 150 | 151 | it 'gets the group', :vcr do 152 | group = subject.get('/test-group') 153 | expect(group).to be_instance_of(described_class) 154 | expect(group.id).to eq('/test-group') 155 | expect(group.apps.first).to be_instance_of(Marathon::App) 156 | end 157 | 158 | it 'fails getting not existing app', :vcr do 159 | expect { 160 | subject.get('fooo group') 161 | }.to raise_error(Marathon::Error::NotFoundError) 162 | end 163 | end 164 | 165 | describe '.changes' do 166 | subject { described_class } 167 | 168 | it 'previews changes', :vcr do 169 | steps = subject.change('/test-group', {'instances' => 20}, false, true) 170 | expect(steps).to be_instance_of(Array) 171 | end 172 | 173 | it 'changes the group', :vcr do 174 | expect(subject.change('/test-group', {'instances' => 2}, true)) 175 | .to be_instance_of(Marathon::DeploymentInfo) 176 | expect(subject.change('/test-group', {'instances' => 1}, true)) 177 | .to be_instance_of(Marathon::DeploymentInfo) 178 | end 179 | end 180 | 181 | describe '.delete' do 182 | subject { described_class } 183 | 184 | it 'deletes the group', :vcr do 185 | expect(subject.delete('/test-group', true)) 186 | .to be_instance_of(Marathon::DeploymentInfo) 187 | end 188 | 189 | it 'fails deleting not existing app', :vcr do 190 | expect { 191 | subject.delete('fooo group') 192 | }.to raise_error(Marathon::Error::NotFoundError) 193 | end 194 | end 195 | 196 | end 197 | -------------------------------------------------------------------------------- /bin/marathon: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'marathon')) 4 | require 'trollop' 5 | require 'json' 6 | 7 | SUB_COMMANDS = %w[kill kill_tasks start scale rollback list list_tasks] 8 | ATTRIBUTES = [:id, :cmd, :executor, :instances, :cpus, :mem, :uris] 9 | DEFAULT_APP_OPTS = { 10 | :instances => 1, 11 | :cpus => 1, 12 | :mem => 10, 13 | } 14 | 15 | # print a list of apps to STDOUT 16 | def print_apps(apps) 17 | if apps.empty? 18 | puts "No apps currently running" 19 | else 20 | apps.each do |app| 21 | app.refresh 22 | puts app.to_pretty_s 23 | puts 24 | end 25 | end 26 | end 27 | 28 | # print a list of tasks to STDOUT 29 | def print_tasks(tasks) 30 | if tasks.empty? 31 | puts "No tasks currently running" 32 | else 33 | tasks.each do |task| 34 | puts task.to_pretty_s 35 | puts 36 | end 37 | end 38 | end 39 | 40 | def subcmd_list(cmd_opts) 41 | apps = Marathon::App.list(cmd_opts[:command], 'apps.tasks') 42 | print_apps(apps) 43 | end 44 | 45 | def subcmd_start(cmd_opts) 46 | if cmd_opts[:json] 47 | path = cmd_opts[:json] 48 | if path == '-' 49 | app_opts = Marathon::Util.keywordize_hash!(JSON.parse($stdin.read)) 50 | elsif File.exists?(path) 51 | app_opts = Marathon::Util.keywordize_hash!(JSON.parse(File.read(cmd_opts[:json]))) 52 | else 53 | raise Marathon::Error::ArgumentError, "File '#{path}' does not exist" 54 | end 55 | else 56 | app_opts = DEFAULT_APP_OPTS 57 | end 58 | app_opts.merge!(cmd_opts.select { |k,_| ATTRIBUTES.include?(k) and cmd_opts["#{k.id2name}_given".to_sym] }) 59 | if cmd_opts[:env] 60 | app_opts[:env] = app_opts.fetch(:env, {}).merge(Hash[cmd_opts[:env].map { |e| e.split('=', 2) }]) 61 | end 62 | if cmd_opts[:constraints] 63 | app_opts[:constraints] = app_opts.fetch(:constraints, {}).merge(cmd_opts[:constraint].map { |c| c.split(':') }) 64 | end 65 | app = Marathon::App.new(app_opts) 66 | puts "Starting app '#{app}'" 67 | deployment = app.start!(cmd_opts[:force]) 68 | deployment.wait(cmd_opts[:timeout] || 60) if cmd_opts[:sync] 69 | print_apps([app]) 70 | rescue Marathon::Error::MarathonError => e 71 | puts "#{e.class}: #{e.message}" 72 | exit 1 73 | rescue TimeoutError => e 74 | puts "Deployment took too long" 75 | exit 1 76 | end 77 | 78 | def subcmd_scale(cmd_opts) 79 | app = Marathon::App.get(cmd_opts[:id]) 80 | puts "Scaling app '#{app.id}' from #{app.instances} to #{cmd_opts[:instances]}" 81 | deployment = app.scale!(cmd_opts[:instances], cmd_opts[:force]) 82 | deployment.wait(cmd_opts[:timeout] || 60) if cmd_opts[:sync] 83 | puts deployment 84 | rescue Marathon::Error::NotFoundError => e 85 | puts "#{e.class}: #{e.message}" 86 | exit 1 87 | rescue TimeoutError => e 88 | puts "Deployment took too long" 89 | exit 1 90 | end 91 | 92 | def subcmd_kill(cmd_opts) 93 | puts "Removing app '#{cmd_opts[:id]}'" 94 | Marathon::App.delete(cmd_opts[:id]) 95 | puts 'done' 96 | rescue Marathon::Error::NotFoundError => e 97 | puts "#{e.class}: #{e.message}" 98 | exit 1 99 | end 100 | 101 | def subcmd_list_tasks(cmd_opts) 102 | if cmd_opts[:id] 103 | tasks = Marathon::Task.get(cmd_opts[:id]) 104 | else 105 | tasks = Marathon::Task.list 106 | end 107 | print_tasks(tasks) 108 | end 109 | 110 | def subcmd_kill_tasks(cmd_opts) 111 | if cmd_opts[:task_id] 112 | puts "Killing task of '#{cmd_opts[:id]}' with id '#{cmd_opts[:task_id]}'" 113 | tasks = [Marathon::Task.delete(cmd_opts[:task_id], cmd_opts[:scale])] 114 | elsif cmd_opts[:host] 115 | puts "Killing tasks of '#{cmd_opts[:id]}' on host '#{cmd_opts[:host]}'" 116 | tasks = Marathon::Task.delete_all(cmd_opts[:id], cmd_opts[:host], cmd_opts[:scale]) 117 | else 118 | puts "Killing tasks of '#{cmd_opts[:id]}'" 119 | tasks = Marathon::Task.delete_all(cmd_opts[:id], nil, cmd_opts[:scale]) 120 | end 121 | print_tasks(tasks) 122 | end 123 | 124 | def subcmd_rollback(cmd_opts) 125 | app = Marathon::App.get(cmd_opts[:id]) 126 | # Get current versions 127 | versions = app.versions 128 | # Retrieve N-1 version if none given 129 | target = cmd_opts[:version_id] ? cmd_opts[:version_id] : versions[1] 130 | # Deploy the target version of the app 131 | puts "Rollback app '#{app.id}' from #{versions[0]} to #{target}" 132 | app.roll_back!(target, cmd_opts[:force]) 133 | rescue Marathon::Error::MarathonError => e 134 | puts "#{e.class}: #{e.message}" 135 | exit 1 136 | rescue TimeoutError => e 137 | puts "Deployment took too long" 138 | exit 1 139 | end 140 | 141 | # parse global options 142 | def parse_global_opts 143 | global_opts = Trollop.options do 144 | version Marathon::VERSION 145 | banner <<-EOS 146 | Usage: marathon [global options] [command] [options] 147 | 148 | Available commands: 149 | 150 | kill Kill an app and remove it from Marathon. 151 | kill_tasks Kill a task or tasks belonging to a specified app. 152 | list Show a list of running apps and their options. 153 | list_tasks Show a list of an app's running tasks. 154 | rollback Rollback an app to a specific version. 155 | scale Scale the number of app instances. 156 | start Start a new app. 157 | 158 | Global options: 159 | EOS 160 | 161 | opt :url, 'Marathon host (default http://localhost:8080, or MARATHON_URL)', 162 | :short => '-M', :type => String, :default => Marathon.url 163 | opt :username, 'User name to authenticate against Marathon (optional, default unset, or MARATHON_USER).', 164 | :short => '-U', :type => String, :default => Marathon.options[:username] 165 | opt :password, 'Password to authenticate against Marathon (optional, default unset, or MARATHON_PASSWORD).', 166 | :short => '-P', :type => String 167 | opt :insecure, 'Ignore certificate verification failure (optional, default false, or MARATHON_INSECURE).', 168 | :short => '-I', :default => Marathon.options[:insecure] 169 | stop_on SUB_COMMANDS 170 | end 171 | return global_opts 172 | end 173 | 174 | # set global options to Marathon API 175 | def set_global_opts(global_opts) 176 | # Set client's URL 177 | Marathon.url = global_opts[:url] if global_opts[:url] 178 | global_opts.delete(:url) 179 | # Hack to hide password from help message. 180 | global_opts.delete(:password) unless global_opts[:password] 181 | # Set client's options 182 | Marathon.options = global_opts if global_opts.size > 0 183 | end 184 | 185 | # get the subcommand 186 | def parse_subcmd 187 | cmd = ARGV.shift 188 | return cmd 189 | end 190 | 191 | # parse subcommand specific options 192 | def parse_subcmd_opts(cmd) 193 | cmd_opts = case cmd 194 | when 'list' 195 | Trollop.options do 196 | opt :command, 'The command for the app.', :short => '-C', :type => String 197 | end 198 | when 'start' 199 | Trollop.options do 200 | opt :json, 'A json formatted file to read application details from. (use - to read from stdin)', :short => '-j', :type => String 201 | opt :id, 'A unique identifier for the app.', :short => '-i', :type => String 202 | opt :cmd, 'The command to start the app.', :short => '-C', :type => String 203 | opt :executor, 'The mesos executor to be used to launch the app.', :short => '-X', :type => String 204 | opt :instances, 'The number of instances to run (default 1).', :default => 1, :short => '-n' 205 | opt :cpus, 'The number of CPUs to give to this app, can be a fraction (default 1.0).', :short => '-c' 206 | opt :mem, 'The memory limit for this app, in MB, can be a fraction (default 10.0).', :short => '-m' 207 | opt :uri, 'URIs to download and unpack into the working directory.', :short => '-u', :type => :strings 208 | opt :env, 'Environment variables to add to the process, as NAME=VALUE.', :short => '-e', :type => :strings 209 | opt :constraint, 'Placement constraint for tasks, e.g. hostname:UNIQUE or rackid:CLUSTER', :type => :strings 210 | opt :force, 'The current deployment can be overridden by setting the `force`.', :short => '-f' 211 | opt :sync, 'Wait for the deployment to finish', :short => '-s' 212 | opt :timeout, 'Timout for sync call in seconds (default 60).', :type => Integer, :short => '-t' 213 | end 214 | when 'scale' 215 | Trollop.options do 216 | opt :id, 'A unique identifier for the app.', :short => '-i', :type => String, :required => true 217 | opt :instances, 'The number of instances to run.', :short => '-n', :type => Integer, :required => true 218 | opt :force, 'The current deployment can be overridden by setting the `force`.', :short => '-f' 219 | opt :sync, 'Wait for the deployment to finish', :short => '-s' 220 | opt :timeout, 'Timout for sync call in seconds (default 60).', :type => Integer, :short => '-t' 221 | end 222 | when 'rollback' 223 | Trollop.options do 224 | opt :id, 'A unique identifier for the app.', :short => '-i', :type => String, :required => true 225 | opt :version_id, 'A version identifier.', :short => '-v', :type => String, :default => nil 226 | opt :force, 'The current deployment can be overridden by setting the `force`.', :short => '-f' 227 | end 228 | when 'kill' 229 | Trollop.options do 230 | opt :id, 'A unique identifier for the app.', :short => '-i', :type => String, :required => true 231 | end 232 | when 'list_tasks' 233 | Trollop.options do 234 | opt :id, 'A unique identifier for the app.', :short => '-i', :type => String, :default => nil 235 | end 236 | when 'kill_tasks' 237 | Trollop.options do 238 | opt :host, 'Scope task killing to the given host.', :short => '-H', :type => String 239 | opt :id, 'A unique identifier for the app.', :short => '-i', :type => String, :required => true 240 | opt :scale, 'If true, the app is scaled down after killing tasks', :short => '-s' 241 | opt :task_id, 'A unique identifier for the task.', :short => '-t', :type => String 242 | end 243 | else 244 | {} 245 | end 246 | 247 | return cmd_opts 248 | end 249 | 250 | # Run selected subcmd 251 | def run_subcmd(cmd, cmd_opts) 252 | case cmd 253 | when 'list' 254 | subcmd_list(cmd_opts) 255 | when 'start' 256 | subcmd_start(cmd_opts) 257 | when 'scale' 258 | subcmd_scale(cmd_opts) 259 | when 'kill' 260 | subcmd_kill(cmd_opts) 261 | when 'list_tasks' 262 | subcmd_list_tasks(cmd_opts) 263 | when 'kill_tasks' 264 | subcmd_kill_tasks(cmd_opts) 265 | when 'rollback' 266 | subcmd_rollback(cmd_opts) 267 | else 268 | Trollop.die "unknown subcommand #{cmd.inspect}" 269 | end 270 | end 271 | 272 | global_opts = parse_global_opts 273 | set_global_opts(global_opts) 274 | 275 | cmd = parse_subcmd 276 | cmd_opts = parse_subcmd_opts(cmd) 277 | 278 | run_subcmd(cmd, cmd_opts) 279 | -------------------------------------------------------------------------------- /lib/marathon/group.rb: -------------------------------------------------------------------------------- 1 | # This class represents a Marathon Group. 2 | # See https://mesosphere.github.io/marathon/docs/rest-api.html#groups for full list of API's methods. 3 | class Marathon::Group < Marathon::Base 4 | 5 | ACCESSORS = %w[ id dependencies version ] 6 | 7 | DEFAULTS = { 8 | :dependencies => [] 9 | } 10 | 11 | attr_reader :apps, :groups 12 | 13 | # Create a new group object. 14 | # ++hash++: Hash including all attributes. 15 | # See https://mesosphere.github.io/marathon/docs/rest-api.html#post-/v2/groups for full details. 16 | # ++marathon_instance++: MarathonInstance holding a connection to marathon 17 | def initialize(hash, marathon_instance = Marathon.singleton) 18 | super(Marathon::Util.merge_keywordized_hash(DEFAULTS, hash), ACCESSORS) 19 | @marathon_instance = marathon_instance 20 | raise ArgumentError, 'Group must have an id' unless id 21 | refresh_attributes 22 | end 23 | 24 | # Reload attributes from marathon API. 25 | def refresh 26 | new_app = @marathon_instance.groups.get(id) 27 | @info = new_app.info 28 | refresh_attributes 29 | end 30 | 31 | # Create and start a the application group. Application groups can contain other application groups. 32 | # An application group can either hold other groups or applications, but can not be mixed in one. 33 | # Since the deployment of the group can take a considerable amount of time, 34 | # this endpoint returns immediatly with a version. The failure or success of the action is signalled via event. 35 | # There is a group_change_success and group_change_failed event with the given version. 36 | def start! 37 | @marathon_instance.groups.start(info) 38 | end 39 | 40 | # Change parameters of a deployed application group. 41 | # Changes to application parameters will result in a restart of this application. 42 | # A new application added to the group is started. 43 | # An existing application removed from the group gets stopped. 44 | # If there are no changes to the application definition, no restart is triggered. 45 | # During restart marathon keeps track, that the configured amount of minimal running instances are always available. 46 | # A deployment can run forever. This is the case, when the new application has a problem and does not become healthy. 47 | # In this case, human interaction is needed with 2 possible choices: 48 | # Rollback to an existing older version (send an existing version in the body) 49 | # Update with a newer version of the group which does not have the problems of the old one. 50 | # If there is an upgrade process already in progress, a new update will be rejected unless the force flag is set. 51 | # With the force flag given, a running upgrade is terminated and a new one is started. 52 | # Since the deployment of the group can take a considerable amount of time, 53 | # this endpoint returns immediatly with a version. The failure or success of the action is signalled via event. 54 | # There is a group_change_success and group_change_failed event with the given version. 55 | # ++hash++: Hash of attributes to change. 56 | # ++force++: If the group is affected by a running deployment, then the update operation will fail. 57 | # The current deployment can be overridden by setting the `force` query parameter. 58 | # ++dry_run++: Get a preview of the deployment steps Marathon would run for a given group update. 59 | def change!(hash, force = false, dry_run = false) 60 | Marathon::Util.keywordize_hash!(hash) 61 | if hash[:version] and hash.size > 1 62 | # remove :version if it's not the only key 63 | new_hash = Marathon::Util.remove_keys(hash, [:version]) 64 | else 65 | new_hash = hash 66 | end 67 | @marathon_instance.groups.change(id, new_hash, force, dry_run) 68 | end 69 | 70 | # Create a new version with parameters of an old version. 71 | # Currently running tasks are restarted, while maintaining the minimumHealthCapacity. 72 | # ++version++: Version name of the old version. 73 | # ++force++: If the group is affected by a running deployment, then the update operation will fail. 74 | # The current deployment can be overridden by setting the `force` query parameter. 75 | def roll_back!(version, force = false) 76 | change!({'version' => version}, force) 77 | end 78 | 79 | def to_s 80 | "Marathon::Group { :id => #{id} }" 81 | end 82 | 83 | # Returns a string for listing the group. 84 | def to_pretty_s 85 | %Q[ 86 | Group ID: #{id} 87 | #{pretty_array(apps)} 88 | #{pretty_array(groups)} 89 | Version: #{version} 90 | ].gsub(/\n\n+/, "\n").strip 91 | end 92 | 93 | private 94 | 95 | def pretty_array(array) 96 | array.map { |e| e.to_pretty_s.split("\n").map { |e| " #{e}" } }.join("\n") 97 | end 98 | 99 | # Rebuild attribute classes 100 | def refresh_attributes 101 | @apps = (info[:apps] || []).map { |e| Marathon::App.new(e, @marathon_instance) } 102 | @groups = (info[:groups] || []).map { |e| Marathon::Group.new(e, @marathon_instance) } 103 | end 104 | 105 | class << self 106 | # List the group with the specified ID. 107 | # ++id++: Group's id. 108 | def get(id) 109 | Marathon.singleton.groups.get(id) 110 | end 111 | 112 | # List all groups. 113 | def list 114 | Marathon.singleton.groups.list 115 | end 116 | 117 | # Delete the application group with id. 118 | # ++id++: Group's id. 119 | # ++force++: If the group is affected by a running deployment, then the update operation will fail. 120 | # The current deployment can be overridden by setting the `force` query parameter. 121 | def delete(id, force = false) 122 | Marathon.singleton.groups.delete(id, force) 123 | end 124 | 125 | alias :remove :delete 126 | 127 | # Create and start a new application group. Application groups can contain other application groups. 128 | # An application group can either hold other groups or applications, but can not be mixed in one. 129 | # Since the deployment of the group can take a considerable amount of time, 130 | # this endpoint returns immediatly with a version. The failure or success of the action is signalled via event. 131 | # There is a group_change_success and group_change_failed event with the given version. 132 | # ++hash++: Hash including all attributes 133 | # see https://mesosphere.github.io/marathon/docs/rest-api.html#post-/v2/groups for full details 134 | def start(hash) 135 | Marathon.singleton.groups.start(hash) 136 | end 137 | 138 | alias :create :start 139 | 140 | # Change parameters of a deployed application group. 141 | # Changes to application parameters will result in a restart of this application. 142 | # A new application added to the group is started. 143 | # An existing application removed from the group gets stopped. 144 | # If there are no changes to the application definition, no restart is triggered. 145 | # During restart marathon keeps track, that the configured amount of minimal running instances are always available. 146 | # A deployment can run forever. This is the case, 147 | # when the new application has a problem and does not become healthy. 148 | # In this case, human interaction is needed with 2 possible choices: 149 | # Rollback to an existing older version (send an existing version in the body) 150 | # Update with a newer version of the group which does not have the problems of the old one. 151 | # If there is an upgrade process already in progress, a new update will be rejected unless the force flag is set. 152 | # With the force flag given, a running upgrade is terminated and a new one is started. 153 | # Since the deployment of the group can take a considerable amount of time, 154 | # this endpoint returns immediatly with a version. The failure or success of the action is signalled via event. 155 | # There is a group_change_success and group_change_failed event with the given version. 156 | # ++id++: Group's id. 157 | # ++hash++: Hash of attributes to change. 158 | # ++force++: If the group is affected by a running deployment, then the update operation will fail. 159 | # The current deployment can be overridden by setting the `force` query parameter. 160 | # ++dry_run++: Get a preview of the deployment steps Marathon would run for a given group update. 161 | def change(id, hash, force = false, dry_run = false) 162 | Marathon.singleton.groups.change(id, hash, force, dry_run) 163 | end 164 | end 165 | end 166 | 167 | # This class represents a set of Groups 168 | class Marathon::Groups < Marathon::Base 169 | 170 | def initialize(marathon_instance) 171 | @marathon_instance = marathon_instance 172 | end 173 | 174 | # List the group with the specified ID. 175 | # ++id++: Group's id. 176 | def get(id) 177 | json = @marathon_instance.connection.get("/v2/groups/#{id}") 178 | Marathon::Group.new(json, @marathon_instance) 179 | end 180 | 181 | # List all groups. 182 | def list 183 | json = @marathon_instance.connection.get('/v2/groups') 184 | Marathon::Group.new(json, @marathon_instance) 185 | end 186 | 187 | # Delete the application group with id. 188 | # ++id++: Group's id. 189 | # ++force++: If the group is affected by a running deployment, then the update operation will fail. 190 | # The current deployment can be overridden by setting the `force` query parameter. 191 | def delete(id, force = false) 192 | query = {} 193 | query[:force] = true if force 194 | json = @marathon_instance.connection.delete("/v2/groups/#{id}", query) 195 | Marathon::DeploymentInfo.new(json, @marathon_instance) 196 | end 197 | 198 | alias :remove :delete 199 | 200 | # Create and start a new application group. Application groups can contain other application groups. 201 | # An application group can either hold other groups or applications, but can not be mixed in one. 202 | # Since the deployment of the group can take a considerable amount of time, 203 | # this endpoint returns immediatly with a version. The failure or success of the action is signalled via event. 204 | # There is a group_change_success and group_change_failed event with the given version. 205 | # ++hash++: Hash including all attributes 206 | # see https://mesosphere.github.io/marathon/docs/rest-api.html#post-/v2/groups for full details 207 | def start(hash) 208 | json = @marathon_instance.connection.post('/v2/groups', nil, :body => hash) 209 | Marathon::DeploymentInfo.new(json, @marathon_instance) 210 | end 211 | 212 | alias :create :start 213 | 214 | # Change parameters of a deployed application group. 215 | # Changes to application parameters will result in a restart of this application. 216 | # A new application added to the group is started. 217 | # An existing application removed from the group gets stopped. 218 | # If there are no changes to the application definition, no restart is triggered. 219 | # During restart marathon keeps track, that the configured amount of minimal running instances are always available. 220 | # A deployment can run forever. This is the case, 221 | # when the new application has a problem and does not become healthy. 222 | # In this case, human interaction is needed with 2 possible choices: 223 | # Rollback to an existing older version (send an existing version in the body) 224 | # Update with a newer version of the group which does not have the problems of the old one. 225 | # If there is an upgrade process already in progress, a new update will be rejected unless the force flag is set. 226 | # With the force flag given, a running upgrade is terminated and a new one is started. 227 | # Since the deployment of the group can take a considerable amount of time, 228 | # this endpoint returns immediatly with a version. The failure or success of the action is signalled via event. 229 | # There is a group_change_success and group_change_failed event with the given version. 230 | # ++id++: Group's id. 231 | # ++hash++: Hash of attributes to change. 232 | # ++force++: If the group is affected by a running deployment, then the update operation will fail. 233 | # The current deployment can be overridden by setting the `force` query parameter. 234 | # ++dry_run++: Get a preview of the deployment steps Marathon would run for a given group update. 235 | def change(id, hash, force = false, dry_run = false) 236 | query = {} 237 | query[:force] = true if force 238 | query[:dryRun] = true if dry_run 239 | json = @marathon_instance.connection.put("/v2/groups/#{id}", query, :body => hash) 240 | if dry_run 241 | json['steps'].map { |e| Marathon::DeploymentStep.new(e) } 242 | else 243 | Marathon::DeploymentInfo.new(json, @marathon_instance) 244 | end 245 | end 246 | end 247 | 248 | -------------------------------------------------------------------------------- /lib/marathon/app.rb: -------------------------------------------------------------------------------- 1 | # This class represents a Marathon App. 2 | # See https://mesosphere.github.io/marathon/docs/rest-api.html#apps for full list of API's methods. 3 | class Marathon::App < Marathon::Base 4 | 5 | ACCESSORS = %w[ id args cmd cpus disk env executor fetch instances mem ports requirePorts 6 | storeUris tasksHealthy tasksUnhealthy tasksRunning tasksStaged upgradeStrategy 7 | deployments uris user version labels ] 8 | 9 | DEFAULTS = { 10 | :env => {}, 11 | :labels => {} 12 | } 13 | 14 | attr_reader :healthChecks, :constraints, :container, :read_only, :tasks 15 | 16 | # Create a new application object. 17 | # ++hash++: Hash including all attributes. 18 | # See https://mesosphere.github.io/marathon/docs/rest-api.html#post-/v2/apps for full details. 19 | # ++read_only++: prevent actions on this application 20 | # ++marathon_instance++: MarathonInstance holding a connection to marathon 21 | def initialize(hash, marathon_instance = Marathon.singleton, read_only = false) 22 | super(Marathon::Util.merge_keywordized_hash(DEFAULTS, hash), ACCESSORS) 23 | raise ArgumentError, 'App must have an id' unless id 24 | @read_only = read_only 25 | @marathon_instance = marathon_instance 26 | refresh_attributes 27 | end 28 | 29 | # Prevent actions on read only instances. 30 | # Raises an ArgumentError when triying to change read_only instances. 31 | def check_read_only 32 | if read_only 33 | raise Marathon::Error::ArgumentError, "This app is 'read only' and does not support any actions" 34 | end 35 | end 36 | 37 | # List the versions of the application. 38 | # ++version++: Get a specific versions 39 | # Returns Array of Strings if ++version = nil++, 40 | # else returns Hash with version information. 41 | def versions(version = nil) 42 | if version 43 | @marathon_instance.apps.version(id, version) 44 | else 45 | @marathon_instance.apps.versions(id) 46 | end 47 | end 48 | 49 | # Reload attributes from marathon API. 50 | def refresh 51 | check_read_only 52 | new_app = @marathon_instance.apps.get(id) 53 | @info = new_app.info 54 | refresh_attributes 55 | self 56 | end 57 | 58 | # Create and start the application. 59 | # ++force++: If the app is affected by a running deployment, then the update operation will fail. 60 | # The current deployment can be overridden by setting the `force` query parameter. 61 | def start!(force = false) 62 | change!(info, force) 63 | end 64 | 65 | # Initiates a rolling restart of all running tasks of the given app. 66 | # This call respects the configured minimumHealthCapacity. 67 | # ++force++: If the app is affected by a running deployment, then the update operation will fail. 68 | # The current deployment can be overridden by setting the `force` query parameter. 69 | def restart!(force = false) 70 | check_read_only 71 | @marathon_instance.apps.restart(id, force) 72 | end 73 | 74 | # Change parameters of a running application. 75 | # The new application parameters apply only to subsequently created tasks. 76 | # Currently running tasks are restarted, while maintaining the minimumHealthCapacity. 77 | # ++hash++: Hash of attributes to change. 78 | # ++force++: If the app is affected by a running deployment, then the update operation will fail. 79 | # The current deployment can be overridden by setting the `force` query parameter. 80 | def change!(hash, force = false) 81 | check_read_only 82 | Marathon::Util.keywordize_hash!(hash) 83 | if hash[:version] and hash.size > 1 84 | # remove :version if it's not the only key 85 | new_hash = Marathon::Util.remove_keys(hash, [:version]) 86 | else 87 | new_hash = hash 88 | end 89 | @marathon_instance.apps.change(id, new_hash, force) 90 | end 91 | 92 | # Create a new version with parameters of an old version. 93 | # Currently running tasks are restarted, while maintaining the minimumHealthCapacity. 94 | # ++version++: Version name of the old version. 95 | # ++force++: If the app is affected by a running deployment, then the update operation will fail. 96 | # The current deployment can be overridden by setting the `force` query parameter. 97 | def roll_back!(version, force = false) 98 | change!({:version => version}, force) 99 | end 100 | 101 | # Change the number of desired instances. 102 | # ++instances++: Number of running instances. 103 | # ++force++: If the app is affected by a running deployment, then the update operation will fail. 104 | # The current deployment can be overridden by setting the `force` query parameter. 105 | def scale!(instances, force = false) 106 | change!({:instances => instances}, force) 107 | end 108 | 109 | # Change the number of desired instances to 0. 110 | # ++force++: If the app is affected by a running deployment, then the update operation will fail. 111 | # The current deployment can be overridden by setting the `force` query parameter. 112 | def suspend!(force = false) 113 | scale!(0, force) 114 | end 115 | 116 | def to_s 117 | "Marathon::App { :id => #{id} }" 118 | end 119 | 120 | # Returns a string for listing the application. 121 | def to_pretty_s 122 | %Q[ 123 | App ID: #{id} 124 | Instances: #{tasks.size}/#{instances} 125 | Command: #{cmd} 126 | CPUs: #{cpus} 127 | Memory: #{mem} MB 128 | #{pretty_container} 129 | #{pretty_uris} 130 | #{pretty_env} 131 | #{pretty_constraints} 132 | Version: #{version} 133 | ] 134 | .gsub(/\n\s+/, "\n") 135 | .gsub(/\n\n+/, "\n") 136 | .strip 137 | end 138 | 139 | private 140 | 141 | def pretty_container 142 | if container and container.docker 143 | "Docker: #{container.docker.to_pretty_s}" 144 | end 145 | end 146 | 147 | def pretty_env 148 | env.map { |k, v| "ENV: #{k}=#{v}" }.join("\n") 149 | end 150 | 151 | def pretty_uris 152 | [ (fetch || []).map { |e| e[:uri] } , uris ].compact.reduce([], :|).map { |e| "URI: #{e}" }.join("\n") 153 | end 154 | 155 | def pretty_constraints 156 | constraints.map { |e| "Constraint: #{e.to_pretty_s}" }.join("\n") 157 | end 158 | 159 | # Rebuild attribute classes 160 | def refresh_attributes 161 | @healthChecks = (info[:healthChecks] || []).map { |e| Marathon::HealthCheck.new(e) } 162 | @constraints = (info[:constraints] || []).map { |e| Marathon::Constraint.new(e) } 163 | if info[:container] 164 | @container = Marathon::Container.new(info[:container]) 165 | else 166 | @container = nil 167 | end 168 | @tasks = (@info[:tasks] || []).map { |e| Marathon::Task.new(e, @marathon_instance) } 169 | end 170 | 171 | class << self 172 | 173 | # List the application with id. 174 | # ++id++: Application's id. 175 | def get(id) 176 | Marathon.singleton.apps.get(id) 177 | end 178 | 179 | # List all applications. 180 | # ++:cmd++: Filter apps to only those whose commands contain cmd. 181 | # ++:embed++: Embeds nested resources that match the supplied path. 182 | # Possible values: 183 | # "apps.tasks". Apps' tasks are not embedded in the response by default. 184 | # "apps.failures". Apps' last failures are not embedded in the response by default. 185 | def list(cmd = nil, embed = nil, id=nil, label=nil) 186 | Marathon.singleton.apps.list(cmd, embed, id, label) 187 | end 188 | 189 | # Delete the application with id. 190 | # ++id++: Application's id. 191 | def delete(id) 192 | Marathon.singleton.apps.delete(id) 193 | end 194 | 195 | alias :remove :delete 196 | 197 | # Create and start an application. 198 | # ++hash++: Hash including all attributes 199 | # see https://mesosphere.github.io/marathon/docs/rest-api.html#post-/v2/apps for full details 200 | def start(hash) 201 | Marathon.singleton.apps.start(hash) 202 | end 203 | 204 | alias :create :start 205 | 206 | # Restart the application with id. 207 | # ++id++: Application's id. 208 | # ++force++: If the app is affected by a running deployment, then the update operation will fail. 209 | # The current deployment can be overridden by setting the `force` query parameter. 210 | def restart(id, force = false) 211 | Marathon.singleton.apps.restart(id, force) 212 | end 213 | 214 | # Change parameters of a running application. The new application parameters apply only to subsequently 215 | # created tasks. Currently running tasks are restarted, while maintaining the minimumHealthCapacity. 216 | # ++id++: Application's id. 217 | # ++hash++: A subset of app's attributes. 218 | # ++force++: If the app is affected by a running deployment, then the update operation will fail. 219 | # The current deployment can be overridden by setting the `force` query parameter. 220 | def change(id, hash, force = false) 221 | Marathon.singleton.apps.change(id, hash, force) 222 | end 223 | 224 | # List the versions of the application with id. 225 | # ++id++: Application id 226 | def versions(id) 227 | Marathon.singleton.apps.versions(id) 228 | end 229 | 230 | # List the configuration of the application with id at version. 231 | # ++id++: Application id 232 | # ++version++: Version name 233 | def version(id, version) 234 | Marathon.singleton.apps.version(id, version) 235 | end 236 | end 237 | end 238 | 239 | # This class represents a set of Apps 240 | class Marathon::Apps 241 | def initialize(marathon_instance) 242 | @marathon_instance = marathon_instance 243 | @connection = marathon_instance.connection 244 | end 245 | 246 | # List the application with id. 247 | # ++id++: Application's id. 248 | def get(id) 249 | json = @connection.get("/v2/apps/#{id}")['app'] 250 | Marathon::App.new(json, @marathon_instance) 251 | end 252 | 253 | # Delete the application with id. 254 | # ++id++: Application's id. 255 | def delete(id) 256 | json = @connection.delete("/v2/apps/#{id}") 257 | Marathon::DeploymentInfo.new(json, @marathon_instance) 258 | end 259 | 260 | # Create and start an application. 261 | # ++hash++: Hash including all attributes 262 | # see https://mesosphere.github.io/marathon/docs/rest-api.html#post-/v2/apps for full details 263 | def start(hash) 264 | json = @connection.post('/v2/apps', nil, :body => hash) 265 | Marathon::App.new(json, @marathon_instance) 266 | end 267 | 268 | # Restart the application with id. 269 | # ++id++: Application's id. 270 | # ++force++: If the app is affected by a running deployment, then the update operation will fail. 271 | # The current deployment can be overridden by setting the `force` query parameter. 272 | def restart(id, force = false) 273 | query = {} 274 | query[:force] = true if force 275 | json = @connection.post("/v2/apps/#{id}/restart", query) 276 | Marathon::DeploymentInfo.new(json, @marathon_instance) 277 | end 278 | 279 | # Change parameters of a running application. The new application parameters apply only to subsequently 280 | # created tasks. Currently running tasks are restarted, while maintaining the minimumHealthCapacity. 281 | # ++id++: Application's id. 282 | # ++hash++: A subset of app's attributes. 283 | # ++force++: If the app is affected by a running deployment, then the update operation will fail. 284 | # The current deployment can be overridden by setting the `force` query parameter. 285 | def change(id, hash, force = false) 286 | query = {} 287 | query[:force] = true if force 288 | json = @connection.put("/v2/apps/#{id}", query, :body => hash.merge(:id => id)) 289 | Marathon::DeploymentInfo.new(json, @marathon_instance) 290 | end 291 | 292 | # List the versions of the application with id. 293 | # ++id++: Application id 294 | def versions(id) 295 | json = @connection.get("/v2/apps/#{id}/versions") 296 | json['versions'] 297 | end 298 | 299 | # List the configuration of the application with id at version. 300 | # ++id++: Application id 301 | # ++version++: Version name 302 | def version(id, version) 303 | json = @connection.get("/v2/apps/#{id}/versions/#{version}") 304 | Marathon::App.new(json, @marathon_instance, true) 305 | end 306 | 307 | # List all applications. 308 | # ++:cmd++: Filter apps to only those whose commands contain cmd. 309 | # ++:embed++: Embeds nested resources that match the supplied path. 310 | # Possible values: 311 | # "apps.tasks". Apps' tasks are not embedded in the response by default. 312 | # "apps.counts". Apps' task counts (tasksStaged, tasksRunning, tasksHealthy, tasksUnhealthy). 313 | # "apps.deployments". Apps' embed all deployment identifier. 314 | # "apps.lastTaskFailure". Apps' embeds the lastTaskFailure 315 | # "apps.failures". Apps' last failures are not embedded in the response by default. 316 | # "apps.taskStats". Apps' exposes task statatistics. 317 | # ++:id++: Filter apps to only return those whose id is or contains id. 318 | # ++:label++: A label selector query contains one or more label selectors 319 | def list(cmd = nil, embed = nil, id=nil, label=nil) 320 | query = {} 321 | query[:cmd] = cmd if cmd 322 | query[:id] = id if id 323 | query[:label] = label if label 324 | Marathon::Util.add_choice(query, :embed, embed, %w[apps.tasks apps.counts 325 | apps.deployments apps.lastTaskFailure apps.failures apps.taskStats ]) 326 | json = @connection.get('/v2/apps', query)['apps'] 327 | json.map { |j| Marathon::App.new(j, @marathon_instance) } 328 | end 329 | 330 | end 331 | -------------------------------------------------------------------------------- /spec/marathon/app_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Marathon::App do 4 | 5 | describe '#to_s' do 6 | subject { described_class.new({ 7 | 'id' => '/app/foo', 8 | 'instances' => 1, 9 | 'tasks' => [], 10 | 'container' => { 11 | :type => 'DOCKER', 'docker' => {'image' => 'foo/bar:latest'}, 12 | }, 13 | 'env' => {'FOO' => 'BAR', 'blubb' => 'blah'}, 14 | 'constraints' => [['hostname', 'UNIQUE']], 15 | 'fetch' => [ 16 | { 'uri' => 'http://example.com/big.tar' }, 17 | ], 18 | 'labels' => {'abc' => '123'}, 19 | 'version' => 'foo-version' 20 | }, double(Marathon::MarathonInstance)) } 21 | 22 | let(:expected_string) do 23 | "Marathon::App { :id => /app/foo }" 24 | end 25 | 26 | let(:expected_pretty_string) do 27 | "App ID: /app/foo\n" + \ 28 | "Instances: 0/1\n" + \ 29 | "Command: \n" + \ 30 | "CPUs: \n" + \ 31 | "Memory: MB\n" + \ 32 | "Docker: foo/bar:latest\n" + \ 33 | "URI: http://example.com/big.tar\n" + \ 34 | "ENV: FOO=BAR\n" + \ 35 | "ENV: blubb=blah\n" + \ 36 | "Constraint: hostname:UNIQUE\n" + \ 37 | "Version: foo-version" 38 | end 39 | 40 | its(:to_s) { should == expected_string } 41 | its(:to_pretty_s) { should == expected_pretty_string } 42 | end 43 | 44 | describe '#to_json' do 45 | subject { described_class.new({'id' => '/app/foo'}, double(Marathon::MarathonInstance)) } 46 | 47 | let(:expected_string) do 48 | '{"env":{},"labels":{},"id":"/app/foo"}' 49 | end 50 | 51 | its(:to_json) { should == expected_string } 52 | end 53 | 54 | describe '#check_read_only' do 55 | subject { described_class.new({'id' => '/ubuntu2'}, double(Marathon::MarathonInstance), true) } 56 | 57 | it 'does not allow changing the app' do 58 | expect { subject.change!({}) }.to raise_error(Marathon::Error::ArgumentError) 59 | end 60 | end 61 | 62 | describe '#container' do 63 | subject { described_class.new({ 64 | 'id' => '/ubuntu2', 65 | 'container' => { 66 | 'type' => 'DOCKER', 67 | 'docker' => {'image' => 'felixb/yocto-httpd'} 68 | } 69 | }, double(Marathon::MarathonInstance)) } 70 | 71 | it 'has container' do 72 | expect(subject.container).to be_instance_of(Marathon::Container) 73 | expect(subject.container.type).to eq('DOCKER') 74 | end 75 | end 76 | 77 | describe '#constraints' do 78 | subject { described_class.new({'id' => '/ubuntu2', 'constraints' => [['hostname', 'UNIQUE']]}, 79 | double(Marathon::MarathonInstance)) } 80 | 81 | it 'has constraints' do 82 | expect(subject.constraints).to be_instance_of(Array) 83 | expect(subject.constraints.first).to be_instance_of(Marathon::Constraint) 84 | end 85 | end 86 | 87 | describe '#labels' do 88 | describe 'w/ lables' do 89 | subject { described_class.new({'id' => '/ubuntu2', 'labels' => {'env' => 'abc', 'xyz' => '123'}}, 90 | double(Marathon::MarathonInstance)) } 91 | it 'has keywordized labels' do 92 | expect(subject.labels).to be_instance_of(Hash) 93 | expect(subject.labels).to have_key(:env) 94 | end 95 | 96 | 97 | end 98 | 99 | describe 'w/o labels' do 100 | subject { described_class.new({'id' => '/ubuntu2'}, double(Marathon::MarathonInstance)) } 101 | it 'has empty labels' do 102 | expect(subject.labels).to eq({}) 103 | end 104 | end 105 | end 106 | 107 | describe '#constraints' do 108 | subject { described_class.new({'id' => '/ubuntu2', 'healthChecks' => [{'path' => '/ping'}]}, 109 | double(Marathon::MarathonInstance)) } 110 | 111 | it 'has healthChecks' do 112 | expect(subject.healthChecks).to be_instance_of(Array) 113 | expect(subject.healthChecks.first).to be_instance_of(Marathon::HealthCheck) 114 | end 115 | end 116 | 117 | describe '#tasks' do 118 | subject { described_class.new({'id' => '/ubuntu2'}, double(Marathon::MarathonInstance)) } 119 | 120 | it 'shows already loaded tasks w/o API call' do 121 | subject.info[:tasks] = [] 122 | expect(subject).not_to receive(:refresh) 123 | expect(subject.tasks).to eq([]) 124 | end 125 | end 126 | 127 | describe '#versions' do 128 | before(:each) do 129 | @apps = double(Marathon::Apps) 130 | @marathon_instance = double(Marathon::MarathonInstance, :apps => @apps) 131 | @subject = described_class.new({'id' => '/ubuntu2'}, @marathon_instance) 132 | end 133 | 134 | it 'loads versions from API' do 135 | expect(@apps).to receive(:versions).with('/ubuntu2') { ['foo-version'] } 136 | expect(@subject.versions).to eq(['foo-version']) 137 | end 138 | 139 | it 'loads version from API' do 140 | expect(@apps).to receive(:version).with('/ubuntu2', 'foo-version') { 141 | Marathon::App.new({'id' => '/ubuntu2', 'version' => 'foo-version'}, true) 142 | } 143 | expect(@subject.versions('foo-version').version).to eq('foo-version') 144 | end 145 | end 146 | 147 | describe '#start!' do 148 | before(:each) do 149 | @apps = double(Marathon::Apps) 150 | @marathon_instance = double(Marathon::MarathonInstance, :apps => @apps) 151 | @subject = described_class.new({'id' => '/app/foo'}, @marathon_instance) 152 | end 153 | 154 | it 'checks for read only' do 155 | expect(@subject).to receive(:check_read_only) 156 | expect(@apps).to receive(:change).with( 157 | '/app/foo', 158 | {:env => {}, :labels => {}, :id => "/app/foo"}, 159 | false 160 | ) 161 | @subject.start! 162 | end 163 | 164 | it 'starts the app' do 165 | expect(@apps).to receive(:change) 166 | .with( 167 | '/app/foo', 168 | {:env => {}, :labels => {}, :id => "/app/foo"}, 169 | false 170 | ) 171 | @subject.start! 172 | end 173 | end 174 | 175 | describe '#refresh' do 176 | before(:each) do 177 | @apps = double(Marathon::Apps) 178 | @marathon_instance = double(Marathon::MarathonInstance, :apps => @apps) 179 | @subject = described_class.new({'id' => '/app/foo'}, @marathon_instance) 180 | end 181 | 182 | it 'checks for read only' do 183 | expect(@subject).to receive(:check_read_only) 184 | expect(@apps).to receive(:get) { described_class.new({'id' => @subject.id}, double(Marathon::MarathonInstance)) } 185 | @subject.refresh 186 | end 187 | 188 | it 'refreshs the app' do 189 | expect(@apps).to receive(:get).with('/app/foo') do 190 | described_class.new({'id' => '/app/foo', 'refreshed' => true}, double(Marathon::MarathonInstance)) 191 | end 192 | @subject.refresh 193 | expect(@subject.info[:refreshed]).to be(true) 194 | end 195 | 196 | it 'returns the app' do 197 | expect(@apps).to receive(:get).with('/app/foo') do 198 | described_class.new({'id' => '/app/foo'}, double(Marathon::MarathonInstance)) 199 | end 200 | expect(@subject.refresh).to be @subject 201 | end 202 | end 203 | 204 | describe '#restart!' do 205 | before(:each) do 206 | @apps = double(Marathon::Apps) 207 | @marathon_instance = double(Marathon::MarathonInstance, :apps => @apps) 208 | @subject = described_class.new({'id' => '/app/foo'}, @marathon_instance) 209 | end 210 | 211 | 212 | it 'checks for read only' do 213 | expect(@subject).to receive(:check_read_only) 214 | expect(@apps).to receive(:restart) 215 | @subject.restart! 216 | end 217 | 218 | it 'restarts the app' do 219 | expect(@apps).to receive(:restart) 220 | .with('/app/foo', false) 221 | @subject.restart! 222 | end 223 | 224 | it 'restarts the app, force' do 225 | expect(@apps).to receive(:restart) 226 | .with('/app/foo', true) 227 | @subject.restart!(true) 228 | end 229 | end 230 | 231 | describe '#change!' do 232 | before(:each) do 233 | @apps = double(Marathon::Apps) 234 | @marathon_instance = double(Marathon::MarathonInstance, :apps => @apps) 235 | @subject = described_class.new({'id' => '/app/foo'}, @marathon_instance) 236 | end 237 | 238 | it 'checks for read only' do 239 | expect(@subject).to receive(:check_read_only) 240 | expect(@apps).to receive(:change) 241 | @subject.change!({}) 242 | end 243 | 244 | it 'changes the app' do 245 | expect(@apps).to receive(:change).with('/app/foo', {:instances => 9000}, false) 246 | @subject.change!('instances' => 9000, 'version' => 'old-version') 247 | end 248 | end 249 | 250 | describe '#roll_back!' do 251 | before(:each) do 252 | @apps = double(Marathon::Apps) 253 | @marathon_instance = double(Marathon::MarathonInstance, :apps => @apps) 254 | @subject = described_class.new({:id => '/app/foo', :instances => 10}, @marathon_instance) 255 | end 256 | 257 | 258 | it 'checks for read only' do 259 | expect(@subject).to receive(:check_read_only) 260 | expect(@apps).to receive(:change) 261 | @subject.roll_back!('old_version') 262 | end 263 | 264 | it 'changes the app' do 265 | expect(@subject).to receive(:change!).with({:version => 'old_version'}, false) 266 | @subject.roll_back!('old_version') 267 | end 268 | 269 | it 'changes the app with force' do 270 | expect(@subject).to receive(:change!).with({:version => 'old_version'}, true) 271 | @subject.roll_back!('old_version', true) 272 | end 273 | end 274 | 275 | describe '#scale!' do 276 | before(:each) do 277 | @apps = double(Marathon::Apps) 278 | @marathon_instance = double(Marathon::MarathonInstance, :apps => @apps) 279 | @subject = described_class.new({:id => '/app/foo', :instances => 10}, @marathon_instance) 280 | end 281 | 282 | 283 | it 'checks for read only' do 284 | expect(@subject).to receive(:check_read_only) 285 | expect(@apps).to receive(:change) 286 | @subject.scale!(5) 287 | end 288 | 289 | it 'changes the app' do 290 | expect(@subject).to receive(:change!).with({:instances => 9000}, false) 291 | @subject.scale!(9000) 292 | end 293 | 294 | it 'changes the app with force' do 295 | expect(@subject).to receive(:change!).with({:instances => 9000}, true) 296 | @subject.scale!(9000, true) 297 | end 298 | end 299 | 300 | describe '#suspend!' do 301 | before(:each) do 302 | @apps = double(Marathon::Apps) 303 | @marathon_instance = double(Marathon::MarathonInstance, :apps => @apps) 304 | @subject = described_class.new({:id => '/app/foo', :instances => 10}, @marathon_instance) 305 | end 306 | 307 | it 'checks for read only' do 308 | expect(@subject).to receive(:check_read_only) 309 | expect(@apps).to receive(:change) 310 | @subject.suspend! 311 | end 312 | 313 | it 'scales the app to 0' do 314 | expect(@subject).to receive(:scale!).with(0, false) 315 | @subject.suspend! 316 | end 317 | 318 | it 'scales the app to 0 with force' do 319 | expect(@subject).to receive(:scale!).with(0, true) 320 | @subject.suspend!(true) 321 | end 322 | end 323 | 324 | describe '.list' do 325 | subject { described_class } 326 | 327 | it 'passes arguments to api call' do 328 | expect(Marathon.connection).to receive(:get) 329 | .with('/v2/apps', {:cmd => 'foo', :embed => 'apps.tasks'}) 330 | .and_return({'apps' => []}) 331 | subject.list('foo', 'apps.tasks') 332 | end 333 | 334 | it 'passing id argument to api call' do 335 | expect(Marathon.connection).to receive(:get) 336 | .with('/v2/apps', {:id => '/app/foo'}) 337 | .and_return({'apps' => []}) 338 | subject.list(nil, nil, '/app/foo') 339 | end 340 | 341 | it 'passing label argument to api call' do 342 | expect(Marathon.connection).to receive(:get) 343 | .with('/v2/apps', {:label => 'abc'}) 344 | .and_return({'apps' => []}) 345 | subject.list(nil, nil, nil, 'abc') 346 | end 347 | 348 | it 'raises error when run with strange embed' do 349 | expect { 350 | subject.list(nil, 'foo') 351 | }.to raise_error(Marathon::Error::ArgumentError) 352 | end 353 | 354 | it 'lists apps', :vcr do 355 | apps = subject.list 356 | expect(apps.size).not_to eq(0) 357 | expect(apps.first).to be_instance_of(described_class) 358 | end 359 | 360 | end 361 | 362 | describe '.start' do 363 | subject { described_class } 364 | 365 | it 'starts the app', :vcr do 366 | app = subject.start({ 367 | :id => '/test-app', 368 | :cmd => 'sleep 10', 369 | :instances => 1, 370 | :cpus => 0.1, 371 | :mem => 32 372 | }) 373 | expect(app).to be_instance_of(described_class) 374 | expect(app.id).to eq('/test-app') 375 | expect(app.instances).to eq(1) 376 | expect(app.cpus).to eq(0.1) 377 | expect(app.mem).to eq(32) 378 | end 379 | 380 | it 'fails getting not existing app', :vcr do 381 | expect { 382 | subject.get('fooo app') 383 | }.to raise_error(Marathon::Error::NotFoundError) 384 | end 385 | end 386 | 387 | describe '.get' do 388 | subject { described_class } 389 | 390 | it 'gets the app', :vcr do 391 | app = subject.get('/ubuntu') 392 | expect(app).to be_instance_of(described_class) 393 | expect(app.id).to eq('/ubuntu') 394 | expect(app.instances).to eq(1) 395 | expect(app.cpus).to eq(0.1) 396 | expect(app.mem).to eq(64) 397 | end 398 | 399 | it 'fails getting not existing app', :vcr do 400 | expect { 401 | subject.get('fooo app') 402 | }.to raise_error(Marathon::Error::NotFoundError) 403 | end 404 | end 405 | 406 | describe '.delete' do 407 | subject { described_class } 408 | 409 | it 'deletes the app', :vcr do 410 | expect(subject.delete('/test-app')) 411 | .to be_instance_of(Marathon::DeploymentInfo) 412 | end 413 | 414 | it 'fails deleting not existing app', :vcr do 415 | expect { 416 | subject.delete('fooo app') 417 | }.to raise_error(Marathon::Error::NotFoundError) 418 | end 419 | end 420 | 421 | describe '.restart' do 422 | subject { described_class } 423 | 424 | it 'restarts an app', :vcr do 425 | expect(subject.restart('/ubuntu2')).to be_instance_of(Marathon::DeploymentInfo) 426 | end 427 | 428 | it 'fails restarting not existing app', :vcr do 429 | expect { 430 | subject.restart('fooo app') 431 | }.to raise_error(Marathon::Error::NotFoundError) 432 | end 433 | end 434 | 435 | describe '.changes' do 436 | subject { described_class } 437 | 438 | it 'changes the app', :vcr do 439 | expect(subject.change('/ubuntu2', {'instances' => 2}, true)) 440 | .to be_instance_of(Marathon::DeploymentInfo) 441 | expect(subject.change('/ubuntu2', {'instances' => 1}, true)) 442 | .to be_instance_of(Marathon::DeploymentInfo) 443 | end 444 | 445 | it 'fails with stange attributes', :vcr do 446 | expect { 447 | subject.change('/ubuntu2', {'instances' => 'foo'}) 448 | }.to raise_error(Marathon::Error::ClientError) 449 | end 450 | end 451 | 452 | describe '.versions' do 453 | subject { described_class } 454 | 455 | it 'gets versions', :vcr do 456 | versions = subject.versions('/ubuntu2') 457 | expect(versions).to be_instance_of(Array) 458 | expect(versions.first).to be_instance_of(String) 459 | end 460 | end 461 | 462 | describe '.version' do 463 | subject { described_class } 464 | 465 | it 'gets a version', :vcr do 466 | versions = subject.versions('/ubuntu2') 467 | version = subject.version('/ubuntu2', versions.first) 468 | expect(version).to be_instance_of(Marathon::App) 469 | expect(version.read_only).to be(true) 470 | end 471 | end 472 | end 473 | --------------------------------------------------------------------------------