module Kontena::Cli::Helpers::ExecHelper
Constants
- WEBSOCKET_CLIENT_OPTIONS
Public Instance Methods
Execute command on container using websocket API.
@param id [String] Container ID (grid/host/name) @param cmd [Array<String>] command to execute @return [Integer] exit code
# File lib/kontena/cli/helpers/exec_helper.rb, line 204 def container_exec(id, cmd, **exec_options) websocket_exec("containers/#{id}/exec", cmd, **exec_options) end
@param ws [Kontena::Websocket::Client] @param tty [Boolean] read stdin in raw mode, sending tty escapes for remote pty @raise [ArgumentError] not a tty @yield [data] @yieldparam data [String] unicode data from stdin @raise [ArgumentError] not a tty @return EOF on stdin (!tty)
# File lib/kontena/cli/helpers/exec_helper.rb, line 34 def read_stdin(tty: nil) if tty raise ArgumentError, "the input device is not a TTY" unless STDIN.tty? STDIN.raw { |io| # we do not expect EOF on a TTY, ^D sends a tty escape to close the pty instead loop do # raises EOFError, SyscallError or IOError chunk = io.readpartial(1024) # STDIN.raw does not use the ruby external_encoding, it returns binary strings (ASCII-8BIT encoding) # however, we use websocket text frames with JSON, which expects unicode strings encodable as UTF-8, and does not handle arbitrary binary data # assume all stdin input is using ruby's external_encoding... the JSON.dump will fail if not. chunk.force_encoding(Encoding.default_external) yield chunk end } else # line-buffered, using the default external_encoding (probably UTF-8) while line = STDIN.gets yield line end end end
Connect to server websocket, send from stdin, and write out messages
@param paths [String] @param options [Hash] @see Kontena::Websocket::Client @param cmd [Array<String>] command to execute @param interactive [Boolean] Interactive TTY on/off @param shell [Boolean] Shell on/of @param tty [Boolean] TTY on/of @return [Integer] exit code
# File lib/kontena/cli/helpers/exec_helper.rb, line 137 def websocket_exec(path, cmd, interactive: false, shell: false, tty: false) exit_status = nil write_thread = nil query = {} query[:interactive] = interactive if interactive query[:shell] = shell if shell query[:tty] = tty if tty server = require_current_master url = websocket_url(path, query) token = require_token options = WEBSOCKET_CLIENT_OPTIONS.dup options[:headers] = { 'Authorization' => "Bearer #{token.access_token}" } options[:ssl_params] = { verify_mode: ENV['SSL_IGNORE_ERRORS'].to_s == 'true' ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER, ca_file: server.ssl_cert_path, } options[:ssl_hostname] = server.ssl_subject_cn logger.debug { "websocket exec connect... #{url}" } # we do not expect CloseError, because the server will send an 'exit' message first, # and we return before seeing the close frame # TODO: handle HTTP 404 errors Kontena::Websocket::Client.connect(url, **options) do |ws| logger.debug { "websocket exec open" } # first frame contains exec command websocket_exec_write(ws, 'cmd' => cmd) if interactive # start new thread to write from stdin to websocket write_thread = websocket_exec_write_thread(ws, tty: tty) end # blocks reading from websocket, returns with exec exit code exit_status = websocket_exec_read(ws) fail ws.close_reason unless exit_status end rescue Kontena::Websocket::Error => exc exit_with_error(exc) rescue => exc logger.error { "websocket exec error: #{exc}" } raise else logger.debug { "websocket exec exit: #{exit_status}"} return exit_status ensure if write_thread write_thread.kill write_thread.join end end
@param ws [Kontena::Websocket::Client] @raise [RuntimeError] exec error @return [Integer] exit code
# File lib/kontena/cli/helpers/exec_helper.rb, line 72 def websocket_exec_read(ws) ws.read do |msg| msg = JSON.parse(msg) logger.debug "websocket exec read: #{msg.inspect}" if msg.has_key?('error') raise msg['error'] elsif msg.has_key?('exit') # breaks the read loop return msg['exit'].to_i elsif msg.has_key?('stream') if msg['stream'] == 'stdout' $stdout << msg['chunk'] else $stderr << msg['chunk'] end end end end
@param ws [Kontena::Websocket::Client] @param msg [Hash]
# File lib/kontena/cli/helpers/exec_helper.rb, line 95 def websocket_exec_write(ws, msg) logger.debug "websocket exec write: #{msg.inspect}" ws.send(JSON.dump(msg)) end
Start thread to read from stdin, and write to websocket. Closes websocket on stdin read errors.
@param ws [Kontena::Websocket::Client] @param tty [Boolean] @return [Thread]
# File lib/kontena/cli/helpers/exec_helper.rb, line 107 def websocket_exec_write_thread(ws, tty: nil) Thread.new do begin if tty console_height, console_width = TTY::Screen.size websocket_exec_write(ws, 'tty_size' => { width: console_width, height: console_height }) end read_stdin(tty: tty) do |stdin| logger.debug "websocket exec stdin with encoding=#{stdin.encoding}: #{stdin.inspect}" websocket_exec_write(ws, 'stdin' => stdin) end websocket_exec_write(ws, 'stdin' => nil) # EOF rescue => exc logger.error exc ws.close(1001, "stdin read #{exc.class}: #{exc}") end end end
@return [String]
# File lib/kontena/cli/helpers/exec_helper.rb, line 61 def websocket_url(path, query = nil) url = URI.parse(require_current_master.url) url.scheme = url.scheme.sub('http', 'ws') url.path = '/v1/' + path url.query = (query && !query.empty?) ? URI.encode_www_form(query) : nil url.to_s end