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,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsCftTools
4
+ module Runbooks
5
+ class Retract
6
+ ##
7
+ # module with methods to manage ordering of templates
8
+ #
9
+ module Templates
10
+ require_relative '../common/templates'
11
+
12
+ include Common::Templates
13
+
14
+ ##
15
+ # list the templates in-scope for this retraction
16
+ #
17
+ # @return [AwsCftTools::TemplateSet]
18
+ #
19
+ def templates
20
+ @templates ||= filtered_templates(client.templates)
21
+ end
22
+
23
+ ##
24
+ # List the templates that are available for deletion.
25
+ #
26
+ # Templates with known dependents that are not in the set will be removed. Note that this does
27
+ # not capture dependencies between environments.
28
+ #
29
+ # @return [AwsCftTools::TemplateSet]
30
+ def free_templates
31
+ set = AwsCftTools::TemplateSet.new(templates.in_folder_order(template_folder_order))
32
+ client.templates.closed_subset(set).reverse
33
+ end
34
+
35
+ ##
36
+ # @return [Array<String>]
37
+ #
38
+ def template_folder_order
39
+ options[:template_folder_priorities] || []
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsCftTools
4
+ module Runbooks
5
+ ##
6
+ # Images - report on available AMIs
7
+ #
8
+ # @example
9
+ # % aws-cli stacks # list all known stacks
10
+ # % aws-cli stacks -e QA # list all known stacks tagged for the QA environment
11
+ # % aws-cli stacks -e QA -r App # list all known stacks tagged for the QA environment and App role
12
+ #
13
+ class Stacks < Runbook::Report
14
+ ###
15
+ # @return [Array<AwsCftTools::Stack>]
16
+ #
17
+ def items
18
+ client.stacks.sort_by(&method(:sort_key))
19
+ end
20
+
21
+ ###
22
+ # @return [Array<String>]
23
+ #
24
+ def columns
25
+ environment_column + role_column + %w[filename created_at name state]
26
+ end
27
+
28
+ private
29
+
30
+ def sort_key(stack)
31
+ stack.name
32
+ end
33
+
34
+ def environment_column
35
+ options[:environment] ? [] : ['environment']
36
+ end
37
+
38
+ def role_column
39
+ options[:role] ? [] : ['role']
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsCftTools
4
+ ##
5
+ # Provides a unified interface for accessing information about deployed CloudFormation templates.
6
+ #
7
+ class Stack
8
+ extend Forwardable
9
+
10
+ def initialize(aws_stack, aws_client)
11
+ @aws_client = aws_client
12
+ @aws_stack = aws_stack
13
+ end
14
+
15
+ def_delegators :@aws_stack, :description
16
+ def_delegator :@aws_stack, :stack_name, :name
17
+ def_delegator :@aws_stack, :creation_time, :created_at
18
+ def_delegator :@aws_stack, :last_updated_time, :updated_at
19
+ def_delegator :@aws_stack, :stack_status, :state
20
+ def_delegator :@aws_stack, :stack_id, :id
21
+
22
+ ###
23
+ # @return [String] the unparsed body of the template definition
24
+ #
25
+ def template_source
26
+ @template ||= begin
27
+ resp = @aws_client.get_template(stack_name: name,
28
+ template_stage: 'Original')
29
+ resp.template_body
30
+ end
31
+ end
32
+
33
+ ##
34
+ # @return [Hash] dictionary of tag names and values for the stack
35
+ #
36
+ def tags
37
+ @tags ||= @aws_stack.tags.each_with_object({}) { |tag, hash| hash[tag.key] = tag.value }
38
+ end
39
+
40
+ ##
41
+ # @return [Hash] mapping of output name with output definition
42
+ def outputs
43
+ @outputs ||= build_hashes(@aws_stack.outputs || [], &:output_key)
44
+ end
45
+
46
+ ##
47
+ # @return [Hash] mapping of parameter name to parameter definition
48
+ #
49
+ def parameters
50
+ @parameters ||= build_hashes(@aws_stack.parameters || [], &:parameter_key)
51
+ end
52
+
53
+ ##
54
+ # @return [String] the environment of the stack
55
+ #
56
+ def environment
57
+ tags['Environment']
58
+ end
59
+
60
+ ##
61
+ # @return [String] the role of the stack
62
+ #
63
+ def role
64
+ tags['Role']
65
+ end
66
+
67
+ ##
68
+ # @return [String] the filename of the stack's template source
69
+ #
70
+ def filename
71
+ @filename ||= begin
72
+ source = tags['Source']
73
+ source ? source.sub(%r{^/+}, '') : nil
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def build_hashes(source, &block)
80
+ source.map(&block).zip(source).to_h
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'erb'
5
+ require 'pathname'
6
+
7
+ module AwsCftTools
8
+ ##
9
+ # The AwsCftTools::Template class wraps a CloudFormation template source to provide support for various
10
+ # operations by the toolset.
11
+ #
12
+ # == CloudFormation Templates
13
+ #
14
+ # As much as possible, this tool uses CloudFormation templates as-is and makes as many inferences as
15
+ # reasonable. However, some things aren't captured in stock template information.
16
+ #
17
+ # @note Stacks should be removed ("retracted") from AWS before they are removed from the set of templates.
18
+ # Otherwise, they won't be considered in the set of available templates or stacks.
19
+ #
20
+ # @todo Fetch templates from deployed stacks and consider them in the dependency tree for removing or
21
+ # deploying templates in the repo. Flag stacks with no local source to not be updated on deployment.
22
+ #
23
+ # @todo Add ability to init to fetch templates from stacks and put them in files.
24
+ #
25
+ # === Allowed Environments
26
+ #
27
+ # The environments in which a template should be deployed is provided by the +AllowedValues+ key of the
28
+ # +Environment+ template parameter.
29
+ #
30
+ # @example Allowed Environments
31
+ # ---
32
+ # Parameters:
33
+ # Environment:
34
+ # AllowedValues:
35
+ # - QA
36
+ # - Staging
37
+ # - Production
38
+ #
39
+ # === Allowed Regions
40
+ #
41
+ # A template can be pinned to a particular region or set of regions by providing a list of values for
42
+ # the +Region+ key in the template metadata. If no such key is present, then the template can be
43
+ # deployed or otherwise used in all regions.
44
+ #
45
+ # @example Allowed Regions
46
+ # ---
47
+ # Metadata:
48
+ # Region:
49
+ # - us-east-1
50
+ # - us-west-1
51
+ #
52
+ # === Explicit Template Dependencies
53
+ #
54
+ # As much as possible, dependencies between templates are inferred based on exported and imported
55
+ # values. However, some templates might depend on another template in a way that isn't captured by
56
+ # these values. For those dependencies, the templates that should be run first can be listed under the
57
+ # +DependesOn.Templates+ metadata key.
58
+ #
59
+ # @example Explicit Template Dependency
60
+ # ---
61
+ # Metadata:
62
+ # DependsOn:
63
+ # Templates:
64
+ # - network/peering.yaml
65
+ #
66
+ # === Template Parameters
67
+ #
68
+ # Rather than require mappings in templates to hold environment-specific values, a template has a
69
+ # corresponding parameters file that holds the value for the stack parameter for each environment.
70
+ # This parameters file is in YAML format and passed through ERB before parsing, so it can incorporate
71
+ # environment variables and other logic into specifying parameter values.
72
+ #
73
+ class Template
74
+ attr_reader :filename
75
+
76
+ require_relative 'template/dsl_context'
77
+ require_relative 'template/file_system'
78
+ require_relative 'template/metadata'
79
+ require_relative 'template/properties'
80
+
81
+ include FileSystem
82
+ include Metadata
83
+ include Properties
84
+
85
+ ##
86
+ # @param filename [String] path to template relative to the +template_dir+ path
87
+ # @param options [Hash] runbook options
88
+ # @option options [String] :environment environment in which parameters should be fetched
89
+ # @option options [String] :parameter_dir directory relative to the +root+ path in which parameter files
90
+ # are found
91
+ # @option options [Pathname] :root path to the root of the project
92
+ # @option options [String] :template_dir directory relative to the +root+ path in which template sources
93
+ # are found
94
+ #
95
+ def initialize(filename, options = {})
96
+ @options = options
97
+ @filename = filename
98
+ end
99
+
100
+ ##
101
+ # @return [Array<Hash>] template tags suitable for use in deploying a stack
102
+ #
103
+ def tags
104
+ [
105
+ { key: 'Environment', value: @options[:environment] },
106
+ { key: 'Source', value: ('/' + filename.to_s).gsub(%r{/+}, '/') }
107
+ ] + role_tag
108
+ end
109
+
110
+ ##
111
+ # @return [Hash] parameters to provide to the AWS client to deploy the template
112
+ #
113
+ def stack_parameters
114
+ {
115
+ stack_name: name,
116
+ template_body: template_source_for_aws,
117
+ parameters: hash_to_param_list(parameters || {}),
118
+ tags: tags
119
+ }
120
+ end
121
+
122
+ private
123
+
124
+ def role_tag
125
+ if role
126
+ [{ key: 'Role', value: role }]
127
+ else
128
+ []
129
+ end
130
+ end
131
+
132
+ def hash_to_param_list(hash)
133
+ hash.map do |key, value|
134
+ {
135
+ parameter_key: key.to_s,
136
+ parameter_value: value_to_string(value),
137
+ use_previous_value: !value && value != false || value == ''
138
+ }
139
+ end
140
+ end
141
+
142
+ def value_to_string(value)
143
+ case value
144
+ when false
145
+ 'false'
146
+ when true
147
+ 'true'
148
+ else
149
+ value ? value.to_s : value
150
+ end
151
+ end
152
+
153
+ ##
154
+ # Looks through the template to find instances of +Fn::ImportValue+
155
+ #
156
+ def pull_imports(hash)
157
+ hash.flat_map do |key, value|
158
+ value ||= key
159
+ if %w[Fn::ImportValue ImportValue].include?(key)
160
+ pull_import(value)
161
+ elsif value.is_a?(Hash) || value.is_a?(Array)
162
+ pull_imports(value)
163
+ else
164
+ []
165
+ end
166
+ end
167
+ end
168
+
169
+ def pull_import(value)
170
+ if value.is_a?(Hash)
171
+ [value['Fn::Sub'] || value['Sub'] || value]
172
+ else
173
+ [value]
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsCftTools
4
+ class Template
5
+ ##
6
+ # Utility module for evaluating ruby dsl templates.
7
+ #
8
+ module DSLContext
9
+ require 'cloudformation-ruby-dsl/cfntemplate'
10
+ require 'cloudformation-ruby-dsl/spotprice'
11
+ require 'cloudformation-ruby-dsl/table'
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module AwsCftTools
6
+ class Template
7
+ ##
8
+ # Manage template and parameter files.
9
+ #
10
+ module FileSystem
11
+ ##
12
+ # Returns the path to the cloud formation template.
13
+ #
14
+ # @return [Pathname]
15
+ def template_file
16
+ filename_path(:template_dir, filename)
17
+ end
18
+
19
+ ##
20
+ # Returns the path to the template parameters file.
21
+ #
22
+ # @return [Pathname]
23
+ def parameter_file
24
+ filename_path(:parameter_dir, filename)
25
+ end
26
+
27
+ ##
28
+ # The unparsed source of the template.
29
+ #
30
+ # @return [String]
31
+ def template_source
32
+ @template_source ||= @options[:template_content] || read_file(template_file)
33
+ end
34
+
35
+ ##
36
+ # The unparsed source of the parameters file for this template.
37
+ #
38
+ # @return [String]
39
+ def parameters_source
40
+ @parameters_source ||= @options[:parameters_content] || read_file(parameter_file)
41
+ end
42
+
43
+ private
44
+
45
+ def read_file(file)
46
+ file ? file.read : nil
47
+ end
48
+
49
+ ##
50
+ # Given the filename relative to the template/parameter root and a symbol indicating which type of
51
+ # file to point to, returns the full path to the file
52
+ #
53
+ # @return [Pathname]
54
+ def filename_path(dir, filename)
55
+ # we need to check .yaml, .yml, and .json versions
56
+ filename = filename.to_s.sub(/\.[^.]*$/, '')
57
+ base = @options[:root] + @options[dir]
58
+ %w[.yaml .yml .json .rb].map { |ext| base + (filename + ext) }.detect(&:exist?)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module AwsCftTools
6
+ class Template
7
+ ##
8
+ # Simple derived information about templates.
9
+ #
10
+ module Metadata
11
+ ##
12
+ # Mapping of filename extensions to content types.
13
+ #
14
+ CONTENT_TYPES = {
15
+ '.json' => 'json',
16
+ '.rb' => 'dsl',
17
+ '.yml' => 'yaml',
18
+ '.yaml' => 'yaml'
19
+ }.freeze
20
+
21
+ ##
22
+ # The type of template content.
23
+ #
24
+ # @return [String] One of +dsl+, +json+, or +yaml+.
25
+ def template_type
26
+ content_type(template_file)
27
+ end
28
+
29
+ ##
30
+ # Queries if the template source looks like a CloudFormation template.
31
+ #
32
+ # @return [Boolean]
33
+ def template?
34
+ template && template['AWSTemplateFormatVersion']
35
+ end
36
+
37
+ ##
38
+ # The name of the stack built by this template.
39
+ #
40
+ # @return [String]
41
+ def name
42
+ @name ||= @options[:environment] + '-' +
43
+ filename.to_s.sub(/\.(ya?ml|json|rb)$/, '').split(%r{/}).reverse.join('-')
44
+ end
45
+
46
+ ##
47
+ # The parsed template as a Ruby data structure.
48
+ #
49
+ # @return [Hash]
50
+ def template
51
+ @template ||= template_content_for_filename(template_file)
52
+ end
53
+
54
+ ##
55
+ # The JSON or YAML source that can be submitted to AWS to build the stack.
56
+ #
57
+ # @return [String]
58
+ def template_source_for_aws
59
+ template_type == 'dsl' ? JSON.pretty_generate(template) : template_source
60
+ end
61
+
62
+ ##
63
+ # The parsed parameters for this template in the deployment environment.
64
+ #
65
+ # @return [Hash]
66
+ def parameters
67
+ @parameters ||= parameters_for_filename_and_environment(parameters_source, @options[:environment])
68
+ end
69
+
70
+ private
71
+
72
+ def content_type(file)
73
+ CONTENT_TYPES[file.extname] if file
74
+ end
75
+
76
+ ##
77
+ # Loads the contents of the full path and returns the content as a Ruby data structure
78
+ #
79
+ # @return [String]
80
+ def template_content_for_filename(file)
81
+ type = content_type(file)
82
+ return {} unless type
83
+ send(:"template_content_for_#{type}!")
84
+ rescue => exception
85
+ raise AwsCftTools::ParseException, "Error while parsing #{template_file}: #{exception}"
86
+ end
87
+
88
+ def template_content_for_yaml!
89
+ YAML.safe_load(template_source, [Date], [], true) || {}
90
+ end
91
+
92
+ def template_content_for_json!
93
+ JSON.parse(template_source) || {}
94
+ end
95
+
96
+ def template_content_for_dsl!
97
+ with_environment { JSON.parse(DSLContext.module_eval(template_source).to_json) }
98
+ end
99
+
100
+ ##
101
+ # Loads the contents of the full path and passes it through ERB before parsing as YAML. Returns
102
+ # a Ruby structure.
103
+ #
104
+ # If no file exists, then a simple hash with the +Environment+ key set.
105
+ #
106
+ def parameters_for_filename_and_environment(param_source, env)
107
+ parameters_for_filename_and_environment!(param_source, env)
108
+ rescue AwsCftTools::ToolingException
109
+ raise
110
+ rescue => exception
111
+ raise AwsCftTools::ParseException, "Error while reading and parsing #{parameter_file}: #{exception}"
112
+ end
113
+
114
+ def parameters_for_filename_and_environment!(param_source, env)
115
+ return { Environment: env } unless param_source
116
+
117
+ params = YAML.safe_load(process_erb_file(param_source), [], [], true)[env] || {}
118
+ params.update(Environment: env)
119
+ end
120
+
121
+ def process_erb_file(content)
122
+ with_environment { ERB.new(content).result }
123
+ end
124
+
125
+ def with_environment
126
+ return unless block_given?
127
+ prior = ENV.to_h
128
+ ENV.update(aws_env)
129
+ yield
130
+ ensure
131
+ ENV.update(prior)
132
+ end
133
+
134
+ def aws_env
135
+ region = @options[:region]
136
+ {
137
+ 'AWS_REGION' => region,
138
+ 'EC2_REGION' => region,
139
+ 'AWS_PROFILE' => @options[:profile]
140
+ }
141
+ end
142
+ end
143
+ end
144
+ end