From bd8ae6825602309b3aeb2297882f2e6a024a10b1 Mon Sep 17 00:00:00 2001 From: Pila Date: Fri, 9 May 2025 11:42:16 +0200 Subject: [PATCH 1/3] Implement background job to prune stale docker images --- appinfo/info.xml | 1 + lib/BackgroundJob/DockerImageCleanupJob.php | 110 ++++++++++++++++++++ lib/DeployActions/DockerActions.php | 41 ++++++++ 3 files changed, 152 insertions(+) create mode 100644 lib/BackgroundJob/DockerImageCleanupJob.php diff --git a/appinfo/info.xml b/appinfo/info.xml index ec1c2edd..73d83825 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 00000000..425b91ac --- /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 09e7b5dd..8841515d 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())) { From 4e8a0a701a4cc283f035e980a0bf1cfe0b37c413 Mon Sep 17 00:00:00 2001 From: Pila Date: Mon, 12 May 2025 14:28:19 +0200 Subject: [PATCH 2/3] Implement background job to prune stale docker images --- InterviewTask.md | 29 +++++++++++++++++++++ lib/BackgroundJob/DockerImageCleanupJob.php | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 InterviewTask.md diff --git a/InterviewTask.md b/InterviewTask.md new file mode 100644 index 00000000..3daf6fc7 --- /dev/null +++ b/InterviewTask.md @@ -0,0 +1,29 @@ +Interview Task Report https://github.com/nextcloud/app_api/issues/519 + +From my understanding, the app_api codebase is mainly a devOps tool used for managing other applications + + +I've struggled with getting the app up and running. I followed the documentation here: https://docs.nextcloud.com/server/latest/developer_manual/exapp_development/index.html + +While following the documentation, I couldn't find the script that this command executes: `./occ app:enable --force app_api` Perhaps this is in the `server` codebase instead? if that's the case, it isn't clear in this documentation + +I However, did manage to get the environment up and running at http://nextcloud.local using the `docker-socket-proxy` repository. + +### Solution + +It took my quite some time to understand the codebase, but I managed to have a fair idea of it + +- In the `lib` folder, I introduced the following method in... + + - `/lib/DeployActions/DockerActions.php` The aim is to call docker's `/images/prune` API + +Next, I implemented the following + +- `lib/BackgroundJob/DockerImageCleanupJob.php` + +With these in place, I am struggling to understand how they tie into the entire nextcloud ecosystem and how to actually debug the code to see it in action. + +this is because with the development environment running, and after I login, I couldn't access `http://nextcloud.local/index.php/settings/admin/app_api` due to permission issues. + +With this, exploration, I need help with some more concrete walkthrough of the codebases and how they work together and to understand how to properly introduce my changes and test them. + diff --git a/lib/BackgroundJob/DockerImageCleanupJob.php b/lib/BackgroundJob/DockerImageCleanupJob.php index 425b91ac..a1af3adf 100644 --- a/lib/BackgroundJob/DockerImageCleanupJob.php +++ b/lib/BackgroundJob/DockerImageCleanupJob.php @@ -3,7 +3,7 @@ declare(strict_types=1); /** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ From 1834acad92bf1c25113f861bc19df1987077a3a6 Mon Sep 17 00:00:00 2001 From: Pila Date: Mon, 12 May 2025 14:32:45 +0200 Subject: [PATCH 3/3] Delete report file --- InterviewTask.md | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 InterviewTask.md diff --git a/InterviewTask.md b/InterviewTask.md deleted file mode 100644 index 3daf6fc7..00000000 --- a/InterviewTask.md +++ /dev/null @@ -1,29 +0,0 @@ -Interview Task Report https://github.com/nextcloud/app_api/issues/519 - -From my understanding, the app_api codebase is mainly a devOps tool used for managing other applications - - -I've struggled with getting the app up and running. I followed the documentation here: https://docs.nextcloud.com/server/latest/developer_manual/exapp_development/index.html - -While following the documentation, I couldn't find the script that this command executes: `./occ app:enable --force app_api` Perhaps this is in the `server` codebase instead? if that's the case, it isn't clear in this documentation - -I However, did manage to get the environment up and running at http://nextcloud.local using the `docker-socket-proxy` repository. - -### Solution - -It took my quite some time to understand the codebase, but I managed to have a fair idea of it - -- In the `lib` folder, I introduced the following method in... - - - `/lib/DeployActions/DockerActions.php` The aim is to call docker's `/images/prune` API - -Next, I implemented the following - -- `lib/BackgroundJob/DockerImageCleanupJob.php` - -With these in place, I am struggling to understand how they tie into the entire nextcloud ecosystem and how to actually debug the code to see it in action. - -this is because with the development environment running, and after I login, I couldn't access `http://nextcloud.local/index.php/settings/admin/app_api` due to permission issues. - -With this, exploration, I need help with some more concrete walkthrough of the codebases and how they work together and to understand how to properly introduce my changes and test them. -