# frozen_string_literal: true require_relative 'base' require_relative 'resolver' require_relative '../websocket_exec' require 'optparse' module PortainerCli module Commands # portainer-cli console [--shell /bin/bash] # # Resolves the endpoint automatically from the stack, so the user never # needs to know or specify an endpoint ID. class Console include Base include Resolver def initialize(config, client, debug: false) @config = config @client = client @debug = debug end def run(args) opts = { shell: '/bin/sh' } OptionParser.new do |o| o.on('--shell SHELL', 'Shell to use (default: /bin/sh)') { |v| opts[:shell] = v } o.on('--run CMD', 'Run command instead of a bare shell') { |v| opts[:run] = v } o.on('--quiet', 'Suppress status output') { opts[:quiet] = true } o.on('--endpoint ID', 'Endpoint ID to disambiguate stacks') { |v| opts[:endpoint] = v } end.parse!(args) stack_name = args.shift or error("Usage: portainer-cli console [container] [--endpoint ID] [--run CMD] [--shell /bin/bash]") container_name = args.shift || stack_name stack = resolve_stack(stack_name, opts[:endpoint]) endpoint_id = stack['EndpointId'] status("Stack: #{stack['Name']} Endpoint: #{endpoint_id}", opts[:quiet]) container_id = resolve_container(endpoint_id, container_name, stack['Name'], opts[:quiet]) status("Connecting to #{container_name} (#{container_id[0, 12]})...", opts[:quiet]) exec_id = create_exec(endpoint_id, container_id, opts[:shell], opts[:run]) ws_url = exec_websocket_url(exec_id, endpoint_id) send_resize(endpoint_id, exec_id) if $stdout.tty? trap('SIGWINCH') { send_resize(endpoint_id, exec_id) } if $stdout.tty? WebsocketExec.new(ws_url, @config.auth_header, ssl_verify: @config.ssl_verify, debug: @debug).run status("\r\nSession ended.", opts[:quiet]) rescue PortainerCli::Client::ApiError => e error(e.message) rescue Interrupt $stderr.puts "\r\nInterrupted." end private def create_exec(endpoint_id, container_id, shell, run_cmd = nil) cmd = run_cmd ? [shell, '-c', run_cmd] : [shell] result = @client.post( "/api/endpoints/#{endpoint_id}/docker/containers/#{container_id}/exec", { 'AttachStdin' => true, 'AttachStdout' => true, 'AttachStderr' => true, 'Tty' => true, 'Cmd' => cmd } ) result['Id'] end def exec_websocket_url(exec_id, endpoint_id) token = @config.token || @config.api_key @client.websocket_url('/api/websocket/exec', token: token, id: exec_id, endpointId: endpoint_id) end def send_resize(endpoint_id, exec_id) rows, cols = $stdout.winsize @client.post( "/api/endpoints/#{endpoint_id}/docker/exec/#{exec_id}/resize", { 'h' => rows, 'w' => cols } ) rescue StandardError # Non-fatal end end end end