aws-cft-tools 0.1.0

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