├── .gitignore ├── Gemfile ├── .travis.yml ├── lib ├── fleet │ ├── version.rb │ ├── client │ │ ├── machines.rb │ │ ├── state.rb │ │ └── unit.rb │ ├── connection.rb │ ├── service_definition.rb │ ├── configuration.rb │ ├── error.rb │ ├── request.rb │ └── client.rb └── fleet.rb ├── Rakefile ├── spec ├── spec_helper.rb ├── fleet │ ├── error_spec.rb │ ├── client │ │ ├── state_spec.rb │ │ ├── machines_spec.rb │ │ └── unit_spec.rb │ ├── service_definition_spec.rb │ ├── configuration_spec.rb │ ├── connection_spec.rb │ ├── request_spec.rb │ └── client_spec.rb └── fleet_spec.rb ├── circle.yml ├── Gemfile.lock ├── fleet-api.gemspec ├── CHANGELOG.md ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | pkg/ 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - 2.0.0 5 | - 2.1.1 6 | -------------------------------------------------------------------------------- /lib/fleet/version.rb: -------------------------------------------------------------------------------- 1 | module Fleet 2 | VERSION = '1.2.0'.freeze unless defined?(Fleet::VERSION) 3 | end 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | RSpec::Core::RakeTask.new(:spec) 6 | 7 | task :test => :spec 8 | task :default => :spec 9 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | require 'simplecov-rcov' 3 | 4 | SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter 5 | SimpleCov.start do 6 | add_filter '/spec' 7 | end 8 | 9 | require 'fleet' 10 | -------------------------------------------------------------------------------- /lib/fleet.rb: -------------------------------------------------------------------------------- 1 | require 'fleet/configuration' 2 | require 'fleet/client' 3 | 4 | module Fleet 5 | extend Configuration 6 | 7 | def self.new(options={}) 8 | Fleet::Client.new(options) 9 | end 10 | 11 | def self.configure 12 | yield self 13 | true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/fleet/client/machines.rb: -------------------------------------------------------------------------------- 1 | module Fleet 2 | class Client 3 | module Machines 4 | 5 | MACHINES_RESOURCE = 'machines' 6 | 7 | def list_machines 8 | get(machines_path, nil) 9 | end 10 | 11 | private 12 | 13 | def machines_path(*parts) 14 | resource_path(MACHINES_RESOURCE, *parts) 15 | end 16 | 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/fleet/client/state.rb: -------------------------------------------------------------------------------- 1 | module Fleet 2 | class Client 3 | module State 4 | 5 | STATE_RESOURCE = 'state' 6 | 7 | def list_states(options={}) 8 | get(state_path, options) 9 | end 10 | 11 | private 12 | 13 | def state_path(*parts) 14 | resource_path(STATE_RESOURCE, *parts) 15 | end 16 | 17 | end 18 | end 19 | end 20 | 21 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | override: 3 | - 'rvm-exec 1.9.3 bundle install' 4 | - 'rvm-exec 2.0.0 bundle install' 5 | - 'rvm-exec 2.1.5 bundle install' 6 | - 'rvm-exec 2.2.0 bundle install' 7 | 8 | test: 9 | override: 10 | - 'rvm-exec 1.9.3 bundle exec rake' 11 | - 'rvm-exec 2.0.0 bundle exec rake' 12 | - 'rvm-exec 2.1.5 bundle exec rake' 13 | - 'rvm-exec 2.2.0 bundle exec rake' 14 | -------------------------------------------------------------------------------- /spec/fleet/error_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Fleet::Error do 4 | 5 | let(:message) { 'some message' } 6 | let(:error_code) { 12345 } 7 | 8 | subject { Fleet::Error.new(message, error_code) } 9 | 10 | it { should respond_to(:message) } 11 | it { should respond_to(:error_code) } 12 | 13 | describe '#initialize' do 14 | 15 | it 'saves the passed-in message' do 16 | expect(subject.message).to eq message 17 | end 18 | 19 | it 'saves the passed-in error code' do 20 | expect(subject.error_code).to eq error_code 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/fleet/client/state_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Fleet::Client::State do 4 | 5 | subject { Fleet::Client.new } 6 | 7 | let(:response) { double(:response) } 8 | 9 | describe '#list_states' do 10 | 11 | before do 12 | allow(subject).to receive(:get).and_return(response) 13 | end 14 | 15 | it 'GETs the state resource' do 16 | expect(subject).to receive(:get) 17 | .with('fleet/v1/state', {}) 18 | .and_return(response) 19 | 20 | subject.list_states 21 | end 22 | 23 | it 'returns the state response' do 24 | expect(subject.list_states).to eql(response) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/fleet/client/machines_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Fleet::Client::Machines do 4 | 5 | subject { Fleet::Client.new } 6 | 7 | let(:response) { double(:response) } 8 | 9 | describe '#list_machines' do 10 | 11 | before do 12 | allow(subject).to receive(:get).and_return(response) 13 | end 14 | 15 | it 'GETs the Fleet machines key' do 16 | expect(subject).to receive(:get) 17 | .with('fleet/v1/machines', nil) 18 | .and_return(response) 19 | 20 | subject.list_machines 21 | end 22 | 23 | it 'returns the job response' do 24 | expect(subject.list_machines).to eql(response) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/fleet/client/unit.rb: -------------------------------------------------------------------------------- 1 | module Fleet 2 | class Client 3 | module Unit 4 | 5 | UNITS_RESOURCE = 'units' 6 | 7 | def list_units() 8 | get(units_path) 9 | end 10 | 11 | def get_unit(name) 12 | get(units_path(name)) 13 | end 14 | 15 | alias_method :get_unit_file, :get_unit 16 | 17 | def create_unit(name, unit) 18 | put(units_path(name), unit) 19 | end 20 | 21 | alias_method :update_unit, :create_unit 22 | 23 | def delete_unit(name) 24 | delete(units_path(name)) 25 | end 26 | 27 | private 28 | 29 | def units_path(*parts) 30 | resource_path(UNITS_RESOURCE, *parts) 31 | end 32 | 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/fleet/connection.rb: -------------------------------------------------------------------------------- 1 | require 'excon' 2 | 3 | module Fleet 4 | module Connection 5 | 6 | def connection 7 | options = { 8 | read_timeout: read_timeout, 9 | connect_timeout: open_timeout, 10 | headers: { 'User-Agent' => user_agent, 'Accept' => 'application/json' } 11 | } 12 | 13 | uri = URI.parse(fleet_api_url) 14 | if uri.scheme == 'unix' 15 | uri, options = 'unix:///', { socket: uri.path }.merge(options) 16 | else 17 | uri = fleet_api_url 18 | end 19 | 20 | Excon.new(uri, options) 21 | end 22 | 23 | private 24 | 25 | def user_agent 26 | ua_chunks = [] 27 | ua_chunks << "fleet/#{Fleet::VERSION}" 28 | ua_chunks << "(#{RUBY_ENGINE}; #{RUBY_VERSION}p#{RUBY_PATCHLEVEL}; #{RUBY_PLATFORM})" 29 | ua_chunks << "excon/#{Excon::VERSION}" 30 | ua_chunks.join(' ') 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/fleet/service_definition.rb: -------------------------------------------------------------------------------- 1 | module Fleet 2 | class ServiceDefinition 3 | 4 | def initialize(service_def={}) 5 | @service_def = service_def 6 | end 7 | 8 | def to_unit(name) 9 | { 10 | 'name' => name, 11 | 'options' => options 12 | } 13 | end 14 | 15 | private 16 | 17 | def options 18 | @service_def.each_with_object([]) do |(section, options), h| 19 | options.each do |name, value| 20 | if value.is_a?(Enumerable) 21 | value.each do |v| 22 | h << { 23 | 'section' => section, 24 | 'name' => name, 25 | 'value' => v 26 | } 27 | end 28 | else 29 | h << { 30 | 'section' => section, 31 | 'name' => name, 32 | 'value' => value 33 | } 34 | end 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | fleet-api (1.1.0) 5 | excon (>= 0.27.4) 6 | 7 | GEM 8 | remote: http://rubygems.org/ 9 | specs: 10 | diff-lcs (1.2.5) 11 | docile (1.1.5) 12 | excon (0.44.2) 13 | multi_json (1.10.1) 14 | rake (10.3.2) 15 | rspec (3.0.0) 16 | rspec-core (~> 3.0.0) 17 | rspec-expectations (~> 3.0.0) 18 | rspec-mocks (~> 3.0.0) 19 | rspec-core (3.0.4) 20 | rspec-support (~> 3.0.0) 21 | rspec-expectations (3.0.4) 22 | diff-lcs (>= 1.2.0, < 2.0) 23 | rspec-support (~> 3.0.0) 24 | rspec-mocks (3.0.4) 25 | rspec-support (~> 3.0.0) 26 | rspec-support (3.0.4) 27 | simplecov (0.9.0) 28 | docile (~> 1.1.0) 29 | multi_json 30 | simplecov-html (~> 0.8.0) 31 | simplecov-html (0.8.0) 32 | simplecov-rcov (0.2.3) 33 | simplecov (>= 0.4.1) 34 | 35 | PLATFORMS 36 | ruby 37 | 38 | DEPENDENCIES 39 | fleet-api! 40 | rake 41 | rspec (~> 3.0) 42 | simplecov (~> 0.9.0) 43 | simplecov-rcov (~> 0.2.3) 44 | -------------------------------------------------------------------------------- /lib/fleet/configuration.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | module Fleet 4 | module Configuration 5 | 6 | VALID_OPTIONS_KEYS = [ 7 | :fleet_api_url, 8 | :fleet_api_version, 9 | :open_timeout, 10 | :read_timeout, 11 | :logger 12 | ] 13 | 14 | DEFAULT_FLEET_API_URL = ENV['FLEETCTL_ENDPOINT'] || 'unix:///var/run/fleet.sock' 15 | DEFAULT_FLEET_API_VERSION = 'v1' 16 | DEFAULT_OPEN_TIMEOUT = 2 17 | DEFAULT_READ_TIMEOUT = 5 18 | DEFAULT_LOGGER = ::Logger.new(STDOUT) 19 | 20 | attr_accessor(*VALID_OPTIONS_KEYS) 21 | 22 | def self.extended(base) 23 | base.reset 24 | end 25 | 26 | # Return a has of all the current config options 27 | def options 28 | VALID_OPTIONS_KEYS.each_with_object({}) { |k, o| o[k] = send(k) } 29 | end 30 | 31 | def reset 32 | self.fleet_api_url = DEFAULT_FLEET_API_URL 33 | self.fleet_api_version = DEFAULT_FLEET_API_VERSION 34 | self.open_timeout = DEFAULT_OPEN_TIMEOUT 35 | self.read_timeout = DEFAULT_READ_TIMEOUT 36 | self.logger = DEFAULT_LOGGER 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /fleet-api.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path('../lib/fleet/version', __FILE__) 2 | 3 | Gem::Specification.new do |gem| 4 | gem.authors = %w(CenturyLink) 5 | gem.email = %w(clt-labs-futuretech@centurylink.com) 6 | gem.description = 'A simple REST client for the CoreOS Fleet API' 7 | gem.summary = 'A simple REST client for the CoreOS Fleet API' 8 | gem.homepage = 'https://github.com/centurylinklabs/fleet-api' 9 | gem.license = 'Apache 2' 10 | gem.platform = Gem::Platform::RUBY 11 | gem.files = `git ls-files`.split($\) 12 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 13 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 14 | gem.name = 'fleet-api' 15 | gem.require_paths = %w(lib) 16 | gem.version = Fleet::VERSION 17 | gem.required_ruby_version = '>= 1.9.3' 18 | gem.add_dependency 'excon', '>= 0.27.4' 19 | gem.add_development_dependency 'rake' 20 | gem.add_development_dependency 'rspec', '~> 3.0' 21 | gem.add_development_dependency 'simplecov', '~> 0.9.0' 22 | gem.add_development_dependency 'simplecov-rcov', '~> 0.2.3' 23 | end 24 | -------------------------------------------------------------------------------- /spec/fleet/service_definition_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Fleet::ServiceDefinition do 4 | 5 | let(:name) { 'foo.service' } 6 | 7 | let(:service_hash) do 8 | { 9 | 'Unit' => { 10 | 'Description' => 'infinite loop' 11 | }, 12 | 'Service' => { 13 | 'ExecStartPre' => ['foo', 'bar'], 14 | 'ExecStart' => "/bin/bash -c \"while true; do sleep 1; done\"" 15 | } 16 | } 17 | end 18 | 19 | describe '#to_unit' do 20 | 21 | subject { described_class.new(service_hash) } 22 | 23 | it 'provides a fleet formatted unit definition' do 24 | 25 | expected = { 26 | "name" => name, 27 | "options"=> [ 28 | { "section" => "Unit", "name" => "Description", "value" => "infinite loop"}, 29 | { "section" => "Service", "name" => "ExecStartPre", "value" => "foo" }, 30 | { "section" => "Service", "name" => "ExecStartPre", "value" => "bar" }, 31 | { "section" => "Service", "name" => "ExecStart", "value" => "/bin/bash -c \"while true; do sleep 1; done\"" } 32 | ] 33 | } 34 | 35 | expect(subject.to_unit(name)).to eq expected 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/fleet/error.rb: -------------------------------------------------------------------------------- 1 | module Fleet 2 | 3 | class Error < StandardError 4 | attr_reader :error_code 5 | attr_reader :cause 6 | 7 | def initialize(msg, error_code=nil) 8 | super(msg) 9 | @error_code = error_code 10 | end 11 | 12 | HTTP_CODE_MAP = { 13 | 400 => 'BadRequest', 14 | 401 => 'Unauthorized', 15 | 403 => 'Forbidden', 16 | 404 => 'NotFound', 17 | 405 => 'MethodNotAllowed', 18 | 406 => 'NotAcceptable', 19 | 408 => 'RequestTimeout', 20 | 409 => 'Conflict', 21 | 412 => 'PreconditionFailed', 22 | 413 => 'RequestEntityTooLarge', 23 | 414 => 'RequestUriTooLong', 24 | 415 => 'UnsupportedMediaType', 25 | 416 => 'RequestRangeNotSatisfiable', 26 | 417 => 'ExpectationFailed', 27 | 500 => 'InternalServerError', 28 | 501 => 'NotImplemented', 29 | 502 => 'BadGateway', 30 | 503 => 'ServiceUnavailable', 31 | 504 => 'GatewayTimeout' 32 | } 33 | end 34 | 35 | # Define a new error class for all of the HTTP codes in the HTTP_CODE_MAP 36 | Error::HTTP_CODE_MAP.each do |code, class_name| 37 | Fleet.const_set(class_name, Class.new(Error)).const_set('HTTP_CODE', code) 38 | end 39 | 40 | class ConnectionError < Error; end 41 | end 42 | -------------------------------------------------------------------------------- /spec/fleet/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fleet/configuration' 3 | 4 | describe Fleet::Configuration do 5 | 6 | subject { Class.new { extend Fleet::Configuration } } 7 | 8 | describe 'exposed attribes' do 9 | Fleet::Configuration::VALID_OPTIONS_KEYS.each do |key| 10 | it { should respond_to key.to_sym } 11 | end 12 | end 13 | 14 | describe 'default values' do 15 | 16 | describe 'fleet_api_url' do 17 | it 'is matches DEFAULT_FLEET_API_URL' do 18 | expect(subject.fleet_api_url).to eq Fleet::Configuration::DEFAULT_FLEET_API_URL 19 | end 20 | end 21 | 22 | describe 'fleet_api_version' do 23 | it 'is matches DEFAULT_FLEET_API_VERSION' do 24 | expect(subject.fleet_api_version).to eq Fleet::Configuration::DEFAULT_FLEET_API_VERSION 25 | end 26 | end 27 | 28 | describe 'open_timeout' do 29 | it 'is matches DEFAULT_OPEN_TIMEOUT' do 30 | expect(subject.open_timeout).to eq Fleet::Configuration::DEFAULT_OPEN_TIMEOUT 31 | end 32 | end 33 | 34 | describe 'read_timeout' do 35 | it 'is matches DEFAULT_READ_TIMEOUT' do 36 | expect(subject.read_timeout).to eq Fleet::Configuration::DEFAULT_READ_TIMEOUT 37 | end 38 | end 39 | 40 | describe 'logger' do 41 | it 'is matches DEFAULT_LOGGER' do 42 | expect(subject.logger).to eq Fleet::Configuration::DEFAULT_LOGGER 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | 1.2.0 - 2015-08-13 5 | ------------------ 6 | 7 | ### Added 8 | - Support for paginated responses (robholland) 9 | 10 | 1.1.0 - 2015-02-19 11 | ------------------ 12 | 13 | ### Added 14 | - New submit method for submitting units without loading them 15 | 16 | ### Fixed 17 | - Error where units are not loaded when submitted via the load method 18 | - Out-dated Gemfile.lock 19 | 20 | 1.0.0 - 2015-02-17 21 | ------------------ 22 | 23 | ### Added 24 | - Support for official Fleet API (no longer reading/writing directly from/to etcd) 25 | - New get_unit_state and get_unit_file methods 26 | 27 | ### Fixed 28 | - Proper encoding of @ symbol when submitting unit templates 29 | 30 | 0.9.0 - 2015-01-14 31 | ------------------ 32 | 33 | ### Added 34 | - Support for listing all loaded units 35 | 36 | 0.8.0 - 2014-11-07 37 | ------------------ 38 | 39 | ### Added 40 | - Support for mutl-value options in unit file 41 | - Enforcement of Fleet service naming conventions 42 | 43 | 0.6.1 - 2014-09-20 44 | ------------------ 45 | 46 | ### Fixed 47 | - Default to async operations to address performance issues 48 | 49 | 0.6.0 - 2014-09-05 50 | ------------------ 51 | 52 | ### Added 53 | - Compatibility for Fleet 0.6.x (not backward compatible with older versions of Fleet) 54 | 55 | 0.5.3 - 2014-08-26 56 | ------------------ 57 | 58 | ### Added 59 | - Support for listing machines in CoreOS cluster 60 | 61 | ### Fixed 62 | - Will follow etcd redirects when communicating with non-master endpoints 63 | 64 | 0.5.2 - 2014-08-20 65 | ------------------ 66 | 67 | Initial release 68 | -------------------------------------------------------------------------------- /spec/fleet_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Fleet do 4 | 5 | after do 6 | Fleet.reset 7 | end 8 | 9 | describe '.new' do 10 | 11 | it 'returns a Fleet::Client' do 12 | expect(Fleet.new).to be_a Fleet::Client 13 | end 14 | 15 | context 'when no options specified' do 16 | 17 | Fleet::Configuration::VALID_OPTIONS_KEYS.each do |option| 18 | 19 | it "new Fleet::Client inherits :#{option} default from Fleet" do 20 | expect(Fleet.new.send(option)).to eq Fleet.send(option) 21 | end 22 | end 23 | end 24 | 25 | context 'when options are specified' do 26 | 27 | Fleet::Configuration::VALID_OPTIONS_KEYS.each do |option| 28 | 29 | it "new Fleet::Client receives specified :#{option} value" do 30 | expect(Fleet.new({option => 'foo'}).send(option)).to eq 'foo' 31 | end 32 | end 33 | end 34 | end 35 | 36 | describe '.fleet_api_url' do 37 | 38 | let(:url) { 'http://foo.com/bar' } 39 | 40 | before do 41 | stub_const('Fleet::Configuration::DEFAULT_FLEET_API_URL', url) 42 | Fleet.reset 43 | end 44 | 45 | it 'defaults to the value of DEFAULT_FLEET_API_URL' do 46 | expect(Fleet.fleet_api_url).to eq url 47 | end 48 | end 49 | 50 | describe '.fleet_api_version' do 51 | it 'defaults to v1' do 52 | expect(Fleet.fleet_api_version).to eq 'v1' 53 | end 54 | end 55 | 56 | describe '.open_timeout' do 57 | it 'defaults to 2' do 58 | expect(Fleet.open_timeout).to eq 2 59 | end 60 | end 61 | 62 | describe '.read_timeout' do 63 | it 'defaults to 5' do 64 | expect(Fleet.read_timeout).to eq 5 65 | end 66 | end 67 | 68 | describe '.configure' do 69 | it "accepts a block" do 70 | expect { Fleet.configure {} }.to_not raise_error 71 | end 72 | 73 | it "yields self" do 74 | Fleet.configure { |conf| expect(conf).to be(Fleet) } 75 | end 76 | 77 | it "returns true" do 78 | expect(Fleet.configure {}).to eq true 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/fleet/request.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'fleet/version' 3 | 4 | module Fleet 5 | module Request 6 | 7 | private 8 | 9 | [:get, :put, :delete].each do |method| 10 | define_method(method) do |path, options={}| 11 | request(connection, method, path, options) 12 | end 13 | end 14 | 15 | def request(connection, method, path, options) 16 | response = perform_request(connection, method, path, options) 17 | return response if method != :get 18 | 19 | next_page_token = response.delete('nextPageToken') 20 | while next_page_token 21 | next_options = options.merge('nextPageToken' => next_page_token) 22 | next_response = perform_request(connection, method, path, next_options) 23 | next_page_token = next_response.delete('nextPageToken') 24 | next_response.each { |k, v| response[k] += v } 25 | end 26 | response 27 | end 28 | 29 | private 30 | 31 | def perform_request(connection, method, path, options) 32 | req = { 33 | path: escape_path(path), 34 | } 35 | 36 | case method 37 | when :get 38 | req[:query] = options 39 | when :put 40 | req[:headers] = { 'Content-Type' => 'application/json' } 41 | req[:body] = ::JSON.dump(options) 42 | end 43 | 44 | resp = connection.send(method, req) 45 | 46 | if (400..600).include?(resp.status) 47 | raise_error(resp) 48 | end 49 | 50 | case method 51 | when :get 52 | ::JSON.parse(resp.body) 53 | else 54 | true 55 | end 56 | rescue Excon::Errors::SocketError => ex 57 | raise Fleet::ConnectionError, ex.message 58 | end 59 | 60 | def escape_path(path) 61 | URI.escape(path).gsub(/@/, '%40') 62 | end 63 | 64 | def raise_error(resp) 65 | error = JSON.parse(resp.body)['error'] 66 | class_name = Fleet::Error::HTTP_CODE_MAP.fetch(resp.status, 'Error') 67 | 68 | fail Fleet.const_get(class_name).new( 69 | error['message'], 70 | error['code']) 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/fleet/connection_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Fleet::Connection do 4 | 5 | describe 'connection options' do 6 | 7 | let(:open_timeout) { 30 } 8 | let(:read_timeout) { 40 } 9 | 10 | subject do 11 | Fleet::Client.new( 12 | open_timeout: open_timeout, 13 | read_timeout: read_timeout).connection 14 | end 15 | 16 | describe 'open_timeout' do 17 | it 'matches the the specified timeout value' do 18 | expect(subject.data[:connect_timeout]).to eq open_timeout 19 | end 20 | end 21 | 22 | describe 'read_timeout' do 23 | it 'matches the the specified timeout value' do 24 | expect(subject.data[:read_timeout]).to eq read_timeout 25 | end 26 | end 27 | 28 | context 'when URL is HTTP' do 29 | let(:url) { 'http://foo.com/bar' } 30 | 31 | subject do 32 | Fleet::Client.new(fleet_api_url: url).connection 33 | end 34 | describe 'scheme' do 35 | it 'matches the scheme of the URL' do 36 | expect(subject.data[:scheme]).to eq 'http' 37 | end 38 | end 39 | 40 | describe 'host' do 41 | it 'matches the host of the URL' do 42 | expect(subject.data[:host]).to eq 'foo.com' 43 | end 44 | end 45 | 46 | describe 'port' do 47 | it 'matches the port of the URL' do 48 | expect(subject.data[:port]).to eq 80 49 | end 50 | end 51 | 52 | describe 'prefix' do 53 | it 'matches the path of the URL' do 54 | expect(subject.data[:path]).to eq '/bar' 55 | end 56 | end 57 | end 58 | 59 | context 'when URL is UNIX' do 60 | let(:url) { 'unix:///foo/bar.socket' } 61 | 62 | subject do 63 | Fleet::Client.new(fleet_api_url: url).connection 64 | end 65 | 66 | describe 'scheme' do 67 | it 'matches the scheme of the URL' do 68 | expect(subject.data[:scheme]).to eq 'unix' 69 | end 70 | end 71 | 72 | describe 'socket' do 73 | it 'matches the port of the URL' do 74 | expect(subject.data[:socket]).to eq '/foo/bar.socket' 75 | end 76 | end 77 | end 78 | 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/fleet/client/unit_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Fleet::Client::Unit do 4 | 5 | subject { Fleet::Client.new } 6 | 7 | let(:response) { double(:response) } 8 | 9 | describe '#list_units' do 10 | 11 | before do 12 | allow(subject).to receive(:get).and_return(response) 13 | end 14 | 15 | it 'GETs all Fleet units' do 16 | expect(subject).to receive(:get) 17 | .with("fleet/v1/units") 18 | .and_return(response) 19 | 20 | subject.list_units 21 | end 22 | 23 | it 'returns the unit response' do 24 | expect(subject.list_units).to eql(response) 25 | end 26 | end 27 | 28 | describe '#get_unit' do 29 | 30 | let(:name) { 'foo.service' } 31 | 32 | before do 33 | allow(subject).to receive(:get).and_return(response) 34 | end 35 | 36 | it 'GETs the Fleet unit' do 37 | expect(subject).to receive(:get) 38 | .with("fleet/v1/units/#{name}") 39 | .and_return(response) 40 | 41 | subject.get_unit(name) 42 | end 43 | 44 | it 'returns the unit response' do 45 | expect(subject.get_unit(name)).to eql(response) 46 | end 47 | end 48 | 49 | describe '#create_unit' do 50 | 51 | let(:name) { 'foo.service' } 52 | let(:options) { { exec_start: '/bin/bash' } } 53 | 54 | before do 55 | allow(subject).to receive(:put).and_return(response) 56 | end 57 | 58 | it 'PUTs the unit def to the Fleet unit key' do 59 | expect(subject).to receive(:put) 60 | .with("fleet/v1/units/#{name}", options) 61 | .and_return(response) 62 | 63 | subject.create_unit(name, options) 64 | end 65 | 66 | it 'returns the unit response' do 67 | expect(subject.create_unit(name, options)).to eql(response) 68 | end 69 | end 70 | 71 | describe '#delete_unit' do 72 | 73 | let(:name) { 'foo.service' } 74 | 75 | before do 76 | allow(subject).to receive(:delete).and_return(response) 77 | end 78 | 79 | it 'DELETEs the named Fleet unit' do 80 | expect(subject).to receive(:delete) 81 | .with("fleet/v1/units/#{name}") 82 | .and_return(response) 83 | 84 | subject.delete_unit(name) 85 | end 86 | 87 | it 'returns the job response' do 88 | expect(subject.delete_unit(name)).to eql(response) 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/fleet/client.rb: -------------------------------------------------------------------------------- 1 | require 'fleet/connection' 2 | require 'fleet/error' 3 | require 'fleet/request' 4 | require 'fleet/service_definition' 5 | require 'fleet/client/machines' 6 | require 'fleet/client/unit' 7 | require 'fleet/client/state' 8 | 9 | module Fleet 10 | class Client 11 | 12 | attr_accessor(*Configuration::VALID_OPTIONS_KEYS) 13 | 14 | def initialize(options={}) 15 | options = Fleet.options.merge(options) 16 | Configuration::VALID_OPTIONS_KEYS.each do |key| 17 | send("#{key}=", options[key]) 18 | end 19 | end 20 | 21 | include Fleet::Connection 22 | include Fleet::Request 23 | 24 | include Fleet::Client::Machines 25 | include Fleet::Client::Unit 26 | include Fleet::Client::State 27 | 28 | def list 29 | machines = list_machines['machines'] || [] 30 | machine_ips = machines.each_with_object({}) do |machine, h| 31 | h[machine['id']] = machine['primaryIP'] 32 | end 33 | 34 | states = list_states['states'] || [] 35 | states.map do |service| 36 | { 37 | name: service['name'], 38 | load_state: service['systemdLoadState'], 39 | active_state: service['systemdActiveState'], 40 | sub_state: service['systemdSubState'], 41 | machine_id: service['machineID'], 42 | machine_ip: machine_ips[service['machineID']] 43 | } 44 | end 45 | end 46 | 47 | def submit(name, service_def) 48 | 49 | unless name =~ /\A[a-zA-Z0-9:_.@-]+\Z/ 50 | raise ArgumentError, 'name may only contain [a-zA-Z0-9:_.@-]' 51 | end 52 | 53 | unless service_def.is_a?(ServiceDefinition) 54 | service_def = ServiceDefinition.new(service_def) 55 | end 56 | 57 | begin 58 | create_unit(name, service_def.to_unit(name)) 59 | rescue Fleet::PreconditionFailed 60 | end 61 | end 62 | 63 | def load(name, service_def=nil) 64 | 65 | if service_def 66 | submit(name, service_def) 67 | end 68 | 69 | opts = { 'desiredState' => 'loaded', 'name' => name } 70 | update_unit(name, opts) 71 | end 72 | 73 | def start(name) 74 | opts = { 'desiredState' => 'launched', 'name' => name } 75 | update_unit(name, opts) 76 | end 77 | 78 | def stop(name) 79 | opts = { 'desiredState' => 'loaded', 'name' => name } 80 | update_unit(name, opts) 81 | end 82 | 83 | def unload(name) 84 | opts = { 'desiredState' => 'inactive', 'name' => name } 85 | update_unit(name, opts) 86 | end 87 | 88 | def destroy(name) 89 | delete_unit(name) 90 | end 91 | 92 | def status(name) 93 | get_unit(name)["currentState"].to_sym 94 | end 95 | 96 | def get_unit_state(name) 97 | options = { unitName: name } 98 | states = list_states(options) 99 | if states["states"] 100 | states["states"].first 101 | else 102 | fail NotFound, "Unit '#{name}' not found" 103 | end 104 | end 105 | 106 | protected 107 | 108 | def resource_path(resource, *parts) 109 | parts.unshift('fleet', fleet_api_version, resource).join('/') 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/fleet/request_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Fleet::Request do 4 | 5 | subject { Fleet::Client.new } 6 | 7 | let(:path) { '/foo bar@' } 8 | 9 | let(:response) do 10 | double(:response, body: '{"name":"foo"}', status: 200) 11 | end 12 | 13 | let(:connection) { double(:connection) } 14 | 15 | before do 16 | allow(connection).to receive(:send).and_return(response) 17 | allow(subject).to receive(:connection).and_return(connection) 18 | end 19 | 20 | describe '#get' do 21 | 22 | let(:options) do 23 | { foo: 'bar' } 24 | end 25 | 26 | it 'invokes #get on the connection with the correct params' do 27 | opts = { path: '/foo%20bar%40', query: options } 28 | expect(connection).to receive(:send).with(:get, opts) 29 | 30 | subject.send(:get, path, options) 31 | end 32 | 33 | it 'returns the parsed response body' do 34 | expect(subject.send(:get, path, options)).to eq('name' => 'foo') 35 | end 36 | 37 | context 'when there is pagination' do 38 | let(:first_response) do 39 | double(:first_response, body: '{"things":[{"name":"foo"}], "nextPageToken":"123"}', status: 200) 40 | end 41 | let(:second_response) do 42 | double(:second_response, body: '{"things":[{"name":"bah"}], "nextPageToken":"456"}', status: 200) 43 | end 44 | let(:third_response) do 45 | double(:second_response, body: '{"things":[{"name":"tah"}]}', status: 200) 46 | end 47 | 48 | it 'merges the responses' do 49 | expect(connection).to receive(:send).with(:get, anything).and_return(first_response) 50 | expect(connection).to receive(:send).with(:get, hash_including(query: { 'nextPageToken' => '123' })).and_return(second_response) 51 | expect(connection).to receive(:send).with(:get, hash_including(query: {'nextPageToken' => '456'})).and_return(third_response) 52 | 53 | expect(subject.send(:get, path)).to eql( 54 | 'things' => [{ 'name' => 'foo' }, { 'name' => 'bah' }, { 'name' => 'tah' }] 55 | ) 56 | end 57 | end 58 | 59 | context 'when there is a SocketError' do 60 | before do 61 | allow(connection).to receive(:send) 62 | .and_raise(Excon::Errors::SocketError, Excon::Errors::Error.new('oops')) 63 | end 64 | 65 | it 'raises a Fleet::ConnectionError' do 66 | expect { subject.send(:get, path, options) }.to raise_error(Fleet::ConnectionError) 67 | end 68 | end 69 | 70 | context 'when a non-200 status code is returned' do 71 | let(:response) do 72 | double(:response, body: '{"error": {"message": "oops", "code": "400"}}', status: 400) 73 | end 74 | 75 | it 'raises a Fleet::ConnectionError' do 76 | expect { subject.send(:get, path, options) }.to raise_error(Fleet::BadRequest, 'oops') 77 | end 78 | end 79 | end 80 | 81 | describe '#put' do 82 | 83 | let(:options) do 84 | { foo: 'bar' } 85 | end 86 | 87 | it 'invokes #put on the connection with the correct params' do 88 | opts = { 89 | path: '/foo%20bar%40', 90 | headers: { 'Content-Type' => 'application/json' }, 91 | body: JSON.dump(options) 92 | } 93 | expect(connection).to receive(:send).with(:put, opts) 94 | 95 | subject.send(:put, path, options) 96 | end 97 | 98 | it 'returns true' do 99 | expect(subject.send(:put, path, options)).to eq(true) 100 | end 101 | end 102 | 103 | describe '#delete' do 104 | 105 | it 'invokes #get on the connection with the correct params' do 106 | opts = { path: '/foo%20bar%40' } 107 | expect(connection).to receive(:send).with(:delete, opts) 108 | 109 | subject.send(:delete, path, nil) 110 | end 111 | 112 | it 'returns true' do 113 | expect(subject.send(:delete, path, nil)).to eq(true) 114 | end 115 | end 116 | 117 | end 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## NOTE 2 | 3 | This repo is no longer being maintained. Users are welcome to fork it, but we make no warranty of its functionality. 4 | 5 | fleet-api 6 | ========= 7 | 8 | [![Gem Version](https://badge.fury.io/rb/fleet-api.svg)](http://badge.fury.io/rb/fleet-api) 9 | [![Circle CI](https://circleci.com/gh/CenturyLinkLabs/fleet-api.svg?style=svg)](https://circleci.com/gh/CenturyLinkLabs/fleet-api) 10 | 11 | Provides a Ruby wrapper around the CoreOS Fleet API. 12 | 13 | The client allows programmatic access to most of the *fleetctl* commands including the ability to load, start, stop, unload and destroy unit files. 14 | 15 | **Important Note:** As of version 1.0.0, this gem is now using the official Fleet REST API. Previous versions of this gem communicated with Fleet by reading/writing directly from/to the etcd key-value store. While this approach was functional, it was extremely brittle due to the fact that we were essentially using a private API. Now that the Fleet API has hit version 1.0 and is presumably stable it makes more sense to leverage the official API. 16 | 17 | Users migrating from an older version of the Gem will simply need to make sure they configure it with the Fleet API endpoint instead of the etcd API endpoint. 18 | 19 | ### Installation 20 | 21 | Install the gem directly: 22 | 23 | gem install fleet-api 24 | 25 | Alternatively, add this line to your application's Gemfile: 26 | 27 | gem 'fleet-api', require: 'fleet' 28 | 29 | 30 | ### Usage 31 | 32 | Configure the URL for the Fleet API: 33 | 34 | require 'fleet' 35 | 36 | Fleet.configure do |fleet| 37 | fleet.fleet_api_url = 'http://10.1.42.1:49153' 38 | end 39 | 40 | If you don't provide an explicit value for the `.fleet_api_url` attribute, it will default to using the value of the `FLEETCTL_ENDPOINT` environment variable (if present) or the socket at `unix:///var/run/fleet.sock`. 41 | 42 | 43 | #### Service Definitions 44 | 45 | When submitting a service definition to the `Fleet::Client` you must convert your [unit file](http://www.freedesktop.org/software/systemd/man/systemd.unit.html) into a Ruby hash. Each section in the unit file is represented as a key/value pair in the hash where the key is the name of the section and the value is another hash containing all the statements for that section. 46 | 47 | For example, look at the following unit file. 48 | 49 | [Unit] 50 | Description=Useless infinite loop 51 | 52 | [Service] 53 | ExecStart=/bin/bash -c "while true; do sleep 1; done" 54 | 55 | This unit file would be represented as the following Ruby hash. 56 | 57 | { 58 | 'Unit' => { 59 | 'Description' => 'Useless infinite loop' 60 | }, 61 | 'Service' => { 62 | 'ExecStart' => "/bin/bash -c \"while true; do sleep 1; done\"" 63 | } 64 | } 65 | 66 | If you need mutiple values for a single statement (like multiple `ExecStart` instructions) you can use an array of strings: 67 | 68 | { 69 | 'Unit' => { 70 | 'Description' => 'Useless infinite loop' 71 | }, 72 | 'Service' => { 73 | 'ExecStart' => ["/bin/bash -c \"while true; do sleep 1; done\"", "some other command"] 74 | } 75 | } 76 | 77 | #### Submitting a Unit File 78 | 79 | Equivalent of `fleetctl submit`: 80 | 81 | service = { 82 | 'Unit' => { 83 | 'Description' => 'Useless infinite loop' 84 | }, 85 | 'Service' => { 86 | 'ExecStart' => "/bin/bash -c \"while true; do sleep 1; done\"" 87 | } 88 | } 89 | 90 | client = Fleet.new 91 | client.submit('forever.service', service) 92 | 93 | Note that the name you pass-in as the first parameter to the `.submit` method should end in ".service" 94 | 95 | #### Loading a Unit File 96 | 97 | Equivalent of `fleetctl load`: 98 | 99 | client = Fleet.new 100 | client.load('forever.service') 101 | 102 | #### Starting a Service 103 | 104 | Equivalent of `fleetctl start`: 105 | 106 | client = Fleet.new 107 | client.start('forever.service') 108 | 109 | #### Stopping a Service 110 | 111 | Equivalent of `fleetctl stop`: 112 | 113 | client = Fleet.new 114 | client.stop('forever.service') 115 | 116 | #### Unloading a Unit File 117 | 118 | Equivalent of `fleetctl unload`: 119 | 120 | client = Fleet.new 121 | client.unload('forever.service') 122 | 123 | #### Destroying a Service 124 | 125 | Equivalent of `fleetctl destroy`: 126 | 127 | client = Fleet.new 128 | client.destroy('forever.service') 129 | 130 | #### Listing Services 131 | 132 | Equivalent of `fleetctl list-units`: 133 | 134 | client = Fleet.new 135 | client.list 136 | 137 | #### Retrieving Service Status 138 | 139 | Equivalent of `fleetctl status`: 140 | 141 | client = Fleet.new 142 | client.get_unit_state('forever.service') 143 | 144 | Retrieves current status of a unit file: 145 | 146 | client = Fleet.new 147 | client.status('forever.service') 148 | 149 | #### Retrieving a Unit File 150 | 151 | Retrieves contents and current state of a unit file: 152 | 153 | client = Fleet.new 154 | client.get_unit_file('foo.service') 155 | 156 | -------------------------------------------------------------------------------- /spec/fleet/client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'fleet/service_definition' 4 | 5 | describe Fleet::Client do 6 | 7 | describe '#initialize' do 8 | 9 | after do 10 | Fleet.reset 11 | end 12 | 13 | Fleet::Configuration::VALID_OPTIONS_KEYS.each do |option| 14 | it "inherits default #{option} value from Panamax" do 15 | client = Fleet::Client.new 16 | expect(client.send(option)).to eql(Fleet.send(option)) 17 | end 18 | 19 | it "overrides default for #{option} when specified" do 20 | client = Fleet::Client.new(option => :foo) 21 | expect(client.send(option)).to eql(:foo) 22 | end 23 | end 24 | end 25 | 26 | describe '#list' do 27 | 28 | let(:machine_list) do 29 | { 30 | 'machines' => [ 31 | { 'id' => '123', 'primaryIP' => '1.1.1.1' } 32 | ] 33 | } 34 | end 35 | 36 | let(:state_list) do 37 | { 38 | 'states' => [ 39 | { 40 | 'hash' => 'abc123', 41 | 'machineID' => '123', 42 | 'name' => 'foo.service', 43 | 'systemdActiveState' => 'b', 44 | 'systemdLoadState' => 'a', 45 | 'systemdSubState' => 'c' 46 | } 47 | ] 48 | } 49 | end 50 | 51 | before do 52 | allow(subject).to receive(:list_machines).and_return(machine_list) 53 | allow(subject).to receive(:list_states).and_return(state_list) 54 | end 55 | 56 | it 'looks-up the list of machines' do 57 | expect(subject).to receive(:list_machines) 58 | subject.list 59 | end 60 | 61 | it 'looks-up the list of job states' do 62 | expect(subject).to receive(:list_states) 63 | subject.list 64 | end 65 | 66 | it 'returns the list of units' do 67 | expected = [{ 68 | name: 'foo.service', 69 | load_state: 'a', 70 | active_state: 'b', 71 | sub_state: 'c', 72 | machine_id: '123', 73 | machine_ip: '1.1.1.1' 74 | }] 75 | 76 | expect(subject.list).to eq(expected) 77 | end 78 | end 79 | 80 | describe '#submit' do 81 | let(:name) { 'foo.service' } 82 | let(:service_def) { { 'Unit' => { 'Description' => 'bar' } } } 83 | let(:sd) { Fleet::ServiceDefinition.new(service_def) } 84 | let(:response) { double(:response) } 85 | 86 | before do 87 | allow(subject).to receive(:create_unit).and_return(response) 88 | allow(Fleet::ServiceDefinition).to receive(:new).and_return(sd) 89 | end 90 | 91 | it 'invokes #create_unit' do 92 | expect(subject).to receive(:create_unit) 93 | .with(name, sd.to_unit(name)) 94 | 95 | subject.submit(name, service_def) 96 | end 97 | 98 | it 'returns the #create_unit response' do 99 | r = subject.submit(name, service_def) 100 | expect(r).to eq response 101 | end 102 | 103 | context 'when #create_unit raises PreconditionFailed' do 104 | 105 | before do 106 | allow(subject).to receive(:create_unit) 107 | .and_raise(Fleet::PreconditionFailed.new('boom')) 108 | end 109 | 110 | it 'does not blow up' do 111 | expect { subject.submit(name, service_def) }.to_not raise_error 112 | end 113 | end 114 | 115 | context 'when #create_unit raises something other than PreconditionFailed' do 116 | 117 | before do 118 | allow(subject).to receive(:create_unit) 119 | .and_raise(Fleet::BadRequest.new('boom')) 120 | end 121 | 122 | it 'propagates the error' do 123 | expect { subject.submit(name, service_def) }.to(raise_error(Fleet::BadRequest)) 124 | end 125 | end 126 | 127 | context 'when the supplied name is invalid' do 128 | 129 | let(:name) { 'foo!.service' } 130 | 131 | it 'raises an ArgumentError' do 132 | expect { subject.submit(name, nil) }.to raise_error(ArgumentError, /only contain/) 133 | end 134 | end 135 | end 136 | 137 | describe '#load' do 138 | 139 | let(:name) { 'foo.service' } 140 | let(:response) { double(:response) } 141 | 142 | before do 143 | allow(subject).to receive(:update_unit).and_return(response) 144 | end 145 | 146 | it 'does NOT invoke #submit' do 147 | expect(subject).not_to receive(:submit) 148 | subject.load(name) 149 | end 150 | 151 | it 'invokes #update' do 152 | expect(subject).to receive(:update_unit) 153 | .with(name, { 'desiredState' => 'loaded', 'name' => name }) 154 | 155 | subject.load(name) 156 | end 157 | 158 | context 'when a service definition is provided' do 159 | 160 | let(:service_def) { { 'Unit' => { 'Description' => 'bar' } } } 161 | 162 | before do 163 | allow(subject).to receive(:submit) 164 | end 165 | 166 | it 'invokes #load' do 167 | expect(subject).to receive(:submit) 168 | subject.load(name, service_def) 169 | end 170 | end 171 | 172 | end 173 | 174 | describe '#start' do 175 | let(:service_name) { 'foo.service' } 176 | 177 | before do 178 | allow(subject).to receive(:update_unit).and_return(nil) 179 | end 180 | 181 | it 'invokes #update_unit' do 182 | expect(subject).to receive(:update_unit) 183 | .with(service_name, { 'desiredState' => 'launched', 'name' => service_name }) 184 | 185 | subject.start(service_name) 186 | end 187 | end 188 | 189 | describe '#stop' do 190 | let(:service_name) { 'foo.service' } 191 | 192 | before do 193 | allow(subject).to receive(:update_unit).and_return(nil) 194 | end 195 | 196 | it 'invokes #update_unit' do 197 | expect(subject).to receive(:update_unit) 198 | .with(service_name, { 'desiredState' => 'loaded', 'name' => service_name }) 199 | 200 | subject.stop(service_name) 201 | end 202 | end 203 | 204 | describe '#unload' do 205 | let(:service_name) { 'foo.service' } 206 | 207 | before do 208 | allow(subject).to receive(:update_unit).and_return(nil) 209 | end 210 | 211 | it 'invokes #update_unit' do 212 | expect(subject).to receive(:update_unit) 213 | .with(service_name, { 'desiredState' => 'inactive', 'name' => service_name }) 214 | 215 | subject.unload(service_name) 216 | end 217 | end 218 | 219 | describe '#destroy' do 220 | let(:service_name) { 'foo.service' } 221 | 222 | before do 223 | allow(subject).to receive(:delete_unit).and_return(nil) 224 | end 225 | 226 | it 'invokes #delete_job' do 227 | 228 | expect(subject).to receive(:delete_unit) 229 | .with(service_name) 230 | .and_return(nil) 231 | 232 | subject.destroy(service_name) 233 | end 234 | end 235 | 236 | describe '#status' do 237 | 238 | let(:service_name) { 'foo.service' } 239 | 240 | let(:fleet_state) do 241 | { 'currentState' => 'launched' } 242 | end 243 | 244 | before do 245 | allow(subject).to receive(:get_unit).and_return(fleet_state) 246 | end 247 | 248 | it 'retrieves service state from the fleet client' do 249 | expect(subject).to receive(:get_unit).with(service_name) 250 | subject.status(service_name) 251 | end 252 | 253 | it 'returns the symbolized state' do 254 | expect(subject.status(service_name)).to eq(:launched) 255 | end 256 | end 257 | 258 | describe '#get_unit_state' do 259 | 260 | let(:service_name) { 'foo.service' } 261 | 262 | let(:states) do 263 | { 'states' => [] } 264 | end 265 | 266 | before do 267 | allow(subject).to receive(:list_states).and_return(states) 268 | end 269 | 270 | it 'retrieves the states from the fleet API' do 271 | expect(subject).to receive(:list_states).with({ unitName: service_name }) 272 | subject.get_unit_state(service_name) 273 | end 274 | 275 | context 'when unit is found' do 276 | 277 | let(:states) do 278 | { 'states' => [{ 'name' => 'foo.service' }, {}] } 279 | end 280 | 281 | it 'returns the first matching state hash' do 282 | expect(subject.get_unit_state(service_name)).to eq(states['states'].first) 283 | end 284 | end 285 | 286 | context 'when unit is NOT found' do 287 | 288 | let(:states) { {} } 289 | 290 | it 'returns the first matching state hash' do 291 | expect { subject.get_unit_state(service_name) }.to( 292 | raise_error(Fleet::NotFound)) 293 | end 294 | end 295 | end 296 | end 297 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2014 CenturyLink 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------