release: r016 fix Docker update cache matching
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/unraid-mcp-status-helper.php';
|
||||
|
||||
function expect_same(string $label, mixed $expected, mixed $actual): void {
|
||||
if ($expected === $actual) {
|
||||
echo "PASS: $label\n";
|
||||
return;
|
||||
}
|
||||
fwrite(STDERR, "FAIL: $label\nExpected: " . json_encode($expected) . "\nActual: " . json_encode($actual) . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$native = [
|
||||
'Windows11' => 'false',
|
||||
'WindowsXP' => false,
|
||||
'AirConnect' => ['status' => 'true'],
|
||||
];
|
||||
expect_same('native container-name key', 'update-available', docker_update_state($native, 'Windows11', 'dockurr/windows'));
|
||||
expect_same('boolean false is an update', 'update-available', docker_update_state($native, 'WindowsXP', 'dockurr/windows'));
|
||||
expect_same('object status is understood', 'up-to-date', docker_update_state($native, 'AirConnect', '1activegeek/airconnect:latest'));
|
||||
|
||||
$legacy = [
|
||||
'library/redis:latest' => ['status' => 'false'],
|
||||
'dockurr/windows:latest' => ['updated' => true],
|
||||
];
|
||||
expect_same('Docker Hub library alias', 'update-available', docker_update_state($legacy, 'Redis', 'redis'));
|
||||
expect_same('implicit latest tag', 'up-to-date', docker_update_state($legacy, 'Windows11', 'dockurr/windows'));
|
||||
|
||||
$list = [
|
||||
['container' => 'Virtual-DSM', 'image' => 'vdsm/virtual-dsm:latest', 'updateAvailable' => true],
|
||||
];
|
||||
expect_same('list format with updateAvailable', 'update-available', docker_update_state($list, 'Virtual-DSM', 'vdsm/virtual-dsm:latest'));
|
||||
|
||||
$summary = docker_update_status_from(
|
||||
['Windows11' => 'false', 'AirConnect' => 'true'],
|
||||
['Windows11|dockurr/windows', 'AirConnect|1activegeek/airconnect:latest', 'Mystery|example/mystery'],
|
||||
);
|
||||
expect_same('known update count', 1, $summary['updates_available']);
|
||||
expect_same('unknown count', 1, $summary['unknown_count']);
|
||||
expect_same('incomplete cache is explicit', false, $summary['status_complete']);
|
||||
expect_same('reported count marked as minimum', true, $summary['updates_available_is_minimum']);
|
||||
|
||||
echo "All Docker update cache tests passed.\n";
|
||||
@@ -234,36 +234,129 @@ function shares_list(?string $requested): array {
|
||||
return ['schema_version' => '1.0', 'share_count' => count($items), 'shares' => $items];
|
||||
}
|
||||
|
||||
function docker_update_state_value(mixed $value): ?string {
|
||||
if (is_array($value)) {
|
||||
foreach (['update_available', 'updateAvailable'] as $field) {
|
||||
if (!array_key_exists($field, $value)) continue;
|
||||
$flag = $value[$field];
|
||||
if (is_bool($flag)) return $flag ? 'update-available' : 'up-to-date';
|
||||
$flag = strtolower(trim((string)$flag));
|
||||
if (in_array($flag, ['true', '1', 'yes'], true)) return 'update-available';
|
||||
if (in_array($flag, ['false', '0', 'no'], true)) return 'up-to-date';
|
||||
}
|
||||
foreach (['status', 'updated', 'update_status', 'updateStatus'] as $field) {
|
||||
if (array_key_exists($field, $value)) return docker_update_state_value($value[$field]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (is_bool($value)) return $value ? 'up-to-date' : 'update-available';
|
||||
if (!is_string($value) && !is_int($value)) return null;
|
||||
$state = strtolower(trim((string)$value));
|
||||
if (in_array($state, ['true', '1', 'up-to-date', 'up_to_date', 'current', 'updated'], true)) {
|
||||
return 'up-to-date';
|
||||
}
|
||||
if (in_array($state, ['false', '0', 'update-available', 'update_available', 'outdated'], true)) {
|
||||
return 'update-available';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function docker_image_aliases(string $image): array {
|
||||
$image = trim($image);
|
||||
$without_digest = preg_replace('/@sha256:[a-f0-9]+$/i', '', $image) ?? $image;
|
||||
$aliases = [$image, $without_digest];
|
||||
$last_part = basename($without_digest);
|
||||
if (!str_contains($last_part, ':')) $aliases[] = $without_digest . ':latest';
|
||||
if (!str_contains($without_digest, '/')) {
|
||||
$aliases[] = 'library/' . $without_digest;
|
||||
if (!str_contains($last_part, ':')) $aliases[] = 'library/' . $without_digest . ':latest';
|
||||
} elseif (str_starts_with($without_digest, 'library/')) {
|
||||
$aliases[] = substr($without_digest, strlen('library/'));
|
||||
}
|
||||
return array_values(array_unique(array_filter($aliases, fn($value) => $value !== '')));
|
||||
}
|
||||
|
||||
function docker_update_state(array $updates, string $name, string $image): ?string {
|
||||
// Unraid's native Docker manager stores the cache by container name.
|
||||
// Older/plugin-generated caches may instead use an image reference, so
|
||||
// keep those forms as compatibility fallbacks.
|
||||
$cache = isset($updates['containers']) && is_array($updates['containers'])
|
||||
? $updates['containers'] : $updates;
|
||||
$keys = array_values(array_unique(array_merge([$name], docker_image_aliases($image))));
|
||||
$key_lookup = [];
|
||||
foreach ($cache as $key => $_value) {
|
||||
if (is_string($key)) $key_lookup[strtolower($key)] = $key;
|
||||
}
|
||||
foreach ($keys as $candidate) {
|
||||
$actual = $key_lookup[strtolower($candidate)] ?? null;
|
||||
if ($actual === null) continue;
|
||||
$state = docker_update_state_value($cache[$actual]);
|
||||
if ($state !== null) return $state;
|
||||
}
|
||||
|
||||
// Also understand list/digest keyed formats that carry their identity in
|
||||
// the entry itself. This prevents a future Unraid cache format change from
|
||||
// turning every container into an unexplained `unknown` again.
|
||||
foreach ($cache as $entry) {
|
||||
if (!is_array($entry)) continue;
|
||||
$entry_name = (string)($entry['container'] ?? $entry['container_name'] ?? $entry['name'] ?? '');
|
||||
$entry_image = (string)($entry['image'] ?? $entry['repository'] ?? '');
|
||||
$name_matches = $entry_name !== '' && strcasecmp($entry_name, $name) === 0;
|
||||
$image_matches = $entry_image !== '' && in_array(
|
||||
strtolower($entry_image),
|
||||
array_map('strtolower', docker_image_aliases($image)),
|
||||
true,
|
||||
);
|
||||
if (!$name_matches && !$image_matches) continue;
|
||||
$state = docker_update_state_value($entry);
|
||||
if ($state !== null) return $state;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function docker_update_status_from(array $updates, array $rows): array {
|
||||
$items = [];
|
||||
foreach ($rows as $row) {
|
||||
[$name, $image] = array_pad(explode('|', $row, 2), 2, '');
|
||||
$state = docker_update_state($updates, $name, $image);
|
||||
$items[] = [
|
||||
'container' => $name,
|
||||
'image' => $image,
|
||||
'status' => $state ?? 'unknown',
|
||||
];
|
||||
}
|
||||
$available = count(array_filter($items, fn($x) => $x['status'] === 'update-available'));
|
||||
$unknown = count(array_filter($items, fn($x) => $x['status'] === 'unknown'));
|
||||
return [
|
||||
'schema_version' => '1.1',
|
||||
'container_count' => count($items),
|
||||
'updates_available' => $available,
|
||||
'unknown_count' => $unknown,
|
||||
'status_complete' => $unknown === 0,
|
||||
'updates_available_is_minimum' => $unknown > 0,
|
||||
'containers' => $items,
|
||||
];
|
||||
}
|
||||
|
||||
function docker_update_status(): array {
|
||||
$path = '/var/lib/docker/unraid-update-status.json';
|
||||
$updates = is_readable($path) ? json_decode((string)file_get_contents($path), true) : [];
|
||||
if (!is_array($updates)) $updates = [];
|
||||
$rows = [];
|
||||
exec('/usr/bin/docker ps -a --format ' . escapeshellarg('{{.Names}}|{{.Image}}'), $rows);
|
||||
$items = [];
|
||||
foreach ($rows as $row) {
|
||||
[$name, $image] = array_pad(explode('|', $row, 2), 2, '');
|
||||
$key = $image;
|
||||
if (!isset($updates[$key]) && !str_contains($key, '/')) $key = 'library/' . $key;
|
||||
$state = $updates[$key]['status'] ?? null;
|
||||
$items[] = [
|
||||
'container' => $name,
|
||||
'image' => $image,
|
||||
'status' => $state === 'true' ? 'up-to-date' : ($state === 'false' ? 'update-available' : 'unknown'),
|
||||
];
|
||||
}
|
||||
$available = count(array_filter($items, fn($x) => $x['status'] === 'update-available'));
|
||||
return ['schema_version' => '1.0', 'container_count' => count($items), 'updates_available' => $available, 'containers' => $items];
|
||||
return docker_update_status_from($updates, $rows);
|
||||
}
|
||||
|
||||
$action = $argv[1] ?? '';
|
||||
switch ($action) {
|
||||
case 'system-health': respond(system_health()); break;
|
||||
case 'storage-status': respond(storage_status()); break;
|
||||
case 'disk-health': respond(disk_health($argv[2] ?? null)); break;
|
||||
case 'notifications': respond(notification_list((int)($argv[2] ?? 20), $argv[3] ?? 'all')); break;
|
||||
case 'shares-list': respond(shares_list(null)); break;
|
||||
case 'share-inspect': respond(shares_list($argv[2] ?? '')); break;
|
||||
case 'docker-update-status': respond(docker_update_status()); break;
|
||||
default: fail_status('Unknown read-only status action');
|
||||
if (realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) {
|
||||
$action = $argv[1] ?? '';
|
||||
switch ($action) {
|
||||
case 'system-health': respond(system_health()); break;
|
||||
case 'storage-status': respond(storage_status()); break;
|
||||
case 'disk-health': respond(disk_health($argv[2] ?? null)); break;
|
||||
case 'notifications': respond(notification_list((int)($argv[2] ?? 20), $argv[3] ?? 'all')); break;
|
||||
case 'shares-list': respond(shares_list(null)); break;
|
||||
case 'share-inspect': respond(shares_list($argv[2] ?? '')); break;
|
||||
case 'docker-update-status': respond(docker_update_status()); break;
|
||||
default: fail_status('Unknown read-only status action');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user