jets 1.1.5 → 1.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 (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)