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.
- checksums.yaml +7 -0
- data/.editorconfig +10 -0
- data/.gitignore +52 -0
- data/.rspec +2 -0
- data/.rubocop.yml +19 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/BEST-PRACTICES.md +136 -0
- data/CONTRIBUTING.md +38 -0
- data/Gemfile +8 -0
- data/LICENSE +15 -0
- data/README.md +118 -0
- data/Rakefile +17 -0
- data/USAGE.adoc +404 -0
- data/aws-cft-tools.gemspec +53 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/code.json +24 -0
- data/exe/aws-cft +176 -0
- data/lib/aws-cft-tools.rb +3 -0
- data/lib/aws_cft_tools.rb +31 -0
- data/lib/aws_cft_tools/aws_enumerator.rb +55 -0
- data/lib/aws_cft_tools/change.rb +66 -0
- data/lib/aws_cft_tools/client.rb +84 -0
- data/lib/aws_cft_tools/client/base.rb +40 -0
- data/lib/aws_cft_tools/client/cft.rb +93 -0
- data/lib/aws_cft_tools/client/cft/changeset_management.rb +109 -0
- data/lib/aws_cft_tools/client/cft/stack_management.rb +85 -0
- data/lib/aws_cft_tools/client/ec2.rb +136 -0
- data/lib/aws_cft_tools/client/templates.rb +84 -0
- data/lib/aws_cft_tools/deletion_change.rb +43 -0
- data/lib/aws_cft_tools/dependency_tree.rb +109 -0
- data/lib/aws_cft_tools/dependency_tree/nodes.rb +71 -0
- data/lib/aws_cft_tools/dependency_tree/variables.rb +37 -0
- data/lib/aws_cft_tools/errors.rb +25 -0
- data/lib/aws_cft_tools/runbook.rb +166 -0
- data/lib/aws_cft_tools/runbook/report.rb +30 -0
- data/lib/aws_cft_tools/runbooks.rb +16 -0
- data/lib/aws_cft_tools/runbooks/common/changesets.rb +30 -0
- data/lib/aws_cft_tools/runbooks/common/templates.rb +38 -0
- data/lib/aws_cft_tools/runbooks/deploy.rb +107 -0
- data/lib/aws_cft_tools/runbooks/deploy/reporting.rb +50 -0
- data/lib/aws_cft_tools/runbooks/deploy/stacks.rb +109 -0
- data/lib/aws_cft_tools/runbooks/deploy/templates.rb +37 -0
- data/lib/aws_cft_tools/runbooks/deploy/threading.rb +37 -0
- data/lib/aws_cft_tools/runbooks/diff.rb +28 -0
- data/lib/aws_cft_tools/runbooks/diff/context.rb +86 -0
- data/lib/aws_cft_tools/runbooks/diff/context/reporting.rb +87 -0
- data/lib/aws_cft_tools/runbooks/hosts.rb +43 -0
- data/lib/aws_cft_tools/runbooks/images.rb +43 -0
- data/lib/aws_cft_tools/runbooks/init.rb +86 -0
- data/lib/aws_cft_tools/runbooks/retract.rb +69 -0
- data/lib/aws_cft_tools/runbooks/retract/templates.rb +44 -0
- data/lib/aws_cft_tools/runbooks/stacks.rb +43 -0
- data/lib/aws_cft_tools/stack.rb +83 -0
- data/lib/aws_cft_tools/template.rb +177 -0
- data/lib/aws_cft_tools/template/dsl_context.rb +14 -0
- data/lib/aws_cft_tools/template/file_system.rb +62 -0
- data/lib/aws_cft_tools/template/metadata.rb +144 -0
- data/lib/aws_cft_tools/template/properties.rb +129 -0
- data/lib/aws_cft_tools/template_set.rb +120 -0
- data/lib/aws_cft_tools/template_set/array_methods.rb +63 -0
- data/lib/aws_cft_tools/template_set/closure.rb +77 -0
- data/lib/aws_cft_tools/template_set/dependencies.rb +55 -0
- data/lib/aws_cft_tools/template_set/each_slice_state.rb +58 -0
- data/lib/aws_cft_tools/version.rb +8 -0
- data/rubycritic.reek +3 -0
- 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
|