Repository navigation
[BUG] Portainer without EndpointId silently uses the first listed environment #279
Description
Summary
When DockerUpdateGuard:DockerInstances:<n>:Portainer:EndpointId is not set, PortainerClient resolves the environment by taking the first entry of Portainer's environment list, on every call. On a Portainer server that manages several environments this can be an environment other than the Docker host the instance describes: the capability check reports "connected", and container actions are looked up — and, for a container with the same name, executed — on the wrong host. Without an explicit EndpointId an environment may only be picked when it is unambiguous.
Area
docs/areas/container-actions.md(Configuration and mapping, Capability check, Actions)- Security area (
.squad/project.md): Container actions (PortainerClient)
Current behavior
src/DockerUpdateGuard/Portainer/PortainerClient.cs:74-86:
private static async Task<string?> ResolveEndpointIdAsync(HttpClient client, CancellationToken cancellationToken)
{
using var response = await client.GetAsync("/api/endpoints", cancellationToken).ConfigureAwait(false);
if (response.IsSuccessStatusCode == false)
{
return null;
}
var endpoints = await response.Content.ReadFromJsonAsync<PortainerEndpointItem[]>(cancellationToken: cancellationToken).ConfigureAwait(false);
return endpoints?.FirstOrDefault()?.Id.ToString();
}It is used by the capability check (PortainerClient.cs:237-252) and by ExecuteActionAsync (PortainerClient.cs:306-320) whenever EndpointId is empty. A non-success answer of the list request is also reported as "No Portainer endpoint found", hiding the HTTP status.
README (README.md:191): "Explicit Portainer endpoint ID; auto-discovered when omitted".
Scenario: one Portainer manages local (Id 1, the Portainer host itself) and nas (Id 2). DockerUpdateGuard instance nas has Portainer enabled without EndpointId. The capability check says "Portainer connected, endpoint 1"; a restart of container db for instance nas restarts db on local if a container of that name exists there.
Expected behavior
Decision (product manager): no silent pick.
- With
EndpointIdconfigured: unchanged — that environment is used. - Without
EndpointId:- Portainer lists exactly one environment → it is used (as today).
- Portainer lists more than one environment → the capability check reports configured, actions not supported with a clear message such as "Portainer manages 3 environments; set DockerUpdateGuard:DockerInstances::Portainer:EndpointId to one of: 1 (local), 2 (nas), 5 (edge)". Actions fail before any container lookup with the same message. No environment is picked.
- Portainer lists no environment → as today, "No Portainer endpoint found".
- The environment-list request answers non-success → the message names the HTTP status code (instead of "No Portainer endpoint found").
- The message lists environment IDs and names only (no URLs or secrets); for very large lists it may be truncated (e.g. the first 10 plus "…").
Acceptance criteria
- Capability check without
EndpointIdand two listed environments →IsConfigured = true,SupportsActions = false, message names the key and the IDs/names. -
ExecuteActionAsyncwithoutEndpointIdand two listed environments → failed with the same message, no container-list and no action request sent. - Exactly one listed environment → used, as today.
- Zero environments → "No Portainer endpoint found", as today.
- Environment list answering 500 → message contains the status code.
- Configured
EndpointId→ environment list not requested at all. - Unit tests (MSTest, naming per
docs/UNIT_TESTS.md) inPortainerClientTests, e.g.PortainerClientGetCapabilityWithMultipleEnvironmentsAndNoEndpointIdReportsAmbiguous,PortainerClientExecuteActionWithMultipleEnvironmentsAndNoEndpointIdFailsWithoutAction,PortainerClientGetCapabilityWithSingleEnvironmentUsesIt. - README (
README.md:191) describes the rule (EndpointIdrequired when Portainer manages more than one environment). - The area document
docs/areas/container-actions.mddescribes the new behavior and its Known gaps entry for this issue (currently Open questions item 5) is removed.
Implementation notes
- Change
ResolveEndpointIdAsyncto return a small result (resolved ID, or a reason: none / ambiguous with the list / HTTP failure) instead ofstring?; both call sites map it to their result type. PortainerEndpointItemalready carriesIdandName(src/DockerUpdateGuard/Portainer/Data/PortainerEndpointItem.cs).- Count every listed environment; do not try to guess the matching one by name or URL (the Docker instance
BaseUrland Portainer's environment URL usually differ, e.g. a socket vs. an agent URL). Recommendation: no heuristic matching. - Portainer may page
/api/endpointson large installations; requesting without paging parameters returns the full list on current versions. If paging headers are present, treat "more than one page" as ambiguous as well. - Startup validation stays unchanged (it cannot know how many environments Portainer has); the check happens at call time.
- No migration impact.
PortainerEndpoint.ExternalEndpointIdstores the configured value only; do not start persisting a resolved ID as part of this fix.
Related
- Portainer container actions feature (UI trigger and action runs) — filed separately; it relies on this fix so an action is never sent to the wrong environment.
- PR [Docs] Describe the current behavior by area #263 (area documents)
- [Feature] Trigger Portainer container actions from the runtime container page and record them as action runs #280 (Portainer container actions feature)
Generated by Claude Code
Activity
LarsLaskowski commentedon Oct 25, 2026
Squad working record
Tier: standard · Security area: Container actions (
PortainerClient) · Decision record: 0010 (Accepted) · Follow-up: #330 · Squad lessons: none · Pull request: #331Plan (
plan.md, final version with challenge and security notes): https://github.com/LarsLaskowski/DockerUpdateGuard/blob/37689cb9cb02985ea430c438485ff0584f8a4880/specs/issue-279/plan.mdSummary: without
EndpointId, an environment is used only when Portainer lists exactly one (countingX-Total-Count, an invalid header counts as unknown); more than one, or an unknown count, fails the capability check and every action before any container lookup with a message namingPortainer:EndpointIdof the Docker instance and the environment IDs/names (max 10, sanitized); an empty list keeps "No Portainer endpoint found"; a non-success list answer reports the HTTP status. Challenge: 1 major (the key in the message must not use the instance name, since configuration addresses instances by list position — accepted), 1 minor (accepted). Security: approved in two rounds.Log
Squad run metrics
Generated by Claude Code