nomade 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
@@ -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
@@ -0,0 +1,12 @@
1
+ ENV["TZ"] = "UTC"
2
+
3
+ require "nomade/shell"
4
+ require "nomade/job"
5
+ require "nomade/logger"
6
+ require "nomade/exceptions"
7
+ require "nomade/http"
8
+ require "nomade/deployer"
9
+ require "nomade/decorators"
10
+
11
+ module Nomade
12
+ end
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: []