fleet-api 0.0.1

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