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.
@@ -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