jets 0.0.1 → 0.2.0

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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +7 -0
  4. data/Gemfile.lock +63 -0
  5. data/Guardfile +19 -9
  6. data/README.md +9 -16
  7. data/jets.gemspec +11 -7
  8. data/lib/jets.rb +11 -0
  9. data/lib/jets/base_controller.rb +54 -0
  10. data/lib/jets/build.rb +46 -0
  11. data/lib/jets/build/handler_generator.rb +46 -0
  12. data/lib/jets/build/lambda_deducer.rb +23 -0
  13. data/lib/jets/build/templates/handler.js +149 -0
  14. data/lib/jets/build/traveling_ruby.rb +133 -0
  15. data/lib/jets/cfn.rb +5 -0
  16. data/lib/jets/cfn/base.rb +17 -0
  17. data/lib/jets/cfn/builder.rb +53 -0
  18. data/lib/jets/cfn/namer.rb +30 -0
  19. data/lib/jets/cli.rb +10 -6
  20. data/lib/jets/cli/help.rb +8 -2
  21. data/lib/jets/process.rb +18 -0
  22. data/lib/jets/process/base_processor.rb +23 -0
  23. data/lib/jets/process/controller_processor.rb +36 -0
  24. data/lib/jets/process/help.rb +11 -0
  25. data/lib/jets/process/processor_deducer.rb +51 -0
  26. data/lib/jets/project.rb +23 -0
  27. data/lib/jets/util.rb +13 -0
  28. data/lib/jets/version.rb +1 -1
  29. data/notes/design.md +107 -0
  30. data/notes/faq.md +3 -0
  31. data/notes/lambda_ruby_info.md +34 -0
  32. data/notes/traveling-ruby-packaging-jets.md +26 -0
  33. data/notes/traveling-ruby-packaging.md +103 -0
  34. data/notes/traveling-ruby-structure.md +6 -0
  35. data/notes/traveling-ruby.md +82 -0
  36. data/spec/fixtures/classes.rb +5 -0
  37. data/spec/fixtures/project/.gitignore +3 -0
  38. data/spec/fixtures/project/.ruby-version +1 -0
  39. data/spec/fixtures/project/Gemfile +4 -0
  40. data/spec/fixtures/project/Gemfile.lock +23 -0
  41. data/spec/fixtures/project/app/controllers/application_controller.rb +2 -0
  42. data/spec/fixtures/project/app/controllers/posts_controller.rb +12 -0
  43. data/spec/fixtures/project/bin/jets +22 -0
  44. data/spec/fixtures/project/config/routes.rb +6 -0
  45. data/spec/fixtures/project/handlers/controllers/posts.js +212 -0
  46. data/spec/lib/cli_spec.rb +5 -4
  47. data/spec/lib/jets/base_controller_spec.rb +18 -0
  48. data/spec/lib/jets/build/handler_generator_spec.rb +20 -0
  49. data/spec/lib/jets/build/lambda_deducer_spec.rb +15 -0
  50. data/spec/lib/jets/build_spec.rb +34 -0
  51. data/spec/lib/jets/cfn/builder_spec.rb +18 -0
  52. data/spec/lib/jets/cfn/namer_spec.rb +16 -0
  53. data/spec/lib/jets/process/controller_processor_spec.rb +22 -0
  54. data/spec/lib/jets/process/infer_spec.rb +24 -0
  55. data/spec/lib/jets/process_spec.rb +18 -0
  56. data/spec/lib/jets/project_spec.rb +14 -0
  57. data/spec/spec_helper.rb +8 -2
  58. metadata +82 -15
@@ -0,0 +1,133 @@
1
+ require "fileutils"
2
+ require "open-uri"
3
+ require "colorize"
4
+
5
+ class Jets::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/jets_build'.freeze
8
+
9
+ class TravelingRuby
10
+ def build
11
+ if File.exist?("#{Jets.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
+ vendor_gemfiles
27
+ end
28
+
29
+ move_bundled_to_project
30
+ end
31
+
32
+ def check_ruby_version
33
+ return if ENV['LAM_SKIP_RUBY_CHECK'] # only use if you absolutely need to
34
+ traveling_version = TRAVELING_RUBY_VERSION.match(/-((\d+)\.(\d+)\.(\d+))-/)[1]
35
+ if RUBY_VERSION != traveling_version
36
+ puts "You are using ruby version #{RUBY_VERSION}."
37
+ abort("You must use ruby #{traveling_version} to build the project because it's what Traveling Ruby uses.".colorize(:red))
38
+ end
39
+ end
40
+
41
+ def copy_gemfiles
42
+ FileUtils.cp("#{Jets.root}Gemfile", "#{TEMP_BUILD_DIR}/")
43
+ FileUtils.cp("#{Jets.root}Gemfile.lock", "#{TEMP_BUILD_DIR}/")
44
+ end
45
+
46
+ def download_traveling_ruby
47
+ puts "Downloading traveling ruby from #{traveling_ruby_url}."
48
+
49
+ FileUtils.rm_rf("#{TEMP_BUILD_DIR}/#{bundled_ruby_dest}")
50
+ File.open(traveling_ruby_tar_file, 'wb') do |saved_file|
51
+ # the following "open" is provided by open-uri
52
+ open(traveling_ruby_url, 'rb') do |read_file|
53
+ saved_file.write(read_file.read)
54
+ end
55
+ end
56
+
57
+ puts 'Download complete.'
58
+ end
59
+
60
+ def unpack_traveling_ruby
61
+ puts 'Unpacking traveling ruby.'
62
+
63
+ FileUtils.mkdir_p(bundled_ruby_dest)
64
+
65
+ success = system("tar -xzf #{traveling_ruby_tar_file} -C #{bundled_ruby_dest}")
66
+ abort('Unpacking traveling ruby failed') unless success
67
+ puts 'Unpacking traveling ruby successful.'
68
+
69
+ puts 'Removing tar.'
70
+ FileUtils.rm_rf(traveling_ruby_tar_file)
71
+ end
72
+
73
+ def bundle_install
74
+ puts 'Installing bundle.'
75
+ require "bundler" # dynamicaly require bundler so user can use any bundler
76
+ Bundler.with_clean_env do
77
+ success = system(
78
+ "cd #{TEMP_BUILD_DIR} && " \
79
+ 'env BUNDLE_IGNORE_CONFIG=1 bundle install --path bundled/gems --without development'
80
+ )
81
+
82
+ abort('Bundle install failed, exiting.') unless success
83
+ end
84
+
85
+ puts 'Bundle install success.'
86
+ end
87
+
88
+ # The wrapper script doesnt work unless you move the gem files in the
89
+ # bundled/gems folder and export it to BUNDLE_GEMFILE in the
90
+ # wrapper script.
91
+ def vendor_gemfiles
92
+ puts "Moving gemfiles into #{bundled_gems_dest}/"
93
+ FileUtils.mv("Gemfile", "#{bundled_gems_dest}/")
94
+ FileUtils.mv("Gemfile.lock", "#{bundled_gems_dest}/")
95
+
96
+ bundle_config_path = "#{bundled_gems_dest}/.bundle/config"
97
+ puts "Generating #{bundle_config_path}"
98
+ FileUtils.mkdir_p(File.dirname(bundle_config_path))
99
+ bundle_config =<<-EOL
100
+ BUNDLE_PATH: .
101
+ BUNDLE_WITHOUT: development
102
+ BUNDLE_DISABLE_SHARED_GEMS: '1'
103
+ EOL
104
+ IO.write(bundle_config_path, bundle_config)
105
+
106
+ end
107
+
108
+ def move_bundled_to_project
109
+ if File.exist?("#{Jets.root}bundled")
110
+ puts "Removing current bundled folder"
111
+ FileUtils.rm_rf("#{Jets.root}bundled")
112
+ end
113
+ puts "Moving bundled ruby to your project."
114
+ FileUtils.mv("#{TEMP_BUILD_DIR}/bundled", Jets.root)
115
+ end
116
+
117
+ def bundled_ruby_dest
118
+ "bundled/ruby"
119
+ end
120
+
121
+ def bundled_gems_dest
122
+ "bundled/gems"
123
+ end
124
+
125
+ def traveling_ruby_url
126
+ TRAVELING_RUBY_VERSION
127
+ end
128
+
129
+ def traveling_ruby_tar_file
130
+ File.basename(traveling_ruby_url)
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,5 @@
1
+ class Jets::Cfn
2
+ autoload :Base, "jets/cfn/base"
3
+ autoload :Builder, "jets/cfn/builder"
4
+ autoload :Namer, "jets/cfn/namer"
5
+ end
@@ -0,0 +1,17 @@
1
+ class Jets::Cfn
2
+ class Base
3
+ def add_resource(logical_id, type, properties)
4
+ @template[:Resources][logical_id] = {
5
+ Type: type,
6
+ Properties: properties
7
+ }
8
+ end
9
+
10
+ def add_parameter(name, options={})
11
+ defaults = { Type: "String" }
12
+ options = defaults.merge(options)
13
+ @template[:Parameters] ||= {}
14
+ @template[:Parameters][name.camelize] = options
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,53 @@
1
+ require 'active_support/core_ext/hash'
2
+ require 'yaml'
3
+
4
+ class Jets::Cfn
5
+ class Builder < Base
6
+ def initialize(controller_class)
7
+ @controller_class = controller_class
8
+ @template = ActiveSupport::HashWithIndifferentAccess.new(Resources: {})
9
+ end
10
+
11
+ def compose!
12
+ add_parameters
13
+ add_functions
14
+ end
15
+
16
+ def add_parameters
17
+ add_parameter("LambdaIamRole", Description: "Iam Role that Lambda function uses.")
18
+ add_parameter("S3Bucket", Description: "S3 Bucket for source code.")
19
+ add_s3_bucket
20
+ end
21
+
22
+ def add_functions
23
+ @controller_class.lambda_functions.each do |name|
24
+ add_function(name)
25
+ end
26
+ end
27
+
28
+ def add_function(name)
29
+ namer = Namer.new(@controller_class, name)
30
+
31
+ add_resource(namer.logical_id, "AWS::Lambda::Function",
32
+ Code: {
33
+ S3Bucket: {Ref: "S3Bucket"}, # from child stack
34
+ S3Key: namer.s3_key
35
+ },
36
+ FunctionName: namer.function_name,
37
+ Handler: namer.handler,
38
+ Role: { Ref: "LambdaIamRole" },
39
+ MemorySize: Jets::Project.memory_size,
40
+ Runtime: Jets::Project.runtime,
41
+ Timeout: Jets::Project.timeout
42
+ )
43
+ end
44
+
45
+ def template
46
+ @template
47
+ end
48
+
49
+ def text
50
+ YAML.dump(@template.to_hash)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,30 @@
1
+ class Jets::Cfn
2
+ class Namer
3
+ def initialize(controller_class, method_name)
4
+ @controller_class, @method_name = controller_class, method_name
5
+ end
6
+
7
+ def handler
8
+ controller_name = @controller_class.to_s.sub('Controller', '').underscore
9
+ "handlers/controllers/#{controller_name}.#{@method_name}"
10
+ end
11
+
12
+ def logical_id
13
+ "#{@controller_class}_#{@method_name}".camelize
14
+ end
15
+
16
+ def function_name
17
+ "#{Jets::Project.project_name}-#{Jets::Project.env}-#{logical_id.underscore.dasherize}"
18
+ end
19
+
20
+ def s3_key
21
+ self.class.s3_key
22
+ end
23
+
24
+ # @@s3_key = "jets/cfn-templates/dev/#{Time.now.strftime("%Y%m%dT%H%M%S")}/jets-app-code.zip"
25
+ @@s3_key = "jets/cfn-templates/code.zip" # hardcode to test
26
+ def self.s3_key
27
+ @@s3_key
28
+ end
29
+ end
30
+ end
@@ -7,13 +7,17 @@ module Jets
7
7
  class_option :verbose, type: :boolean
8
8
  class_option :noop, type: :boolean
9
9
 
10
- desc "hello NAME", "say hello to NAME"
11
- long_desc Help.hello
12
- option :from, desc: "from person"
13
- def hello(name)
14
- puts "from: #{options[:from]}" if options[:from]
15
- puts "Hello #{name}"
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
+ Jets::Build.new(options).run
16
17
  end
17
18
 
19
+ desc "process TYPE", "process subcommand tasks"
20
+ long_desc Help.process
21
+ subcommand "process", Jets::Process
18
22
  end
19
23
  end
@@ -2,9 +2,15 @@ module Jets
2
2
  class CLI < Command
3
3
  class Help
4
4
  class << self
5
- def hello
5
+ def build
6
6
  <<-EOL
7
- Hello world example
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
8
14
  EOL
9
15
  end
10
16
  end
@@ -0,0 +1,18 @@
1
+ class Jets::Process < Jets::Command
2
+ autoload :Help, 'jets/process/help'
3
+ autoload :ProcessorDeducer, 'jets/process/processor_deducer'
4
+ autoload :BaseProcessor, 'jets/process/base_processor'
5
+ autoload :ControllerProcessor, 'jets/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 Jets::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
@@ -0,0 +1,36 @@
1
+ require_relative "base_processor"
2
+
3
+ class Jets::Process
4
+ class ControllerProcessor < Jets::Process::BaseProcessor
5
+ def run
6
+ # Use the handler value (ie: posts.create) to deduce the user's business
7
+ # code to require and run.
8
+ deducer = ProcessorDeducer.new(handler)
9
+ path = deducer.controller[:path]
10
+ code = deducer.controller[:code]
11
+
12
+ begin
13
+ require path # require "app/controllers/posts_controller.rb"
14
+ # Puts the return value of user's code to stdout because this is
15
+ # what eventually gets used by API Gateway.
16
+ # Explicitly using $stdout since puts redirected to $stderr.
17
+
18
+ # result = PostsController.new(event, context).create
19
+ result = instance_eval(code, path)
20
+
21
+ # JSON.dump is pretty robust. If it cannot dump the structure into a
22
+ # json string, it just dumps it to a plain text string.
23
+ $stdout.puts JSON.dump(result) # only place where we write to stdout.
24
+ rescue Exception => e
25
+ # Customize error message slightly so nodejs shim can process the
26
+ # returned error message.
27
+ # The "RubyError: " is a marker that the javascript shim scans for.
28
+ $stderr.puts("RubyError: #{e.class}: #{e.message}") # js needs this as the first line
29
+ backtrace = e.backtrace.map {|l| " #{l}" }
30
+ $stderr.puts(backtrace)
31
+ # $stderr.puts("END OF RUBY OUTPUT")
32
+ exit 1 # instead of re-raising to control the error backtrace output
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,11 @@
1
+ class Jets::Process::Help
2
+ class << self
3
+ def controller
4
+ <<-EOL
5
+ Examples:
6
+
7
+ jets process controller '{ "we" : "love", "using" : "Lambda" }' '{"test": "1"}' "handlers/controllers/posts.create"
8
+ EOL
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,51 @@
1
+ class Jets::Process::ProcessorDeducer
2
+ def initialize(handler)
3
+ @handler = handler
4
+ end
5
+
6
+ # Deduces the path and method from the handler. Example:
7
+ #
8
+ # ProcessorDeducer.new("handlers/functions/posts.create").function
9
+ # => {path: "app/functions/posts.rb", code: "create(event, context)"}
10
+ #
11
+ # Summary:
12
+ #
13
+ # Input:
14
+ # handler: handlers/functions/posts.create
15
+ # Output:
16
+ # path: app/functions/posts.rb
17
+ # code: create(event, context) # code to instance_eval
18
+ #
19
+ # Returns: {path: path, code: code}
20
+ def function
21
+ path, meth = @handler.split('.')
22
+ path = Jets.root + path.sub("handlers", "app") + ".rb"
23
+ code = "#{meth}(event, context)"
24
+ {path: path, code: code}
25
+ end
26
+
27
+ # Deduces the path and method from the handler. Example:
28
+ #
29
+ # ProcessorDeducer.new("handlers/controllers/posts.create").controller
30
+ # => {path: "controllers/posts_controller.rb", code: "create"}
31
+ #
32
+ # Summary:
33
+ #
34
+ # Input:
35
+ # handler: handlers/controllers/posts.create
36
+ # Output:
37
+ # path: app/controllers/posts_controller.rb
38
+ # code: create # code to instance_eval
39
+ #
40
+ # Returns: {path: path, code: code}
41
+ def controller
42
+ handler_path, meth = @handler.split('.')
43
+
44
+ path = Jets.root + handler_path.sub("handlers", "app") + "_controller.rb"
45
+ controller_name = handler_path.sub(%r{.*handlers/controllers/}, "") + "_controller" # posts_controller
46
+ controller_class = controller_name.split('_').collect(&:capitalize).join # PostsController
47
+ code = "#{controller_class}.new(event, context).#{meth}" # PostsController.new(event, context).create
48
+
49
+ {path: path, code: code, class_name: controller_class}
50
+ end
51
+ end
@@ -0,0 +1,23 @@
1
+ require 'ostruct'
2
+
3
+ # The Project default options.
4
+ # Overriden with config/project.yml
5
+ class Jets::Project
6
+ class << self
7
+ def method_missing(method_name)
8
+ options = Jets::Project.new.options
9
+ options[method_name.to_sym]
10
+ end
11
+ end
12
+
13
+ # Defaults
14
+ def options
15
+ OpenStruct.new(
16
+ project_name: "proj", # shouldnt really be a default here
17
+ env: ENV['JETS_ENV'] || 'dev', # shouldnt really be a default here
18
+ timeout: 30,
19
+ runtime: "nodejs6.10",
20
+ memory_size: 1536
21
+ )
22
+ end
23
+ end