cuffsert 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +62 -0
- data/LICENSE +29 -0
- data/README.md +86 -0
- data/bin/cuffdown +55 -0
- data/bin/cuffsert +5 -0
- data/bin/cuffup +46 -0
- data/cuffsert.gemspec +24 -0
- data/lib/cuffsert/cfarguments.rb +61 -0
- data/lib/cuffsert/cfstates.rb +41 -0
- data/lib/cuffsert/cli_args.rb +96 -0
- data/lib/cuffsert/confirmation.rb +50 -0
- data/lib/cuffsert/main.rb +129 -0
- data/lib/cuffsert/messages.rb +18 -0
- data/lib/cuffsert/metadata.rb +90 -0
- data/lib/cuffsert/presenters.rb +313 -0
- data/lib/cuffsert/rxcfclient.rb +116 -0
- metadata +193 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1935cccf195c341e79939f107502b5317fd17668
|
4
|
+
data.tar.gz: dc129419560c77c02cf3aff12f750cca442620cc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9be2de52da5a8255994b30a13ae6dc4289f110c158c328450a7cde474c1c2ed52d1e1382f92f4d13c10565ed08a1597356546ffac7ab0c196ab2996e9b29a8d1
|
7
|
+
data.tar.gz: d8d354ee5e17d0419f90bb5a2c528bcd330598ccb123e438733b34cf93473ca7f4a5409bb0c22efb8689ed3ceb2f5080cd43b4755c6121091893416d3583e85e
|
data/.gitignore
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.0.0-p647
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
cuffsert (0.9.0)
|
5
|
+
aws-sdk
|
6
|
+
colorize
|
7
|
+
ruby-termios
|
8
|
+
rx
|
9
|
+
|
10
|
+
GEM
|
11
|
+
remote: https://rubygems.org/
|
12
|
+
specs:
|
13
|
+
aws-sdk (2.6.42)
|
14
|
+
aws-sdk-resources (= 2.6.42)
|
15
|
+
aws-sdk-core (2.6.42)
|
16
|
+
aws-sigv4 (~> 1.0)
|
17
|
+
jmespath (~> 1.0)
|
18
|
+
aws-sdk-resources (2.6.42)
|
19
|
+
aws-sdk-core (= 2.6.42)
|
20
|
+
aws-sigv4 (1.0.0)
|
21
|
+
byebug (9.0.6)
|
22
|
+
colorize (0.8.1)
|
23
|
+
diff-lcs (1.2.5)
|
24
|
+
docile (1.1.5)
|
25
|
+
jmespath (1.3.1)
|
26
|
+
json (2.0.2)
|
27
|
+
rspec (3.5.0)
|
28
|
+
rspec-core (~> 3.5.0)
|
29
|
+
rspec-expectations (~> 3.5.0)
|
30
|
+
rspec-mocks (~> 3.5.0)
|
31
|
+
rspec-core (3.5.4)
|
32
|
+
rspec-support (~> 3.5.0)
|
33
|
+
rspec-expectations (3.5.0)
|
34
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
35
|
+
rspec-support (~> 3.5.0)
|
36
|
+
rspec-mocks (3.5.0)
|
37
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
38
|
+
rspec-support (~> 3.5.0)
|
39
|
+
rspec-support (3.5.0)
|
40
|
+
ruby-termios (1.0.2)
|
41
|
+
rx (0.0.3)
|
42
|
+
rx-rspec (0.1.3)
|
43
|
+
rx
|
44
|
+
simplecov (0.12.0)
|
45
|
+
docile (~> 1.1.0)
|
46
|
+
json (>= 1.8, < 3)
|
47
|
+
simplecov-html (~> 0.10.0)
|
48
|
+
simplecov-html (0.10.0)
|
49
|
+
|
50
|
+
PLATFORMS
|
51
|
+
ruby
|
52
|
+
|
53
|
+
DEPENDENCIES
|
54
|
+
bundler (~> 1.12)
|
55
|
+
byebug
|
56
|
+
cuffsert!
|
57
|
+
rspec (~> 3.0)
|
58
|
+
rx-rspec (~> 0.1.3)
|
59
|
+
simplecov
|
60
|
+
|
61
|
+
BUNDLED WITH
|
62
|
+
1.13.6
|
data/LICENSE
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
BSD 3-Clause License
|
2
|
+
|
3
|
+
Copyright (c) 2016, Burt
|
4
|
+
All rights reserved.
|
5
|
+
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
8
|
+
|
9
|
+
* Redistributions of source code must retain the above copyright notice, this
|
10
|
+
list of conditions and the following disclaimer.
|
11
|
+
|
12
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
14
|
+
and/or other materials provided with the distribution.
|
15
|
+
|
16
|
+
* Neither the name of the copyright holder nor the names of its
|
17
|
+
contributors may be used to endorse or promote products derived from
|
18
|
+
this software without specific prior written permission.
|
19
|
+
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# Cuffsert - CloudFormation CLI
|
2
|
+
|
3
|
+
The primary goal of cuffsert is to provide a quick "up-arrow-enter" loading of a CloudFormation stack with good feedback, removing the need to click through three pesky screens each time. It figures out whether the stack needs to be created or rolled-back and whether it needs to be deleted first.
|
4
|
+
|
5
|
+
Cuffsert allows encoding the metadata and commandline arguments needed to load a template in a versionable file which takes CloudFormation the last mile to really become an infrastructure-as-code platform.
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
Given the file cuffsert.yml:
|
10
|
+
```yaml
|
11
|
+
Format: v1
|
12
|
+
Suffix: webserver
|
13
|
+
Tags:
|
14
|
+
- Name: Role
|
15
|
+
Value: webserver
|
16
|
+
Variants:
|
17
|
+
production:
|
18
|
+
Tags:
|
19
|
+
- Name: Environment
|
20
|
+
Value: production
|
21
|
+
Variants:
|
22
|
+
eu1:
|
23
|
+
Tags:
|
24
|
+
- Name: DC
|
25
|
+
- Value: eu1
|
26
|
+
Parameters:
|
27
|
+
- Name: ElasticIP
|
28
|
+
Value: 1.2.3.4
|
29
|
+
us1:
|
30
|
+
Tags:
|
31
|
+
- Name: DC
|
32
|
+
Value: us1
|
33
|
+
Parameters:
|
34
|
+
- Name: ElasticIP
|
35
|
+
Value: 5.6.7.8
|
36
|
+
```
|
37
|
+
you can invoke cuffsert like so:
|
38
|
+
```
|
39
|
+
cuffsert --metadata=./nginx-parameters.yml \
|
40
|
+
--selector=production-us1 \
|
41
|
+
./nginx.yml
|
42
|
+
```
|
43
|
+
|
44
|
+
This will select tags `Role=webserver, Environment=production, DC=us1` and parameter `ElasticIP=5.6.7.8` and create or update the stack `production-us1-webserver` as necessary.
|
45
|
+
|
46
|
+
## Metadata file format
|
47
|
+
|
48
|
+
The metadata file consists of a hierarchy of configuration sections called "variants". Cuffsert splits the selector by [/-] and starts at the top of the metadata and tries to match the path elements against each variants sections.
|
49
|
+
|
50
|
+
Each level can contain the following keys:
|
51
|
+
|
52
|
+
- **StackName**: Stack name used for creating a new stack and finding existing stack to update. If no StackName parameter is found, one will be constructed by joining lowercase variants values and basename of stack file. From the example at the top, stack name will be `production-eu1-webserver`.
|
53
|
+
- **Tags**: Tags applied at stack level.
|
54
|
+
- **Parameters**: Provide values for parameters that the stack needs.
|
55
|
+
- **Variations**: Each sub-key is a possible path element whose value is the hash for the next level.
|
56
|
+
- **DefaultPath**: You can supply a default selection for a particular path element which is given if none is supplied. Please be advised that having identical names at different hierarchical levels may lead to unexpected results.
|
57
|
+
|
58
|
+
Values from deeper levels merged onto values from higher levels to produce a configuration used to create/update the stack.
|
59
|
+
|
60
|
+
## Commandline options
|
61
|
+
|
62
|
+
cuffsert [--stack-name=name] [--tag=k:v ...] [--parameter=k:v ...]
|
63
|
+
[--metadata=directory | yml-file] [--metadata-path=path/to]
|
64
|
+
cloudformation-file | cloudformation-directory
|
65
|
+
|
66
|
+
All values set in the metadata file can be overridden on commandline.
|
67
|
+
|
68
|
+
`--stack-name=name (-n)` Explicity set the name of the generated stack.
|
69
|
+
|
70
|
+
`--tag=key:value (-t)` Override (or set) the value for a specific tag for the template.
|
71
|
+
|
72
|
+
`--parameter=key:value (-p)` Override (or set) the value for a specific parameter that is passed on template creation.
|
73
|
+
|
74
|
+
`--metadata=file (-m)` File or directory to read metadata from. Defaults to `cufsert/` or `cufsert.yml` relative to stack file.
|
75
|
+
|
76
|
+
`--selector=path-through-metadata (-P)` Path through variant keys to apply metadata from.
|
77
|
+
|
78
|
+
## AWS authentication
|
79
|
+
|
80
|
+
cuffsert assumes that the aws client library can authenticate your access via the normal means and makes no particular effort to aid the process, nor does it select revion for you.
|
81
|
+
|
82
|
+
## Future work
|
83
|
+
|
84
|
+
- Stack policies and policy overrides
|
85
|
+
- Find and delete resources that block delete or update
|
86
|
+
- provide detailed diffs on changes
|
data/bin/cuffdown
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'cuffsert/metadata'
|
4
|
+
require 'cuffsert/rxcfclient'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
module CuffDown
|
8
|
+
def self.parameters(stack)
|
9
|
+
(stack[:parameters] || []).map do |param|
|
10
|
+
{
|
11
|
+
'Name' => param[:parameter_key],
|
12
|
+
'Value' => param[:parameter_value],
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.tags(stack)
|
18
|
+
(stack[:tags] || []).map do |param|
|
19
|
+
{
|
20
|
+
'Name' => param[:key],
|
21
|
+
'Value' => param[:value],
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.dump(name, params, tags, output)
|
27
|
+
result = {
|
28
|
+
'Format' => 'v1',
|
29
|
+
'Suffix' => name,
|
30
|
+
'Parameters' => params,
|
31
|
+
'Tags' => tags,
|
32
|
+
}
|
33
|
+
YAML.dump(result, output)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.run(args)
|
37
|
+
meta = CuffSert::StackConfig.new
|
38
|
+
meta.stackname = args[0]
|
39
|
+
client = CuffSert::RxCFClient.new
|
40
|
+
stack = client.find_stack_blocking(meta)
|
41
|
+
unless stack
|
42
|
+
STDERR.puts "No such stack #{meta.stackname}"
|
43
|
+
exit(1)
|
44
|
+
end
|
45
|
+
stack = stack.to_h
|
46
|
+
self.dump(
|
47
|
+
stack[:stack_name],
|
48
|
+
self.parameters(stack),
|
49
|
+
self.tags(stack),
|
50
|
+
STDOUT
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
CuffDown.run(ARGV)
|
data/bin/cuffsert
ADDED
data/bin/cuffup
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
module CuffUp
|
7
|
+
def self.parameters(io)
|
8
|
+
template = YAML.load(io)
|
9
|
+
(template['Parameters'] || [])
|
10
|
+
.map do |key, data|
|
11
|
+
{
|
12
|
+
'Name' => key,
|
13
|
+
'Value' => data['Default'],
|
14
|
+
}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.dump(args, input, output)
|
19
|
+
result = {
|
20
|
+
'Format' => 'v1',
|
21
|
+
}
|
22
|
+
result['Parameters'] = input if input.size > 0
|
23
|
+
result['Suffix'] = args[:selector].join('-') if args.include?(:selector)
|
24
|
+
YAML.dump(result, output)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.run(args, template)
|
28
|
+
self.dump(args, self.parameters(open(template)), STDOUT)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
args = {}
|
33
|
+
parser = OptionParser.new do |opts|
|
34
|
+
opts.on('--selector selector', '-s selector', 'Set as sufflx in the generated output') do |selector|
|
35
|
+
args[:selector] = selector.split(/[-,\/]/)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
template = parser.parse(ARGV)
|
40
|
+
|
41
|
+
unless template
|
42
|
+
STDERR.puts("Usage: #{__FILE__} <template>")
|
43
|
+
exit(1)
|
44
|
+
end
|
45
|
+
|
46
|
+
CuffUp.run(args, template[0])
|
data/cuffsert.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
Gem::Specification.new do |spec|
|
2
|
+
spec.name = 'cuffsert'
|
3
|
+
spec.version = '0.9.0'
|
4
|
+
spec.summary = 'Cuffsert provides a quick up-arrow-enter loading of a CloudFormation stack with good feedback'
|
5
|
+
spec.description = 'Cuffsert allows encoding the metadata and commandline arguments needed to load a template in a versionable file which takes CloudFormation the last mile to really become an infrastructure-as-code platform.'
|
6
|
+
spec.authors = ['Anders Qvist']
|
7
|
+
spec.email = 'quest@lysator.liu.se'
|
8
|
+
spec.homepage = 'http://rubygems.org/gems/cuffsert'
|
9
|
+
spec.license = 'MIT'
|
10
|
+
|
11
|
+
spec.executables = ['cuffsert']
|
12
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(/^spec/) }
|
13
|
+
|
14
|
+
spec.add_runtime_dependency 'aws-sdk'
|
15
|
+
spec.add_runtime_dependency 'colorize'
|
16
|
+
spec.add_runtime_dependency 'ruby-termios'
|
17
|
+
spec.add_runtime_dependency 'rx'
|
18
|
+
|
19
|
+
spec.add_development_dependency 'bundler', '~> 1.12'
|
20
|
+
spec.add_development_dependency 'byebug'
|
21
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
22
|
+
spec.add_development_dependency 'rx-rspec', '~> 0.1.3'
|
23
|
+
spec.add_development_dependency 'simplecov'
|
24
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
|
3
|
+
# TODO:
|
4
|
+
# - propagate timeout here (from config?)
|
5
|
+
# - fail on template body > 51200 bytes
|
6
|
+
# - creation change-set: cfargs[:change_set_type] = 'CREATE'
|
7
|
+
|
8
|
+
module CuffSert
|
9
|
+
TIMEOUT = 10
|
10
|
+
|
11
|
+
def self.as_cloudformation_args(meta)
|
12
|
+
cfargs = {
|
13
|
+
:stack_name => meta.stackname,
|
14
|
+
:capabilities => %w[
|
15
|
+
CAPABILITY_IAM
|
16
|
+
CAPABILITY_NAMED_IAM
|
17
|
+
],
|
18
|
+
}
|
19
|
+
|
20
|
+
unless meta.parameters.empty?
|
21
|
+
cfargs[:parameters] = meta.parameters.map do |k, v|
|
22
|
+
{:parameter_key => k, :parameter_value => v.to_s}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
unless meta.tags.empty?
|
27
|
+
cfargs[:tags] = meta.tags.map do |k, v|
|
28
|
+
{:key => k, :value => v.to_s}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
if meta.stack_uri.scheme == 's3'
|
33
|
+
cfargs[:template_url] = meta.stack_uri.to_s
|
34
|
+
elsif meta.stack_uri.scheme == 'file'
|
35
|
+
file = meta.stack_uri.to_s.sub(/^file:\/+/, '/')
|
36
|
+
cfargs[:template_body] = open(file).read
|
37
|
+
else
|
38
|
+
raise "Unsupported scheme #{meta.stack_uri.scheme}"
|
39
|
+
end
|
40
|
+
cfargs
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.as_create_stack_args(meta)
|
44
|
+
cfargs = self.as_cloudformation_args(meta)
|
45
|
+
cfargs[:timeout_in_minutes] = TIMEOUT
|
46
|
+
cfargs[:on_failure] = 'DELETE'
|
47
|
+
cfargs
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.as_update_change_set(meta)
|
51
|
+
cfargs = self.as_cloudformation_args(meta)
|
52
|
+
cfargs[:use_previous_template] = false
|
53
|
+
cfargs[:change_set_name] = meta.stackname
|
54
|
+
cfargs[:change_set_type] = 'UPDATE'
|
55
|
+
cfargs
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.as_delete_stack_args(stack)
|
59
|
+
{ :stack_name => stack[:stack_id] }
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module CuffSert
|
2
|
+
INPROGRESS_STATES = %w[
|
3
|
+
CREATE_IN_PROGRESS
|
4
|
+
UPDATE_IN_PROGRESS
|
5
|
+
UPDATE_COMPLETE_CLEANUP_IN_PROGRESS
|
6
|
+
UPDATE_ROLLBACK_IN_PROGRESS
|
7
|
+
UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS
|
8
|
+
DELETE_IN_PROGRESS
|
9
|
+
]
|
10
|
+
|
11
|
+
GOOD_STATES = %w[
|
12
|
+
CREATE_COMPLETE
|
13
|
+
ROLLBACK_COMPLETE
|
14
|
+
UPDATE_COMPLETE
|
15
|
+
UPDATE_ROLLBACK_COMPLETE
|
16
|
+
DELETE_COMPLETE
|
17
|
+
DELETE_SKIPPED
|
18
|
+
]
|
19
|
+
|
20
|
+
BAD_STATES = %w[
|
21
|
+
CREATE_FAILED
|
22
|
+
UPDATE_ROLLBACK_FAILED
|
23
|
+
UPDATE_FAILED
|
24
|
+
DELETE_FAILED
|
25
|
+
FAILED
|
26
|
+
]
|
27
|
+
|
28
|
+
FINAL_STATES = GOOD_STATES + BAD_STATES
|
29
|
+
|
30
|
+
def self.state_category(state)
|
31
|
+
if BAD_STATES.include?(state)
|
32
|
+
:bad
|
33
|
+
elsif GOOD_STATES.include?(state)
|
34
|
+
:good
|
35
|
+
elsif INPROGRESS_STATES.include?(state)
|
36
|
+
:progress
|
37
|
+
else
|
38
|
+
raise "Cannot categorize state #{state}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module CuffSert
|
4
|
+
STACKNAME_RE = /^[A-Za-z0-9_-]+$/
|
5
|
+
|
6
|
+
def self.parse_cli_args(argv)
|
7
|
+
args = {
|
8
|
+
:output => :progressbar,
|
9
|
+
:verbosity => 1,
|
10
|
+
:force_replace => false,
|
11
|
+
:op_mode => nil,
|
12
|
+
:overrides => {
|
13
|
+
:parameters => {},
|
14
|
+
:tags => {},
|
15
|
+
}
|
16
|
+
}
|
17
|
+
parser = OptionParser.new do |opts|
|
18
|
+
opts.banner = 'Upsert a CloudFormation template, reading creation options and metadata from a yaml file. Currently, parameter values, stack name and stack tags are read from metadata file.'
|
19
|
+
opts.separator('')
|
20
|
+
opts.separator('Usage: cuffsert --selector production/us stack.json')
|
21
|
+
opts.on('--metadata path', '-m path', 'Yaml file to read stack metadata from') do |path|
|
22
|
+
path = '/dev/stdin' if path == '-'
|
23
|
+
unless File.exist?(path)
|
24
|
+
raise "--metadata #{path} does not exist"
|
25
|
+
end
|
26
|
+
args[:metadata] = path
|
27
|
+
end
|
28
|
+
|
29
|
+
opts.on('--selector selector', '-s selector', 'Dash or slash-separated variant names used to navigate the metadata') do |selector|
|
30
|
+
args[:selector] = selector.split(/[-,\/]/)
|
31
|
+
end
|
32
|
+
|
33
|
+
opts.on('--name stackname', '-n name', 'Alternative stackname (default is to construct the name from the selector') do |stackname|
|
34
|
+
unless stackname =~ STACKNAME_RE
|
35
|
+
raise "--name #{stackname} is expected to be #{STACKNAME_RE.inspect}"
|
36
|
+
end
|
37
|
+
args[:overrides][:stackname] = stackname
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.on('--parameter kv', '-p kv', 'Set the value of a particular parameter, overriding any file metadata') do |kv|
|
41
|
+
key, val = kv.split(/=/, 2)
|
42
|
+
if val.nil?
|
43
|
+
raise "--parameter #{kv} should be key=value"
|
44
|
+
end
|
45
|
+
if args[:overrides][:parameters].include?(key)
|
46
|
+
raise "cli args include duplicate parameter #{key}"
|
47
|
+
end
|
48
|
+
args[:overrides][:parameters][key] = val
|
49
|
+
end
|
50
|
+
|
51
|
+
opts.on('--tag kv', '-t kv', 'Set a stack tag, overriding any file metadata') do |kv|
|
52
|
+
key, val = kv.split(/=/, 2)
|
53
|
+
if val.nil?
|
54
|
+
raise "--tag #{kv} should be key=value"
|
55
|
+
end
|
56
|
+
if args[:overrides][:tags].include?(key)
|
57
|
+
raise "cli args include duplicate tag #{key}"
|
58
|
+
end
|
59
|
+
args[:overrides][:tags][key] = val
|
60
|
+
end
|
61
|
+
|
62
|
+
opts.on('--json', 'Output events in JSON, no progressbar, colors') do
|
63
|
+
args[:output] = :json
|
64
|
+
end
|
65
|
+
|
66
|
+
opts.on('--verbose', '-v', 'More detailed output. Once will print all stack evwnts, twice will print debug info') do
|
67
|
+
args[:verbosity] += 1
|
68
|
+
end
|
69
|
+
|
70
|
+
opts.on('--quiet', '-q', 'Output only fatal errors') do
|
71
|
+
args[:verbosity] = 0
|
72
|
+
end
|
73
|
+
|
74
|
+
opts.on('--replace', 'Re-create the stack if it already exist') do
|
75
|
+
args[:force_replace] = true
|
76
|
+
end
|
77
|
+
|
78
|
+
opts.on('--yes', '-y', 'Don\'t ask to replace and delete stack resources') do
|
79
|
+
raise 'You cannot do --yes and --dry-run at the same time' if args[:op_mode]
|
80
|
+
args[:op_mode] = :dangerous_ok
|
81
|
+
end
|
82
|
+
|
83
|
+
opts.on('--dry-run', 'Describe what would be done') do
|
84
|
+
raise 'You cannot do --yes and --dry-run at the same time' if args[:op_mode]
|
85
|
+
args[:op_mode] = :dry_run
|
86
|
+
end
|
87
|
+
|
88
|
+
opts.on('--help', '-h', 'Produce this message') do
|
89
|
+
abort(opts.to_s)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
args[:stack_path] = parser.parse(argv)
|
94
|
+
args
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'termios'
|
2
|
+
|
3
|
+
module CuffSert
|
4
|
+
def self.need_confirmation(meta, action, desc)
|
5
|
+
return false if meta.op_mode == :dangerous_ok
|
6
|
+
case action
|
7
|
+
when :create
|
8
|
+
false
|
9
|
+
when :update
|
10
|
+
change_set = desc
|
11
|
+
change_set[:changes].any? do |change|
|
12
|
+
rc = change[:resource_change]
|
13
|
+
rc[:action] == 'Remove' || (
|
14
|
+
rc[:action] == 'Modify' &&
|
15
|
+
['Always', 'True', 'Conditional'].include?(rc[:replacement])
|
16
|
+
)
|
17
|
+
end
|
18
|
+
when :recreate
|
19
|
+
true
|
20
|
+
else
|
21
|
+
true # safety first
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.ask_confirmation(input = STDIN, output = STDOUT)
|
26
|
+
return false unless input.isatty
|
27
|
+
state = Termios.tcgetattr(input)
|
28
|
+
mystate = state.dup
|
29
|
+
mystate.c_lflag |= Termios::ISIG
|
30
|
+
mystate.c_lflag &= ~Termios::ECHO
|
31
|
+
mystate.c_lflag &= ~Termios::ICANON
|
32
|
+
output.write 'Continue? [yN] '
|
33
|
+
begin
|
34
|
+
Termios.tcsetattr(input, Termios::TCSANOW, mystate)
|
35
|
+
answer = input.getc.chr.downcase
|
36
|
+
output.write("\n")
|
37
|
+
answer == 'y'
|
38
|
+
rescue Interrupt
|
39
|
+
false
|
40
|
+
ensure
|
41
|
+
Termios.tcsetattr(input, Termios::TCSANOW, state)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.confirmation(meta, action, change_set)
|
46
|
+
return false if meta.op_mode == :dry_run
|
47
|
+
return true unless CuffSert.need_confirmation(meta, action, change_set)
|
48
|
+
return CuffSert.ask_confirmation(STDIN, STDOUT)
|
49
|
+
end
|
50
|
+
end
|