rory-deploy 1.8.4.1
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 +10 -0
- data/CONTRIBUTORS.md +77 -0
- data/Gemfile +5 -0
- data/LICENSE +19 -0
- data/README.md +574 -0
- data/Rakefile +15 -0
- data/bin/rory +80 -0
- data/bin/rory-gen-config +79 -0
- data/lib/capistrano_dsl.rb +91 -0
- data/lib/centurion.rb +9 -0
- data/lib/centurion/deploy.rb +139 -0
- data/lib/centurion/deploy_dsl.rb +180 -0
- data/lib/centurion/docker_registry.rb +89 -0
- data/lib/centurion/docker_server.rb +79 -0
- data/lib/centurion/docker_server_group.rb +33 -0
- data/lib/centurion/docker_via_api.rb +166 -0
- data/lib/centurion/docker_via_cli.rb +81 -0
- data/lib/centurion/dogestry.rb +92 -0
- data/lib/centurion/logging.rb +28 -0
- data/lib/centurion/service.rb +218 -0
- data/lib/centurion/shell.rb +46 -0
- data/lib/centurion/version.rb +3 -0
- data/lib/core_ext/numeric_bytes.rb +94 -0
- data/lib/tasks/centurion.rake +15 -0
- data/lib/tasks/deploy.rake +250 -0
- data/lib/tasks/info.rake +24 -0
- data/lib/tasks/list.rake +56 -0
- data/rory-deploy.gemspec +33 -0
- data/spec/capistrano_dsl_spec.rb +67 -0
- data/spec/deploy_dsl_spec.rb +184 -0
- data/spec/deploy_spec.rb +212 -0
- data/spec/docker_registry_spec.rb +105 -0
- data/spec/docker_server_group_spec.rb +31 -0
- data/spec/docker_server_spec.rb +92 -0
- data/spec/docker_via_api_spec.rb +246 -0
- data/spec/docker_via_cli_spec.rb +91 -0
- data/spec/dogestry_spec.rb +73 -0
- data/spec/logging_spec.rb +41 -0
- data/spec/service_spec.rb +288 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/support/matchers/capistrano_dsl_matchers.rb +13 -0
- data/spec/support/matchers/exit_code_matches.rb +38 -0
- metadata +214 -0
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
$: << File.expand_path("lib")
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rspec/core/rake_task'
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
8
|
+
t.rspec_opts = %w[--color --format=documentation]
|
9
|
+
t.pattern = "spec/**/*_spec.rb"
|
10
|
+
end
|
11
|
+
|
12
|
+
task :default => [:spec]
|
13
|
+
rescue LoadError
|
14
|
+
# don't generate Rspec tasks if we don't have it installed
|
15
|
+
end
|
data/bin/rory
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$: << File.join(File.dirname(__FILE__), '..', 'lib')
|
3
|
+
require_relative '../lib/centurion'
|
4
|
+
require 'capistrano_dsl'
|
5
|
+
|
6
|
+
self.extend Capistrano::DSL
|
7
|
+
self.extend Centurion::DeployDSL
|
8
|
+
self.extend Centurion::Logging
|
9
|
+
|
10
|
+
#
|
11
|
+
# Initialize Rake engine
|
12
|
+
#
|
13
|
+
require 'rake'
|
14
|
+
Rake.application.options.trace = true
|
15
|
+
|
16
|
+
task_dir = File.expand_path(File.join(File.dirname(__FILE__), *%w{.. lib tasks}))
|
17
|
+
Dir.glob(File.join(task_dir, '*.rake')).each { |file| load file }
|
18
|
+
|
19
|
+
#
|
20
|
+
# Trollop option setup
|
21
|
+
#
|
22
|
+
require 'trollop'
|
23
|
+
|
24
|
+
opts = Trollop::options do
|
25
|
+
opt :project, 'project (blog, forums...)', type: String, required: true, short: '-p'
|
26
|
+
opt :environment, "environment (production, staging...)", type: String, required: true, short: '-e'
|
27
|
+
opt :action, 'action (deploy, list...)', type: String, default: 'list', short: '-a'
|
28
|
+
opt :image, 'image (yourco/project...)', type: String, short: '-i'
|
29
|
+
opt :tag, 'tag (latest...)', type: String, short: '-t'
|
30
|
+
opt :hosts, 'hosts, comma separated', type: String, short: '-h'
|
31
|
+
opt :docker_path, 'path to docker executable', type: String, default: 'docker', short: '-d'
|
32
|
+
opt :no_pull, 'Skip the pull_image step', type: :flag, default: false, short: '-n'
|
33
|
+
opt :registry_user, 'user for registry auth', type: String, short: :none
|
34
|
+
opt :registry_password,'password for registry auth', type: String, short: :none
|
35
|
+
opt :override_env, 'override environment variables, comma separated', type: String
|
36
|
+
end
|
37
|
+
|
38
|
+
set_current_environment(opts[:environment].to_sym)
|
39
|
+
set :project, opts[:project]
|
40
|
+
set :environment, opts[:environment]
|
41
|
+
|
42
|
+
# Load the per-project config and execute the task for the current environment
|
43
|
+
projects_dir = File.join(Dir.getwd(), 'config', 'centurion')
|
44
|
+
config_file = "#{opts[:project]}.rake"
|
45
|
+
if File.exists?(File.join(projects_dir, config_file))
|
46
|
+
load File.join(File.join(projects_dir, config_file))
|
47
|
+
elsif File.exists?(config_file)
|
48
|
+
load config_file
|
49
|
+
else
|
50
|
+
raise "Can't find '#{config_file}'!"
|
51
|
+
end
|
52
|
+
invoke("environment:#{opts[:environment]}")
|
53
|
+
|
54
|
+
# Override the config with command line values if given
|
55
|
+
set :image, opts[:image] if opts[:image]
|
56
|
+
set :tag, opts[:tag] if opts[:tag]
|
57
|
+
set :hosts, opts[:hosts].split(",") if opts[:hosts]
|
58
|
+
set :name, opts[:project].gsub(/_/, '-')
|
59
|
+
|
60
|
+
# Override environment variables when specified
|
61
|
+
if opts[:override_env]
|
62
|
+
opts[:override_env].split(',').each do |envvar|
|
63
|
+
key, value = envvar.split('=')
|
64
|
+
env_vars(key => value)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Default tag should be "latest"
|
69
|
+
set :tag, 'latest' unless any?(:tag)
|
70
|
+
set :docker_registry, Centurion::DockerRegistry::OFFICIAL_URL unless any?(:docker_registry)
|
71
|
+
|
72
|
+
# Specify a path to docker executable
|
73
|
+
set :docker_path, opts[:docker_path]
|
74
|
+
|
75
|
+
set :no_pull, opts[:no_pull_given]
|
76
|
+
set :registry_user, opts[:registry_user] if opts[:registry_user]
|
77
|
+
set :registry_password, opts[:registry_password] if opts[:registry_password]
|
78
|
+
|
79
|
+
invoke('centurion:setup')
|
80
|
+
invoke(opts[:action])
|
data/bin/rory-gen-config
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'trollop'
|
5
|
+
|
6
|
+
opts = Trollop::options do
|
7
|
+
opt :project, 'The short name (no spaces) of the project to scaffold', required: true, type: String
|
8
|
+
opt :registry_base, 'The base url for your registry (ex: example.com/yourcompany/) slash terminated', default: ''
|
9
|
+
opt :centurion_dir, 'The base dir for centurion configs', default: File.join(Dir.getwd, 'config', 'centurion')
|
10
|
+
end
|
11
|
+
|
12
|
+
project_file = File.join(opts[:centurion_dir], "#{opts[:project]}.rake")
|
13
|
+
|
14
|
+
unless Dir.exists?(opts[:centurion_dir])
|
15
|
+
puts "Creating #{opts[:centurion_dir]}"
|
16
|
+
FileUtils.mkdir_p(opts[:centurion_dir])
|
17
|
+
end
|
18
|
+
|
19
|
+
unless File.exists?(project_file)
|
20
|
+
puts "Writing example config to #{project_file}"
|
21
|
+
File.write(project_file, <<-EOS.gsub(/^\s{4}/, ''))
|
22
|
+
namespace :environment do
|
23
|
+
task :common do
|
24
|
+
set :image, '#{opts[:registry_base]}#{opts[:project]}'
|
25
|
+
# Point this to an appropriate health check endpoint for rolling deploys (defaults to '/')
|
26
|
+
# set :status_endpoint, '/status/check'
|
27
|
+
|
28
|
+
# Example on how to change docker registry to Dogestry.
|
29
|
+
# This requires:
|
30
|
+
# - aws_access_key_id
|
31
|
+
# - aws_secret_key
|
32
|
+
# - s3_bucket
|
33
|
+
#
|
34
|
+
# And optionally:
|
35
|
+
# - s3_region (default to us-east-1)
|
36
|
+
#
|
37
|
+
# registry :dogestry
|
38
|
+
# set :aws_access_key_id, 'abc123'
|
39
|
+
# set :aws_secret_key, 'xyz'
|
40
|
+
# set :s3_bucket, 'bucket-for-docker-images'
|
41
|
+
end
|
42
|
+
|
43
|
+
desc 'Staging environment'
|
44
|
+
task :staging => :common do
|
45
|
+
set_current_environment(:staging)
|
46
|
+
# env_vars YOUR_ENV: 'staging'
|
47
|
+
# host_port 10234, container_port: 9292
|
48
|
+
# host 'docker-server-staging-1.example.com'
|
49
|
+
# host 'docker-server-staging-2.example.com'
|
50
|
+
|
51
|
+
# You can assign different docker daemon port. Example:
|
52
|
+
# host 'docker-server-staging-3.example.com:4243'
|
53
|
+
end
|
54
|
+
|
55
|
+
desc 'Production environment'
|
56
|
+
task :production => :common do
|
57
|
+
set_current_environment(:production)
|
58
|
+
# env_vars YOUR_ENV: 'production'
|
59
|
+
# host_port 23235, container_port: 9293
|
60
|
+
# host 'docker-server-prod-1.example.com'
|
61
|
+
# host 'docker-server-prod-2.example.com'
|
62
|
+
# host_volume '/mnt/volume1', container_volume: '/mnt/volume1'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
EOS
|
66
|
+
end
|
67
|
+
|
68
|
+
gemfile = File.join(opts[:centurion_dir], *%w{.. .. Gemfile})
|
69
|
+
unless File.exists?(gemfile)
|
70
|
+
raise "Error creating Gemfile: $!" unless system("bash -c 'cd #{File.dirname(gemfile)} && bundle init'")
|
71
|
+
end
|
72
|
+
|
73
|
+
unless File.read(gemfile) =~ /centurion/s
|
74
|
+
puts 'Adding Centurion to the Gemfile'
|
75
|
+
File.open(gemfile, 'a') { |f| f << "gem 'centurion'" }
|
76
|
+
puts "\n\nRemember to run `bundle install` before running Centurion\n\n"
|
77
|
+
end
|
78
|
+
|
79
|
+
puts 'Done!'
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# This file borrows heavily from the Capistrano project, so although much of
|
2
|
+
# the code has been re-written at this point, we include their license here
|
3
|
+
# as a reminder. NOTE that THIS LICENSE ONLY APPLIES TO THIS FILE itself, not
|
4
|
+
# to the rest of the project.
|
5
|
+
#
|
6
|
+
# ORIGINAL CAPISTRANO LICENSE FOLLOWS:
|
7
|
+
#
|
8
|
+
# MIT License (MIT)
|
9
|
+
#
|
10
|
+
# Copyright (c) 2012-2013 Tom Clements, Lee Hambley
|
11
|
+
#
|
12
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
13
|
+
# of this software and associated documentation files (the "Software"), to deal
|
14
|
+
# in the Software without restriction, including without limitation the rights
|
15
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
16
|
+
# copies of the Software, and to permit persons to whom the Software is
|
17
|
+
# furnished to do so, subject to the following conditions:
|
18
|
+
#
|
19
|
+
# The above copyright notice and this permission notice shall be included in
|
20
|
+
# all copies or substantial portions of the Software.
|
21
|
+
#
|
22
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
23
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
24
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
25
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
26
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
27
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
28
|
+
# THE SOFTWARE.
|
29
|
+
|
30
|
+
require 'singleton'
|
31
|
+
|
32
|
+
module Capistrano
|
33
|
+
module DSL
|
34
|
+
module Env
|
35
|
+
class CurrentEnvironmentNotSetError < RuntimeError; end
|
36
|
+
|
37
|
+
class Store < Hash
|
38
|
+
include Singleton
|
39
|
+
end
|
40
|
+
|
41
|
+
def env
|
42
|
+
Store.instance
|
43
|
+
end
|
44
|
+
|
45
|
+
def fetch(key, default=nil, &block)
|
46
|
+
env[current_environment][key] || default
|
47
|
+
end
|
48
|
+
|
49
|
+
def any?(key)
|
50
|
+
value = fetch(key)
|
51
|
+
if value && value.respond_to?(:any?)
|
52
|
+
value.any?
|
53
|
+
else
|
54
|
+
!fetch(key).nil?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def set(key, value)
|
59
|
+
env[current_environment][key] = value
|
60
|
+
end
|
61
|
+
|
62
|
+
def delete(key)
|
63
|
+
env[current_environment].delete(key)
|
64
|
+
end
|
65
|
+
|
66
|
+
def set_current_environment(environment)
|
67
|
+
env[:current_environment] = environment
|
68
|
+
env[environment] ||= {}
|
69
|
+
end
|
70
|
+
|
71
|
+
def current_environment
|
72
|
+
raise CurrentEnvironmentNotSetError.new('Must set current environment') unless env[:current_environment]
|
73
|
+
env[:current_environment]
|
74
|
+
end
|
75
|
+
|
76
|
+
def clear_env
|
77
|
+
env.clear
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
module Capistrano
|
84
|
+
module DSL
|
85
|
+
include Env
|
86
|
+
|
87
|
+
def invoke(task, *args)
|
88
|
+
Rake::Task[task].invoke(*args)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/centurion.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'excon'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
module Centurion; end
|
5
|
+
|
6
|
+
module Centurion::Deploy
|
7
|
+
FAILED_CONTAINER_VALIDATION = 100
|
8
|
+
|
9
|
+
def stop_containers(target_server, service, timeout = 30)
|
10
|
+
old_containers = if service.public_ports.nil? || service.public_ports.empty?
|
11
|
+
target_server.find_containers_by_name(service.name)
|
12
|
+
else
|
13
|
+
target_server.find_containers_by_public_port(service.public_ports.first)
|
14
|
+
end
|
15
|
+
|
16
|
+
info "Stopping container(s): #{old_containers.inspect}"
|
17
|
+
|
18
|
+
old_containers.each do |old_container|
|
19
|
+
info "Stopping old container #{old_container['Id'][0..7]} (#{old_container['Names'].join(',')})"
|
20
|
+
target_server.stop_container(old_container['Id'], timeout)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def wait_for_health_check_ok(health_check_method, target_server, container_id, port, endpoint, image_id, tag, sleep_time=5, retries=12)
|
25
|
+
info 'Waiting for the port to come up'
|
26
|
+
1.upto(retries) do
|
27
|
+
if container_up?(target_server, container_id) && health_check_method.call(target_server, port, endpoint)
|
28
|
+
info 'Container is up!'
|
29
|
+
break
|
30
|
+
end
|
31
|
+
|
32
|
+
info "Waiting #{sleep_time} seconds to test the #{endpoint} endpoint..."
|
33
|
+
sleep(sleep_time)
|
34
|
+
end
|
35
|
+
|
36
|
+
unless health_check_method.call(target_server, port, endpoint)
|
37
|
+
error "Failed to validate started container on #{target_server.hostname}:#{port}"
|
38
|
+
exit(FAILED_CONTAINER_VALIDATION)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def container_up?(target_server, container_id)
|
43
|
+
# The API returns a record set like this:
|
44
|
+
#[{"Command"=>"script/run ", "Created"=>1394470428, "Id"=>"41a68bda6eb0a5bb78bbde19363e543f9c4f0e845a3eb130a6253972051bffb0", "Image"=>"quay.io/newrelic/rubicon:5f23ac3fad7979cd1efdc9295e0d8c5707d1c806", "Names"=>["/happy_pike"], "Ports"=>[{"IP"=>"0.0.0.0", "PrivatePort"=>80, "PublicPort"=>8484, "Type"=>"tcp"}], "Status"=>"Up 13 seconds"}]
|
45
|
+
|
46
|
+
container = target_server.find_container_by_id(container_id)
|
47
|
+
|
48
|
+
if container
|
49
|
+
info "Found container up for #{Time.now.to_i - container['Created'].to_i} seconds"
|
50
|
+
return true
|
51
|
+
end
|
52
|
+
|
53
|
+
false
|
54
|
+
end
|
55
|
+
|
56
|
+
def http_status_ok?(target_server, port, endpoint)
|
57
|
+
url = "http://#{target_server.hostname}:#{port}#{endpoint}"
|
58
|
+
response = begin
|
59
|
+
Excon.get(url, headers: {'Accept' => '*/*'})
|
60
|
+
rescue Excon::Errors::SocketError
|
61
|
+
warn "Failed to connect to #{url}, no socket open."
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
|
65
|
+
return false unless response
|
66
|
+
return true if response.status >= 200 && response.status < 300
|
67
|
+
|
68
|
+
warn "Got HTTP status: #{response.status}"
|
69
|
+
false
|
70
|
+
end
|
71
|
+
|
72
|
+
def wait_for_load_balancer_check_interval
|
73
|
+
sleep(fetch(:rolling_deploy_check_interval, 5))
|
74
|
+
end
|
75
|
+
|
76
|
+
def cleanup_containers(target_server, service)
|
77
|
+
old_containers = target_server.old_containers_for_name(service.name)
|
78
|
+
old_containers.shift(2)
|
79
|
+
|
80
|
+
info "Service name #{service.name}"
|
81
|
+
old_containers.each do |old_container|
|
82
|
+
info "Removing old container #{old_container['Id'][0..7]} (#{old_container['Names'].join(',')})"
|
83
|
+
target_server.remove_container(old_container['Id'])
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def hostname_proc
|
88
|
+
hostname = fetch(:container_hostname)
|
89
|
+
return nil if hostname.nil?
|
90
|
+
|
91
|
+
if hostname.respond_to?(:call)
|
92
|
+
hostname
|
93
|
+
else
|
94
|
+
->(h) { hostname }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def start_new_container(server, service, restart_policy)
|
99
|
+
container_config = service.build_config(server.hostname, &hostname_proc)
|
100
|
+
info "Creating new container for #{container_config['Image']}"
|
101
|
+
container = server.create_container(container_config, service.name)
|
102
|
+
|
103
|
+
host_config = service.build_host_config(restart_policy)
|
104
|
+
|
105
|
+
info "Starting new container #{container['Id'][0..7]}"
|
106
|
+
server.start_container(container['Id'], host_config)
|
107
|
+
|
108
|
+
info "Inspecting new container #{container['Id'][0..7]}:"
|
109
|
+
(server.inspect_container(container['Id']) || {}).each_pair do |key,value|
|
110
|
+
info "\t#{key} => #{value.inspect}"
|
111
|
+
end
|
112
|
+
|
113
|
+
container
|
114
|
+
end
|
115
|
+
|
116
|
+
def launch_console(server, service)
|
117
|
+
container_config = service.build_console_config(server.hostname)
|
118
|
+
info "Creating new container for #{container_config['Image'][0..7]}"
|
119
|
+
|
120
|
+
container = server.create_container(container_config, service.name)
|
121
|
+
|
122
|
+
host_config = service.build_host_config
|
123
|
+
|
124
|
+
info "Starting new container #{container['Id'][0..7]}"
|
125
|
+
server.start_container(container['Id'], host_config)
|
126
|
+
|
127
|
+
server.attach(container['Id'])
|
128
|
+
end
|
129
|
+
|
130
|
+
def enter_container(server, service)
|
131
|
+
container = if service.public_ports.nil? || service.public_ports.empty?
|
132
|
+
server.find_containers_by_name(service.name).first
|
133
|
+
else
|
134
|
+
server.find_containers_by_public_port(service.public_ports.first).first
|
135
|
+
end
|
136
|
+
|
137
|
+
server.exec_it(container["Id"], "/bin/bash")
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
require_relative 'docker_server_group'
|
2
|
+
require_relative 'docker_server'
|
3
|
+
require_relative 'service'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
module Centurion::DeployDSL
|
7
|
+
def on_each_docker_host(&block)
|
8
|
+
build_server_group.tap { |hosts| hosts.each { |host| block.call(host) } }
|
9
|
+
end
|
10
|
+
|
11
|
+
def on_first_docker_host(&block)
|
12
|
+
build_server_group.tap { |hosts| block.call(hosts.first) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def env_vars(new_vars)
|
16
|
+
current = fetch(:env_vars, {})
|
17
|
+
new_vars.each_pair do |new_key, new_value|
|
18
|
+
current[new_key.to_s] = new_value.to_s
|
19
|
+
end
|
20
|
+
set(:env_vars, current)
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_capability(new_cap_adds)
|
24
|
+
if !valid_capability?(new_cap_adds)
|
25
|
+
abort("Invalid capability addition #{new_cap_adds} specified.")
|
26
|
+
end
|
27
|
+
current = fetch(:cap_adds, [])
|
28
|
+
set(:cap_adds, current << new_cap_adds)
|
29
|
+
end
|
30
|
+
|
31
|
+
def drop_capability(new_cap_drops)
|
32
|
+
if !valid_capability?(new_cap_drops)
|
33
|
+
abort("Invalid capability drop #{new_cap_drops} specified.")
|
34
|
+
end
|
35
|
+
current = fetch(:cap_drops, [])
|
36
|
+
set(:cap_drops, current << new_cap_drops)
|
37
|
+
end
|
38
|
+
|
39
|
+
def host(hostname)
|
40
|
+
current = fetch(:hosts, [])
|
41
|
+
current << hostname
|
42
|
+
set(:hosts, current)
|
43
|
+
end
|
44
|
+
|
45
|
+
def memory(memory)
|
46
|
+
set(:memory, memory)
|
47
|
+
end
|
48
|
+
|
49
|
+
def cpu_shares(cpu_shares)
|
50
|
+
set(:cpu_shares, cpu_shares)
|
51
|
+
end
|
52
|
+
|
53
|
+
def command(command)
|
54
|
+
set(:command, command)
|
55
|
+
end
|
56
|
+
|
57
|
+
def localhost
|
58
|
+
# DOCKER_HOST is like 'tcp://127.0.0.1:2375'
|
59
|
+
docker_host_uri = URI.parse(ENV['DOCKER_HOST'] || "tcp://127.0.0.1")
|
60
|
+
host_and_port = [docker_host_uri.host, docker_host_uri.port].compact.join(':')
|
61
|
+
host(host_and_port)
|
62
|
+
end
|
63
|
+
|
64
|
+
def host_port(port, options)
|
65
|
+
validate_options_keys(options, [ :host_ip, :container_port, :type ])
|
66
|
+
require_options_keys(options, [ :container_port ])
|
67
|
+
|
68
|
+
set(:port_bindings, fetch(:port_bindings, []).tap do |bindings|
|
69
|
+
bindings << Centurion::Service::PortBinding.new(port, options[:container_port], options[:type] || 'tcp', options[:host_ip])
|
70
|
+
end)
|
71
|
+
end
|
72
|
+
|
73
|
+
def network_mode(mode)
|
74
|
+
if %w(bridge host).include?(mode) or mode =~ /container.*/
|
75
|
+
set(:network_mode, mode)
|
76
|
+
else
|
77
|
+
abort("invalid value for network_mode: #{mode}, value must be one of 'bridge', 'host', or 'container:<name|id>'")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def public_port_for(port_bindings)
|
82
|
+
# port_bindings = [#<struct Centurion::Service::PortBinding
|
83
|
+
# host_port=17090,
|
84
|
+
# container_port=80,
|
85
|
+
# type="tcp",
|
86
|
+
# host_ip=nil>]
|
87
|
+
port_bindings.first.host_port
|
88
|
+
end
|
89
|
+
|
90
|
+
def host_volume(volume, options)
|
91
|
+
validate_options_keys(options, [ :container_volume ])
|
92
|
+
require_options_keys(options, [ :container_volume ])
|
93
|
+
|
94
|
+
set(:binds, fetch(:binds, []).tap do |volumes|
|
95
|
+
volumes << Centurion::Service::Volume.new(volume, options[:container_volume])
|
96
|
+
end)
|
97
|
+
end
|
98
|
+
|
99
|
+
def get_current_tags_for(image)
|
100
|
+
build_server_group.inject([]) do |memo, target_server|
|
101
|
+
tags = target_server.current_tags_for(image)
|
102
|
+
memo += [{ server: target_server.hostname, tags: tags }] if tags
|
103
|
+
memo
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def registry(type)
|
108
|
+
set(:registry, type.to_s)
|
109
|
+
end
|
110
|
+
|
111
|
+
def health_check(method)
|
112
|
+
abort("Health check expects a callable (lambda, proc, method), but #{method.class} was specified") unless method.respond_to?(:call)
|
113
|
+
set(:health_check, method)
|
114
|
+
end
|
115
|
+
|
116
|
+
def extra_host(ip, name)
|
117
|
+
current = fetch(:extra_hosts, [])
|
118
|
+
current.push("#{name}:#{ip}")
|
119
|
+
set(:extra_hosts, current)
|
120
|
+
end
|
121
|
+
|
122
|
+
def defined_service
|
123
|
+
Centurion::Service.from_env
|
124
|
+
end
|
125
|
+
|
126
|
+
def defined_health_check
|
127
|
+
Centurion::HealthCheck.new(fetch(:health_check, method(:http_status_ok?)),
|
128
|
+
fetch(:status_endpoint, '/'),
|
129
|
+
fetch(:rolling_deploy_wait_time, 5),
|
130
|
+
fetch(:rolling_deploy_retries, 24))
|
131
|
+
end
|
132
|
+
|
133
|
+
def defined_restart_policy
|
134
|
+
Centurion::Service::RestartPolicy.new(fetch(:restart_policy_name, 'on-failure'), fetch(:restart_policy_max_retry_count, 10))
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def build_server_group
|
140
|
+
hosts, docker_path = fetch(:hosts, []), fetch(:docker_path)
|
141
|
+
Centurion::DockerServerGroup.new(hosts, docker_path, build_tls_params)
|
142
|
+
end
|
143
|
+
|
144
|
+
def validate_options_keys(options, valid_keys)
|
145
|
+
unless options.keys.all? { |k| valid_keys.include?(k) }
|
146
|
+
raise ArgumentError.new('Options passed with invalid key!')
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def require_options_keys(options, required_keys)
|
151
|
+
missing = required_keys.reject { |k| options.keys.include?(k) }
|
152
|
+
|
153
|
+
unless missing.empty?
|
154
|
+
raise ArgumentError.new("Options must contain #{missing.inspect}")
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def valid_capability?(capability)
|
159
|
+
%w(ALL SETPCAP SYS_MODULE SYS_RAWIO SYS_PACCT SYS_ADMIN SYS_NICE
|
160
|
+
SYS_RESOURCE SYS_TIME SYS_TTY_CONFIG MKNOD AUDIT_WRITE AUDIT_CONTROL
|
161
|
+
MAC_OVERRIDE MAC_ADMIN NET_ADMIN SYSLOG CHOWN NET_RAW DAC_OVERRIDE FOWNER
|
162
|
+
DAC_READ_SEARCH FSETID KILL SETGID SETUID LINUX_IMMUTABLE
|
163
|
+
NET_BIND_SERVICE NET_BROADCAST IPC_LOCK IPC_OWNER SYS_CHROOT SYS_PTRACE
|
164
|
+
SYS_BOOT LEASE SETFCAP WAKE_ALARM BLOCK_SUSPEND).include?(capability)
|
165
|
+
end
|
166
|
+
|
167
|
+
def tls_paths_available?
|
168
|
+
Centurion::DockerViaCli.tls_keys.all? { |key| fetch(key).present? }
|
169
|
+
end
|
170
|
+
|
171
|
+
def build_tls_params
|
172
|
+
return {} unless fetch(:tlsverify)
|
173
|
+
{
|
174
|
+
tls: fetch(:tlsverify || tls_paths_available?),
|
175
|
+
tlscacert: fetch(:tlscacert),
|
176
|
+
tlscert: fetch(:tlscert),
|
177
|
+
tlskey: fetch(:tlskey)
|
178
|
+
}
|
179
|
+
end
|
180
|
+
end
|