jets 2.1.1 → 2.1.2

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 (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