kumo_dockercloud 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|