keep specific images from being pruned with a dockhand.prune=false label
This commit is contained in:
3 files changed
+71
-6
No files matched your search
@@ -15,6 +15,7 @@ import * as tls from 'node:tls';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { pumpWebStreamToWritable } from './stream-pump';
|
||||
import { toWebReadableStream } from './node-readable-stream';
|
||||
import { buildImagePruneFilters } from './image-prune-core';
|
||||
import { computeRequestTimeoutMs } from './backups/request-timeout';
|
||||
import { helperWaitDeadline } from './helper-wait-core';
|
||||
import type { Environment } from './db';
|
||||
@@ -4605,19 +4606,21 @@ export async function pruneImages(dangling = true, envId?: number | null) {
|
||||
// scanner images are always tagged so they can't be dangling anyway.
|
||||
if (dangling) {
|
||||
return dockerJsonRequest(
|
||||
`/images/prune?filters=${encodeURIComponent('{"dangling":["true"]}')}`,
|
||||
`/images/prune?filters=${buildImagePruneFilters(true)}`,
|
||||
{ method: 'POST' },
|
||||
envId
|
||||
);
|
||||
}
|
||||
|
||||
// dangling=false: "prune all unused." When the scanner-protection setting
|
||||
// is on, shield grype + trivy with stopped holder containers (Docker's
|
||||
// "in use" check keeps them) then tear them down in a finally (#625).
|
||||
// dangling=false: "prune all unused." The label filter makes Docker skip any
|
||||
// image tagged dockhand.prune=false (#1391). When the scanner-protection
|
||||
// setting is on, ALSO shield grype + trivy + backup helper with stopped
|
||||
// holder containers (Docker's "in use" check keeps them) then tear them down
|
||||
// in a finally (#625). Scanner images are third-party so we can't label them.
|
||||
const protect = (await getSetting('protect_scanner_images')) !== false;
|
||||
if (!protect) {
|
||||
return dockerJsonRequest(
|
||||
`/images/prune?filters=${encodeURIComponent('{"dangling":["false"]}')}`,
|
||||
`/images/prune?filters=${buildImagePruneFilters(false)}`,
|
||||
{ method: 'POST' },
|
||||
envId
|
||||
);
|
||||
@@ -4652,7 +4655,7 @@ export async function pruneImages(dangling = true, envId?: number | null) {
|
||||
|
||||
try {
|
||||
return await dockerJsonRequest(
|
||||
`/images/prune?filters=${encodeURIComponent('{"dangling":["false"]}')}`,
|
||||
`/images/prune?filters=${buildImagePruneFilters(false)}`,
|
||||
{ method: 'POST' },
|
||||
envId
|
||||
);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Pure helpers for image pruning. Kept dependency-free (no db / better-sqlite3)
|
||||
// so they can be unit-tested directly, mirroring volume-prune-core.ts.
|
||||
|
||||
/**
|
||||
* Image label that opts an image out of an "all unused" prune. Tag an image
|
||||
* with `dockhand.prune=false` (e.g. `docker build --label dockhand.prune=false`)
|
||||
* and Docker's own prune skips it - no holder container needed.
|
||||
*/
|
||||
export const PRUNE_KEEP_LABEL = 'dockhand.prune=false';
|
||||
|
||||
/**
|
||||
* Build the `filters=` query value for a Docker `/images/prune` call.
|
||||
*
|
||||
* @param dangling true = only untagged images (keep-label does not apply, since
|
||||
* dangling images have no labels of interest); false = all unused, and images
|
||||
* carrying PRUNE_KEEP_LABEL are excluded.
|
||||
*
|
||||
* IMPORTANT: over the Docker ENGINE API the label negation is a SEPARATE filter
|
||||
* key `label!` (`{"label!":["dockhand.prune=false"]}`). The `label!=value` form
|
||||
* is the docker CLI surface and is a no-op over the API - verified against a
|
||||
* live daemon (both Docker 27 and 20.10 / API 1.41). A plain `label` key would
|
||||
* invert the meaning (prune ONLY matching images).
|
||||
*/
|
||||
export function buildImagePruneFilters(dangling: boolean): string {
|
||||
const filters: Record<string, string[]> = dangling
|
||||
? { dangling: ['true'] }
|
||||
: { dangling: ['false'], 'label!': [PRUNE_KEEP_LABEL] };
|
||||
return encodeURIComponent(JSON.stringify(filters));
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// @ts-expect-error -- bun:test is a runtime built-in with no types installed
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { buildImagePruneFilters, PRUNE_KEEP_LABEL } from '../src/lib/server/image-prune-core';
|
||||
|
||||
function decode(filters: string): Record<string, string[]> {
|
||||
return JSON.parse(decodeURIComponent(filters));
|
||||
}
|
||||
|
||||
describe('buildImagePruneFilters', () => {
|
||||
test('dangling=true has no label filter (untagged images have no keep label)', () => {
|
||||
expect(decode(buildImagePruneFilters(true))).toEqual({ dangling: ['true'] });
|
||||
});
|
||||
|
||||
test('dangling=false excludes keep-labelled images via the label! key', () => {
|
||||
// The negation MUST be the separate `label!` key, not a `!=` value prefix -
|
||||
// the API ignores the `!=` form (verified against a live daemon).
|
||||
expect(decode(buildImagePruneFilters(false))).toEqual({
|
||||
dangling: ['false'],
|
||||
'label!': ['dockhand.prune=false']
|
||||
});
|
||||
});
|
||||
|
||||
test('does NOT use a plain "label" key (which would invert the meaning)', () => {
|
||||
const f = decode(buildImagePruneFilters(false));
|
||||
expect(f['label']).toBeUndefined();
|
||||
expect(f['label!']).toEqual([PRUNE_KEEP_LABEL]);
|
||||
});
|
||||
|
||||
test('the produced value is URL-encoded', () => {
|
||||
expect(buildImagePruneFilters(false)).not.toContain('{');
|
||||
expect(buildImagePruneFilters(false)).toContain('%7B');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user