diff --git a/lib/portainer_cli.rb b/lib/portainer_cli.rb index 9e6b414..ef4765b 100644 --- a/lib/portainer_cli.rb +++ b/lib/portainer_cli.rb @@ -31,6 +31,7 @@ module PortainerCli 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 console [container] Open interactive shell (resolves endpoint automatically) + --endpoint ID Disambiguate when stack name exists on multiple endpoints --shell SHELL Shell to use (default: /bin/sh) --run CMD Run command instead of a bare shell portainer-cli exec Open interactive shell (manual endpoint) diff --git a/lib/portainer_cli/commands/console.rb b/lib/portainer_cli/commands/console.rb index e7c4645..0e34243 100644 --- a/lib/portainer_cli/commands/console.rb +++ b/lib/portainer_cli/commands/console.rb @@ -22,15 +22,16 @@ module PortainerCli 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('--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] [--run CMD] [--shell /bin/bash]") + 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) + stack = resolve_stack(stack_name, opts[:endpoint]) endpoint_id = stack['EndpointId'] status("Stack: #{stack['Name']} Endpoint: #{endpoint_id}", opts[:quiet]) @@ -60,16 +61,42 @@ module PortainerCli $stderr.puts dim(msg) unless quiet end - def resolve_stack(name_or_id) + def resolve_stack(name_or_id, endpoint_id = nil) stacks = @client.get('/api/stacks') - match = stacks.find do |s| + + matches = stacks.select 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") + + if matches.empty? + available = stacks.map { |s| " #{s['Id']} #{s['Name']} (endpoint: #{s['EndpointId']})" }.join("\n") error("Stack '#{name_or_id}' not found. Available stacks:\n#{available}") end - match + + if matches.size > 1 + if endpoint_id + matches = matches.select { |s| s['EndpointId'].to_s == endpoint_id.to_s } + if matches.empty? + error("Stack '#{name_or_id}' not found on endpoint #{endpoint_id}. " \ + "It exists on endpoint(s): #{matches.map { |s| s['EndpointId'] }.join(', ')}") + end + else + endpoint_names = endpoint_name_map + list = matches.map do |s| + ep_name = endpoint_names[s['EndpointId']] || s['EndpointId'].to_s + " #{s['Id']} #{s['Name']} (endpoint #{s['EndpointId']}: #{ep_name})" + end.join("\n") + error("Ambiguous stack '#{name_or_id}' exists on multiple endpoints. Use --endpoint ID:\n#{list}") + end + end + + matches.first + end + + def endpoint_name_map + @client.get('/api/endpoints').each_with_object({}) { |e, h| h[e['Id']] = e['Name'] } + rescue StandardError + {} end def resolve_container(endpoint_id, name_or_id, stack_name = nil, quiet = false)