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