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,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module AwsCftTools
6
+ class Client
7
+ ##
8
+ # All of the business logic behind direct interaction with the AWS Template sources.
9
+ #
10
+ class Templates < Base
11
+ ##
12
+ # Default template directory in the project.
13
+ DEFAULT_TEMPLATE_DIR = 'cloudformation/templates/'
14
+
15
+ ##
16
+ # Default parameters directory in the project.
17
+ DEFAULT_PARAMETER_DIR = 'cloudformation/parameters/'
18
+
19
+ ##
20
+ # Default set of file extensions that might contain templates.
21
+ #
22
+ TEMPLATE_FILE_EXTENSIONS = %w[.yaml .yml .json .rb].freeze
23
+
24
+ ##
25
+ #
26
+ # @param options [Hash] client configuration
27
+ # @option options [String] :environment the operational environment in which to act
28
+ # @option options [String] :parameter_dir
29
+ # @option options [String] :region the AWS region in which to act
30
+ # @option options [Pathname] :root
31
+ # @option options [String] :template_dir
32
+ #
33
+ def initialize(options)
34
+ super({
35
+ template_dir: DEFAULT_TEMPLATE_DIR,
36
+ parameter_dir: DEFAULT_PARAMETER_DIR
37
+ }.merge(options))
38
+ end
39
+
40
+ ##
41
+ # Lists all templates.
42
+ #
43
+ # @return AwsCftTools::TemplateSet
44
+ #
45
+ def templates
46
+ template_file_root = (options[:root] + options[:template_dir]).cleanpath
47
+ filtered_by_region(
48
+ filtered_by_environment(
49
+ all_templates(
50
+ template_file_root
51
+ )
52
+ )
53
+ )
54
+ end
55
+
56
+ private
57
+
58
+ def filtered_by_environment(set)
59
+ set.select { |template| template.environment?(options[:environment]) }
60
+ end
61
+
62
+ def filtered_by_region(set)
63
+ set.select { |template| template.region?(options[:region]) }
64
+ end
65
+
66
+ def all_templates(root)
67
+ AwsCftTools::TemplateSet.new(glob_templates(root)).tap do |set|
68
+ set.known_exports = options[:client].exports.map(&:name)
69
+ end
70
+ end
71
+
72
+ def glob_templates(root)
73
+ Pathname.glob(root + '**/*')
74
+ .select { |file| TEMPLATE_FILE_EXTENSIONS.include?(file.extname) }
75
+ .map { |file| file_to_template(root, file) }
76
+ .select(&:template?)
77
+ end
78
+
79
+ def file_to_template(root, file)
80
+ AwsCftTools::Template.new(file.relative_path_from(root), options)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsCftTools
4
+ # Represents a change in a changeset.
5
+ class DeletionChange < Change
6
+ attr_reader :resource
7
+
8
+ ###
9
+ #
10
+ # @param resource
11
+ #
12
+ def initialize(resource)
13
+ @resource = resource
14
+ end
15
+
16
+ ###
17
+ # Return the action taken. For deletion, this is always +DELETE+.
18
+ #
19
+ # @return [String] +'DELETE'+ to indicate a deletion
20
+ #
21
+ def action
22
+ 'DELETE'
23
+ end
24
+
25
+ ###
26
+ # Return the status of this change as a replacement. For deletion, this is always +nil+.
27
+ #
28
+ # @return [nil]
29
+ #
30
+ def replacement
31
+ nil
32
+ end
33
+
34
+ ###
35
+ # Return the scopes of the change. For deletions, this is always +Resource+.
36
+ #
37
+ # @return [String] +'Resource'+
38
+ #
39
+ def scopes
40
+ 'Resource'
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsCftTools
4
+ ##
5
+ # = Dependency Tree
6
+ #
7
+ # Manage dependencies between CloudFormation templates based on exported and imported variables.
8
+ #
9
+ class DependencyTree
10
+ extend Forwardable
11
+
12
+ require_relative 'dependency_tree/nodes'
13
+ require_relative 'dependency_tree/variables'
14
+
15
+ attr_reader :filenames, :nodes, :variables
16
+
17
+ def initialize
18
+ @nodes = Nodes.new
19
+ @variables = Variables.new
20
+ @filenames = []
21
+ end
22
+
23
+ # @!method undefined_variables
24
+ # @see AwsCftTools::DependencyTree::Variables#undefined_variables
25
+ # @!method defined_variables
26
+ # @see AwsCftTools::DependencyTree::Variables#defined_variables
27
+ def_delegators :variables, :undefined_variables, :defined_variables
28
+ # @!method exported
29
+ # @see AwsCftTools::DependencyTree::Variables#defined
30
+ def_delegator :variables, :defined, :exported
31
+
32
+ # @!method dependencies_for
33
+ # @see AwsCftTools::DependencyTree::Nodes#dependencies_for
34
+ # @!method dependents_for
35
+ # @see AwsCftTools::DependencyTree::Nodes#dependents_for
36
+ def_delegators :nodes, :dependencies_for, :dependents_for
37
+
38
+ ##
39
+ # computes a topological sort and returns the filenames in that sort order
40
+ #
41
+ # @return [Array<String>]
42
+ #
43
+ def sort
44
+ nodes.tsort & filenames
45
+ end
46
+
47
+ ##
48
+ # notes that the given filename defines the given variable name
49
+ #
50
+ # @param filename [#to_s]
51
+ # @param variable [String]
52
+ #
53
+ def provided(filename, variable)
54
+ filename = filename.to_s
55
+ nodes.make_link(variable, filename)
56
+ @filenames |= [filename]
57
+ exported(variable)
58
+ end
59
+
60
+ ##
61
+ # notes that the given filename requires the given variable name to be defined before deployment
62
+ #
63
+ # @param filename [#to_s]
64
+ # @param variable [String]
65
+ #
66
+ def required(filename, variable)
67
+ filename = filename.to_s
68
+ nodes.make_link(filename, variable)
69
+ @filenames |= [filename]
70
+ variables.referenced(variable)
71
+ end
72
+
73
+ ##
74
+ # links two nodes in a directed fashion
75
+ #
76
+ # The template named by _from_ provides resources required by the template named by _to_.
77
+ #
78
+ # @param from [#to_s]
79
+ # @param to [#to_s]
80
+ #
81
+ def linked(from, to)
82
+ linker = "#{from}$$#{to}"
83
+ provided(from, linker)
84
+ required(to, linker)
85
+ end
86
+
87
+ ##
88
+ # finds a subset of the given set that has no dependencies outside the set
89
+ #
90
+ # @param set [Array<T>]
91
+ # @return [Array<T>]
92
+ #
93
+ def closed_subset(set)
94
+ # list all nodes that have no dependents outside the set
95
+ close_subset(set, &method(:dependents_for))
96
+ end
97
+
98
+ private
99
+
100
+ def close_subset(set, &block)
101
+ return [] unless block_given?
102
+ set - items_outside_subset(set, &block)
103
+ end
104
+
105
+ def items_outside_subset(set)
106
+ set.select { |node| (yield(node) - set).any? }
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsCftTools
4
+ class DependencyTree
5
+ ##
6
+ # Manages a list of nodes or vertices. Edges pass from a filename node to a variable node or from
7
+ # a variable node to a filename node, but never from a filename node to a filename node or from
8
+ # a variable node to a variable node.
9
+ #
10
+ class Nodes
11
+ include TSort
12
+
13
+ def initialize
14
+ @nodes = default_hash
15
+ @inverse_nodes = default_hash
16
+ end
17
+
18
+ ##
19
+ # Computes the direct dependencies of a node that are of the same type as the node. If the node
20
+ # is a filename, then the returned nodes will be filenames. Likewise with variable names.
21
+ #
22
+ # @param node [String]
23
+ # @return [Array<String>]
24
+ #
25
+ def dependencies_for(node)
26
+ double_hop(@nodes, node.to_s)
27
+ end
28
+
29
+ ##
30
+ # Computes the things dependent on the given node. If the node is a filename, then the returned
31
+ # nodes will be filenames. Likewise with variable names.
32
+ #
33
+ # @param node [String]
34
+ # @return [Array<String>]
35
+ #
36
+ def dependents_for(node)
37
+ double_hop(@inverse_nodes, node.to_s)
38
+ end
39
+
40
+ ##
41
+ # Draws a directed link from +from+ to +to+.
42
+ #
43
+ # @param from [String]
44
+ # @param to [String]
45
+ def make_link(from, to)
46
+ @nodes[from] << to
47
+ @inverse_nodes[to] << from
48
+ end
49
+
50
+ private
51
+
52
+ def double_hop(set, node)
53
+ # we hop from a filename to a variable child to a filename child
54
+ # or from a variable to a filename child to a variable child
55
+ set[node].flat_map { |neighbor| set[neighbor] }.uniq
56
+ end
57
+
58
+ def tsort_each_node(&block)
59
+ @nodes.each_key(&block)
60
+ end
61
+
62
+ def tsort_each_child(node, &block)
63
+ @nodes[node].each(&block) if @nodes.include?(node)
64
+ end
65
+
66
+ def default_hash
67
+ Hash.new { |hash, key| hash[key] = [] }
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsCftTools
4
+ class DependencyTree
5
+ ##
6
+ # Manage list of defined/undefined variables
7
+ #
8
+ class Variables
9
+ attr_reader :undefined_variables, :defined_variables
10
+
11
+ def initialize
12
+ @undefined_variables = []
13
+ @defined_variables = []
14
+ end
15
+
16
+ ##
17
+ # Notes that the given variable name is provided either by the CloudFormation environment or by
18
+ # another template.
19
+ #
20
+ # @param name [String]
21
+ #
22
+ def defined(name)
23
+ @undefined_variables -= [name]
24
+ @defined_variables |= [name]
25
+ end
26
+
27
+ ##
28
+ # Notes that the given variable name is used as an input into a template.
29
+ #
30
+ # @param name [String]
31
+ #
32
+ def referenced(name)
33
+ @undefined_variables |= [name] unless @defined_variables.include?(name)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsCftTools
4
+ ##
5
+ # Provides a root class for matching any internal exceptions.
6
+ #
7
+ class ToolingException < StandardError; end
8
+
9
+ ##
10
+ # Exception when a needed environment variable is not provided. Intended for use in
11
+ # parameter files.
12
+ class IncompleteEnvironmentError < ToolingException; end
13
+
14
+ ##
15
+ # Exception raised when the template or parameter source file can not be read and parsed.
16
+ class ParseException < ToolingException; end
17
+
18
+ ##
19
+ # Exception raised when the AWS SDK raises an error interacting with the AWS API.
20
+ class CloudFormationError < ToolingException; end
21
+
22
+ ##
23
+ # Exception raised when an expected resolvable template dependency can not be satisfied.
24
+ class UnsatisfiedDependencyError < ToolingException; end
25
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsCftTools
4
+ ##
5
+ # This is the base class for runbooks.
6
+ #
7
+ # A runbook is a command that can be accessed via the `aws-cft` script. A runbook is implemented as a
8
+ # subclass of this class.
9
+ #
10
+ # == Callbacks
11
+ # The AwsCftTools::CLI uses callbacks to manage runbook-specific options and behaviors.
12
+ #
13
+ # == Helpers
14
+ #
15
+ # The various helpers make managing logic flow much easier. Rather than having to be aware of how the
16
+ # different modes (+verbose+, +change+, +noop+) interplay and are configured, you can use the
17
+ # methods in this section to annotate the flow with your intent.
18
+ #
19
+ # @abstract Subclass and override {.default_options}, {.options}, and {#run} to implement
20
+ # a custom Runbook class.
21
+ class Runbook
22
+ require_relative 'runbook/report'
23
+
24
+ ##
25
+ # The AwsCftTools::Client instance used by the runbook.
26
+ #
27
+ attr_reader :client
28
+
29
+ ##
30
+ # The configuration options passed in to the runbook.
31
+ #
32
+ attr_reader :options
33
+
34
+ ##
35
+ # Recognized configuration options depend on the runbook but include any options valid for
36
+ # AwsCftTools::Client.
37
+ #
38
+ # Modes are selected by various configuration options:
39
+ #
40
+ # +:verbose+:: provide more narrative as the runbook executes
41
+ # +:noop+:: do nothing that could modify state
42
+ # +:change+:: do nothing that could permanently modify state, though some modifications are permitted
43
+ # in order to examine what might change if permanent changes were made
44
+ #
45
+ # @param configuration [Hash] Various options passed to the AwsCftTools::Client.
46
+ # @return AwsCftTools::Runbook
47
+ #
48
+ def initialize(configuration = {})
49
+ @options = configuration
50
+ @client = AwsCftTools::Client.new(options)
51
+ end
52
+
53
+ # @!group Callbacks
54
+
55
+ ##
56
+ # A callback to implement the runbook actions. Nothing is passed in or returned.
57
+ #
58
+ # @return void
59
+ #
60
+ def run; end
61
+
62
+ ##
63
+ # An internal wrapper around +#run+ to catch credential errors and print a useful message.
64
+ #
65
+ def _run
66
+ run
67
+ rescue Aws::Errors::MissingCredentialsError
68
+ puts 'Unable to access AWS without valid credentials. Either define a default credential' \
69
+ ' profile or use the -p option to specify an AWS credential profile.'
70
+ end
71
+
72
+ # @!group Helpers
73
+
74
+ ##
75
+ # @param description [String] an optional description of the operation
76
+ # @yield runs the block if not in +noop+ mode
77
+ # @return void
78
+ #
79
+ # Defines an operation that may or may not be narrated and that should not be run if in +noop+ mode.
80
+ #
81
+ # @example
82
+ # operation("considering the change" ) do
83
+ # checking("seeing what changes") { check_what_changes }
84
+ # doing("committing the change") { make_the_change }
85
+ # end
86
+ #
87
+ def operation(description = nil)
88
+ narrative(description)
89
+ return if options[:noop]
90
+
91
+ yield if block_given?
92
+ end
93
+
94
+ ##
95
+ # @param description [String] an optional description of the check
96
+ # @yield runs the block if in +check+ mode and not in +noop+ mode
97
+ # @return void
98
+ #
99
+ # Runs the given block when in +check+ mode and not in +noop+ mode. Outputs the description if the
100
+ # block is run.
101
+ #
102
+ # @example (see #operation)
103
+ #
104
+ def checking(description = nil)
105
+ return if options[:noop] || !options[:check]
106
+ output(description)
107
+ yield if block_given?
108
+ end
109
+
110
+ ##
111
+ # @param description [String] an optional description of the action
112
+ # @yield runs the block if not in +check+ or +noop+ mode
113
+ # @return void
114
+ #
115
+ # Runs the given block when not in +check+ or +noop+ mode. Outputs the description if the block is run.
116
+ #
117
+ # @example (see #operation)
118
+ #
119
+ def doing(description = nil)
120
+ return if options[:noop] || options[:check]
121
+ output(description)
122
+ yield if block_given?
123
+ end
124
+
125
+ ##
126
+ # @param description [String] an optional narrative string
127
+ # @return void
128
+ #
129
+ # Prints out the given description to stdout. If in +noop+ mode, +" (noop)"+ is appended.
130
+ #
131
+ def narrative(description = nil)
132
+ return unless description
133
+ if options[:noop]
134
+ puts "#{description} (noop)"
135
+ else
136
+ puts description
137
+ end
138
+ end
139
+
140
+ ##
141
+ # @param description [String] an optional verbose description
142
+ # @yield runs the block if in +verbose+ mode
143
+ # @return void
144
+ #
145
+ # Prints out the given description and runs the block if in +verbose+ mode. This is useful if the
146
+ # verbose narrative might be computationally expensive.
147
+ #
148
+ # @example
149
+ # detail do
150
+ # ... run some extensive processing to summarize stuff
151
+ # puts "The results are #{interesting}."
152
+ # end
153
+ #
154
+ def detail(description = nil)
155
+ return unless options[:verbose]
156
+ output(description)
157
+ yield if block_given?
158
+ end
159
+
160
+ private
161
+
162
+ def output(description)
163
+ puts description if description
164
+ end
165
+ end
166
+ end