jets 1.0.18 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/Gemfile.lock +10 -10
  4. data/README/testing.md +5 -2
  5. data/lib/jets.rb +2 -2
  6. data/lib/jets/application.rb +69 -40
  7. data/lib/jets/booter.rb +17 -20
  8. data/lib/jets/builders/code_builder.rb +7 -8
  9. data/lib/jets/cfn/ship.rb +0 -6
  10. data/lib/jets/commands/build.rb +0 -5
  11. data/lib/jets/commands/deploy.rb +0 -4
  12. data/lib/jets/commands/main.rb +31 -4
  13. data/lib/jets/commands/templates/skeleton/{.env → .env.tt} +1 -0
  14. data/lib/jets/commands/templates/skeleton/config.ru +1 -0
  15. data/lib/jets/commands/upgrade/v1.rb +12 -0
  16. data/lib/jets/controller.rb +5 -0
  17. data/lib/jets/controller/base.rb +43 -21
  18. data/lib/jets/controller/cookies.rb +40 -0
  19. data/lib/jets/controller/cookies/jar.rb +269 -0
  20. data/lib/jets/controller/middleware.rb +4 -0
  21. data/lib/jets/controller/middleware/local.rb +119 -0
  22. data/lib/jets/{server/lambda_aws_proxy.rb → controller/middleware/local/api_gateway.rb} +11 -49
  23. data/lib/jets/controller/middleware/local/mimic_aws_call.rb +38 -0
  24. data/lib/jets/{server → controller/middleware/local}/route_matcher.rb +4 -4
  25. data/lib/jets/controller/middleware/main.rb +46 -0
  26. data/lib/jets/{server → controller/middleware}/webpacker_setup.rb +0 -1
  27. data/lib/jets/controller/params.rb +2 -1
  28. data/lib/jets/controller/rack.rb +5 -0
  29. data/lib/jets/controller/rack/adapter.rb +60 -0
  30. data/lib/jets/controller/rack/env.rb +96 -0
  31. data/lib/jets/controller/redirection.rb +1 -1
  32. data/lib/jets/controller/renderers.rb +1 -1
  33. data/lib/jets/controller/renderers/base_renderer.rb +0 -4
  34. data/lib/jets/controller/renderers/{aws_proxy_renderer.rb → rack_renderer.rb} +7 -19
  35. data/lib/jets/controller/renderers/template_renderer.rb +1 -1
  36. data/lib/jets/controller/request.rb +14 -44
  37. data/lib/jets/controller/response.rb +55 -7
  38. data/lib/jets/internal/app/controllers/jets/rack_controller.rb +13 -3
  39. data/lib/jets/mega.rb +7 -0
  40. data/lib/jets/{rack → mega}/hash_converter.rb +1 -1
  41. data/lib/jets/{rack → mega}/request.rb +17 -4
  42. data/lib/jets/middleware.rb +38 -0
  43. data/lib/jets/middleware/configurator.rb +84 -0
  44. data/lib/jets/middleware/default_stack.rb +44 -0
  45. data/lib/jets/middleware/layer.rb +34 -0
  46. data/lib/jets/middleware/stack.rb +77 -0
  47. data/lib/jets/resource/function.rb +1 -1
  48. data/lib/jets/ruby_server.rb +1 -1
  49. data/lib/jets/server.rb +48 -13
  50. data/lib/jets/version.rb +1 -1
  51. metadata +24 -17
  52. data/lib/jets/application/middleware.rb +0 -23
  53. data/lib/jets/default/application.rb +0 -23
  54. data/lib/jets/rack.rb +0 -7
  55. data/lib/jets/rack/server.rb +0 -47
  56. data/lib/jets/server/api_gateway.rb +0 -39
  57. data/lib/jets/server/timing_middleware.rb +0 -33
  58. data/lib/jets/timing.rb +0 -65
  59. data/lib/jets/timing/report.rb +0 -82
@@ -0,0 +1,4 @@
1
+ module Jets::Controller::Middleware
2
+ autoload :Local, "jets/controller/middleware/local"
3
+ autoload :Main, "jets/controller/middleware/main"
4
+ end
@@ -0,0 +1,119 @@
1
+ require 'kramdown'
2
+
3
+ # Handles mimicking of API Gateway to Lambda function call locally
4
+ module Jets::Controller::Middleware
5
+ class Local
6
+ extend Memoist
7
+
8
+ autoload :ApiGateway, 'jets/controller/middleware/local/api_gateway'
9
+ autoload :MimicAwsCall, 'jets/controller/middleware/local/mimic_aws_call'
10
+ autoload :RouteMatcher, 'jets/controller/middleware/local/route_matcher'
11
+
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ def call(env)
17
+ route = RouteMatcher.new(env).find_route
18
+ unless route
19
+ return [404, {'Content-Type' => 'text/html'}, not_found(env)]
20
+ end
21
+
22
+ mimic = MimicAwsCall.new(route, env)
23
+ # Make @controller and @meth instance available so we dont not have to pass it around.
24
+ @controller, @meth, @event = mimic.controller, mimic.meth, mimic.event
25
+
26
+ if route.to == 'jets/rack#process' # megamode
27
+ # Bypass the Jets middlewares since it could interfere with the Rack
28
+ # application's middleware stack.
29
+ #
30
+ # Rails sends back a transfer-encoding=chunked. Curling Rails directly works,
31
+ # but passing the Rails response back through this middleware results in errors.
32
+ # Disable chunking responses by deleting the transfer-encoding response header.
33
+ # Would like to understand why this happens this better, if someone can explain please let me know.
34
+ status, headers, body = @controller.dispatch! # jets/rack_controller
35
+ headers.delete "transfer-encoding"
36
+ [status, headers, body]
37
+ elsif polymorphic_function?
38
+ # Will never hit when calling polymorphic function on AWS Lambda.
39
+ # This can only really get called with the local server.
40
+ run_polymophic_function
41
+ else # Normal Jets request
42
+ mimick_aws_lambda!(env, mimic.vars) unless on_aws?(env)
43
+ @app.call(env)
44
+ end
45
+ end
46
+
47
+ def polymorphic_function?
48
+ polymorphic_function.task.lang != :ruby
49
+ end
50
+
51
+ def polymorphic_function
52
+ # Abusing PolyFun to run polymorphic code, should call LambdaExecutor directly
53
+ # after reworking LambdaExecutor so it has a better interface.
54
+ Jets::PolyFun.new(@controller.class, @meth)
55
+ end
56
+ memoize :polymorphic_function
57
+
58
+ def run_polymophic_function
59
+ resp = polymorphic_function.run(@event, @meth) # polymorphic code
60
+ status = resp['statusCode']
61
+ headers = resp['headers']
62
+ body = StringIO.new(resp['body'])
63
+ [status, headers, body] # triplet
64
+ end
65
+
66
+ # Modifies env the in the same way real call from AWS lambda would modify env
67
+ def mimick_aws_lambda!(env, vars)
68
+ env.merge!(vars)
69
+ env
70
+ end
71
+
72
+ def on_aws?(env)
73
+ return false if ENV['TEST'] # usually with test we're passing in full API Gateway fixtures with the HTTP_X_AMZN_TRACE_ID
74
+ !!env['HTTP_X_AMZN_TRACE_ID']
75
+ end
76
+
77
+ def routes_error_message(env)
78
+ message = "<h2>404 Error: Route #{env['PATH_INFO'].sub('/','')} not found</h2>"
79
+ if Jets.env != "production"
80
+ message << "<p>Here are the routes defined in your application:</p>"
81
+ message << "#{routes_table}"
82
+ end
83
+ message
84
+ end
85
+
86
+ def not_found(env)
87
+ message = routes_error_message(env)
88
+ body = <<~HTML
89
+ <!DOCTYPE html>
90
+ <html>
91
+ <head>
92
+ <meta charset="utf-8">
93
+ <title>Route not found</title>
94
+ </head>
95
+ <body>
96
+ #{message}
97
+ </body>
98
+ </html>
99
+ HTML
100
+ StringIO.new(body)
101
+ end
102
+
103
+ # Show pretty route table for user to help with debugging in non-production mode
104
+ def routes_table
105
+ routes = Jets::Router.routes
106
+
107
+ return "Your routes table is empty." if routes.empty?
108
+
109
+ text = "Verb | Path | Controller#action\n"
110
+ text << "--- | --- | ---\n"
111
+ routes.each do |route|
112
+ text << "#{route.method} | #{route.path} | #{route.to}\n"
113
+ end
114
+ html = Kramdown::Document.new(text).to_html
115
+ puts html
116
+ html
117
+ end
118
+ end
119
+ end
@@ -1,38 +1,13 @@
1
- require 'cgi'
2
- require 'stringio'
1
+ # Takes a Rack env and converts to ApiGateway event
2
+ class Jets::Controller::Middleware::Local
3
+ class ApiGateway
4
+ extend Memoist
3
5
 
4
- class Jets::Server
5
- # This doesnt really need to be middleware
6
- class LambdaAwsProxy
7
6
  def initialize(route, env)
8
- @route = route
9
- @env = env
10
- # puts "Rack env:".colorize(:yellow)
11
- # @env.each do |k,v|
12
- # puts "#{k}: #{v}"
13
- # end
7
+ @route, @env = route, env
14
8
  end
15
9
 
16
- def response
17
- event = build_event
18
- context = {}
19
-
20
- controller_class = find_controller_class
21
- controller_action = find_controller_action
22
-
23
- fun = Jets::PolyFun.new(controller_class, controller_action)
24
- resp = fun.run(event, context) # check the logs for polymorphic function errors
25
-
26
- # Map lambda proxy response format to rack format
27
- status = resp["statusCode"]
28
- headers = resp["headers"] || {}
29
- headers = {'Content-Type' => 'text/html'}.merge(headers)
30
- body = resp["body"]
31
-
32
- [status, headers, [body]]
33
- end
34
-
35
- def build_event
10
+ def event
36
11
  resource = @route.path(:api_gateway) # posts/{id}/edit
37
12
  path = @env['PATH_INFO'].sub('/','') # remove beginning slash
38
13
  {
@@ -48,6 +23,7 @@ class Jets::Server
48
23
  "isBase64Encoded" => false,
49
24
  }
50
25
  end
26
+ memoize :event
51
27
 
52
28
  # Annoying. The headers part of the AWS Lambda proxy structure
53
29
  # does not consisently use the same casing scheme for the header keys.
@@ -97,12 +73,6 @@ class Jets::Server
97
73
  end
98
74
  end
99
75
 
100
- # Way to fake X-Amzn-Trace-Id which on_aws? helper checks.
101
- # This is how we distinguish a request from API gateway vs local.
102
- if ENV['JETS_ON_AWS']
103
- headers["X-Amzn-Trace-Id"] = "Root=fake-trace-id"
104
- end
105
-
106
76
  headers
107
77
  end
108
78
 
@@ -114,21 +84,13 @@ class Jets::Server
114
84
  # rack.input: #<StringIO:0x007f8ccf8db9a0>
115
85
  def get_body
116
86
  # @env["rack.input"] is always provided by rack and we should make
117
- # the test data always have rack.input to mimic rack, but but handling
118
- # it this way because it's simpler.
87
+ # the test data always have rack.input to mimic rack defaulting to
88
+ # StringIO.new to help make testing easier
119
89
  input = @env["rack.input"] || StringIO.new
120
90
  body = input.read
121
- # return nil for blank string, because thats what Lambda AWS_PROXY does
91
+ input.rewind # IMPORTANT or else it screws up other middlewares that use the body
92
+ # return nil for blank string, because Lambda AWS_PROXY does this
122
93
  body unless body.empty?
123
94
  end
124
-
125
- def find_controller_class
126
- # posts#edit => PostsController
127
- @route.controller_name.constantize
128
- end
129
-
130
- def find_controller_action
131
- @route.action_name
132
- end
133
95
  end
134
96
  end
@@ -0,0 +1,38 @@
1
+ class Jets::Controller::Middleware::Local
2
+ class MimicAwsCall
3
+ extend Memoist
4
+
5
+ def initialize(route, env)
6
+ @route, @env = route, env
7
+ end
8
+
9
+ def vars
10
+ {
11
+ 'jets.controller' => controller,
12
+ 'lambda.context' => context,
13
+ 'lambda.event' => event,
14
+ 'lambda.meth' => meth,
15
+ }
16
+ end
17
+
18
+ # Actual controller instance
19
+ def controller
20
+ controller_class = @route.controller_name.constantize
21
+ meth = @route.action_name
22
+ controller_class.new(event, context, meth)
23
+ end
24
+
25
+ def meth
26
+ @route.action_name
27
+ end
28
+
29
+ def event
30
+ ApiGateway.new(@route, @env).event
31
+ end
32
+ memoize :event
33
+
34
+ def context
35
+ {}
36
+ end
37
+ end
38
+ end
@@ -1,4 +1,4 @@
1
- class Jets::Server
1
+ class Jets::Controller::Middleware::Local
2
2
  class RouteMatcher
3
3
  def initialize(env)
4
4
  @env = env
@@ -12,14 +12,14 @@ class Jets::Server
12
12
  # Within these 2 groups we consider the routes with the longest path first
13
13
  # since posts/:id and posts/:id/edit can both match.
14
14
  routes = router.ordered_routes
15
- route = routes.find do |route|
16
- route_found?(route)
15
+ route = routes.find do |r|
16
+ route_found?(r)
17
17
  end
18
18
  route
19
19
  end
20
20
 
21
21
  def route_found?(route)
22
- request_method = @env["REQUEST_METHOD"]
22
+ request_method = @env["REQUEST_METHOD"] || "GET"
23
23
  actual_path = @env["PATH_INFO"].sub(/^\//,'') # remove beginning slash
24
24
 
25
25
  # Immediately stop checking when the request method: GET, POST, ANY, etc
@@ -0,0 +1,46 @@
1
+ # All roads lead here
2
+ #
3
+ # 1. AWS Lambda: PostsController - Rack::Adapter - Jets.application.call
4
+ # 2. Local server: config.ru - run Jet.application - Jets.application.call
5
+ #
6
+ # Then eventually:
7
+ #
8
+ # Jets.application.call - Middleware stack - Jets::Controller::Middleware::Main
9
+ #
10
+ module Jets::Controller::Middleware
11
+ class Main
12
+ def initialize(env)
13
+ @env = env
14
+ @controller = env['jets.controller']
15
+ @event = env['lambda.event']
16
+ @context = env['lambda.context']
17
+ @meth = env['lambda.meth']
18
+ end
19
+
20
+ def call
21
+ dup.call!
22
+ end
23
+
24
+ def call!
25
+ setup
26
+ @controller.dispatch! # Returns triplet
27
+ end
28
+
29
+ # Common setup logical at this point of middleware processing right before
30
+ # calling any controller actions.
31
+ def setup
32
+ # We already recreated a mimicke rack env earlier as part of the very first
33
+ # middleware layer. However, by the time the rack env reaches the main middleware
34
+ # it could had been updated by other middlewares. We update the env here again.
35
+ @controller.request.set_env!(@env)
36
+ # This allows sesison helpers to work. Sessions are managed by
37
+ # the Rack::Session::Cookie middleware by default.
38
+ @controller.session = @env['rack.session'] || {}
39
+ end
40
+
41
+ def self.call(env)
42
+ instance = new(env)
43
+ instance.call
44
+ end
45
+ end
46
+ end
@@ -4,4 +4,3 @@ require "webpacker"
4
4
  require "webpacker/dev_server_proxy"
5
5
 
6
6
  Webpacker.bootstrap # whenever jets server is runs should Webpacker.bootstrap
7
-
@@ -1,4 +1,5 @@
1
1
  require "action_controller/metal/strong_parameters"
2
+ require "rack"
2
3
 
3
4
  class Jets::Controller
4
5
  module Params
@@ -39,7 +40,7 @@ class Jets::Controller
39
40
  # API Gateway seems to use either: content-type or Content-Type
40
41
  content_type = headers["content-type"]
41
42
  if content_type.to_s.include?("application/x-www-form-urlencoded")
42
- return Rack::Utils.parse_nested_query(body)
43
+ return ::Rack::Utils.parse_nested_query(body)
43
44
  end
44
45
 
45
46
  {} # fallback to empty Hash
@@ -0,0 +1,5 @@
1
+ module Jets::Controller::Rack
2
+ autoload :Adapter, "jets/controller/rack/adapter"
3
+ autoload :Env, "jets/controller/rack/env"
4
+ autoload :Middleware, "jets/controller/rack/middleware"
5
+ end
@@ -0,0 +1,60 @@
1
+ module Jets::Controller::Rack
2
+ class Adapter
3
+ extend Memoist
4
+
5
+ # Returns back API Gateway response hash structure
6
+ def self.process(event, context, meth)
7
+ adapter = new(event, context, meth)
8
+ adapter.process
9
+ end
10
+
11
+ def initialize(event, context, meth)
12
+ @event, @context, @meth = event, context, meth
13
+ end
14
+
15
+ # 1. Convert API Gateway event event to Rack env
16
+ # 2. Process using full Rack middleware stack
17
+ # 3. Convert back to API gateway response structure payload
18
+ def process
19
+ status, headers, body = Jets.application.call(env)
20
+ convert_to_api_gateway(status, headers, body)
21
+ end
22
+
23
+ def env
24
+ Env.new(@event, @context).convert # convert to Rack env
25
+ end
26
+ memoize :env
27
+
28
+ # Transform the structure to AWS_PROXY compatiable structure
29
+ # http://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format
30
+ def convert_to_api_gateway(status, headers, body)
31
+ base64 = headers["x-jets-base64"] == 'true'
32
+ body = body.respond_to?(:read) ? body.read : body
33
+ {
34
+ "statusCode" => status,
35
+ "headers" => headers,
36
+ "body" => body,
37
+ "isBase64Encoded" => base64,
38
+ }
39
+ end
40
+
41
+ # Called from Jets::Controller::Base.process. Example:
42
+ #
43
+ # adapter.rack_vars(
44
+ # 'jets.controller' => self,
45
+ # 'lambda.context' => context,
46
+ # 'lambda.event' => event,
47
+ # 'lambda.meth' => meth,
48
+ # )
49
+ #
50
+ # Passes a these special variables so we have access to them in the middleware.
51
+ # The controller instance is called in the Main middleware.
52
+ # The lambda.* info is used by the Rack::Local middleware to create a mimicked
53
+ # controller for the local server.
54
+ #
55
+ def rack_vars(vars)
56
+ env.merge!(vars)
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,96 @@
1
+ require 'rack'
2
+
3
+ # Takes an ApiGateway event and converts it to an Rack env that can be used for
4
+ # rack.call(env).
5
+ module Jets::Controller::Rack
6
+ class Env
7
+ def initialize(event, context)
8
+ @event, @context = event, context
9
+ end
10
+
11
+ def convert
12
+ options = {}
13
+ options = add_top_level(options)
14
+ options = add_http_headers(options)
15
+ path = @event['path'] || '/' # always set by API Gateway but might not be when testing shim, so setting it to make testing easier
16
+ Rack::MockRequest.env_for(path, options)
17
+ end
18
+
19
+ private
20
+ def add_top_level(options)
21
+ map = {
22
+ 'CONTENT_TYPE' => content_type,
23
+ 'QUERY_STRING' => query_string,
24
+ 'REMOTE_ADDR' => headers['X-Forwarded-For'],
25
+ 'REMOTE_HOST' => headers['Host'],
26
+ 'REQUEST_METHOD' => @event['httpMethod'],
27
+ 'REQUEST_PATH' => @event['path'],
28
+ 'REQUEST_URI' => request_uri,
29
+ 'SCRIPT_NAME' => "",
30
+ 'SERVER_NAME' => headers['Host'],
31
+ 'SERVER_PORT' => headers['X-Forwarded-Port'],
32
+ 'SERVER_PROTOCOL' => "HTTP/1.1", # unsure if this should be set
33
+ 'SERVER_SOFTWARE' => "WEBrick/1.3.1 (Ruby/2.2.2/2015-04-13)",
34
+ }
35
+
36
+ map['CONTENT_LENGTH'] = content_length if content_length
37
+ # Even if not set, Rack always assigns an StringIO to "rack.input"
38
+ map[:input] = StringIO.new(body) if body
39
+
40
+ # TODO: handle decoding base64 encoded body from API Gateaway
41
+ # Will need to make sure that pass the base64 info via a request header
42
+
43
+ options.merge(map)
44
+ end
45
+
46
+ def content_type
47
+ headers['Content-Type'] || Jets::Controller::DEFAULT_CONTENT_TYPE
48
+ end
49
+
50
+ def content_length
51
+ bytesize = body.bytesize.to_s if body
52
+ headers['Content-Length'] || bytesize
53
+ end
54
+
55
+ def body
56
+ @event['body']
57
+ end
58
+
59
+ def add_http_headers(options)
60
+ headers.each do |k,v|
61
+ # content-type => HTTP_CONTENT_TYPE
62
+ key = k.gsub('-','_').upcase
63
+ key = "HTTP_#{key}"
64
+ options[key] = v
65
+ end
66
+ options
67
+ end
68
+
69
+ def request_uri
70
+ # IE: "http://localhost:8888/posts/tung/edit?foo=bar"
71
+ proto = headers['X-Forwarded-Proto']
72
+ host = headers['Host']
73
+ port = headers['X-Forwarded-Port']
74
+
75
+ # Add port if needed
76
+ if proto == 'https' && port != '443' or
77
+ proto == 'http' && port != '80'
78
+ host = "#{host}:#{port}"
79
+ end
80
+
81
+ path = @event['path']
82
+ qs = "?#{query_string}" unless query_string.empty?
83
+ "#{proto}://#{host}#{path}#{qs}"
84
+ end
85
+
86
+ def query_string
87
+ qs_params = @event["queryStringParameters"] || {} # always set with API Gateway but when testing node shim might not be
88
+ hash = Jets::Mega::HashConverter.encode(qs_params)
89
+ hash.to_query
90
+ end
91
+
92
+ def headers
93
+ @event['headers'] || {}
94
+ end
95
+ end
96
+ end