fleet-ruby 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
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
@@ -0,0 +1,13 @@
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'
@@ -0,0 +1,23 @@
1
+ require File.expand_path('../lib/fleet/version', __FILE__)
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.authors = 'Barry Martin'
5
+ gem.email = 'nyxcharon@gmail.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/nyxcharon/fleet-ruby.git'
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-ruby'
15
+ gem.require_paths = %w(lib)
16
+ gem.version = '0.1.0'
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
@@ -0,0 +1,15 @@
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
@@ -0,0 +1,119 @@
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 list_unit_states
48
+ list_states['states'] || []
49
+ end
50
+
51
+ def list_fleet_machines
52
+ list_machines['machines'] || []
53
+ end
54
+
55
+ def submit(name, service_def)
56
+ unless name =~ /\A[a-zA-Z0-9:_.@-]+\Z/
57
+ raise ArgumentError, 'name may only contain [a-zA-Z0-9:_.@-]'
58
+ end
59
+
60
+ unless service_def.is_a?(ServiceDefinition)
61
+ service_def = ServiceDefinition.new(service_def)
62
+ end
63
+
64
+ begin
65
+ create_unit(name, service_def.to_unit(name))
66
+ rescue Fleet::PreconditionFailed
67
+ end
68
+ end
69
+
70
+ def load(name, service_def=nil)
71
+
72
+ if service_def
73
+ submit(name, service_def)
74
+ end
75
+
76
+ opts = { 'desiredState' => 'loaded', 'name' => name }
77
+ update_unit(name, opts)
78
+ end
79
+
80
+ def start(name)
81
+ opts = { 'desiredState' => 'launched', 'name' => name }
82
+ update_unit(name, opts)
83
+ end
84
+
85
+ def stop(name)
86
+ opts = { 'desiredState' => 'loaded', 'name' => name }
87
+ update_unit(name, opts)
88
+ end
89
+
90
+ def unload(name)
91
+ opts = { 'desiredState' => 'inactive', 'name' => name }
92
+ update_unit(name, opts)
93
+ end
94
+
95
+ def destroy(name)
96
+ delete_unit(name)
97
+ end
98
+
99
+ def status(name)
100
+ get_unit(name)["currentState"].to_sym
101
+ end
102
+
103
+ def get_unit_state(name)
104
+ options = { unitName: name }
105
+ states = list_states(options)
106
+ if states["states"]
107
+ states["states"].first
108
+ else
109
+ fail NotFound, "Unit '#{name}' not found"
110
+ end
111
+ end
112
+
113
+ protected
114
+
115
+ def resource_path(resource, *parts)
116
+ parts.unshift('fleet', fleet_api_version, resource).join('/')
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,18 @@
1
+ module Fleet
2
+ class Client
3
+ module Machines
4
+ MACHINES_RESOURCE = 'machines'.freeze
5
+
6
+ def list_machines
7
+ opts = { consistent: true, recursive: true, sorted: true }
8
+ get(machines_path, opts)
9
+ end
10
+
11
+ private
12
+
13
+ def machines_path(*parts)
14
+ resource_path(MACHINES_RESOURCE, *parts)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ module Fleet
2
+ class Client
3
+ module State
4
+ STATE_RESOURCE = 'state'.freeze
5
+
6
+ def list_states
7
+ opts = { consistent: true, recursive: true, sorted: true }
8
+ get(state_path, opts)
9
+ end
10
+
11
+ private
12
+
13
+ def state_path(*parts)
14
+ resource_path(STATE_RESOURCE, *parts)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,33 @@
1
+ module Fleet
2
+ class Client
3
+ module Unit
4
+ UNITS_RESOURCE = 'units'.freeze
5
+
6
+ def list_units
7
+ get(units_path)
8
+ end
9
+
10
+ def get_unit(name)
11
+ get(units_path(name))
12
+ end
13
+
14
+ alias_method :get_unit_file, :get_unit
15
+
16
+ def create_unit(name, unit)
17
+ put(units_path(name), unit)
18
+ end
19
+
20
+ alias_method :update_unit, :create_unit
21
+
22
+ def delete_unit(name)
23
+ delete(units_path(name))
24
+ end
25
+
26
+ private
27
+
28
+ def units_path(*parts)
29
+ resource_path(UNITS_RESOURCE, *parts)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,39 @@
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
@@ -0,0 +1,33 @@
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
@@ -0,0 +1,41 @@
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
@@ -0,0 +1,73 @@
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