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