kumo_dockercloud 1.0.0
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 +9 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +49 -0
- data/Rakefile +7 -0
- data/kumo_dockercloud.gemspec +29 -0
- data/lib/kumo_dockercloud.rb +3 -0
- data/lib/kumo_dockercloud/deployment.rb +126 -0
- data/lib/kumo_dockercloud/docker_cloud_api.rb +31 -0
- data/lib/kumo_dockercloud/environment.rb +110 -0
- data/lib/kumo_dockercloud/environment_config.rb +116 -0
- data/lib/kumo_dockercloud/stack.rb +46 -0
- data/lib/kumo_dockercloud/stack_file.rb +28 -0
- data/lib/kumo_dockercloud/state_validator.rb +49 -0
- data/lib/kumo_dockercloud/version.rb +3 -0
- data/script/build.sh +11 -0
- data/script/release-gem +14 -0
- data/script/unit_test.sh +14 -0
- data/spec/fixtures/config/test.yml +0 -0
- data/spec/fixtures/config/test_encrypted_secrets.yml +2 -0
- data/spec/fixtures/config/test_secrets.yml +2 -0
- data/spec/fixtures/stack.yml.erb +2 -0
- data/spec/kumo_docker_cloud_spec.rb +7 -0
- data/spec/kumo_dockercloud/docker_cloud_api_spec.rb +174 -0
- data/spec/kumo_dockercloud/environment_config_spec.rb +109 -0
- data/spec/kumo_dockercloud/environment_spec.rb +95 -0
- data/spec/kumo_dockercloud/stack_file_spec.rb +168 -0
- data/spec/kumo_dockercloud/stack_spec.rb +29 -0
- data/spec/kumo_dockercloud/state_validator_spec.rb +29 -0
- data/spec/spec_helper.rb +2 -0
- metadata +190 -0
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'kumo_ki'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
module KumoDockerCloud
|
5
|
+
class EnvironmentConfig
|
6
|
+
LOGGER = Logger.new(STDOUT)
|
7
|
+
|
8
|
+
attr_reader :env_name, :app_name
|
9
|
+
|
10
|
+
def initialize(options, logger = LOGGER)
|
11
|
+
@env_name = options.fetch(:env_name)
|
12
|
+
@config_path = options.fetch(:config_path)
|
13
|
+
@log = logger
|
14
|
+
@app_name = options.fetch(:app_name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def get_binding
|
18
|
+
binding
|
19
|
+
end
|
20
|
+
|
21
|
+
def stack_name
|
22
|
+
"#{app_name}-#{env_name}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def deploy_tag
|
26
|
+
production? ? 'production' : 'non-production'
|
27
|
+
end
|
28
|
+
|
29
|
+
def production?
|
30
|
+
env_name == 'production'
|
31
|
+
end
|
32
|
+
|
33
|
+
def development?
|
34
|
+
!(%w(production staging).include?(env_name))
|
35
|
+
end
|
36
|
+
|
37
|
+
def ruby_env
|
38
|
+
return 'development' if development?
|
39
|
+
env_name
|
40
|
+
end
|
41
|
+
|
42
|
+
def image_name
|
43
|
+
if existing_image_name?
|
44
|
+
existing_image_name
|
45
|
+
else
|
46
|
+
"redbubble/#{app_name}:master"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def image_tag
|
51
|
+
image_name.split(':').last
|
52
|
+
end
|
53
|
+
|
54
|
+
def plain_text_secrets
|
55
|
+
@plain_text_secrets ||= Hash[
|
56
|
+
encrypted_secrets.map do |name, cipher_text|
|
57
|
+
@log.info "Decrypting '#{name}'"
|
58
|
+
if cipher_text.start_with? '[ENC,'
|
59
|
+
begin
|
60
|
+
[name, "#{kms.decrypt cipher_text[5, cipher_text.size]}"]
|
61
|
+
rescue
|
62
|
+
@log.error "Error decrypting secret '#{name}' from '#{encrypted_secrets_filename}'"
|
63
|
+
raise
|
64
|
+
end
|
65
|
+
else
|
66
|
+
[name, cipher_text]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
]
|
70
|
+
end
|
71
|
+
|
72
|
+
def tags
|
73
|
+
[deploy_tag]
|
74
|
+
end
|
75
|
+
|
76
|
+
def error_queue_url
|
77
|
+
@error_queue_url ||= AssetWala::SqsQueue.get_error_queue_url
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def existing_image_name?
|
83
|
+
!!existing_image_name
|
84
|
+
end
|
85
|
+
|
86
|
+
def existing_image_name
|
87
|
+
@service ||= docker_cloud_api.services_by_stack_name(stack_name).first
|
88
|
+
@service ? @service.image_name : nil
|
89
|
+
end
|
90
|
+
|
91
|
+
def docker_cloud_api
|
92
|
+
@docker_cloud_api ||= KumoDockerCloud::DockerCloudApi.new
|
93
|
+
end
|
94
|
+
|
95
|
+
def kms
|
96
|
+
@kms ||= KumoKi::KMS.new
|
97
|
+
end
|
98
|
+
|
99
|
+
def encrypted_secrets_path
|
100
|
+
config_path = File.expand_path(File.join(@config_path), __FILE__)
|
101
|
+
secrets_filepath = File.join(config_path, "#{env_name}_secrets.yml")
|
102
|
+
secrets_filepath = File.join(config_path, 'development_secrets.yml') unless File.exist?(secrets_filepath)
|
103
|
+
secrets_filepath
|
104
|
+
end
|
105
|
+
|
106
|
+
def encrypted_secrets_filename
|
107
|
+
File.basename encrypted_secrets_path
|
108
|
+
end
|
109
|
+
|
110
|
+
def encrypted_secrets
|
111
|
+
file = File.read(encrypted_secrets_path)
|
112
|
+
erb_result = ERB.new(file).result(get_binding)
|
113
|
+
@encrypted_secrets ||= YAML.load(erb_result)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module KumoDockerCloud
|
2
|
+
class Stack
|
3
|
+
attr_reader :stack_name, :app_name, :options
|
4
|
+
|
5
|
+
def initialize(app_name, env_name, options = { contactable: true })
|
6
|
+
@app_name = app_name
|
7
|
+
@stack_name = "#{app_name}-#{env_name}"
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def deploy(version)
|
12
|
+
update_image(version)
|
13
|
+
redeploy
|
14
|
+
validate_deployment(version)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def update_image(version)
|
20
|
+
docker_cloud_api.services.update(service_uuid, image: "redbubble/#{app_name}:#{version}")
|
21
|
+
end
|
22
|
+
|
23
|
+
def redeploy
|
24
|
+
docker_cloud_api.services.redeploy(service_uuid)
|
25
|
+
end
|
26
|
+
|
27
|
+
def validate_deployment(version)
|
28
|
+
deployment = Deployment.new(stack_name, version)
|
29
|
+
deployment.app_name = app_name
|
30
|
+
deployment.contactable = options[:contactable]
|
31
|
+
deployment.validate
|
32
|
+
end
|
33
|
+
|
34
|
+
def service_uuid
|
35
|
+
@service_uuid ||= begin
|
36
|
+
services = docker_cloud_api.services_by_stack_name(stack_name)
|
37
|
+
services.first.uuid
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def docker_cloud_api
|
42
|
+
@docker_cloud_api ||= KumoDockerCloud::DockerCloudApi.new
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module KumoDockerCloud
|
4
|
+
module StackFile
|
5
|
+
def self.create_from_template(stack_template, config, env_vars)
|
6
|
+
parsed = YAML.load(ERB.new(stack_template).result(config.get_binding))
|
7
|
+
|
8
|
+
parsed[config.app_name]['environment'] ||= {}
|
9
|
+
parsed[config.app_name]['environment'].merge!(config.plain_text_secrets)
|
10
|
+
parsed[config.app_name]['environment'].merge!(env_vars.fetch(config.app_name, {}))
|
11
|
+
|
12
|
+
converted_env_vars = make_all_root_level_keys_strings(env_vars)
|
13
|
+
|
14
|
+
env_vars.each do |key, _|
|
15
|
+
key_string = key.to_s
|
16
|
+
parsed[key_string]['environment'].merge!(converted_env_vars.fetch(key_string))
|
17
|
+
end
|
18
|
+
|
19
|
+
parsed
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.make_all_root_level_keys_strings(env_vars)
|
23
|
+
env_vars.keys.reduce({}) { |acc, key| acc[key.to_s] = env_vars[key]; acc }
|
24
|
+
end
|
25
|
+
|
26
|
+
private_class_method :make_all_root_level_keys_strings
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module KumoDockerCloud
|
4
|
+
class StateValidator
|
5
|
+
attr_reader :state_provider
|
6
|
+
|
7
|
+
def initialize(state_provider)
|
8
|
+
@state_provider = state_provider
|
9
|
+
@stateful = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def wait_for_state(expected_state, time_limit)
|
13
|
+
start_time = Time.now
|
14
|
+
last_state = nil
|
15
|
+
|
16
|
+
while Time.now.to_i - start_time.to_i < time_limit
|
17
|
+
@stateful = state_provider.call
|
18
|
+
|
19
|
+
if last_state != current_state
|
20
|
+
print "\n#{@stateful[:name]} is currently #{current_state}"
|
21
|
+
else
|
22
|
+
print "."
|
23
|
+
end
|
24
|
+
last_state = current_state
|
25
|
+
|
26
|
+
if current_state == expected_state
|
27
|
+
break
|
28
|
+
end
|
29
|
+
|
30
|
+
sleep(1)
|
31
|
+
end
|
32
|
+
|
33
|
+
print "\n"
|
34
|
+
if current_state != expected_state
|
35
|
+
puts "Timed out after #{time_limit} seconds"
|
36
|
+
raise TimeoutError.new
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def current_state
|
43
|
+
return 'an unknown state' if @stateful.nil?
|
44
|
+
@stateful.fetch(:state, 'an unknown state')
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
data/script/build.sh
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -e
|
4
|
+
|
5
|
+
if [[ -z "$KUMO_DOCKERCLOUD_VERSION" && -n "$BUILDKITE_BUILD_NUMBER" ]]; then
|
6
|
+
export KUMO_DOCKERCLOUD_VERSION="$BUILDKITE_BUILD_NUMBER"
|
7
|
+
fi
|
8
|
+
|
9
|
+
echo "--- :wind_chime: Building gem :wind_chime:"
|
10
|
+
|
11
|
+
gem build kumo_dockercloud.gemspec
|
data/script/release-gem
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../lib/kumo_docker_cloud'
|
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 = KumoDockerCloud::VERSION
|
12
|
+
|
13
|
+
run_command "git tag #{tag}"
|
14
|
+
run_command "git push origin #{tag}"
|
data/script/unit_test.sh
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -e
|
4
|
+
|
5
|
+
echo "--- :clock1: :clock2: running specs :clock3: :clock4:"
|
6
|
+
bundle install
|
7
|
+
bundle exec rspec
|
8
|
+
|
9
|
+
function inline_image {
|
10
|
+
printf '\033]1338;url='"$1"';alt='"$2"'\a\n'
|
11
|
+
}
|
12
|
+
|
13
|
+
echo "+++ Done! :thumbsup: :shipit:"
|
14
|
+
inline_image "https://giftoppr.desktopprassets.com/uploads/f828c372186b5fa80a1c553adbcd4bc4d331396b/tumblr_m2cg550aq21ql201ao1_500.gif" "Yuss"
|
File without changes
|
@@ -0,0 +1,174 @@
|
|
1
|
+
require 'kumo_dockercloud/docker_cloud_api'
|
2
|
+
require 'webmock/rspec'
|
3
|
+
|
4
|
+
describe KumoDockerCloud::DockerCloudApi do
|
5
|
+
let(:username) { 'pebbles' }
|
6
|
+
let(:api_key) { 'bam bam' }
|
7
|
+
let(:api) { KumoDockerCloud::DockerCloudApi.new(username: username, api_key: api_key) }
|
8
|
+
let(:stack) { double(DockerCloud::Stack, name: stack_name, services: services) }
|
9
|
+
let(:stack_name) { "foo" }
|
10
|
+
let(:services) { [] }
|
11
|
+
|
12
|
+
describe '#initialize' do
|
13
|
+
subject { api }
|
14
|
+
|
15
|
+
it "passes through username and api_key from hash" do
|
16
|
+
expect(::DockerCloud::Client).to receive(:new).with(username, api_key)
|
17
|
+
subject
|
18
|
+
end
|
19
|
+
|
20
|
+
context "without params" do
|
21
|
+
subject { KumoDockerCloud::DockerCloudApi.new }
|
22
|
+
|
23
|
+
after do
|
24
|
+
ENV.delete('DOCKERCLOUD_USER')
|
25
|
+
ENV.delete('DOCKERCLOUD_APIKEY')
|
26
|
+
end
|
27
|
+
|
28
|
+
it "uses user name from env variable DOCKERCLOUD_USER" do
|
29
|
+
ENV['DOCKERCLOUD_USER'] = username
|
30
|
+
expect(::DockerCloud::Client).to receive(:new).with(username, anything)
|
31
|
+
subject
|
32
|
+
end
|
33
|
+
|
34
|
+
it "uses api key from env variable DOCKERCLOUD_APIKEY" do
|
35
|
+
ENV['DOCKERCLOUD_APIKEY'] = api_key
|
36
|
+
expect(::DockerCloud::Client).to receive(:new).with(anything, api_key)
|
37
|
+
subject
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
describe '#stack_by_name' do
|
45
|
+
subject { api.stack_by_name(stack_name_in) }
|
46
|
+
let(:stacks_mock) { double(DockerCloud::StackAPI, all: [stack] ) }
|
47
|
+
|
48
|
+
before do
|
49
|
+
allow_any_instance_of(::DockerCloud::Client).to receive(:stacks).and_return(stacks_mock)
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'when you have 1 stack, with matching name' do
|
53
|
+
let(:stack_name_in) { stack_name }
|
54
|
+
|
55
|
+
it { should == stack }
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'when you have 1 stack, with non-matching name' do
|
59
|
+
let(:stack_name_in) { 'bar' }
|
60
|
+
|
61
|
+
it { should be_nil }
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'when your stacks have partially matching names' do
|
65
|
+
let(:stack_name_in) { 'fo' }
|
66
|
+
|
67
|
+
it { should be_nil }
|
68
|
+
end
|
69
|
+
|
70
|
+
context 'when you have no stacks' do
|
71
|
+
let(:stack_name_in) { stack_name }
|
72
|
+
let(:stacks_mock) { double(DockerCloud::StackAPI, all: [] ) }
|
73
|
+
|
74
|
+
it { should be_nil }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe '#services_by_stack_name' do
|
79
|
+
subject { api.services_by_stack_name(stack_name) }
|
80
|
+
|
81
|
+
context 'when the stack exists' do
|
82
|
+
before do
|
83
|
+
allow(api).to receive(:stack_by_name).and_return(stack)
|
84
|
+
end
|
85
|
+
|
86
|
+
context 'without any services' do
|
87
|
+
let(:services) { [] }
|
88
|
+
it do
|
89
|
+
expect(api).to receive(:stack_by_name).with(stack_name)
|
90
|
+
subject
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'returns a blank list' do
|
94
|
+
expect(subject).to eq []
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context 'with services' do
|
99
|
+
let(:service) { double(DockerCloud::Service) }
|
100
|
+
let(:services) { [ service ] }
|
101
|
+
let(:stack_name) { 'bar' }
|
102
|
+
|
103
|
+
it 'returns list of service data' do
|
104
|
+
expect(subject).to eq services
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'appropriately passes through the correct stack name' do
|
108
|
+
expect(api).to receive(:stack_by_name).with(stack_name)
|
109
|
+
subject
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
context "when the stack doesn't exist" do
|
115
|
+
before do
|
116
|
+
allow(api).to receive(:stack_by_name).and_return(nil)
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'returns a blank list' do
|
120
|
+
expect(subject).to eq []
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe '#containers_by_stack_name' do
|
126
|
+
subject { api.containers_by_stack_name(stack_name) }
|
127
|
+
|
128
|
+
before do
|
129
|
+
allow(api).to receive(:services_by_stack_name).and_return(services)
|
130
|
+
end
|
131
|
+
|
132
|
+
context 'with only 1 service' do
|
133
|
+
let(:service) { double(DockerCloud::Service, containers: containers) }
|
134
|
+
let(:services) { [service] }
|
135
|
+
|
136
|
+
context 'without any containers' do
|
137
|
+
let(:containers) { [] }
|
138
|
+
|
139
|
+
it { should == [] }
|
140
|
+
end
|
141
|
+
|
142
|
+
context 'with multiple containers' do
|
143
|
+
let(:container) { double(DockerCloud::Container) }
|
144
|
+
let(:containers) { [container, container] }
|
145
|
+
|
146
|
+
it { should == containers }
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
context 'with multiple services' do
|
151
|
+
let(:service1) { double(DockerCloud::Service, containers: containers1) }
|
152
|
+
let(:service2) { double(DockerCloud::Service, containers: containers2) }
|
153
|
+
let(:services) { [service1, service2] }
|
154
|
+
|
155
|
+
let(:container) { double(DockerCloud::Container) }
|
156
|
+
let(:containers1) { [container] }
|
157
|
+
let(:containers2) { [container, container] }
|
158
|
+
|
159
|
+
it { should == [container, container, container] }
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
context 'forwarded methods' do
|
164
|
+
describe '#services' do
|
165
|
+
let(:client) { instance_double(DockerCloud::Client) }
|
166
|
+
let(:api) { KumoDockerCloud::DockerCloudApi.new(username: username, api_key: api_key, client: client) }
|
167
|
+
|
168
|
+
it 'forwards to the docker cloud client' do
|
169
|
+
expect(client).to receive(:services)
|
170
|
+
api.services
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|