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