lam 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +16 -8
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -3
- data/Gemfile.lock +107 -0
- data/Guardfile +22 -0
- data/LICENSE.txt +22 -0
- data/README.md +19 -17
- data/Rakefile +4 -0
- data/bin/lam +14 -0
- data/bin/lamb +14 -0
- data/lam.gemspec +18 -20
- data/lib/lam.rb +10 -1
- data/lib/lam/base_controller.rb +54 -0
- data/lib/lam/build.rb +43 -0
- data/lib/lam/build/handler_generator.rb +34 -0
- data/lib/lam/build/lambda_deducer.rb +47 -0
- data/lib/lam/build/templates/handler.js +156 -0
- data/lib/lam/build/traveling_ruby.rb +108 -0
- data/lib/lam/cli.rb +23 -0
- data/lib/lam/cli/help.rb +19 -0
- data/lib/lam/command.rb +25 -0
- data/lib/lam/process.rb +18 -0
- data/lib/lam/process/base_processor.rb +23 -0
- data/lib/lam/process/controller_processor.rb +36 -0
- data/lib/lam/process/help.rb +11 -0
- data/lib/lam/process/processor_deducer.rb +52 -0
- data/lib/lam/util.rb +13 -0
- data/lib/lam/version.rb +1 -1
- data/notes/design.md +43 -0
- data/notes/traveling-ruby-packaging-lam.md +26 -0
- data/notes/traveling-ruby-packaging.md +103 -0
- data/notes/traveling-ruby.md +82 -0
- data/spec/fixtures/project/.gitignore +3 -0
- data/spec/fixtures/project/.ruby-version +1 -0
- data/spec/fixtures/project/Gemfile +4 -0
- data/spec/fixtures/project/Gemfile.lock +35 -0
- data/spec/fixtures/project/app/controllers/application_controller.rb +2 -0
- data/spec/fixtures/project/app/controllers/posts_controller.rb +12 -0
- data/spec/fixtures/project/bin/lam +22 -0
- data/spec/fixtures/project/handlers/controllers/posts.js +156 -0
- data/spec/lib/cli_spec.rb +20 -0
- data/spec/lib/lam/base_controller_spec.rb +18 -0
- data/spec/lib/lam/build/lambda_deducer_spec.rb +20 -0
- data/spec/lib/lam/build_spec.rb +29 -0
- data/spec/lib/lam/process/controller_processor_spec.rb +22 -0
- data/spec/lib/lam/process/infer_spec.rb +24 -0
- data/spec/lib/lam/process_spec.rb +18 -0
- data/spec/spec_helper.rb +25 -0
- metadata +191 -21
- data/bin/console +0 -14
- data/bin/setup +0 -8
@@ -0,0 +1,34 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
require "erb"
|
3
|
+
|
4
|
+
class Lam::Build
|
5
|
+
class HandlerGenerator
|
6
|
+
# handler_info:
|
7
|
+
# {:handler=>"handlers/controllers/posts.create",
|
8
|
+
# :js_path=>"handlers/controllers/posts.js",
|
9
|
+
# :js_method=>"create"}
|
10
|
+
def initialize(handler_info)
|
11
|
+
@handler_info = handler_info
|
12
|
+
@handler = handler_info[:handler]
|
13
|
+
@js_path = handler_info[:js_path]
|
14
|
+
@js_method = handler_info[:js_method]
|
15
|
+
end
|
16
|
+
|
17
|
+
def generate
|
18
|
+
js_path = "#{Lam.root}#{@js_path}"
|
19
|
+
FileUtils.mkdir_p(File.dirname(js_path))
|
20
|
+
|
21
|
+
template_path = File.expand_path('../templates/handler.js', __FILE__)
|
22
|
+
template = IO.read(template_path)
|
23
|
+
|
24
|
+
# Important ERB variables with examples:
|
25
|
+
# @handler - handlers/controllers/posts.create
|
26
|
+
# @process_type - controller
|
27
|
+
@process_type = @handler.split('/')[1].singularize
|
28
|
+
result = ERB.new(template, nil, "-").result(binding)
|
29
|
+
puts "generating #{js_path}"
|
30
|
+
IO.write(js_path, result)
|
31
|
+
# FileUtils.cp(template_path, js_path)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class Lam::Build
|
2
|
+
class LambdaDeducer
|
3
|
+
attr_reader :handlers
|
4
|
+
def initialize(path)
|
5
|
+
@path = path
|
6
|
+
end
|
7
|
+
|
8
|
+
def run
|
9
|
+
deduce
|
10
|
+
end
|
11
|
+
|
12
|
+
def deduce
|
13
|
+
# Example: require "./app/controllers/posts_controller.rb"
|
14
|
+
require_path = @path.starts_with?('/') ? @path : "#{Lam.root}#{@path}"
|
15
|
+
require require_path
|
16
|
+
|
17
|
+
# Example: @klass_name = "PostsController"
|
18
|
+
@klass_name = File.basename(@path, '.rb').classify
|
19
|
+
klass = @klass_name.constantize
|
20
|
+
@handlers = klass.lambda_functions.map { |fn| handler_info(fn) }
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
# Transform the method to the handler info
|
25
|
+
def handler_info(function_name)
|
26
|
+
handler = get_handler(function_name)
|
27
|
+
js_path = get_js_path(function_name)
|
28
|
+
{
|
29
|
+
handler: handler,
|
30
|
+
js_path: js_path,
|
31
|
+
js_method: function_name.to_s
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def get_handler(function_name)
|
36
|
+
"handlers/controllers/#{module_name}.create"
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_js_path(function_name)
|
40
|
+
"handlers/controllers/#{module_name}.js"
|
41
|
+
end
|
42
|
+
|
43
|
+
def module_name
|
44
|
+
@klass_name.sub(/Controller$/,'').underscore
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
'use strict';
|
2
|
+
|
3
|
+
// handler: <%= @handler %>
|
4
|
+
const spawn = require('child_process').spawn;
|
5
|
+
|
6
|
+
// Once hooked up to API Gateway can use the curl command to test:
|
7
|
+
// curl -s -X POST -d @event.json https://endpoint | jq .
|
8
|
+
|
9
|
+
// Filters out lines so only the error lines remain.
|
10
|
+
// Uses the "RubyError: " marker to find the starting error lines.
|
11
|
+
//
|
12
|
+
// Input: String
|
13
|
+
// random line
|
14
|
+
// RubyError: RuntimeError: error in submethod
|
15
|
+
// line1
|
16
|
+
// line2
|
17
|
+
// line3
|
18
|
+
//
|
19
|
+
// Output: String
|
20
|
+
// RubyError: RuntimeError: error in submethod
|
21
|
+
// line1
|
22
|
+
// line2
|
23
|
+
// line3
|
24
|
+
function filterErrorLines(text) {
|
25
|
+
var lines = text.split("\n")
|
26
|
+
var markerIndex = lines.findIndex(line => line.startsWith("RubyError: ") )
|
27
|
+
lines = lines.filter((line, index) => index >= markerIndex )
|
28
|
+
return lines.join("\n")
|
29
|
+
}
|
30
|
+
|
31
|
+
// Produces an Error object that displays in the AWS Lambda test console nicely.
|
32
|
+
// The backtrace are the ruby lines, not the nodejs shim error lines.
|
33
|
+
// The json payload in the Lambda console looks something like this:
|
34
|
+
//
|
35
|
+
// {
|
36
|
+
// "errorMessage": "RubyError: RuntimeError: error in submethod",
|
37
|
+
// "errorType": "RubyError",
|
38
|
+
// "stackTrace": [
|
39
|
+
// [
|
40
|
+
// "line1",
|
41
|
+
// "line2",
|
42
|
+
// "line3"
|
43
|
+
// ]
|
44
|
+
// ]
|
45
|
+
// }
|
46
|
+
//
|
47
|
+
// Input: String
|
48
|
+
// RubyError: RuntimeError: error in submethod
|
49
|
+
// line1
|
50
|
+
// line2
|
51
|
+
// line3
|
52
|
+
//
|
53
|
+
// Output: Error object
|
54
|
+
// { RubyError: RuntimeError: error in submethod
|
55
|
+
// line1
|
56
|
+
// line2
|
57
|
+
// line3 name: 'RubyError' }
|
58
|
+
function customError(text) {
|
59
|
+
text = filterErrorLines(text) // filter for error lines only
|
60
|
+
var lines = text.split("\n")
|
61
|
+
var message = lines[0]
|
62
|
+
var error = new Error(message)
|
63
|
+
error.name = message.split(':')[0]
|
64
|
+
error.stack = lines.slice(0, lines.length-1) // drop final empty line
|
65
|
+
.map(e => e.replace(/^\s+/g,'')) // trim leading whitespaces
|
66
|
+
.join("\n")
|
67
|
+
return error
|
68
|
+
}
|
69
|
+
|
70
|
+
module.exports.<%= @js_method %> = (event, context, callback) => {
|
71
|
+
// To test on mac, set these environment variables:
|
72
|
+
// export RUBY_BIN=$HOME/.rbenv/shims/ruby
|
73
|
+
// export PROCESSOR_COMMAND="lam process controller"
|
74
|
+
|
75
|
+
// Command: lam process controller [event] [context] [handler]
|
76
|
+
const processor_command = process.env.PROCESSOR_COMMAND || "lam"
|
77
|
+
var args = [
|
78
|
+
"process",
|
79
|
+
"<%= @process_type %>",
|
80
|
+
JSON.stringify(event),
|
81
|
+
JSON.stringify(context),
|
82
|
+
"<%= @handler %>"
|
83
|
+
]
|
84
|
+
// console.log("processor_command %o", processor_command)
|
85
|
+
// console.log("args %o", args)
|
86
|
+
|
87
|
+
var ruby = spawn("bin/lam", args);
|
88
|
+
|
89
|
+
// string concatation in javascript is faster than array concatation
|
90
|
+
// http://bit.ly/2gBMDs6
|
91
|
+
var stdout_buffer = ""; // stdout buffer
|
92
|
+
// In the processor_command we do NOT call puts directly and write to stdout
|
93
|
+
// because it will mess up the eventual response that we want API Gateway to
|
94
|
+
// process.
|
95
|
+
// The Lambda prints out function to whatever the return value the ruby method
|
96
|
+
ruby.stdout.on('data', function(data) {
|
97
|
+
// Not using console.log because it decorates output with a newline.
|
98
|
+
//
|
99
|
+
// Uncomment process.stdout.write to see stdout streamed for debugging.
|
100
|
+
// process.stdout.write(data)
|
101
|
+
stdout_buffer += data;
|
102
|
+
});
|
103
|
+
|
104
|
+
// react to potential errors
|
105
|
+
var stderr_buffer = "";
|
106
|
+
ruby.stderr.on('data', function(data) {
|
107
|
+
// not using console.error because it decorates output with a newline
|
108
|
+
stderr_buffer += data
|
109
|
+
process.stderr.write(data)
|
110
|
+
});
|
111
|
+
|
112
|
+
//finalize when ruby process is done.
|
113
|
+
ruby.on('close', function(exit_code) {
|
114
|
+
// http://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-handler.html#nodejs-prog-model-handler-callback
|
115
|
+
|
116
|
+
// succcess
|
117
|
+
if (exit_code == 0) {
|
118
|
+
var result
|
119
|
+
try {
|
120
|
+
result = JSON.parse(stdout_buffer)
|
121
|
+
} catch(e) {
|
122
|
+
// if json cannot be parse assume simple text output intended
|
123
|
+
process.stderr.write("WARN: error parsing json, assuming plain text is desired.")
|
124
|
+
result = stdout_buffer
|
125
|
+
}
|
126
|
+
callback(null, result);
|
127
|
+
|
128
|
+
// callback(null, stdout_buffer);
|
129
|
+
} else {
|
130
|
+
|
131
|
+
// TODO: if this works, allow a way to not decorate the error in case
|
132
|
+
// it actually errors in javascript land
|
133
|
+
// Customize error object with ruby error info
|
134
|
+
var error = customError(stderr_buffer)
|
135
|
+
callback(error);
|
136
|
+
// console.log("error!")
|
137
|
+
}
|
138
|
+
});
|
139
|
+
}
|
140
|
+
|
141
|
+
// for local testing
|
142
|
+
if (process.platform == "darwin") {
|
143
|
+
// fake event and context
|
144
|
+
var event = {"hello": "world"}
|
145
|
+
// var event = {"body": {"hello": "world"}} // API Gateway wrapper structure
|
146
|
+
var context = {"fake": "context"}
|
147
|
+
module.exports.<%= @js_method %>(event, context, (error, message) => {
|
148
|
+
console.error("\nLOCAL TESTING OUTPUT")
|
149
|
+
if (error) {
|
150
|
+
console.error("error message: %o", error)
|
151
|
+
} else {
|
152
|
+
console.error("success message %o", message)
|
153
|
+
// console.log(JSON.stringify(message)) // stringify
|
154
|
+
}
|
155
|
+
})
|
156
|
+
}
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
require "open-uri"
|
3
|
+
require "colorize"
|
4
|
+
|
5
|
+
class Lam::Build
|
6
|
+
TRAVELING_RUBY_VERSION = 'http://d6r77u77i8pq3.cloudfront.net/releases/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz'.freeze
|
7
|
+
TEMP_BUILD_DIR = '/tmp/lam_build'.freeze
|
8
|
+
|
9
|
+
class TravelingRuby
|
10
|
+
def build
|
11
|
+
if File.exist?("#{Lam.root}bundled")
|
12
|
+
puts "Ruby bundled already exists."
|
13
|
+
puts "To force rebundling: rm -rf bundled"
|
14
|
+
return
|
15
|
+
end
|
16
|
+
|
17
|
+
check_ruby_version
|
18
|
+
|
19
|
+
FileUtils.mkdir_p(TEMP_BUILD_DIR)
|
20
|
+
copy_gemfiles
|
21
|
+
|
22
|
+
Dir.chdir(TEMP_BUILD_DIR) do
|
23
|
+
download_traveling_ruby
|
24
|
+
unpack_traveling_ruby
|
25
|
+
bundle_install
|
26
|
+
end
|
27
|
+
|
28
|
+
move_bundled_to_project
|
29
|
+
end
|
30
|
+
|
31
|
+
def check_ruby_version
|
32
|
+
return if ENV['LAM_SKIP_RUBY_CHECK'] # only use if you absolutely need to
|
33
|
+
traveling_version = TRAVELING_RUBY_VERSION.match(/-((\d+)\.(\d+)\.(\d+))-/)[1]
|
34
|
+
if RUBY_VERSION != traveling_version
|
35
|
+
puts "You are using ruby version #{RUBY_VERSION}."
|
36
|
+
abort("You must use ruby #{traveling_version} to build the project because it's what Traveling Ruby uses.".colorize(:red))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def copy_gemfiles
|
41
|
+
FileUtils.cp("#{Lam.root}Gemfile", "#{TEMP_BUILD_DIR}/")
|
42
|
+
FileUtils.cp("#{Lam.root}Gemfile.lock", "#{TEMP_BUILD_DIR}/")
|
43
|
+
end
|
44
|
+
|
45
|
+
def download_traveling_ruby
|
46
|
+
puts "Downloading traveling ruby from #{traveling_ruby_url}."
|
47
|
+
|
48
|
+
FileUtils.rm_rf("#{TEMP_BUILD_DIR}/#{bundled_ruby_dest}")
|
49
|
+
File.open(traveling_ruby_tar_file, 'wb') do |saved_file|
|
50
|
+
# the following "open" is provided by open-uri
|
51
|
+
open(traveling_ruby_url, 'rb') do |read_file|
|
52
|
+
saved_file.write(read_file.read)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
puts 'Download complete.'
|
57
|
+
end
|
58
|
+
|
59
|
+
def unpack_traveling_ruby
|
60
|
+
puts 'Unpacking traveling ruby.'
|
61
|
+
|
62
|
+
FileUtils.mkdir_p(bundled_ruby_dest)
|
63
|
+
|
64
|
+
success = system("tar -xzf #{traveling_ruby_tar_file} -C #{bundled_ruby_dest}")
|
65
|
+
abort('Unpacking traveling ruby failed') unless success
|
66
|
+
puts 'Unpacking traveling ruby successful.'
|
67
|
+
|
68
|
+
puts 'Removing tar.'
|
69
|
+
FileUtils.rm_rf(traveling_ruby_tar_file)
|
70
|
+
end
|
71
|
+
|
72
|
+
def bundle_install
|
73
|
+
puts 'Installing bundle.'
|
74
|
+
require "bundler" # dynamicaly require bundler so user can use any bundler
|
75
|
+
Bundler.with_clean_env do
|
76
|
+
success = system(
|
77
|
+
"cd #{TEMP_BUILD_DIR} && " \
|
78
|
+
'env BUNDLE_IGNORE_CONFIG=1 bundle install --path bundled/gems --without development'
|
79
|
+
)
|
80
|
+
|
81
|
+
abort('Bundle install failed, exiting.') unless success
|
82
|
+
end
|
83
|
+
|
84
|
+
puts 'Bundle install success.'
|
85
|
+
end
|
86
|
+
|
87
|
+
def move_bundled_to_project
|
88
|
+
if File.exist?("#{Lam.root}bundled")
|
89
|
+
puts "Removing current bundled folder"
|
90
|
+
FileUtils.rm_rf("#{Lam.root}bundled")
|
91
|
+
end
|
92
|
+
puts "Moving bundled ruby to your project."
|
93
|
+
FileUtils.mv("#{TEMP_BUILD_DIR}/bundled", Lam.root)
|
94
|
+
end
|
95
|
+
|
96
|
+
def bundled_ruby_dest
|
97
|
+
"bundled/ruby"
|
98
|
+
end
|
99
|
+
|
100
|
+
def traveling_ruby_url
|
101
|
+
TRAVELING_RUBY_VERSION
|
102
|
+
end
|
103
|
+
|
104
|
+
def traveling_ruby_tar_file
|
105
|
+
File.basename(traveling_ruby_url)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/lib/lam/cli.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require "thor"
|
2
|
+
require "lam/cli/help"
|
3
|
+
|
4
|
+
module Lam
|
5
|
+
|
6
|
+
class CLI < Command
|
7
|
+
class_option :verbose, type: :boolean
|
8
|
+
class_option :noop, type: :boolean
|
9
|
+
|
10
|
+
desc "build", "Builds and prepares project for Lambda"
|
11
|
+
long_desc Help.build
|
12
|
+
option :force, type: :boolean, aliases: "-f", desc: "override existing starter files"
|
13
|
+
option :quiet, type: :boolean, aliases: "-q", desc: "silence the output"
|
14
|
+
option :format, type: :string, default: "yaml", desc: "starter project template format: json or yaml"
|
15
|
+
def build
|
16
|
+
Lam::Build.new(options).run
|
17
|
+
end
|
18
|
+
|
19
|
+
desc "process TYPE", "process subcommand tasks"
|
20
|
+
long_desc Help.process
|
21
|
+
subcommand "process", Lam::Process
|
22
|
+
end
|
23
|
+
end
|
data/lib/lam/cli/help.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Lam
|
2
|
+
class CLI < Command
|
3
|
+
class Help
|
4
|
+
class << self
|
5
|
+
def build
|
6
|
+
<<-EOL
|
7
|
+
Builds and prepares project for AWS Lambda. Generates a node shim and vendors Traveling Ruby. Creates a zip file to be uploaded to Lambda for each handler.
|
8
|
+
EOL
|
9
|
+
end
|
10
|
+
|
11
|
+
def process
|
12
|
+
<<-EOL
|
13
|
+
TODO: update process help menu
|
14
|
+
EOL
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/lam/command.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require "thor"
|
2
|
+
|
3
|
+
module Lam
|
4
|
+
class Command < Thor
|
5
|
+
class << self
|
6
|
+
def dispatch(m, args, options, config)
|
7
|
+
# Allow calling for help via:
|
8
|
+
# lam command help
|
9
|
+
# lam command -h
|
10
|
+
# lam command --help
|
11
|
+
# lam command -D
|
12
|
+
#
|
13
|
+
# as well thor's normal way:
|
14
|
+
#
|
15
|
+
# lam help command
|
16
|
+
help_flags = Thor::HELP_MAPPINGS + ["help"]
|
17
|
+
if args.length > 1 && !(args & help_flags).empty?
|
18
|
+
args -= help_flags
|
19
|
+
args.insert(-2, "help")
|
20
|
+
end
|
21
|
+
super
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/lam/process.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
class Lam::Process < Lam::Command
|
2
|
+
autoload :Help, 'lam/process/help'
|
3
|
+
autoload :ProcessorDeducer, 'lam/process/processor_deducer'
|
4
|
+
autoload :BaseProcessor, 'lam/process/base_processor'
|
5
|
+
autoload :ControllerProcessor, 'lam/process/controller_processor'
|
6
|
+
|
7
|
+
class_option :verbose, type: :boolean
|
8
|
+
class_option :noop, type: :boolean
|
9
|
+
class_option :project_root, desc: "Project folder. Defaults to current directory", default: "."
|
10
|
+
class_option :region, desc: "AWS region"
|
11
|
+
|
12
|
+
desc "create STACK", "create a CloudFormation stack"
|
13
|
+
option :randomize_stack_name, type: :boolean, desc: "tack on random string at the end of the stack name", default: nil
|
14
|
+
long_desc Help.controller
|
15
|
+
def controller(event, context, handler)
|
16
|
+
ControllerProcessor.new(event, context, handler).run
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'json'
|
2
|
+
require_relative 'processor_deducer'
|
3
|
+
|
4
|
+
# Global overrides for Lambda processing
|
5
|
+
$stdout.sync = true
|
6
|
+
# This might seem weird but we want puts to write to stderr which is set in
|
7
|
+
# the node shim to write to stderr. This directs the output to Lambda logs.
|
8
|
+
# Printing to stdout can managle up the payload returned from Lambda function.
|
9
|
+
# This is not desired if you want to return say a json payload to API Gateway
|
10
|
+
# eventually.
|
11
|
+
def puts(text)
|
12
|
+
$stderr.puts(text)
|
13
|
+
end
|
14
|
+
|
15
|
+
class Lam::Process::BaseProcessor
|
16
|
+
attr_reader :event, :context, :handler
|
17
|
+
def initialize(event, context, handler)
|
18
|
+
# assume valid json from Lambda
|
19
|
+
@event = JSON.parse(event)
|
20
|
+
@context = JSON.parse(context)
|
21
|
+
@handler = handler
|
22
|
+
end
|
23
|
+
end
|