kumo_dockercloud 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ module KumoDockerCloud
2
+ VERSION = '1.0.0'
3
+ 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
@@ -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}"
@@ -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,2 @@
1
+ testkey: someval
2
+ enctest: '[ENC,ZW5jcnlwdGVkIGJpbmFyeSBkYXRh'
@@ -0,0 +1,2 @@
1
+ testkey: someval
2
+ moretest: otherval
@@ -0,0 +1,2 @@
1
+ application-stack-name:
2
+ image: a-thing
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe KumoDockerCloud do
4
+ it 'has a version number' do
5
+ expect(KumoDockerCloud::VERSION).not_to be nil
6
+ end
7
+ end
@@ -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