/
driver.rb
361 lines (317 loc) · 11.8 KB
/
driver.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
require "json"
require "log4r"
require_relative "./driver/compose"
module VagrantPlugins
module DockerProvider
class Driver
# The executor is responsible for actually executing Docker commands.
# This is set by the provider, but defaults to local execution.
attr_accessor :executor
def initialize
@logger = Log4r::Logger.new("vagrant::docker::driver")
@executor = Executor::Local.new
end
# Returns the id for a new container built from `docker build`. Raises
# an exception if the id was unable to be captured from the output
#
# @return [String] id - ID matched from the docker build output.
def build(dir, **opts, &block)
args = Array(opts[:extra_args])
args << dir
opts = {with_stderr: true}
result = execute('docker', 'build', *args, opts, &block)
matches = result.match(/Successfully built (?<id>.+)$/i)
if !matches
if podman?
# Check for podman format when it is emulating docker CLI.
# Podman outputs the full hash of the container on
# the last line after a successful build.
@logger.warn("Could not find docker container ID. Scanning for podman output instead")
match = result.split.select { |str| str.match?(/[0-9a-z]{64}/) }.last
return match[0..7] unless match.nil?
else
# Check for the new output format 'writing image sha256...'
# In this case, docker builtkit is enabled. Its format is different
# from standard docker
@logger.warn("Could not determine docker container ID. Scanning for buildkit output instead")
matches = result.match(/writing image .+:(?<id>[0-9a-z]+) done/i)
end
if !matches
# This will cause a stack trace in Vagrant, but it is a bug
# if this happens anyways.
raise Errors::BuildError, result: result
end
end
# Return the matched group `id`
matches[:id]
end
# Check if podman emulating docker CLI is enabled.
#
# @return [Bool]
def podman?
execute('docker', '-v')
.scan(/podman/)
.any?
end
def create(params, **opts, &block)
image = params.fetch(:image)
links = params.fetch(:links)
ports = Array(params[:ports])
volumes = Array(params[:volumes])
name = params.fetch(:name)
cmd = Array(params.fetch(:cmd))
env = params.fetch(:env)
expose = Array(params[:expose])
run_cmd = %W(docker run --name #{name})
run_cmd << "-d" if params[:detach]
run_cmd += env.map { |k,v| ['-e', "#{k}=#{v}"] }
run_cmd += expose.map { |p| ['--expose', "#{p}"] }
run_cmd += links.map { |k, v| ['--link', "#{k}:#{v}"] }
run_cmd += ports.map { |p| ['-p', p.to_s] }
run_cmd += volumes.map { |v|
v = v.to_s
if v.include?(":") && @executor.windows?
if v.index(":") != v.rindex(":")
# If we have 2 colons, the host path is an absolute Windows URL
# and we need to remove the colon from it
host, colon, guest = v.rpartition(":")
host = "//" + host[0].downcase + host[2..-1]
v = [host, guest].join(":")
else
host, guest = v.split(":", 2)
host = Vagrant::Util::Platform.windows_path(host)
# NOTE: Docker does not support UNC style paths (which also
# means that there's no long path support). Hopefully this
# will be fixed someday and the gsub below can be removed.
host.gsub!(/^[^A-Za-z]+/, "")
v = [host, guest].join(":")
end
end
['-v', v.to_s]
}
run_cmd += %W(--privileged) if params[:privileged]
run_cmd += %W(-h #{params[:hostname]}) if params[:hostname]
run_cmd << "-t" if params[:pty]
run_cmd << "--rm=true" if params[:rm]
run_cmd += params[:extra_args] if params[:extra_args]
run_cmd += [image, cmd]
execute(*run_cmd.flatten, **opts, &block).chomp.lines.last
end
def state(cid)
case
when running?(cid)
:running
when created?(cid)
:stopped
else
:not_created
end
end
def created?(cid)
result = execute('docker', 'ps', '-a', '-q', '--no-trunc').to_s
result =~ /^#{Regexp.escape cid}$/
end
def image?(id)
result = execute('docker', 'images', '-q').to_s
result =~ /^#{Regexp.escape(id)}$/
end
def running?(cid)
result = execute('docker', 'ps', '-q', '--no-trunc')
result =~ /^#{Regexp.escape cid}$/m
end
def privileged?(cid)
inspect_container(cid)['HostConfig']['Privileged']
end
def login(email, username, password, server)
cmd = %W(docker login)
cmd += ["-e", email] if email != ""
cmd += ["-u", username] if username != ""
cmd += ["-p", password] if password != ""
cmd << server if server && server != ""
execute(*cmd.flatten)
end
def logout(server)
cmd = %W(docker logout)
cmd << server if server && server != ""
execute(*cmd.flatten)
end
def pull(image)
execute('docker', 'pull', image)
end
def start(cid)
if !running?(cid)
execute('docker', 'start', cid)
# This resets the cached information we have around, allowing `vagrant reload`s
# to work properly
@data = nil
end
end
def stop(cid, timeout)
if running?(cid)
execute('docker', 'stop', '-t', timeout.to_s, cid)
end
end
def rm(cid)
if created?(cid)
execute('docker', 'rm', '-f', '-v', cid)
end
end
def rmi(id)
execute('docker', 'rmi', id)
return true
rescue => e
return false if e.to_s.include?("is using it")
raise if !e.to_s.include?("No such image")
end
# Inspect the provided container
#
# @param [String] cid ID or name of container
# @return [Hash]
def inspect_container(cid)
JSON.parse(execute('docker', 'inspect', cid)).first
end
# @return [Array<String>] list of all container IDs
def all_containers
execute('docker', 'ps', '-a', '-q', '--no-trunc').to_s.split
end
# @return [String] IP address of the docker bridge
def docker_bridge_ip
output = execute('/sbin/ip', '-4', 'addr', 'show', 'scope', 'global', 'docker0')
if output =~ /^\s+inet ([0-9.]+)\/[0-9]+\s+/
return $1.to_s
else
# TODO: Raise an user friendly message
raise 'Unable to fetch docker bridge IP!'
end
end
# @param [String] network - name of network to connect conatiner to
# @param [String] cid - container id
# @param [Array] opts - An array of flags used for listing networks
def connect_network(network, cid, opts=nil)
command = ['docker', 'network', 'connect', network, cid].push(*opts)
output = execute(*command)
output
end
# @param [String] network - name of network to create
# @param [Array] opts - An array of flags used for listing networks
def create_network(network, opts=nil)
command = ['docker', 'network', 'create', network].push(*opts)
output = execute(*command)
output
end
# @param [String] network - name of network to disconnect container from
# @param [String] cid - container id
def disconnect_network(network, cid)
command = ['docker', 'network', 'disconnect', network, cid, "--force"]
output = execute(*command)
output
end
# @param [Array] networks - list of networks to inspect
# @param [Array] opts - An array of flags used for listing networks
def inspect_network(network, opts=nil)
command = ['docker', 'network', 'inspect'] + Array(network)
command = command.push(*opts)
output = execute(*command)
begin
JSON.load(output)
rescue JSON::ParserError
@logger.warn("Failed to parse network inspection of network: #{network}")
@logger.debug("Failed network output content: `#{output.inspect}`")
nil
end
end
# @param [String] opts - Flags used for listing networks
def list_network(*opts)
command = ['docker', 'network', 'ls', *opts]
output = execute(*command)
output
end
# Will delete _all_ defined but unused networks in the docker engine. Even
# networks not created by Vagrant.
#
# @param [Array] opts - An array of flags used for listing networks
def prune_network(opts=nil)
command = ['docker', 'network', 'prune', '--force'].push(*opts)
output = execute(*command)
output
end
# Delete network(s)
#
# @param [String] network - name of network to remove
def rm_network(*network)
command = ['docker', 'network', 'rm', *network]
output = execute(*command)
output
end
# @param [Array] opts - An array of flags used for listing networks
def execute(*cmd, **opts, &block)
@executor.execute(*cmd, **opts, &block)
end
# ######################
# Docker network helpers
# ######################
# Determines if a given network has been defined through vagrant with a given
# subnet string
#
# @param [String] subnet_string - Subnet to look for
# @return [String] network name - Name of network with requested subnet.`nil` if not found
def network_defined?(subnet_string)
all_networks = list_network_names
network_info = inspect_network(all_networks)
network_info.each do |network|
config = network["IPAM"]["Config"]
if (config.size > 0 &&
config.first["Subnet"] == subnet_string)
@logger.debug("Found existing network #{network["Name"]} already configured with #{subnet_string}")
return network["Name"]
end
end
return nil
end
# Locate network which contains given address
#
# @param [String] address IP address
# @return [String] network name
def network_containing_address(address)
names = list_network_names
networks = inspect_network(names)
return if !networks
networks.each do |net|
next if !net["IPAM"]
config = net["IPAM"]["Config"]
next if !config || config.size < 1
config.each do |opts|
subnet = IPAddr.new(opts["Subnet"])
if subnet.include?(address)
return net["Name"]
end
end
end
nil
end
# Looks to see if a docker network has already been defined
# with the given name
#
# @param [String] network_name - name of network to look for
# @return [Bool]
def existing_named_network?(network_name)
result = list_network_names
result.any?{|net_name| net_name == network_name}
end
# @return [Array<String>] list of all docker networks
def list_network_names
list_network("--format={{.Name}}").split("\n").map(&:strip)
end
# Returns true or false if network is in use or not.
# Nil if Vagrant fails to receive proper JSON from `docker network inspect`
#
# @param [String] network - name of network to look for
# @return [Bool,nil]
def network_used?(network)
result = inspect_network(network)
return nil if !result
return result.first["Containers"].size > 0
end
end
end
end