kumo_keisei 0.0.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.buildkite/pipeline.yml +10 -0
- data/.gitignore +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +61 -0
- data/Rakefile +2 -0
- data/VERSION +1 -0
- data/kumo_keisei.gemspec +25 -0
- data/lib/kumo_keisei/cloud_formation_stack.rb +186 -0
- data/lib/kumo_keisei/console_jockey.rb +19 -0
- data/lib/kumo_keisei/environment_config.rb +110 -0
- data/lib/kumo_keisei/file_loader.rb +24 -0
- data/lib/kumo_keisei/parameter_builder.rb +36 -0
- data/lib/kumo_keisei.rb +2 -0
- data/script/build.sh +13 -0
- data/script/release-gem +14 -0
- data/script/unit_test.sh +13 -0
- data/spec/lib/kumo_keisei/cloud_formation_stack_spec.rb +191 -0
- data/spec/lib/kumo_keisei/environment_config_spec.rb +160 -0
- data/spec/lib/kumo_keisei/file_loader_spec.rb +52 -0
- data/spec/lib/kumo_keisei/parameter_builder_spec.rb +50 -0
- data/spec/spec_helper.rb +101 -0
- metadata +141 -0
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
|
data/.gitignore
ADDED
data/Gemfile
ADDED
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 [](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
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.52
|
data/kumo_keisei.gemspec
ADDED
@@ -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
|
data/lib/kumo_keisei.rb
ADDED
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
|
data/script/release-gem
ADDED
@@ -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}"
|
data/script/unit_test.sh
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|