jets 1.1.5 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/Gemfile.lock +10 -6
  4. data/README/testing.md +5 -1
  5. data/jets.gemspec +1 -0
  6. data/lib/jets.rb +5 -1
  7. data/lib/jets/application.rb +39 -19
  8. data/lib/jets/aws_services.rb +16 -10
  9. data/lib/jets/aws_services/stack_status.rb +7 -0
  10. data/lib/jets/booter.rb +6 -2
  11. data/lib/jets/builders/code_builder.rb +14 -0
  12. data/lib/jets/builders/handler_generator.rb +15 -0
  13. data/lib/jets/builders/shim_vars/app.rb +4 -3
  14. data/lib/jets/builders/shim_vars/shared.rb +8 -4
  15. data/lib/jets/builders/templates/shim.js +7 -3
  16. data/lib/jets/camelizer.rb +2 -1
  17. data/lib/jets/cfn/builders.rb +0 -1
  18. data/lib/jets/cfn/builders/api_deployment_builder.rb +27 -0
  19. data/lib/jets/cfn/builders/api_gateway_builder.rb +22 -2
  20. data/lib/jets/cfn/ship.rb +38 -6
  21. data/lib/jets/commands/call.rb +0 -1
  22. data/lib/jets/commands/call/guesser.rb +0 -3
  23. data/lib/jets/commands/clean/log.rb +18 -0
  24. data/lib/jets/commands/console.rb +1 -1
  25. data/lib/jets/commands/import/sequence.rb +2 -3
  26. data/lib/jets/commands/runner.rb +1 -1
  27. data/lib/jets/commands/sequence.rb +0 -1
  28. data/lib/jets/commands/templates/skeleton/config/application.rb.tt +11 -0
  29. data/lib/jets/commands/url.rb +32 -7
  30. data/lib/jets/controller/base.rb +21 -5
  31. data/lib/jets/controller/layout.rb +0 -3
  32. data/lib/jets/controller/middleware/local/api_gateway.rb +2 -5
  33. data/lib/jets/controller/middleware/local/mimic_aws_call.rb +2 -2
  34. data/lib/jets/controller/params.rb +42 -10
  35. data/lib/jets/controller/rack/adapter.rb +5 -2
  36. data/lib/jets/controller/rack/env.rb +17 -8
  37. data/lib/jets/controller/renderers/rack_renderer.rb +1 -1
  38. data/lib/jets/controller/rendering.rb +4 -1
  39. data/lib/jets/core.rb +8 -16
  40. data/lib/jets/internal/app/functions/jets/base_path.rb +153 -0
  41. data/lib/jets/klass.rb +38 -5
  42. data/lib/jets/lambda/dsl.rb +0 -2
  43. data/lib/jets/mega/request.rb +44 -13
  44. data/lib/jets/mega/request/source.rb +21 -0
  45. data/lib/jets/middleware/configurator.rb +1 -1
  46. data/lib/jets/middleware/default_stack.rb +2 -2
  47. data/lib/jets/resource.rb +1 -0
  48. data/lib/jets/resource/api_gateway.rb +5 -3
  49. data/lib/jets/resource/api_gateway/base_path.rb +5 -0
  50. data/lib/jets/resource/api_gateway/base_path/function.rb +42 -0
  51. data/lib/jets/resource/api_gateway/base_path/mapping.rb +44 -0
  52. data/lib/jets/resource/api_gateway/base_path/role.rb +76 -0
  53. data/lib/jets/resource/api_gateway/cors.rb +1 -1
  54. data/lib/jets/resource/api_gateway/deployment.rb +9 -5
  55. data/lib/jets/resource/api_gateway/domain_name.rb +56 -0
  56. data/lib/jets/resource/api_gateway/method.rb +3 -4
  57. data/lib/jets/resource/api_gateway/resource.rb +4 -3
  58. data/lib/jets/resource/api_gateway/rest_api.rb +42 -14
  59. data/lib/jets/resource/api_gateway/rest_api/change_detection.rb +42 -0
  60. data/lib/jets/resource/api_gateway/rest_api/logical_id.rb +59 -0
  61. data/lib/jets/resource/api_gateway/rest_api/routes.rb +127 -0
  62. data/lib/jets/resource/child_stack/api_deployment.rb +5 -1
  63. data/lib/jets/resource/function.rb +3 -20
  64. data/lib/jets/resource/function/environment.rb +23 -0
  65. data/lib/jets/resource/iam/application_role.rb +1 -1
  66. data/lib/jets/resource/route53.rb +3 -0
  67. data/lib/jets/resource/route53/record_set.rb +70 -0
  68. data/lib/jets/router.rb +2 -0
  69. data/lib/jets/ruby_server.rb +6 -3
  70. data/lib/jets/stack.rb +1 -3
  71. data/lib/jets/stack/main/dsl.rb +1 -1
  72. data/lib/jets/stack/main/extensions/lambda.rb +4 -2
  73. data/lib/jets/turbine.rb +0 -3
  74. data/lib/jets/version.rb +1 -1
  75. data/vendor/jets-gems/lib/jets/gems.rb +1 -0
  76. data/vendor/jets-gems/lib/jets/gems/agree.rb +41 -0
  77. data/vendor/jets-gems/lib/jets/gems/check.rb +15 -2
  78. metadata +30 -2
@@ -1,3 +1,5 @@
1
+ require 'base64'
2
+
1
3
  module Jets::Controller::Rack
2
4
  class Adapter
3
5
  extend Memoist
@@ -21,15 +23,16 @@ module Jets::Controller::Rack
21
23
  end
22
24
 
23
25
  def env
24
- Env.new(@event, @context).convert # convert to Rack env
26
+ Env.new(@event, @context, adapter: true).convert # convert to Rack env
25
27
  end
26
28
  memoize :env
27
29
 
28
30
  # Transform the structure to AWS_PROXY compatiable structure
29
31
  # http://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format
30
32
  def convert_to_api_gateway(status, headers, body)
31
- base64 = headers["x-jets-base64"] == 'true'
33
+ base64 = headers["x-jets-base64"] == 'yes'
32
34
  body = body.respond_to?(:read) ? body.read : body
35
+ body = Base64.encode64(body) if base64
33
36
  {
34
37
  "statusCode" => status,
35
38
  "headers" => headers,
@@ -1,11 +1,12 @@
1
1
  require 'rack'
2
+ require 'base64'
2
3
 
3
4
  # Takes an ApiGateway event and converts it to an Rack env that can be used for
4
5
  # rack.call(env).
5
6
  module Jets::Controller::Rack
6
7
  class Env
7
- def initialize(event, context)
8
- @event, @context = event, context
8
+ def initialize(event, context, options={})
9
+ @event, @context, @options = event, context, options
9
10
  end
10
11
 
11
12
  def convert
@@ -13,7 +14,12 @@ module Jets::Controller::Rack
13
14
  options = add_top_level(options)
14
15
  options = add_http_headers(options)
15
16
  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
+ env = Rack::MockRequest.env_for(path, options)
18
+ if @options[:adapter]
19
+ env['adapter.event'] = @event
20
+ env['adapter.context'] = @context
21
+ end
22
+ env
17
23
  end
18
24
 
19
25
  private
@@ -35,10 +41,7 @@ module Jets::Controller::Rack
35
41
 
36
42
  map['CONTENT_LENGTH'] = content_length if content_length
37
43
  # 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
44
+ map['rack.input'] = StringIO.new(body) if body
42
45
 
43
46
  options.merge(map)
44
47
  end
@@ -52,8 +55,14 @@ module Jets::Controller::Rack
52
55
  headers['Content-Length'] || bytesize
53
56
  end
54
57
 
58
+ # Decoding base64 from API Gateaway if necessary
59
+ # Rack will be none the wiser
55
60
  def body
56
- @event['body']
61
+ if @event['isBase64Encoded']
62
+ Base64.decode64(@event['body'])
63
+ else
64
+ @event['body']
65
+ end
57
66
  end
58
67
 
59
68
  def add_http_headers(options)
@@ -17,7 +17,7 @@ module Jets::Controller::Renderers
17
17
  headers = cors_headers.merge(headers)
18
18
  headers["Content-Type"] ||= @options[:content_type] || Jets::Controller::DEFAULT_CONTENT_TYPE
19
19
  # x-jets-base64 to convert this Rack triplet to a API Gateway hash structure later
20
- headers["x-jets-base64"] = base64 ? "true" : "false"
20
+ headers["x-jets-base64"] = base64 ? 'yes' : 'no' # headers values must be Strings
21
21
  body = StringIO.new(body)
22
22
  [status, headers, body] # triplet
23
23
  end
@@ -71,7 +71,10 @@ class Jets::Controller
71
71
  end
72
72
 
73
73
  def url_for(url)
74
- add_stage_name(url)
74
+ # No longer need to add stage name, think this is due to rack middleware support
75
+ # Leaving in as comment for now just in case.
76
+ # add_stage_name(url)
77
+ url
75
78
  end
76
79
 
77
80
  def actual_host
@@ -1,22 +1,9 @@
1
- require 'active_support/dependencies'
2
- require 'memoist'
3
-
4
1
  module Jets::Core
5
2
  extend Memoist
6
3
 
7
- # Calling application triggers load of configs.
8
- # Jets' the default config/application.rb is loaded,
9
- # then the project's config/application.rb is loaded.
10
- @@application = nil
11
4
  def application
12
- return @@application if @@application
13
-
14
- @@application = Jets::Application.instance
15
- @@application.setup!
16
- @@application
5
+ Jets::Application.instance
17
6
  end
18
- # For some reason memoize doesnt work with application, think there's
19
- # some circular dependency issue. Figure this out later.
20
7
 
21
8
  def config
22
9
  application.config
@@ -123,7 +110,7 @@ module Jets::Core
123
110
  def eager_load_app
124
111
  Dir.glob("#{Jets.root}app/**/*.rb").select do |path|
125
112
  next if !File.file?(path) or path =~ %r{/javascript/} or path =~ %r{/views/}
126
- next if path.include?('app/functions') || path.include?('app/shared/functions')
113
+ next if path.include?('app/functions') || path.include?('app/shared/functions') || path.include?('app/internal/functions')
127
114
 
128
115
  class_name = path
129
116
  .sub(/\.rb$/,'') # remove .rb
@@ -177,9 +164,14 @@ module Jets::Core
177
164
 
178
165
  def report_exception(exception)
179
166
  Jets::Turbine.subclasses.each do |subclass|
180
- subclass.exception_reporters.each do |label, block|
167
+ reporters = subclass.exception_reporters || []
168
+ reporters.each do |label, block|
181
169
  block.call(exception)
182
170
  end
183
171
  end
184
172
  end
173
+
174
+ def custom_domain?
175
+ Jets.config.domain.hosted_zone_name
176
+ end
185
177
  end
@@ -0,0 +1,153 @@
1
+ require 'aws-sdk-apigateway'
2
+ require 'aws-sdk-cloudformation'
3
+
4
+ STAGE_NAME = "<%= @stage_name %>"
5
+
6
+ def lambda_handler(event:, context:)
7
+ puts("event['RequestType'] #{event['RequestType']}")
8
+ puts("event: #{JSON.dump(event)}")
9
+ puts("context: #{JSON.dump(context)}")
10
+ puts("context.log_stream_name #{context.log_stream_name.inspect}")
11
+
12
+ mimic = event['ResourceProperties']['Mimic']
13
+ physical_id = event['ResourceProperties']['PhysicalId'] || "PhysicalId"
14
+
15
+ puts "mimic: #{mimic}"
16
+ puts "physical_id: #{physical_id}"
17
+
18
+ if event['RequestType'] == 'Delete'
19
+ if mimic == 'FAILED'
20
+ send_response(event, context, "FAILED")
21
+ else
22
+ mapping = BasePathMapping.new(event)
23
+ mapping.delete(true) if mapping.should_delete?
24
+ send_response(event, context, "SUCCESS")
25
+ end
26
+ return # early return
27
+ end
28
+
29
+ mapping = BasePathMapping.new(event)
30
+ mapping.update
31
+
32
+ response_status = mimic == "FAILED" ? "FAILED" : "SUCCESS"
33
+ response_data = { "Hello" => "World" }
34
+
35
+ send_response(event, context, response_status, response_data, physical_id)
36
+
37
+ # We rescue all exceptions and send an message to CloudFormation so we dont have to
38
+ # wait for over an hour for the stack operation to timeout and rollback.
39
+ rescue Exception => e
40
+ puts e.message
41
+ puts e.backtrace
42
+ sleep 10 # provide delete to make sure that the log gets sent to CloudWatch
43
+ send_response(event, context, "FAILED")
44
+ end
45
+
46
+ def send_response(event, context, response_status, response_data={}, physical_id="PhysicalId")
47
+ response_body = JSON.dump(
48
+ Status: response_status,
49
+ Reason: "See the details in CloudWatch Log Stream: #{context.log_stream_name.inspect}",
50
+ PhysicalResourceId: physical_id,
51
+ StackId: event['StackId'],
52
+ RequestId: event['RequestId'],
53
+ LogicalResourceId: event['LogicalResourceId'],
54
+ Data: response_data
55
+ )
56
+
57
+ puts "RESPONSE BODY:\n"
58
+ puts response_body
59
+
60
+ url = event['ResponseURL']
61
+ uri = URI(url)
62
+ http = Net::HTTP.new(uri.host, uri.port)
63
+ http.open_timeout = http.read_timeout = 30
64
+ http.use_ssl = true if uri.scheme == 'https'
65
+
66
+
67
+ # must used url to include the AWSAccessKeyId and Signature
68
+ req = Net::HTTP::Put.new(url) # url includes query string and uri.path does not, must used url t
69
+ req.body = response_body
70
+ req.content_length = response_body.bytesize
71
+
72
+ # set headers
73
+ req['content-type'] = ''
74
+ req['content-length'] = response_body.bytesize
75
+
76
+ res = http.request(req)
77
+ puts "status code: #{res.code}"
78
+ puts "headers: #{res.each_header.to_h.inspect}"
79
+ puts "body: #{res.body}"
80
+ end
81
+
82
+
83
+ class BasePathMapping
84
+ def initialize(event)
85
+ @event = event
86
+ @rest_api_id = get_rest_api_id
87
+ @domain_name = get_domain_name
88
+ @base_path = ''
89
+ end
90
+
91
+ def update
92
+ # Cannot use update_base_path_mapping to update the base_mapping because it doesnt
93
+ # allow us to change the rest_api_id. So we delete and create.
94
+ delete(true)
95
+ create
96
+ end
97
+
98
+ # Dont delete the newly created base path mapping unless this is an operation
99
+ # where we're fully deleting the stack
100
+ def should_delete?
101
+ deleting_parent?
102
+ end
103
+
104
+ def delete(fail_silently=false)
105
+ apigateway.delete_base_path_mapping(
106
+ domain_name: @domain_name, # required
107
+ base_path: '(none)',
108
+ )
109
+ rescue Aws::APIGateway::Errors::NotFoundException => e
110
+ raise(e) unless fail_silently
111
+ end
112
+
113
+ def create
114
+ apigateway.create_base_path_mapping(
115
+ domain_name: @domain_name, # required
116
+ base_path: @base_path,
117
+ rest_api_id: @rest_api_id, # required
118
+ stage: STAGE_NAME,
119
+ )
120
+ end
121
+
122
+ def get_domain_name
123
+ param = deployment_stack[:parameters].find { |p| p.parameter_key == 'DomainName' }
124
+ param.parameter_value
125
+ end
126
+
127
+ def deployment_stack
128
+ @deployment_stack ||= cfn.describe_stacks(stack_name: @event['StackId']).stacks.first
129
+ end
130
+
131
+ def get_rest_api_id
132
+ param = deployment_stack[:parameters].find { |p| p.parameter_key == 'RestApi' }
133
+ param.parameter_value
134
+ end
135
+
136
+ def deleting_parent?
137
+ stack = cfn.describe_stacks(stack_name: parent_stack_name).stacks.first
138
+ stack.stack_status == 'DELETE_IN_PROGRESS'
139
+ end
140
+
141
+ def parent_stack_name
142
+ deployment_stack[:root_id]
143
+ end
144
+
145
+ private
146
+ def apigateway
147
+ @apigateway ||= Aws::APIGateway::Client.new
148
+ end
149
+
150
+ def cfn
151
+ @cfn ||= Aws::CloudFormation::Client.new
152
+ end
153
+ end
@@ -28,12 +28,12 @@ class Jets::Klass
28
28
  # app/controllers, app/jobs, and app/functions.
29
29
  def from_path(path)
30
30
  class_name = class_name(path)
31
-
32
31
  if path.include?("/functions/") # simple function
33
- load_anonymous_class(class_name, path)
32
+ class_name = load_anonymous_class(class_name, path)
33
+ class_name.constantize # removed :: for anonymous classes
34
+ else
35
+ class_name.constantize # autoload
34
36
  end
35
-
36
- class_name.constantize # autoload or nothing if load_anonymous_class called
37
37
  end
38
38
 
39
39
  # app/controllers/posts_controller.rb => PostsController
@@ -61,15 +61,48 @@ class Jets::Klass
61
61
 
62
62
  @@loaded_anonymous_classes = []
63
63
  def load_anonymous_class(class_name, path)
64
+ parent_mod = modularize(class_name)
65
+
64
66
  constructor = Jets::Lambda::FunctionConstructor.new(path)
65
67
  # Dont load anonyomous class more than once to avoid these warnings:
66
68
  # warning: already initialized constant Hello
67
69
  # warning: previous definition of Hello was here
68
70
  unless @@loaded_anonymous_classes.include?(class_name)
69
71
  # use class_name as the variable name for prettier class name.
70
- Object.const_set(class_name, constructor.build)
72
+ leaf_class_name = class_name.split('::').last
73
+ parent_mod.const_set(leaf_class_name, constructor.build)
71
74
  @@loaded_anonymous_classes << class_name
72
75
  end
76
+
77
+ class_name
78
+ end
79
+
80
+ # Ensures the parent namespace modules are defined. Example:
81
+ #
82
+ # modularize("Foo::Bar::Test")
83
+ # => Foo::Bar # is a now defined as a module if it wasnt before
84
+ #
85
+ # Also returns the parent module, so we can use it to do a const_set if needed. IE:
86
+ #
87
+ # parent_mod = modularize("Foo::Bar::Test")
88
+ # parent_mod.const_set("Test")
89
+ def modularize(class_name)
90
+ leaves = []
91
+ mods = class_name.split('::')[0..-2] # drop the last word
92
+ # puts "mods: #{mods}"
93
+ return Object if mods.empty?
94
+
95
+ leaves = []
96
+ mods.each do |leaf_mod|
97
+ leaves += [leaf_mod]
98
+ namespace = leaves.join('::')
99
+ previous_namespace = leaves[0..-2].join('::')
100
+ previous_namespace = "Object" if previous_namespace.empty?
101
+ previous_namespace = previous_namespace.constantize
102
+ previous_namespace.const_set(leaf_mod, Module.new) unless Object.const_defined?(namespace)
103
+ end
104
+
105
+ mods.join('::').constantize
73
106
  end
74
107
 
75
108
  end
@@ -1,5 +1,3 @@
1
- require 'active_support/concern'
2
-
3
1
  # Other dsl that rely on this must implement
4
2
  #
5
3
  # default_associated_resource_definition
@@ -3,6 +3,10 @@ require 'rack'
3
3
 
4
4
  module Jets::Mega
5
5
  class Request
6
+ autoload :Source, 'jets/mega/request/source'
7
+
8
+ extend Memoist
9
+
6
10
  def initialize(event, controller)
7
11
  @event = event
8
12
  @controller = controller # Jets::Controller instance
@@ -12,10 +16,10 @@ module Jets::Mega
12
16
  http_method = @event['httpMethod'] # GET, POST, PUT, DELETE, etc
13
17
  params = @controller.params(raw: true, path_parameters: false)
14
18
 
15
- uri = URI("http://localhost:9292#{@controller.request.path}") # local rack server
19
+ uri = get_uri
20
+
16
21
  http = Net::HTTP.new(uri.host, uri.port)
17
- http.open_timeout = 60
18
- http.read_timeout = 60
22
+ http.open_timeout = http.read_timeout = 60
19
23
 
20
24
  # Rails sets _method=patch or _method=put as workaround
21
25
  # Falls back to GET when testing in lambda console
@@ -24,25 +28,23 @@ module Jets::Mega
24
28
 
25
29
  request_class = "Net::HTTP::#{http_class}".constantize # IE: Net::HTTP::Get
26
30
  request = request_class.new(uri.path)
31
+
32
+ # Set form data
27
33
  if %w[Post Patch Put].include?(http_class)
28
34
  params = HashConverter.encode(params)
29
35
  request.set_form_data(params)
30
36
  end
31
37
 
32
- request = set_headers!(request)
38
+ # Set body info
39
+ request.body = source.body
40
+ request.content_length = source.content_length
33
41
 
34
- # Setup body
35
- env = Jets::Controller::Rack::Env.new(@event, {}).convert # convert to Rack env
36
- source_request = Rack::Request.new(env)
37
- if source_request.body.respond_to?(:read)
38
- request.body = source_request.body.read
39
- request.content_length = source_request.content_length.to_i
40
- source_request.body.rewind
41
- end
42
+ # Need to set headers after body and form_data for some reason
43
+ request = set_headers!(request)
42
44
 
45
+ # Make request
43
46
  response = http.request(request)
44
47
 
45
- # TODO: handle binary
46
48
  {
47
49
  status: response.code.to_i,
48
50
  headers: response.each_header.to_h,
@@ -50,6 +52,35 @@ module Jets::Mega
50
52
  }
51
53
  end
52
54
 
55
+ def get_uri
56
+ url = "http://localhost:9292#{@controller.request.path}" # local rack server
57
+ unless @controller.query_parameters.empty?
58
+ # Thanks: https://stackoverflow.com/questions/798710/ruby-how-to-turn-a-hash-into-http-parameters
59
+ query_string = Rack::Utils.build_nested_query(@controller.query_parameters)
60
+ url += "?#{query_string}"
61
+ end
62
+ URI(url)
63
+ end
64
+
65
+ def source
66
+ Source.new(@event)
67
+ end
68
+ memoize :source
69
+
70
+ # Rails sets _method=patch or _method=put as workaround
71
+ # Falls back to GET when testing in lambda console
72
+ # @event['httpMethod'] is GET, POST, PUT, DELETE, etc
73
+ def http_class
74
+ http_class = params['_method'] || @event['httpMethod'] || 'GET'
75
+ http_class.capitalize!
76
+ http_class
77
+ end
78
+
79
+ def params
80
+ @controller.params(raw: true, path_parameters: false, body_parameters: true)
81
+ end
82
+ memoize :params
83
+
53
84
  # Set request headers. Forwards original request info from remote API gateway.
54
85
  # By this time, the server/api_gateway.rb middleware.
55
86
  def set_headers!(request)