Add console command with automatic endpoint resolution
portainer-cli console <stack> <container> looks up the stack by name to find its endpoint ID automatically, so the user never needs to specify it. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ require_relative 'portainer_cli/commands/configure'
|
|||||||
require_relative 'portainer_cli/commands/list'
|
require_relative 'portainer_cli/commands/list'
|
||||||
require_relative 'portainer_cli/commands/create'
|
require_relative 'portainer_cli/commands/create'
|
||||||
require_relative 'portainer_cli/commands/exec'
|
require_relative 'portainer_cli/commands/exec'
|
||||||
|
require_relative 'portainer_cli/commands/console'
|
||||||
|
|
||||||
module PortainerCli
|
module PortainerCli
|
||||||
module CLI
|
module CLI
|
||||||
@@ -29,7 +30,9 @@ module PortainerCli
|
|||||||
portainer-cli list networks <endpoint-id> List networks
|
portainer-cli list networks <endpoint-id> List networks
|
||||||
portainer-cli create stack --endpoint ID --name NAME --file FILE
|
portainer-cli create stack --endpoint ID --name NAME --file FILE
|
||||||
portainer-cli create container --endpoint ID --image IMAGE [--name NAME] [--port h:c] [--env K=V]
|
portainer-cli create container --endpoint ID --image IMAGE [--name NAME] [--port h:c] [--env K=V]
|
||||||
portainer-cli exec <endpoint-id> <container> Open interactive shell
|
portainer-cli console <stack> <container> Open interactive shell (resolves endpoint automatically)
|
||||||
|
--shell SHELL Shell to use (default: /bin/sh)
|
||||||
|
portainer-cli exec <endpoint-id> <container> Open interactive shell (manual endpoint)
|
||||||
--shell SHELL Shell to use (default: /bin/sh)
|
--shell SHELL Shell to use (default: /bin/sh)
|
||||||
|
|
||||||
Config file: ~/.portainer-cli/config.yml
|
Config file: ~/.portainer-cli/config.yml
|
||||||
@@ -50,7 +53,11 @@ module PortainerCli
|
|||||||
require_config!(config)
|
require_config!(config)
|
||||||
client = Client.new(config)
|
client = Client.new(config)
|
||||||
Commands::Create.new(config, client).run(argv)
|
Commands::Create.new(config, client).run(argv)
|
||||||
when 'exec', 'shell', 'console'
|
when 'console'
|
||||||
|
require_config!(config)
|
||||||
|
client = Client.new(config)
|
||||||
|
Commands::Console.new(config, client).run(argv)
|
||||||
|
when 'exec', 'shell'
|
||||||
require_config!(config)
|
require_config!(config)
|
||||||
client = Client.new(config)
|
client = Client.new(config)
|
||||||
Commands::Exec.new(config, client).run(argv)
|
Commands::Exec.new(config, client).run(argv)
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative 'base'
|
||||||
|
require_relative '../websocket_exec'
|
||||||
|
require 'optparse'
|
||||||
|
|
||||||
|
module PortainerCli
|
||||||
|
module Commands
|
||||||
|
# portainer-cli console <stack-name> <container-name> [--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
|
||||||
|
|
||||||
|
def initialize(config, client)
|
||||||
|
@config = config
|
||||||
|
@client = client
|
||||||
|
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 }
|
||||||
|
end.parse!(args)
|
||||||
|
|
||||||
|
stack_name = args.shift or error("Usage: portainer-cli console <stack> <container> [--shell /bin/bash]")
|
||||||
|
container_name = args.shift or error("Usage: portainer-cli console <stack> <container> [--shell /bin/bash]")
|
||||||
|
|
||||||
|
stack = resolve_stack(stack_name)
|
||||||
|
endpoint_id = stack['EndpointId']
|
||||||
|
|
||||||
|
$stderr.puts dim("Stack: #{stack['Name']} Endpoint: #{endpoint_id}")
|
||||||
|
|
||||||
|
container_id = resolve_container(endpoint_id, container_name)
|
||||||
|
|
||||||
|
$stderr.puts dim("Connecting to #{container_name} (#{container_id[0, 12]})...")
|
||||||
|
|
||||||
|
exec_id = create_exec(endpoint_id, container_id, opts[:shell])
|
||||||
|
ws_url = exec_websocket_url(exec_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).run
|
||||||
|
|
||||||
|
$stderr.puts dim("\r\nSession ended.")
|
||||||
|
rescue PortainerCli::Client::ApiError => e
|
||||||
|
error(e.message)
|
||||||
|
rescue Interrupt
|
||||||
|
$stderr.puts "\r\nInterrupted."
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def resolve_stack(name_or_id)
|
||||||
|
stacks = @client.get('/api/stacks')
|
||||||
|
match = stacks.find do |s|
|
||||||
|
s['Name'] == name_or_id || s['Id'].to_s == name_or_id
|
||||||
|
end
|
||||||
|
unless match
|
||||||
|
available = stacks.map { |s| " #{s['Id']} #{s['Name']}" }.join("\n")
|
||||||
|
error("Stack '#{name_or_id}' not found. Available stacks:\n#{available}")
|
||||||
|
end
|
||||||
|
match
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolve_container(endpoint_id, name_or_id)
|
||||||
|
containers = @client.get("/api/endpoints/#{endpoint_id}/docker/containers/json", all: 1)
|
||||||
|
match = containers.find do |c|
|
||||||
|
c['Id'].start_with?(name_or_id) ||
|
||||||
|
Array(c['Names']).any? { |n| n.delete_prefix('/') == name_or_id }
|
||||||
|
end
|
||||||
|
unless match
|
||||||
|
available = containers.map do |c|
|
||||||
|
" #{c['Id'][0, 12]} #{Array(c['Names']).first&.delete_prefix('/')}"
|
||||||
|
end.join("\n")
|
||||||
|
error("Container '#{name_or_id}' not found on endpoint #{endpoint_id}. Available:\n#{available}")
|
||||||
|
end
|
||||||
|
match['Id']
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_exec(endpoint_id, container_id, shell)
|
||||||
|
result = @client.post(
|
||||||
|
"/api/endpoints/#{endpoint_id}/docker/containers/#{container_id}/exec",
|
||||||
|
{
|
||||||
|
'AttachStdin' => true,
|
||||||
|
'AttachStdout' => true,
|
||||||
|
'AttachStderr' => true,
|
||||||
|
'Tty' => true,
|
||||||
|
'Cmd' => [shell]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result['Id']
|
||||||
|
end
|
||||||
|
|
||||||
|
def exec_websocket_url(exec_id)
|
||||||
|
token = @config.token || @config.api_key
|
||||||
|
@client.websocket_url('/api/websocket/exec', token: token, id: exec_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
|
||||||
Reference in New Issue
Block a user