cuffsert 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,5 @@
1
+ .bundle/
2
+ vendor/
3
+ coverage/
4
+ cuffsert-*.gem
5
+ .byebug_history
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.0.0-p647
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0
4
+ - 2.2
5
+ script:
6
+ - bundle exec rspec -c -fd
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'cuffsert/main'
4
+
5
+ CuffSert.run(ARGV)
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