From 791a2f86bf16871eefe19acf3bedadb4d0d3edaa Mon Sep 17 00:00:00 2001 From: "courtmanr@gmail.com" Date: Tue, 11 Aug 2026 15:38:42 +0100 Subject: [PATCH] Route Pulse images through product update checks --- frontend-modern/browser-verification.json | 28 ++++++------ ...dockerImagePresentation.branchcov2.test.ts | 44 +++++++++++++++++++ .../docker/dockerImagePresentation.ts | 26 +++++++++++ internal/dockeragent/registry.go | 18 ++++++++ internal/dockeragent/registry_http_test.go | 20 +++++++++ 5 files changed, 121 insertions(+), 15 deletions(-) diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 6c8082a40..f3253e9fb 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,17 +1,16 @@ { "version": 1, - "base_sha": "66d8e90c0c93b12ae17bde406254a7c734613b03", - "verified_at": "2026-08-11T14:34:04Z", + "base_sha": "a49e016246ec3dd030c0f50896326a9a07876ba9", + "verified_at": "2026-08-11T14:37:59Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts" + "frontend-modern/src/features/docker/dockerImagePresentation.ts" ], "content_sha256": { - "frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts": "85b0efd3b3f002414daeee7c29cff51c9688539452eea74388f4d72a453d6ee2" + "frontend-modern/src/features/docker/dockerImagePresentation.ts": "f51882f1d4110e6f2fc69d9f15ac384409eb32497b5529bf5c500ff4574a3f20" }, "routes": [ - "/proxmox/backups", - "/settings/infrastructure" + "/docker/images" ], "viewports": [ { @@ -24,16 +23,15 @@ } ], "states": [ - "Proxmox Backups with the mock PBS server backup-vault visible in the Backup server inventory.", - "Agent Profiles expanded inside the Pulse Agent installation dialog with 21 actionable assignment controls and no backup-vault provider projection.", - "Desktop Agent Profiles at 1440 by 1000 with no document or body horizontal overflow.", - "Narrow Agent Profiles at 390 by 844 with the Agent Assignments heading and all 21 assignment controls present, no backup-vault provider projection, and no document or body horizontal overflow." + "Docker Images with a used license.pulserelay.pro/pulse-pro image rendered as Managed by Pulse rather than as a registry authentication failure.", + "The managed-image update cell exposed the detail that Pulse checks the private image through its product update service.", + "Desktop Docker Images at 1440 by 1000 with the managed image row present and no document or body horizontal overflow.", + "Narrow Docker Images at 390 by 844 with the managed image row present and no document or body horizontal overflow." ], "interactions": [ - "Applied the repository's sanctioned infra entitlement profile only to the ignored isolated runtime billing-state path, then opened the Pro-linked unique-port browser runtime.", - "Opened Proxmox Backups and confirmed backup-vault was rendered as a real PBS fixture.", - "Opened Settings, Infrastructure, Install Pulse Agent, and Manage agent profiles through visible controls.", - "Confirmed the PBS provider fixture was not exposed as an actionable profile assignment while 21 legitimate assignment comboboxes remained available.", - "Repeated the assignment-table and overflow checks at desktop and narrow viewports." + "Used a temporary uncommitted mock-only image reference in the isolated runtime so the private Pulse image branch was reachable; removed that fixture immediately after verification.", + "Opened Docker and then Images through visible navigation controls.", + "Confirmed one used private Pulse image row showed Managed by Pulse and the product-update-service explanation at desktop width.", + "Repeated the managed-row visibility and overflow checks at the narrow viewport." ] } diff --git a/frontend-modern/src/features/docker/__tests__/dockerImagePresentation.branchcov2.test.ts b/frontend-modern/src/features/docker/__tests__/dockerImagePresentation.branchcov2.test.ts index 3f556a5c1..c1498f855 100644 --- a/frontend-modern/src/features/docker/__tests__/dockerImagePresentation.branchcov2.test.ts +++ b/frontend-modern/src/features/docker/__tests__/dockerImagePresentation.branchcov2.test.ts @@ -324,6 +324,50 @@ describe('dockerImagePresentation.branchcov2', () => { }); describe('getDockerImageOperationalPresentation (update-state branches)', () => { + it('uses the Pulse update service for the running private Pulse image', () => { + const pulseImage = image({ + name: 'license.pulserelay.pro/pulse-pro:6.2.0-rc.4', + displayName: 'license.pulserelay.pro/pulse-pro:6.2.0-rc.4', + docker: { + runtime: 'docker', + image: 'license.pulserelay.pro/pulse-pro:6.2.0-rc.4', + }, + }); + const result = getDockerImageOperationalPresentation(pulseImage, [ + container({ + name: 'pulse', + docker: { + runtime: 'docker', + image: 'license.pulserelay.pro/pulse-pro:6.2.0-rc.4', + updateStatus: { error: 'authentication required' }, + }, + }), + ]); + expect(result).toStrictEqual({ + consumerCount: 1, + consumerSummary: 'pulse', + updateLabel: 'Managed by Pulse', + updateDetail: 'Pulse checks this private image through its product update service.', + updateTone: 'muted', + }); + }); + + it('leaves an unused private Pulse image as not checked', () => { + const result = getDockerImageOperationalPresentation( + image({ + name: 'license.pulserelay.pro/pulse-pro@sha256:abc', + displayName: 'license.pulserelay.pro/pulse-pro@sha256:abc', + docker: { + runtime: 'docker', + image: 'license.pulserelay.pro/pulse-pro@sha256:abc', + }, + }), + [], + ); + expect(result.updateLabel).toBe('Not checked'); + expect(result.updateTone).toBe('muted'); + }); + it('returns the danger branch when an updateStatus has a non-empty error', () => { const result = getDockerImageOperationalPresentation( image({ diff --git a/frontend-modern/src/features/docker/dockerImagePresentation.ts b/frontend-modern/src/features/docker/dockerImagePresentation.ts index 01ddf95cf..a5d56b0bc 100644 --- a/frontend-modern/src/features/docker/dockerImagePresentation.ts +++ b/frontend-modern/src/features/docker/dockerImagePresentation.ts @@ -36,6 +36,23 @@ const containerUsesImage = (container: Resource, tokens: ReadonlySet): b const resourceLabel = (resource: Resource): string => trimmed(resource.name) || trimmed(resource.displayName) || resource.id; +const isPulseManagedImage = (image: Resource): boolean => { + const references = [ + image.name, + image.displayName, + image.docker?.image, + ...(image.docker?.repoTags ?? []), + ].map((value) => trimmed(value).toLowerCase()); + return references.some((reference) => + ['license.pulserelay.pro/pulse-pro', 'registry.pulserelay.pro/pulse/pulse-pro'].some( + (repository) => + reference === repository || + reference.startsWith(`${repository}:`) || + reference.startsWith(`${repository}@`), + ), + ); +}; + const summarizeConsumers = (consumers: readonly Resource[], reportedCount: number): string => { if (consumers.length === 0) { if (reportedCount <= 0) return 'Unused'; @@ -62,6 +79,15 @@ export function getDockerImageOperationalPresentation( (state) => trimmed(state.error).length > 0 && !isContainerUpdatePinned(state), ); + if (consumerCount > 0 && isPulseManagedImage(image)) { + return { + consumerCount, + consumerSummary: summarizeConsumers(consumers, reportedCount), + updateLabel: 'Managed by Pulse', + updateDetail: 'Pulse checks this private image through its product update service.', + updateTone: 'muted', + }; + } if (failed) { return { consumerCount, diff --git a/internal/dockeragent/registry.go b/internal/dockeragent/registry.go index 64780d291..79b9e37aa 100644 --- a/internal/dockeragent/registry.go +++ b/internal/dockeragent/registry.go @@ -66,6 +66,21 @@ type ImageUpdateResult struct { Error string `json:"error,omitempty"` } +func isPulseManagedImageReference(image string) bool { + normalized := strings.ToLower(strings.TrimSpace(image)) + for _, repository := range []string{ + "license.pulserelay.pro/pulse-pro", + "registry.pulserelay.pro/pulse/pulse-pro", + } { + if normalized == repository || + strings.HasPrefix(normalized, repository+":") || + strings.HasPrefix(normalized, repository+"@") { + return true + } + } + return false +} + // NewRegistryChecker creates a new registry checker for the Docker / Podman module. func NewRegistryChecker(logger zerolog.Logger) *RegistryChecker { return newRegistryCheckerWithConfig(logger, true) @@ -151,6 +166,9 @@ func (r *RegistryChecker) CheckImageUpdate(ctx context.Context, image, currentDi if !r.Enabled() { return nil } + if isPulseManagedImageReference(image) { + return nil + } registry, repository, tag := parseImageReference(image) diff --git a/internal/dockeragent/registry_http_test.go b/internal/dockeragent/registry_http_test.go index 46e8d8718..4ce15e9d2 100644 --- a/internal/dockeragent/registry_http_test.go +++ b/internal/dockeragent/registry_http_test.go @@ -36,6 +36,26 @@ func TestRegistryChecker_CheckImageUpdate_Behavior(t *testing.T) { } }) + t.Run("Pulse private image uses the product update service", func(t *testing.T) { + checker := NewRegistryChecker(logger) + for _, image := range []string{ + "license.pulserelay.pro/pulse-pro:6.2.0-rc.4", + "license.pulserelay.pro/pulse-pro@sha256:abc123", + "registry.pulserelay.pro/pulse/pulse-pro:v6.0.5", + } { + if result := checker.CheckImageUpdate( + context.Background(), + image, + "sha256:current", + "", + "", + "", + ); result != nil { + t.Fatalf("CheckImageUpdate(%q) = %#v, want nil", image, result) + } + } + }) + t.Run("empty image name", func(t *testing.T) { checker := NewRegistryChecker(logger) checker.httpClient = &http.Client{