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
@@ -0,0 +1,21 @@
1
+ class Jets::Mega::Request
2
+ class Source
3
+ def initialize(event)
4
+ @event = event
5
+ env = Jets::Controller::Rack::Env.new(@event, {}).convert # convert to Rack env
6
+ @source_request = Rack::Request.new(env)
7
+ end
8
+
9
+ def body
10
+ if @source_request.body.respond_to?(:read)
11
+ body = @source_request.body.read
12
+ @source_request.body.rewind
13
+ end
14
+ body
15
+ end
16
+
17
+ def content_length
18
+ @source_request.content_length.to_i
19
+ end
20
+ end
21
+ end
@@ -1,6 +1,6 @@
1
1
  # Based on Rails MiddlewareStackProxy
2
2
  #
3
- # Configurator is a proxy for the Jets middleware stack that allows
3
+ # Configurator is a recorder for the Jets middleware stack that allows
4
4
  # you to configure middlewares in your application. It works basically as a
5
5
  # command recorder, saving each command to be applied after initialization
6
6
  # over the default middleware stack, so you can add, swap, or remove any
@@ -8,9 +8,9 @@ module Jets::Middleware
8
8
 
9
9
  def build_stack
10
10
  Stack.new do |middleware|
11
- middleware.use Jets::Controller::Middleware::Local # mimics AWS Lambda for local server only
12
11
  middleware.use Rack::Runtime
13
- middleware.use Rack::MethodOverride
12
+ middleware.use Rack::MethodOverride # must come before Middleware::Local for multipart post forms to work
13
+ middleware.use Jets::Controller::Middleware::Local # mimics AWS Lambda for local server only
14
14
  middleware.use session_store, session_options # use session_store, session_options
15
15
  middleware.use Rack::Head
16
16
  middleware.use Rack::ConditionalGet
@@ -11,6 +11,7 @@ class Jets::Resource
11
11
  autoload :Iam, 'jets/resource/iam'
12
12
  autoload :Permission, 'jets/resource/permission'
13
13
  autoload :Replacer, 'jets/resource/replacer'
14
+ autoload :Route53, 'jets/resource/route53'
14
15
  autoload :S3, 'jets/resource/s3'
15
16
  autoload :Sns, 'jets/resource/sns'
16
17
  autoload :Standardizer, 'jets/resource/standardizer'
@@ -1,7 +1,9 @@
1
1
  module Jets::Resource::ApiGateway
2
- autoload :Resource, 'jets/resource/api_gateway/resource'
3
- autoload :RestApi, 'jets/resource/api_gateway/rest_api'
2
+ autoload :BasePath, 'jets/resource/api_gateway/base_path'
3
+ autoload :Cors, 'jets/resource/api_gateway/cors'
4
4
  autoload :Deployment, 'jets/resource/api_gateway/deployment'
5
+ autoload :DomainName, 'jets/resource/api_gateway/domain_name'
5
6
  autoload :Method, 'jets/resource/api_gateway/method'
6
- autoload :Cors, 'jets/resource/api_gateway/cors'
7
+ autoload :Resource, 'jets/resource/api_gateway/resource'
8
+ autoload :RestApi, 'jets/resource/api_gateway/rest_api'
7
9
  end
@@ -0,0 +1,5 @@
1
+ module Jets::Resource::ApiGateway::BasePath
2
+ autoload :Function, 'jets/resource/api_gateway/base_path/function'
3
+ autoload :Mapping, 'jets/resource/api_gateway/base_path/mapping'
4
+ autoload :Role, 'jets/resource/api_gateway/base_path/role'
5
+ end
@@ -0,0 +1,42 @@
1
+ module Jets::Resource::ApiGateway::BasePath
2
+ class Function < Jets::Resource::Base
3
+ include Jets::Resource::Function::Environment
4
+
5
+ def definition
6
+ {
7
+ base_path_function: {
8
+ type: "AWS::Lambda::Function",
9
+ properties: {
10
+ function_name: function_name,
11
+ code: {
12
+ s3_bucket: "!Ref S3Bucket",
13
+ s3_key: code_s3_key,
14
+ },
15
+ role: "!GetAtt BasePathRole.Arn",
16
+ handler: handler,
17
+ runtime: "ruby2.5",
18
+ timeout: 60,
19
+ memory_size: 1536,
20
+ environment: env_properties[:environment],
21
+ }
22
+ }
23
+ }
24
+ end
25
+
26
+ def function_name
27
+ method = "jets-base-path"
28
+ # need to add the deployment timestamp because or else function name collides between deploys
29
+ timestamp = Jets::Resource::ApiGateway::Deployment.timestamp
30
+ "#{Jets.config.project_namespace}-#{method}-#{timestamp}"
31
+ end
32
+
33
+ def handler
34
+ "handlers/functions/jets/base_path.lambda_handler"
35
+ end
36
+
37
+ def code_s3_key
38
+ checksum = Jets::Builders::Md5.checksums["stage/code"]
39
+ "jets/code/code-#{checksum}.zip" # s3_key
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,44 @@
1
+ # CloudFormation Docs AWS::ApiGateway::DomainName: https://amzn.to/2BsrSqo
2
+ #
3
+ # Example:
4
+ #
5
+ # Type: AWS::ApiGateway::BasePathMapping
6
+ # Properties:
7
+ # BasePath: String
8
+ # DomainName: String
9
+ # RestApiId: String
10
+ # Stage: String
11
+ #
12
+ # Currently unable to add base path mapping in-place with CloudFormation.
13
+ # The workaround for this is to do it post deployment with raw API calls outside
14
+ # of CloudFormation. Leaving this around for now in case there's a workaround
15
+ # to get this into CloudFormation instead of raw API calls. Some notes:
16
+ # * Also tried to change the domain name of to something like demo-dev-[random].mydomain.com
17
+ # That does not work because the domain name has to match the route53 record exactly.
18
+ #
19
+ module Jets::Resource::ApiGateway::BasePath
20
+ class Mapping < Jets::Resource::Base
21
+ def definition
22
+ function_logical_id = "BasePathFunction" # lambda function that supports custom resource
23
+ {
24
+ base_path_mapping: {
25
+ type: "Custom::BasePathMapping",
26
+ properties: {
27
+ service_token: "!GetAtt #{function_logical_id}.Arn",
28
+ # base_path: '', # empty path represents root
29
+ # domain_name: "!Ref DomainName",
30
+ # rest_api_id: "!Ref RestApi", # since this is in the Deployment template
31
+ # stage: Deployment.stage_name,
32
+ },
33
+ depends_on: Jets::Resource::ApiGateway::Deployment.logical_id,
34
+ }
35
+ }
36
+ end
37
+
38
+ def outputs
39
+ {
40
+ "BasePathMapping" => "!Ref BasePathMapping",
41
+ }
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,76 @@
1
+ module Jets::Resource::ApiGateway::BasePath
2
+ class Role < Jets::Resource::Base
3
+ extend Memoist
4
+ include Jets::AwsServices
5
+
6
+ def definition
7
+ {
8
+ base_path_role: {
9
+ type: "AWS::IAM::Role",
10
+ properties: {
11
+ role_name: role_name,
12
+ path: "/",
13
+ assume_role_policy_document: {
14
+ version: "2012-10-17",
15
+ statement: [{
16
+ effect: "Allow",
17
+ principal: {service: ["lambda.amazonaws.com"]},
18
+ action: ["sts:AssumeRole"]}
19
+ ]
20
+ },
21
+ policies: [
22
+ policy_name: "#{role_name}-policy",
23
+ policy_document: policy_document,
24
+ ]
25
+ },
26
+ }
27
+ }
28
+ end
29
+
30
+ def policy_document
31
+ project_namespace = Jets.config.project_namespace
32
+ default_policy_statements = Jets::Application.default_iam_policy # Array of Hashes
33
+ apigateway = [{
34
+ action: [ "apigateway:*" ],
35
+ effect: "Allow",
36
+ resource: "arn:aws:apigateway:#{Jets.aws.region}::/restapis/*", # scoped to all restapis because this changes
37
+ },{
38
+ action: [ "apigateway:*" ],
39
+ effect: "Allow",
40
+ resource: "arn:aws:apigateway:#{Jets.aws.region}::/domainnames/*", # scoped to all restapis because this changes
41
+ }]
42
+ cloudformation = [{
43
+ action: ["cloudformation:DescribeStacks"],
44
+ effect: "Allow",
45
+ resource: "arn:aws:cloudformation:#{Jets.aws.region}:#{Jets.aws.account}:stack/#{project_namespace}*",
46
+ }]
47
+
48
+ # Combine the statements
49
+ {
50
+ version: '2012-10-17',
51
+ statement: default_policy_statements + apigateway + cloudformation
52
+ }
53
+ end
54
+
55
+ # Duplicated in rest_api/change_detection.rb, base_path/role.rb, rest_api/routes.rb
56
+ def rest_api_id
57
+ stack_name = Jets::Naming.parent_stack_name
58
+ return default unless stack_exists?(stack_name)
59
+
60
+ stack = cfn.describe_stacks(stack_name: stack_name).stacks.first
61
+
62
+ api_gateway_stack_arn = lookup(stack[:outputs], "ApiGateway")
63
+
64
+ # resources = cfn.describe_stack_resources(stack_name: api_gateway_stack_arn).stack_resources
65
+ stack = cfn.describe_stacks(stack_name: api_gateway_stack_arn).stacks.first
66
+ rest_api_id = lookup(stack[:outputs], "RestApi")
67
+ end
68
+ memoize :rest_api_id
69
+
70
+ def role_name
71
+ # TODO: dont think we should change the role name every time but have to right now because the deployment logical id changes
72
+ timestamp = Jets::Resource::ApiGateway::Deployment.timestamp
73
+ "#{Jets.config.project_namespace}-base-path-mapping-#{timestamp}"
74
+ end
75
+ end
76
+ end
@@ -9,7 +9,7 @@ module Jets::Resource::ApiGateway
9
9
 
10
10
  properties: {
11
11
  resource_id: "!Ref #{resource_id}",
12
- rest_api_id: "!Ref RestApi",
12
+ rest_api_id: "!Ref #{RestApi.logical_id}",
13
13
  authorization_type: authorization_type,
14
14
  http_method: "OPTIONS",
15
15
  method_responses: [{
@@ -6,23 +6,27 @@ module Jets::Resource::ApiGateway
6
6
  type: "AWS::ApiGateway::Deployment",
7
7
  properties: {
8
8
  description: "Version #{timestamp} deployed by jets",
9
- rest_api_id: "!Ref RestApi",
9
+ rest_api_id: "!Ref #{RestApi.logical_id}",
10
10
  stage_name: stage_name,
11
11
  }
12
12
  }
13
13
  }
14
14
  end
15
15
 
16
- # value is Description
17
16
  def parameters
18
- {
17
+ p = {
18
+ "IamRole" => "IamRole",
19
19
  "RestApi" => "RestApi",
20
+ "S3Bucket" => "S3Bucket",
20
21
  }
22
+ p[:DomainName] = "DomainName" if Jets.custom_domain?
23
+ p
21
24
  end
22
25
 
23
- def outputs
26
+ def outputs(internal=false)
27
+ rest_api = internal ? RestApi.internal_logical_id : "RestApi"
24
28
  {
25
- "RestApiUrl" => "!Sub 'https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/#{stage_name}/'",
29
+ "RestApiUrl" => "!Sub 'https://${#{rest_api}}.execute-api.${AWS::Region}.amazonaws.com/#{stage_name}/'",
26
30
  }
27
31
  end
28
32
 
@@ -0,0 +1,56 @@
1
+ # CloudFormation Docs AWS::ApiGateway::DomainName: https://amzn.to/2Bsnmbq
2
+ #
3
+ # Example:
4
+ #
5
+ # MyDomainName:
6
+ # Type: 'AWS::ApiGateway::DomainName'
7
+ # Properties:
8
+ # DomainName: api.mydomain.com
9
+ # CertificateArn: arn:aws:acm:us-east-1:111122223333:certificate/fb1b9770-a305-495d-aefb-27e5e101ff3
10
+ #
11
+ module Jets::Resource::ApiGateway
12
+ class DomainName < Jets::Resource::Base
13
+ def definition
14
+ properties = {
15
+ domain_name: domain_name,
16
+ endpoint_configuration: {
17
+ types: endpoint_types
18
+ }
19
+ }
20
+ # Can really only be REGIONAL or EDGE
21
+ if endpoint_types.include?("REGIONAL")
22
+ properties[:regional_certificate_arn] = cert_arn
23
+ end
24
+ if endpoint_types.include?("EDGE")
25
+ properties[:certificate_arn] = cert_arn
26
+ end
27
+
28
+ {
29
+ domain_name: {
30
+ type: "AWS::ApiGateway::DomainName",
31
+ properties: properties
32
+ }
33
+ }
34
+ end
35
+
36
+ def outputs
37
+ {
38
+ "DomainName" => "!Ref DomainName",
39
+ }
40
+ end
41
+
42
+ def domain_name
43
+ subdomain = Jets.project_namespace
44
+ managed_domain_name = "#{subdomain}.#{Jets.config.domain.hosted_zone_name}"
45
+ Jets.config.domain.name || managed_domain_name
46
+ end
47
+
48
+ def endpoint_types
49
+ [Jets.config.domain.endpoint_type].flatten
50
+ end
51
+
52
+ def cert_arn
53
+ Jets.config.domain.cert_arn
54
+ end
55
+ end
56
+ end
@@ -1,5 +1,3 @@
1
- require "active_support/core_ext/object"
2
-
3
1
  # Converts a Jets::Route to a CloudFormation Jets::Resource::ApiGateway::Method resource
4
2
  module Jets::Resource::ApiGateway
5
3
  class Method < Jets::Resource::Base
@@ -18,7 +16,7 @@ module Jets::Resource::ApiGateway
18
16
  type: "AWS::ApiGateway::Method",
19
17
  properties: {
20
18
  resource_id: "!Ref #{resource_id}",
21
- rest_api_id: "!Ref RestApi",
19
+ rest_api_id: "!Ref #{RestApi.logical_id}",
22
20
  http_method: @route.method,
23
21
  request_parameters: {},
24
22
  authorization_type: authorization_type,
@@ -47,7 +45,8 @@ module Jets::Resource::ApiGateway
47
45
  .gsub(/[^0-9a-z]/i, ' ')
48
46
  .gsub(/\s+/, '_')
49
47
  path = nil if path == ''
50
- [path, "{namespace}_api_method"].compact.join('_')
48
+ http_verb = @route.method.downcase
49
+ [path, "{namespace}_#{http_verb}_api_method"].compact.join('_')
51
50
  end
52
51
 
53
52
  def replacements
@@ -1,7 +1,8 @@
1
1
  module Jets::Resource::ApiGateway
2
2
  class Resource < Jets::Resource::Base
3
- def initialize(path)
3
+ def initialize(path, internal: false)
4
4
  @path = path # Examples: "posts/:id/edit" or "posts"
5
+ @internal = internal
5
6
  end
6
7
 
7
8
  def definition
@@ -11,7 +12,7 @@ module Jets::Resource::ApiGateway
11
12
  properties: {
12
13
  parent_id: parent_id,
13
14
  path_part: path_part,
14
- rest_api_id: "!Ref RestApi",
15
+ rest_api_id: "!Ref #{RestApi.logical_id(@internal)}",
15
16
  }
16
17
  }
17
18
  }
@@ -42,7 +43,7 @@ module Jets::Resource::ApiGateway
42
43
  parent_logical_id = path_logical_id(parent_path)
43
44
  "!Ref #{parent_logical_id}ApiResource"
44
45
  else
45
- "!GetAtt RestApi.RootResourceId"
46
+ "!GetAtt #{RestApi.logical_id(@internal)}.RootResourceId"
46
47
  end
47
48
  end
48
49
 
@@ -1,32 +1,60 @@
1
1
  module Jets::Resource::ApiGateway
2
2
  class RestApi < Jets::Resource::Base
3
+ autoload :ChangeDetection, 'jets/resource/api_gateway/rest_api/change_detection'
4
+ autoload :LogicalId, 'jets/resource/api_gateway/rest_api/logical_id'
5
+ autoload :Routes, 'jets/resource/api_gateway/rest_api/routes'
3
6
 
4
7
  def definition
8
+ properties = {
9
+ name: Jets::Naming.gateway_api_name,
10
+ endpoint_configuration: { types: endpoint_types }
11
+ }
12
+ properties[:binary_media_types] = binary_media_types if binary_media_types
13
+
5
14
  {
6
- rest_api: {
15
+ internal_logical_id => {
7
16
  type: "AWS::ApiGateway::RestApi",
8
- properties: {
9
- name: Jets::Naming.gateway_api_name,
10
- endpoint_configuration: {
11
- types: types
12
- }
13
- # binary_media_types: ['*/*'], # TODO: comment out, breaking form post
14
- }
17
+ properties: properties
15
18
  }
16
19
  }
17
20
  end
18
-
21
+
22
+ def internal_logical_id
23
+ self.class.logical_id(true)
24
+ end
25
+
26
+ def self.logical_id(internal=false)
27
+ internal ? internal_logical_id : "RestApi"
28
+ end
29
+
30
+ @@internal_logical_id = nil
31
+ def self.internal_logical_id
32
+ @@internal_logical_id ||= LogicalId.new.get
33
+ end
34
+
19
35
  def outputs
20
36
  {
21
- "RestApi" => "!Ref RestApi",
37
+ "RestApi" => "!Ref #{internal_logical_id}",
22
38
  "Region" => "!Ref AWS::Region",
23
- "RootResourceId" => "!GetAtt RestApi.RootResourceId",
39
+ "RootResourceId" => "!GetAtt #{internal_logical_id}.RootResourceId",
24
40
  }
25
41
  end
26
42
 
27
- def types
28
- endpoint_type = Jets.config.api.endpoint_type || 'EDGE'
29
- [endpoint_type.upcase]
43
+ def endpoint_types
44
+ [Jets.config.api.endpoint_type].flatten
45
+ end
46
+
47
+ # TODO: Looks like there's a bug with CloudFormation. On an API Gateway update
48
+ # we need to pass in the escaped version: multipart~1form-data
49
+ # On a brand new API Gateway creation, we need to pass in the unescaped form:
50
+ # multipart/form-data
51
+ # We are handling this with a full API Gateway replacement instead because it
52
+ # can be generalized more easily.
53
+ def binary_media_types
54
+ types = Jets.config.api.binary_media_types
55
+ return nil if types.nil? || types.empty?
56
+
57
+ [types].flatten
30
58
  end
31
59
  end
32
60
  end