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,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'table_print'
4
+
5
+ module AwsCftTools
6
+ class Runbook
7
+ ##
8
+ # A subclass of the Runbook designed for reporting out status or other information about resources.
9
+ #
10
+ class Report < Runbook
11
+ def run
12
+ tp(items, columns)
13
+ end
14
+
15
+ ###
16
+ # @return [Array<Object>]
17
+ #
18
+ def items
19
+ []
20
+ end
21
+
22
+ ###
23
+ # @return [Array<String>]
24
+ #
25
+ def columns
26
+ []
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsCftTools
4
+ ##
5
+ # Namespace for runbooks
6
+ #
7
+ module Runbooks
8
+ require_relative 'runbooks/deploy'
9
+ require_relative 'runbooks/diff'
10
+ require_relative 'runbooks/hosts'
11
+ require_relative 'runbooks/images'
12
+ require_relative 'runbooks/init'
13
+ require_relative 'runbooks/retract'
14
+ require_relative 'runbooks/stacks'
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsCftTools
4
+ module Runbooks
5
+ module Common
6
+ ##
7
+ # Changesets - operations on changesets in the deploy runbook
8
+ #
9
+ module Changesets
10
+ private
11
+
12
+ # @todo store this somewhere so we can have an "active" changeset to be reviewed and committed.
13
+ #
14
+ def changeset_set
15
+ @changeset_set ||= SecureRandom.hex(16)
16
+ end
17
+
18
+ ##
19
+ # provide a tabular report of changeset actions
20
+ #
21
+ def narrate_changes(changes)
22
+ tp(
23
+ changes.map(&:to_narrative),
24
+ %i[action logical_id physical_id type replacement scopes]
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsCftTools
4
+ module Runbooks
5
+ module Common
6
+ ##
7
+ # Templates - operations on templates in multiple runbooks
8
+ #
9
+ module Templates
10
+ private
11
+
12
+ def filtered_templates(set)
13
+ filtered_by_environment(
14
+ filtered_by_role(
15
+ filtered_by_selection(options[:templates], set)
16
+ )
17
+ )
18
+ end
19
+
20
+ def filtered_by_role(set)
21
+ set.select { |template| template.role?(options[:role]) }
22
+ end
23
+
24
+ def filtered_by_environment(set)
25
+ set.select { |template| template.environment?(options[:environment]) }
26
+ end
27
+
28
+ def filtered_by_selection(templates, set)
29
+ if templates && templates.any?
30
+ set.select { |template| templates.include?(template.filename.to_s) }
31
+ else
32
+ set
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module AwsCftTools
6
+ module Runbooks
7
+ ##
8
+ # Deploy - manage CloudFormation stack deployment
9
+ #
10
+ # @example
11
+ # % aws-cft deploy -e QA # deploy all templates to the QA environment
12
+ # % aws-cft deploy -e Staging -n -v # narrate the templates that would be used for Staging
13
+ # % aws-cft deploy -e Production -c -v # narrate the changes that would go into Production
14
+ #
15
+ class Deploy < Runbook
16
+ require_relative 'common/changesets'
17
+ require_relative 'common/templates'
18
+ require_relative 'deploy/reporting'
19
+ require_relative 'deploy/stacks'
20
+ require_relative 'deploy/templates'
21
+ require_relative 'deploy/threading'
22
+
23
+ extend Forwardable
24
+
25
+ include Common::Changesets
26
+ include Common::Templates
27
+ include Reporting
28
+ include Stacks
29
+ include Templates
30
+ include Threading
31
+
32
+ def_delegators :client, :images
33
+ def_delegator :client, :all_stacks, :stacks
34
+
35
+ def run
36
+ run_reports
37
+
38
+ detail 'Updating template parameters...'
39
+ update_parameters
40
+
41
+ process_templates(options[:jobs] || 1)
42
+ end
43
+
44
+ private
45
+
46
+ def process_templates(slice_size)
47
+ templates_in_folder_order.each_slice(slice_size, &method(:process_slice))
48
+ end
49
+
50
+ def process_slice(templates)
51
+ old_stdout = $stdout
52
+ threads = create_threads(templates) { |template| process_template(template) }
53
+ threads.map(&:thread).map(&:join)
54
+ puts threads.map(&:output).map(&:string)
55
+ ensure
56
+ $stdout = old_stdout # just in case!
57
+ end
58
+
59
+ def process_template(template)
60
+ is_update = deployed_templates.include?(template)
61
+ operation("#{is_update ? 'Updating' : 'Creating'}: #{template.name}") do
62
+ exec_template(template: template, type: is_update ? :update : :create)
63
+ end
64
+ end
65
+
66
+ def exec_template(params) # template:, type:
67
+ checking { exec_template_check(**params) }
68
+ doing { exec_template_for_real(**params) }
69
+ end
70
+
71
+ def exec_template_check(template:, type:)
72
+ narrate_changes(client.send(:"changes_on_stack_#{type}", template, changeset_set))
73
+ rescue Aws::CloudFormation::Errors::ValidationError => error
74
+ puts "Error checking #{template.filename}: #{error.message}"
75
+ end
76
+
77
+ def exec_template_for_real(template:, type:)
78
+ client.send(:"#{type}_stack", template)
79
+ rescue Aws::CloudFormation::Errors::ValidationError => error
80
+ raise AwsCftTools::CloudFormationError, "Error processing #{template.filename}: #{error}"
81
+ end
82
+
83
+ ##
84
+ # update_parameters - notate templates with region and image id as appropriate
85
+ #
86
+ def update_parameters
87
+ templates.each { |template| update_template_with_image_id(template) }
88
+ end
89
+
90
+ ##
91
+ # the set of templates corresponding to deployed CloudFormation stacks
92
+ #
93
+ def deployed_templates
94
+ @deployed_templates ||= templates_in_folder_order.select do |template|
95
+ deployed_stack_names.include?(template.name)
96
+ end
97
+ end
98
+
99
+ ##
100
+ # the set of templates with no corresponding deployed CloudFormation stack
101
+ #
102
+ def new_templates
103
+ @new_templates ||= templates_in_folder_order - deployed_templates
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'table_print'
4
+
5
+ module AwsCftTools
6
+ module Runbooks
7
+ class Deploy
8
+ ##
9
+ # module with various reporting functions for deployment
10
+ #
11
+ module Reporting
12
+ private
13
+
14
+ def run_reports
15
+ report_available_images
16
+ report_undefined_variables
17
+
18
+ detail do
19
+ tp(templates, ['filename'])
20
+ end
21
+ end
22
+
23
+ ##
24
+ # report_available_images - provide tabular report of available images
25
+ #
26
+ def report_available_images
27
+ detail('Available Images') { tp(report_available_images_data) }
28
+ end
29
+
30
+ def report_available_images_data
31
+ available_images.map { |role_env, ami| role_env.split(/:/).reverse + [ami] }
32
+ .compact
33
+ .sort
34
+ .map { |role_env_ami| %w[role environment ami].zip(role_env_ami).to_h }
35
+ end
36
+
37
+ ##
38
+ # report_undefined_image - provide list of undefined imports that block stack deployment
39
+ #
40
+ def report_undefined_variables
41
+ vars = templates_in_folder_order.undefined_variables
42
+ return unless vars.any?
43
+ puts '*** Unable to update or create templates.'
44
+ puts 'The following variables are referenced but not defined: ', vars
45
+ exit 1 # TODO: convert to a raise
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsCftTools
4
+ module Runbooks
5
+ class Deploy
6
+ ##
7
+ # Stacks - operations on stacks in the deploy runbook
8
+ #
9
+ module Stacks
10
+ private
11
+
12
+ ##
13
+ # Provides the next environment to look at if the current one fails.
14
+ #
15
+ # This is mainly used to find an AMI that is appropriate for the environment. Since we want
16
+ # to be able to promote AMIs from one environment to the next without rebuilding them, we
17
+ # should use the AMI that is most appropriate even if it's not tagged with the environment
18
+ # in scope.
19
+ #
20
+ # This can be configured in the configuration file with the +:environment_successors+ key.
21
+ #
22
+ def successor_environment(env)
23
+ (options[:environment_successors] || {})[env]
24
+ end
25
+
26
+ ##
27
+ # list the available images by role and environment
28
+ #
29
+ # An image for a role/environment is the most recent AMI tagged with that role and environment.
30
+ #
31
+ def available_images
32
+ @available_images ||= images.sort_by(&:created_at).each_with_object({}) do |image, mapping|
33
+ key = image_key(image)
34
+ mapping[key] = image.image_id if key != ':'
35
+ end
36
+ end
37
+
38
+ def image_key(image)
39
+ (image.environment || '') + ':' + (image.role || '')
40
+ end
41
+
42
+ ##
43
+ # retrieve the appropriate AMI identifier for an environment/role
44
+ #
45
+ # This takes into account environment succession (see #successor_environment) to find the
46
+ # best image for an environment/role as determined by #available_images.
47
+ #
48
+ def find_image(role, env)
49
+ key = "#{env}:#{role}"
50
+ image = available_images[key]
51
+ return image if image
52
+ next_env = successor_environment(env)
53
+ find_image(role, next_env) if next_env
54
+ end
55
+
56
+ ##
57
+ # list the template files containing `Region` metadata
58
+ #
59
+ def files_with_region_param
60
+ @files_with_region_param ||=
61
+ templates.select { |template| template.allowed_regions.any? }.map(&:filename)
62
+ end
63
+
64
+ ##
65
+ # list the template source filenames for deployed stacks
66
+ #
67
+ # This returns the filename as reported by the `Source` tag on the CloudFormation stack.
68
+ #
69
+ def deployed_stack_names
70
+ stacks.map(&:name).compact
71
+ end
72
+
73
+ ##
74
+ # list the filenames of all of the in-scope templates
75
+ #
76
+ def files
77
+ templates.map(&:filename)
78
+ end
79
+
80
+ ##
81
+ # add `ImageId` parameter to templates as-needed
82
+ #
83
+ def update_template_with_image_id(template)
84
+ params = template.parameters
85
+ params.each do |key, value|
86
+ update_params_with_image_id(params, key, value)
87
+ end
88
+ end
89
+
90
+ def update_params_with_image_id(params, key, value)
91
+ return unless value.is_a?(Hash)
92
+ role = value['Role']
93
+ image = find_image(role, options[:environment])
94
+ params[key] = image if role
95
+ report_undefined_image(role) if role && !image
96
+ end
97
+
98
+ ##
99
+ # utility function to create an error message about undefined AMIs
100
+ #
101
+ def report_undefined_image(role)
102
+ puts format('Unable to find image for %s suitable for %s',
103
+ role,
104
+ options[:environment])
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsCftTools
4
+ module Runbooks
5
+ class Deploy
6
+ ##
7
+ # module with methods to manage ordering of templates
8
+ #
9
+ module Templates
10
+ ##
11
+ # list the templates in-scope for this deployment
12
+ #
13
+ def templates
14
+ @templates ||= begin
15
+ candidates = client.templates
16
+
17
+ candidates.closure(
18
+ filtered_templates(
19
+ candidates
20
+ )
21
+ )
22
+ end
23
+ end
24
+
25
+ def template_folder_order
26
+ options[:template_folder_priorities] || []
27
+ end
28
+
29
+ private
30
+
31
+ def templates_in_folder_order
32
+ templates.in_folder_order(template_folder_order)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsCftTools
4
+ module Runbooks
5
+ class Deploy
6
+ ##
7
+ # module with methods to manage threading
8
+ #
9
+ module Threading
10
+ private
11
+
12
+ # FIXME: things don't always work out well when capturing output
13
+ # for now, we don't, and output gets mangled a bit when running with
14
+ # multiple jobs in parallel
15
+ def with_captured_stdout(capture)
16
+ old_stdout = $stdout
17
+ old_table_io = TablePrint::Config.io
18
+ TablePrint::Config.io = $stdout = capture
19
+ yield
20
+ ensure
21
+ $stdout = old_stdout
22
+ TablePrint::Config.io = old_table_io
23
+ end
24
+
25
+ def create_threads(list, &_block)
26
+ list.map { |item| threaded_process { yield item } }
27
+ end
28
+
29
+ def threaded_process(&block)
30
+ output = StringIO.new
31
+ thread = Thread.new { with_captured_stdout(output, &block) }
32
+ OpenStruct.new(output: output, thread: thread)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end