File: driver.rb

package info (click to toggle)
vagrant 2.2.14%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 9,800 kB
  • sloc: ruby: 97,301; sh: 375; makefile: 16; lisp: 1
file content (392 lines) | stat: -rw-r--r-- 13,128 bytes parent folder | download
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
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
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)
        # Check for the new output format 'writing image sha256...'
        # In this case, docker builtkit is enabled. Its format is different
        # from standard docker
        matches = result.scan(/writing image .+:([0-9a-z]+) done/i).last
        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.
            match = result.split.select { |str| str.match?(/[0-9a-z]{64}/) }.last
            return match[0..7] unless match.nil?
          else
            matches = result.scan(/Successfully built (.+)$/i).last
          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[0]
      end

      # Check if podman emulating docker CLI is enabled.
      #
      # @return [Bool]
      def podman?
        execute('docker', '--version').include?("podman")
      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

      # Reads all current docker containers and determines what ports
      # are currently registered to be forwarded
      # {2222=>#<Set: {"127.0.0.1"}>, 8080=>#<Set: {"*"}>, 9090=>#<Set: {"*"}>}
      #
      # Note: This is this format because of what the builtin action for resolving colliding
      # port forwards expects.
      #
      # @return [Hash[Set]] used_ports - {forward_port: #<Set: {"host ip address"}>}
      def read_used_ports
        used_ports = Hash.new{|hash,key| hash[key] = Set.new}

        all_containers.each do |c|
          container_info = inspect_container(c)

          if container_info["HostConfig"]["PortBindings"]
            port_bindings = container_info["HostConfig"]["PortBindings"]
            next if port_bindings.empty? # Nothing defined, but not nil either

            port_bindings.each do |guest_port,host_mapping|
              host_mapping.each do |h|
                if h["HostIp"] == ""
                  hostip = "*"
                else
                  hostip = h["HostIp"]
                end
                hostport = h["HostPort"]
                used_ports[hostport].add(hostip)
              end
            end
          end
        end

        used_ports
      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")
        return false if e.to_s.include?("is being used")
        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