kumo_dockercloud 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 30cba0c29df6e2bd9db7bae50838fc0bf317c2fa
4
+ data.tar.gz: e5b9bf55b8ff0cf329e6537099db2ea54840d383
5
+ SHA512:
6
+ metadata.gz: 926d693084ea08c039a9fcd643fc388399d771e73b9d221d0f4ac6a82734382dbf1090c025b1b7809e4056c514fa9810c0e4d2ad240ceabf53de7ee29dc78a41
7
+ data.tar.gz: db132f216e112a0a15cf6cec3563bc340f63f2cd065b0177d1a2a19bf5d127556270c93f81981cbb4e96ecca1e82c4170611357e460b634197a371299460d40f
@@ -0,0 +1,9 @@
1
+ steps:
2
+ - name: ':gem: build'
3
+ command: script/build.sh
4
+ agents:
5
+ location: aws
6
+ - name: ':rspec: unit-test'
7
+ command: script/unit_test.sh
8
+ agents:
9
+ location: aws
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+
16
+ .idea
17
+ *.gem
18
+ .#*
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.2.3
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in kumo_dockercloud.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Redbubble
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # KumoDockerCloud [![Build status](https://badge.buildkite.com/e9ebd06f4732bbb2a914228ac8816a2bbbeaf8bf0444ea00b4.svg)](https://buildkite.com/redbubble/kumo-docker-cloud)
2
+
3
+ This is the Redbubble wrapper around creating environments in docker cloud. It is built into our rbdevltools container and can then be used during `apply-env` and `deploy` tasks.
4
+
5
+ ## Installation
6
+
7
+ This is installed into the rbdevtools container.
8
+
9
+ ## Usage
10
+
11
+ Apply env example
12
+ ```ruby
13
+ KumoDockerCloud::Environment.new(
14
+ name: environment_name,
15
+ env_vars: env_vars,
16
+ app_name: 'your-app-here',
17
+ config_path: File.join('/app', 'config'),
18
+ stack_template_path: File.join('/app', 'docker-cloud, 'stack.yml.erb')
19
+ ).apply
20
+ ```
21
+
22
+ Deploy example
23
+ ```ruby
24
+ begin
25
+ KumoDockerCloud::Stack.new(app_name, env_name).deploy(version)
26
+ rescue KumoDockerCloud::Deployment::DeploymentError, TimeoutError
27
+ exit 1
28
+ end
29
+ ```
30
+
31
+ ## Testing changes
32
+
33
+ Changes to the gem can be manually tested end to end in a project that uses the gem (i.e. http-wala).
34
+
35
+ - First start the dev-tools container: `baxter kumo tools debug non-production`
36
+ - Re-install the gem: `gem specific_install https://github.com/redbubble/kumo_dockercloud_gem.git -b <your_branch>`
37
+ - Fire up a console: `irb`
38
+ - Require the gem: `require "kumo_dockercloud"`
39
+ - Interact with the gem's classes. `KumoDockerCloud::Stack.new('http-wala', 'test').deploy('1518')`
40
+ - If your container doesn't have a version check endpoint, add the `contactable: false` option: `KumoDockerCloud::Stack.new('http-wala', 'test', contactable: false).deploy('1518')`
41
+
42
+
43
+ ## Contributing
44
+
45
+ 1. Fork it ( https://github.com/[my-github-username]/kumo_dockercloud_gem/fork )
46
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
47
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
48
+ 4. Push to the branch (`git push origin my-new-feature`)
49
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'kumo_dockercloud/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'kumo_dockercloud'
8
+ spec.version = KumoDockerCloud::VERSION
9
+ spec.authors = %w(Redbubble Delivery Engineering)
10
+ spec.email = %w(delivery-engineering@redbubble.com)
11
+ spec.summary = %q{Use to create Redbubble environments on the DockerCloud platform}
12
+ spec.description = %q{Use to create Redbubble environments on the DockerCloud platform}
13
+ spec.homepage = ''
14
+ spec.license = 'mit'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_runtime_dependency 'httpi', '~> 2.4'
22
+ spec.add_runtime_dependency 'docker_cloud', '~> 0.1'
23
+ spec.add_runtime_dependency 'kumo_ki', '~>1.0'
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.6'
26
+ spec.add_development_dependency 'rake', '~> 10.0'
27
+ spec.add_development_dependency 'rspec', '~> 3.4'
28
+ spec.add_development_dependency 'webmock', '~> 1.22'
29
+ end
@@ -0,0 +1,3 @@
1
+ require 'kumo_dockercloud/environment'
2
+ require 'kumo_dockercloud/deployment'
3
+ require 'kumo_dockercloud/stack'
@@ -0,0 +1,126 @@
1
+ require 'time'
2
+ require 'httpi'
3
+
4
+ require_relative 'docker_cloud_api'
5
+ require_relative 'state_validator'
6
+
7
+ module KumoDockerCloud
8
+ class Deployment
9
+
10
+ class DeploymentError < StandardError; end
11
+
12
+ attr_accessor :app_name, :contactable
13
+ attr_reader :stack_name, :version, :health_check_path, :version_check_path
14
+
15
+ def initialize(stack_name, version, _ = nil)
16
+ @stack_name = stack_name
17
+ @version = version
18
+ @contactable = true
19
+
20
+ @health_check_path = 'site_status'
21
+ @version_check_path = "#{health_check_path}/version"
22
+ end
23
+
24
+ def validate
25
+ wait_for_running_state
26
+ validate_containers
27
+ end
28
+
29
+ def wait_for_running_state
30
+ service_state_provider = lambda {
31
+ service = docker_cloud_api.services.get(service_uuid)
32
+ { name: service.name, state: service.state }
33
+ }
34
+
35
+ StateValidator.new(service_state_provider).wait_for_state('Running', 240)
36
+ end
37
+
38
+ private
39
+
40
+ def service_uuid
41
+ @service_uuid ||= begin
42
+ services = docker_cloud_api.services_by_stack_name(stack_name)
43
+ services.first.uuid
44
+ end
45
+ end
46
+
47
+ def docker_cloud_api
48
+ @docker_cloud_api ||= KumoDockerCloud::DockerCloudApi.new
49
+ end
50
+
51
+ def current_state
52
+ return 'an unknown state' if @parsed_service_info.nil?
53
+ @parsed_service_info.fetch('state', 'an unknown state')
54
+ end
55
+
56
+ def validate_containers
57
+ puts "Getting containers"
58
+
59
+ containers = docker_cloud_api.containers_by_stack_name(stack_name)
60
+
61
+ HTTPI.log = false
62
+
63
+ containers.each do |container|
64
+ validate_container_data(container)
65
+ end
66
+ end
67
+
68
+ def validate_container_data(container)
69
+ unless container.name.start_with?(app_name)
70
+ puts "Skipping #{container.name}"
71
+ return
72
+ end
73
+ print "Checking '#{container.name}' (#{container.uuid}): "
74
+
75
+ raise "Unexpected number of open container ports" if container.container_ports.size != 1
76
+
77
+ if contactable
78
+ endpoint_uri = container.container_ports.first.endpoint_uri.gsub(/^tcp:/, 'http:')
79
+ validate_container_version(endpoint_uri)
80
+ validate_container_health(endpoint_uri)
81
+ end
82
+ print "\n"
83
+ end
84
+
85
+ def validate_container_version(endpoint_uri)
86
+ version_check_uri = "#{endpoint_uri}#{version_check_path}"
87
+ print "Version "
88
+ response = safe_http_get(version_check_uri)
89
+ actual_version = response.body.strip
90
+
91
+ if actual_version == version
92
+ print "OK, "
93
+ else
94
+ puts "Incorrect: Should be '#{version}'; reported '#{actual_version}'"
95
+ raise DeploymentError.new
96
+ end
97
+ end
98
+
99
+ def validate_container_health(endpoint_uri)
100
+ health_check_uri = "#{endpoint_uri}#{health_check_path}"
101
+ print "Health "
102
+ response = safe_http_get(health_check_uri)
103
+ if response.code == 200
104
+ print "OK"
105
+ else
106
+ puts "Unhealthy (HTTP #{response.code}): #{response.body}"
107
+ raise DeploymentError.new
108
+ end
109
+ end
110
+
111
+ def safe_http_get(url)
112
+ tries ||= 3
113
+ request = HTTPI::Request.new(url: url, open_timeout: 5, read_timeout: 5)
114
+ HTTPI.get(request)
115
+ rescue Timeout::Error, Errno::ECONNREFUSED, Errno::EINVAL, Errno::ECONNRESET, Errno::ETIMEDOUT, EOFError,
116
+ Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError => e
117
+ sleep 5
118
+
119
+ if (tries -= 1).zero?
120
+ raise e
121
+ else
122
+ retry
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,31 @@
1
+ require 'docker_cloud'
2
+ require 'base64' # left out of DockerCloud gem
3
+
4
+ module KumoDockerCloud
5
+ class DockerCloudApi
6
+ extend Forwardable
7
+ def_delegator :@client, :services
8
+
9
+ def initialize(options = {})
10
+ options[:username] ||= ENV['DOCKERCLOUD_USER']
11
+ options[:api_key] ||= ENV['DOCKERCLOUD_APIKEY']
12
+ @client = options[:client] || ::DockerCloud::Client.new(options[:username], options[:api_key])
13
+ end
14
+
15
+ def stack_by_name(name)
16
+ @client.stacks.all.find { |s| s.name == name }
17
+ end
18
+
19
+ def services_by_stack_name(stack_name)
20
+ stack = stack_by_name(stack_name)
21
+ return [] unless stack
22
+ stack.services
23
+ end
24
+
25
+ def containers_by_stack_name(stack_name)
26
+ services_by_stack_name(stack_name).collect do |service|
27
+ service.containers
28
+ end.flatten
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,110 @@
1
+ require 'yaml'
2
+ require 'erb'
3
+ require 'tempfile'
4
+ require 'forwardable'
5
+
6
+ require_relative 'docker_cloud_api'
7
+ require_relative 'state_validator'
8
+ require_relative 'environment_config'
9
+ require_relative 'stack_file'
10
+
11
+ module KumoDockerCloud
12
+ class Environment
13
+ extend ::Forwardable
14
+ def_delegators :@config, :stack_name, :env_name
15
+
16
+ def initialize(params = {})
17
+ @env_name = params.fetch(:name)
18
+ @env_vars = params.fetch(:env_vars, {})
19
+ @stack_template_path = params.fetch(:stack_template_path)
20
+ @timeout = params.fetch(:timeout, 120)
21
+
22
+ app_name = params.fetch(:app_name)
23
+ @config = EnvironmentConfig.new(app_name: app_name, env_name: @env_name, config_path: params.fetch(:config_path))
24
+ end
25
+
26
+ def apply
27
+ if @config.image_tag == 'latest'
28
+ puts 'WARNING: Deploying latest. The deployed container version may arbitrarily change'
29
+ end
30
+
31
+ stack_file = write_stack_config_file(configure_stack(stack_template))
32
+ run_command(stack_command(stack_file))
33
+ stack_file.unlink
34
+
35
+ run_command("docker-cloud stack redeploy #{stack_name}")
36
+
37
+ wait_for_running(@timeout)
38
+ end
39
+
40
+ def destroy
41
+ run_command("docker-cloud stack terminate --sync #{stack_name}")
42
+ end
43
+
44
+ private
45
+
46
+ def configure_stack(stack_template)
47
+ StackFile.create_from_template(stack_template, @config, @env_vars)
48
+ end
49
+
50
+ def wait_for_running(timeout)
51
+ StateValidator.new(stack_state_provider).wait_for_state('Redeploying', timeout)
52
+ StateValidator.new(stack_state_provider).wait_for_state(expected_state, timeout)
53
+ StateValidator.new(service_state_provider).wait_for_state('Running', timeout)
54
+ end
55
+
56
+ def expected_state
57
+ env_name == 'production' ? 'Partly running' : 'Running'
58
+ end
59
+
60
+ def stack_state_provider
61
+ docker_cloud_api = DockerCloudApi.new
62
+ lambda {
63
+ stack = docker_cloud_api.stack_by_name(stack_name)
64
+ { name: stack.name, state: stack.state }
65
+ }
66
+ end
67
+
68
+ def service_state_provider
69
+ docker_cloud_api = DockerCloudApi.new
70
+ lambda {
71
+ services = docker_cloud_api.services_by_stack_name(stack_name)
72
+ services.select! { |service| service.name != 'geckoboardwidget' }
73
+ { name: 'services', state: services.map { |s| s.state }.uniq.join }
74
+ }
75
+ end
76
+
77
+ def run_command(cmd)
78
+ puts "Executing -> #{cmd}"
79
+ puts `#{cmd}`
80
+ end
81
+
82
+ def evaluate_command(cmd)
83
+ `#{cmd}`
84
+ end
85
+
86
+ def stack_command(stack_file)
87
+ if exists?
88
+ "docker-cloud stack update -f #{stack_file.path} #{stack_name}"
89
+ else
90
+ "docker-cloud stack create -f #{stack_file.path} -n #{stack_name}"
91
+ end
92
+ end
93
+
94
+ def exists?
95
+ result = evaluate_command('docker-cloud stack ls')
96
+ result.include?(stack_name)
97
+ end
98
+
99
+ def write_stack_config_file(stack_file_data)
100
+ output_file = Tempfile.new('docker-cloud_stack_config')
101
+ output_file.write(stack_file_data.to_yaml)
102
+ output_file.close
103
+ output_file
104
+ end
105
+
106
+ def stack_template
107
+ File.read(@stack_template_path)
108
+ end
109
+ end
110
+ end