centurion 1.0.6
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.
- data/.gitignore +10 -0
- data/CONTRIBUTORS.md +42 -0
- data/Gemfile +6 -0
- data/LICENSE +19 -0
- data/README.md +239 -0
- data/Rakefile +15 -0
- data/bin/centurion +70 -0
- data/bin/centurionize +60 -0
- data/centurion.gemspec +40 -0
- data/lib/capistrano_dsl.rb +91 -0
- data/lib/centurion.rb +5 -0
- data/lib/centurion/deploy.rb +145 -0
- data/lib/centurion/deploy_dsl.rb +94 -0
- data/lib/centurion/docker_registry.rb +35 -0
- data/lib/centurion/docker_server.rb +58 -0
- data/lib/centurion/docker_server_group.rb +31 -0
- data/lib/centurion/docker_via_api.rb +121 -0
- data/lib/centurion/docker_via_cli.rb +71 -0
- data/lib/centurion/logging.rb +28 -0
- data/lib/centurion/version.rb +3 -0
- data/lib/tasks/deploy.rake +177 -0
- data/lib/tasks/info.rake +24 -0
- data/lib/tasks/list.rake +52 -0
- data/spec/capistrano_dsl_spec.rb +67 -0
- data/spec/deploy_dsl_spec.rb +104 -0
- data/spec/deploy_spec.rb +220 -0
- data/spec/docker_server_group_spec.rb +31 -0
- data/spec/docker_server_spec.rb +43 -0
- data/spec/docker_via_api_spec.rb +111 -0
- data/spec/docker_via_cli_spec.rb +42 -0
- data/spec/logging_spec.rb +41 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/support/matchers/capistrano_dsl_matchers.rb +13 -0
- metadata +243 -0
data/centurion.gemspec
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'centurion/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'centurion'
|
8
|
+
spec.version = Centurion::VERSION
|
9
|
+
spec.authors = [
|
10
|
+
'Nic Benders', 'Karl Matthias', 'Andrew Bloomgarden', 'Aaron Bento',
|
11
|
+
'Paul Showalter', 'David Kerr', 'Jonathan Owens', 'Jon Guymon',
|
12
|
+
'Merlyn Albery-Speyer', 'Amjith Ramanujam', 'David Celis', 'Emily Hyland',
|
13
|
+
'Bryan Stearns']
|
14
|
+
spec.email = [
|
15
|
+
'nic@newrelic.com', 'kmatthias@newrelic.com', 'andrew@newrelic.com',
|
16
|
+
'aaron@newrelic.com', 'poeslacker@gmail.com', 'dkerr@newrelic.com',
|
17
|
+
'jonathan@newrelic.com', 'jon@newrelic.com', 'merlyn@newrelic.com',
|
18
|
+
'amjith@newrelic.com', 'dcelis@newrelic.com', 'ehyland@newrelic.com',
|
19
|
+
'bryan@newrelic.com']
|
20
|
+
spec.summary = %q{Deploy images to a fleet of Docker servers}
|
21
|
+
spec.homepage = 'https://github.com/newrelic/centurion'
|
22
|
+
spec.license = 'MIT'
|
23
|
+
|
24
|
+
spec.files = `git ls-files -z`.split("\x0")
|
25
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
26
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
27
|
+
spec.require_paths = ['lib']
|
28
|
+
|
29
|
+
spec.add_dependency 'trollop'
|
30
|
+
spec.add_dependency 'excon', '~> 0.33'
|
31
|
+
spec.add_dependency 'logger-colors'
|
32
|
+
|
33
|
+
spec.add_development_dependency 'bundler'
|
34
|
+
spec.add_development_dependency 'rake'
|
35
|
+
spec.add_development_dependency 'rspec'
|
36
|
+
spec.add_development_dependency 'pry'
|
37
|
+
spec.add_development_dependency 'simplecov'
|
38
|
+
|
39
|
+
spec.required_ruby_version = '>= 1.9.3'
|
40
|
+
end
|
@@ -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,145 @@
|
|
1
|
+
require 'excon'
|
2
|
+
|
3
|
+
module Centurion; end
|
4
|
+
|
5
|
+
module Centurion::Deploy
|
6
|
+
FAILED_CONTAINER_VALIDATION = 100
|
7
|
+
|
8
|
+
def stop_containers(target_server, port_bindings)
|
9
|
+
public_port = public_port_for(port_bindings)
|
10
|
+
old_containers = target_server.find_containers_by_public_port(public_port)
|
11
|
+
info "Stopping container(s): #{old_containers.inspect}"
|
12
|
+
|
13
|
+
old_containers.each do |old_container|
|
14
|
+
info "Stopping old container #{old_container['Id'][0..7]} (#{old_container['Names'].join(',')})"
|
15
|
+
target_server.stop_container(old_container['Id'])
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def wait_for_http_status_ok(target_server, port, endpoint, image_id, tag, sleep_time=5, retries=12)
|
20
|
+
info 'Waiting for the port to come up'
|
21
|
+
1.upto(retries) do
|
22
|
+
if container_up?(target_server, port) && http_status_ok?(target_server, port, endpoint)
|
23
|
+
info 'Container is up!'
|
24
|
+
break
|
25
|
+
end
|
26
|
+
|
27
|
+
info "Waiting #{sleep_time} seconds to test the #{endpoint} endpoint..."
|
28
|
+
sleep(sleep_time)
|
29
|
+
end
|
30
|
+
|
31
|
+
unless http_status_ok?(target_server, port, endpoint)
|
32
|
+
error "Failed to validate started container on #{target_server}:#{port}"
|
33
|
+
exit(FAILED_CONTAINER_VALIDATION)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def container_up?(target_server, port)
|
38
|
+
# The API returns a record set like this:
|
39
|
+
#[{"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"}]
|
40
|
+
|
41
|
+
running_containers = target_server.find_containers_by_public_port(port)
|
42
|
+
container = running_containers.pop
|
43
|
+
|
44
|
+
unless running_containers.empty?
|
45
|
+
# This _should_ never happen, but...
|
46
|
+
error "More than one container is bound to port #{port} on #{target_server}!"
|
47
|
+
return false
|
48
|
+
end
|
49
|
+
|
50
|
+
if container && container['Ports'].any? { |bind| bind['PublicPort'].to_i == port.to_i }
|
51
|
+
info "Found container up for #{Time.now.to_i - container['Created'].to_i} seconds"
|
52
|
+
return true
|
53
|
+
end
|
54
|
+
|
55
|
+
false
|
56
|
+
end
|
57
|
+
|
58
|
+
def http_status_ok?(target_server, port, endpoint)
|
59
|
+
url = "http://#{target_server.hostname}:#{port}#{endpoint}"
|
60
|
+
response = begin
|
61
|
+
Excon.get(url)
|
62
|
+
rescue Excon::Errors::SocketError
|
63
|
+
warn "Failed to connect to #{url}, no socket open."
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
|
67
|
+
return false unless response
|
68
|
+
return true if response.status >= 200 && response.status < 300
|
69
|
+
|
70
|
+
warn "Got HTTP status: #{response.status}"
|
71
|
+
false
|
72
|
+
end
|
73
|
+
|
74
|
+
def wait_for_load_balancer_check_interval
|
75
|
+
sleep(fetch(:rolling_deploy_check_interval, 5))
|
76
|
+
end
|
77
|
+
|
78
|
+
def cleanup_containers(target_server, port_bindings)
|
79
|
+
public_port = public_port_for(port_bindings)
|
80
|
+
old_containers = target_server.old_containers_for_port(public_port)
|
81
|
+
old_containers.shift(2)
|
82
|
+
|
83
|
+
info "Public port #{public_port}"
|
84
|
+
old_containers.each do |old_container|
|
85
|
+
info "Removing old container #{old_container['Id'][0..7]} (#{old_container['Names'].join(',')})"
|
86
|
+
target_server.remove_container(old_container['Id'])
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def container_config_for(target_server, image_id, port_bindings=nil, env_vars=nil)
|
91
|
+
container_config = {
|
92
|
+
'Image' => image_id,
|
93
|
+
'Hostname' => target_server.hostname,
|
94
|
+
}
|
95
|
+
|
96
|
+
if port_bindings
|
97
|
+
container_config['ExposedPorts'] = { port_bindings.keys.first => {} }
|
98
|
+
end
|
99
|
+
|
100
|
+
if env_vars
|
101
|
+
container_config['Env'] = env_vars.map { |k,v| "#{k}=#{v}" }
|
102
|
+
end
|
103
|
+
|
104
|
+
container_config
|
105
|
+
end
|
106
|
+
|
107
|
+
def start_new_container(target_server, image_id, port_bindings, volumes, env_vars=nil)
|
108
|
+
container_config = container_config_for(target_server, image_id, port_bindings, env_vars)
|
109
|
+
start_container_with_config(target_server, volumes, port_bindings, container_config)
|
110
|
+
end
|
111
|
+
|
112
|
+
def launch_console(target_server, image_id, port_bindings, volumes, env_vars=nil)
|
113
|
+
container_config = container_config_for(target_server, image_id, port_bindings, env_vars).merge(
|
114
|
+
'Cmd' => [ '/bin/bash' ],
|
115
|
+
'AttachStdin' => true,
|
116
|
+
'Tty' => true,
|
117
|
+
'OpenStdin' => true,
|
118
|
+
)
|
119
|
+
|
120
|
+
container = start_container_with_config(target_server, volumes, port_bindings, container_config)
|
121
|
+
|
122
|
+
target_server.attach(container['Id'])
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def start_container_with_config(target_server, volumes, port_bindings, container_config)
|
128
|
+
info "Creating new container for #{container_config['Image'][0..7]}"
|
129
|
+
new_container = target_server.create_container(container_config)
|
130
|
+
|
131
|
+
host_config = {
|
132
|
+
'PortBindings' => port_bindings
|
133
|
+
}
|
134
|
+
# Map some host volumes if needed
|
135
|
+
host_config['Binds'] = volumes if volumes && !volumes.empty?
|
136
|
+
|
137
|
+
info "Starting new container #{new_container['Id'][0..7]}"
|
138
|
+
target_server.start_container(new_container['Id'], host_config)
|
139
|
+
|
140
|
+
info "Inspecting new container #{new_container['Id'][0..7]}:"
|
141
|
+
info target_server.inspect_container(new_container['Id'])
|
142
|
+
|
143
|
+
new_container
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require_relative 'docker_server_group'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module Centurion::DeployDSL
|
5
|
+
def on_each_docker_host(&block)
|
6
|
+
Centurion::DockerServerGroup.new(fetch(:hosts, []), fetch(:docker_path)).tap do |hosts|
|
7
|
+
hosts.each { |host| block.call(host) }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def env_vars(new_vars)
|
12
|
+
current = fetch(:env_vars, {})
|
13
|
+
new_vars.each_pair do |new_key, new_value|
|
14
|
+
current[new_key.to_s] = new_value
|
15
|
+
end
|
16
|
+
set(:env_vars, current)
|
17
|
+
end
|
18
|
+
|
19
|
+
def host(hostname)
|
20
|
+
current = fetch(:hosts, [])
|
21
|
+
current << hostname
|
22
|
+
set(:hosts, current)
|
23
|
+
end
|
24
|
+
|
25
|
+
def localhost
|
26
|
+
# DOCKER_HOST is like 'tcp://127.0.0.1:4243'
|
27
|
+
docker_host_uri = URI.parse(ENV['DOCKER_HOST'] || "tcp://127.0.0.1")
|
28
|
+
host_and_port = [docker_host_uri.host, docker_host_uri.port].compact.join(':')
|
29
|
+
host(host_and_port)
|
30
|
+
end
|
31
|
+
|
32
|
+
def host_port(port, options)
|
33
|
+
validate_options_keys(options, [ :host_ip, :container_port, :type ])
|
34
|
+
require_options_keys(options, [ :container_port ])
|
35
|
+
|
36
|
+
add_to_bindings(
|
37
|
+
options[:host_ip] || '0.0.0.0',
|
38
|
+
options[:container_port],
|
39
|
+
port,
|
40
|
+
options[:type] || 'tcp'
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
def public_port_for(port_bindings)
|
45
|
+
# {'80/tcp'=>[{'HostIp'=>'0.0.0.0', 'HostPort'=>'80'}]}
|
46
|
+
first_port_binding = port_bindings.values.first
|
47
|
+
first_port_binding.first['HostPort']
|
48
|
+
end
|
49
|
+
|
50
|
+
def host_volume(volume, options)
|
51
|
+
validate_options_keys(options, [ :container_volume ])
|
52
|
+
require_options_keys(options, [ :container_volume ])
|
53
|
+
|
54
|
+
binds = fetch(:binds, [])
|
55
|
+
container_volume = options[:container_volume]
|
56
|
+
|
57
|
+
binds << "#{volume}:#{container_volume}"
|
58
|
+
set(:binds, binds)
|
59
|
+
end
|
60
|
+
|
61
|
+
def get_current_tags_for(image)
|
62
|
+
hosts = Centurion::DockerServerGroup.new(fetch(:hosts), fetch(:docker_path))
|
63
|
+
hosts.inject([]) do |memo, target_server|
|
64
|
+
tags = target_server.current_tags_for(image)
|
65
|
+
memo += [{ server: target_server.hostname, tags: tags }] if tags
|
66
|
+
memo
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def add_to_bindings(host_ip, container_port, port, type='tcp')
|
73
|
+
set(:port_bindings, fetch(:port_bindings, {}).tap do |bindings|
|
74
|
+
bindings["#{container_port.to_s}/#{type}"] = [
|
75
|
+
{'HostIp' => host_ip, 'HostPort' => port.to_s}
|
76
|
+
]
|
77
|
+
bindings
|
78
|
+
end)
|
79
|
+
end
|
80
|
+
|
81
|
+
def validate_options_keys(options, valid_keys)
|
82
|
+
unless options.keys.all? { |k| valid_keys.include?(k) }
|
83
|
+
raise ArgumentError.new('Options passed with invalid key!')
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def require_options_keys(options, required_keys)
|
88
|
+
missing = required_keys.reject { |k| options.keys.include?(k) }
|
89
|
+
|
90
|
+
unless missing.empty?
|
91
|
+
raise ArgumentError.new("Options must contain #{missing.inspect}")
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'excon'
|
2
|
+
require 'json'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module Centurion; end
|
6
|
+
|
7
|
+
class Centurion::DockerRegistry
|
8
|
+
def initialize()
|
9
|
+
# @base_uri = "https://staging-docker-registry.nr-ops.net"
|
10
|
+
@base_uri = 'http://chi-docker-registry.nr-ops.net'
|
11
|
+
end
|
12
|
+
|
13
|
+
def digest_for_tag( repository, tag)
|
14
|
+
path = "/v1/repositories/#{repository}/tags/#{tag}"
|
15
|
+
$stderr.puts "GET: #{path.inspect}"
|
16
|
+
response = Excon.get(
|
17
|
+
@base_uri + path,
|
18
|
+
:headers => { "Content-Type" => "application/json" }
|
19
|
+
)
|
20
|
+
raise response.inspect unless response.status == 200
|
21
|
+
|
22
|
+
# This hack is stupid, and I hate it. But it works around the fact that
|
23
|
+
# the Docker Registry will return a base JSON String, which the Ruby parser
|
24
|
+
# refuses (possibly correctly) to handle
|
25
|
+
JSON.load('[' + response.body + ']').first
|
26
|
+
end
|
27
|
+
|
28
|
+
def respository_tags( respository )
|
29
|
+
path = "/v1/repositories/#{respository}/tags"
|
30
|
+
$stderr.puts "GET: #{path.inspect}"
|
31
|
+
response = Excon.get(@base_uri + path)
|
32
|
+
raise response.inspect unless response.status == 200
|
33
|
+
JSON.load(response.body)
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'pty'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
require_relative 'logging'
|
5
|
+
require_relative 'docker_via_api'
|
6
|
+
require_relative 'docker_via_cli'
|
7
|
+
|
8
|
+
module Centurion; end
|
9
|
+
|
10
|
+
class Centurion::DockerServer
|
11
|
+
include Centurion::Logging
|
12
|
+
extend Forwardable
|
13
|
+
|
14
|
+
attr_reader :hostname, :port
|
15
|
+
|
16
|
+
def_delegators :docker_via_api, :create_container, :inspect_container,
|
17
|
+
:inspect_image, :ps, :start_container, :stop_container,
|
18
|
+
:old_containers_for_port, :remove_container
|
19
|
+
def_delegators :docker_via_cli, :pull, :tail, :attach
|
20
|
+
|
21
|
+
def initialize(host, docker_path)
|
22
|
+
@docker_path = docker_path
|
23
|
+
@hostname, @port = host.split(':')
|
24
|
+
@port ||= '4243'
|
25
|
+
end
|
26
|
+
|
27
|
+
def current_tags_for(image)
|
28
|
+
running_containers = ps.select { |c| c['Image'] =~ /#{image}/ }
|
29
|
+
return [] if running_containers.empty?
|
30
|
+
|
31
|
+
parse_image_tags_for(running_containers)
|
32
|
+
end
|
33
|
+
|
34
|
+
def find_containers_by_public_port(public_port, type='tcp')
|
35
|
+
ps.select do |container|
|
36
|
+
if container['Ports']
|
37
|
+
container['Ports'].find do |port|
|
38
|
+
port['PublicPort'] == public_port.to_i && port['Type'] == type
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def docker_via_api
|
47
|
+
@docker_via_api ||= Centurion::DockerViaApi.new(@hostname, @port)
|
48
|
+
end
|
49
|
+
|
50
|
+
def docker_via_cli
|
51
|
+
@docker_via_cli ||= Centurion::DockerViaCli.new(@hostname, @port, @docker_path)
|
52
|
+
end
|
53
|
+
|
54
|
+
def parse_image_tags_for(running_containers)
|
55
|
+
running_container_names = running_containers.map { |c| c['Image'] }
|
56
|
+
running_container_names.map { |name| name.split(/:/).last } # (image, tag)
|
57
|
+
end
|
58
|
+
end
|