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,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsCftTools
4
+ class Client
5
+ ##
6
+ # = CloudFormation Client
7
+ #
8
+ # All of the business logic behind direct interaction with the AWS API for CloudFormation templates
9
+ # and stacks.
10
+ #
11
+ class Base
12
+ attr_reader :options
13
+
14
+ ##
15
+ #
16
+ # @param options [Hash] client configuration
17
+ # @option options [String] :environment the operational environment in which to act
18
+ # @option options [String] :profile the AWS credential profile to use
19
+ # @option options [String] :region the AWS region in which to act
20
+ #
21
+ def initialize(options = {})
22
+ @options = options
23
+ end
24
+
25
+ ##
26
+ # The AWS SDK client object for this part of the AwsCftTools client
27
+ def aws_client
28
+ @aws_client ||= begin
29
+ klass = self.class.aws_client_class
30
+ klass && klass.new(
31
+ region: options[:region],
32
+ profile: options[:profile]
33
+ )
34
+ end
35
+ end
36
+
37
+ def self.aws_client_class; end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsCftTools
4
+ class Client
5
+ ##
6
+ # = CloudFormation Client
7
+ #
8
+ # All of the business logic behind direct interaction with the AWS API for CloudFormation templates
9
+ # and stacks.
10
+ #
11
+ class CFT < Base
12
+ require_relative 'cft/changeset_management'
13
+ require_relative 'cft/stack_management'
14
+
15
+ include ChangesetManagement
16
+ include StackManagement
17
+
18
+ ##
19
+ #
20
+ # @param options [Hash] client configuration
21
+ # @option options [String] :environment the operational environment in which to act
22
+ # @option options [String] :profile the AWS credential profile to use
23
+ # @option options [String] :region the AWS region in which to act
24
+ #
25
+ def initialize(options)
26
+ super(options)
27
+ end
28
+
29
+ def self.aws_client_class
30
+ Aws::CloudFormation::Client
31
+ end
32
+
33
+ ##
34
+ # Lists all exports from stacks in CloudFormation.
35
+ #
36
+ # @return [Array<Aws::CloudFormation::Types::Export>]
37
+ #
38
+ def exports
39
+ @exports ||= AWSEnumerator.new(aws_client, :list_exports, {}, &:exports).to_a
40
+ end
41
+
42
+ ##
43
+ # Lists all of the stacks in CloudFormation that are specific to the selected environment.
44
+ #
45
+ # @return [Array<OpenStruct>]
46
+ #
47
+ def stacks
48
+ @stacks ||= all_stacks.select do |stack|
49
+ tags = stack.tags
50
+ satisfies_environment(tags) &&
51
+ satisfies_role(tags) &&
52
+ satisfies_tags(tags)
53
+ end
54
+ end
55
+
56
+ ##
57
+ # List all of the stacks in CloudFormation.
58
+ #
59
+ # @return [Array<OpenStruct>]
60
+ #
61
+ def all_stacks
62
+ @all_stacks ||= AWSEnumerator.new(aws_client, :describe_stacks, &method(:map_stacks)).to_a
63
+ end
64
+
65
+ private
66
+
67
+ def map_stacks(resp)
68
+ resp.stacks.map { |stack| Stack.new(stack, aws_client) }
69
+ end
70
+
71
+ def satisfies_environment(tag_set)
72
+ env = options[:environment]
73
+ !env || tag_set['Environment'] == env
74
+ end
75
+
76
+ def satisfies_role(tag_set)
77
+ role = options[:role]
78
+ !role || tag_set['Role'] == role
79
+ end
80
+
81
+ def satisfies_tags(tag_set)
82
+ tags = options[:tags]
83
+ return true unless tags
84
+ tag_set.all? { |key, value| satisfies_tag(tags, key, value) }
85
+ end
86
+
87
+ def satisfies_tag(tags, key, value)
88
+ tag = tags[key]
89
+ !tag || tag == value
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsCftTools
4
+ class Client
5
+ class CFT
6
+ ##
7
+ # Provides changeset management functions for the CFT client.
8
+ #
9
+ module ChangesetManagement
10
+ ##
11
+ # Accepts a template for a stack and tries to create an update changeset for that stack. The stack
12
+ # must already exist in CloudFormation.
13
+ #
14
+ # @param template [AwsCftTools::Template]
15
+ # @param changeset_set [String] an identifier linking various changesets as part of the same run
16
+ # @return [Array<AwsCftTools::Change>]
17
+ #
18
+ def changes_on_stack_update(template, changeset_set)
19
+ do_changeset(update_changeset_params(template, changeset_set))
20
+ end
21
+
22
+ ##
23
+ # Accepts a template for a stack and tries to create a creation changeset for the stack. The stack
24
+ # must not exist yet in CloudFormation.
25
+ #
26
+ # @param template [AwsCftTools::Template]
27
+ # @param changeset_set [String] an identifier linking various changesets as part of the same run
28
+ # @return [Array<AwsCftTools::Change>]
29
+ #
30
+ def changes_on_stack_create(template, changeset_set)
31
+ do_changeset(create_changeset_params(template, changeset_set))
32
+ end
33
+
34
+ ##
35
+ # Accepts a template and creates a mock changeset listing the resources that would be removed if
36
+ # the stack were deleted. The stack must exist in CloudFormation.
37
+ #
38
+ # @param template [AwsCftTools::Template]
39
+ # @param _changeset_set [Object] ignored to maintain compatibility with the other changeset methods
40
+ # @return [Array<AwsCftTools::DeletionChange>]
41
+ #
42
+ def changes_on_stack_delete(template, _changeset_set)
43
+ mock_delete_changeset(template)
44
+ end
45
+
46
+ private
47
+
48
+ ##
49
+ # perform the changeset creation/deletion and return the changes that would happen if the
50
+ # changes moved forward
51
+ #
52
+ def do_changeset(params)
53
+ id = id_params(params)
54
+
55
+ aws_client.create_change_set(params)
56
+
57
+ aws_client.wait_until(:change_set_create_complete, id)
58
+
59
+ mapped_changes(AWSEnumerator.new(aws_client, :describe_change_set, id, &:changes).to_a)
60
+ rescue Aws::Waiters::Errors::FailureStateError
61
+ []
62
+ ensure
63
+ aws_client.delete_change_set(id)
64
+ end
65
+
66
+ def id_params(params)
67
+ {
68
+ change_set_name: params[:change_set_name],
69
+ stack_name: params[:stack_name]
70
+ }
71
+ end
72
+
73
+ def update_changeset_params(template, changeset_set)
74
+ common_changeset_params(template, changeset_set).merge(change_set_type: 'UPDATE')
75
+ end
76
+
77
+ def create_changeset_params(template, changeset_set)
78
+ common_changeset_params(template, changeset_set).merge(change_set_type: 'CREATE')
79
+ end
80
+
81
+ def mock_delete_changeset(template)
82
+ # act like we're doing a changeset, but just narrate all of the resources being removed
83
+ id_params = { stack_name: template.name }
84
+ mapped_deleted_resources(
85
+ AWSEnumerator.new(aws_client, :list_stack_resources, id_params, &:stack_resource_summaries).to_a
86
+ )
87
+ rescue Aws::CloudFormation::Errors::ValidationError
88
+ []
89
+ end
90
+
91
+ def common_changeset_params(template, changeset_set)
92
+ params = template.stack_parameters
93
+ params.merge(
94
+ capabilities: %w[CAPABILITY_IAM CAPABILITY_NAMED_IAM],
95
+ change_set_name: "#{params[:stack_name]}-#{changeset_set}"
96
+ )
97
+ end
98
+
99
+ def mapped_deleted_resources(resources)
100
+ resources.map { |resource| AwsCftTools::DeletionChange.new(resource) }
101
+ end
102
+
103
+ def mapped_changes(changes)
104
+ changes.flat_map { |change| AwsCftTools::Change.new(change) }
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsCftTools
4
+ class Client
5
+ class CFT
6
+ ##
7
+ # Provide stack management functions for the CFT client.
8
+ #
9
+ module StackManagement
10
+ ##
11
+ # Accepts information about a stack and tries to update that stack. The stack must already
12
+ # exist in CloudFormation.
13
+ #
14
+ # Metadata keys:
15
+ # - filename (required)
16
+ # - name
17
+ # - parameters
18
+ # - template_file
19
+ #
20
+ # If the update would result in no changes, no error is raised. Otherwise, all errors are
21
+ # raised and halt deployment.
22
+ #
23
+ # @param template [AwsCftTools::Template]
24
+ #
25
+ def update_stack(template)
26
+ aws_client.update_stack(update_stack_params(template))
27
+ # we want to wait for the update to complete before we proceed
28
+ aws_client.wait_until(:stack_update_complete, stack_name: template.name)
29
+ rescue Aws::CloudFormation::Errors::ValidationError => exception
30
+ raise exception unless exception.message.match?(/No updates/)
31
+ end
32
+
33
+ ##
34
+ # Accepts information about a stack and tries to create the stack. The stack must not exist in
35
+ # CloudFormation.
36
+ #
37
+ # @param template [AwsCftTools::Template]
38
+ #
39
+ def create_stack(template)
40
+ aws_client.create_stack(create_stack_params(template))
41
+ # we want to wait for the create to complete before we proceed
42
+ aws_client.wait_until(:stack_create_complete, stack_name: template.name)
43
+ end
44
+
45
+ ##
46
+ # Accepts information about a stack and tries to remove the stack. The stack must exist in
47
+ # CloudFormation.
48
+ #
49
+ # @param template [AwsCftTools::Template]
50
+ #
51
+ def delete_stack(template)
52
+ aws_client.delete_stack(delete_stack_params(template))
53
+ aws_client.wait_until(:stack_delete_complete, stack_name: template.name)
54
+ end
55
+
56
+ private
57
+
58
+ def update_stack_params(template)
59
+ common_stack_params(template).merge(
60
+ use_previous_template: false,
61
+ # on_failure: "ROLLBACK", # for updating
62
+ )
63
+ end
64
+
65
+ def create_stack_params(template)
66
+ common_stack_params(template).merge(
67
+ on_failure: 'DELETE', # for creation
68
+ )
69
+ end
70
+
71
+ def delete_stack_params(template)
72
+ {
73
+ stack_name: template.name
74
+ }
75
+ end
76
+
77
+ def common_stack_params(template)
78
+ template.stack_parameters.merge(
79
+ capabilities: %w[CAPABILITY_IAM CAPABILITY_NAMED_IAM]
80
+ )
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwsCftTools
4
+ class Client
5
+ ##
6
+ # = EC2 Instance Client
7
+ #
8
+ # All of the business logic behind direct interaction with the AWS API for EC2 instances and related
9
+ # resources.
10
+ #
11
+ class EC2 < Base
12
+ ##
13
+ #
14
+ # @param options [Hash] client configuration
15
+ # @option options [String] :environment the operational environment in which to act
16
+ # @option options [String] :profile the AWS credential profile to use
17
+ # @option options [String] :region the AWS region in which to act
18
+ # @option options [String] :role the operational role of the resources under consideration
19
+ #
20
+ def initialize(options)
21
+ super(options)
22
+ end
23
+
24
+ def self.aws_client_class
25
+ Aws::EC2::Resource
26
+ end
27
+
28
+ ##
29
+ # Returns a list of running instances filtered by any environment or role specified in the
30
+ # options passed to the constructor.
31
+ #
32
+ # Each instance is represented by a Hash with the following keys:
33
+ # - private_ip: the private IP address of the instance
34
+ # - public_ip: the public IP address (if any) of the instance
35
+ # - instance: the ID of the instance
36
+ # - role: the value of the `Role` tag if not filtering by role
37
+ # - environment: the value of the `Environment` tag if not filtering by environment
38
+ #
39
+ # @return [Array<OpenStruct>]
40
+ #
41
+ def instances
42
+ @instances ||= aws_client.instances(filters: instance_filters).map do |instance|
43
+ OpenStruct.new(
44
+ with_tags(instance, private_ip: instance.private_ip_address,
45
+ public_ip: instance.public_ip_address,
46
+ instance: instance.instance_id)
47
+ )
48
+ end
49
+ end
50
+
51
+ ##
52
+ # Returns a list of available AMI images filtered by any environment or role specified in the
53
+ # options passed to the constructor.
54
+ #
55
+ # Each image is represented by an OpenStruct with the following keys/methods:
56
+ # - image_id: the ID of the AMI or image
57
+ # - type: the type of AMI or image
58
+ # - public: a flag indicating if the image is public
59
+ # - created_at: the date/time at which the image was created
60
+ # - role: the value of the `Role` tag if not filtering by role
61
+ # - environment: the value of the `Environment` tag if not filtering by environment
62
+ #
63
+ # @return [Array<OpenStruct>]
64
+ #
65
+ def images
66
+ @images ||= aws_client.images(owners: ['self'], filters: image_filters).map do |image|
67
+ OpenStruct.new(
68
+ with_tags(image, image_id: image.image_id,
69
+ type: image.image_type,
70
+ public: image.public,
71
+ created_at: image.creation_date)
72
+ )
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def instance_filters
79
+ @instance_filters ||= begin
80
+ [
81
+ { name: 'instance-state-name', values: ['running'] }
82
+ ] + tag_filters
83
+ end
84
+ end
85
+
86
+ def image_filters
87
+ @image_filters ||= begin
88
+ [
89
+ { name: 'state', values: ['available'] }
90
+ ] + tag_filters
91
+ end
92
+ end
93
+
94
+ def tag_filters
95
+ @tag_filters ||= begin
96
+ environment_filter +
97
+ role_filter +
98
+ arbitrary_tag_filters
99
+ end
100
+ end
101
+
102
+ def environment_filter
103
+ tag_filter('Environment', options[:environment])
104
+ end
105
+
106
+ def role_filter
107
+ tag_filter('Role', options[:role])
108
+ end
109
+
110
+ def arbitrary_tag_filters
111
+ tags = options[:tags] || []
112
+ tags.inject([]) do |filter_set, tag_value|
113
+ tag, value = tag_value
114
+ filter_set << { name: "tag:#{tag}", values: [value] }
115
+ end
116
+ end
117
+
118
+ def tag_filter(tag, value)
119
+ if value
120
+ [{ name: "tag:#{tag}", values: [value] }]
121
+ else
122
+ []
123
+ end
124
+ end
125
+
126
+ def with_tags(resource, info = {})
127
+ tags = resource.tags.each_with_object({}) { |tag, collection| collection[tag.key] = tag.value }
128
+ info.merge(
129
+ role: tags.delete('Role'),
130
+ environment: tags.delete('Environment'),
131
+ tags: tags
132
+ )
133
+ end
134
+ end
135
+ end
136
+ end