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