docker-armada 2.0.53
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 +12 -0
- data/.travis.yml +5 -0
- data/Gemfile +5 -0
- data/LICENSE +20 -0
- data/README.md +293 -0
- data/Rakefile +9 -0
- data/Thorfile +1 -0
- data/armada.gemspec +37 -0
- data/bin/armada +5 -0
- data/lib/armada.rb +37 -0
- data/lib/armada/clean.rb +2 -0
- data/lib/armada/clean/containers.rb +30 -0
- data/lib/armada/clean/images.rb +29 -0
- data/lib/armada/cli.rb +40 -0
- data/lib/armada/cli/clean.rb +23 -0
- data/lib/armada/cli/deploy.rb +32 -0
- data/lib/armada/cli/inspect.rb +28 -0
- data/lib/armada/configuration.rb +33 -0
- data/lib/armada/connection.rb +3 -0
- data/lib/armada/connection/docker.rb +22 -0
- data/lib/armada/connection/health_check.rb +66 -0
- data/lib/armada/connection/remote.rb +26 -0
- data/lib/armada/deploy.rb +2 -0
- data/lib/armada/deploy/parallel.rb +58 -0
- data/lib/armada/deploy/rolling.rb +60 -0
- data/lib/armada/deploy_dsl.rb +156 -0
- data/lib/armada/docker.rb +6 -0
- data/lib/armada/docker/config.rb +55 -0
- data/lib/armada/docker/container.rb +120 -0
- data/lib/armada/docker/host.rb +68 -0
- data/lib/armada/docker/image.rb +86 -0
- data/lib/armada/thor.rb +1 -0
- data/lib/armada/ui.rb +15 -0
- data/lib/armada/utils.rb +2 -0
- data/lib/armada/utils/array.rb +9 -0
- data/lib/armada/utils/time.rb +23 -0
- data/lib/armada/version.rb +3 -0
- data/spec/connection/health_check_spec.rb +36 -0
- data/spec/deploy_dsl_spec.rb +84 -0
- data/spec/docker/container_spec.rb +124 -0
- data/spec/docker/image_spec.rb +110 -0
- data/spec/spec_helper.rb +12 -0
- metadata +289 -0
@@ -0,0 +1,156 @@
|
|
1
|
+
# Copyright (c) 2014 New Relic, Inc.
|
2
|
+
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'singleton'
|
22
|
+
|
23
|
+
module Armada::DeployDSL
|
24
|
+
class CurrentEnvironmentNotSetError < RuntimeError; end
|
25
|
+
|
26
|
+
class Store < Hash
|
27
|
+
include Singleton
|
28
|
+
end
|
29
|
+
|
30
|
+
def env
|
31
|
+
Store.instance
|
32
|
+
end
|
33
|
+
|
34
|
+
def fetch(key, default=nil, &block)
|
35
|
+
env[current_environment][key] || default
|
36
|
+
end
|
37
|
+
|
38
|
+
def any?(key)
|
39
|
+
value = fetch(key)
|
40
|
+
if value && value.respond_to?(:any?)
|
41
|
+
value.any?
|
42
|
+
else
|
43
|
+
!fetch(key).nil?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def set(key, value)
|
48
|
+
env[current_environment][key] = value
|
49
|
+
end
|
50
|
+
|
51
|
+
def delete(key)
|
52
|
+
env[current_environment].delete(key)
|
53
|
+
end
|
54
|
+
|
55
|
+
def set_current_environment(environment)
|
56
|
+
env[:current_environment] = environment
|
57
|
+
env[environment] ||= {}
|
58
|
+
end
|
59
|
+
|
60
|
+
def current_environment
|
61
|
+
raise CurrentEnvironmentNotSetError.new('Must set current environment') unless env[:current_environment]
|
62
|
+
env[:current_environment]
|
63
|
+
end
|
64
|
+
|
65
|
+
def clear_env
|
66
|
+
env.clear
|
67
|
+
end
|
68
|
+
|
69
|
+
def container_name(name)
|
70
|
+
set(:container_name, name)
|
71
|
+
end
|
72
|
+
|
73
|
+
def env_vars(new_vars)
|
74
|
+
current = fetch(:env_vars, {})
|
75
|
+
new_vars.each_pair do |new_key, new_value|
|
76
|
+
current[new_key.to_s] = new_value
|
77
|
+
end
|
78
|
+
set(:env_vars, current)
|
79
|
+
end
|
80
|
+
|
81
|
+
def host(hostname)
|
82
|
+
current = fetch(:hosts, [])
|
83
|
+
current << hostname
|
84
|
+
set(:hosts, current)
|
85
|
+
end
|
86
|
+
|
87
|
+
def localhost
|
88
|
+
# DOCKER_HOST is like 'tcp://127.0.0.1:4243'
|
89
|
+
docker_host_uri = URI.parse(ENV['DOCKER_HOST'] || "tcp://127.0.0.1")
|
90
|
+
host_and_port = [docker_host_uri.host, docker_host_uri.port].compact.join(':')
|
91
|
+
host(host_and_port)
|
92
|
+
end
|
93
|
+
|
94
|
+
def host_port(port, options)
|
95
|
+
validate_options_keys(options, [ :host_ip, :container_port, :type ])
|
96
|
+
require_options_keys(options, [ :container_port ])
|
97
|
+
|
98
|
+
add_to_bindings(
|
99
|
+
options[:host_ip] || '0.0.0.0',
|
100
|
+
options[:container_port],
|
101
|
+
port,
|
102
|
+
options[:type] || 'tcp'
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
def restart_policy(opts)
|
107
|
+
set(:restart_policy, opts)
|
108
|
+
end
|
109
|
+
|
110
|
+
def secret_value(key)
|
111
|
+
`conjur variable value #{key}`
|
112
|
+
end
|
113
|
+
|
114
|
+
def public_port_for(port_bindings)
|
115
|
+
# {'80/tcp'=>[{'HostIp'=>'0.0.0.0', 'HostPort'=>'80'}]}
|
116
|
+
first_port_binding = port_bindings.values.first
|
117
|
+
first_port_binding.first['HostPort']
|
118
|
+
end
|
119
|
+
|
120
|
+
def host_volume(volume, options)
|
121
|
+
validate_options_keys(options, [ :container_volume ])
|
122
|
+
require_options_keys(options, [ :container_volume ])
|
123
|
+
|
124
|
+
binds = fetch(:binds, [])
|
125
|
+
container_volume = options[:container_volume]
|
126
|
+
|
127
|
+
binds << "#{volume}:#{container_volume}"
|
128
|
+
set(:binds, binds)
|
129
|
+
end
|
130
|
+
|
131
|
+
def container_config(cfg)
|
132
|
+
set(:container_config, cfg)
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def add_to_bindings(host_ip, container_port, port, type='tcp')
|
138
|
+
ports = fetch(:port_bindings, {})
|
139
|
+
ports["#{container_port.to_s}/#{type}"] = [{'HostIp' => host_ip, 'HostPort' => port.to_s}]
|
140
|
+
set(:port_bindings, ports)
|
141
|
+
end
|
142
|
+
|
143
|
+
def validate_options_keys(options, valid_keys)
|
144
|
+
unless options.keys.all? { |k| valid_keys.include?(k) }
|
145
|
+
raise ArgumentError.new('Options passed with invalid key!')
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def require_options_keys(options, required_keys)
|
150
|
+
missing = required_keys.reject { |k| options.keys.include?(k) }
|
151
|
+
|
152
|
+
unless missing.empty?
|
153
|
+
raise ArgumentError.new("Options must contain #{missing.inspect}")
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module Armada
|
4
|
+
module Docker
|
5
|
+
class Config
|
6
|
+
attr_reader :configs
|
7
|
+
def initialize(configs)
|
8
|
+
@configs = configs
|
9
|
+
end
|
10
|
+
|
11
|
+
def for_image(url)
|
12
|
+
@configs.each do |config|
|
13
|
+
# debate starts_with? vs ==
|
14
|
+
return config if url.start_with? config.url
|
15
|
+
end
|
16
|
+
|
17
|
+
return nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.load(path)
|
21
|
+
abs_path = File.expand_path path
|
22
|
+
configs = []
|
23
|
+
|
24
|
+
if File.readable? abs_path
|
25
|
+
json_hash = JSON.parse(IO.read(abs_path))
|
26
|
+
Armada.ui.info "Loading dockercfg from: #{abs_path}"
|
27
|
+
json_hash.each do |url, obj|
|
28
|
+
configs.push Credentials.parse(url, obj)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
Config.new configs
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Credentials
|
37
|
+
attr_reader :url, :username, :password, :email
|
38
|
+
def initialize(url, username, password, email)
|
39
|
+
@username = username
|
40
|
+
@password = password
|
41
|
+
@email = email
|
42
|
+
@url = url
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.parse(url, obj)
|
46
|
+
username, password = Base64.decode64(obj["auth"]).split(':', 2)
|
47
|
+
return self.new url, username, password, obj["email"]
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.dummy()
|
51
|
+
self.new '', nil, nil, ''
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Armada
|
2
|
+
class Container
|
3
|
+
|
4
|
+
attr_reader :id, :container, :name
|
5
|
+
def initialize(image, docker_host, options)
|
6
|
+
@docker_host = docker_host
|
7
|
+
@id = nil
|
8
|
+
@image = image
|
9
|
+
@name = options[:container_name]
|
10
|
+
@container = docker_host.get_container(@name)
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def stop
|
15
|
+
if @container
|
16
|
+
info "Stopping the running container named - #{@name}"
|
17
|
+
kill
|
18
|
+
remove
|
19
|
+
else
|
20
|
+
warn "No container found with the name #{@name}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def start
|
25
|
+
info "Creating new container for image - #{@image.name}:#{@image.tag} with image id (#{@image.id}) with container name #{@name}"
|
26
|
+
container_config = Armada::Container.create_container_config(@image.id, @name, @docker_host.host, @options)
|
27
|
+
begin
|
28
|
+
@container = create(container_config)
|
29
|
+
@id = @container.id
|
30
|
+
info "Starting new container #{@id[0..11]}"
|
31
|
+
@container.start!(Armada::Container.create_host_config(@options))
|
32
|
+
rescue Exception => e
|
33
|
+
raise "Error occured on #{@docker_host.host}:#{@docker_host.port}: #{e.message}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def create(container_config)
|
38
|
+
::Docker::Container.create(container_config, @docker_host.connection)
|
39
|
+
end
|
40
|
+
|
41
|
+
def kill
|
42
|
+
return if @container.nil?
|
43
|
+
info "Stopping old container #{@container.id[0..7]} (#{@name})"
|
44
|
+
@container.kill
|
45
|
+
end
|
46
|
+
|
47
|
+
def remove
|
48
|
+
return if @container.nil?
|
49
|
+
info "Deleting old container #{@container.id[0..7]} (#{@name})"
|
50
|
+
begin
|
51
|
+
@container.remove
|
52
|
+
rescue Exception => e
|
53
|
+
error "Could not remove container #{@container.id[0..7]} (#{@name}).\nException was: #{e.message}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.create_host_config(options)
|
58
|
+
host_config = {}
|
59
|
+
host_config['Binds'] = options[:binds] if options[:binds] && !options[:binds].empty?
|
60
|
+
host_config['PortBindings'] = options[:port_bindings] if options[:port_bindings]
|
61
|
+
host_config['PublishAllPorts'] = true
|
62
|
+
host_config
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.create_container_config(image_id, container_name, host, options = {})
|
66
|
+
container_config = options[:container_config] || {}
|
67
|
+
|
68
|
+
container_config['Image'] = image_id || options[:image]
|
69
|
+
container_config['Hostname'] = host
|
70
|
+
|
71
|
+
if options[:port_bindings]
|
72
|
+
container_config['ExposedPorts'] ||= {}
|
73
|
+
options[:port_bindings].keys.each do |port|
|
74
|
+
container_config['ExposedPorts'][port] = {}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
if options[:env_vars]
|
79
|
+
container_config['Env'] = options[:env_vars].map { |k,v| "#{k}=#{v}" }
|
80
|
+
end
|
81
|
+
|
82
|
+
if options[:binds]
|
83
|
+
container_config['Volumes'] = options[:binds].inject({}) do |memo, v|
|
84
|
+
memo[v.split(/:/).last] = {}
|
85
|
+
memo
|
86
|
+
end
|
87
|
+
container_config['VolumesFrom'] = 'parent'
|
88
|
+
end
|
89
|
+
|
90
|
+
if container_name
|
91
|
+
container_config['name'] = container_name
|
92
|
+
#should we do soemthing if container name isnt set?
|
93
|
+
end
|
94
|
+
|
95
|
+
if options[:restart_policy]
|
96
|
+
container_config["RestartPolicy"] = options[:restart_policy]
|
97
|
+
end
|
98
|
+
|
99
|
+
container_config
|
100
|
+
end
|
101
|
+
|
102
|
+
def ports
|
103
|
+
return @container.json["NetworkSettings"]["Ports"]
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def info(message)
|
109
|
+
Armada.ui.info "#{@docker_host.host} -- #{message}"
|
110
|
+
end
|
111
|
+
|
112
|
+
def warn(message)
|
113
|
+
Armada.ui.warn "#{@docker_host.host} -- #{message}"
|
114
|
+
end
|
115
|
+
|
116
|
+
def error(message)
|
117
|
+
Armada.ui.error "#{@docker_host.host} -- #{message}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Armada
|
2
|
+
class Host
|
3
|
+
attr_reader :docker_connection
|
4
|
+
def initialize(docker_connection)
|
5
|
+
@docker_connection = docker_connection
|
6
|
+
end
|
7
|
+
|
8
|
+
def get_image(name, tag, options = {})
|
9
|
+
begin
|
10
|
+
image = ::Docker::Image.get("#{name}:#{tag}", {}, connection)
|
11
|
+
options[:docker_image] = image
|
12
|
+
options[:id] = image.id if image
|
13
|
+
rescue Exception => e
|
14
|
+
Armada.ui.warn "#{host} -- #{e.message}"
|
15
|
+
ensure
|
16
|
+
return Image.new(self, options)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_image_by_id(id, options)
|
21
|
+
begin
|
22
|
+
image = ::Docker::Image.get(id, {}, connection)
|
23
|
+
options[:docker_image] = image
|
24
|
+
options[:id] = id
|
25
|
+
rescue Exception => e
|
26
|
+
Armada.ui.warn "#{host} -- #{e.message}"
|
27
|
+
ensure
|
28
|
+
return Image.new(self, options)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_all_images
|
33
|
+
::Docker::Image.all({:all => true}, connection)
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_all_containers
|
37
|
+
::Docker::Container.all({:all => true}, connection)
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_container(id)
|
41
|
+
begin
|
42
|
+
return ::Docker::Container.get(id, {}, connection)
|
43
|
+
rescue Exception => e
|
44
|
+
return nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def connection
|
49
|
+
@docker_connection.connection
|
50
|
+
end
|
51
|
+
|
52
|
+
def host
|
53
|
+
@docker_connection.host
|
54
|
+
end
|
55
|
+
|
56
|
+
def port
|
57
|
+
@docker_connection.port
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_s
|
61
|
+
"#{host}:#{port} -- #{connection}"
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.create(host, options = {})
|
65
|
+
Host.new(Armada::Connection::Docker.new(host, options[:ssh_gateway], options[:ssh_gateway_user]))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Armada
|
2
|
+
class Image
|
3
|
+
|
4
|
+
attr_reader :id, :image, :name, :tag, :auth
|
5
|
+
#do not use this method directly, instead call create
|
6
|
+
def initialize(docker_host, options)
|
7
|
+
@name = options[:image]
|
8
|
+
@tag = options[:tag]
|
9
|
+
@pull = options[:pull]
|
10
|
+
@docker_host = docker_host
|
11
|
+
@image = options[:docker_image]
|
12
|
+
@id = @image.id if @image
|
13
|
+
@auth = generate_auth(options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def valid?
|
17
|
+
return @id && @image
|
18
|
+
end
|
19
|
+
|
20
|
+
def pull
|
21
|
+
if @pull
|
22
|
+
begin
|
23
|
+
info "Pulling image [#{@name}] with tag [#{@tag}]"
|
24
|
+
@image = ::Docker::Image.create({:fromImage => @name, :tag => @tag}, @auth, @docker_host.connection)
|
25
|
+
@id = @image.id
|
26
|
+
rescue Exception => e
|
27
|
+
raise "An error occurred while trying to pull image [#{@name}] with tag [#{@tag}] -- #{e.message}"
|
28
|
+
end
|
29
|
+
else
|
30
|
+
info "Not pulling image [#{@name}] with tag [#{@tag}] because `--no-pull` was specified."
|
31
|
+
raise "The image id is not set, you cannot proceed with the deploy until a valid image is found -- [#{@name}:#{@tag}]" unless valid?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def extract_credentials(options)
|
36
|
+
options_credentials = options.select { |k, v| [:username, :password, :email].include? k }
|
37
|
+
unless options_credentials.empty?
|
38
|
+
return options_credentials
|
39
|
+
else
|
40
|
+
dockercfg = options[:dockercfg].for_image @name if options[:dockercfg]
|
41
|
+
if dockercfg.nil?
|
42
|
+
return {}
|
43
|
+
else
|
44
|
+
return {
|
45
|
+
:username => dockercfg.username,
|
46
|
+
:password => dockercfg.password,
|
47
|
+
:email => dockercfg.email
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def valid_credentials?(m)
|
54
|
+
[:username, :password].each do |k|
|
55
|
+
unless m.include? k
|
56
|
+
return false
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
return true
|
61
|
+
end
|
62
|
+
|
63
|
+
def generate_auth(options)
|
64
|
+
credentials = extract_credentials(options)
|
65
|
+
|
66
|
+
if valid_credentials? credentials
|
67
|
+
return credentials
|
68
|
+
else
|
69
|
+
return nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def info(message)
|
74
|
+
Armada.ui.info "#{@docker_host.host} -- #{message}"
|
75
|
+
end
|
76
|
+
|
77
|
+
def warn(message)
|
78
|
+
Armada.ui.warn "#{@docker_host.host} -- #{message}"
|
79
|
+
end
|
80
|
+
|
81
|
+
def error(message)
|
82
|
+
Armada.ui.error "#{@docker_host.host} -- #{message}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|