mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-08-16 04:23:29 +00:00
Route Pulse images through product update checks
This commit is contained in:
parent
a49e016246
commit
791a2f86bf
5 changed files with 121 additions and 15 deletions
|
|
@ -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."
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue