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}")
|
||||
|
||||
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]})...")
|
||||
|
||||
@@ -65,23 +65,35 @@ module PortainerCli
|
||||
match
|
||||
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)
|
||||
names_for = ->(c) { Array(c['Names']).map { |n| n.delete_prefix('/') } }
|
||||
|
||||
# 1. Exact match: full name or ID prefix
|
||||
exact = containers.find do |c|
|
||||
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
|
||||
return exact['Id'] if exact
|
||||
|
||||
# 2. Fuzzy: container name contains the search term as a word segment
|
||||
# Prefers "<stack>-<name>-<n>" pattern, falls back to any substring match
|
||||
names_for = ->(c) { Array(c['Names']).map { |n| n.delete_prefix('/') } }
|
||||
|
||||
fuzzy = containers.select do |c|
|
||||
names_for.(c).any? { |n| n.include?(name_or_id) }
|
||||
# 2. Compose-style match: "<stack>-<service>-<replica>"
|
||||
# This is the most precise fuzzy match when we know the stack name.
|
||||
if stack_name
|
||||
composed = 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/) }
|
||||
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
|
||||
|
||||
# 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
|
||||
matched_name = names_for.(fuzzy.first).first
|
||||
@@ -94,8 +106,8 @@ module PortainerCli
|
||||
error("Ambiguous container '#{name_or_id}'. Did you mean one of:\n#{list}")
|
||||
end
|
||||
|
||||
available = containers.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}")
|
||||
available = pool.map { |c| " #{c['Id'][0, 12]} #{names_for.(c).first}" }.join("\n")
|
||||
error("Container '#{name_or_id}' not found. Available:\n#{available}")
|
||||
end
|
||||
|
||||
def create_exec(endpoint_id, container_id, shell)
|
||||
|
||||
Reference in New Issue
Block a user