lam 0.1.0 → 0.1.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 +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
|