kumo_keisei 0.0.52

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: 3f395bd579c7d7c66b09e3e7972a391b19bca4a7
4
+ data.tar.gz: e0b846c39e2b565e82393745cf0ddab08013a401
5
+ SHA512:
6
+ metadata.gz: 0b1a8b17c4e14841c2d88c8ce48a7451f94c806ecf75871fc50cb60ab176621833debac2bde345c7ffdd383e1dd67e48f79a74a077772b86a3dcab451e4635be
7
+ data.tar.gz: 8635f05b270af603bcf66d47dc3e353f25b4c8718d43470468ce2c1295c6326572501984a8112cce0cdcbf715c91543e335ebbdb4f56c59e1704c4564f9d2e8d
@@ -0,0 +1,10 @@
1
+ steps:
2
+ - name: ':rspec: unit-test'
3
+ command: script/unit_test.sh
4
+ agents:
5
+ location: aws
6
+ - wait
7
+ - name: ':gem: build'
8
+ command: script/build.sh
9
+ agents:
10
+ location: aws
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ .idea
2
+ /.bundle/
3
+ /.yardoc
4
+ /Gemfile.lock
5
+ /_yardoc/
6
+ /coverage/
7
+ /doc/
8
+ /pkg/
9
+ /spec/reports/
10
+ /tmp/
11
+ *.bundle
12
+ *.so
13
+ *.o
14
+ *.a
15
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in kumo_keisei.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Craig Read
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # KumoKeisei [![Build status](https://badge.buildkite.com/fdbcc9783971fc3c18903abe78ccb4a7a4ebe1bdbbb753c502.svg)](https://buildkite.com/redbubble/kumo-keisei-gem)
2
+
3
+ A collection of utilities wrapping the libraries for dealing with AWS Cloud Formation.
4
+
5
+ ## Installation
6
+
7
+ This gem is automatically installed in the rbdevtools container, so any `apply-env` or `deploy` scripts have access to it.
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'kumo_keisei'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install kumo_keisei
22
+
23
+ ## Usage
24
+
25
+ ```ruby
26
+ stack_name = "my_awesome_stack"
27
+ template = "./cloudformation/environment_template.json"
28
+ template_params = "./cloudformation/environments/production/params.json"
29
+
30
+ KumoKeisei::CloudFormationStack.new(stack_name, template, template_params).apply!
31
+ ```
32
+
33
+ ## Dependencies
34
+
35
+ #### Ruby Versions
36
+
37
+ This gem is tested with Ruby (MRI) versions 1.9.3 and 2.2.3.
38
+
39
+ ## Release
40
+
41
+ 1. Upgrade version in VERSION
42
+ 2. Run ./script/release-gem
43
+
44
+ ## Contributing
45
+
46
+ 1. Fork it ( https://github.com/[my-github-username]/kumo_keisei/fork )
47
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
48
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
49
+ 4. Push to the branch (`git push origin my-new-feature`)
50
+ 5. Create a new Pull Request
51
+
52
+ ## Testing changes
53
+
54
+ Changes to the gem can be manually tested end to end in a project that uses the gem (i.e. http-wala).
55
+
56
+ 1. First start the dev-tools container: `baxter kumo tools debug non-production`
57
+ 1. Re-install the gem: `gem specific_install https://github.com/redbubble/kumo_keisei_gem.git -b <your_branch>`
58
+ 1. Fire up a console: `irb`
59
+ 1. Require the gem: `require "kumo_keisei"`
60
+ 1. Interact with the gem's classes. `KumoKeisei::CloudFormationStack.new(...).apply!`
61
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.52
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ version = File.read(File.expand_path('../VERSION', __FILE__)).strip
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "kumo_keisei"
8
+ spec.version = version
9
+ spec.authors = ["Redbubble"]
10
+ spec.email = ["delivery-engineering@redbubble.com"]
11
+ spec.summary = %q{A collection of utilities for dealing with AWS Cloud Formation.}
12
+ spec.homepage = "http://redbubble.com"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_runtime_dependency 'aws-sdk', "~> 2.2"
21
+ spec.add_runtime_dependency 'kumo_ki'
22
+ spec.add_development_dependency "bundler", "~> 1.6"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "rspec", "~> 3.4"
25
+ end
@@ -0,0 +1,186 @@
1
+ require 'aws-sdk'
2
+
3
+ require_relative "parameter_builder"
4
+ require_relative "console_jockey"
5
+
6
+ module KumoKeisei
7
+ class CloudFormationStack
8
+ class CreateError < StandardError; end
9
+ class UpdateError < StandardError; end
10
+
11
+ UPDATEABLE_STATUSES = [
12
+ 'UPDATE_ROLLBACK_COMPLETE',
13
+ 'CREATE_COMPLETE',
14
+ 'UPDATE_COMPLETE',
15
+ 'DELETE_COMPLETE'
16
+ ]
17
+
18
+ RECOVERABLE_STATUSES = [
19
+ 'ROLLBACK_COMPLETE',
20
+ 'ROLLBACK_FAILED'
21
+ ]
22
+
23
+ UNRECOVERABLE_STATUSES = [
24
+ 'UPDATE_ROLLBACK_FAILED'
25
+ ]
26
+
27
+ attr_reader :stack_name
28
+
29
+ def self.exists?(stack_name)
30
+ self.new(stack_name, nil).exists?
31
+ end
32
+
33
+ def initialize(stack_name, stack_template, stack_params_filepath = nil)
34
+ @stack_name = stack_name
35
+ @stack_template = stack_template
36
+ @stack_params_filepath = stack_params_filepath
37
+
38
+ flash_message "Stack name: #{stack_name}"
39
+ end
40
+
41
+ def apply!(dynamic_params={})
42
+ if updatable?
43
+ update!(dynamic_params)
44
+ else
45
+ ConsoleJockey.write_line "There's a previous stack called #{@stack_name} that didn't create properly, I'll clean it up for you..."
46
+ ensure_deleted!
47
+ ConsoleJockey.write_line "Creating your new stack #{@stack_name}"
48
+ create!(dynamic_params)
49
+ end
50
+ end
51
+
52
+ def destroy!
53
+ wait_until_ready(false)
54
+ ensure_deleted!
55
+ end
56
+
57
+ def outputs(output)
58
+ outputs_hash = get_stack.outputs.reduce({}) { |acc, o| acc.merge(o.output_key.to_s => o.output_value) }
59
+
60
+ outputs_hash[output]
61
+ end
62
+
63
+ def logical_resource(resource_name)
64
+ response = cloudformation.describe_stack_resource(stack_name: @stack_name, logical_resource_id: resource_name)
65
+ stack_resource = response.stack_resource_detail
66
+ stack_resource.each_pair.reduce({}) {|acc, (k, v)| acc.merge(transform_logical_resource_id(k) => v) }
67
+ end
68
+
69
+ def exists?
70
+ !get_stack.nil?
71
+ end
72
+
73
+ private
74
+
75
+ def transform_logical_resource_id(id)
76
+ id.to_s.split('_').map {|w| w.capitalize }.join
77
+ end
78
+
79
+ def get_stack(options={})
80
+ @stack = nil if options[:dump_cache]
81
+
82
+ @stack ||= cloudformation.describe_stacks(stack_name: @stack_name).stacks.find { |stack| stack.stack_name == @stack_name }
83
+ end
84
+
85
+ def cloudformation
86
+ @cloudformation ||= Aws::CloudFormation::Client.new
87
+ end
88
+
89
+ def ensure_deleted!
90
+ cloudformation.delete_stack(stack_name: @stack_name)
91
+ cloudformation.wait_until(:stack_delete_complete, stack_name: @stack_name) { |waiter| waiter.delay = 20; waiter.max_attempts = 45 }
92
+ end
93
+
94
+ def updatable?
95
+ stack = get_stack
96
+
97
+ return true if UPDATEABLE_STATUSES.include? stack.stack_status
98
+ return false if RECOVERABLE_STATUSES.include? stack.stack_status
99
+ raise UpdateError.new("Stack is in an unrecoverable state") if UNRECOVERABLE_STATUSES.include? stack.stack_status
100
+ raise UpdateError.new("Stack is busy, try again soon")
101
+ rescue Aws::CloudFormation::Errors::ValidationError
102
+ false
103
+ end
104
+
105
+ def create!(dynamic_params)
106
+ cloudformation_params = ParameterBuilder.new(dynamic_params, @stack_params_filepath).params
107
+ cloudformation.create_stack(
108
+ stack_name: @stack_name,
109
+ template_body: File.read(@stack_template),
110
+ parameters: cloudformation_params,
111
+ capabilities: ["CAPABILITY_IAM"],
112
+ on_failure: "DELETE"
113
+ )
114
+
115
+ begin
116
+ cloudformation.wait_until(:stack_create_complete, stack_name: @stack_name) { |waiter| waiter.delay = 20; waiter.max_attempts = 45 }
117
+ rescue Aws::Waiters::Errors::UnexpectedError => ex
118
+ handle_unexpected_error(ex)
119
+ end
120
+ end
121
+
122
+ def update!(dynamic_params={})
123
+ cloudformation_params = ParameterBuilder.new(dynamic_params, @stack_params_filepath).params
124
+ wait_until_ready(false)
125
+
126
+ cloudformation.update_stack(
127
+ stack_name: @stack_name,
128
+ template_body: File.read(@stack_template),
129
+ parameters: cloudformation_params,
130
+ capabilities: ["CAPABILITY_IAM"]
131
+ )
132
+
133
+ cloudformation.wait_until(:stack_update_complete, stack_name: @stack_name) { |waiter| waiter.delay = 20; waiter.max_attempts = 45 }
134
+ rescue Aws::CloudFormation::Errors::ValidationError => ex
135
+ raise ex unless ex.message == "No updates are to be performed."
136
+ ConsoleJockey.write_line "No changes need to be applied for #{@stack_name}."
137
+ rescue Aws::Waiters::Errors::FailureStateError => ex
138
+ ConsoleJockey.write_line "Failed to apply the environment update. The stack has been rolled back. It is still safe to apply updates."
139
+ ConsoleJockey.write_line "Find error details in the AWS CloudFormation console: #{stack_events_url}"
140
+ raise UpdateError.new("Stack update failed for #{@stack_name}.")
141
+ end
142
+
143
+ def stack_events_url
144
+ "https://console.aws.amazon.com/cloudformation/home?region=#{ENV['AWS_DEFAULT_REGION']}#/stacks?filter=active&tab=events&stackId=#{get_stack.stack_id}"
145
+ end
146
+
147
+ def wait_until_ready(raise_on_error=true)
148
+ loop do
149
+ stack = get_stack(dump_cache: true)
150
+
151
+ if stack_ready?(stack.stack_status)
152
+ if raise_on_error && stack_operation_failed?(stack.stack_status)
153
+ raise stack.stack_status
154
+ end
155
+
156
+ break
157
+ end
158
+ puts "waiting for #{@stack_name} to be READY, current: #{last_event_status}"
159
+ sleep 10
160
+ end
161
+ rescue Aws::CloudFormation::Errors::ValidationError
162
+ nil
163
+ end
164
+
165
+ def stack_ready?(last_event_status)
166
+ last_event_status =~ /COMPLETE/ || last_event_status =~ /ROLLBACK_FAILED/
167
+ end
168
+
169
+ def stack_operation_failed?(last_event_status)
170
+ last_event_status =~ /ROLLBACK/
171
+ end
172
+
173
+ def handle_unexpected_error(error)
174
+ if error.message =~ /does not exist/
175
+ ConsoleJockey.write_line "There was an error during stack creation for #{@stack_name}, and the stack has been cleaned up."
176
+ raise CreateError.new("There was an error during stack creation. The stack has been deleted.")
177
+ else
178
+ raise error
179
+ end
180
+ end
181
+
182
+ def flash_message(message)
183
+ ConsoleJockey.flash_message(message)
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,19 @@
1
+ module KumoKeisei
2
+ class ConsoleJockey
3
+ def self.flash_message(message)
4
+ puts "\n"
5
+ puts "###################=============================------------"
6
+ puts message
7
+ puts "------------=============================###################"
8
+ puts "\n"
9
+
10
+ $stdout.flush
11
+ end
12
+
13
+ def self.write_line(message)
14
+ puts message
15
+
16
+ $stdout.flush
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,110 @@
1
+ require 'kumo_ki'
2
+ require 'logger'
3
+ require 'yaml'
4
+
5
+ require_relative 'file_loader'
6
+ require_relative 'parameter_builder'
7
+
8
+ class KumoKeisei::EnvironmentConfig
9
+ LOGGER = Logger.new(STDOUT)
10
+
11
+ attr_reader :app_name, :env_name
12
+
13
+ def initialize(options, logger = LOGGER)
14
+ @app_name = options[:app_name]
15
+ @env_name = options[:env_name]
16
+ @config_dir_path = options[:config_dir_path]
17
+ @params_template_file_path = options[:params_template_file_path]
18
+ @injected_config = options[:injected_config] || {}
19
+ @file_loader = KumoKeisei::FileLoader.new(options)
20
+
21
+ @log = logger
22
+ end
23
+
24
+ def get_binding
25
+ binding
26
+ end
27
+
28
+ def production?
29
+ env_name == "production"
30
+ end
31
+
32
+ def development?
33
+ !(%w(production staging).include?(env_name))
34
+ end
35
+
36
+ def plain_text_secrets
37
+ @plain_text_secrets ||= decrypt_secrets(encrypted_secrets)
38
+ end
39
+
40
+ def config
41
+ @config ||= common_config.merge(env_config).merge(@injected_config)
42
+ end
43
+
44
+ def cf_params
45
+ return [] unless params_template
46
+ KumoKeisei::ParameterBuilder.new(get_stack_params(params_template)).params
47
+ end
48
+
49
+ private
50
+
51
+ def kms
52
+ @kms ||= KumoKi::KMS.new
53
+ end
54
+
55
+ def get_stack_params(params_template)
56
+ YAML.load(ERB.new(params_template).result(get_binding))
57
+ end
58
+
59
+ def params_template
60
+ return nil unless @params_template_file_path
61
+
62
+ @file_loader.load_config!(@params_template_file_path)
63
+ end
64
+
65
+ def decrypt_secrets(secrets)
66
+ Hash[
67
+ secrets.map do |name, cipher_text|
68
+ @log.debug "Decrypting '#{name}'"
69
+ if cipher_text.start_with? '[ENC,'
70
+ begin
71
+ [name, "#{kms.decrypt cipher_text[5,cipher_text.size]}"]
72
+ rescue
73
+ @log.error "Error decrypting secret '#{name}'"
74
+ raise
75
+ end
76
+ else
77
+ [name, cipher_text]
78
+ end
79
+ end
80
+ ]
81
+ end
82
+
83
+ def env_config_file_name
84
+ "#{env_name}.yml"
85
+ end
86
+
87
+ def env_secrets_file_name
88
+ "#{env_name}_secrets.yml"
89
+ end
90
+
91
+ def encrypted_secrets
92
+ encrypted_common_secrets.merge(encrypted_env_secrets)
93
+ end
94
+
95
+ def encrypted_common_secrets
96
+ @file_loader.load_config('common_secrets.yml')
97
+ end
98
+
99
+ def encrypted_env_secrets
100
+ @file_loader.load_config(env_secrets_file_name)
101
+ end
102
+
103
+ def common_config
104
+ @file_loader.load_config('common.yml')
105
+ end
106
+
107
+ def env_config
108
+ @file_loader.load_config(env_config_file_name)
109
+ end
110
+ end
@@ -0,0 +1,24 @@
1
+ module KumoKeisei
2
+ class FileLoader
3
+ def initialize(options)
4
+ @config_dir_path = options[:config_dir_path]
5
+ end
6
+
7
+ def load_config!(file_name, context = nil)
8
+ erb_result = ERB.new(File.read(file_path(file_name))).result(context)
9
+ YAML.load(erb_result)
10
+ end
11
+
12
+ def load_config(file_name)
13
+ path = file_path(file_name)
14
+ return {} unless File.exist?(path)
15
+ load_config!(file_name)
16
+ end
17
+
18
+ private
19
+
20
+ def file_path(file_name)
21
+ File.join(@config_dir_path, file_name)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,36 @@
1
+ require 'json'
2
+
3
+ module KumoKeisei
4
+ class ParameterBuilder
5
+ def initialize(dynamic_params = {}, file_path = nil)
6
+ @dynamic_params = dynamic_params
7
+ @file_path = file_path
8
+ end
9
+
10
+ def params
11
+ parsed_dynamic_params + parsed_file_params
12
+ end
13
+
14
+ def parsed_dynamic_params
15
+ @dynamic_params.map do |key, value|
16
+ {
17
+ parameter_key: key.to_s,
18
+ parameter_value: value
19
+ }
20
+ end
21
+ end
22
+
23
+ def parsed_file_params
24
+ return [] unless (@file_path && File.exists?(@file_path))
25
+
26
+ file_contents = JSON.parse(File.read(@file_path))
27
+
28
+ file_contents.map do |param|
29
+ {
30
+ parameter_key: param["ParameterKey"],
31
+ parameter_value: param["ParameterValue"]
32
+ }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,2 @@
1
+ require_relative "kumo_keisei/cloud_formation_stack"
2
+ require_relative "kumo_keisei/environment_config"
data/script/build.sh ADDED
@@ -0,0 +1,13 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ bundle install
6
+
7
+ if [[ -z "$KUMO_KEISEI_VERSION" && -n "$BUILDKITE_BUILD_NUMBER" ]]; then
8
+ export KUMO_KEISEI_VERSION="$BUILDKITE_BUILD_NUMBER"
9
+ fi
10
+
11
+ echo "--- :wind_chime: Building gem :wind_chime:"
12
+
13
+ gem build kumo_keisei.gemspec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/kumo_keisei'
4
+
5
+ def run_command(cmd)
6
+ puts cmd
7
+ puts `#{cmd}`
8
+ raise "non zero exit code" if $?.exitstatus != 0
9
+ end
10
+
11
+ tag = File.read(File.expand_path('../../VERSION', __FILE__)).strip
12
+
13
+ run_command "git tag #{tag}"
14
+ run_command "git push origin #{tag}"
@@ -0,0 +1,13 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ echo "--- :clock1: :clock2: running specs :clock3: :clock4:"
6
+ bundle install && bundle exec rspec
7
+
8
+ function inline_image {
9
+ printf '\033]1338;url='"$1"';alt='"$2"'\a\n'
10
+ }
11
+
12
+ echo "+++ Done! :thumbsup: :shipit:"
13
+ inline_image "https://giftoppr.desktopprassets.com/uploads/f828c372186b5fa80a1c553adbcd4bc4d331396b/tumblr_m2cg550aq21ql201ao1_500.gif" "Yuss"
@@ -0,0 +1,191 @@
1
+ require 'spec_helper'
2
+ require 'ostruct'
3
+
4
+ describe KumoKeisei::CloudFormationStack do
5
+
6
+ def stack_result_list_with_status(status, stack_name)
7
+ stack = OpenStruct.new(stack_status: status, stack_name: stack_name)
8
+ OpenStruct.new(stacks: [stack])
9
+ end
10
+
11
+ let(:stack_name) { "my-stack" }
12
+ let(:stack_template_path) { "template.json" }
13
+ let(:file_params_path) { nil }
14
+ let(:cloudformation) { instance_double(Aws::CloudFormation::Client) }
15
+ let(:happy_stack_status) { "CREATE_COMPLETE" }
16
+ let(:cf_stack) { stack_result_list_with_status(happy_stack_status, stack_name) }
17
+ let(:parameter_builder) { instance_double(KumoKeisei::ParameterBuilder, params: {}) }
18
+ let(:stack_template_body) { double(:stack_template_body) }
19
+ let(:cf_stack_update_params) do
20
+ {
21
+ stack_name: stack_name,
22
+ template_body: stack_template_body,
23
+ parameters: {},
24
+ capabilities: ["CAPABILITY_IAM"]
25
+ }
26
+ end
27
+ let(:cf_stack_create_params) do
28
+ cf_stack_update_params.merge(on_failure: "DELETE")
29
+ end
30
+
31
+ subject(:instance) { KumoKeisei::CloudFormationStack.new(stack_name, stack_template_path, file_params_path) }
32
+
33
+ before do
34
+ allow(KumoKeisei::ConsoleJockey).to receive(:flash_message)
35
+ allow(KumoKeisei::ConsoleJockey).to receive(:write_line).and_return(nil)
36
+ allow(Aws::CloudFormation::Client).to receive(:new).and_return(cloudformation)
37
+ allow(cloudformation).to receive(:describe_stacks).with({stack_name: stack_name}).and_return(cf_stack)
38
+ allow(KumoKeisei::ParameterBuilder).to receive(:new).and_return(parameter_builder)
39
+ allow(File).to receive(:read).with(stack_template_path).and_return(stack_template_body)
40
+ end
41
+
42
+ describe "#destroy!" do
43
+
44
+ it "deletes the stack" do
45
+ expect(cloudformation).to receive(:delete_stack).with({stack_name: stack_name}).and_return(cf_stack)
46
+ allow(cloudformation).to receive(:wait_until).with(:stack_delete_complete, stack_name: stack_name).and_return(nil)
47
+
48
+ subject.destroy!
49
+ end
50
+
51
+ end
52
+
53
+ describe "#apply!" do
54
+ context "when the stack is updatable" do
55
+ UPDATEABLE_STATUSES = ['UPDATE_ROLLBACK_COMPLETE', 'CREATE_COMPLETE', 'UPDATE_COMPLETE', 'DELETE_COMPLETE']
56
+
57
+ context "when the stack has changed" do
58
+ before do
59
+ allow(cloudformation).to receive(:wait_until).with(:stack_update_complete, stack_name: stack_name).and_return(nil)
60
+ end
61
+
62
+ UPDATEABLE_STATUSES.each do |stack_status|
63
+ it "updates the stack when in #{stack_status} status" do
64
+ allow(cloudformation).to receive(:describe_stacks).with({stack_name: stack_name}).and_return(
65
+ stack_result_list_with_status(stack_status, stack_name),
66
+ stack_result_list_with_status("UPDATE_COMPLETE", stack_name)
67
+ )
68
+ expect(cloudformation).to receive(:update_stack).with(cf_stack_update_params).and_return("stack_id")
69
+ subject.apply!
70
+ end
71
+ end
72
+
73
+ it "politely informs the user of any failures" do
74
+ allow(cloudformation).to receive(:wait_until)
75
+ .with(:stack_update_complete, stack_name: stack_name)
76
+ .and_raise(Aws::Waiters::Errors::FailureStateError.new(""))
77
+
78
+ allow(cloudformation).to receive(:update_stack).with(cf_stack_update_params).and_return("stack_id")
79
+ expect(KumoKeisei::ConsoleJockey).to receive(:write_line).with("Failed to apply the environment update. The stack has been rolled back. It is still safe to apply updates.")
80
+
81
+ expect { subject.apply! }.to raise_error(KumoKeisei::CloudFormationStack::UpdateError)
82
+ end
83
+ end
84
+
85
+ context "when the stack has not changed" do
86
+ let(:error) { Aws::CloudFormation::Errors::ValidationError.new('', 'No updates are to be performed.') }
87
+
88
+ UPDATEABLE_STATUSES.each do |stack_status|
89
+ it "reports that nothing has changed when in #{stack_status} status" do
90
+ allow(cloudformation).to receive(:describe_stacks)
91
+ .with({stack_name: stack_name})
92
+ .and_return(stack_result_list_with_status(stack_status, stack_name))
93
+
94
+ expect(cloudformation).to receive(:update_stack).with(cf_stack_update_params).and_raise(error)
95
+ expect(KumoKeisei::ConsoleJockey).to receive(:flash_message).with(/Stack name: #{stack_name}/)
96
+
97
+ subject.apply!
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ context "when the stack is not updatable" do
104
+ before do
105
+ allow(cloudformation).to receive(:wait_until).with(:stack_delete_complete, stack_name: stack_name).and_return(nil)
106
+ allow(cloudformation).to receive(:wait_until).with(:stack_create_complete, stack_name: stack_name).and_return(nil)
107
+ allow(cloudformation).to receive(:delete_stack).with(stack_name: stack_name)
108
+ end
109
+
110
+ context "and the stack does not exist" do
111
+ let(:stack_name) { "my-stack" }
112
+
113
+ it "creates the stack" do
114
+ allow(cloudformation).to receive(:delete_stack)
115
+ allow(cloudformation).to receive(:describe_stacks).with(stack_name: stack_name).and_raise(Aws::CloudFormation::Errors::ValidationError.new('',''))
116
+ expect(cloudformation).to receive(:create_stack).with(cf_stack_create_params)
117
+ subject.apply!
118
+ end
119
+
120
+ it "shows a friendly error message if the stack had issues during creation" do
121
+ allow(cloudformation).to receive(:delete_stack)
122
+ allow(cloudformation).to receive(:describe_stacks).with(stack_name: stack_name).and_raise(Aws::CloudFormation::Errors::ValidationError.new('',''))
123
+ allow(cloudformation).to receive(:create_stack).with(cf_stack_create_params)
124
+
125
+ error = Aws::Waiters::Errors::UnexpectedError.new(RuntimeError.new("Stack with id #{stack_name} does not exist"))
126
+ allow(cloudformation).to receive(:wait_until).with(:stack_create_complete, stack_name: stack_name).and_raise(error)
127
+
128
+ expect(KumoKeisei::ConsoleJockey).to receive(:write_line).with(/There was an error during stack creation for #{stack_name}, and the stack has been cleaned up./).and_return nil
129
+ expect { subject.apply! }.to raise_error(KumoKeisei::CloudFormationStack::CreateError)
130
+ end
131
+ end
132
+
133
+ context "and the stack is in ROLLBACK_COMPLETE, or ROLLBACK_FAILED" do
134
+ it "deletes the dead stack and creates a new one" do
135
+ allow(cloudformation).to receive(:describe_stacks).and_return(stack_result_list_with_status("ROLLBACK_COMPLETE", stack_name))
136
+
137
+ expect(cloudformation).to receive(:delete_stack).with(stack_name: stack_name)
138
+ expect(cloudformation).to receive(:create_stack)
139
+
140
+ allow(cloudformation).to receive(:wait_until).with(:stack_create_complete, stack_name: stack_name).and_return(nil)
141
+
142
+ subject.apply!
143
+ end
144
+ end
145
+
146
+ context "and the stack in in UPDATE_ROLLBACK_FAILED" do
147
+ it "should blow up" do
148
+ allow(cloudformation).to receive(:describe_stacks).and_return(stack_result_list_with_status("UPDATE_ROLLBACK_FAILED", stack_name))
149
+
150
+ expect { subject.apply! }.to raise_error("Stack is in an unrecoverable state")
151
+ end
152
+ end
153
+
154
+ context "and the stack is busy" do
155
+ it "should blow up" do
156
+ allow(cloudformation).to receive(:describe_stacks).and_return(stack_result_list_with_status("UPDATE_IN_PROGRESS", stack_name))
157
+
158
+ expect { subject.apply! }.to raise_error("Stack is busy, try again soon")
159
+ end
160
+ end
161
+ end
162
+
163
+ describe "#outputs" do
164
+ let(:output) { double(:output, output_key: "Key", output_value: "Value") }
165
+ let(:stack) { double(:stack, stack_name: stack_name, outputs: [output])}
166
+ let(:stack_result) { double(:stack_result, stacks: [stack]) }
167
+
168
+ it "returns the outputs given by CloudFormation" do
169
+ allow(cloudformation).to receive(:describe_stacks).and_return(stack_result)
170
+ expect(subject.outputs("Key")).to eq("Value")
171
+ end
172
+ end
173
+
174
+ describe "#logical_resource" do
175
+ let(:stack_resource_detail) { OpenStruct.new(logical_resource_id: "with-a-fox", physical_resource_id: "i-am-sam", resource_type: "green-eggs-and-ham")}
176
+ let(:response) { double(:response, stack_resource_detail: stack_resource_detail) }
177
+ let(:stack_resource_name) { "with-a-fox" }
178
+
179
+ it "returns a hash of the stack resource detail params" do
180
+ allow(cloudformation).to receive(:describe_stack_resource)
181
+ .with(stack_name: stack_name, logical_resource_id: stack_resource_name)
182
+ .and_return(response)
183
+
184
+ expect(subject.logical_resource(stack_resource_name)).to include(
185
+ "PhysicalResourceId" => "i-am-sam",
186
+ "ResourceType" => "green-eggs-and-ham"
187
+ )
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,160 @@
1
+ require 'spec_helper'
2
+
3
+ describe KumoKeisei::EnvironmentConfig do
4
+ let(:env_name) { 'the_jungle' }
5
+ let(:config_dir_path) { '/var/config' }
6
+ let(:options) do
7
+ {
8
+ env_name: env_name,
9
+ config_dir_path: config_dir_path,
10
+ params_template_file_path: params_template_file_path
11
+ }
12
+ end
13
+ let(:file_loader) { instance_double(KumoKeisei::FileLoader) }
14
+ let(:parameter_template) { "stack_name: <%= config['stack_name'] %>" }
15
+ let(:params_template_file_path) { '/junk.txt' }
16
+ let(:environment_config_file_name) { "#{env_name}.yml" }
17
+ let(:kms) { instance_double(KumoKi::KMS) }
18
+ let(:logger) { double(:test_logger, debug: nil) }
19
+
20
+ before do
21
+ allow(KumoKeisei::FileLoader).to receive(:new).and_return(file_loader)
22
+ allow(KumoKi::KMS).to receive(:new).and_return(kms)
23
+ allow(file_loader).to receive(:load_config!).with(params_template_file_path).and_return(parameter_template)
24
+ end
25
+
26
+ describe '#cf_params' do
27
+ subject { described_class.new(options, logger).cf_params }
28
+
29
+ context 'params template file path is not provided' do
30
+ let(:options) do
31
+ {
32
+ env_name: env_name,
33
+ config_dir_path: config_dir_path
34
+ }
35
+ end
36
+
37
+ it 'creates an empty array' do
38
+ expect(subject).to eq([])
39
+ end
40
+ end
41
+
42
+ context 'params file is empty' do
43
+ let(:parameter_template) { nil }
44
+
45
+ it 'creates an empty array' do
46
+ expect(subject).to eq([])
47
+ end
48
+ end
49
+
50
+ context 'a hard-coded param' do
51
+ let(:parameter_template) { "parameter_key: <%= 'parameter_value' %>" }
52
+
53
+ it 'creates a array containing an aws formatted parameter hash' do
54
+ expect(subject).to eq([{parameter_key: "parameter_key", parameter_value: "parameter_value"}])
55
+ end
56
+ end
57
+
58
+ context 'templated params' do
59
+ let(:environment_parameters) { { "stack_name" => "okonomiyaki" } }
60
+
61
+ context 'environment params' do
62
+ it 'creates a array containing an aws formatted parameter hash' do
63
+ allow(file_loader).to receive(:load_config).with('common.yml').and_return({})
64
+ allow(file_loader).to receive(:load_config).with(environment_config_file_name).and_return(environment_parameters)
65
+
66
+ expect(subject).to eq([{parameter_key: "stack_name", parameter_value: "okonomiyaki"}])
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ describe "#config" do
73
+ subject { described_class.new(options, logger).config }
74
+
75
+ context 'injected config' do
76
+
77
+ let(:options) do
78
+ {
79
+ env_name: env_name,
80
+ config_dir_path: config_dir_path,
81
+ params_template_file_path: params_template_file_path,
82
+ injected_config: { "injected" => "yes" }
83
+ }
84
+ end
85
+
86
+ let(:common_parameters) { { "stack_name" => "okonomiyaki" } }
87
+ it 'adds injected config to the config hash' do
88
+ allow(file_loader).to receive(:load_config).with('common.yml').and_return(common_parameters)
89
+ allow(file_loader).to receive(:load_config).with(environment_config_file_name).and_return({})
90
+
91
+ expect(subject).to eq({ "stack_name" => "okonomiyaki", "injected" => "yes" })
92
+ end
93
+ end
94
+
95
+ context 'common config' do
96
+ let(:common_parameters) { { "stack_name" => "okonomiyaki" } }
97
+
98
+ it 'creates a array containing an aws formatted parameter hash' do
99
+ allow(file_loader).to receive(:load_config).with('common.yml').and_return(common_parameters)
100
+ allow(file_loader).to receive(:load_config).with(environment_config_file_name).and_return({})
101
+
102
+ expect(subject).to eq('stack_name' => 'okonomiyaki')
103
+ end
104
+ end
105
+
106
+ context 'merging common and environment specific configurations' do
107
+ context 'with environmental overrides' do
108
+ let(:parameter_template) { "image: <%= config['image'] %>" }
109
+ let(:common_config) { {'image' => 'ami-1234'} }
110
+ let(:environment_config) { {'image' => 'ami-5678'} }
111
+ let(:env_name) { 'development' }
112
+
113
+ it 'replaces the common value with the env value' do
114
+ allow(file_loader).to receive(:load_config).with('common.yml').and_return(common_config)
115
+ allow(file_loader).to receive(:load_config).with('development.yml').and_return(environment_config)
116
+
117
+ expect(subject).to eq('image' => 'ami-5678')
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ describe "#plain_text_secrets" do
124
+ subject { described_class.new(options, logger).plain_text_secrets }
125
+
126
+ let(:crypted_password) { 'lookatmyencryptedpasswords' }
127
+ let(:plain_text_password) { 'plain_text_password' }
128
+ let(:secrets) { { 'secret_password' => "[ENC,#{crypted_password}" } }
129
+
130
+ let(:crypted_env_password) { 'cryptedenvpassword' }
131
+ let(:plain_text_env_password) { 'plain_text_env_password' }
132
+ let(:env_secrets) { { 'secret_password' => "[ENC,#{crypted_env_password}"}}
133
+
134
+ before do
135
+ allow(kms).to receive(:decrypt).with(crypted_password).and_return(plain_text_password)
136
+ end
137
+
138
+ it 'should decrypt common secrets' do
139
+ allow(file_loader).to receive(:load_config).with('common_secrets.yml').and_return(secrets)
140
+ allow(file_loader).to receive(:load_config).with("#{env_name}_secrets.yml").and_return({})
141
+
142
+ expect(subject).to eq('secret_password' => plain_text_password)
143
+ end
144
+
145
+ it 'should decrypt environment secrets' do
146
+ allow(file_loader).to receive(:load_config).with('common_secrets.yml').and_return({})
147
+ allow(file_loader).to receive(:load_config).with("#{env_name}_secrets.yml").and_return(secrets)
148
+
149
+ expect(subject).to eq('secret_password' => plain_text_password)
150
+ end
151
+
152
+ it 'should give preference to environment secrets' do
153
+ allow(file_loader).to receive(:load_config).with('common_secrets.yml').and_return(secrets)
154
+ allow(file_loader).to receive(:load_config).with("#{env_name}_secrets.yml").and_return(env_secrets)
155
+ allow(kms).to receive(:decrypt).with(crypted_env_password).and_return(plain_text_env_password)
156
+
157
+ expect(subject).to eq('secret_password' => plain_text_env_password)
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+
3
+ describe KumoKeisei::FileLoader do
4
+ describe "#load_config" do
5
+ let(:config_dir_path) { '/the/garden/path' }
6
+ let(:options) { { config_dir_path: config_dir_path } }
7
+ let(:file_name) { 'environment.yml' }
8
+ let(:full_file_path) { config_dir_path + '/' + file_name }
9
+
10
+ subject { KumoKeisei::FileLoader.new(options).load_config(file_name) }
11
+
12
+ context "when the file does not exist" do
13
+ it "returns an empty hash" do
14
+ expect(subject).to eq({})
15
+ end
16
+ end
17
+
18
+ context "when the file does exist" do
19
+ let(:file_contents) { 'key: value' }
20
+
21
+ it "populates a hash" do
22
+ expect(File).to receive(:exist?).with(full_file_path).and_return(true)
23
+ expect(File).to receive(:read).with(full_file_path).and_return(file_contents)
24
+ expect(subject).to eq({ 'key' => 'value' })
25
+ end
26
+ end
27
+ end
28
+
29
+ describe '#load_config!' do
30
+ let(:config_dir_path) { '/the/garden/path' }
31
+ let(:options) { { config_dir_path: config_dir_path } }
32
+ let(:file_name) { 'environment.yml' }
33
+ let(:full_file_path) { config_dir_path + '/' + file_name }
34
+
35
+ subject { KumoKeisei::FileLoader.new(options).load_config!(file_name) }
36
+
37
+ context 'when the file does not exist' do
38
+ it 'raises an error' do
39
+ expect { subject }.to raise_error(Errno::ENOENT)
40
+ end
41
+ end
42
+
43
+ context 'when the file exists' do
44
+ let(:file_contents) { 'key: value' }
45
+
46
+ it 'populates a hash' do
47
+ expect(File).to receive(:read).with(full_file_path).and_return(file_contents)
48
+ expect(subject).to eq({ 'key' => 'value' })
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+
3
+ describe KumoKeisei::ParameterBuilder do
4
+ subject {described_class.new(dynamic_params, file_path) }
5
+ let(:dynamic_params) { {} }
6
+ let(:file_path) { nil }
7
+ let(:file_content) do
8
+ [
9
+ {
10
+ "ParameterKey" => "testFileKey",
11
+ "ParameterValue" => "testFileValue",
12
+ },
13
+ ]
14
+ end
15
+
16
+ describe '#params' do
17
+ before do
18
+ allow(File).to receive(:exists?).with(file_path).and_return(true)
19
+ allow(File).to receive(:read).with(file_path).and_return(file_content.to_json)
20
+ end
21
+
22
+ context "when there are dynamic params" do
23
+ let(:dynamic_params) { { key: 'value', other_key: 'other_value' } }
24
+ it 'includes command line params' do
25
+ expect(subject.params).to eq([{ parameter_key: 'key', parameter_value: 'value' }, { parameter_key: 'other_key', parameter_value: 'other_value'}])
26
+ end
27
+ end
28
+
29
+ context "when there are file params" do
30
+ let(:file_path) { '/some/path/to/params.json' }
31
+
32
+ it 'includes an input file' do
33
+ expect(subject.params).to eq([{ parameter_key: 'testFileKey', parameter_value: 'testFileValue' }])
34
+ end
35
+ end
36
+
37
+ context "there are both" do
38
+ let(:dynamic_params) { { key: 'value', other_key: 'other_value' } }
39
+ let(:file_path) { '/some/path/to/params.json' }
40
+
41
+ it 'includes command line params' do
42
+ expect(subject.params).to eq([
43
+ { parameter_key: 'key', parameter_value: 'value' },
44
+ { parameter_key: 'other_key', parameter_value: 'other_value'},
45
+ { parameter_key: 'testFileKey', parameter_value: 'testFileValue' }
46
+ ])
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,101 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
4
+ # this file to always be loaded, without a need to explicitly require it in any
5
+ # files.
6
+ #
7
+ # Given that it is always loaded, you are encouraged to keep this file as
8
+ # light-weight as possible. Requiring heavyweight dependencies from this file
9
+ # will add to the boot time of your test suite on EVERY test run, even for an
10
+ # individual file that may not need all of that loaded. Instead, consider making
11
+ # a separate helper file that requires the additional dependencies and performs
12
+ # the additional setup, and require it from the spec files that actually need
13
+ # it.
14
+ #
15
+ # The `.rspec` file also contains a few flags that are not defaults but that
16
+ # users commonly want.
17
+ #
18
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
19
+
20
+ ENV['RUBY_ENV'] ||= 'test'
21
+ require File.expand_path("../../lib/kumo_keisei.rb", __FILE__)
22
+ Dir[File.expand_path("../support/*.rb", __FILE__)].each {|file| require file }
23
+
24
+ RSpec.configure do |config|
25
+ # rspec-expectations config goes here. You can use an alternate
26
+ # assertion/expectation library such as wrong or the stdlib/minitest
27
+ # assertions if you prefer.
28
+ config.expect_with :rspec do |expectations|
29
+ # This option will default to `true` in RSpec 4. It makes the `description`
30
+ # and `failure_message` of custom matchers include text for helper methods
31
+ # defined using `chain`, e.g.:
32
+ # be_bigger_than(2).and_smaller_than(4).description
33
+ # # => "be bigger than 2 and smaller than 4"
34
+ # ...rather than:
35
+ # # => "be bigger than 2"
36
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
37
+ end
38
+
39
+ # rspec-mocks config goes here. You can use an alternate test double
40
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
41
+ config.mock_with :rspec do |mocks|
42
+ # Prevents you from mocking or stubbing a method that does not exist on
43
+ # a real object. This is generally recommended, and will default to
44
+ # `true` in RSpec 4.
45
+ mocks.verify_partial_doubles = true
46
+ end
47
+
48
+ # The settings below are suggested to provide a good initial experience
49
+ # with RSpec, but feel free to customize to your heart's content.
50
+ =begin
51
+ # These two settings work together to allow you to limit a spec run
52
+ # to individual examples or groups you care about by tagging them with
53
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
54
+ # get run.
55
+ config.filter_run :focus
56
+ config.run_all_when_everything_filtered = true
57
+
58
+ # Allows RSpec to persist some state between runs in order to support
59
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
60
+ # you configure your source control system to ignore this file.
61
+ config.example_status_persistence_file_path = "spec/examples.txt"
62
+
63
+ # Limits the available syntax to the non-monkey patched syntax that is
64
+ # recommended. For more details, see:
65
+ # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
66
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
67
+ # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
68
+ config.disable_monkey_patching!
69
+
70
+ # This setting enables warnings. It's recommended, but in some cases may
71
+ # be too noisy due to issues in dependencies.
72
+ config.warnings = true
73
+
74
+ # Many RSpec users commonly either run the entire suite or an individual
75
+ # file, and it's useful to allow more verbose output when running an
76
+ # individual spec file.
77
+ if config.files_to_run.one?
78
+ # Use the documentation formatter for detailed output,
79
+ # unless a formatter has already been configured
80
+ # (e.g. via a command-line flag).
81
+ config.default_formatter = 'doc'
82
+ end
83
+
84
+ # Print the 10 slowest examples and example groups at the
85
+ # end of the spec run, to help surface which specs are running
86
+ # particularly slow.
87
+ config.profile_examples = 10
88
+
89
+ # Run specs in random order to surface order dependencies. If you find an
90
+ # order dependency and want to debug it, you can fix the order by providing
91
+ # the seed, which is printed after each run.
92
+ # --seed 1234
93
+ config.order = :random
94
+
95
+ # Seed global randomization in this process using the `--seed` CLI option.
96
+ # Setting this allows you to use `--seed` to deterministically reproduce
97
+ # test failures related to randomization by passing the same `--seed` value
98
+ # as the one that triggered the failure.
99
+ Kernel.srand config.seed
100
+ =end
101
+ end
metadata ADDED
@@ -0,0 +1,141 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kumo_keisei
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.52
5
+ platform: ruby
6
+ authors:
7
+ - Redbubble
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-05-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: kumo_ki
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.6'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.4'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.4'
83
+ description:
84
+ email:
85
+ - delivery-engineering@redbubble.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".buildkite/pipeline.yml"
91
+ - ".gitignore"
92
+ - Gemfile
93
+ - LICENSE.txt
94
+ - README.md
95
+ - Rakefile
96
+ - VERSION
97
+ - kumo_keisei.gemspec
98
+ - lib/kumo_keisei.rb
99
+ - lib/kumo_keisei/cloud_formation_stack.rb
100
+ - lib/kumo_keisei/console_jockey.rb
101
+ - lib/kumo_keisei/environment_config.rb
102
+ - lib/kumo_keisei/file_loader.rb
103
+ - lib/kumo_keisei/parameter_builder.rb
104
+ - script/build.sh
105
+ - script/release-gem
106
+ - script/unit_test.sh
107
+ - spec/lib/kumo_keisei/cloud_formation_stack_spec.rb
108
+ - spec/lib/kumo_keisei/environment_config_spec.rb
109
+ - spec/lib/kumo_keisei/file_loader_spec.rb
110
+ - spec/lib/kumo_keisei/parameter_builder_spec.rb
111
+ - spec/spec_helper.rb
112
+ homepage: http://redbubble.com
113
+ licenses:
114
+ - MIT
115
+ metadata: {}
116
+ post_install_message:
117
+ rdoc_options: []
118
+ require_paths:
119
+ - lib
120
+ required_ruby_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ requirements: []
131
+ rubyforge_project:
132
+ rubygems_version: 2.2.2
133
+ signing_key:
134
+ specification_version: 4
135
+ summary: A collection of utilities for dealing with AWS Cloud Formation.
136
+ test_files:
137
+ - spec/lib/kumo_keisei/cloud_formation_stack_spec.rb
138
+ - spec/lib/kumo_keisei/environment_config_spec.rb
139
+ - spec/lib/kumo_keisei/file_loader_spec.rb
140
+ - spec/lib/kumo_keisei/parameter_builder_spec.rb
141
+ - spec/spec_helper.rb