diff --git a/appinfo/info.xml b/appinfo/info.xml index ec1c2edde..73d838257 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -62,6 +62,7 @@ to join us in shaping a more versatile, stable, and secure app landscape. OCA\AppAPI\BackgroundJob\ExAppInitStatusCheckJob + OCA\AppAPI\BackgroundJob\DockerImageCleanupJob diff --git a/lib/BackgroundJob/DockerImageCleanupJob.php b/lib/BackgroundJob/DockerImageCleanupJob.php new file mode 100644 index 000000000..a1af3adf1 --- /dev/null +++ b/lib/BackgroundJob/DockerImageCleanupJob.php @@ -0,0 +1,110 @@ +appConfig->getValueString('app_api', 'docker_cleanup_interval_days', (string)self::DEFAULT_INTERVAL_DAYS); + + // If interval is 0, job is disabled + if ($intervalDays === 0) { + $this->setInterval(0); + return; + } + + // Convert days to seconds for the job interval + $this->setInterval($intervalDays * self::SECONDS_IN_DAY); + } + + protected function run($argument): void + { + // Check if cleanup is enabled + $enabled = $this->appConfig->getValueString('app_api', 'docker_cleanup_enabled', 'yes'); + if ($enabled !== 'yes') { + $this->logger->debug('Docker image cleanup is disabled'); + return; + } + + $this->logger->info('Starting Docker image cleanup job'); + + try { + // Get cleanup filters from config + $filters = []; + + // Handle dangling images filter + $pruneDangling = $this->appConfig->getValueString('app_api', 'docker_cleanup_dangling', 'yes'); + if ($pruneDangling === 'yes') { + $filters['dangling'] = true; + } + + // Handle until timestamp filter + $pruneUntil = $this->appConfig->getValueString('app_api', 'docker_cleanup_until', ''); + if ($pruneUntil !== '') { + $filters['until'] = $pruneUntil; + } + + // Handle label filters + $pruneLabels = $this->appConfig->getValueString('app_api', 'docker_cleanup_labels', ''); + if ($pruneLabels !== '') { + $labels = json_decode($pruneLabels, true); + if (is_array($labels)) { + $filters['label'] = $labels; + } + } + + $defaultDaemonConfigName = $this->appConfig->getValueString('app_api', 'default_daemon_config', lazy: true); + $daemonConfig = $this->daemonConfigService->getDaemonConfigByName($defaultDaemonConfigName); + + $dockerUrl = $this->dockerActions->buildDockerUrl($daemonConfig); + $this->dockerActions->initGuzzleClient($daemonConfig); + + $result = $this->dockerActions->pruneImages($dockerUrl, $filters); + + if (empty($result['imagesDeleted'])) { + $this->logger->info(sprintf('No unused Docker images found for daemon %s', $daemonConfig->getName())); + return; + } + + $this->logger->info( + sprintf( + 'Successfully pruned %d Docker images from daemon %s, reclaimed %d bytes', + count($result['imagesDeleted']), + $daemonConfig->getName(), + $result['spaceReclaimed'] + ) + ); + + } catch (\Exception $e) { + $this->logger->error('Error during Docker image cleanup: ' . $e->getMessage()); + } + } + +} diff --git a/lib/DeployActions/DockerActions.php b/lib/DeployActions/DockerActions.php index 09e7b5dd5..8841515d1 100644 --- a/lib/DeployActions/DockerActions.php +++ b/lib/DeployActions/DockerActions.php @@ -937,6 +937,47 @@ public function buildDockerUrl(DaemonConfig $daemonConfig): string { return $url; } + /** + * Prune unused Docker images + * + * @param string $dockerUrl Docker daemon URL + * @param array $filters Optional filters to apply: + * - dangling: bool - When true, prune only unused and untagged images + * - until: string - Prune images created before this timestamp + * - label: array - Prune images with specified labels + * @return array{imagesDeleted: array, spaceReclaimed: int} Result of the prune operation + */ + public function pruneImages(string $dockerUrl, array $filters = []): array { + try { + $url = $this->buildApiUrl($dockerUrl, 'images/prune'); + if (!empty($filters)) { + $url .= '?' . http_build_query(['filters' => json_encode($filters)]); + } + + $response = $this->guzzleClient->post($url); + $result = json_decode($response->getBody()->getContents(), true); + + $this->logger->info( + sprintf( + 'Pruned %d Docker images, reclaimed %d bytes', + count($result['ImagesDeleted'] ?? []), + $result['SpaceReclaimed'] ?? 0 + ) + ); + + return [ + 'imagesDeleted' => $result['ImagesDeleted'] ?? [], + 'spaceReclaimed' => $result['SpaceReclaimed'] ?? 0 + ]; + } catch (GuzzleException $e) { + $this->logger->error('Error pruning Docker images: ' . $e->getMessage()); + return [ + 'imagesDeleted' => [], + 'spaceReclaimed' => 0 + ]; + } + } + public function initGuzzleClient(DaemonConfig $daemonConfig): void { $guzzleParams = []; if ($this->isLocalSocket($daemonConfig->getHost())) {