mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-05-16 00:11:35 +00:00
Add Docker update support via Watchtower integration (#1330)
* Add Docker update support via Watchtower integration Add web-based Docker container updates using Watchtower HTTP API. When configured with WATCHTOWER_API_URL and WATCHTOWER_API_TOKEN environment variables, administrators can trigger container updates from the Update Manager page. Features: - WatchtowerClient service for Watchtower HTTP API communication - Docker update progress page with animated Docker whale logo - Real-time step tracking: Trigger, Pull, Stop, Restart, Health Check, Verify - CSP-compatible progress bar using CSS classes - Translated UI strings via Stimulus values - Health endpoint polling to detect container restart - Watchtower setup documentation for Docker installations - WatchtowerClient made nullable for non-Docker installations - Unit tests for WatchtowerClient * Fixed translation message IDs * Switch Watchtower docs to maintained nicholas-fedor fork The original containrrr/watchtower is no longer maintained (last release Nov 2023). Point users to the drop-in compatible active fork and add an info note explaining why. No code changes — the HTTP API is identical, so WatchtowerClient works against either image. * Fixed exception when github is not reachable * Only show version string in health endpoint, when user has permissions * Do not expose watchtower API port in example docker-compose file * Show if updates, backup restore and backup download are allowed in update manager page * Report 'not authorized' for version in health endpoint if user lacks permission --------- Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
This commit is contained in:
parent
91bf8371ad
commit
d346708150
14 changed files with 1577 additions and 55 deletions
|
|
@ -46,7 +46,7 @@ enum InstallationType: string
|
|||
{
|
||||
return match ($this) {
|
||||
self::GIT => true,
|
||||
self::DOCKER => false,
|
||||
self::DOCKER => true,
|
||||
// ZIP_RELEASE auto-update not yet implemented
|
||||
self::ZIP_RELEASE => false,
|
||||
self::UNKNOWN => false,
|
||||
|
|
@ -57,7 +57,7 @@ enum InstallationType: string
|
|||
{
|
||||
return match ($this) {
|
||||
self::GIT => 'Run: php bin/console partdb:update',
|
||||
self::DOCKER => 'Pull the new Docker image and recreate the container: docker-compose pull && docker-compose up -d',
|
||||
self::DOCKER => 'Configure Watchtower for one-click updates, or manually: docker-compose pull && docker-compose up -d',
|
||||
self::ZIP_RELEASE => 'Download the new release ZIP from GitHub, extract it over your installation, and run: php bin/console doctrine:migrations:migrate && php bin/console cache:clear',
|
||||
self::UNKNOWN => 'Unable to determine installation type. Please update manually.',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ class UpdateChecker
|
|||
private readonly InstallationTypeDetector $installationTypeDetector,
|
||||
private readonly GitVersionInfoProvider $gitVersionInfoProvider,
|
||||
#[Autowire(param: 'kernel.debug')] private readonly bool $is_dev_mode,
|
||||
#[Autowire(param: 'kernel.project_dir')] private readonly string $project_dir)
|
||||
#[Autowire(param: 'kernel.project_dir')] private readonly string $project_dir,
|
||||
private readonly ?WatchtowerClient $watchtowerClient = null)
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -284,8 +285,16 @@ class UpdateChecker
|
|||
$updateBlockers[] = 'local_changes';
|
||||
}
|
||||
|
||||
if ($installInfo['type'] === InstallationType::DOCKER) {
|
||||
$updateBlockers[] = 'docker_installation';
|
||||
// Docker installations require Watchtower for auto-update
|
||||
$watchtowerConfigured = $this->watchtowerClient !== null && $this->watchtowerClient->isConfigured();
|
||||
$watchtowerAvailable = $watchtowerConfigured && $this->watchtowerClient->isAvailable();
|
||||
|
||||
if ($installInfo['type'] === InstallationType::DOCKER && !$watchtowerConfigured) {
|
||||
$canAutoUpdate = false;
|
||||
$updateBlockers[] = 'docker_no_watchtower';
|
||||
} elseif ($installInfo['type'] === InstallationType::DOCKER && !$watchtowerAvailable) {
|
||||
$canAutoUpdate = false;
|
||||
$updateBlockers[] = 'docker_watchtower_unreachable';
|
||||
}
|
||||
|
||||
return [
|
||||
|
|
@ -301,6 +310,8 @@ class UpdateChecker
|
|||
'can_auto_update' => $canAutoUpdate,
|
||||
'update_blockers' => $updateBlockers,
|
||||
'check_enabled' => $this->privacySettings->checkForUpdates,
|
||||
'watchtower_configured' => $watchtowerConfigured,
|
||||
'watchtower_available' => $watchtowerAvailable,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -299,6 +299,23 @@ class UpdateExecutor
|
|||
);
|
||||
}
|
||||
|
||||
// Docker installations are updated via Watchtower - skip Git/Composer/Yarn checks
|
||||
if ($installType === InstallationType::DOCKER) {
|
||||
// Only check if already locked
|
||||
if ($this->isLocked()) {
|
||||
$lockInfo = $this->getLockInfo();
|
||||
$errors[] = sprintf(
|
||||
'An update is already in progress (started at %s).',
|
||||
$lockInfo['started_at'] ?? 'unknown time'
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'valid' => empty($errors),
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
|
||||
// Check for Git installation
|
||||
if ($installType === InstallationType::GIT) {
|
||||
// Check if git is available
|
||||
|
|
|
|||
125
src/Services/System/WatchtowerClient.php
Normal file
125
src/Services/System/WatchtowerClient.php
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\System;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
* HTTP client for communicating with the Watchtower container updater API.
|
||||
* Used to trigger Docker container updates from the Part-DB UI.
|
||||
*
|
||||
* @see https://containrrr.dev/watchtower/
|
||||
*/
|
||||
readonly class WatchtowerClient
|
||||
{
|
||||
public function __construct(
|
||||
private HttpClientInterface $httpClient,
|
||||
private LoggerInterface $logger,
|
||||
#[Autowire(env: 'WATCHTOWER_API_URL')] private string $apiUrl,
|
||||
#[Autowire(env: 'WATCHTOWER_API_TOKEN')] private string $apiToken,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether Watchtower integration is configured (URL and token are set).
|
||||
*/
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return $this->apiUrl !== '' && $this->apiToken !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Watchtower API is reachable.
|
||||
* Makes a lightweight HTTP request with a short timeout.
|
||||
*/
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
if (!$this->isConfigured()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->httpClient->request('GET', $this->getUpdateEndpoint(), [
|
||||
'headers' => $this->getAuthHeaders(),
|
||||
'timeout' => 3,
|
||||
]);
|
||||
|
||||
// Any response means Watchtower is reachable
|
||||
$statusCode = $response->getStatusCode();
|
||||
return $statusCode < 500;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->debug('Watchtower availability check failed: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a container update via the Watchtower HTTP API.
|
||||
* This is fire-and-forget: Watchtower will pull the new image and restart the container.
|
||||
*
|
||||
* @return bool True if Watchtower accepted the update request
|
||||
*/
|
||||
public function triggerUpdate(): bool
|
||||
{
|
||||
if (!$this->isConfigured()) {
|
||||
throw new \RuntimeException('Watchtower is not configured. Set WATCHTOWER_API_URL and WATCHTOWER_API_TOKEN.');
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->httpClient->request('POST', $this->getUpdateEndpoint(), [
|
||||
'headers' => $this->getAuthHeaders(),
|
||||
'timeout' => 10,
|
||||
]);
|
||||
|
||||
$statusCode = $response->getStatusCode();
|
||||
|
||||
if ($statusCode >= 200 && $statusCode < 300) {
|
||||
$this->logger->info('Watchtower update triggered successfully.');
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->logger->error('Watchtower update request returned HTTP ' . $statusCode);
|
||||
return false;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('Failed to trigger Watchtower update: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function getUpdateEndpoint(): string
|
||||
{
|
||||
return rtrim($this->apiUrl, '/') . '/v1/update';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function getAuthHeaders(): array
|
||||
{
|
||||
return [
|
||||
'Authorization' => 'Bearer ' . $this->apiToken,
|
||||
];
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue