fleet-api 0.0.1

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,56 @@
1
+ require 'fleet/version'
2
+
3
+ module Fleet
4
+ module Request
5
+
6
+ private
7
+
8
+ [:get, :head, :put, :post, :delete].each do |method|
9
+ define_method(method) do |path, options={}, headers={}|
10
+ request(connection, method, path, options, headers)
11
+ end
12
+ end
13
+
14
+ def request(connection, method, path, options, headers)
15
+ options ||= {}
16
+
17
+ response = connection.send(method) do |request|
18
+ request.options[:open_timeout] = open_timeout
19
+ request.options[:timeout] = read_timeout
20
+ request.headers = {
21
+ user_agent: user_agent,
22
+ accept: 'application/json'
23
+ }.merge(headers)
24
+
25
+ request.path = URI.escape(path)
26
+
27
+ case method
28
+ when :delete, :get, :head
29
+ request.params = options unless options.empty?
30
+ when :post, :put
31
+ if options.key?(:querystring)
32
+ request.params = options[:querystring]
33
+ request.body = options[:body]
34
+ else
35
+ request.body = options unless options.empty?
36
+ end
37
+ end
38
+ end
39
+
40
+ response.body
41
+ rescue Faraday::Error::ConnectionFailed => ex
42
+ raise Fleet::ConnectionError, ex.message
43
+ end
44
+
45
+ private
46
+
47
+ def user_agent
48
+ ua_chunks = []
49
+ ua_chunks << "fleet/#{Fleet::VERSION}"
50
+ ua_chunks << "(#{RUBY_ENGINE}; #{RUBY_VERSION}p#{RUBY_PATCHLEVEL}; #{RUBY_PLATFORM})"
51
+ ua_chunks << "faraday/#{Faraday::VERSION}"
52
+ ua_chunks << "(#{adapter})"
53
+ ua_chunks.join(' ')
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,63 @@
1
+ require 'digest/sha1'
2
+
3
+ module Fleet
4
+ class ServiceDefinition
5
+
6
+ attr_reader :name
7
+
8
+ def initialize(name, service_def={})
9
+ @name = name
10
+ @service_def = service_def
11
+ end
12
+
13
+ def to_unit
14
+ {
15
+ 'Contents' => unit_body,
16
+ 'Raw' => raw
17
+ }
18
+ end
19
+
20
+ def to_job
21
+ {
22
+ 'Name' => name,
23
+ 'UnitHash' => sha1_byte_array
24
+ }
25
+ end
26
+
27
+ def sha1
28
+ Digest::SHA1.hexdigest raw
29
+ end
30
+
31
+ private
32
+
33
+ def unit_body
34
+ @service_def.each_with_object({}) do |(heading, section), memo|
35
+ memo[heading] = section.each_with_object({}) do |(key, value), memo|
36
+ memo[key] = [value]
37
+ end
38
+ end
39
+ end
40
+
41
+ def raw
42
+ raw_string = ''
43
+
44
+ @service_def.each do |heading, section|
45
+ raw_string += "[#{heading}]\n"
46
+
47
+ if section.is_a?(Enumerable)
48
+ section.each do |key, value|
49
+ raw_string += "#{key}=#{value}\n"
50
+ end
51
+ end
52
+
53
+ raw_string += "\n"
54
+ end
55
+
56
+ raw_string.chomp
57
+ end
58
+
59
+ def sha1_byte_array
60
+ Digest::SHA1.digest(raw).unpack('C20')
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,3 @@
1
+ module Fleet
2
+ VERSION = '0.0.1'.freeze unless defined?(Fleet::VERSION)
3
+ end
data/lib/fleet.rb ADDED
@@ -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,37 @@
1
+ require 'faraday'
2
+ require 'json'
3
+ require 'fleet/error'
4
+
5
+ module Middleware
6
+ module Response
7
+
8
+ class RaiseError < Faraday::Response::Middleware
9
+
10
+ def on_complete(env)
11
+ status = env[:status].to_i
12
+ return unless (400..600).include?(status)
13
+
14
+ error = parse_error(env[:body])
15
+
16
+ # Find the error class that matches the HTTP status code. Default to
17
+ # Error if no matching class exists.
18
+ class_name = Fleet::Error::HTTP_CODE_MAP.fetch(status, 'Error')
19
+
20
+ fail Fleet.const_get(class_name).new(
21
+ error['message'],
22
+ error['errorCode'],
23
+ error['cause'])
24
+ end
25
+
26
+ private
27
+
28
+ def parse_error(body)
29
+ JSON.parse(body)
30
+ rescue StandardError
31
+ { 'message' => body }
32
+ end
33
+ end
34
+
35
+ Faraday.register_middleware :response, raise_error: -> { RaiseError }
36
+ end
37
+ end
@@ -0,0 +1,123 @@
1
+ require 'spec_helper'
2
+
3
+ describe Fleet::Client::Job do
4
+
5
+ subject { Fleet::Client.new }
6
+
7
+ let(:response) { double(:response) }
8
+
9
+ describe '#list_jobs' do
10
+
11
+ before do
12
+ allow(subject).to receive(:get).and_return(response)
13
+ end
14
+
15
+ it 'GETs the Fleet job key' do
16
+ opts = { consistent: true, recursive: true, sorted: true }
17
+ expect(subject).to receive(:get)
18
+ .with('v2/keys/_coreos.com/fleet/job', opts)
19
+ .and_return(response)
20
+
21
+ subject.list_jobs
22
+ end
23
+
24
+ it 'returns the job response' do
25
+ expect(subject.list_jobs).to eql(response)
26
+ end
27
+ end
28
+
29
+ describe '#get_job' do
30
+
31
+ let(:service_name) { 'foo.service' }
32
+
33
+ before do
34
+ allow(subject).to receive(:get).and_return(response)
35
+ end
36
+
37
+ it 'GETs the named Fleet job key' do
38
+ opts = { consistent: true, recursive: true, sorted: false }
39
+ expect(subject).to receive(:get)
40
+ .with("v2/keys/_coreos.com/fleet/job/#{service_name}/object", opts)
41
+ .and_return(response)
42
+
43
+ subject.get_job(service_name)
44
+ end
45
+
46
+ it 'returns the job response' do
47
+ expect(subject.get_job(service_name)).to eql(response)
48
+ end
49
+ end
50
+
51
+ describe '#create_job' do
52
+
53
+ let(:service_name) { 'foo.service' }
54
+ let(:service_def) { { name: service_name } }
55
+
56
+ before do
57
+ allow(subject).to receive(:put).and_return(response)
58
+ end
59
+
60
+ it 'PUTs the service def to the Fleet job key' do
61
+ opts = {
62
+ querystring: { 'prevExist' => false },
63
+ body: { value: service_def.to_json }
64
+ }
65
+
66
+ expect(subject).to receive(:put)
67
+ .with("v2/keys/_coreos.com/fleet/job/#{service_name}/object", opts)
68
+ .and_return(response)
69
+
70
+ subject.create_job(service_name, service_def)
71
+ end
72
+
73
+ it 'returns the job response' do
74
+ expect(subject.create_job(service_name, service_def)).to eql(response)
75
+ end
76
+ end
77
+
78
+ describe '#delete_job' do
79
+
80
+ let(:service_name) { 'foo.service' }
81
+
82
+ before do
83
+ allow(subject).to receive(:delete).and_return(response)
84
+ end
85
+
86
+ it 'DELETEs the named Fleet job key' do
87
+ opts = { dir: false, recursive: true }
88
+ expect(subject).to receive(:delete)
89
+ .with("v2/keys/_coreos.com/fleet/job/#{service_name}", opts)
90
+ .and_return(response)
91
+
92
+ subject.delete_job(service_name)
93
+ end
94
+
95
+ it 'returns the job response' do
96
+ expect(subject.delete_job(service_name)).to eql(response)
97
+ end
98
+ end
99
+
100
+ describe '#update_job_target_state' do
101
+
102
+ let(:service_name) { 'foo.service' }
103
+ let(:state) { :foobared }
104
+
105
+ before do
106
+ allow(subject).to receive(:put).and_return(response)
107
+ end
108
+
109
+ it 'PUTs the state to the Fleet job state key' do
110
+ opts = { value: state }
111
+
112
+ expect(subject).to receive(:put)
113
+ .with("v2/keys/_coreos.com/fleet/job/#{service_name}/target-state", opts)
114
+ .and_return(response)
115
+
116
+ subject.update_job_target_state(service_name, state)
117
+ end
118
+
119
+ it 'returns the job response' do
120
+ expect(subject.update_job_target_state(service_name, state)).to eql(response)
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,50 @@
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 Fleet state key' do
16
+ opts = { consistent: true, recursive: true, sorted: false }
17
+ expect(subject).to receive(:get)
18
+ .with('v2/keys/_coreos.com/fleet/state', opts)
19
+ .and_return(response)
20
+
21
+ subject.list_states
22
+ end
23
+
24
+ it 'returns the state response' do
25
+ expect(subject.list_states).to eql(response)
26
+ end
27
+ end
28
+
29
+ describe '#get_state' do
30
+
31
+ let(:service_name) { 'foo.service' }
32
+
33
+ before do
34
+ allow(subject).to receive(:get).and_return(response)
35
+ end
36
+
37
+ it 'GETs the named Fleet state key' do
38
+ opts = { consistent: true, recursive: true, sorted: false }
39
+ expect(subject).to receive(:get)
40
+ .with("v2/keys/_coreos.com/fleet/state/#{service_name}", opts)
41
+ .and_return(response)
42
+
43
+ subject.get_state(service_name)
44
+ end
45
+
46
+ it 'returns the state response' do
47
+ expect(subject.get_state(service_name)).to eql(response)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,76 @@
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 the Fleet unit key' do
16
+ expect(subject).to receive(:get)
17
+ .with("v2/keys/_coreos.com/fleet/unit")
18
+ .and_return(response)
19
+
20
+ subject.list_units
21
+ end
22
+
23
+ it 'returns the job response' do
24
+ expect(subject.list_units).to eql(response)
25
+ end
26
+ end
27
+
28
+ describe '#create_unit' do
29
+
30
+ let(:sha) { '33ef9ba9029c' }
31
+ let(:unit_def) { { exec_start: '/bin/bash' } }
32
+
33
+ before do
34
+ allow(subject).to receive(:put).and_return(response)
35
+ end
36
+
37
+ it 'PUTs the unit def to the Fleet unit key' do
38
+ opts = {
39
+ querystring: { 'prevExist' => false },
40
+ body: { value: unit_def.to_json }
41
+ }
42
+
43
+ expect(subject).to receive(:put)
44
+ .with("v2/keys/_coreos.com/fleet/unit/#{sha}", opts)
45
+ .and_return(response)
46
+
47
+ subject.create_unit(sha, unit_def)
48
+ end
49
+
50
+ it 'returns the job response' do
51
+ expect(subject.create_unit(sha, unit_def)).to eql(response)
52
+ end
53
+ end
54
+
55
+ describe '#delete_unit' do
56
+
57
+ let(:sha) { '33ef9ba9029c' }
58
+
59
+ before do
60
+ allow(subject).to receive(:delete).and_return(response)
61
+ end
62
+
63
+ it 'DELETEs the named Fleet unit key' do
64
+ opts = { dir: false, recursive: false }
65
+ expect(subject).to receive(:delete)
66
+ .with("v2/keys/_coreos.com/fleet/unit/#{sha}", opts)
67
+ .and_return(response)
68
+
69
+ subject.delete_unit(sha)
70
+ end
71
+
72
+ it 'returns the job response' do
73
+ expect(subject.delete_unit(sha)).to eql(response)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,247 @@
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 '#load' do
27
+
28
+ let(:name) { 'foo.service' }
29
+ let(:service_def) { { 'Unit' => { 'Description' => 'bar' } } }
30
+ let(:sd) { Fleet::ServiceDefinition.new(name, service_def) }
31
+ let(:fleet_state) do
32
+ { 'node' => { 'value' => '{ "loadState": "loaded" }' } }
33
+ end
34
+
35
+ before do
36
+ allow(subject).to receive(:create_unit).and_return(nil)
37
+ allow(subject).to receive(:create_job).and_return(nil)
38
+ allow(subject).to receive(:update_job_target_state).and_return(nil)
39
+ allow(subject).to receive(:get_state).and_return(fleet_state)
40
+ allow(Fleet::ServiceDefinition).to receive(:new).and_return(sd)
41
+ end
42
+
43
+ it 'invokes #create_unit' do
44
+ expect(subject).to receive(:create_unit)
45
+ .with(sd.sha1, sd.to_unit)
46
+
47
+ subject.load(name, service_def)
48
+ end
49
+
50
+ it 'invokes #create_job' do
51
+ expect(subject).to receive(:create_job)
52
+ .with(sd.name, sd.to_job)
53
+
54
+ subject.load(name, service_def)
55
+ end
56
+
57
+ it 'invokes #update_job_target_state' do
58
+ expect(subject).to receive(:update_job_target_state)
59
+ .with(sd.name, :loaded)
60
+
61
+ subject.load(name, service_def)
62
+ end
63
+
64
+ it 'checks the job state' do
65
+ expect(subject).to receive(:get_state).with(sd.name)
66
+ subject.load(name, service_def)
67
+ end
68
+
69
+ context 'when #create_unit raises PreconditionFailed' do
70
+
71
+ before do
72
+ allow(subject).to receive(:create_unit)
73
+ .and_raise(Fleet::PreconditionFailed.new('boom'))
74
+ end
75
+
76
+ it 'does not blow up' do
77
+ expect { subject.load(name, service_def) }.to_not raise_error
78
+ end
79
+ end
80
+
81
+ context 'when #create_unit raises something other than PreconditionFailed' do
82
+
83
+ before do
84
+ allow(subject).to receive(:create_unit)
85
+ .and_raise(Fleet::BadRequest.new('boom'))
86
+ end
87
+
88
+ it 'propagates the error' do
89
+ expect { subject.load(name, service_def) }.to(raise_error(Fleet::BadRequest))
90
+ end
91
+ end
92
+
93
+ context 'when #create_job raises PreconditionFailed' do
94
+
95
+ before do
96
+ allow(subject).to receive(:create_job)
97
+ .and_raise(Fleet::PreconditionFailed.new('boom'))
98
+ end
99
+
100
+ it 'does not blow up' do
101
+ expect { subject.load(name, service_def) }.to_not raise_error
102
+ end
103
+ end
104
+
105
+ context 'when #create_job raises something other than PreconditionFailed' do
106
+
107
+ before do
108
+ allow(subject).to receive(:create_job)
109
+ .and_raise(Fleet::BadRequest.new('boom'))
110
+ end
111
+
112
+ it 'propagates the error' do
113
+ expect { subject.load(name, service_def) }.to(raise_error(Fleet::BadRequest))
114
+ end
115
+ end
116
+ end
117
+
118
+ describe '#start' do
119
+ let(:service_name) { 'foo.service' }
120
+
121
+ before do
122
+ allow(subject).to receive(:update_job_target_state)
123
+ end
124
+
125
+ it 'invokes #update_job_target_state' do
126
+ expect(subject).to receive(:update_job_target_state)
127
+ .with(service_name, :launched)
128
+
129
+ subject.start(service_name)
130
+ end
131
+ end
132
+
133
+ describe '#stop' do
134
+ let(:service_name) { 'foo.service' }
135
+
136
+ let(:fleet_state) do
137
+ { 'node' => { 'value' => '{ "load_state": "loaded" }' } }
138
+ end
139
+
140
+ before do
141
+ allow(subject).to receive(:update_job_target_state)
142
+ allow(subject).to receive(:get_state).and_return(fleet_state)
143
+ end
144
+
145
+ it 'invokes #update_job_target_state' do
146
+ expect(subject).to receive(:update_job_target_state)
147
+ .with(service_name, :loaded)
148
+
149
+ subject.stop(service_name)
150
+ end
151
+
152
+ it 'checks the job state' do
153
+ expect(subject).to receive(:get_state).with(service_name)
154
+ subject.stop(service_name)
155
+ end
156
+ end
157
+
158
+ describe '#unload' do
159
+ let(:service_name) { 'foo.service' }
160
+
161
+ let(:fleet_state) do
162
+ { 'node' => { 'value' => '{ "load_state": "not-found" }' } }
163
+ end
164
+
165
+ before do
166
+ allow(subject).to receive(:update_job_target_state)
167
+ allow(subject).to receive(:get_state).and_return(fleet_state)
168
+ end
169
+
170
+ it 'invokes #update_job_target_state' do
171
+ expect(subject).to receive(:update_job_target_state)
172
+ .with(service_name, :inactive)
173
+
174
+ subject.unload(service_name)
175
+ end
176
+
177
+ it 'checks the job state' do
178
+ expect(subject).to receive(:get_state).with(service_name)
179
+ subject.unload(service_name)
180
+ end
181
+
182
+ context 'when the unload state cannot be achieved' do
183
+
184
+ before do
185
+ allow(subject).to receive(:get_state).and_raise(Fleet::NotFound, 'boom')
186
+ allow(subject).to receive(:sleep)
187
+ end
188
+
189
+ it 're-checks the state 10 times' do
190
+ expect(subject).to receive(:get_state).exactly(10).times
191
+ subject.unload(service_name) rescue nil
192
+ end
193
+
194
+ it 'raises an error' do
195
+ expect do
196
+ subject.unload(service_name)
197
+ end.to raise_error(Fleet::Error)
198
+ end
199
+
200
+ end
201
+ end
202
+
203
+ describe '#destroy' do
204
+ let(:service_name) { 'foo.service' }
205
+
206
+ before do
207
+ allow(subject).to receive(:delete_job).and_return(nil)
208
+ allow(subject).to receive(:get_state).and_raise(Fleet::NotFound, 'boom')
209
+ end
210
+
211
+ it 'invokes #delete_job' do
212
+
213
+ expect(subject).to receive(:delete_job)
214
+ .with(service_name)
215
+ .and_return(nil)
216
+
217
+ subject.destroy(service_name)
218
+ end
219
+
220
+ it 'checks the job state' do
221
+ expect(subject).to receive(:get_state).with(service_name)
222
+ subject.destroy(service_name)
223
+ end
224
+ end
225
+
226
+ describe '#states' do
227
+
228
+ let(:service_name) { 'foo.service' }
229
+
230
+ let(:fleet_state) do
231
+ { 'node' => { 'value' => '{"load": "loaded", "run": "running"}' } }
232
+ end
233
+
234
+ before do
235
+ allow(subject).to receive(:get_state).and_return(fleet_state)
236
+ end
237
+
238
+ it 'retrieves service state from the fleet client' do
239
+ expect(subject).to receive(:get_state).with(service_name)
240
+ subject.states(service_name)
241
+ end
242
+
243
+ it 'returns the state hash w/ normalized keys' do
244
+ expect(subject.states(service_name)).to eq(load: 'loaded', run: 'running')
245
+ end
246
+ end
247
+ end