aws-cft-tools 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +10 -0
- data/.gitignore +52 -0
- data/.rspec +2 -0
- data/.rubocop.yml +19 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/BEST-PRACTICES.md +136 -0
- data/CONTRIBUTING.md +38 -0
- data/Gemfile +8 -0
- data/LICENSE +15 -0
- data/README.md +118 -0
- data/Rakefile +17 -0
- data/USAGE.adoc +404 -0
- data/aws-cft-tools.gemspec +53 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/code.json +24 -0
- data/exe/aws-cft +176 -0
- data/lib/aws-cft-tools.rb +3 -0
- data/lib/aws_cft_tools.rb +31 -0
- data/lib/aws_cft_tools/aws_enumerator.rb +55 -0
- data/lib/aws_cft_tools/change.rb +66 -0
- data/lib/aws_cft_tools/client.rb +84 -0
- data/lib/aws_cft_tools/client/base.rb +40 -0
- data/lib/aws_cft_tools/client/cft.rb +93 -0
- data/lib/aws_cft_tools/client/cft/changeset_management.rb +109 -0
- data/lib/aws_cft_tools/client/cft/stack_management.rb +85 -0
- data/lib/aws_cft_tools/client/ec2.rb +136 -0
- data/lib/aws_cft_tools/client/templates.rb +84 -0
- data/lib/aws_cft_tools/deletion_change.rb +43 -0
- data/lib/aws_cft_tools/dependency_tree.rb +109 -0
- data/lib/aws_cft_tools/dependency_tree/nodes.rb +71 -0
- data/lib/aws_cft_tools/dependency_tree/variables.rb +37 -0
- data/lib/aws_cft_tools/errors.rb +25 -0
- data/lib/aws_cft_tools/runbook.rb +166 -0
- data/lib/aws_cft_tools/runbook/report.rb +30 -0
- data/lib/aws_cft_tools/runbooks.rb +16 -0
- data/lib/aws_cft_tools/runbooks/common/changesets.rb +30 -0
- data/lib/aws_cft_tools/runbooks/common/templates.rb +38 -0
- data/lib/aws_cft_tools/runbooks/deploy.rb +107 -0
- data/lib/aws_cft_tools/runbooks/deploy/reporting.rb +50 -0
- data/lib/aws_cft_tools/runbooks/deploy/stacks.rb +109 -0
- data/lib/aws_cft_tools/runbooks/deploy/templates.rb +37 -0
- data/lib/aws_cft_tools/runbooks/deploy/threading.rb +37 -0
- data/lib/aws_cft_tools/runbooks/diff.rb +28 -0
- data/lib/aws_cft_tools/runbooks/diff/context.rb +86 -0
- data/lib/aws_cft_tools/runbooks/diff/context/reporting.rb +87 -0
- data/lib/aws_cft_tools/runbooks/hosts.rb +43 -0
- data/lib/aws_cft_tools/runbooks/images.rb +43 -0
- data/lib/aws_cft_tools/runbooks/init.rb +86 -0
- data/lib/aws_cft_tools/runbooks/retract.rb +69 -0
- data/lib/aws_cft_tools/runbooks/retract/templates.rb +44 -0
- data/lib/aws_cft_tools/runbooks/stacks.rb +43 -0
- data/lib/aws_cft_tools/stack.rb +83 -0
- data/lib/aws_cft_tools/template.rb +177 -0
- data/lib/aws_cft_tools/template/dsl_context.rb +14 -0
- data/lib/aws_cft_tools/template/file_system.rb +62 -0
- data/lib/aws_cft_tools/template/metadata.rb +144 -0
- data/lib/aws_cft_tools/template/properties.rb +129 -0
- data/lib/aws_cft_tools/template_set.rb +120 -0
- data/lib/aws_cft_tools/template_set/array_methods.rb +63 -0
- data/lib/aws_cft_tools/template_set/closure.rb +77 -0
- data/lib/aws_cft_tools/template_set/dependencies.rb +55 -0
- data/lib/aws_cft_tools/template_set/each_slice_state.rb +58 -0
- data/lib/aws_cft_tools/version.rb +8 -0
- data/rubycritic.reek +3 -0
- metadata +321 -0
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
class Template
|
5
|
+
##
|
6
|
+
# Simple properties of templates.
|
7
|
+
#
|
8
|
+
module Properties
|
9
|
+
##
|
10
|
+
# Returns the list of environments allowed for the +Environment+ parameter.
|
11
|
+
#
|
12
|
+
# @return [Array<String>]
|
13
|
+
#
|
14
|
+
def allowed_environments
|
15
|
+
template.dig('Parameters', 'Environment', 'AllowedValues') || []
|
16
|
+
end
|
17
|
+
|
18
|
+
def environment?(value)
|
19
|
+
allowed_environments.include?(value)
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Returns the parameter defaults for the template.
|
24
|
+
#
|
25
|
+
def default_parameters
|
26
|
+
(template['Parameters'] || []).each_with_object({}) do |param, hash|
|
27
|
+
hash[param.first] = param.last['Default']
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Returns the role of the template as specified in the template metadata.
|
33
|
+
#
|
34
|
+
# @return [String]
|
35
|
+
#
|
36
|
+
def role
|
37
|
+
template.dig('Metadata', 'Role')
|
38
|
+
end
|
39
|
+
|
40
|
+
def role?(value)
|
41
|
+
!value || role == value
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Returns the list of regions in which the template is allowed.
|
46
|
+
#
|
47
|
+
# @note The region in which a template is deployed is available as the +AWS::Region+ pseudo-parameter.
|
48
|
+
#
|
49
|
+
# @return [Array<String>]
|
50
|
+
#
|
51
|
+
def allowed_regions
|
52
|
+
template.dig('Metadata', 'Region') || []
|
53
|
+
end
|
54
|
+
|
55
|
+
def region?(region)
|
56
|
+
!region || allowed_regions.empty? || allowed_regions.include?(region)
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Returns any templates on which this template has an explicit dependency.
|
61
|
+
#
|
62
|
+
# These explicit dependencies are combined with any dependencies implied by imported values.
|
63
|
+
#
|
64
|
+
def template_dependencies
|
65
|
+
template.dig('Metadata', 'DependsOn', 'Templates') || []
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# lists the exported values from the template
|
70
|
+
#
|
71
|
+
# Note that this does substitutions of any references to template parameters.
|
72
|
+
#
|
73
|
+
# @return [Array<String>]
|
74
|
+
#
|
75
|
+
def outputs
|
76
|
+
(template['Outputs'] || []).map do |_, output|
|
77
|
+
with_substitutions(output_name(output.dig('Export', 'Name')))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# lists the imports expected by the template
|
83
|
+
#
|
84
|
+
# Note that this does substitutions of any references to template parameters.
|
85
|
+
#
|
86
|
+
# @return [Array<String>]
|
87
|
+
#
|
88
|
+
def inputs
|
89
|
+
(template['Resources'] || {})
|
90
|
+
.values
|
91
|
+
.flat_map { |resource| pull_imports(resource['Properties'] || {}) }
|
92
|
+
.uniq
|
93
|
+
.map(&method(:with_substitutions))
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def output_name(name)
|
99
|
+
if name.is_a?(Hash)
|
100
|
+
name['Sub'] || name['Fn::Sub']
|
101
|
+
else
|
102
|
+
name
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def substitutions
|
107
|
+
@substitutions ||= begin
|
108
|
+
defaults = Hash.new { |hash, key| hash[key] = "${#{key}}" }
|
109
|
+
# need to get the default values of parameters from the template and populate those
|
110
|
+
|
111
|
+
[default_parameters, parameters].flat_map(&:to_a).each do |key, value|
|
112
|
+
defaults["${#{key}}"] = value
|
113
|
+
end
|
114
|
+
|
115
|
+
defaults
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
## expands ${param} when ${param} is defined in the parameters
|
120
|
+
## but only works with "${param}" and not [ "${param}", {param: value}]
|
121
|
+
## only substitutes when a value is provided as a parameter - otherwise, leaves it unsubsituted
|
122
|
+
def with_substitutions(string)
|
123
|
+
return string if string.is_a?(Array)
|
124
|
+
|
125
|
+
string.gsub(/(\${[^}]*})/, substitutions)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
##
|
5
|
+
# Management of a set of template sources.
|
6
|
+
#
|
7
|
+
class TemplateSet < ::Array
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
attr_reader :known_exports
|
11
|
+
attr_reader :dependency_tree
|
12
|
+
|
13
|
+
require_relative 'template_set/array_methods'
|
14
|
+
require_relative 'template_set/closure'
|
15
|
+
require_relative 'template_set/dependencies'
|
16
|
+
|
17
|
+
include ArrayMethods
|
18
|
+
|
19
|
+
# @!method closure
|
20
|
+
# @see AwsCftTools::TemplateSet::Closure
|
21
|
+
# @!method closed_subset
|
22
|
+
# @see AwsCftTools::TemplateSet::Closure
|
23
|
+
include Closure
|
24
|
+
|
25
|
+
include Dependencies
|
26
|
+
|
27
|
+
# @!method undefined_variables
|
28
|
+
# @see AwsCftTools::DependencyTree#undefined_variables
|
29
|
+
# @!method defined_variables
|
30
|
+
# @see AwsCftTools::DependencyTree#defined_variables
|
31
|
+
def_delegators :dependency_tree, :undefined_variables, :defined_variables
|
32
|
+
|
33
|
+
##
|
34
|
+
# @param list [Array<AwsCftTools::Template>] the templates in the set
|
35
|
+
#
|
36
|
+
def initialize(list = [])
|
37
|
+
@dependency_tree = AwsCftTools::DependencyTree.new
|
38
|
+
@sorted_names = []
|
39
|
+
@known_exports = []
|
40
|
+
super(list)
|
41
|
+
|
42
|
+
list.each { |template| process_template_addition(template) }
|
43
|
+
end
|
44
|
+
|
45
|
+
# @!visibility private
|
46
|
+
def initialize_clone(other)
|
47
|
+
initialize_copy(other)
|
48
|
+
end
|
49
|
+
|
50
|
+
# @!visibility private
|
51
|
+
def initialize_copy(other)
|
52
|
+
super(other)
|
53
|
+
@dependency_tree = other.dependency_tree.clone
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# Set the list of known exported values in CloudFormation
|
58
|
+
#
|
59
|
+
# @param list [Array<String>]
|
60
|
+
#
|
61
|
+
def known_exports=(list)
|
62
|
+
@known_exports |= list
|
63
|
+
list.each do |name|
|
64
|
+
@dependency_tree.exported(name)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# @param filenames [Array<String>]
|
70
|
+
# @return [AwsCftTools::TemplateSet] set of templates with the given filenames
|
71
|
+
#
|
72
|
+
def templates_for(filenames)
|
73
|
+
select { |template| filenames.include?(template.filename.to_s) }
|
74
|
+
end
|
75
|
+
|
76
|
+
##
|
77
|
+
# @param template [AwsCftTools::Template]
|
78
|
+
# @return [AwsCftTools::TemplateSet] set of templates on which the given template depends
|
79
|
+
#
|
80
|
+
def dependencies_for(template)
|
81
|
+
templates_for(@dependency_tree.dependencies_for(template.filename.to_s))
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# @param template [AwsCftTools::Template]
|
86
|
+
# @return [AwsCftTools::TemplateSet] set of templates that depend on the given template
|
87
|
+
#
|
88
|
+
def dependents_for(template)
|
89
|
+
templates_for(@dependency_tree.dependents_for(template.filename.to_s))
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def resort_templates
|
95
|
+
@sorted_names = @dependency_tree.sort
|
96
|
+
sort_by! { |template| @sorted_names.index(template.filename.to_s) || -1 }
|
97
|
+
end
|
98
|
+
|
99
|
+
def process_template_outputs(template)
|
100
|
+
template.outputs.each { |name| provided(template, name) }
|
101
|
+
end
|
102
|
+
|
103
|
+
def process_template_inputs(template)
|
104
|
+
template.inputs.each { |name| required(template, name) }
|
105
|
+
end
|
106
|
+
|
107
|
+
def process_template_dependencies(template)
|
108
|
+
templates_for(template.template_dependencies).each { |other| linked(other, template) }
|
109
|
+
end
|
110
|
+
|
111
|
+
def process_template_addition(template)
|
112
|
+
process_template_inputs(template)
|
113
|
+
process_template_outputs(template)
|
114
|
+
process_template_dependencies(template)
|
115
|
+
|
116
|
+
resort_templates
|
117
|
+
self
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
class TemplateSet
|
5
|
+
##
|
6
|
+
# Array methods that need to be overridden to work well with template sets.
|
7
|
+
#
|
8
|
+
module ArrayMethods
|
9
|
+
##
|
10
|
+
# create a new template set holding templates in either set without duplicates
|
11
|
+
#
|
12
|
+
# Note that this is identical to `|`.
|
13
|
+
#
|
14
|
+
# @param other [AwsCftTools::TemplateSet]
|
15
|
+
# @return [AwsCftTools::TemplateSet]
|
16
|
+
#
|
17
|
+
def +(other)
|
18
|
+
self.class.new(super(other).uniq(&:name)).tap do |union|
|
19
|
+
union.known_exports = @known_exports
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# create a new template set holding templates in either set without duplicates
|
25
|
+
#
|
26
|
+
# @param other [AwsCftTools::TemplateSet]
|
27
|
+
# @return [AwsCftTools::TemplateSet]
|
28
|
+
#
|
29
|
+
def |(other)
|
30
|
+
self + other
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# create a new template set holding templates in the first set not in the second
|
35
|
+
#
|
36
|
+
# @param other [AwsCftTools::TemplateSet]
|
37
|
+
# @return [AwsCftTools::TemplateSet]
|
38
|
+
#
|
39
|
+
def -(other)
|
40
|
+
forbidden_names = other.map(&:name)
|
41
|
+
clone.replace_list(
|
42
|
+
reject { |template| forbidden_names.include?(template.name) }
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# @return [AwsCftTools::TemplateSet]
|
48
|
+
# @yield [AwsCftTools::Template]
|
49
|
+
#
|
50
|
+
def select
|
51
|
+
return unless block_given?
|
52
|
+
clone.replace_list(super)
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
def replace_list(new_list)
|
58
|
+
self[0..size - 1] = new_list
|
59
|
+
self
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
class TemplateSet
|
5
|
+
##
|
6
|
+
# Closure-related functions for a TemplateSet.
|
7
|
+
#
|
8
|
+
module Closure
|
9
|
+
##
|
10
|
+
# Provides the given templates and any from the set that those templates depend on.
|
11
|
+
#
|
12
|
+
# @param templates [Array<AwsCftTools::Template>]
|
13
|
+
# @return [AwsCftTools::TemplateSet]
|
14
|
+
#
|
15
|
+
def closure(templates)
|
16
|
+
templates_for(
|
17
|
+
calculate_closure(templates.filenames) { |template| @dependency_tree.dependencies_for(template) }
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# Provides a list of filenames holding the source for the templates in a set.
|
23
|
+
#
|
24
|
+
# @return [Array<String>]
|
25
|
+
#
|
26
|
+
def filenames
|
27
|
+
map(&:filename).map(&:to_s)
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Provides the subset of the given templates that have no dependent templates outside the set.
|
32
|
+
#
|
33
|
+
# @param templates [AwsCftTools::TemplateSet]
|
34
|
+
# @return [AwsCftTools::TemplateSet]
|
35
|
+
#
|
36
|
+
def closed_subset(templates)
|
37
|
+
templates_for(@dependency_tree.closed_subset(templates.filenames))
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# @param folders [Array<String>]
|
42
|
+
# @return [AwsCftTools::TemplateSet]
|
43
|
+
#
|
44
|
+
def in_folder_order(folders)
|
45
|
+
proper_ordered_set, set = folders.reduce([clone, []]) do |memo, folder|
|
46
|
+
set, proper_ordered_set = memo
|
47
|
+
selected = closure(templates_in_folder(set, folder)) - proper_ordered_set
|
48
|
+
[set - selected, proper_ordered_set | selected]
|
49
|
+
end
|
50
|
+
|
51
|
+
proper_ordered_set | set
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def templates_in_folder(set, folder)
|
57
|
+
set.select { |template| template.filename.to_s.start_with?(folder + '/') }
|
58
|
+
end
|
59
|
+
|
60
|
+
def calculate_closure(set, &block)
|
61
|
+
stack = set.clone
|
62
|
+
|
63
|
+
stack += closure_step(stack.shift, set, &block) while stack.any?
|
64
|
+
set
|
65
|
+
end
|
66
|
+
|
67
|
+
def closure_step(template, set)
|
68
|
+
[].tap do |stack|
|
69
|
+
(yield(template) - set).each do |depedency|
|
70
|
+
stack << depedency
|
71
|
+
set.unshift(depedency)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'each_slice_state'
|
4
|
+
|
5
|
+
module AwsCftTools
|
6
|
+
class TemplateSet
|
7
|
+
##
|
8
|
+
# Simple derived information about templates.
|
9
|
+
#
|
10
|
+
module Dependencies
|
11
|
+
##
|
12
|
+
# @param template [AwsCftTools::Template]
|
13
|
+
# @param variable [#to_s]
|
14
|
+
def provided(template, variable)
|
15
|
+
@dependency_tree.provided(template.filename.to_s, variable.to_s)
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# @param template [AwsCftTools::Template]
|
20
|
+
# @param variable [#to_s]
|
21
|
+
def required(template, variable)
|
22
|
+
@dependency_tree.required(template.filename.to_s, variable.to_s)
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# @param from [AwsCftTools::Template]
|
27
|
+
# @param to [AwsCftTools::Template]
|
28
|
+
#
|
29
|
+
def linked(from, to)
|
30
|
+
@dependency_tree.linked(from.filename.to_s, to.filename.to_s)
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Iterates through the sorted list and yields an array of templates with no unsatisfied dependencies,
|
35
|
+
# up to the maximum slice size.
|
36
|
+
#
|
37
|
+
# @param maximum_slice_size [Integer]
|
38
|
+
# @yield [Array<AwsCftTools::Template>] up to +maximum_slice_size+ templates with no unsatisfied
|
39
|
+
# dependencies
|
40
|
+
#
|
41
|
+
def each_slice(maximum_slice_size, &block)
|
42
|
+
return unless block_given?
|
43
|
+
# we want to start at the beginning and get up to <n> items for which all prior dependencies have
|
44
|
+
# already been returned in a prior call
|
45
|
+
state = EachSliceState.new(maximum_slice_size, &block)
|
46
|
+
|
47
|
+
each do |template|
|
48
|
+
state.add_template(template, @dependency_tree.dependencies_for(template.filename.to_s))
|
49
|
+
end
|
50
|
+
# catch the last templates
|
51
|
+
state.process_slice
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
class TemplateSet
|
5
|
+
##
|
6
|
+
# Keeps track of state for the .each_slice(n) method.
|
7
|
+
#
|
8
|
+
class EachSliceState
|
9
|
+
##
|
10
|
+
# @param slice_size [Integer] maximum number of templates to yield at once
|
11
|
+
# @yield [Array<AwsCftTools::Template>]
|
12
|
+
#
|
13
|
+
def initialize(slice_size, &block)
|
14
|
+
@seen = []
|
15
|
+
@size = slice_size
|
16
|
+
@slice = []
|
17
|
+
@block = block
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# Have all of the listed dependencies been seen in prior yields?
|
22
|
+
#
|
23
|
+
# @param deps [Array<String>]
|
24
|
+
# @return [Boolean]
|
25
|
+
#
|
26
|
+
def fulfilled?(deps)
|
27
|
+
(deps - @seen).empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
###
|
31
|
+
# Add the template to the current slice and process the slice if it reaches the maximum slice size.
|
32
|
+
#
|
33
|
+
# @param template [AwsCftTools::Template]
|
34
|
+
#
|
35
|
+
def add_template(template, dependencies = [])
|
36
|
+
process_slice unless fulfilled?(dependencies)
|
37
|
+
unless fulfilled?(dependencies)
|
38
|
+
raise AwsCftTools::UnsatisfiedDependencyError, "Unable to process #{template.filename}"
|
39
|
+
end
|
40
|
+
|
41
|
+
@slice << template
|
42
|
+
|
43
|
+
process_slice if @slice.count == @size
|
44
|
+
end
|
45
|
+
|
46
|
+
###
|
47
|
+
# Pass the current slice through the block and reset for the next slice.
|
48
|
+
#
|
49
|
+
# @return [Integer] number of templates processed in this batch
|
50
|
+
#
|
51
|
+
def process_slice
|
52
|
+
@block.call(@slice) if @slice.any?
|
53
|
+
@seen |= @slice.map(&:filename).map(&:to_s)
|
54
|
+
@slice = []
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|