nomade 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.
- 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: []
|