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