Fix 5 pre-release bugs

- Help text showed wrong config path (~/.portainer-cli vs ~/.config/portainer-cli)
- --port without colon (e.g. --port 8080) crashed with nil/tcp key; now errors clearly
- --cmd used naive split(' ') breaking quoted args; switched to Shellwords.split
- --env without '=' (e.g. --env FOO) silently passed empty value; now errors clearly
- SSL/connection errors surfaced as raw exception; now caught with actionable message

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 15:35:28 +01:00
parent f263088d15
commit ed5eca8924
3 changed files with 18 additions and 4 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ module PortainerCli
portainer-cli exec <endpoint-id> <container> Open interactive shell (manual endpoint) 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: ~/.config/portainer-cli/config.yml
HELP HELP
def self.run(argv) def self.run(argv)
+8 -2
View File
@@ -2,6 +2,7 @@
require_relative 'base' require_relative 'base'
require 'optparse' require 'optparse'
require 'shellwords'
module PortainerCli module PortainerCli
module Commands module Commands
@@ -55,6 +56,7 @@ module PortainerCli
end end
env_vars = opts[:env].map do |pair| env_vars = opts[:env].map do |pair|
error("Invalid --env '#{pair}': expected KEY=VALUE") unless pair.include?('=')
k, v = pair.split('=', 2) k, v = pair.split('=', 2)
{ 'name' => k, 'value' => v.to_s } { 'name' => k, 'value' => v.to_s }
end end
@@ -90,7 +92,11 @@ module PortainerCli
port_bindings = {} port_bindings = {}
exposed_ports = {} exposed_ports = {}
opts[:ports].each do |mapping| opts[:ports].each do |mapping|
host_port, cont_port = mapping.split(':', 2) parts = mapping.split(':', 2)
error("Invalid --port '#{mapping}': expected HOST:CONTAINER (e.g. 8080:80)") unless parts.size == 2
host_port, cont_port = parts
error("Invalid --port '#{mapping}': missing host port") if host_port.empty?
error("Invalid --port '#{mapping}': missing container port") if cont_port.empty?
cont_key = "#{cont_port}/tcp" cont_key = "#{cont_port}/tcp"
exposed_ports[cont_key] = {} exposed_ports[cont_key] = {}
port_bindings[cont_key] = [{ 'HostPort' => host_port }] port_bindings[cont_key] = [{ 'HostPort' => host_port }]
@@ -102,7 +108,7 @@ module PortainerCli
'ExposedPorts' => exposed_ports, 'ExposedPorts' => exposed_ports,
'HostConfig' => { 'PortBindings' => port_bindings } 'HostConfig' => { 'PortBindings' => port_bindings }
} }
body['Cmd'] = opts[:cmd].split(' ') if opts[:cmd] body['Cmd'] = Shellwords.split(opts[:cmd]) if opts[:cmd]
query = opts[:name] ? "?name=#{URI.encode_www_form_component(opts[:name])}" : '' query = opts[:name] ? "?name=#{URI.encode_www_form_component(opts[:name])}" : ''
result = @client.post("/api/endpoints/#{opts[:endpoint]}/docker/containers/create#{query}", body) result = @client.post("/api/endpoints/#{opts[:endpoint]}/docker/containers/create#{query}", body)
+8
View File
@@ -40,11 +40,19 @@ module PortainerCli
ssl = OpenSSL::SSL::SSLSocket.new(raw, ctx) ssl = OpenSSL::SSL::SSLSocket.new(raw, ctx)
ssl.hostname = @url.host ssl.hostname = @url.host
ssl.sync_close = true ssl.sync_close = true
begin
ssl.connect ssl.connect
rescue OpenSSL::SSL::SSLError => e
raw.close rescue nil
hint = @ssl_verify ? " (try: portainer-cli configure → answer 'n' to SSL verification)" : ""
raise "SSL connection failed: #{e.message}#{hint}"
end
ssl ssl
else else
raw raw
end end
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError => e
raise "Cannot connect to #{@url.host}:#{@url.port}#{e.message}"
end end
def perform_handshake(socket) def perform_handshake(socket)