release: r019 idempotent batch container updates

This commit is contained in:
Mikei386
2026-08-24 11:21:19 +02:00
parent 47c27b06c4
commit d90874ed18
11 changed files with 174 additions and 14 deletions
+110 -1
View File
@@ -8,6 +8,7 @@
* create <template_name>
* modify <container> <field> <value>
* update <container>
* update-verified-batch <container1|container2|...>
* rebuild <container>
* ca-install <template_url> <container_name> <overrides_json> <start:true|false>
*
@@ -116,6 +117,109 @@ function rebuild_container(string $name, bool $pull_image = false, bool $force_s
out("Done: $Name");
}
function docker_inspect_value(string $format, string $name): string {
return docker_exec('inspect --format ' . escapeshellarg($format) . ' ' . escapeshellarg($name));
}
/**
* Pull and update several exact containers without trusting Unraid's cached
* update flag. The immutable image ID is the authority. A stale cache can
* therefore never cause a needless container recreation.
*/
function update_verified_batch(string $raw_names): void {
$names = array_values(array_unique(array_filter(array_map('trim', explode('|', $raw_names)))));
if (count($names) < 1 || count($names) > 25) {
fail('Expected 1-25 container names separated by |');
}
$results = [];
foreach ($names as $name) {
if (!preg_match('/^[a-zA-Z0-9._-]{1,128}$/', $name)) {
$results[] = ['container' => $name, 'result' => 'error', 'error' => 'invalid container name'];
continue;
}
try {
$tmpl = get_template_path($name);
[, $template_name, $repository] = xmlToCommand($tmpl);
if ($template_name !== $name) {
throw new RuntimeException("Template name mismatch: $template_name");
}
$before_id = docker_inspect_value('{{.Id}}', $name);
$before_image_id = docker_inspect_value('{{.Image}}', $name);
$before_state = docker_inspect_value('{{.State.Status}}', $name);
$was_running = docker_inspect_value('{{.State.Running}}', $name) === 'true';
$pull_output = docker_exec('pull ' . escapeshellarg($repository));
if (preg_match('/(?:^|\n)(?:Error response|error:|failed)/i', $pull_output)) {
throw new RuntimeException('Image pull failed: ' . substr($pull_output, 0, 500));
}
$pulled_image_id = docker_exec('image inspect --format ' . escapeshellarg('{{.Id}}') . ' ' . escapeshellarg($repository));
if ($pulled_image_id === '') {
throw new RuntimeException('Pulled image ID could not be resolved');
}
if ($before_image_id === $pulled_image_id) {
$results[] = [
'container' => $name,
'result' => 'already-current',
'recreated' => false,
'original_state' => $before_state,
'final_state' => $before_state,
'container_id_changed' => false,
'image_id_changed' => false,
];
continue;
}
ob_start();
rebuild_container($name, false, false);
ob_end_clean();
$after_id = docker_inspect_value('{{.Id}}', $name);
$after_image_id = docker_inspect_value('{{.Image}}', $name);
$after_state = docker_inspect_value('{{.State.Status}}', $name);
$restart_count = (int)docker_inspect_value('{{.RestartCount}}', $name);
$health = docker_inspect_value('{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}', $name);
$state_preserved = $was_running ? $after_state === 'running' : $after_state !== 'running';
$results[] = [
'container' => $name,
'result' => ($after_image_id === $pulled_image_id && $state_preserved) ? 'updated' : 'verification-failed',
'recreated' => true,
'original_state' => $before_state,
'final_state' => $after_state,
'state_preserved' => $state_preserved,
'container_id_changed' => $before_id !== $after_id,
'image_id_changed' => $before_image_id !== $after_image_id,
'restart_count' => $restart_count,
'health' => $health,
];
} catch (Throwable $error) {
if (ob_get_level() > 0) ob_end_clean();
$results[] = [
'container' => $name,
'result' => 'error',
'error' => substr($error->getMessage(), 0, 600),
];
}
}
$counts = [];
foreach ($results as $result) {
$key = (string)$result['result'];
$counts[$key] = ($counts[$key] ?? 0) + 1;
}
echo json_encode([
'schema_version' => '1.0',
'requested_count' => count($names),
'result_counts' => $counts,
'all_verified' => !isset($counts['error']) && !isset($counts['verification-failed']),
'containers' => $results,
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n";
}
/**
* Set the text content of a DOM element.
* This is the actual value Unraid uses (not the Default attribute).
@@ -354,6 +458,11 @@ switch ($action) {
rebuild_container($arg1, true);
break;
case 'update-verified-batch':
if (!$arg1) fail('Missing container names');
update_verified_batch($arg1);
break;
case 'rebuild':
if (!$arg1) fail("Missing container name");
out("Rebuilding container: $arg1");
@@ -366,5 +475,5 @@ switch ($action) {
break;
default:
fail("Unknown action: $action. Usage: create|modify|update|rebuild|ca-install");
fail("Unknown action: $action. Usage: create|modify|update|update-verified-batch|rebuild|ca-install");
}