aws-cft-tools 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +10 -0
  3. data/.gitignore +52 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +19 -0
  6. data/.travis.yml +5 -0
  7. data/.yardopts +1 -0
  8. data/BEST-PRACTICES.md +136 -0
  9. data/CONTRIBUTING.md +38 -0
  10. data/Gemfile +8 -0
  11. data/LICENSE +15 -0
  12. data/README.md +118 -0
  13. data/Rakefile +17 -0
  14. data/USAGE.adoc +404 -0
  15. data/aws-cft-tools.gemspec +53 -0
  16. data/bin/console +15 -0
  17. data/bin/setup +8 -0
  18. data/code.json +24 -0
  19. data/exe/aws-cft +176 -0
  20. data/lib/aws-cft-tools.rb +3 -0
  21. data/lib/aws_cft_tools.rb +31 -0
  22. data/lib/aws_cft_tools/aws_enumerator.rb +55 -0
  23. data/lib/aws_cft_tools/change.rb +66 -0
  24. data/lib/aws_cft_tools/client.rb +84 -0
  25. data/lib/aws_cft_tools/client/base.rb +40 -0
  26. data/lib/aws_cft_tools/client/cft.rb +93 -0
  27. data/lib/aws_cft_tools/client/cft/changeset_management.rb +109 -0
  28. data/lib/aws_cft_tools/client/cft/stack_management.rb +85 -0
  29. data/lib/aws_cft_tools/client/ec2.rb +136 -0
  30. data/lib/aws_cft_tools/client/templates.rb +84 -0
  31. data/lib/aws_cft_tools/deletion_change.rb +43 -0
  32. data/lib/aws_cft_tools/dependency_tree.rb +109 -0
  33. data/lib/aws_cft_tools/dependency_tree/nodes.rb +71 -0
  34. data/lib/aws_cft_tools/dependency_tree/variables.rb +37 -0
  35. data/lib/aws_cft_tools/errors.rb +25 -0
  36. data/lib/aws_cft_tools/runbook.rb +166 -0
  37. data/lib/aws_cft_tools/runbook/report.rb +30 -0
  38. data/lib/aws_cft_tools/runbooks.rb +16 -0
  39. data/lib/aws_cft_tools/runbooks/common/changesets.rb +30 -0
  40. data/lib/aws_cft_tools/runbooks/common/templates.rb +38 -0
  41. data/lib/aws_cft_tools/runbooks/deploy.rb +107 -0
  42. data/lib/aws_cft_tools/runbooks/deploy/reporting.rb +50 -0
  43. data/lib/aws_cft_tools/runbooks/deploy/stacks.rb +109 -0
  44. data/lib/aws_cft_tools/runbooks/deploy/templates.rb +37 -0
  45. data/lib/aws_cft_tools/runbooks/deploy/threading.rb +37 -0
  46. data/lib/aws_cft_tools/runbooks/diff.rb +28 -0
  47. data/lib/aws_cft_tools/runbooks/diff/context.rb +86 -0
  48. data/lib/aws_cft_tools/runbooks/diff/context/reporting.rb +87 -0
  49. data/lib/aws_cft_tools/runbooks/hosts.rb +43 -0
  50. data/lib/aws_cft_tools/runbooks/images.rb +43 -0
  51. data/lib/aws_cft_tools/runbooks/init.rb +86 -0
  52. data/lib/aws_cft_tools/runbooks/retract.rb +69 -0
  53. data/lib/aws_cft_tools/runbooks/retract/templates.rb +44 -0
  54. data/lib/aws_cft_tools/runbooks/stacks.rb +43 -0
  55. data/lib/aws_cft_tools/stack.rb +83 -0
  56. data/lib/aws_cft_tools/template.rb +177 -0
  57. data/lib/aws_cft_tools/template/dsl_context.rb +14 -0
  58. data/lib/aws_cft_tools/template/file_system.rb +62 -0
  59. data/lib/aws_cft_tools/template/metadata.rb +144 -0
  60. data/lib/aws_cft_tools/template/properties.rb +129 -0
  61. data/lib/aws_cft_tools/template_set.rb +120 -0
  62. data/lib/aws_cft_tools/template_set/array_methods.rb +63 -0
  63. data/lib/aws_cft_tools/template_set/closure.rb +77 -0
  64. data/lib/aws_cft_tools/template_set/dependencies.rb +55 -0
  65. data/lib/aws_cft_tools/template_set/each_slice_state.rb +58 -0
  66. data/lib/aws_cft_tools/version.rb +8 -0
  67. data/rubycritic.reek +3 -0
  68. 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