simplerubysteps 0.0.7 → 0.0.8
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 +4 -4
- data/README.md +64 -24
- data/exe/simplerubysteps +3 -3
- data/exe/srs +6 -0
- data/lib/function.rb +4 -1
- data/lib/simplerubysteps/dsl.rb +86 -0
- data/lib/simplerubysteps/model.rb +186 -0
- data/lib/{statemachine.yaml → simplerubysteps/statemachine.yaml} +2 -0
- data/lib/simplerubysteps/tool.rb +454 -0
- data/lib/simplerubysteps/version.rb +1 -1
- data/lib/simplerubysteps.rb +2 -273
- data/simplerubysteps.gemspec +2 -4
- metadata +9 -15
- data/.gitignore +0 -9
- data/Gemfile +0 -5
- data/LICENSE.txt +0 -21
- data/bin/console +0 -7
- data/bin/setup +0 -6
- data/lib/tool.rb +0 -448
- data/samples/sample1/sample-task-worker.sh +0 -5
- data/samples/sample1/start-callbackbranch.sh +0 -3
- data/samples/sample1/start-directbranch.sh +0 -3
- data/samples/sample1/workflow.rb +0 -39
- data/samples/sample2/workflow.rb +0 -42
@@ -0,0 +1,454 @@
|
|
1
|
+
require "simplerubysteps/version"
|
2
|
+
require "aws-sdk-cloudformation"
|
3
|
+
require "aws-sdk-s3"
|
4
|
+
require "aws-sdk-states"
|
5
|
+
require "digest"
|
6
|
+
require "zip"
|
7
|
+
require "tempfile"
|
8
|
+
require "json"
|
9
|
+
require "optparse"
|
10
|
+
require "aws-sdk-cloudwatchlogs"
|
11
|
+
require "time"
|
12
|
+
|
13
|
+
module Simplerubysteps
|
14
|
+
class Tool
|
15
|
+
def initialize
|
16
|
+
@cloudformation_client = Aws::CloudFormation::Client.new
|
17
|
+
@s3_client = Aws::S3::Client.new
|
18
|
+
@states_client = Aws::States::Client.new
|
19
|
+
@logs_client = Aws::CloudWatchLogs::Client.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def tail_follow_logs(log_group_name, extract_pattern = nil) # FIXME too hacky and not really working
|
23
|
+
Signal.trap("INT") do
|
24
|
+
exit
|
25
|
+
end
|
26
|
+
|
27
|
+
first_event_time = Time.now.to_i * 1000
|
28
|
+
|
29
|
+
next_tokens = {}
|
30
|
+
first_round = true
|
31
|
+
loop do
|
32
|
+
log_streams = @logs_client.describe_log_streams(
|
33
|
+
log_group_name: log_group_name,
|
34
|
+
order_by: "LastEventTime",
|
35
|
+
descending: true,
|
36
|
+
).log_streams
|
37
|
+
|
38
|
+
log_streams.each do |log_stream|
|
39
|
+
get_log_events_params = {
|
40
|
+
log_group_name: log_group_name,
|
41
|
+
log_stream_name: log_stream.log_stream_name,
|
42
|
+
}
|
43
|
+
|
44
|
+
if next_tokens.key?(log_stream.log_stream_name)
|
45
|
+
get_log_events_params[:next_token] = next_tokens[log_stream.log_stream_name]
|
46
|
+
else
|
47
|
+
get_log_events_params[:start_time] = first_round ? log_stream.last_event_timestamp : first_event_time
|
48
|
+
end
|
49
|
+
|
50
|
+
response = @logs_client.get_log_events(get_log_events_params)
|
51
|
+
|
52
|
+
response.events.each do |event|
|
53
|
+
if event.timestamp >= first_event_time
|
54
|
+
if extract_pattern
|
55
|
+
if /#{extract_pattern}/ =~ event.message
|
56
|
+
puts $1
|
57
|
+
exit
|
58
|
+
end
|
59
|
+
else
|
60
|
+
puts "#{Time.at(event.timestamp / 1000).utc} - #{log_stream.log_stream_name} - #{event.message}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
next_tokens[log_stream.log_stream_name] = response.next_forward_token
|
66
|
+
end
|
67
|
+
|
68
|
+
sleep 5
|
69
|
+
|
70
|
+
first_round = false
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def stack_outputs(stack_name)
|
75
|
+
begin
|
76
|
+
response = @cloudformation_client.describe_stacks(stack_name: stack_name)
|
77
|
+
outputs = {}
|
78
|
+
response.stacks.first.outputs.each do |output|
|
79
|
+
outputs[output.output_key] = output.output_value
|
80
|
+
end
|
81
|
+
outputs
|
82
|
+
rescue Aws::CloudFormation::Errors::ServiceError => error
|
83
|
+
return nil if error.message =~ /Stack .* does not exist/
|
84
|
+
raise error
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def stack_params(stack_name, template, parameters)
|
89
|
+
params = {
|
90
|
+
stack_name: stack_name,
|
91
|
+
template_body: template,
|
92
|
+
capabilities: ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"],
|
93
|
+
parameters: [],
|
94
|
+
}
|
95
|
+
parameters.each do |k, v|
|
96
|
+
params[:parameters].push({
|
97
|
+
parameter_key: k,
|
98
|
+
parameter_value: v,
|
99
|
+
})
|
100
|
+
end
|
101
|
+
params
|
102
|
+
end
|
103
|
+
|
104
|
+
def stack_create(stack_name, template, parameters)
|
105
|
+
@cloudformation_client.create_stack(stack_params(stack_name, template, parameters))
|
106
|
+
@cloudformation_client.wait_until(:stack_create_complete, stack_name: stack_name)
|
107
|
+
stack_outputs(stack_name)
|
108
|
+
end
|
109
|
+
|
110
|
+
def stack_update(stack_name, template, parameters)
|
111
|
+
begin
|
112
|
+
@cloudformation_client.update_stack(stack_params(stack_name, template, parameters))
|
113
|
+
@cloudformation_client.wait_until(:stack_update_complete, stack_name: stack_name)
|
114
|
+
stack_outputs(stack_name)
|
115
|
+
rescue Aws::CloudFormation::Errors::ServiceError => error
|
116
|
+
return stack_outputs(stack_name).merge({ :no_update => true }) if error.message =~ /No updates are to be performed/
|
117
|
+
raise unless error.message =~ /No updates are to be performed/
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def upload_to_s3(bucket, key, body)
|
122
|
+
@s3_client.put_object(
|
123
|
+
bucket: bucket,
|
124
|
+
key: key,
|
125
|
+
body: body,
|
126
|
+
)
|
127
|
+
end
|
128
|
+
|
129
|
+
def upload_file_to_s3(bucket, key, file_path)
|
130
|
+
File.open(file_path, "rb") do |file|
|
131
|
+
upload_to_s3(bucket, key, file)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def empty_s3_bucket(bucket_name)
|
136
|
+
@s3_client.list_objects_v2(bucket: bucket_name).contents.each do |object|
|
137
|
+
@s3_client.delete_object(bucket: bucket_name, key: object.key)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def create_zip(zip_file, files_by_name)
|
142
|
+
Zip::File.open(zip_file, create: true) do |zipfile|
|
143
|
+
base_dir = File.expand_path(File.dirname(__FILE__))
|
144
|
+
files_by_name.each do |n, f|
|
145
|
+
zipfile.add n, f
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def dir_files(base_dir, glob)
|
151
|
+
files_by_name = {}
|
152
|
+
base_dir = File.expand_path(base_dir)
|
153
|
+
Dir.glob("#{base_dir}/#{glob}").select { |path| File.file?(path) }.each do |f|
|
154
|
+
files_by_name[File.expand_path(f)[base_dir.length + 1..-1]] = f
|
155
|
+
end
|
156
|
+
files_by_name
|
157
|
+
end
|
158
|
+
|
159
|
+
def stack_name_from_current_dir
|
160
|
+
File.basename(File.expand_path("."))
|
161
|
+
end
|
162
|
+
|
163
|
+
def workflow_files
|
164
|
+
dir_files ".", "**/*.rb"
|
165
|
+
end
|
166
|
+
|
167
|
+
def my_lib_files
|
168
|
+
files = dir_files(File.dirname(__FILE__) + "/..", "**/*.rb").filter { |f| not(f =~ /tool.rb/) }
|
169
|
+
end
|
170
|
+
|
171
|
+
def cloudformation_template
|
172
|
+
File.open("#{File.dirname(__FILE__)}/statemachine.yaml", "r") do |file|
|
173
|
+
return file.read
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def log(extract_pattern = nil)
|
178
|
+
current_stack_outputs = stack_outputs(stack_name_from_current_dir)
|
179
|
+
raise "State Machine is not deployed" unless current_stack_outputs
|
180
|
+
function_name = current_stack_outputs["LambdaFunctionName"]
|
181
|
+
|
182
|
+
tail_follow_logs "/aws/lambda/#{function_name}", extract_pattern
|
183
|
+
end
|
184
|
+
|
185
|
+
def destroy
|
186
|
+
current_stack_outputs = stack_outputs(stack_name_from_current_dir)
|
187
|
+
deploy_bucket = current_stack_outputs["DeployBucket"]
|
188
|
+
rause "No CloudFormation stack to destroy" unless deploy_bucket
|
189
|
+
|
190
|
+
empty_s3_bucket deploy_bucket
|
191
|
+
|
192
|
+
puts "Bucket emptied: #{deploy_bucket}"
|
193
|
+
|
194
|
+
@cloudformation_client.delete_stack(stack_name: stack_name_from_current_dir)
|
195
|
+
@cloudformation_client.wait_until(:stack_delete_complete, stack_name: stack_name_from_current_dir)
|
196
|
+
|
197
|
+
puts "Stack deleted"
|
198
|
+
end
|
199
|
+
|
200
|
+
def deploy
|
201
|
+
current_stack_outputs = stack_outputs(stack_name_from_current_dir)
|
202
|
+
|
203
|
+
unless current_stack_outputs
|
204
|
+
current_stack_outputs = stack_create(stack_name_from_current_dir, cloudformation_template, {
|
205
|
+
"DeployLambda" => "no",
|
206
|
+
"DeployStepfunctions" => "no",
|
207
|
+
"LambdaS3" => "",
|
208
|
+
"StepFunctionsS3" => "",
|
209
|
+
"StateMachineType" => "",
|
210
|
+
})
|
211
|
+
|
212
|
+
puts "Deployment bucket created"
|
213
|
+
end
|
214
|
+
|
215
|
+
deploy_bucket = current_stack_outputs["DeployBucket"]
|
216
|
+
|
217
|
+
puts "Deployment bucket: #{deploy_bucket}"
|
218
|
+
|
219
|
+
function_zip_temp = Tempfile.new("function")
|
220
|
+
create_zip function_zip_temp.path, my_lib_files.merge(workflow_files)
|
221
|
+
lambda_sha = Digest::SHA1.file function_zip_temp.path
|
222
|
+
lambda_zip_name = "function-#{lambda_sha}.zip"
|
223
|
+
upload_file_to_s3 deploy_bucket, lambda_zip_name, function_zip_temp.path
|
224
|
+
|
225
|
+
puts "Uploaded: #{lambda_zip_name}"
|
226
|
+
|
227
|
+
unless current_stack_outputs["LambdaFunctionARN"]
|
228
|
+
current_stack_outputs = stack_update(stack_name_from_current_dir, cloudformation_template, {
|
229
|
+
"DeployLambda" => "yes",
|
230
|
+
"DeployStepfunctions" => "no",
|
231
|
+
"LambdaS3" => lambda_zip_name,
|
232
|
+
"StepFunctionsS3" => "",
|
233
|
+
"StateMachineType" => "",
|
234
|
+
})
|
235
|
+
|
236
|
+
puts "Lambda function created"
|
237
|
+
end
|
238
|
+
|
239
|
+
lambda_arn = current_stack_outputs["LambdaFunctionARN"]
|
240
|
+
|
241
|
+
puts "Lambda function: #{lambda_arn}"
|
242
|
+
|
243
|
+
workflow_type = `ruby -e 'require "./workflow.rb";puts $sm.kind'`.strip
|
244
|
+
|
245
|
+
state_machine_json = JSON.parse(`LAMBDA_FUNCTION_ARN=#{lambda_arn} ruby -e 'require "./workflow.rb";puts $sm.render.to_json'`).to_json
|
246
|
+
state_machine_json_sha = Digest::SHA1.hexdigest state_machine_json
|
247
|
+
state_machine_json_name = "statemachine-#{state_machine_json_sha}.json"
|
248
|
+
upload_to_s3 deploy_bucket, state_machine_json_name, state_machine_json
|
249
|
+
|
250
|
+
puts "Uploaded: #{state_machine_json_name}"
|
251
|
+
|
252
|
+
current_stack_outputs = stack_update(stack_name_from_current_dir, cloudformation_template, {
|
253
|
+
"DeployLambda" => "yes",
|
254
|
+
"DeployStepfunctions" => "yes",
|
255
|
+
"LambdaS3" => lambda_zip_name,
|
256
|
+
"StepFunctionsS3" => state_machine_json_name,
|
257
|
+
"StateMachineType" => workflow_type,
|
258
|
+
})
|
259
|
+
|
260
|
+
if current_stack_outputs[:no_update]
|
261
|
+
puts "Stack not updated"
|
262
|
+
else
|
263
|
+
puts "Stack updated"
|
264
|
+
end
|
265
|
+
|
266
|
+
puts "State machine: #{current_stack_outputs["StepFunctionsStateMachineARN"]}"
|
267
|
+
end
|
268
|
+
|
269
|
+
def start_sync_execution(state_machine_arn, input)
|
270
|
+
@states_client.start_sync_execution(
|
271
|
+
state_machine_arn: state_machine_arn,
|
272
|
+
input: input,
|
273
|
+
)
|
274
|
+
end
|
275
|
+
|
276
|
+
def start_async_execution(state_machine_arn, input)
|
277
|
+
@states_client.start_execution(
|
278
|
+
state_machine_arn: state_machine_arn,
|
279
|
+
input: input,
|
280
|
+
)
|
281
|
+
end
|
282
|
+
|
283
|
+
def describe_execution(execution_arn)
|
284
|
+
@states_client.describe_execution(
|
285
|
+
execution_arn: execution_arn,
|
286
|
+
)
|
287
|
+
end
|
288
|
+
|
289
|
+
def wait_for_async_execution_completion(execution_arn)
|
290
|
+
response = nil
|
291
|
+
|
292
|
+
loop do
|
293
|
+
response = describe_execution(execution_arn)
|
294
|
+
status = response.status
|
295
|
+
|
296
|
+
break if %w[SUCCEEDED FAILED TIMED_OUT].include?(status)
|
297
|
+
|
298
|
+
sleep 5
|
299
|
+
end
|
300
|
+
|
301
|
+
response
|
302
|
+
end
|
303
|
+
|
304
|
+
def start(wait = true, input = $stdin)
|
305
|
+
current_stack_outputs = stack_outputs(stack_name_from_current_dir)
|
306
|
+
raise "State Machine is not deployed" unless current_stack_outputs
|
307
|
+
state_machine_arn = current_stack_outputs["StepFunctionsStateMachineARN"]
|
308
|
+
|
309
|
+
input_json = JSON.parse(input.read).to_json
|
310
|
+
|
311
|
+
if current_stack_outputs["StateMachineType"] == "STANDARD"
|
312
|
+
start_response = start_async_execution(state_machine_arn, input_json)
|
313
|
+
|
314
|
+
unless wait
|
315
|
+
puts start_response.to_json
|
316
|
+
else
|
317
|
+
execution_arn = start_response.execution_arn
|
318
|
+
|
319
|
+
puts wait_for_async_execution_completion(execution_arn).to_json
|
320
|
+
end
|
321
|
+
elsif current_stack_outputs["StateMachineType"] == "EXPRESS"
|
322
|
+
puts start_sync_execution(state_machine_arn, input_json).to_json
|
323
|
+
else
|
324
|
+
raise "Unknown state machine type: #{current_stack_outputs["StateMachineType"]}"
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
def send_task_success(task_token, output = $stdin)
|
329
|
+
raise "No token" unless task_token
|
330
|
+
|
331
|
+
output_json = JSON.parse(output.read).to_json
|
332
|
+
|
333
|
+
puts @states_client.send_task_success(
|
334
|
+
task_token: task_token,
|
335
|
+
output: output_json,
|
336
|
+
).to_json
|
337
|
+
end
|
338
|
+
|
339
|
+
def run
|
340
|
+
options = {
|
341
|
+
:wait => false,
|
342
|
+
:input => $stdin,
|
343
|
+
}
|
344
|
+
|
345
|
+
subcommands = {
|
346
|
+
"deploy" => OptionParser.new do |opts|
|
347
|
+
opts.banner = "Usage: #{$0} deploy [options]"
|
348
|
+
|
349
|
+
opts.on("-h", "--help", "Display this help message") do
|
350
|
+
puts opts
|
351
|
+
exit
|
352
|
+
end
|
353
|
+
end,
|
354
|
+
"destroy" => OptionParser.new do |opts|
|
355
|
+
opts.banner = "Usage: #{$0} destroy [options]"
|
356
|
+
|
357
|
+
opts.on("-h", "--help", "Display this help message") do
|
358
|
+
puts opts
|
359
|
+
exit
|
360
|
+
end
|
361
|
+
end,
|
362
|
+
"log" => OptionParser.new do |opts|
|
363
|
+
opts.banner = "Usage: #{$0} log [options]"
|
364
|
+
|
365
|
+
opts.on("--extract_pattern VALUE", "Wait for and extract pattern") do |value|
|
366
|
+
options[:extract_pattern] = value
|
367
|
+
end
|
368
|
+
|
369
|
+
opts.on("-h", "--help", "Display this help message") do
|
370
|
+
puts opts
|
371
|
+
exit
|
372
|
+
end
|
373
|
+
end,
|
374
|
+
"start" => OptionParser.new do |opts|
|
375
|
+
opts.banner = "Usage: #{$0} start [options]"
|
376
|
+
|
377
|
+
opts.on("--wait", "Wait for STANDARD state machine to complete") do
|
378
|
+
options[:wait] = true
|
379
|
+
end
|
380
|
+
|
381
|
+
opts.on("--input VALUE", "/path/to/file (STDIN will be used per default)") do |value|
|
382
|
+
options[:input] = File.new(value)
|
383
|
+
end
|
384
|
+
|
385
|
+
opts.on("-h", "--help", "Display this help message") do
|
386
|
+
puts opts
|
387
|
+
exit
|
388
|
+
end
|
389
|
+
end,
|
390
|
+
"task-success" => OptionParser.new do |opts|
|
391
|
+
opts.banner = "Usage: #{$0} task-success [options]"
|
392
|
+
|
393
|
+
opts.on("--input VALUE", "/path/to/file (STDIN will be used per default)") do |value|
|
394
|
+
options[:input] = File.new(value)
|
395
|
+
end
|
396
|
+
|
397
|
+
opts.on("--token VALUE", "The task token") do |value|
|
398
|
+
options[:token] = value
|
399
|
+
end
|
400
|
+
|
401
|
+
opts.on("-h", "--help", "Display this help message") do
|
402
|
+
puts opts
|
403
|
+
exit
|
404
|
+
end
|
405
|
+
end,
|
406
|
+
}
|
407
|
+
|
408
|
+
global = OptionParser.new do |opts|
|
409
|
+
opts.banner = "Usage: #{$0} [command] [options]"
|
410
|
+
opts.separator ""
|
411
|
+
opts.separator "Commands (#{Simplerubysteps::VERSION}):"
|
412
|
+
opts.separator " deploy Create Step Functions State Machine"
|
413
|
+
opts.separator " destroy Delete Step Functions State Machine"
|
414
|
+
opts.separator " log Continuously prints Lambda function log output"
|
415
|
+
opts.separator " start Start State Machine execution"
|
416
|
+
opts.separator " task-success Continue Start State Machine execution"
|
417
|
+
opts.separator ""
|
418
|
+
|
419
|
+
opts.on_tail("-h", "--help", "Display this help message") do
|
420
|
+
puts opts
|
421
|
+
exit
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
begin
|
426
|
+
global.order!(ARGV)
|
427
|
+
command = ARGV.shift
|
428
|
+
options[:command] = command
|
429
|
+
subcommands.fetch(command).parse!(ARGV)
|
430
|
+
rescue KeyError
|
431
|
+
puts "Unknown command: '#{command}'"
|
432
|
+
puts
|
433
|
+
puts global
|
434
|
+
exit 1
|
435
|
+
rescue OptionParser::ParseError => error
|
436
|
+
puts error.message
|
437
|
+
puts subcommands.fetch(command)
|
438
|
+
exit 1
|
439
|
+
end
|
440
|
+
|
441
|
+
if options[:command] == "deploy"
|
442
|
+
deploy
|
443
|
+
elsif options[:command] == "start"
|
444
|
+
start options[:wait], options[:input]
|
445
|
+
elsif options[:command] == "log"
|
446
|
+
log options[:extract_pattern]
|
447
|
+
elsif options[:command] == "task-success"
|
448
|
+
send_task_success options[:token], options[:input]
|
449
|
+
elsif options[:command] == "destroy"
|
450
|
+
destroy
|
451
|
+
end
|
452
|
+
end
|
453
|
+
end
|
454
|
+
end
|