nomade 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/nomade/decorators.rb +18 -0
- data/lib/nomade/deployer.rb +328 -0
- data/lib/nomade/exceptions.rb +14 -0
- data/lib/nomade/http.rb +228 -0
- data/lib/nomade/job.rb +76 -0
- data/lib/nomade/logger.rb +18 -0
- data/lib/nomade/shell.rb +45 -0
- data/lib/nomade.rb +12 -0
- metadata +78 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a5ac73ef2143a1d2553ba24433aa6ee34b572a6465f3dc80f3a9f5e71b3750b0
|
4
|
+
data.tar.gz: 7c1e97c20ec00bb4d6e6405c92a91aa3946f2b2187a5f8de053f090555fffff1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0625b32824125388ce00910ef895263168b9d56d57cf5be15673e51ddf8c2e88e8965db2dd23942ed1748feb48035f3876454abe67eead80c128928807b89842
|
7
|
+
data.tar.gz: 8b4e23a2594d7b66885023580b238a7cfac12f829ed99ba5a4265cc30eee341707326715c07c594683959d2a2fe4b630276d7391dba61a977e4f4b1c89a69ef9
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Nomade
|
2
|
+
class Decorator
|
3
|
+
def self.task_state_decorator(task_state, task_failed)
|
4
|
+
case task_state
|
5
|
+
when "pending"
|
6
|
+
"Pending"
|
7
|
+
when "running"
|
8
|
+
"Running"
|
9
|
+
when "dead"
|
10
|
+
if task_failed
|
11
|
+
"Failed with errors!"
|
12
|
+
else
|
13
|
+
"Completed succesfully!"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,328 @@
|
|
1
|
+
module Nomade
|
2
|
+
class Deployer
|
3
|
+
def initialize(nomad_endpoint, nomad_job)
|
4
|
+
@nomad_job = nomad_job
|
5
|
+
@evaluation_id = nil
|
6
|
+
@deployment_id = nil
|
7
|
+
@timeout = Time.now.utc + 60 * 9 # minutes
|
8
|
+
@http = Nomade::Http.new(nomad_endpoint)
|
9
|
+
end
|
10
|
+
|
11
|
+
def deploy!
|
12
|
+
deploy
|
13
|
+
rescue Nomade::NoModificationsError => e
|
14
|
+
Nomade.logger.warn "No modifications to make, exiting!"
|
15
|
+
exit(0)
|
16
|
+
rescue Nomade::GeneralError => e
|
17
|
+
Nomade.logger.warn e.message
|
18
|
+
Nomade.logger.warn "GeneralError hit, exiting!"
|
19
|
+
exit(1)
|
20
|
+
rescue Nomade::PlanningError => e
|
21
|
+
Nomade.logger.warn "Couldn't make a plan, maybe a bad connection to Nomad server, exiting!"
|
22
|
+
exit(2)
|
23
|
+
rescue Nomade::AllocationFailedError => e
|
24
|
+
Nomade.logger.warn "Allocation failed with errors, exiting!"
|
25
|
+
exit(3)
|
26
|
+
rescue Nomade::UnsupportedDeploymentMode => e
|
27
|
+
Nomade.logger.warn e.message
|
28
|
+
Nomade.logger.warn "Deployment failed with errors, exiting!"
|
29
|
+
exit(4)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def deploy
|
35
|
+
Nomade.logger.info "Deploying #{@nomad_job.job_name} (#{@nomad_job.job_type}) with #{@nomad_job.image_name_and_version}"
|
36
|
+
|
37
|
+
Nomade.logger.info "Checking cluster for connectivity and capacity.."
|
38
|
+
@http.plan_job(@nomad_job)
|
39
|
+
|
40
|
+
@evaluation_id = if @http.check_if_job_exists?(@nomad_job)
|
41
|
+
Nomade.logger.info "Updating existing job"
|
42
|
+
@http.update_job(@nomad_job)
|
43
|
+
else
|
44
|
+
Nomade.logger.info "Creating new job"
|
45
|
+
@http.create_job(@nomad_job)
|
46
|
+
end
|
47
|
+
|
48
|
+
Nomade.logger.info "EvaluationID: #{@evaluation_id}"
|
49
|
+
Nomade.logger.info "#{@evaluation_id} Waiting until evaluation is complete"
|
50
|
+
eval_status = nil
|
51
|
+
while(eval_status != "complete") do
|
52
|
+
evaluation = @http.evaluation_request(@evaluation_id)
|
53
|
+
@deployment_id ||= evaluation["DeploymentID"]
|
54
|
+
eval_status = evaluation["Status"]
|
55
|
+
Nomade.logger.info "."
|
56
|
+
sleep(1)
|
57
|
+
end
|
58
|
+
|
59
|
+
Nomade.logger.info "Waiting until allocations are complete"
|
60
|
+
case @nomad_job.job_type
|
61
|
+
when "service"
|
62
|
+
service_deploy
|
63
|
+
when "batch"
|
64
|
+
batch_deploy
|
65
|
+
else
|
66
|
+
raise Nomade::GeneralError.new("Job-type '#{@nomad_job.job_type}' not implemented")
|
67
|
+
end
|
68
|
+
rescue Nomade::AllocationFailedError => e
|
69
|
+
e.allocations.each do |allocation|
|
70
|
+
allocation["TaskStates"].sort.each do |task_name, task_data|
|
71
|
+
pretty_state = Nomade::Decorator.task_state_decorator(task_data["State"], task_data["Failed"])
|
72
|
+
|
73
|
+
Nomade.logger.info ""
|
74
|
+
Nomade.logger.info "#{allocation["ID"]} #{allocation["Name"]} #{task_name}: #{pretty_state}"
|
75
|
+
unless task_data["Failed"]
|
76
|
+
Nomade.logger.info "Task \"#{task_name}\" was succesfully run, skipping log-printing because it isn't relevant!"
|
77
|
+
next
|
78
|
+
end
|
79
|
+
|
80
|
+
stdout = @http.get_allocation_logs(allocation["ID"], task_name, "stdout")
|
81
|
+
if stdout != ""
|
82
|
+
Nomade.logger.info
|
83
|
+
Nomade.logger.info "stdout:"
|
84
|
+
stdout.lines do |logline|
|
85
|
+
Nomade.logger.info(logline.strip)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
stderr = @http.get_allocation_logs(allocation["ID"], task_name, "stderr")
|
90
|
+
if stderr != ""
|
91
|
+
Nomade.logger.info
|
92
|
+
Nomade.logger.info "stderr:"
|
93
|
+
stderr.lines do |logline|
|
94
|
+
Nomade.logger.info(logline.strip)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
task_data["Events"].each do |event|
|
99
|
+
event_type = event["Type"]
|
100
|
+
event_time = Time.at(event["Time"]/1000/1000000).utc
|
101
|
+
event_message = event["DisplayMessage"]
|
102
|
+
|
103
|
+
event_details = if event["Details"].any?
|
104
|
+
dts = event["Details"].map{|k,v| "#{k}: #{v}"}.join(", ")
|
105
|
+
"(#{dts})"
|
106
|
+
end
|
107
|
+
|
108
|
+
Nomade.logger.info "[#{event_time}] #{event_type}: #{event_message} #{event_details}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
raise
|
114
|
+
end
|
115
|
+
|
116
|
+
def service_deploy
|
117
|
+
Nomade.logger.info "Waiting until tasks are placed"
|
118
|
+
Nomade.logger.info ".. deploy timeout is #{@timeout}"
|
119
|
+
|
120
|
+
json = @http.deployment_request(@deployment_id)
|
121
|
+
Nomade.logger.info "#{json["JobID"]} version #{json["JobVersion"]}"
|
122
|
+
|
123
|
+
need_manual_promotion = json["TaskGroups"].values.any?{|tg| tg["DesiredCanaries"] > 0 && tg["AutoPromote"] == false}
|
124
|
+
need_manual_rollback = json["TaskGroups"].values.any?{|tg| tg["DesiredCanaries"] > 0 && tg["AutoRevert"] == false}
|
125
|
+
|
126
|
+
manual_work_required = case [need_manual_promotion, need_manual_rollback]
|
127
|
+
when [true, true]
|
128
|
+
Nomade.logger.info "Job needs manual promotion/rollback, we'll take care of that!"
|
129
|
+
true
|
130
|
+
when [false, false]
|
131
|
+
Nomade.logger.info "Job manages its own promotion/rollback, we will just monitor in a hands-off mode!"
|
132
|
+
false
|
133
|
+
when [false, true]
|
134
|
+
raise UnsupportedDeploymentMode.new("Unsupported deployment-mode, manual-promotion=#{need_manual_promotion}, manual-rollback=#{need_manual_rollback}")
|
135
|
+
when [true, false]
|
136
|
+
raise UnsupportedDeploymentMode.new("Unsupported deployment-mode, manual-promotion=#{need_manual_promotion}, manual-rollback=#{need_manual_rollback}")
|
137
|
+
end
|
138
|
+
|
139
|
+
announced_completed = []
|
140
|
+
promoted = false
|
141
|
+
failed = false
|
142
|
+
succesful_deployment = nil
|
143
|
+
while(succesful_deployment == nil) do
|
144
|
+
json = @http.deployment_request(@deployment_id)
|
145
|
+
|
146
|
+
json["TaskGroups"].each do |task_name, task_data|
|
147
|
+
next if announced_completed.include?(task_name)
|
148
|
+
|
149
|
+
desired_canaries = task_data["DesiredCanaries"]
|
150
|
+
desired_total = task_data["DesiredTotal"]
|
151
|
+
placed_allocations = task_data["PlacedAllocs"]
|
152
|
+
healthy_allocations = task_data["HealthyAllocs"]
|
153
|
+
unhealthy_allocations = task_data["UnhealthyAllocs"]
|
154
|
+
|
155
|
+
if manual_work_required
|
156
|
+
Nomade.logger.info "#{json["ID"]} #{task_name}: #{healthy_allocations}/#{desired_canaries}/#{desired_total} (Healthy/WantedCanaries/Total)"
|
157
|
+
announced_completed << task_name if healthy_allocations == desired_canaries
|
158
|
+
else
|
159
|
+
Nomade.logger.info "#{json["ID"]} #{task_name}: #{healthy_allocations}/#{desired_total} (Healthy/Total)"
|
160
|
+
announced_completed << task_name if healthy_allocations == desired_total
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
if manual_work_required
|
165
|
+
if json["Status"] == "failed"
|
166
|
+
Nomade.logger.info "#{json["Status"]}: #{json["StatusDescription"]}"
|
167
|
+
succesful_deployment = false
|
168
|
+
end
|
169
|
+
|
170
|
+
if succesful_deployment == nil && Time.now.utc > @timeout
|
171
|
+
Nomade.logger.info "Timeout hit, rolling back deploy!"
|
172
|
+
@http.fail_deployment(@deployment_id)
|
173
|
+
succesful_deployment = false
|
174
|
+
end
|
175
|
+
|
176
|
+
if succesful_deployment == nil && json["TaskGroups"].values.all?{|tg| tg["HealthyAllocs"] >= tg["DesiredCanaries"]}
|
177
|
+
if !promoted
|
178
|
+
Nomade.logger.info "Promoting #{@deployment_id} (version #{json["JobVersion"]})"
|
179
|
+
@http.promote_deployment(@deployment_id)
|
180
|
+
promoted = true
|
181
|
+
Nomade.logger.info ".. promoted!"
|
182
|
+
else
|
183
|
+
if json["Status"] == "successful"
|
184
|
+
succesful_deployment = true
|
185
|
+
else
|
186
|
+
Nomade.logger.info "Waiting for promotion to complete #{@deployment_id} (version #{json["JobVersion"]})"
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
else
|
191
|
+
case json["Status"]
|
192
|
+
when "running"
|
193
|
+
# no-op
|
194
|
+
when "failed"
|
195
|
+
Nomade.logger.info "#{json["Status"]}: #{json["StatusDescription"]}"
|
196
|
+
succesful_deployment = false
|
197
|
+
when "successful"
|
198
|
+
Nomade.logger.info "#{json["Status"]}: #{json["StatusDescription"]}"
|
199
|
+
succesful_deployment = true
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
sleep 10 if succesful_deployment == nil
|
204
|
+
end
|
205
|
+
|
206
|
+
if succesful_deployment
|
207
|
+
Nomade.logger.info ""
|
208
|
+
Nomade.logger.info "#{@deployment_id} (version #{json["JobVersion"]}) was succesfully deployed!"
|
209
|
+
else
|
210
|
+
Nomade.logger.warn ""
|
211
|
+
Nomade.logger.warn "#{@deployment_id} (version #{json["JobVersion"]}) deployment _failed_!"
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def batch_deploy
|
216
|
+
alloc_status = nil
|
217
|
+
announced_dead = []
|
218
|
+
|
219
|
+
while(alloc_status != true) do
|
220
|
+
allocations = @http.allocations_from_evaluation_request(@evaluation_id)
|
221
|
+
|
222
|
+
allocations.each do |allocation|
|
223
|
+
allocation["TaskStates"].sort.each do |task_name, task_data|
|
224
|
+
full_task_address = [allocation["ID"], allocation["Name"], task_name].join(" ")
|
225
|
+
pretty_state = Nomade::Decorator.task_state_decorator(task_data["State"], task_data["Failed"])
|
226
|
+
|
227
|
+
unless announced_dead.include?(full_task_address)
|
228
|
+
Nomade.logger.info "#{allocation["ID"]} #{allocation["Name"]} #{task_name}: #{pretty_state}"
|
229
|
+
|
230
|
+
if task_data["State"] == "dead"
|
231
|
+
announced_dead << full_task_address
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
tasks = get_tasks(allocations)
|
238
|
+
upcoming_tasks = get_upcoming_tasks(tasks)
|
239
|
+
succesful_tasks = get_succesful_tasks(tasks)
|
240
|
+
failed_tasks = get_failed_tasks(tasks)
|
241
|
+
|
242
|
+
if upcoming_tasks.size == 0
|
243
|
+
if failed_tasks.any?
|
244
|
+
raise Nomade::AllocationFailedError.new(@evaluation_id, allocations)
|
245
|
+
end
|
246
|
+
|
247
|
+
Nomade.logger.info "Deployment complete"
|
248
|
+
|
249
|
+
allocations.each do |allocation|
|
250
|
+
allocation["TaskStates"].sort.each do |task_name, task_data|
|
251
|
+
pretty_state = Nomade::Decorator.task_state_decorator(task_data["State"], task_data["Failed"])
|
252
|
+
|
253
|
+
Nomade.logger.info ""
|
254
|
+
Nomade.logger.info "#{allocation["ID"]} #{allocation["Name"]} #{task_name}: #{pretty_state}"
|
255
|
+
|
256
|
+
stdout = @http.get_allocation_logs(allocation["ID"], task_name, "stdout")
|
257
|
+
if stdout != ""
|
258
|
+
Nomade.logger.info
|
259
|
+
Nomade.logger.info "stdout:"
|
260
|
+
stdout.lines do |logline|
|
261
|
+
Nomade.logger.info(logline.strip)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
stderr = @http.get_allocation_logs(allocation["ID"], task_name, "stderr")
|
266
|
+
if stderr != ""
|
267
|
+
Nomade.logger.info
|
268
|
+
Nomade.logger.info "stderr:"
|
269
|
+
stderr.lines do |logline|
|
270
|
+
Nomade.logger.info(logline.strip)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
alloc_status = true
|
277
|
+
end
|
278
|
+
|
279
|
+
sleep(1)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# Task-helpers
|
284
|
+
def get_tasks(allocations)
|
285
|
+
[].tap do |it|
|
286
|
+
allocations.each do |allocation|
|
287
|
+
allocation["TaskStates"].sort.each do |task_name, task_data|
|
288
|
+
it << {
|
289
|
+
"Name" => task_name,
|
290
|
+
"Allocation" => allocation,
|
291
|
+
}.merge(task_data)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def get_upcoming_tasks(tasks)
|
298
|
+
[].tap do |it|
|
299
|
+
tasks.each do |task|
|
300
|
+
if ["pending", "running"].include?(task["State"])
|
301
|
+
it << task
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def get_succesful_tasks(tasks)
|
308
|
+
[].tap do |it|
|
309
|
+
tasks.each do |task|
|
310
|
+
if task["State"] == "dead" && task["Failed"] == false
|
311
|
+
it << task
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
def get_failed_tasks(tasks)
|
318
|
+
[].tap do |it|
|
319
|
+
tasks.each do |task|
|
320
|
+
if task["State"] == "dead" && task["Failed"] == true
|
321
|
+
it << task
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
end
|
328
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Nomade
|
2
|
+
class GeneralError < StandardError; end
|
3
|
+
class NoModificationsError < StandardError; end
|
4
|
+
class PlanningError < StandardError; end
|
5
|
+
|
6
|
+
class AllocationFailedError < StandardError
|
7
|
+
def initialize(evaluation_id, allocations)
|
8
|
+
@evaluation_id = evaluation_id
|
9
|
+
@allocations = allocations
|
10
|
+
end
|
11
|
+
attr_reader :evaluation_id, :allocations
|
12
|
+
end
|
13
|
+
class UnsupportedDeploymentMode < StandardError; end
|
14
|
+
end
|
data/lib/nomade/http.rb
ADDED
@@ -0,0 +1,228 @@
|
|
1
|
+
require "net/https"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Nomade
|
5
|
+
class Http
|
6
|
+
def initialize(nomad_endpoint)
|
7
|
+
@nomad_endpoint = nomad_endpoint
|
8
|
+
end
|
9
|
+
|
10
|
+
def job_index_request(search_prefix = nil)
|
11
|
+
search_prefix = if search_prefix
|
12
|
+
"?prefix=#{search_prefix}"
|
13
|
+
else
|
14
|
+
""
|
15
|
+
end
|
16
|
+
uri = URI("#{@nomad_endpoint}/v1/jobs#{search_prefix}")
|
17
|
+
|
18
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
19
|
+
http.use_ssl = true
|
20
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
21
|
+
|
22
|
+
req = Net::HTTP::Get.new(uri)
|
23
|
+
req.add_field "Content-Type", "application/json"
|
24
|
+
|
25
|
+
res = http.request(req)
|
26
|
+
|
27
|
+
raise if res.code != "200"
|
28
|
+
raise if res.content_type != "application/json"
|
29
|
+
|
30
|
+
return JSON.parse(res.body)
|
31
|
+
rescue StandardError => e
|
32
|
+
Nomade.logger.fatal "HTTP Request failed (#{e.message})"
|
33
|
+
raise
|
34
|
+
end
|
35
|
+
|
36
|
+
def evaluation_request(evaluation_id)
|
37
|
+
uri = URI("#{@nomad_endpoint}/v1/evaluation/#{evaluation_id}")
|
38
|
+
|
39
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
40
|
+
http.use_ssl = true
|
41
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
42
|
+
|
43
|
+
req = Net::HTTP::Get.new(uri)
|
44
|
+
req.add_field "Content-Type", "application/json"
|
45
|
+
|
46
|
+
res = http.request(req)
|
47
|
+
|
48
|
+
raise if res.code != "200"
|
49
|
+
raise if res.content_type != "application/json"
|
50
|
+
|
51
|
+
return JSON.parse(res.body)
|
52
|
+
rescue StandardError => e
|
53
|
+
Nomade.logger.fatal "HTTP Request failed (#{e.message})"
|
54
|
+
raise
|
55
|
+
end
|
56
|
+
|
57
|
+
def allocations_from_evaluation_request(evaluation_id)
|
58
|
+
uri = URI("#{@nomad_endpoint}/v1/evaluation/#{evaluation_id}/allocations")
|
59
|
+
|
60
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
61
|
+
http.use_ssl = true
|
62
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
63
|
+
|
64
|
+
req = Net::HTTP::Get.new(uri)
|
65
|
+
req.add_field "Content-Type", "application/json"
|
66
|
+
|
67
|
+
res = http.request(req)
|
68
|
+
|
69
|
+
raise if res.code != "200"
|
70
|
+
raise if res.content_type != "application/json"
|
71
|
+
|
72
|
+
return JSON.parse(res.body)
|
73
|
+
rescue StandardError => e
|
74
|
+
Nomade.logger.fatal "HTTP Request failed (#{e.message})"
|
75
|
+
raise
|
76
|
+
end
|
77
|
+
|
78
|
+
def deployment_request(deployment_id)
|
79
|
+
uri = URI("#{@nomad_endpoint}/v1/deployment/#{deployment_id}")
|
80
|
+
|
81
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
82
|
+
http.use_ssl = true
|
83
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
84
|
+
|
85
|
+
req = Net::HTTP::Get.new(uri)
|
86
|
+
req.add_field "Content-Type", "application/json"
|
87
|
+
|
88
|
+
res = http.request(req)
|
89
|
+
|
90
|
+
raise if res.code != "200"
|
91
|
+
raise if res.content_type != "application/json"
|
92
|
+
|
93
|
+
return JSON.parse(res.body)
|
94
|
+
rescue StandardError => e
|
95
|
+
Nomade.logger.fatal "HTTP Request failed (#{e.message})"
|
96
|
+
raise
|
97
|
+
end
|
98
|
+
|
99
|
+
def check_if_job_exists?(nomad_job)
|
100
|
+
jobs = job_index_request(nomad_job.job_name)
|
101
|
+
jobs.map{|job| job["ID"]}.include?(nomad_job.job_name)
|
102
|
+
end
|
103
|
+
|
104
|
+
def create_job(nomad_job)
|
105
|
+
uri = URI("#{@nomad_endpoint}/v1/jobs")
|
106
|
+
|
107
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
108
|
+
http.use_ssl = true
|
109
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
110
|
+
|
111
|
+
req = Net::HTTP::Post.new(uri)
|
112
|
+
req.add_field "Content-Type", "application/json"
|
113
|
+
req.body = nomad_job.configuration(:json)
|
114
|
+
|
115
|
+
res = http.request(req)
|
116
|
+
raise if res.code != "200"
|
117
|
+
raise if res.content_type != "application/json"
|
118
|
+
|
119
|
+
return JSON.parse(res.body)["EvalID"]
|
120
|
+
rescue StandardError => e
|
121
|
+
Nomade.logger.fatal "HTTP Request failed (#{e.message})"
|
122
|
+
raise
|
123
|
+
end
|
124
|
+
|
125
|
+
def update_job(nomad_job)
|
126
|
+
uri = URI("#{@nomad_endpoint}/v1/job/#{nomad_job.job_name}")
|
127
|
+
|
128
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
129
|
+
http.use_ssl = true
|
130
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
131
|
+
|
132
|
+
req = Net::HTTP::Post.new(uri)
|
133
|
+
req.add_field "Content-Type", "application/json"
|
134
|
+
req.body = nomad_job.configuration(:json)
|
135
|
+
|
136
|
+
res = http.request(req)
|
137
|
+
raise if res.code != "200"
|
138
|
+
raise if res.content_type != "application/json"
|
139
|
+
|
140
|
+
return JSON.parse(res.body)["EvalID"]
|
141
|
+
rescue StandardError => e
|
142
|
+
Nomade.logger.fatal "HTTP Request failed (#{e.message})"
|
143
|
+
raise
|
144
|
+
end
|
145
|
+
|
146
|
+
def promote_deployment(deployment_id)
|
147
|
+
uri = URI("#{@nomad_endpoint}/v1/deployment/promote/#{deployment_id}")
|
148
|
+
|
149
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
150
|
+
http.use_ssl = true
|
151
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
152
|
+
|
153
|
+
req = Net::HTTP::Post.new(uri)
|
154
|
+
req.add_field "Content-Type", "application/json"
|
155
|
+
req.body = {
|
156
|
+
"DeploymentID" => deployment_id,
|
157
|
+
"All" => true,
|
158
|
+
}.to_json
|
159
|
+
|
160
|
+
res = http.request(req)
|
161
|
+
raise if res.code != "200"
|
162
|
+
raise if res.content_type != "application/json"
|
163
|
+
|
164
|
+
return true
|
165
|
+
rescue StandardError => e
|
166
|
+
Nomade.logger.fatal "HTTP Request failed (#{e.message})"
|
167
|
+
raise
|
168
|
+
end
|
169
|
+
|
170
|
+
def fail_deployment(deployment_id)
|
171
|
+
uri = URI("#{@nomad_endpoint}/v1/deployment/fail/#{deployment_id}")
|
172
|
+
|
173
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
174
|
+
http.use_ssl = true
|
175
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
176
|
+
|
177
|
+
req = Net::HTTP::Post.new(uri)
|
178
|
+
req.add_field "Content-Type", "application/json"
|
179
|
+
|
180
|
+
res = http.request(req)
|
181
|
+
raise if res.code != "200"
|
182
|
+
raise if res.content_type != "application/json"
|
183
|
+
|
184
|
+
return true
|
185
|
+
rescue StandardError => e
|
186
|
+
Nomade.logger.fatal "HTTP Request failed (#{e.message})"
|
187
|
+
raise
|
188
|
+
end
|
189
|
+
|
190
|
+
def get_allocation_logs(allocation_id, task_name, logtype)
|
191
|
+
uri = URI("#{@nomad_endpoint}/v1/client/fs/logs/#{allocation_id}?task=#{task_name}&type=#{logtype}&plain=true&origin=end")
|
192
|
+
|
193
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
194
|
+
http.use_ssl = true
|
195
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
196
|
+
|
197
|
+
req = Net::HTTP::Get.new(uri)
|
198
|
+
res = http.request(req)
|
199
|
+
raise if res.code != "200"
|
200
|
+
|
201
|
+
return res.body.gsub(/\e\[\d+m/, '')
|
202
|
+
rescue StandardError => e
|
203
|
+
Nomade.logger.fatal "HTTP Request failed (#{e.message})"
|
204
|
+
raise
|
205
|
+
end
|
206
|
+
|
207
|
+
def plan_job(nomad_job)
|
208
|
+
rendered_template = nomad_job.configuration(:hcl)
|
209
|
+
# 0: No allocations created or destroyed. Nothing to do.
|
210
|
+
# 1: Allocations created or destroyed.
|
211
|
+
# 255: Error determining plan results. Nothing to do.
|
212
|
+
allowed_exit_codes = [0, 1, 255]
|
213
|
+
|
214
|
+
exit_status, stdout, stderr = Shell.exec("NOMAD_ADDR=#{@nomad_endpoint} nomad job plan -diff -verbose -no-color -", rendered_template, allowed_exit_codes)
|
215
|
+
|
216
|
+
case exit_status
|
217
|
+
when 0
|
218
|
+
raise Nomade::NoModificationsError.new
|
219
|
+
when 1
|
220
|
+
# no-op
|
221
|
+
when 255
|
222
|
+
raise Nomade::PlanningError.new
|
223
|
+
end
|
224
|
+
|
225
|
+
true
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
data/lib/nomade/job.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
require "erb"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Nomade
|
5
|
+
class Job
|
6
|
+
class FormattingError < StandardError; end
|
7
|
+
|
8
|
+
def initialize(template_file, image_full_name, environment_variables = {})
|
9
|
+
@image_full_name = image_full_name
|
10
|
+
@environment_variables = environment_variables
|
11
|
+
|
12
|
+
# image_full_name should be in the form of:
|
13
|
+
# redis:4.0.1
|
14
|
+
# kaspergrubbe/secretimage:latest
|
15
|
+
unless @image_full_name.match(/\A[a-zA-Z0-9\/]+\:[a-zA-Z0-9\.\-\_]+\z/)
|
16
|
+
raise Nomade::Job::FormattingError.new("Image-format wrong: #{@image_full_name}")
|
17
|
+
end
|
18
|
+
|
19
|
+
@config_hcl = render_erb(template_file)
|
20
|
+
@config_json = convert_job_hcl_to_json(@config_hcl)
|
21
|
+
@config_hash = JSON.parse(@config_json)
|
22
|
+
end
|
23
|
+
|
24
|
+
def configuration(format)
|
25
|
+
case format
|
26
|
+
when :hcl
|
27
|
+
@config_hcl
|
28
|
+
when :json
|
29
|
+
@config_json
|
30
|
+
else
|
31
|
+
@config_hash
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def job_name
|
36
|
+
@config_hash["Job"]["ID"]
|
37
|
+
end
|
38
|
+
|
39
|
+
def job_type
|
40
|
+
@config_hash["Job"]["Type"]
|
41
|
+
end
|
42
|
+
|
43
|
+
def image_name_and_version
|
44
|
+
@image_full_name
|
45
|
+
end
|
46
|
+
|
47
|
+
def image_name
|
48
|
+
image_name_and_version.split(":").first
|
49
|
+
end
|
50
|
+
|
51
|
+
def image_version
|
52
|
+
image_name_and_version.split(":").last
|
53
|
+
end
|
54
|
+
|
55
|
+
def environment_variables
|
56
|
+
@environment_variables
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def render_erb(erb_template)
|
62
|
+
file = File.open(erb_template).read
|
63
|
+
rendered = ERB.new(file, nil, '-').result(binding)
|
64
|
+
|
65
|
+
rendered
|
66
|
+
end
|
67
|
+
|
68
|
+
def convert_job_hcl_to_json(rendered_template)
|
69
|
+
exit_status, stdout, stderr = Shell.exec("nomad job run -output -no-color -", rendered_template)
|
70
|
+
|
71
|
+
JSON.pretty_generate({
|
72
|
+
"Job": JSON.parse(stdout)["Job"],
|
73
|
+
})
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'yell'
|
2
|
+
|
3
|
+
module Nomade
|
4
|
+
def self.logger
|
5
|
+
$logger ||= begin
|
6
|
+
stdout = if ARGV.include?("-d") || ARGV.include?("--debug")
|
7
|
+
[:debug, :info, :warn]
|
8
|
+
else
|
9
|
+
[:info, :warn]
|
10
|
+
end
|
11
|
+
|
12
|
+
Yell.new do |l|
|
13
|
+
l.adapter STDOUT, level: stdout
|
14
|
+
l.adapter STDERR, level: [:error, :fatal]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/nomade/shell.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require "open3"
|
2
|
+
|
3
|
+
module Nomade
|
4
|
+
class Shell
|
5
|
+
def self.exec(command, input = nil, allowed_exit_codes = [0])
|
6
|
+
Nomade.logger.debug("+: #{command}")
|
7
|
+
|
8
|
+
process, status, stdout, stderr = Open3.popen3(command) do |stdin, stdout, stderr, wait_thread|
|
9
|
+
if input
|
10
|
+
stdin.puts(input)
|
11
|
+
end
|
12
|
+
stdin.close
|
13
|
+
|
14
|
+
threads = {}.tap do |it|
|
15
|
+
it[:stdout] = Thread.new do
|
16
|
+
output = []
|
17
|
+
stdout.each do |l|
|
18
|
+
output << l
|
19
|
+
Nomade.logger.debug(l)
|
20
|
+
end
|
21
|
+
Thread.current[:output] = output.join
|
22
|
+
end
|
23
|
+
|
24
|
+
it[:stderr] = Thread.new do
|
25
|
+
output = []
|
26
|
+
stderr.each do |l|
|
27
|
+
output << l
|
28
|
+
Nomade.logger.debug(l)
|
29
|
+
end
|
30
|
+
Thread.current[:output] = output.join
|
31
|
+
end
|
32
|
+
end
|
33
|
+
threads.values.map(&:join)
|
34
|
+
|
35
|
+
[wait_thread.value, wait_thread.value.exitstatus, threads[:stdout][:output], threads[:stderr][:output]]
|
36
|
+
end
|
37
|
+
|
38
|
+
unless allowed_exit_codes.include?(status)
|
39
|
+
raise "`#{command}` failed with status=#{status}"
|
40
|
+
end
|
41
|
+
|
42
|
+
return [status, stdout, stderr]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/nomade.rb
ADDED
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nomade
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kasper Grubbe
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-10-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: yell
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pry
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.12.2
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.12.2
|
41
|
+
description:
|
42
|
+
email: nomade@kaspergrubbe.com
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- lib/nomade.rb
|
48
|
+
- lib/nomade/decorators.rb
|
49
|
+
- lib/nomade/deployer.rb
|
50
|
+
- lib/nomade/exceptions.rb
|
51
|
+
- lib/nomade/http.rb
|
52
|
+
- lib/nomade/job.rb
|
53
|
+
- lib/nomade/logger.rb
|
54
|
+
- lib/nomade/shell.rb
|
55
|
+
homepage: https://billetto.com
|
56
|
+
licenses:
|
57
|
+
- MIT
|
58
|
+
metadata: {}
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options: []
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
requirements: []
|
74
|
+
rubygems_version: 3.0.3
|
75
|
+
signing_key:
|
76
|
+
specification_version: 4
|
77
|
+
summary: Gem that deploys nomad jobs
|
78
|
+
test_files: []
|