Skip to content

[Feature] Manual tag selection pins the update target of a runtime container #291

Open

Description

@LarsLaskowski

Summary

On the runtime container detail page a user can select one tag (with digest) per container, but the selection is only stored and highlighted: update evaluation, the assessment status, the findings and the recommended tag ignore it. Decision (product manager): the selection becomes a pin — the update evaluation of that container uses the pinned tag/digest as its target, so the user can say "this container should run 1.26.x, not the newest major" and get statuses and findings relative to that choice. The Portainer container actions feature (filed separately) will use the pin as the target of its update action.

Area

  • docs/areas/update-detection.md (added in [Docs] Describe the current behavior by area #263), Manual tag selection, Evaluation rules, Runtime container assessment, Update findings and tag candidates; also docs/areas/web-ui.md (Select / Clear selection) and docs/areas/container-actions.md (future consumer).
  • Security area: None for this issue (no container action is triggered; the pin is read-only input for evaluation). The later Portainer update action touches Container actions.

Current behavior

  • Storage: RuntimeContainerTagSelection (src/DockerUpdateGuard.Data/Entities/RuntimeContainerTagSelection.cs) — Docker instance, container id, registry repository, tag, optional digest, selection time; one per container (unique index in RuntimeContainerTagSelectionConfiguration.cs:43).
  • Writing: src/DockerUpdateGuard/UI/RuntimeContainerTagSelectionService.cs:37-81 (SaveSelectionAsync: trims tag/digest, takes the registry repository of the latest snapshot) and :84-105 (ClearSelectionAsync). The UI offers Select only on tag candidates of the container's findings (src/DockerUpdateGuard/Components/Pages/RuntimeContainerDetail.razor:281-323, .razor.cs:211-262).
  • Reading: only for display — ApplicationViewService.cs:1976-1982 loads it (matching the repository of the latest snapshot) and MapUpdateFinding marks the matching candidate IsSelected (:1452-1474).
  • Evaluation ignores it: RuntimeContainerScanOrchestrator.EvaluateContainerUpdateAsync (src/DockerUpdateGuard/Images/RuntimeContainerScanOrchestrator.cs:696-743) calls _updateDetectionService.Evaluate(parsedReference, availableTags) with no pin input; IUpdateDetectionService.Evaluate(ImageReference, IReadOnlyList<DockerHubTagData>) has no parameter for it.
  • The tag list given to evaluation is filtered to tags newer than / ordering above the current one (FilterAvailableTags, :308-342, and the registry-side query options from CreateTagQueryOptions, :239-258), so an older pinned tag (a deliberate downgrade or "stay on this line") is usually not even in the list.

Scenario: a container runs postgres:16.3; the evaluation recommends 16.4. The user selects 16.3 because they want to stay there until a maintenance window. Every scan still reports Update available → 16.4, the dashboard keeps counting it, and the selection has no visible effect except the "Selected" chip.

Expected behavior

Decision (product manager): the selection acts as a pin; update evaluation for that container uses the pinned tag/digest as the target.

When a pin applies

  • A pin applies to a container while the container's current image (latest snapshot) is in the same registry repository the pin was made for (same rule as today's display). If the container now runs another repository, the pin is ignored for evaluation (normal evaluation) and kept, shown as "Pin for does not apply to the current image".
  • The major-upgrade policy and the variant-family rules do not restrict a pin: the user's choice wins, including pins to an older version (downgrade) or another variant.

Pinned evaluation (first matching rule wins)

Inputs: the running image (tag, running digest), the pin (tag, optional pinned digest) and the registry's current data for the pinned tag (digest, publication time) from a direct tag lookup.

# Situation Assessment status Recommended tag/digest Summary
P1 Running digest equals the pinned tag's current registry digest (or, when the registry digest is unknown, the pinned digest) Up to date none "Running the pinned tag ''"
P2 Pinned tag exists, and either no pinned digest is stored or it equals the registry's current digest; running digest differs Update available pinned tag + registry digest "Pinned tag '' is the update target" (details: "Pinned by the user on "; for a lower version than the running one also "This is a downgrade from ''")
P3 Pinned tag exists but its registry digest differs from the stored pinned digest (the tag was republished) Manual review required pinned tag + new registry digest "Pinned tag '' now points to a different digest" (details name old and new digest and ask the user to confirm by selecting it again)
P4 Pinned tag no longer exists in the registry (tag lookup not found) Manual review required none "Pinned tag '' is no longer available in the registry" (details: "Clear or change the pin")
P5 Pinned tag lookup fails for another reason Failed with the registry message, previous findings stay active (same rule as any failed tag lookup today) — registry message
  • The pin is never changed or removed automatically (not in P3, not in P4); the user confirms a moved digest by selecting the candidate again, or clears the pin.
  • Running digest unknown (rare): P1 cannot match, P2 applies.

Findings and candidates

  • P2 creates a runtime image update finding, P3/P4 a tag recommendation finding (same types and deactivation rules as today).
  • Candidates of a pinned finding: the pinned target first (rank 1, recommended, when it has a digest), followed by the candidates of the normal (unpinned) evaluation of the same scan, so the user can still see and pick newer tags. The pinned candidate's reason is the pinned summary.
  • The snapshot's available update version is the pinned tag (or the semantic tag it resolves to, as for aliases today) in P2/P3; none otherwise. The resolved version is computed as today.
  • The assessment message starts with "Pinned: " so lists that only show the status/message make the pin visible.

Clearing the pin

  • Clear selection removes the pin; the next runtime scan evaluates the container normally (all evaluation rules, major-upgrade policy). Saving or clearing a pin does not re-scan immediately; the detail page says "Takes effect with the next runtime scan" next to the pin.

UI

  • /runtime-containers/{instance}/{container} shows the pin as "Pinned target: " (instead of "Selected"), with the pin state (applies / tag moved / tag missing / does not apply) and the actions Change pin (selecting another candidate) and Clear pin. Candidate buttons read Pin / Pinned.
  • The runtime containers list shows a small Pinned marker next to the update status of pinned containers.

Portainer actions (out of scope here)

  • The container actions feature (filed separately) will use the pin, when it applies and is in state P2, as the target of its update action. This issue only has to expose the effective pin target (tag + digest) through a query/service the actions feature can call; no container action is triggered here.

Acceptance criteria

  • IUpdateDetectionService gets a pure entry point for pinned evaluation (e.g. Evaluate(ImageReference currentImage, IReadOnlyList<DockerHubTagData> availableTags, UpdatePinData? pin) where UpdatePinData carries pinned tag, pinned digest and the pinned tag's registry data or a not found marker); without a pin the result is identical to today's.
  • Rules P1–P4 are implemented in the evaluation; P5 is handled in the orchestrator.
  • RuntimeContainerScanOrchestrator loads the pin of each container (one query per instance scan, not per container), performs a direct tag lookup for the pinned tag via IRegistryMetadataService.GetTagAsync (the filtered tag list cannot be relied on), and passes the result to the evaluation.
  • Findings, candidates, the assessment status/message and the available update version follow Findings and candidates.
  • Clearing the pin restores normal evaluation in the next scan; a pin for another repository is ignored.
  • Unit tests (MSTest, NSubstitute only for registry/clock boundaries), e.g. UpdateDetectionServicePinnedRunningPinnedDigestIsUpToDate, UpdateDetectionServicePinnedOlderTagRecommendsDowngrade, UpdateDetectionServicePinnedIgnoresMajorUpgradePolicy, UpdateDetectionServicePinnedTagWithMovedDigestNeedsReview, UpdateDetectionServicePinnedTagMissingNeedsReview, UpdateDetectionServiceWithoutPinMatchesUnpinnedEvaluation, RuntimeContainerScanOrchestratorPinnedContainerLooksUpPinnedTag, RuntimeContainerScanOrchestratorPinnedTagLookupFailureKeepsFindingsActive, RuntimeContainerScanOrchestratorPinForOtherRepositoryIsIgnored, RuntimeContainerScanOrchestratorClearedPinRestoresNormalEvaluation; render tests for the detail page and list marker.
  • The area document docs/areas/update-detection.md describes the new behavior and its Known gaps entry for this issue is removed; docs/areas/web-ui.md and docs/ARCHITECTURE.md (if it describes the selection as informational) are updated.

Implementation notes

  • Guarantee: update detection (IUpdateDetectionService) and VersionTagResolutionHelper stay pure and I/O-free (.squad/project.md). All registry access for the pin (the direct tag lookup) happens in RuntimeContainerScanOrchestrator; the evaluation receives plain data. Use the injected TimeProvider (see [BUG] Major-upgrade age check reads the wall clock instead of an injected TimeProvider #289) if the pinned evaluation needs time; it should not.
  • The direct lookup uses the container's OS/architecture like the current-tag lookup (RuntimeContainerScanOrchestrator.cs:602-608); map not found to P4 and other non-success statuses to P5.
  • Compute the normal evaluation as well when a pin applies, to fill the secondary candidates; this is a second pure call, no extra I/O.
  • The pin entity and its table stay as they are (no migration needed). Consider renaming the UI-facing service methods (SaveSelectionAsync → SavePinAsync) only if it does not ripple widely; the table name stays.
  • RuntimeContainerTagSelectionService.SaveSelectionAsync uses DateTimeOffset.UtcNow (:79); switch to the injected TimeProvider when touching it.
  • Dashboard counts (active update findings) and inventory gauges follow automatically from the findings; no separate change.
  • Candidate persistence still drops candidates without a digest (UpdateFindingPersistenceHelper.HasPersistableDigest); a pinned target without a registry digest is therefore shown through the finding's recommended version, not as a candidate.
  • No configuration change.

Related


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions