Handle duplicate stack names across endpoints in console command
When a stack name exists on multiple endpoints, error with a list showing each match with its endpoint ID and name. Pass --endpoint ID to disambiguate. Single matches continue to work without any extra flags. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
ed5eca8924
commit
e002cdf170
2 files changed
+35
-7
No files matched your search
@@ -31,6 +31,7 @@ module PortainerCli
|
|||||||
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 console <stack> [container] Open interactive shell (resolves endpoint automatically)
|
portainer-cli console <stack> [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)
|
--shell SHELL Shell to use (default: /bin/sh)
|
||||||
--run CMD Run command instead of a bare shell
|
--run CMD Run command instead of a bare shell
|
||||||
portainer-cli exec <endpoint-id> <container> Open interactive shell (manual endpoint)
|
portainer-cli exec <endpoint-id> <container> Open interactive shell (manual endpoint)
|
||||||
|
|||||||
@@ -25,12 +25,13 @@ module PortainerCli
|
|||||||
o.on('--shell SHELL', 'Shell to use (default: /bin/sh)') { |v| opts[:shell] = v }
|
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('--run CMD', 'Run command instead of a bare shell') { |v| opts[:run] = v }
|
||||||
o.on('--quiet', 'Suppress status output') { opts[:quiet] = true }
|
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)
|
end.parse!(args)
|
||||||
|
|
||||||
stack_name = args.shift or error("Usage: portainer-cli console <stack> [container] [--run CMD] [--shell /bin/bash]")
|
stack_name = args.shift or error("Usage: portainer-cli console <stack> [container] [--endpoint ID] [--run CMD] [--shell /bin/bash]")
|
||||||
container_name = args.shift || stack_name
|
container_name = args.shift || stack_name
|
||||||
|
|
||||||
stack = resolve_stack(stack_name)
|
stack = resolve_stack(stack_name, opts[:endpoint])
|
||||||
endpoint_id = stack['EndpointId']
|
endpoint_id = stack['EndpointId']
|
||||||
|
|
||||||
status("Stack: #{stack['Name']} Endpoint: #{endpoint_id}", opts[:quiet])
|
status("Stack: #{stack['Name']} Endpoint: #{endpoint_id}", opts[:quiet])
|
||||||
@@ -60,16 +61,42 @@ module PortainerCli
|
|||||||
$stderr.puts dim(msg) unless quiet
|
$stderr.puts dim(msg) unless quiet
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolve_stack(name_or_id)
|
def resolve_stack(name_or_id, endpoint_id = nil)
|
||||||
stacks = @client.get('/api/stacks')
|
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
|
s['Name'] == name_or_id || s['Id'].to_s == name_or_id
|
||||||
end
|
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}")
|
error("Stack '#{name_or_id}' not found. Available stacks:\n#{available}")
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
def resolve_container(endpoint_id, name_or_id, stack_name = nil, quiet = false)
|
def resolve_container(endpoint_id, name_or_id, stack_name = nil, quiet = false)
|
||||||
|
|||||||