Skip to content

feat(image-cleanup): event-driven cleanup of orphaned ExApp Docker images via HaRP#865

Draft
oleksandr-nc wants to merge 1 commit into
mainfrom
feat/exapp-image-cleanup
Draft

feat(image-cleanup): event-driven cleanup of orphaned ExApp Docker images via HaRP#865
oleksandr-nc wants to merge 1 commit into
mainfrom
feat/exapp-image-cleanup

Conversation

@oleksandr-nc
Copy link
Copy Markdown
Contributor

@oleksandr-nc oleksandr-nc commented May 11, 2026

Adds automatic cleanup of ExApp Docker images orphaned by uninstall or update, scheduled per-event via a one-shot QueuedJob with a configurable grace period (default 24h). Docker's own 409 response handles the "still in use" case, so no AppAPI-side state tracks image references.

HaRP-only by design; direct Docker support is intentionally not implemented since it's being removed in NC35.

Per-call CLI overrides on app:remove (--purge-now / --keep-image) and app:update (--purge-old-image-now / --keep-old-image). Admin settings expose a master toggle and the grace period (0–720 hours).

Targets Nextcloud 35.

Companion HaRP endpoint already landed in nextcloud/HaRP#102.


A few non-obvious choices, documented for future reference:

1. Per-event one-shot job vs periodic prune

Each orphan event (uninstall, update) schedules a QueuedJob that fires after the grace period, deletes the image, then removes itself from the queue.

  • vs. periodic dangling=true prune (the original approach in feat(IS-667): Add automatic cleanup of outdated ExApp Docker images #837): wrong scope (only catches already-untagged images), no per-event control, can't honor a per-uninstall override.
  • vs. periodic per-repo scan: would need AppAPI to track known repos persistently; harder to reason about per-event grace; periodic CPU cost.

2. Live Docker query for image ref vs AppAPI-side state

At hook time AppAPI asks HaRP for the container's Image field via the extended /docker/exapp/exists response (added in nextcloud/HaRP#102).

  • vs. current_image_ref column on ex_apps: schema migration, duplicates Docker's state, drift risk if a container is rebuilt out-of-band.
  • vs. AppConfig JSON list of orphans: another piece of state that can rot; concurrency footgun.
  • vs. re-fetch appinfo from the app store at uninstall: network call from a critical path, can fail offline / when an app is removed from the store, and the manifest may have changed since pull.

3. "In use" check delegated to Docker

The cleanup job just calls DELETE; Docker returns 409 if the image is still referenced by any container (running or stopped). No AppAPI-side refcount.

  • vs. AppAPI tracking which ExApps use what image: duplicates Docker's atomic refcount, opens a TOCTOU window between check and delete.

4. HaRP-only by design

The hooks short-circuit for non-HaRP Docker daemons. Direct Docker (and DockerSocketProxy) are being removed in NC35; carrying a parallel cleanup path would be code we'd delete in a few months. Existing direct-Docker users keep working, just without auto-cleanup until they migrate to HaRP.

Other choices worth noting

  • bytes_freed accuracy (caught in review of HaRP#102): parse Docker's DELETE response, only count the inspected size when at least one entry has a Deleted key. A multi-tag delete that only untags reports bytes_freed: 0 instead of lying.
  • Uninstall dialog deferred to a follow-up: the "Remove" button lives in nc-server's apps/settings/, not this repo, so the dropdown + "don't ask again" UX needs a coordinated server PR. The HTTP endpoint here already accepts an imageCleanup request param so the future UI PR can wire to it without back-end changes.

Screenshots

TODO

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b78e9d89-8d8c-46b3-b95e-51cdee7adba0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Comment @coderabbitai help to get the list of available commands and usage tips.

…ages via HaRP

Signed-off-by: Oleksander Piskun <oleksandr2088@icloud.com>
@oleksandr-nc oleksandr-nc force-pushed the feat/exapp-image-cleanup branch from ab3e6fe to 2812588 Compare May 11, 2026 08:35
@kyteinsky kyteinsky requested a review from Copilot May 11, 2026 10:51
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Implements event-driven cleanup of orphaned ExApp Docker images (HaRP-only) after uninstall/update, with admin-configurable enablement + grace period and per-command overrides.

Changes:

  • Add ExAppImageCleanupService + OrphanedImageCleanupJob to capture image refs and delete images immediately or via grace-period queued jobs.
  • Add admin settings + typed config persistence for image_cleanup_enabled and image_cleanup_grace_hours.
  • Add unit tests for scheduling/job behavior and an end-to-end test validating HaRP/Docker behavior (including multi-tag bytes_freed semantics).

Reviewed changes

Copilot reviewed 15 out of 17 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/test_image_cleanup.py New E2E test covering uninstall/update image cleanup scenarios via HaRP + Docker CLI.
tests/php/Service/ExAppImageCleanupServiceTest.php Unit tests for capture/schedule behavior and grace clamping.
tests/php/Controller/ExAppsPageControllerTest.php Updates controller test wiring for new cleanup service dependency.
tests/php/BackgroundJob/OrphanedImageCleanupJobTest.php Unit tests for the queued cleanup job behavior and logging paths.
src/components/AdminSettings.vue Adds admin UI for cleanup toggle and grace period input.
lib/Settings/Admin.php Exposes cleanup config values to the admin settings template.
lib/Service/ExAppImageCleanupService.php New service to capture refs, schedule/purge cleanup, and cancel pending jobs on daemon removal.
lib/Service/DaemonConfigService.php Cancels pending cleanup jobs when unregistering a daemon.
lib/DeployActions/DockerActions.php Adds HaRP-only removeImage() and getRunningImageRef() helpers.
lib/Controller/ExAppsPageController.php Captures image ref on uninstall and schedules cleanup based on request choice.
lib/Controller/ConfigController.php Persists admin config using bool/int/string setters based on value type.
lib/Command/ExApp/Update.php Adds options to purge/keep old image on update and schedules cleanup post-success.
lib/Command/ExApp/Unregister.php Adds options to purge/keep image on uninstall and schedules cleanup once container removed.
lib/BackgroundJob/OrphanedImageCleanupJob.php New queued job that attempts image deletion and logs outcomes (409/in use, not found, etc.).
lib/AppInfo/Application.php Adds config keys and defaults for image cleanup.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +155 to +161
onGraceHoursInput(value) {
delay(() => {
const clamped = Math.min(720, Math.max(0, parseInt(value, 10) || 0))
this.state.image_cleanup_grace_hours = clamped
this.saveOptions({ image_cleanup_grace_hours: clamped })
}, 2000)()
},
if (!is_array($arg) || ($arg['daemon_id'] ?? null) !== $daemonName) {
continue;
}
$this->jobList->removeById((string)$row['id']);
Comment on lines +142 to +145
try:
occ("app_api:app:unregister", APP_ID, "--silent", "--force", "--purge-now", check=False)
except CalledProcessError:
pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants