jets 2.1.1 → 2.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.codebuild/README.md +10 -60
  3. data/.codebuild/docs/bin/build.sh +6 -0
  4. data/.codebuild/docs/bin/cli_docs.sh +5 -0
  5. data/.codebuild/docs/bin/git_commit.sh +27 -0
  6. data/.codebuild/docs/bin/git_setup.sh +19 -0
  7. data/.codebuild/docs/bin/subnav.sh +14 -0
  8. data/.codebuild/docs/buildspec.yml +8 -0
  9. data/.codebuild/docs/project.rb +10 -0
  10. data/.gitignore +5 -5
  11. data/CHANGELOG.md +8 -0
  12. data/jets.gemspec +5 -5
  13. data/lib/jets/aws_services/stack_status.rb +5 -1
  14. data/lib/jets/cfn/builders/api_gateway_builder.rb +10 -19
  15. data/lib/jets/cfn/builders/api_resources_builder.rb +46 -0
  16. data/lib/jets/cfn/builders/parent_builder.rb +15 -0
  17. data/lib/jets/cfn/built_template.rb +15 -0
  18. data/lib/jets/commands/clean/log.rb +18 -1
  19. data/lib/jets/commands/delete.rb +14 -1
  20. data/lib/jets/commands/deploy.rb +43 -12
  21. data/lib/jets/commands/help/build.md +1 -1
  22. data/lib/jets/commands/help/{destroy.md → degenerate.md} +1 -1
  23. data/lib/jets/commands/help/upgrade.md +2 -0
  24. data/lib/jets/commands/main.rb +2 -1
  25. data/lib/jets/commands/templates/skeleton/Gemfile.tt +1 -0
  26. data/lib/jets/naming.rb +4 -0
  27. data/lib/jets/resource/api_gateway/resource.rb +7 -3
  28. data/lib/jets/resource/api_gateway/rest_api/change_detection.rb +0 -32
  29. data/lib/jets/resource/api_gateway/rest_api/routes/change.rb +9 -1
  30. data/lib/jets/resource/api_gateway/rest_api/routes/change/base.rb +26 -5
  31. data/lib/jets/resource/api_gateway/rest_api/routes/change/media_types.rb +36 -0
  32. data/lib/jets/resource/api_gateway/rest_api/routes/change/page.rb +93 -0
  33. data/lib/jets/resource/api_gateway/rest_api/routes/change/to.rb +0 -4
  34. data/lib/jets/resource/api_gateway/rest_api/routes/change/variable.rb +0 -4
  35. data/lib/jets/resource/child_stack/api_resource.rb +60 -0
  36. data/lib/jets/resource/child_stack/api_resource/page.rb +20 -0
  37. data/lib/jets/resource/child_stack/app_class.rb +14 -3
  38. data/lib/jets/router.rb +57 -40
  39. data/lib/jets/router/method_creator/code.rb +3 -1
  40. data/lib/jets/router/method_creator/edit.rb +6 -1
  41. data/lib/jets/router/method_creator/index.rb +9 -3
  42. data/lib/jets/router/method_creator/new.rb +6 -1
  43. data/lib/jets/router/method_creator/show.rb +6 -1
  44. data/lib/jets/router/route.rb +1 -0
  45. data/lib/jets/version.rb +1 -1
  46. metadata +22 -17
  47. data/.codebuild/bin/jets +0 -3
  48. data/.codebuild/buildspec-base.yml +0 -14
  49. data/.codebuild/integration.sh +0 -72
  50. data/.codebuild/jets.postman_collection.json +0 -323
  51. data/.codebuild/scripts/install-docker.sh +0 -12
  52. data/.codebuild/scripts/install-dynamodb-local.sh +0 -22
  53. data/.codebuild/scripts/install-java.sh +0 -22
  54. data/.codebuild/scripts/install-node.sh +0 -4
@@ -44,13 +44,26 @@ class Jets::Commands::Delete
44
44
  end
45
45
 
46
46
  def confirm_project_exists
47
+ retries = 0
47
48
  begin
48
- resp = cfn.describe_stacks(stack_name: parent_stack_name)
49
+ cfn.describe_stacks(stack_name: parent_stack_name)
49
50
  rescue Aws::CloudFormation::Errors::ValidationError
50
51
  # Aws::CloudFormation::Errors::ValidationError is thrown when the stack
51
52
  # does not exist
52
53
  puts "The parent stack #{Jets.config.project_namespace.color(:green)} for the project #{Jets.config.project_name.color(:green)} does not exist. So it cannot be deleted."
53
54
  exit 0
55
+ rescue Aws::CloudFormation::Errors::Throttling => e
56
+ retries += 1
57
+ seconds = 2 ** retries
58
+
59
+ puts "WARN: confirm_project_exists #{e.class} #{e.message}".color(:yellow)
60
+ puts "Backing off and will retry in #{seconds} seconds."
61
+ sleep(seconds)
62
+ if seconds > 90 # 2 ** 6 is 64 so will give up after 6 retries
63
+ puts "Giving up after #{retries} retries"
64
+ else
65
+ retry
66
+ end
54
67
  end
55
68
  end
56
69
 
@@ -1,3 +1,5 @@
1
+ require "aws-sdk-core"
2
+
1
3
  module Jets::Commands
2
4
  class Deploy
3
5
  extend Memoist
@@ -7,6 +9,7 @@ module Jets::Commands
7
9
  end
8
10
 
9
11
  def run
12
+ aws_config_update!
10
13
  deployment_env = Jets.config.project_namespace.color(:green)
11
14
  puts "Deploying to Lambda #{deployment_env} environment..."
12
15
  return if @options[:noop]
@@ -34,6 +37,29 @@ module Jets::Commands
34
37
  ship(stack_type: :full, s3_bucket: s3_bucket)
35
38
  end
36
39
 
40
+ # Override the AWS retry settings during a deploy.
41
+ #
42
+ # The aws-sdk-core has expondential backup with this formula:
43
+ #
44
+ # 2 ** c.retries * c.config.retry_base_delay
45
+ #
46
+ # So the max delay will be 2 ** 7 * 0.6 = 76.8s
47
+ #
48
+ # Only scoping this to deploy because dont want to affect people's application that use the aws sdk.
49
+ #
50
+ # There is also additional rate backoff logic elsewhere, since this is only scoped to deploys.
51
+ #
52
+ # Useful links:
53
+ # https://github.com/aws/aws-sdk-ruby/blob/master/gems/aws-sdk-core/lib/aws-sdk-core/plugins/retry_errors.rb
54
+ # https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html
55
+ #
56
+ def aws_config_update!
57
+ Aws.config.update(
58
+ retry_limit: 7, # default: 3
59
+ retry_base_delay: 0.6, # default: 0.3
60
+ )
61
+ end
62
+
37
63
  def create_s3_event_buckets
38
64
  buckets = Jets::Job::Base.s3_events.keys
39
65
  buckets.each do |bucket|
@@ -60,19 +86,11 @@ module Jets::Commands
60
86
  end
61
87
 
62
88
  def validate_routes!
63
- check_route_connected_functions
64
- end
65
-
66
- # Checks that all routes are validate and have corresponding lambda functions
67
- def check_route_connected_functions
68
- return if Jets::Router.all_routes_valid
69
-
70
- puts "Deploy fail: The jets application contain invalid routes.".color(:red)
71
- puts "Please double check the routes below map to valid controllers:"
72
- Jets::Router.invalid_routes.each do |route|
73
- puts " /#{route.path} => #{route.controller_name}##{route.action_name}"
89
+ valid = Jets::Router.validate_routes!
90
+ unless valid
91
+ puts "Deploy fail: The jets application contain invalid routes.".color(:red)
92
+ exit 1
74
93
  end
75
- exit 1
76
94
  end
77
95
 
78
96
  def ship(stack_options)
@@ -108,6 +126,7 @@ module Jets::Commands
108
126
  end
109
127
 
110
128
  def find_stack(stack_name)
129
+ retries = 0
111
130
  resp = cfn.describe_stacks(stack_name: stack_name)
112
131
  resp.stacks.first
113
132
  rescue Aws::CloudFormation::Errors::ValidationError => e
@@ -117,6 +136,18 @@ module Jets::Commands
117
136
  else
118
137
  raise
119
138
  end
139
+ rescue Aws::CloudFormation::Errors::Throttling => e
140
+ retries += 1
141
+ seconds = 2 ** retries
142
+
143
+ puts "WARN: find_stack #{e.class} #{e.message}".color(:yellow)
144
+ puts "Backing off and will retry in #{seconds} seconds."
145
+ sleep(seconds)
146
+ if seconds > 90 # 2 ** 6 is 64 so will give up after 6 retries
147
+ puts "Giving up after #{retries} retries"
148
+ else
149
+ retry
150
+ end
120
151
  end
121
152
 
122
153
  # All CloudFormation states listed here: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html
@@ -3,4 +3,4 @@ Builds a zip file package to be uploaded to AWS Lambda. This allows you to build
3
3
  * your application code
4
4
  * generated shims
5
5
 
6
- If the application has no Ruby code and only uses Polymorphic functions, then gems are not bundled up.
6
+ If the application has no Ruby code and only uses Polymorphic functions, then gems are not bundled up.
@@ -2,7 +2,7 @@ This piggy backs off of the [rails scaffold destroy](https://guides.rubyonrails.
2
2
 
3
3
  ## Example
4
4
 
5
- $ jets destroy scaffold Post title:string body:text published:boolean
5
+ $ jets degenerate scaffold post title:string body:text published:boolean
6
6
  invoke active_record
7
7
  remove db/migrate/20190225231821_create_posts.rb
8
8
  remove app/models/post.rb
@@ -3,3 +3,5 @@ Upgrades the Jets project structure to the latest version. This command is desig
3
3
  ## Example
4
4
 
5
5
  $ jets upgrade
6
+
7
+ Refer to https://rubyonjets.com/docs/extras/upgrading/
@@ -52,6 +52,7 @@ module Jets::Commands
52
52
  long_desc Help.text(:routes)
53
53
  def routes
54
54
  puts Jets::Router.help(Jets::Router.routes)
55
+ Jets::Router.validate_routes!
55
56
  end
56
57
 
57
58
  desc "console", "REPL console with Jets environment loaded"
@@ -103,7 +104,7 @@ module Jets::Commands
103
104
  end
104
105
 
105
106
  desc "degenerate [type] [args]", "Destroys things like scaffolds"
106
- long_desc Help.text(:generate) # do use Jets::Generator.help as it'll load Rails const
107
+ long_desc Help.text(:degenerate) # do use Jets::Generator.help as it'll load Rails const
107
108
  def degenerate(generator, *args)
108
109
  Jets::Generator.revoke(generator, *args)
109
110
  end
@@ -19,6 +19,7 @@ gem "mysql2", "~> 0.5.2"
19
19
  gem "dynomite"
20
20
  <% end -%>
21
21
 
22
+ # development and test groups are not bundled as part of the deployment
22
23
  group :development, :test do
23
24
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
24
25
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
data/lib/jets/naming.rb CHANGED
@@ -30,6 +30,10 @@ class Jets::Naming
30
30
  "#{template_path_prefix}-api-gateway.yml"
31
31
  end
32
32
 
33
+ def api_resources_template_path(page)
34
+ "#{template_path_prefix}-api-resources-#{page}.yml"
35
+ end
36
+
33
37
  def api_deployment_template_path
34
38
  "#{template_path_prefix}-api-deployment.yml"
35
39
  end
@@ -37,16 +37,20 @@ module Jets::Resource::ApiGateway
37
37
  path.empty? ? 'Homepage route: /' : "Route for: /#{path}"
38
38
  end
39
39
 
40
- def parent_id
40
+ def parent_path_parameter
41
41
  if @path.include?('/') # posts/:id or posts/:id/edit
42
42
  parent_path = @path.split('/')[0..-2].join('/')
43
43
  parent_logical_id = path_logical_id(parent_path)
44
- "!Ref " + Jets::Resource.truncate_id("#{parent_logical_id}ApiResource")
44
+ Jets::Resource.truncate_id("#{parent_logical_id}ApiResource")
45
45
  else
46
- "!GetAtt #{RestApi.logical_id(@internal)}.RootResourceId"
46
+ "RootResourceId"
47
47
  end
48
48
  end
49
49
 
50
+ def parent_id
51
+ "!Ref " + parent_path_parameter
52
+ end
53
+
50
54
  def path_part
51
55
  last_part = path.split('/').last
52
56
  last_part.split('/').map {|s| transform_capture(s) }.join('/') if last_part
@@ -4,39 +4,7 @@ class Jets::Resource::ApiGateway::RestApi
4
4
  include Jets::AwsServices
5
5
 
6
6
  def changed?
7
- return false unless parent_stack_exists?
8
- current_binary_media_types != new_binary_media_types ||
9
7
  Routes.changed?
10
8
  end
11
-
12
- def new_binary_media_types
13
- rest_api = Jets::Resource::ApiGateway::RestApi.new
14
- rest_api.binary_media_types
15
- end
16
- memoize :new_binary_media_types
17
-
18
- # Duplicated in rest_api/change_detection.rb, base_path/role.rb, rest_api/routes.rb
19
- def current_binary_media_types
20
- return nil unless parent_stack_exists?
21
-
22
- stack = cfn.describe_stacks(stack_name: parent_stack_name).stacks.first
23
-
24
- api_gateway_stack_arn = lookup(stack[:outputs], "ApiGateway")
25
-
26
- stack = cfn.describe_stacks(stack_name: api_gateway_stack_arn).stacks.first
27
- rest_api_id = lookup(stack[:outputs], "RestApi")
28
-
29
- resp = apigateway.get_rest_api(rest_api_id: rest_api_id)
30
- resp.binary_media_types
31
- end
32
- memoize :current_binary_media_types
33
-
34
- def parent_stack_exists?
35
- stack_exists?(parent_stack_name)
36
- end
37
-
38
- def parent_stack_name
39
- Jets::Naming.parent_stack_name
40
- end
41
9
  end
42
10
  end
@@ -1,8 +1,16 @@
1
1
  # Detects route changes
2
2
  class Jets::Resource::ApiGateway::RestApi::Routes
3
3
  class Change
4
+ include Jets::AwsServices
5
+
4
6
  def changed?
5
- To.changed? || Variable.changed? || ENV['JETS_REPLACE_API']
7
+ return false unless parent_stack_exists?
8
+
9
+ MediaTypes.changed? || To.changed? || Variable.changed? || Page.changed? || ENV['JETS_REPLACE_API']
10
+ end
11
+
12
+ def parent_stack_exists?
13
+ stack_exists?(Jets::Naming.parent_stack_name)
6
14
  end
7
15
  end
8
16
  end
@@ -3,6 +3,10 @@ class Jets::Resource::ApiGateway::RestApi::Routes::Change
3
3
  extend Memoist
4
4
  include Jets::AwsServices
5
5
 
6
+ def self.changed?
7
+ new.changed?
8
+ end
9
+
6
10
  # Build up deployed routes from the existing CloudFormation resources.
7
11
  def deployed_routes
8
12
  routes = []
@@ -13,6 +17,7 @@ class Jets::Resource::ApiGateway::RestApi::Routes::Change
13
17
  resp = apigateway.get_resources(
14
18
  rest_api_id: rest_api_id,
15
19
  position: position,
20
+ limit: 500, # default: 25 max: 500
16
21
  )
17
22
  resources += resp.items
18
23
  position = resp.position
@@ -53,11 +58,27 @@ class Jets::Resource::ApiGateway::RestApi::Routes::Change
53
58
  end
54
59
 
55
60
  def method_uri(resource_id, http_method)
56
- resp = apigateway.get_method(
57
- rest_api_id: rest_api_id,
58
- resource_id: resource_id,
59
- http_method: http_method
60
- )
61
+ # https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html
62
+ retries = 0
63
+ begin
64
+ resp = apigateway.get_method(
65
+ rest_api_id: rest_api_id,
66
+ resource_id: resource_id,
67
+ http_method: http_method
68
+ )
69
+ rescue Aws::APIGateway::Errors::TooManyRequestsException => e
70
+ retries += 1
71
+ seconds = 2 ** retries
72
+
73
+ puts "WARN: method_uri #{e.class} #{e.message}".color(:yellow)
74
+ puts "Backing off and will retry in #{seconds} seconds."
75
+ sleep(seconds)
76
+ if seconds > 90 # 2 ** 6 is 64 so will give up after 6 retries
77
+ puts "Giving up after #{retries} retries"
78
+ else
79
+ retry
80
+ end
81
+ end
61
82
  resp.method_integration.uri
62
83
  end
63
84
 
@@ -0,0 +1,36 @@
1
+ class Jets::Resource::ApiGateway::RestApi::Routes::Change
2
+ class MediaTypes < Base
3
+ def changed?
4
+ current_binary_media_types != new_binary_media_types
5
+ end
6
+
7
+ def new_binary_media_types
8
+ rest_api = Jets::Resource::ApiGateway::RestApi.new
9
+ rest_api.binary_media_types
10
+ end
11
+ memoize :new_binary_media_types
12
+
13
+ def current_binary_media_types
14
+ return nil unless parent_stack_exists?
15
+
16
+ stack = cfn.describe_stacks(stack_name: parent_stack_name).stacks.first
17
+
18
+ api_gateway_stack_arn = lookup(stack[:outputs], "ApiGateway")
19
+
20
+ stack = cfn.describe_stacks(stack_name: api_gateway_stack_arn).stacks.first
21
+ rest_api_id = lookup(stack[:outputs], "RestApi")
22
+
23
+ resp = apigateway.get_rest_api(rest_api_id: rest_api_id)
24
+ resp.binary_media_types
25
+ end
26
+ memoize :current_binary_media_types
27
+
28
+ def parent_stack_exists?
29
+ stack_exists?(parent_stack_name)
30
+ end
31
+
32
+ def parent_stack_name
33
+ Jets::Naming.parent_stack_name
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,93 @@
1
+ class Jets::Resource::ApiGateway::RestApi::Routes::Change
2
+ class Page < Base
3
+ def changed?
4
+ route_page_moved? || old_api_template?
5
+ end
6
+
7
+ def route_page_moved?
8
+ moved?(new_pages, deployed_pages)
9
+ end
10
+
11
+ # Routes page to logical ids
12
+ def moved?(new_pages, deployed_pages)
13
+ not_moved = true # page has not moved
14
+ new_pages.each do |logical_id, new_page_number|
15
+ if !deployed_pages[logical_id].nil? && deployed_pages[logical_id] != new_page_number
16
+ not_moved = false # page has moved
17
+ break
18
+ end
19
+ end
20
+ !not_moved # moved
21
+ end
22
+
23
+ def new_pages
24
+ local_logical_ids_map
25
+ end
26
+ memoize :new_pages
27
+
28
+ def deployed_pages
29
+ remote_logical_ids_map
30
+ end
31
+ memoize :deployed_pages
32
+
33
+ # logical id to page map
34
+ # Important: In Cfn::Builders::ApiGatewayBuilder, the add_gateway_routes and ApiResourcesBuilder needs to run
35
+ # before the parent add_gateway_rest_api method.
36
+ def local_logical_ids_map(path_expression="#{Jets::Naming.template_path_prefix}-api-resources-*.yml")
37
+ logical_ids = {} # logical id => page number
38
+
39
+ Dir.glob(path_expression).each do |path|
40
+ md = path.match(/-api-resources-(\d+).yml/)
41
+ page_number = md[1]
42
+
43
+ template = Jets::Cfn::BuiltTemplate.get(path)
44
+ template['Resources'].keys.each do |logical_id|
45
+ logical_ids[logical_id] = page_number
46
+ end
47
+ end
48
+
49
+ logical_ids
50
+ end
51
+
52
+ # aws cloudformation describe-stack-resources --stack-name demo-dev-ApiResources1-DYGLIEY3VAWT | jq -r '.StackResources[].LogicalResourceId'
53
+ def remote_logical_ids_map
54
+ logical_ids = {} # logical id => page number
55
+
56
+ parent_resources.each do |resource|
57
+ stack_name = resource.physical_resource_id # full physical id can be used as stack name also
58
+ regexp = Regexp.new("#{Jets.config.project_namespace}-ApiResources(\\d+)-") # tricky to escape \d pattern
59
+ md = stack_name.match(regexp)
60
+ if md
61
+ page_number = md[1]
62
+
63
+ resp = cfn.describe_stack_resources(stack_name: stack_name)
64
+ resp.stack_resources.map(&:logical_resource_id).each do |logical_id|
65
+ logical_ids[logical_id] = page_number
66
+ end
67
+ end
68
+ end
69
+
70
+ logical_ids
71
+ end
72
+
73
+ def old_api_template?
74
+ logical_resource_ids = parent_resources.map(&:logical_resource_id)
75
+
76
+ api_gateway_found = logical_resource_ids.detect do |logical_id|
77
+ logical_id == "ApiGateway"
78
+ end
79
+ return false unless api_gateway_found
80
+
81
+ api_resources_found = logical_resource_ids.detect do |logical_id|
82
+ logical_id.match(/^ApiResources\d+$/)
83
+ end
84
+ !api_resources_found # if api_resources_found then it's the new structure. so opposite is old structure
85
+ end
86
+
87
+ def parent_resources
88
+ resp = cfn.describe_stack_resources(stack_name: Jets::Naming.parent_stack_name)
89
+ resp.stack_resources
90
+ end
91
+ memoize :parent_resources
92
+ end
93
+ end