Fix image deletion: per-tag delete, repository delete, daily GC.

- Deleting a tag now untags only that tag; the manifest is removed only when
  no other tag references the same digest (previously deleting by digest also
  removed every sibling tag).
- Add repository deletion. The registry API cannot drop a repository from the
  catalog, so empty repositories stayed in the UI forever with nothing left to
  delete. The app now mounts registry storage and removes the repository's
  metadata directories after deleting its manifests.
- Run `registry garbage-collect` daily (GC_HOUR_UTC) so deleted blobs free disk.
- Disable Watchtower for the locally built app image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ysj7705andClaude Opus 5.5 committed 2026-10-05 12:31:16 +09:00
1 parent f3d8d4c8ea
commit 89c6cbb5ad
9 files changed
+258 -41

No files matched your search

+3 -21
View File
@@ -10,26 +10,8 @@ export default defineEventHandler(async (event) => {
await ensureBootstrap()
const body = bodySchema.parse(await readBody(event))
await requireDeletePermission(session, body.name)
if (session.role !== 'ADMIN') {
const user = await prisma.user.findUnique({
where: { id: session.id },
include: { acls: true },
})
if (!user) {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
}
const allowed = allowedActionsForRepo(user, body.name)
if (!allowed.includes('delete') && !allowed.includes('*')) {
throw createError({ statusCode: 403, statusMessage: 'Delete not allowed' })
}
}
const digest = await getManifestDigest(body.name, body.tag)
if (!digest) {
throw createError({ statusCode: 404, statusMessage: 'Tag not found' })
}
await deleteManifest(body.name, digest)
return { ok: true, name: body.name, tag: body.tag, digest }
const { digest, manifestDeleted } = await deleteTag(body.name, body.tag)
return { ok: true, name: body.name, tag: body.tag, digest, manifestDeleted }
})
@@ -0,0 +1,27 @@
import { z } from 'zod'
const bodySchema = z.object({
name: z.string().min(1),
})
export default defineEventHandler(async (event) => {
const session = await requireSession(event)
await ensureBootstrap()
const { name } = bodySchema.parse(await readBody(event))
await requireDeletePermission(session, name)
// Delete manifests through the API first so blobs become unreferenced for GC
const tags = await listTags(name)
const digests = new Set<string>()
for (const tag of tags) {
const digest = await getManifestDigest(name, tag)
if (digest) digests.add(digest)
}
for (const digest of digests) {
await deleteManifest(name, digest)
}
await removeRepositoryFromStorage(name)
return { ok: true, name, deletedTags: tags.length }
})