File: docker.rb

package info (click to toggle)
ruby-train 3.13.4-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,208 kB
  • sloc: ruby: 10,002; sh: 17; makefile: 8
file content (150 lines) | stat: -rw-r--r-- 4,329 bytes parent folder | download | duplicates (2)
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
require "docker"

module Train::Transports
  class Docker < Train.plugin(1)
    name "docker"

    include_options Train::Extras::CommandWrapper
    option :host, required: true
    option :docker_url, required: false

    def connection(state = {}, &block)
      opts = merge_options(options, state || {})
      validate_options(opts)

      if @connection && @connection_options == opts
        reuse_connection(&block)
      else
        create_new_connection(opts, &block)
      end
    end

    private

    # Creates a new Docker connection instance and save it for potential future
    # reuse.
    #
    # @param options [Hash] connection options
    # @return [Docker::Connection] a Docker connection instance
    # @api private
    def create_new_connection(options, &block)
      if @connection
        logger.debug("[Docker] shutting previous connection #{@connection}")
        @connection.close
      end

      @connection_options = options
      @connection = Connection.new(options, &block)
    end

    # Return the last saved Docker connection instance.
    #
    # @return [Docker::Connection] a Docker connection instance
    # @api private
    def reuse_connection
      logger.debug("[Docker] reusing existing connection #{@connection}")
      yield @connection if block_given?
      @connection
    end
  end
end

class Train::Transports::Docker
  class Connection < BaseConnection
    def initialize(conf)
      super(conf)
      @id = options[:host]

      docker_url = options[:docker_url]
      if RUBY_PLATFORM =~ /windows|mswin|msys|mingw|cygwin/
        # Docker Desktop for windows. Must override socket location.
        # https://docs.docker.com/desktop/faqs/#how-do-i-connect-to-the-remote-docker-engine-api
        # docker_socket ||= "npipe:////./pipe/docker_engine" # # Doesn't require a settings change, but also doesn't work
        docker_url ||= "tcp://localhost:2375"
      end
      Docker.url = docker_url if docker_url

      @container = ::Docker::Container.get(@id) ||
        raise("Can't find Docker container #{@id}")
      @cmd_wrapper = nil
      @cmd_wrapper = CommandWrapper.load(self, @options)
      @probably_windows = nil
    end

    def close
      # nothing to do at the moment
    end

    def uri
      if @container.nil?
        "docker://#{@id}"
      else
        "docker://#{@container.id}"
      end
    end

    def unique_identifier
      uuid = @container.nil? ? @id : @container.id # default uuid set to the docker host.
      unless sniff_for_windows?
        cmd = run_command_via_connection("head -1 /proc/self/cgroup|cut -d/ -f3") if file("/proc/self/cgroup").exist?
        unless cmd.stdout.empty?
          uuid = cmd.stdout.strip
        end
      end
      uuid
    end

    private

    def file_via_connection(path)
      if os.aix?
        Train::File::Remote::Aix.new(self, path)
      elsif os.solaris?
        Train::File::Remote::Unix.new(self, path)
      elsif os.windows?
        Train::File::Remote::Windows.new(self, path)
      else
        Train::File::Remote::Linux.new(self, path)
      end
    end

    def run_command_via_connection(cmd, &_data_handler)
      cmd = @cmd_wrapper.run(cmd) unless @cmd_wrapper.nil?

      # Cannot use os.windows? here because it calls run_command_via_connection,
      # causing infinite recursion during initial platform detection
      if sniff_for_windows?
        invocation = cmd_run_command(cmd)
      else
        invocation = sh_run_command(cmd)
      end
      stdout, stderr, exit_status = @container.exec(
        invocation, user: @options[:user]
      )
      CommandResult.new(stdout.join, stderr.join, exit_status)
    rescue ::Docker::Error::DockerError => _
      raise
    rescue => _
      # @TODO: differentiate any other error
      raise
    end

    def sh_run_command(cmd)
      ["/bin/sh", "-c", cmd]
    end

    def cmd_run_command(cmd)
      ["cmd.exe", "/s", "/c", cmd]
    end

    def sniff_for_windows?
      return @probably_windows unless @probably_windows.nil?

      # Run a command using /bin/sh, which should fail under Windows
      stdout, _stderr, _exit_status = @container.exec(
        sh_run_command("true"), user: @options[:user]
      )
      @probably_windows = !!stdout.detect { |l| l.include? "failure in a Windows system call" }
    end
  end
end