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.
- checksums.yaml +4 -4
- data/.codebuild/README.md +10 -60
- data/.codebuild/docs/bin/build.sh +6 -0
- data/.codebuild/docs/bin/cli_docs.sh +5 -0
- data/.codebuild/docs/bin/git_commit.sh +27 -0
- data/.codebuild/docs/bin/git_setup.sh +19 -0
- data/.codebuild/docs/bin/subnav.sh +14 -0
- data/.codebuild/docs/buildspec.yml +8 -0
- data/.codebuild/docs/project.rb +10 -0
- data/.gitignore +5 -5
- data/CHANGELOG.md +8 -0
- data/jets.gemspec +5 -5
- data/lib/jets/aws_services/stack_status.rb +5 -1
- data/lib/jets/cfn/builders/api_gateway_builder.rb +10 -19
- data/lib/jets/cfn/builders/api_resources_builder.rb +46 -0
- data/lib/jets/cfn/builders/parent_builder.rb +15 -0
- data/lib/jets/cfn/built_template.rb +15 -0
- data/lib/jets/commands/clean/log.rb +18 -1
- data/lib/jets/commands/delete.rb +14 -1
- data/lib/jets/commands/deploy.rb +43 -12
- data/lib/jets/commands/help/build.md +1 -1
- data/lib/jets/commands/help/{destroy.md → degenerate.md} +1 -1
- data/lib/jets/commands/help/upgrade.md +2 -0
- data/lib/jets/commands/main.rb +2 -1
- data/lib/jets/commands/templates/skeleton/Gemfile.tt +1 -0
- data/lib/jets/naming.rb +4 -0
- data/lib/jets/resource/api_gateway/resource.rb +7 -3
- data/lib/jets/resource/api_gateway/rest_api/change_detection.rb +0 -32
- data/lib/jets/resource/api_gateway/rest_api/routes/change.rb +9 -1
- data/lib/jets/resource/api_gateway/rest_api/routes/change/base.rb +26 -5
- data/lib/jets/resource/api_gateway/rest_api/routes/change/media_types.rb +36 -0
- data/lib/jets/resource/api_gateway/rest_api/routes/change/page.rb +93 -0
- data/lib/jets/resource/api_gateway/rest_api/routes/change/to.rb +0 -4
- data/lib/jets/resource/api_gateway/rest_api/routes/change/variable.rb +0 -4
- data/lib/jets/resource/child_stack/api_resource.rb +60 -0
- data/lib/jets/resource/child_stack/api_resource/page.rb +20 -0
- data/lib/jets/resource/child_stack/app_class.rb +14 -3
- data/lib/jets/router.rb +57 -40
- data/lib/jets/router/method_creator/code.rb +3 -1
- data/lib/jets/router/method_creator/edit.rb +6 -1
- data/lib/jets/router/method_creator/index.rb +9 -3
- data/lib/jets/router/method_creator/new.rb +6 -1
- data/lib/jets/router/method_creator/show.rb +6 -1
- data/lib/jets/router/route.rb +1 -0
- data/lib/jets/version.rb +1 -1
- metadata +22 -17
- data/.codebuild/bin/jets +0 -3
- data/.codebuild/buildspec-base.yml +0 -14
- data/.codebuild/integration.sh +0 -72
- data/.codebuild/jets.postman_collection.json +0 -323
- data/.codebuild/scripts/install-docker.sh +0 -12
- data/.codebuild/scripts/install-dynamodb-local.sh +0 -22
- data/.codebuild/scripts/install-java.sh +0 -22
- data/.codebuild/scripts/install-node.sh +0 -4
data/lib/jets/commands/delete.rb
CHANGED
@@ -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
|
-
|
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
|
|
data/lib/jets/commands/deploy.rb
CHANGED
@@ -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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
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
|
data/lib/jets/commands/main.rb
CHANGED
@@ -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(:
|
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
|
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
|
-
|
44
|
+
Jets::Resource.truncate_id("#{parent_logical_id}ApiResource")
|
45
45
|
else
|
46
|
-
"
|
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
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|