capistrano-docker-cloud 0.2.2
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/.gitignore +17 -0
- data/CHANGELOG.md +27 -0
- data/Dockerfile +37 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +88 -0
- data/Rakefile +1 -0
- data/capistrano-rails.gemspec +22 -0
- data/lib/capistrano/docker-cloud.rb +2 -0
- data/lib/capistrano/docker-cloud/base.rb +82 -0
- data/lib/capistrano/docker-cloud/client.rb +138 -0
- data/lib/capistrano/docker-cloud/helpers/collection_helper.rb +15 -0
- data/lib/capistrano/docker-cloud/service.rb +145 -0
- data/lib/capistrano/docker-cloud/stack.rb +141 -0
- data/lib/capistrano/docker-cloud/version.rb +5 -0
- data/lib/capistrano/tasks/docker-cloud.rake +11 -0
- data/make_new_release.sh +23 -0
- metadata +102 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 80e86ce3e62045cd06a1c371fc22e6d4f7b20333
|
|
4
|
+
data.tar.gz: 42d178fda17226ffe9d369db7879e5398a23aee7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6f003869c4196581f4809d9a12b51f355bd133466793e6adcb6ab8b39043fbbeecf4ce672e8a2a3be5010152f85c4af18f9a2fc0f6c38f3206331e076a15bdbc
|
|
7
|
+
data.tar.gz: a7b64a9d31cb3591cd179721b9b5078166b2ca0279c6afe9378c0d596ecc3cff6c9a0b2abb825509e3f16ce4d06e5a852311b39fcd30155f7a9d3d788c5bbf25
|
data/.gitignore
ADDED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# master
|
|
2
|
+
|
|
3
|
+
* Your contribution here!
|
|
4
|
+
|
|
5
|
+
# 0.2.2 (Jul 1 2016)
|
|
6
|
+
|
|
7
|
+
- Ignore endpoint URI update when not configured
|
|
8
|
+
|
|
9
|
+
# 0.2.1 (Jul 1 2016)
|
|
10
|
+
|
|
11
|
+
- Remove the load balancer redeploy
|
|
12
|
+
|
|
13
|
+
The HAProxy image is reload automatically when updating the links.
|
|
14
|
+
|
|
15
|
+
# 0.2.0 (Jun 25 2016)
|
|
16
|
+
|
|
17
|
+
- Service creation for your image
|
|
18
|
+
- Service start
|
|
19
|
+
- Load balancer update and redeploy using the new service
|
|
20
|
+
|
|
21
|
+
This release is not yet ready as there is a downtime in case the application is
|
|
22
|
+
taking some times to boot (as of now, the gem is only waiting on the Docker
|
|
23
|
+
Cloud status, which is not the application status).
|
|
24
|
+
|
|
25
|
+
# 0.1.0 (Jun 23 2016)
|
|
26
|
+
|
|
27
|
+
Initial release
|
data/Dockerfile
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# How to use it
|
|
2
|
+
# =============
|
|
3
|
+
#
|
|
4
|
+
# Visit http://blog.zedroot.org/using-docker-to-maintain-a-ruby-gem/
|
|
5
|
+
|
|
6
|
+
# ~~~~ Image base ~~~~
|
|
7
|
+
# Base image with the latest Ruby only
|
|
8
|
+
FROM ruby:2.3.0-slim
|
|
9
|
+
MAINTAINER Guillaume Hain zedtux@zedroot.org
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ~~~~ Set up the environment ~~~~
|
|
13
|
+
ENV DEBIAN_FRONTEND noninteractive
|
|
14
|
+
|
|
15
|
+
RUN mkdir -p /gem/
|
|
16
|
+
WORKDIR /gem/
|
|
17
|
+
ADD . /gem/
|
|
18
|
+
|
|
19
|
+
# ~~~~ OS Maintenance & Rails Preparation ~~~~
|
|
20
|
+
# Rubygems and Bundler
|
|
21
|
+
RUN apt-get update && \
|
|
22
|
+
apt-get install -y git build-essential && \
|
|
23
|
+
touch ~/.gemrc && \
|
|
24
|
+
echo "gem: --no-ri --no-rdoc" >> ~/.gemrc && \
|
|
25
|
+
gem install rubygems-update && \
|
|
26
|
+
update_rubygems && \
|
|
27
|
+
gem install bundler && \
|
|
28
|
+
bundle install && \
|
|
29
|
+
apt-get remove --purge -y build-essential && \
|
|
30
|
+
apt-get autoclean -y && \
|
|
31
|
+
apt-get clean
|
|
32
|
+
|
|
33
|
+
# Import the gem source code
|
|
34
|
+
VOLUME .:/gem/
|
|
35
|
+
|
|
36
|
+
ENTRYPOINT ["bundle", "exec"]
|
|
37
|
+
CMD ["rake", "-T"]
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2016 Guillaume Hain
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Capistrano::DockerCloud
|
|
2
|
+
|
|
3
|
+
[Docker cloud](https://cloud.docker.com/) specific tasks for Capistrano v3:
|
|
4
|
+
|
|
5
|
+
- `cap docker_cloud:deploy`
|
|
6
|
+
|
|
7
|
+
## TODO
|
|
8
|
+
|
|
9
|
+
- [ ] Add a `docker_cloud:rollback` task [#1](https://github.com/YourCursus/capistrano-docker-cloud/issues/1)
|
|
10
|
+
- [ ] Add a task for A/B testing [#2](https://github.com/YourCursus/capistrano-docker-cloud/issues/2)
|
|
11
|
+
- [ ] Add a task to delete old Docker image tags [#3](https://github.com/YourCursus/capistrano-docker-cloud/issues/3)
|
|
12
|
+
- [ ] Add more options to be configured on the Capistrano `deploy.rb` file
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Add this line to your application's Gemfile:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
group :development do
|
|
20
|
+
gem 'capistrano', '~> 3.1'
|
|
21
|
+
gem 'capistrano-docker-cloud'
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Run the following command to install the gems:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
bundle install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then run the generator to create a basic set of configuration files:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
bundle exec cap install
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
# Capfile
|
|
41
|
+
require 'capistrano/docker-cloud'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Please note that any `require`s should be placed in `Capfile`, not in `config/deploy.rb`.
|
|
45
|
+
|
|
46
|
+
You can tweak some Docker Cloud's specific options in `config/deploy.rb`:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
# Docker image name to deploy
|
|
50
|
+
set :docker_image, 'me/my-image'
|
|
51
|
+
|
|
52
|
+
# Docker Cloud credentials
|
|
53
|
+
set :docker_cloud_credentials, {
|
|
54
|
+
username: 'username',
|
|
55
|
+
api_key: 'my-api-key'
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Debugging
|
|
60
|
+
|
|
61
|
+
#### RestClient
|
|
62
|
+
|
|
63
|
+
Under the hood, this Capistrano gem is using
|
|
64
|
+
[Docker cloud](https://cloud.docker.com/) which is using the [RestClient](https://github.com/rest-client/rest-client) gem.
|
|
65
|
+
|
|
66
|
+
You can see all the RestClient requests setting the `RESTCLIENT_LOG` environment
|
|
67
|
+
variable like the following:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
$ cap staging docker_cloud:deploy RESTCLIENT_LOG=stdout
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
#### Capistrano tasks
|
|
74
|
+
|
|
75
|
+
Of course you can also pass `--trace` in order to reveal the backtrace in case
|
|
76
|
+
of an error:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
$ cap staging docker_cloud:deploy --trace
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Contributing
|
|
83
|
+
|
|
84
|
+
1. Fork it
|
|
85
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
86
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
87
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
88
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require 'bundler/gem_tasks'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'capistrano/docker-cloud/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |gem|
|
|
7
|
+
gem.name = 'capistrano-docker-cloud'
|
|
8
|
+
gem.version = Capistrano::DockerCloud::VERSION
|
|
9
|
+
gem.authors = ['Guillaume Hain']
|
|
10
|
+
gem.email = ['zedtux@zedroot.org']
|
|
11
|
+
gem.description = %q{Docker cloud specific Capistrano tasks}
|
|
12
|
+
gem.summary = %q{Docker cloud specific Capistrano tasks}
|
|
13
|
+
gem.homepage = 'https://github.com/YourCursus/capistrano-docker-cloud'
|
|
14
|
+
|
|
15
|
+
gem.files = `git ls-files`.split($/)
|
|
16
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
17
|
+
gem.require_paths = ["lib"]
|
|
18
|
+
|
|
19
|
+
gem.add_dependency 'capistrano', '~> 3.1'
|
|
20
|
+
gem.add_dependency 'capistrano-bundler', '~> 1.1'
|
|
21
|
+
gem.add_dependency 'docker_cloud', '~> 0.3.0'
|
|
22
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require 'docker_cloud'
|
|
2
|
+
require 'capistrano/docker-cloud/helpers/collection_helper'
|
|
3
|
+
|
|
4
|
+
module Capistrano
|
|
5
|
+
module DockerCloud
|
|
6
|
+
class RecordNotFound < Exception; end
|
|
7
|
+
|
|
8
|
+
class Base
|
|
9
|
+
include Capistrano::DockerCloud::Helpers::CollectionHelper
|
|
10
|
+
|
|
11
|
+
# ~~~~ Class Methods ~~~~
|
|
12
|
+
def self.subclass_name
|
|
13
|
+
# FIXME : Find a better way to archive this.
|
|
14
|
+
ancestors.first.to_s.split('::').last.downcase + 's'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.find(uuid)
|
|
18
|
+
object = resource.get(uuid)
|
|
19
|
+
return object if object
|
|
20
|
+
raise Capistrano::DockerCloud::RecordNotFound
|
|
21
|
+
rescue RestClient::ResourceNotFound => error
|
|
22
|
+
puts "ERROR: #{error.response}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.find_by(options = {})
|
|
26
|
+
case
|
|
27
|
+
when options.key?(:name)
|
|
28
|
+
new(find_by_name(options[:name]))
|
|
29
|
+
else
|
|
30
|
+
raise "You can only search #{subclass_name} by `:name`."
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.find_by_name(name)
|
|
35
|
+
resource.all.detect do |stack|
|
|
36
|
+
stack.name.downcase == name
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.resource
|
|
41
|
+
connection.send("#{subclass_name}")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.configure(username, api_key)
|
|
45
|
+
@@dockercloud_username = username
|
|
46
|
+
@@dockercloud_api_key = api_key
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.connection
|
|
50
|
+
@@connection ||= ::DockerCloud::Client.new(@@dockercloud_username,
|
|
51
|
+
@@dockercloud_api_key)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
#
|
|
55
|
+
# Rails like belongs_to association
|
|
56
|
+
#
|
|
57
|
+
def self.belongs_to(name)
|
|
58
|
+
define_method("#{name.to_s}=") do |object|
|
|
59
|
+
instance_variable_set("@#{name.to_s}", object)
|
|
60
|
+
end
|
|
61
|
+
define_method("#{name.to_s}") { instance_variable_get("@#{name.to_s}") }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.redeploy(uuid)
|
|
65
|
+
resource.redeploy(uuid)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# ~~~~ Instance Methods ~~~~
|
|
69
|
+
def connection
|
|
70
|
+
self.class.connection
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def reload!
|
|
74
|
+
initialize(self.class.find(uuid))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def redeploy
|
|
78
|
+
self.class.redeploy(uuid)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
require 'capistrano/docker-cloud/base'
|
|
2
|
+
require 'capistrano/docker-cloud/service'
|
|
3
|
+
require 'capistrano/docker-cloud/stack'
|
|
4
|
+
require 'capistrano/docker-cloud/helpers/collection_helper'
|
|
5
|
+
|
|
6
|
+
module Capistrano
|
|
7
|
+
module DockerCloud
|
|
8
|
+
class Client
|
|
9
|
+
include Capistrano::DockerCloud::Helpers::CollectionHelper
|
|
10
|
+
|
|
11
|
+
def initialize(capistrano_instance)
|
|
12
|
+
@capistrano = capistrano_instance
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def deploy
|
|
16
|
+
# Ensure all is fine before to deploy
|
|
17
|
+
ensure_credential_exists!
|
|
18
|
+
ensure_docker_image_tag_to_deploy_is_defined!
|
|
19
|
+
prepare_dockercloud_connection
|
|
20
|
+
ensure_stack_exists_with_name!(stage_name)
|
|
21
|
+
ensure_no_container_with_image_to_deploy_already_exists!
|
|
22
|
+
ensure_stack_has_a_load_balancer!
|
|
23
|
+
|
|
24
|
+
# Deploy the image
|
|
25
|
+
create_new_service!
|
|
26
|
+
start_created_service!
|
|
27
|
+
wait_service_to_boot!
|
|
28
|
+
switch_load_balancer_linked_service!
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def fetch(key)
|
|
34
|
+
@capistrano.fetch(key)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def stage_name
|
|
38
|
+
fetch(:stage).to_s.downcase
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ensure_credential_exists!
|
|
42
|
+
@credentials = fetch(:docker_cloud_credentials)
|
|
43
|
+
return if @credentials
|
|
44
|
+
raise 'You have not defined the :docker_cloud_credentials option in ' \
|
|
45
|
+
'capistrano deploy.rb file.'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def ensure_docker_image_tag_to_deploy_is_defined!
|
|
49
|
+
return if fetch(:docker_image_tag)
|
|
50
|
+
raise 'No tag defined to be used to deploy the Docker image ' \
|
|
51
|
+
"#{fetch(:docker_image)}."
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def prepare_dockercloud_connection
|
|
55
|
+
Capistrano::DockerCloud::Base.configure(@credentials[:username],
|
|
56
|
+
@credentials[:api_key])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def ensure_stack_exists_with_name!(name)
|
|
60
|
+
@capistrano.info "Looking for the Stack named #{name} ..."
|
|
61
|
+
@stack = Capistrano::DockerCloud::Stack.find_by(name: name)
|
|
62
|
+
return if @stack
|
|
63
|
+
raise "Unable to find the Stack with name #{name}."
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def image_name_to_deploy
|
|
67
|
+
@image_name_to_deploy ||= "#{fetch(:docker_image)}:" \
|
|
68
|
+
"#{fetch(:docker_image_tag)}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def ensure_no_container_with_image_to_deploy_already_exists!
|
|
72
|
+
@capistrano.info "Ensuring no service is running the image " \
|
|
73
|
+
"#{image_name_to_deploy} ..."
|
|
74
|
+
service = @stack.find_service_by_image_name(image_name_to_deploy).first
|
|
75
|
+
if service.nil? || %w(Terminating Terminated).include?(service.state)
|
|
76
|
+
return
|
|
77
|
+
end
|
|
78
|
+
raise "The service #{service.name} is already running the image " \
|
|
79
|
+
"#{image_name_to_deploy}."
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def ensure_stack_has_a_load_balancer!
|
|
83
|
+
@capistrano.info "Checking the presence of a Load Balancer ..."
|
|
84
|
+
if @stack.has_a_load_balancer?
|
|
85
|
+
@load_balancer = @stack.load_balancer_service
|
|
86
|
+
return
|
|
87
|
+
end
|
|
88
|
+
raise "The stack #{stage_name} is missing a Load Balancer. Please " \
|
|
89
|
+
'add one using the dockercloud:haproxy Docker image.'
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
#
|
|
93
|
+
# Extract image name from the capistrano `docker_image` option.
|
|
94
|
+
#
|
|
95
|
+
# Example:
|
|
96
|
+
#
|
|
97
|
+
# - yourcursus/cursus => cursus
|
|
98
|
+
# - ruby => ruby
|
|
99
|
+
#
|
|
100
|
+
def docker_image_name
|
|
101
|
+
fetch(:docker_image).match(/^(?:.*\/)?(.*)$/)[1]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def docker_image_tag
|
|
105
|
+
fetch(:docker_image_tag).gsub(/[\.\_]/, '-')
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def service_name
|
|
109
|
+
@service_name ||= "#{docker_image_name}-#{docker_image_tag}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def create_new_service!
|
|
113
|
+
@capistrano.info "Creating a new service #{service_name} with image " \
|
|
114
|
+
"#{image_name_to_deploy} ..."
|
|
115
|
+
@stack.build_service(image: image_name_to_deploy, name: service_name)
|
|
116
|
+
@stack.save
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def start_created_service!
|
|
120
|
+
@capistrano.info "Starting newly created service #{service_name} ..."
|
|
121
|
+
@stack.start_service(name: service_name)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def wait_service_to_boot!
|
|
125
|
+
@capistrano.info "Waiting #{service_name} container booting ..."
|
|
126
|
+
@stack.waiting_service_boot(name: service_name)
|
|
127
|
+
@capistrano.info "Waiting #{service_name} application booting ..."
|
|
128
|
+
@stack.waiting_service_application_boot(name: service_name)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def switch_load_balancer_linked_service!
|
|
132
|
+
@capistrano.info 'Switching the Load Balancer linked services to ' \
|
|
133
|
+
"#{service_name} ..."
|
|
134
|
+
@load_balancer.switch!(service_name: service_name)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Capistrano
|
|
2
|
+
module DockerCloud
|
|
3
|
+
module Helpers
|
|
4
|
+
module CollectionHelper
|
|
5
|
+
def first_or_raise_error!(objects, name)
|
|
6
|
+
unless objects.size == 1
|
|
7
|
+
raise "1 object expected with #{name}, #{objects.size} objects " \
|
|
8
|
+
'found'
|
|
9
|
+
end
|
|
10
|
+
objects.first
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
module Capistrano
|
|
2
|
+
module DockerCloud
|
|
3
|
+
class Service < Capistrano::DockerCloud::Base
|
|
4
|
+
|
|
5
|
+
# https://docs.docker.com/apidocs/docker-cloud/#update-an-existing-service
|
|
6
|
+
ATTRIBUTES = [
|
|
7
|
+
:autorestart, :autodestroy, :container_envvars, :container_ports,
|
|
8
|
+
:cpu_shares, :entrypoint, :image, :linked_to_service, :memory, :net,
|
|
9
|
+
:privileged, :roles, :run_command, :sequential_deployment, :tags,
|
|
10
|
+
:target_num_containers, :deployment_strategy, :autoredeploy, :pid,
|
|
11
|
+
:working_dir, :nickname,
|
|
12
|
+
# Special keys
|
|
13
|
+
:name, :image_name
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
attr_accessor *ATTRIBUTES, :uuid
|
|
17
|
+
|
|
18
|
+
belongs_to :stack
|
|
19
|
+
|
|
20
|
+
def initialize(service)
|
|
21
|
+
ATTRIBUTES.each do |attribute|
|
|
22
|
+
value = default_value_for(attribute)
|
|
23
|
+
value = service.info[attribute] if service
|
|
24
|
+
self.send("#{attribute.to_s}=", value)
|
|
25
|
+
end
|
|
26
|
+
if service
|
|
27
|
+
# The Service API update is expecting an `image` attribute instead of
|
|
28
|
+
# `image_name`
|
|
29
|
+
self.image = service.info[:image_name]
|
|
30
|
+
self.uuid = service.uuid
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def load_balancer?
|
|
35
|
+
image_name =~ /^dockercloud\/haproxy\:/
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def linked_to_service_for_update
|
|
39
|
+
links = linked_to_service.dup
|
|
40
|
+
links.each { |link| link.delete(:from_service) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def container_ports_for_update
|
|
44
|
+
ports = container_ports.dup
|
|
45
|
+
ports.each do |port|
|
|
46
|
+
port.delete(:endpoint_uri)
|
|
47
|
+
port.delete(:port_name)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def update(options)
|
|
52
|
+
options.each do |key, value|
|
|
53
|
+
self.send("#{key.to_s}=", value)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def save
|
|
58
|
+
connection.services.update(uuid, build_update_hash)
|
|
59
|
+
reload!
|
|
60
|
+
rescue RestClient::BadRequest => error
|
|
61
|
+
puts "ERROR: #{error.response}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def default_value_for(attribute)
|
|
65
|
+
case attribute
|
|
66
|
+
when :privileged, :autoredeploy, :sequential_deployment
|
|
67
|
+
false
|
|
68
|
+
when :autorestart, :autodestroy
|
|
69
|
+
'OFF'
|
|
70
|
+
when :net
|
|
71
|
+
'bridge'
|
|
72
|
+
when :deployment_strategy
|
|
73
|
+
'EMPTIEST_NODE'
|
|
74
|
+
when :pid
|
|
75
|
+
'none'
|
|
76
|
+
when :target_num_containers
|
|
77
|
+
1
|
|
78
|
+
when :tags
|
|
79
|
+
[]
|
|
80
|
+
when :nickname
|
|
81
|
+
''
|
|
82
|
+
when :image, :name, :image_name
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def build_update_hash
|
|
88
|
+
attributes = {}
|
|
89
|
+
ignored_attributes = [:nickname, :image_name]
|
|
90
|
+
calling = caller.first.match(/^.*\/([a-z\_\-]+)\.rb\:.*$/)[1]
|
|
91
|
+
ignored_attributes << :name unless calling == 'stack'
|
|
92
|
+
ATTRIBUTES.each do |attribute|
|
|
93
|
+
next if ignored_attributes.include?(attribute)
|
|
94
|
+
|
|
95
|
+
value = self.send("#{attribute.to_s}")
|
|
96
|
+
next if value.nil? || value == []
|
|
97
|
+
next if default_value_for(attribute) == value
|
|
98
|
+
|
|
99
|
+
case attribute
|
|
100
|
+
when :linked_to_service
|
|
101
|
+
attributes[attribute] = linked_to_service_for_update
|
|
102
|
+
when :container_ports
|
|
103
|
+
attributes[attribute] = container_ports_for_update
|
|
104
|
+
else
|
|
105
|
+
attributes[attribute] = value
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
attributes
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def switch!(options)
|
|
112
|
+
unless load_balancer?
|
|
113
|
+
raise 'This service is not a Load Balancer, therefor you cannot ' \
|
|
114
|
+
'call switch.'
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
update_links_with_service!(name: options[:service_name])
|
|
118
|
+
save
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def update_links_with_service!(options)
|
|
122
|
+
links = linked_to_service.dup
|
|
123
|
+
|
|
124
|
+
services = stack.find_service_by_name(options[:name])
|
|
125
|
+
switch_to = first_or_raise_error!(services, options[:name])
|
|
126
|
+
|
|
127
|
+
to_be_replaced = remove_service_with_same_image_than(switch_to)
|
|
128
|
+
|
|
129
|
+
links.delete_if { |link| link[:name] == to_be_replaced.name }
|
|
130
|
+
links << { name: options[:name], to_service: "/api/app/v1/yourcursus/service/#{switch_to.uuid}/" }
|
|
131
|
+
|
|
132
|
+
self.linked_to_service = links
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def remove_service_with_same_image_than(switch_to)
|
|
138
|
+
services = stack.find_service_by_image_name(switch_to.image_name,
|
|
139
|
+
tag: false)
|
|
140
|
+
services.delete_if { |service| service.name == switch_to.name }
|
|
141
|
+
first_or_raise_error!(services, switch_to.name)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
module Capistrano
|
|
2
|
+
module DockerCloud
|
|
3
|
+
class Stack < Capistrano::DockerCloud::Base
|
|
4
|
+
|
|
5
|
+
def initialize(stack)
|
|
6
|
+
@original_stack = stack
|
|
7
|
+
sync_services_from_stack
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# ~~~~ Class Methods ~~~~
|
|
11
|
+
|
|
12
|
+
# ~~~~ Instance Methods ~~~~
|
|
13
|
+
# TODO : Refactor this code in order to move it in the Service class
|
|
14
|
+
def find_service_by_name(name)
|
|
15
|
+
@original_stack.services.select do |service|
|
|
16
|
+
compare(service.name, name)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def find_service_by_image_name(name, options = {})
|
|
21
|
+
options[:tag] = true unless options.key?(:tag)
|
|
22
|
+
|
|
23
|
+
@original_stack.services.select do |service|
|
|
24
|
+
compare(service.image_name, name, tag: options[:tag])
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def build_service(options = {})
|
|
29
|
+
services = find_service_by_image_name(options[:image], tag: false)
|
|
30
|
+
service = first_or_raise_error!(services, options[:image])
|
|
31
|
+
service = Capistrano::DockerCloud::Service.new(service)
|
|
32
|
+
service.update(image: options[:image], name: options[:name])
|
|
33
|
+
@services << service
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def save
|
|
37
|
+
connection.stacks.update(@original_stack.uuid, build_update_body)
|
|
38
|
+
reload!
|
|
39
|
+
rescue RestClient::BadRequest => error
|
|
40
|
+
puts "ERROR: #{error.response}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def has_a_load_balancer?
|
|
44
|
+
# TODO : Refactor this code in order to move it in the Service class
|
|
45
|
+
@load_balancer_service = @original_stack.services.detect do |service|
|
|
46
|
+
service.image_name =~ /^dockercloud\/haproxy\:/
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def load_balancer_service
|
|
51
|
+
service = Capistrano::DockerCloud::Service.new(@load_balancer_service)
|
|
52
|
+
service.stack = self
|
|
53
|
+
service
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def start_service(options)
|
|
57
|
+
services = find_service_by_name(options[:name])
|
|
58
|
+
service = first_or_raise_error!(services, options[:name])
|
|
59
|
+
service.start
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def uuid
|
|
63
|
+
@original_stack.uuid
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def waiting_service_boot(options)
|
|
67
|
+
options[:timeout] ||= 60
|
|
68
|
+
services = find_service_by_name(options[:name])
|
|
69
|
+
service = first_or_raise_error!(services, options[:name])
|
|
70
|
+
|
|
71
|
+
Timeout.timeout(options[:timeout]) do
|
|
72
|
+
begin
|
|
73
|
+
sleep 0.5
|
|
74
|
+
service.reload
|
|
75
|
+
end until service.state == 'Running'
|
|
76
|
+
end
|
|
77
|
+
rescue Timeout::Error
|
|
78
|
+
raise "The service #{options[:name]} was not able to boot in less " \
|
|
79
|
+
"than #{options[:timeout]} seconds. (Service state was " \
|
|
80
|
+
"#{service.state.inspect})"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def waiting_service_application_boot(options)
|
|
84
|
+
options[:timeout] ||= 60
|
|
85
|
+
services = find_service_by_name(options[:name])
|
|
86
|
+
service = first_or_raise_error!(services, options[:name])
|
|
87
|
+
|
|
88
|
+
uri = service.containers.flat_map do |container|
|
|
89
|
+
container.info[:container_ports].map do |container_port|
|
|
90
|
+
next unless container_port[:endpoint_uri]
|
|
91
|
+
container_port[:endpoint_uri].gsub(/^tcp\:\/\//, 'http://')
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
result = 0
|
|
96
|
+
|
|
97
|
+
Timeout.timeout(options[:timeout]) do
|
|
98
|
+
while result != 200 do
|
|
99
|
+
sleep 0.5
|
|
100
|
+
result = uri.map do |url|
|
|
101
|
+
begin
|
|
102
|
+
response = RestClient.get(url)
|
|
103
|
+
response.code
|
|
104
|
+
rescue Errno::ECONNREFUSED
|
|
105
|
+
500
|
|
106
|
+
end
|
|
107
|
+
end.reduce(0, :+) / uri.size
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
rescue Timeout::Error
|
|
111
|
+
raise "The service #{options[:name]} application was not able to boot" \
|
|
112
|
+
" in less than #{options[:timeout]} seconds."
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def sync_services_from_stack(from_stack = nil)
|
|
118
|
+
@services = []
|
|
119
|
+
@services = (from_stack || @original_stack).services.map do |service|
|
|
120
|
+
Capistrano::DockerCloud::Service.new(service)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def compare(source, from, options = {})
|
|
125
|
+
compare_source = source.dup
|
|
126
|
+
compare_from = from.dup
|
|
127
|
+
|
|
128
|
+
if options[:tag] == false
|
|
129
|
+
compare_source = compare_source.gsub(/\:.*$/, '')
|
|
130
|
+
compare_from = compare_from.gsub(/\:.*$/, '')
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
compare_source == compare_from
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def build_update_body
|
|
137
|
+
{ services: @services.map(&:build_update_hash) }
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
require 'capistrano/docker-cloud/client'
|
|
2
|
+
|
|
3
|
+
namespace :docker_cloud do
|
|
4
|
+
desc 'Deploy with zero-downtime on Docker cloud'
|
|
5
|
+
task :deploy do
|
|
6
|
+
run_locally do
|
|
7
|
+
set :docker_image_tag, ask('What tag do you want to deploy?', 'latest')
|
|
8
|
+
Capistrano::DockerCloud::Client.new(self).deploy
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
data/make_new_release.sh
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
DOCKER_IMAGE_NAME="$USER/capistrano-docker-cloud"
|
|
4
|
+
LIBRARY_NEW_VERSION=`cat lib/**/*.rb | grep VERSION | awk '{ print $3 }' | tr -d "'"`
|
|
5
|
+
|
|
6
|
+
LIBRARY_UPDATED=`git status --porcelain | grep -v "lib/capistrano/docker-cloud/version.rb"`
|
|
7
|
+
if [[ -n "$LIBRARY_UPDATED" ]]; then
|
|
8
|
+
echo "Your repository is not clean !"
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
echo "Ensuring Docker image $DOCKER_IMAGE_NAME exists ..."
|
|
13
|
+
EXISTING_DOCKER_IMAGE=`docker images | grep "$DOCKER_IMAGE_NAME"`
|
|
14
|
+
if [[ -z "$EXISTING_DOCKER_IMAGE" ]]; then
|
|
15
|
+
echo "Building the Docker image ..."
|
|
16
|
+
docker build -t "$DOCKER_IMAGE_NAME" .
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
echo "Releasing gem ..."
|
|
20
|
+
docker run --rm -v ~/.gitconfig:/root/.gitconfig \
|
|
21
|
+
-v ~/.ssh/:/root/.ssh/ \
|
|
22
|
+
-v ~/.gem/:/root/.gem/ \
|
|
23
|
+
-v `pwd`:/gem/ "$DOCKER_IMAGE_NAME" rake release
|
metadata
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: capistrano-docker-cloud
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Guillaume Hain
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2016-08-28 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: capistrano
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '3.1'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: capistrano-bundler
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.1'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.1'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: docker_cloud
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: 0.3.0
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: 0.3.0
|
|
55
|
+
description: Docker cloud specific Capistrano tasks
|
|
56
|
+
email:
|
|
57
|
+
- zedtux@zedroot.org
|
|
58
|
+
executables: []
|
|
59
|
+
extensions: []
|
|
60
|
+
extra_rdoc_files: []
|
|
61
|
+
files:
|
|
62
|
+
- ".gitignore"
|
|
63
|
+
- CHANGELOG.md
|
|
64
|
+
- Dockerfile
|
|
65
|
+
- Gemfile
|
|
66
|
+
- LICENSE.txt
|
|
67
|
+
- README.md
|
|
68
|
+
- Rakefile
|
|
69
|
+
- capistrano-rails.gemspec
|
|
70
|
+
- lib/capistrano/docker-cloud.rb
|
|
71
|
+
- lib/capistrano/docker-cloud/base.rb
|
|
72
|
+
- lib/capistrano/docker-cloud/client.rb
|
|
73
|
+
- lib/capistrano/docker-cloud/helpers/collection_helper.rb
|
|
74
|
+
- lib/capistrano/docker-cloud/service.rb
|
|
75
|
+
- lib/capistrano/docker-cloud/stack.rb
|
|
76
|
+
- lib/capistrano/docker-cloud/version.rb
|
|
77
|
+
- lib/capistrano/tasks/docker-cloud.rake
|
|
78
|
+
- make_new_release.sh
|
|
79
|
+
homepage: https://github.com/YourCursus/capistrano-docker-cloud
|
|
80
|
+
licenses: []
|
|
81
|
+
metadata: {}
|
|
82
|
+
post_install_message:
|
|
83
|
+
rdoc_options: []
|
|
84
|
+
require_paths:
|
|
85
|
+
- lib
|
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ">="
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '0'
|
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
requirements: []
|
|
97
|
+
rubyforge_project:
|
|
98
|
+
rubygems_version: 2.6.6
|
|
99
|
+
signing_key:
|
|
100
|
+
specification_version: 4
|
|
101
|
+
summary: Docker cloud specific Capistrano tasks
|
|
102
|
+
test_files: []
|