Route Pulse images through product update checks

This commit is contained in:
courtmanr@gmail.com 2026-08-11 15:38:42 +01:00
parent a49e016246
commit 791a2f86bf
5 changed files with 121 additions and 15 deletions

View file

@ -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."
]
}

View file

@ -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({

View file

@ -36,6 +36,23 @@ const containerUsesImage = (container: Resource, tokens: ReadonlySet<string>): 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,

View file

@ -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)

View file

@ -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{