Smarter container matching using stack-service-replica pattern
Prefer exact "<stack>-<service>-<N>" compose naming over generic substring match, so "librarian" in stack "librarian" resolves to "librarian-librarian-1" unambiguously. Substring fallback is also scoped to the stack's own containers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32,7 +32,7 @@ module PortainerCli
|
|||||||
|
|
||||||
$stderr.puts dim("Stack: #{stack['Name']} Endpoint: #{endpoint_id}")
|
$stderr.puts dim("Stack: #{stack['Name']} Endpoint: #{endpoint_id}")
|
||||||
|
|
||||||
container_id = resolve_container(endpoint_id, container_name)
|
container_id = resolve_container(endpoint_id, container_name, stack['Name'])
|
||||||
|
|
||||||
$stderr.puts dim("Connecting to #{container_name} (#{container_id[0, 12]})...")
|
$stderr.puts dim("Connecting to #{container_name} (#{container_id[0, 12]})...")
|
||||||
|
|
||||||
@@ -65,24 +65,36 @@ module PortainerCli
|
|||||||
match
|
match
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolve_container(endpoint_id, name_or_id)
|
def resolve_container(endpoint_id, name_or_id, stack_name = nil)
|
||||||
containers = @client.get("/api/endpoints/#{endpoint_id}/docker/containers/json", all: 1)
|
containers = @client.get("/api/endpoints/#{endpoint_id}/docker/containers/json", all: 1)
|
||||||
|
names_for = ->(c) { Array(c['Names']).map { |n| n.delete_prefix('/') } }
|
||||||
|
|
||||||
# 1. Exact match: full name or ID prefix
|
# 1. Exact match: full name or ID prefix
|
||||||
exact = containers.find do |c|
|
exact = containers.find do |c|
|
||||||
c['Id'].start_with?(name_or_id) ||
|
c['Id'].start_with?(name_or_id) ||
|
||||||
Array(c['Names']).any? { |n| n.delete_prefix('/') == name_or_id }
|
names_for.(c).any? { |n| n == name_or_id }
|
||||||
end
|
end
|
||||||
return exact['Id'] if exact
|
return exact['Id'] if exact
|
||||||
|
|
||||||
# 2. Fuzzy: container name contains the search term as a word segment
|
# 2. Compose-style match: "<stack>-<service>-<replica>"
|
||||||
# Prefers "<stack>-<name>-<n>" pattern, falls back to any substring match
|
# This is the most precise fuzzy match when we know the stack name.
|
||||||
names_for = ->(c) { Array(c['Names']).map { |n| n.delete_prefix('/') } }
|
if stack_name
|
||||||
|
composed = containers.select do |c|
|
||||||
fuzzy = containers.select do |c|
|
names_for.(c).any? { |n| n == "#{stack_name}-#{name_or_id}-1" || n.match?(/\A#{Regexp.escape(stack_name)}-#{Regexp.escape(name_or_id)}-\d+\z/) }
|
||||||
names_for.(c).any? { |n| n.include?(name_or_id) }
|
end
|
||||||
|
if composed.size == 1
|
||||||
|
matched_name = names_for.(composed.first).first
|
||||||
|
$stderr.puts dim("Matched '#{name_or_id}' → #{matched_name}")
|
||||||
|
return composed.first['Id']
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# 3. Substring fallback (only within this stack's containers)
|
||||||
|
stack_prefix = stack_name ? "#{stack_name}-" : nil
|
||||||
|
pool = stack_prefix ? containers.select { |c| names_for.(c).any? { |n| n.start_with?(stack_prefix) } } : containers
|
||||||
|
|
||||||
|
fuzzy = pool.select { |c| names_for.(c).any? { |n| n.include?(name_or_id) } }
|
||||||
|
|
||||||
if fuzzy.size == 1
|
if fuzzy.size == 1
|
||||||
matched_name = names_for.(fuzzy.first).first
|
matched_name = names_for.(fuzzy.first).first
|
||||||
$stderr.puts dim("Matched '#{name_or_id}' → #{matched_name}")
|
$stderr.puts dim("Matched '#{name_or_id}' → #{matched_name}")
|
||||||
@@ -94,8 +106,8 @@ module PortainerCli
|
|||||||
error("Ambiguous container '#{name_or_id}'. Did you mean one of:\n#{list}")
|
error("Ambiguous container '#{name_or_id}'. Did you mean one of:\n#{list}")
|
||||||
end
|
end
|
||||||
|
|
||||||
available = containers.map { |c| " #{c['Id'][0, 12]} #{names_for.(c).first}" }.join("\n")
|
available = pool.map { |c| " #{c['Id'][0, 12]} #{names_for.(c).first}" }.join("\n")
|
||||||
error("Container '#{name_or_id}' not found on endpoint #{endpoint_id}. Available:\n#{available}")
|
error("Container '#{name_or_id}' not found. Available:\n#{available}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_exec(endpoint_id, container_id, shell)
|
def create_exec(endpoint_id, container_id, shell)
|
||||||
|
|||||||
Reference in New Issue
Block a user