aws-cft-tools 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.md +23 -0
- data/README.md +65 -0
- data/code.json +1 -1
- data/exe/aws-cft +8 -1
- data/lib/aws_cft_tools.rb +1 -0
- data/lib/aws_cft_tools/client/cft/changeset_management.rb +25 -5
- data/lib/aws_cft_tools/client/cft/stack_management.rb +16 -2
- data/lib/aws_cft_tools/runbook.rb +12 -0
- data/lib/aws_cft_tools/runbooks/common/changesets.rb +1 -0
- data/lib/aws_cft_tools/runbooks/deploy.rb +23 -6
- data/lib/aws_cft_tools/runbooks/deploy/threading.rb +3 -20
- data/lib/aws_cft_tools/threaded_output.rb +103 -0
- data/lib/aws_cft_tools/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e2c4148f5b7aa3cf1465fcc735a1b3ebd8b559d9
|
4
|
+
data.tar.gz: 654d8ffb95b10000a1e3d364803fc014aaf2788a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 31d7266d177d7cc7359b35ddea6271b4c4f1d14ef8e4fdf850056e6a7823ffc4bef9ee802c85513e99944bab14110098c33e568ca1faa58883a41ae44efc6e60
|
7
|
+
data.tar.gz: 12d6742f6050e73e68a0a54d10138ba21d33fc3ac10be49e2b150ee248651b7be6f6337d34558e8a5ea51153e174b2a59e9487977e968897f74debb1652a5913
|
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## [0.1.1] - 2017-12-15
|
4
|
+
|
5
|
+
### Added
|
6
|
+
|
7
|
+
* The `-d` and `--debug` options for `deploy` show more narrative. This can be useful if running parallel
|
8
|
+
jobs in a deployment (`-j` option with a value greater than one). This option is available for all
|
9
|
+
runbooks, but only the `deploy` runbook uses it in this release.
|
10
|
+
* Documentation on why `aws-cft-tools` is better than plain vanilla CloudFormation or Terraform. (Issue #10)
|
11
|
+
|
12
|
+
### Changed
|
13
|
+
|
14
|
+
* Parallel deployments use threads. We weren't always handling output properly, so some output could be lost
|
15
|
+
if there was an error. We've reworked how we coordinate output from threads to reduce the chance of this
|
16
|
+
happening. (Issue #7)
|
17
|
+
* Deployments special case the `-j1` option to avoid threads. If you run into issues with `-j` and a value
|
18
|
+
greater than one, run without the option or with `-j1` to make sure that threads aren't the source of the
|
19
|
+
problem. (Issue #7)
|
20
|
+
|
21
|
+
## [0.1.0] - 2017-11-29
|
22
|
+
|
23
|
+
Initial release.
|
data/README.md
CHANGED
@@ -1,9 +1,18 @@
|
|
1
1
|
# AwsCftTools
|
2
2
|
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/aws-cft-tools.svg)](https://badge.fury.io/rb/aws-cft-tools)
|
4
|
+
[![Build Status](https://travis-ci.org/USSBA/aws-cft-tools.svg?branch=master)](https://travis-ci.org/USSBA/aws-cft-tools)
|
5
|
+
|
3
6
|
CloudFormation and related services provide a way to manage infrastructure state in "the cloud." This
|
4
7
|
gem and its included command (`aws-cft`) build on top of this state management system to create an
|
5
8
|
infrastructure management solution native to the AWS environment.
|
6
9
|
|
10
|
+
`aws-cft-tools` empowers users to organize their CloudFormation templates using any form of directory
|
11
|
+
structure, without the need to tediously deploy their templates in a specific order or create quickly
|
12
|
+
outdated scripts to manage the deployment thereof. This project links together templates using the
|
13
|
+
Export/ImportValue features of CloudFormation to determine the order of operations, manages stack
|
14
|
+
names, and supports multiple parallel "Environments" within a single AWS account.
|
15
|
+
|
7
16
|
## Installation
|
8
17
|
|
9
18
|
Add this line to your application's Gemfile:
|
@@ -71,6 +80,62 @@ update the version number in `version.rb`, and then run `bundle exec rake releas
|
|
71
80
|
tag for the version, push git commits and tags, and push the `.gem` file to
|
72
81
|
[rubygems.org](https://rubygems.org).
|
73
82
|
|
83
|
+
## Why `aws-cft-tools`?
|
84
|
+
|
85
|
+
`aws-cft-tools` is designed to work in an "infrastructure as code" DevOps environment. Infrastructure is
|
86
|
+
software that is developed, tested, peer reviewed, and finally merged and deployed.
|
87
|
+
|
88
|
+
### "Vanilla" CloudFormation
|
89
|
+
|
90
|
+
When first using CloudFormation, it is very easy to launch a single stack and get off the ground quickly.
|
91
|
+
As you move forward, users quickly find out that their Templates need to be managed in source control.
|
92
|
+
Later, users want to test their infrastructure changes in a different Environment, so a "dev" layer is
|
93
|
+
created, then an "integration", then a "staging", etc. Before too long, launching stacks is a nightmare
|
94
|
+
due to dependency conflicts, manual naming failures of Stacks, typos, and so on. On top of that,
|
95
|
+
remembering which Stacks have been deployed for which environment becomes impossible, so infrastructure
|
96
|
+
drift is inevitable.
|
97
|
+
|
98
|
+
This tool builds on top of the normal progression of teams using CloudFormation, enabling managed
|
99
|
+
Environments using parameters on templates. It offers simple deployments to roll out a full stack in
|
100
|
+
a new environment with a single command. It allows developers to continue to use CloudFormation for all
|
101
|
+
their infrastructure, while vastly simplifying the deployment and retraction process.
|
102
|
+
|
103
|
+
### Ansible
|
104
|
+
|
105
|
+
[Ansible](https://www.ansible.com/) provides features that are a mix of infrastructure management and
|
106
|
+
instance configuration. For example, Ansible can do the work of TerraForm and Chef, combined. However,
|
107
|
+
Ansible works best when working with an expected inventory of resources. It makes changes to bring
|
108
|
+
infrastructure in line with the inventory. `aws-cft-tools` only manages CloudFormation templates and leaves
|
109
|
+
configuration of instances to other tools such as Chef or Ansible.
|
110
|
+
|
111
|
+
#### Using Ansible with `aws-cft-tools`
|
112
|
+
|
113
|
+
Ansible can manage the production of an Amazon Machine Image (AMI). It can spin up a temporary EC2 instance
|
114
|
+
and install all of the necessary system packages, make any configuration changes, and trigger the creation
|
115
|
+
of a tagged AMI. If the AMI is tagged with an Environment and Role, then `aws-cft-tools` can discover the
|
116
|
+
AMI and provide it as a parameter to any CloudFormation stacks that require the image. For example, creating
|
117
|
+
a new AMI and then using `aws-cft-tools` to deploy the CloudFormation Template for an auto-scaling group
|
118
|
+
that uses that AMI can result in the deployment of a new version of an application.
|
119
|
+
|
120
|
+
### TerraForm
|
121
|
+
|
122
|
+
[TerraForm](https://www.terraform.io/) and `aws-cft-tools` are solving similar problems with fundamentally
|
123
|
+
different approaches. TerraForm is designed to work with multiple cloud providers while `aws-cft-tools` is
|
124
|
+
specific to AWS. So TerraForm can't depend on features that aren't provided by all cloud providers. Thus,
|
125
|
+
TerraForm requires a state file that introduces some complexity into managing infrastructure.
|
126
|
+
|
127
|
+
Using `aws-cft-tools` doesn't mean infrastructure management is less complex than when using TerraForm. Only
|
128
|
+
that the complexity is different. Instead of managing a state file outside of AWS, `aws-cft-tools` assumes
|
129
|
+
that AWS is the source of all state information.
|
130
|
+
|
131
|
+
Rather than computing changes, for example, `aws-cft-tools` requests a list of changes from AWS for a given
|
132
|
+
change in template and parameters. This does take more time than if all of that information was in a local
|
133
|
+
state file, but it ensures that any changes reflect the current deployment.
|
134
|
+
|
135
|
+
In exchange for taking a little more time to make changes (e.g., pull requests and code reviews after
|
136
|
+
initial development), teams can work on different parts of the infrastructure without having to coordinate
|
137
|
+
with each other.
|
138
|
+
|
74
139
|
## Building Gem for Local Use
|
75
140
|
|
76
141
|
```shell
|
data/code.json
CHANGED
data/exe/aws-cft
CHANGED
@@ -21,6 +21,7 @@ Clamp do
|
|
21
21
|
option ['-T', '--tag'], 'NAME:VALUE', 'require a tag have the given value (may be given more than once)',
|
22
22
|
multivalued: true
|
23
23
|
option ['-v', '--[no-]verbose'], :flag, 'verbose narration of actions'
|
24
|
+
option ['-D', '--debug'], :flag, 'extra verbosity to aid in debugging'
|
24
25
|
option '--version', :flag, 'Show version' do
|
25
26
|
puts AwsCftTools::VERSION
|
26
27
|
exit(0)
|
@@ -160,10 +161,16 @@ Clamp do
|
|
160
161
|
profile: profile,
|
161
162
|
root: root,
|
162
163
|
region: region,
|
164
|
+
tags: tag_hash
|
165
|
+
}.merge(flag_options)
|
166
|
+
end
|
167
|
+
|
168
|
+
def flag_options
|
169
|
+
{
|
163
170
|
noop: noop?,
|
164
171
|
check: check?,
|
165
172
|
verbose: verbose?,
|
166
|
-
|
173
|
+
debug: debug?
|
167
174
|
}
|
168
175
|
end
|
169
176
|
|
data/lib/aws_cft_tools.rb
CHANGED
@@ -23,6 +23,7 @@ module AwsCftTools
|
|
23
23
|
require 'aws_cft_tools/deletion_change'
|
24
24
|
require 'aws_cft_tools/client'
|
25
25
|
require 'aws_cft_tools/dependency_tree'
|
26
|
+
require 'aws_cft_tools/threaded_output'
|
26
27
|
require 'aws_cft_tools/stack'
|
27
28
|
require 'aws_cft_tools/template'
|
28
29
|
require 'aws_cft_tools/template_set'
|
@@ -53,16 +53,36 @@ module AwsCftTools
|
|
53
53
|
id = id_params(params)
|
54
54
|
|
55
55
|
aws_client.create_change_set(params)
|
56
|
-
|
57
|
-
aws_client.wait_until(:change_set_create_complete, id)
|
58
|
-
|
56
|
+
return [] if wait_for_changeset(id) == :nochanges
|
59
57
|
mapped_changes(AWSEnumerator.new(aws_client, :describe_change_set, id, &:changes).to_a)
|
60
|
-
rescue Aws::Waiters::Errors::FailureStateError
|
61
|
-
[]
|
62
58
|
ensure
|
63
59
|
aws_client.delete_change_set(id)
|
64
60
|
end
|
65
61
|
|
62
|
+
def wait_for_changeset(id, times_waited = 0)
|
63
|
+
times_waited += 1
|
64
|
+
aws_client.wait_until(:change_set_create_complete, id)
|
65
|
+
rescue Aws::Waiters::Errors::FailureStateError
|
66
|
+
status = check_failure(id)
|
67
|
+
return status unless status == :retry
|
68
|
+
raise_if_too_many_retries(params, times_waited)
|
69
|
+
sleep(2**times_waited + 1)
|
70
|
+
retry
|
71
|
+
end
|
72
|
+
|
73
|
+
def raise_if_too_many_retries(params, retries)
|
74
|
+
return if retries < 5
|
75
|
+
raise CloudFormationError, "Error waiting on changeset for #{params[:stack_name]}"
|
76
|
+
end
|
77
|
+
|
78
|
+
def check_failure(id)
|
79
|
+
status = aws_client.describe_change_set(id)
|
80
|
+
return :retry unless status.status == 'FAILED'
|
81
|
+
return :no_changes if status.status_reason.match?(/didn't contain changes/)
|
82
|
+
raise CloudFormationError,
|
83
|
+
"Error creating changeset for #{params[:stack_name]}: #{status.status_reason}"
|
84
|
+
end
|
85
|
+
|
66
86
|
def id_params(params)
|
67
87
|
{
|
68
88
|
change_set_name: params[:change_set_name],
|
@@ -25,7 +25,7 @@ module AwsCftTools
|
|
25
25
|
def update_stack(template)
|
26
26
|
aws_client.update_stack(update_stack_params(template))
|
27
27
|
# we want to wait for the update to complete before we proceed
|
28
|
-
|
28
|
+
wait_for_stack_operation(:stack_update_complete, template.name)
|
29
29
|
rescue Aws::CloudFormation::Errors::ValidationError => exception
|
30
30
|
raise exception unless exception.message.match?(/No updates/)
|
31
31
|
end
|
@@ -39,7 +39,7 @@ module AwsCftTools
|
|
39
39
|
def create_stack(template)
|
40
40
|
aws_client.create_stack(create_stack_params(template))
|
41
41
|
# we want to wait for the create to complete before we proceed
|
42
|
-
|
42
|
+
wait_for_stack_operation(:stack_create_complete, template.name)
|
43
43
|
end
|
44
44
|
|
45
45
|
##
|
@@ -55,6 +55,20 @@ module AwsCftTools
|
|
55
55
|
|
56
56
|
private
|
57
57
|
|
58
|
+
def wait_for_stack_operation(op, stack_name, times_waited = 0)
|
59
|
+
times_waited += 1
|
60
|
+
aws_client.wait_until(op, stack_name: stack_name)
|
61
|
+
rescue Aws::Waiters::Errors::FailureStateError
|
62
|
+
raise_if_too_many_retries(stack_name, times_waited)
|
63
|
+
sleep(2**times_waited + 1)
|
64
|
+
retry
|
65
|
+
end
|
66
|
+
|
67
|
+
def raise_if_too_many_retries(stack_name, retries)
|
68
|
+
return if retries < 5
|
69
|
+
raise CloudFormationError, "Error waiting on stack operation for #{stack_name}"
|
70
|
+
end
|
71
|
+
|
58
72
|
def update_stack_params(template)
|
59
73
|
common_stack_params(template).merge(
|
60
74
|
use_previous_template: false,
|
@@ -48,6 +48,7 @@ module AwsCftTools
|
|
48
48
|
def initialize(configuration = {})
|
49
49
|
@options = configuration
|
50
50
|
@client = AwsCftTools::Client.new(options)
|
51
|
+
@stdout = $stdout
|
51
52
|
end
|
52
53
|
|
53
54
|
# @!group Callbacks
|
@@ -137,6 +138,17 @@ module AwsCftTools
|
|
137
138
|
end
|
138
139
|
end
|
139
140
|
|
141
|
+
##
|
142
|
+
# @param note [String] a debug note
|
143
|
+
#
|
144
|
+
# Prints the given content to stdout if running in +debug+ mode. Debug statements are output
|
145
|
+
# without any capture when running multiple threads.
|
146
|
+
#
|
147
|
+
def debug(note = nil)
|
148
|
+
return unless note && options[:debug]
|
149
|
+
@stdout.puts "DEBUG\nDEBUG " + note.split(/\n/).join("\nDEBUG ") + "\nDEBUG"
|
150
|
+
end
|
151
|
+
|
140
152
|
##
|
141
153
|
# @param description [String] an optional verbose description
|
142
154
|
# @yield runs the block if in +verbose+ mode
|
@@ -48,19 +48,36 @@ module AwsCftTools
|
|
48
48
|
end
|
49
49
|
|
50
50
|
def process_slice(templates)
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
51
|
+
jobs = options[:jobs]
|
52
|
+
if jobs && jobs > 1
|
53
|
+
process_slice_threaded(templates)
|
54
|
+
else
|
55
|
+
templates.each(&method(:process_template))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def process_slice_threaded(templates)
|
60
|
+
original_stdout = $stdout
|
61
|
+
$stdout = ThreadedOutput.new(original_stdout)
|
62
|
+
template_list = templates.map(&:name).join(', ')
|
63
|
+
debug("Creating threads for #{template_list}")
|
64
|
+
threads = create_threads(templates, &method(:process_template))
|
65
|
+
debug("Waiting on threads for #{template_list}")
|
66
|
+
threads.map(&:join)
|
55
67
|
ensure
|
56
|
-
$stdout =
|
68
|
+
$stdout = original_stdout
|
57
69
|
end
|
58
70
|
|
59
71
|
def process_template(template)
|
72
|
+
ThreadedOutput.prefix = template_name = template.name
|
73
|
+
debug("Processing #{template_name}")
|
60
74
|
is_update = deployed_templates.include?(template)
|
61
|
-
operation("#{is_update ? 'Updating' : 'Creating'}: #{
|
75
|
+
operation("#{is_update ? 'Updating' : 'Creating'}: #{template_name}") do
|
62
76
|
exec_template(template: template, type: is_update ? :update : :create)
|
63
77
|
end
|
78
|
+
ensure
|
79
|
+
$stdout.flush
|
80
|
+
debug("Finished processing #{template_name}")
|
64
81
|
end
|
65
82
|
|
66
83
|
def exec_template(params) # template:, type:
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'thread'
|
4
|
+
|
3
5
|
module AwsCftTools
|
4
6
|
module Runbooks
|
5
7
|
class Deploy
|
@@ -9,27 +11,8 @@ module AwsCftTools
|
|
9
11
|
module Threading
|
10
12
|
private
|
11
13
|
|
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
14
|
def create_threads(list, &_block)
|
26
|
-
list.map { |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)
|
15
|
+
list.map { |item| Thread.new { yield item } }
|
33
16
|
end
|
34
17
|
end
|
35
18
|
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require 'thread'
|
5
|
+
|
6
|
+
module AwsCftTools
|
7
|
+
##
|
8
|
+
# Provides a way to process output and prefix with a thread identifier. The object is shared by
|
9
|
+
# threads. Each thread should set its own prefix.
|
10
|
+
#
|
11
|
+
class ThreadedOutput
|
12
|
+
extend Forwardable
|
13
|
+
|
14
|
+
#
|
15
|
+
# @param real_stdout [IO] The file object that should be written to with prefixed text.
|
16
|
+
#
|
17
|
+
def initialize(real_stdout)
|
18
|
+
@stdout = real_stdout
|
19
|
+
@buffer = Hash.new { |hash, key| hash[key] = '' }
|
20
|
+
@semaphore = Mutex.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def_delegator :@semaphore, :synchronize, :guarded
|
24
|
+
def_delegators ThreadedOutput, :prefix
|
25
|
+
|
26
|
+
##
|
27
|
+
# The prefix for output from the current thread.
|
28
|
+
#
|
29
|
+
def self.prefix
|
30
|
+
Thread.current['output_prefix'] || ''
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# @param prefix [String] The prefix for each line of text output by the calling thread.
|
35
|
+
#
|
36
|
+
def self.prefix=(prefix)
|
37
|
+
Thread.current['output_prefix'] = prefix + ': '
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Ensure all buffered text is output. If any text is output, a newline is output as well.
|
42
|
+
#
|
43
|
+
def flush
|
44
|
+
guarded { @stdout.puts prefix + buffer } if buffer != ''
|
45
|
+
self.buffer = ''
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Write the string to the output with prefixes as appropriate. If the string does not end in a
|
50
|
+
# newline, then the remaining text will be buffered until a newline is seen.
|
51
|
+
#
|
52
|
+
def write(string)
|
53
|
+
print(string)
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# Writes all of the arguments to the output with prefixes. Appends a newline to each argument.
|
58
|
+
#
|
59
|
+
# @param args [Array<String>]
|
60
|
+
#
|
61
|
+
def puts(*args)
|
62
|
+
print(args.join("\n") + "\n")
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# Writes all of the arugments to the output without newlines. Will output the prefix after each
|
67
|
+
# newline.
|
68
|
+
#
|
69
|
+
# @param args [Array<String>]
|
70
|
+
#
|
71
|
+
def print(*args)
|
72
|
+
append(args.join(''))
|
73
|
+
printable_lines.each do |line|
|
74
|
+
guarded { @stdout.puts prefix + line }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def printable_lines
|
81
|
+
lines = buffer.split(/\n/)
|
82
|
+
if buffer[-1..-1] == "\n"
|
83
|
+
self.buffer = ''
|
84
|
+
else
|
85
|
+
self.buffer = lines.last
|
86
|
+
lines = lines[0..-2]
|
87
|
+
end
|
88
|
+
lines
|
89
|
+
end
|
90
|
+
|
91
|
+
def buffer
|
92
|
+
@buffer[prefix]
|
93
|
+
end
|
94
|
+
|
95
|
+
def append(value)
|
96
|
+
@buffer[prefix] += value
|
97
|
+
end
|
98
|
+
|
99
|
+
def buffer=(string)
|
100
|
+
@buffer[prefix] = string
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aws-cft-tools
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Small Business Administration
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-12-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk
|
@@ -235,6 +235,7 @@ files:
|
|
235
235
|
- ".travis.yml"
|
236
236
|
- ".yardopts"
|
237
237
|
- BEST-PRACTICES.md
|
238
|
+
- CHANGELOG.md
|
238
239
|
- CONTRIBUTING.md
|
239
240
|
- Gemfile
|
240
241
|
- LICENSE
|
@@ -292,6 +293,7 @@ files:
|
|
292
293
|
- lib/aws_cft_tools/template_set/closure.rb
|
293
294
|
- lib/aws_cft_tools/template_set/dependencies.rb
|
294
295
|
- lib/aws_cft_tools/template_set/each_slice_state.rb
|
296
|
+
- lib/aws_cft_tools/threaded_output.rb
|
295
297
|
- lib/aws_cft_tools/version.rb
|
296
298
|
- rubycritic.reek
|
297
299
|
homepage: https://github.com/USSBA/aws-cft-tools
|