ligthweight companion for registry/distribution
  • Go 96%
  • Makefile 1.3%
  • Just 1.3%
  • HTML 0.9%
  • Shell 0.2%
  • Other 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Oleksii 8daa62ac40
All checks were successful
CI / Test & Build (push) Successful in 2m25s
CI / Release image (push) Has been skipped
CI / Dev image (push) Successful in 1m30s
Merge pull request #58 from noroutine/slice/sealed-coverage
proof: sealed() witnesses cover — return makes them visible
2026-10-25 20:14:16 +01:00
.forgejo/workflows slice tails: adopt epoch cut, lease port, gc preview, Deps.Cfg removal, clock factory (#33) 2026-10-11 01:30:30 +02:00
.github build(deps): bump actions/download-artifact from 4 to 8 (#42) 2026-10-11 14:21:24 +02:00
cmd/app Wire mutation testing with gremlins, kill every survivor 2026-09-27 16:14:30 +02:00
contrib Bootstrap kpr: rename skeleton, refresh toolchain, add docs and dev stack 2026-09-27 14:05:14 +02:00
docs proof: sealed() witnesses cover — return makes them visible 2026-10-25 20:09:57 +01:00
internal proof: sealed() witnesses cover — return makes them visible 2026-10-25 20:09:57 +01:00
LICENSES Ship third-party license bundle in image 2026-10-04 16:35:59 +02:00
observability Unpublish registry debug port: internal-only, scraped over kpr-net 2026-10-03 03:00:45 +02:00
scripts Ship third-party license bundle in image 2026-10-04 16:35:59 +02:00
site Pin tag-side traversal containment (CodeQL follow-up) (#7) 2026-10-04 21:47:50 +02:00
test/e2e slice tails: adopt epoch cut, lease port, gc preview, Deps.Cfg removal, clock factory (#33) 2026-10-11 01:30:30 +02:00
.dockerignore Ship third-party license bundle in image 2026-10-04 16:35:59 +02:00
.gitignore Ship third-party license bundle in image 2026-10-04 16:35:59 +02:00
.golangci.yml proof: sealed() witnesses cover — return makes them visible 2026-10-25 20:09:57 +01:00
AGENTS.md AGENTS.md: releases ship by tag push, never hand-published 2026-10-12 03:01:58 +02:00
CODE_OF_CONDUCT.md Add community files (CoC, contributing+DCO, security, templates) (#8) 2026-10-04 22:08:02 +02:00
codecov.yml gate pull requests on codecov project + patch coverage 2026-10-04 20:25:09 +02:00
CONTRIBUTING.md Fix contribution license to GPLv3 (Apache text is dep attribution) (#9) 2026-10-04 22:12:20 +02:00
docker-compose.observability.yml De-spike observability, add stack targets 2026-09-27 17:44:01 +02:00
docker-compose.redis.yml Leave the receiver port unpublished in dev stacks 2026-10-12 02:10:57 +02:00
docker-compose.shadow.yml Unpublish registry debug port: internal-only, scraped over kpr-net 2026-10-03 03:00:45 +02:00
docker-compose.yml Leave the receiver port unpublished in dev stacks 2026-10-12 02:10:57 +02:00
Dockerfile Ship third-party license bundle in image 2026-10-04 16:35:59 +02:00
Dockerfile.package Ship third-party license bundle in image 2026-10-04 16:35:59 +02:00
go.mod Merge branch 'master' into dependabot/go_modules/go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc-0.23.0 2026-10-25 12:33:16 +01:00
go.sum Merge branch 'master' into dependabot/go_modules/go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc-0.23.0 2026-10-25 12:33:16 +01:00
justfile validate codecov.yml in CI via make/just targets 2026-10-04 20:26:50 +02:00
LICENSE License under GPLv3 2026-10-04 16:25:27 +02:00
license.go GPL banner on version/help/serve-startup, kpr license command (#10) 2026-10-04 22:25:12 +02:00
license_test.go GPL banner on version/help/serve-startup, kpr license command (#10) 2026-10-04 22:25:12 +02:00
Makefile Gather test support under testing umbrella 2026-10-10 00:51:19 +02:00
README.md Drop store status; cards carry every fact now 2026-10-11 18:29:11 +02:00
registry-config.file.yml Split /events onto its own receiver port (:9000) 2026-10-12 02:06:41 +02:00
registry-config.shadow.yml Enable registry upload purging: disk, not rows 2026-10-03 03:25:22 +02:00
registry-config.yml Split /events onto its own receiver port (:9000) 2026-10-12 02:06:41 +02:00
SECURITY.md Add community files (CoC, contributing+DCO, security, templates) (#8) 2026-10-04 22:08:02 +02:00

kpr (keeper)

Stand With Ukraine

CI codecov release go license muse

Push app:10m to your registry and the tag is gone ten minutes later.

kpr fronts a stock OCI distribution registry: ephemeral images and lightweight retention cleanup, without running Harbor or Nexus. Inspired by ttl.sh.

  • One binary. Goes in front of the registry you already run — no fork, no patches, stock distribution behind it.
  • One state backend. Plain files by default — no redis, no database, nothing to run. Set KPR_REDIS_ADDR to use redis instead. See STORES.
  • Policies are Go code, not a rule language. Simple, use-case driven, tuned by consts.

The cycle is two steps, and nothing runs on its own: kpr reap marks a due tag with a reason, and the sweeper in kpr serve deletes it by digest.

Design and current state live in ARCHITECTURE.

Docs

Full index, grouped by task: docs/

Shortcuts to the three most-asked-for pages:

Doc Answers
QUICKSTART fresh setup: two containers, one volume, first expiring tag
ADOPT_KPR bolting kpr onto a registry you already run
ARCHITECTURE components, policies, gc, data keys, surfaces

How it works

  1. docker push → the registry notifies the receiver in kpr serve, which records the row (repo/tag/digest/push time) in the state backend.
  2. kpr reap evaluates the policies and marks rows due with a reason. Dry-run unless --no-dry-run — unarmed, it only prints.
  3. The sweeper in kpr serve (tick backstop, sweep-on-start, or kpr sweep trigger) deletes due rows by digest and resolves them: deleted, gone, untracked, planned, failed, or skipped.
  4. Console (:9300) shows banner, counters, plan, and activity; kpr status / kpr plan show the same as text.

Marking a row due with a reason is the interface: anything that can write the mark (a script, cron, a human with redis-cli) decides how and when to clean what. Deleting has one owner: the sweeper.

Quickstart

make up            # kpr + registry, file backend (detached)
# or: just up        (COMPOSE=redis make up for the redis stack)

# Push something ephemeral (needs localhost:5000 free —
# macOS AirPlay Receiver squats it when enabled):
crane copy busybox:latest localhost:5000/test/busybox:10s

docker exec kpr kpr reap --no-dry-run   # mark (nothing before minute 10)
sleep 15
docker exec kpr kpr reap --no-dry-run   # ttl:10s elapsed
docker exec kpr kpr sweep               # delete by digest, watch the summary
docker exec kpr kpr status              # counters

Console: http://localhost:9300. Registry GC (reclaims blob bytes after manifest deletes — offline, registry stops) is make gc.

Reaping policies

kpr reap [policy] marks rows due with a reason (reap all runs every policy). Full table: Reaping policies.

Policy Reason
ttl ttl:<d> elapsed — explicit TTL (10m, myapp-10m) elapsed since push
hash ttl:<d> elapsed — bare hash past the default (next-day triage)
partial partial:older than <age> — digest-less row past the max age
untagged untagged:past grace <grace> — tracked tag gone from the live catalog
keep-n keep-n:exceeds <n> — everything past the freshest 10 per repo

Suffix is intent: any stem + -ttl matches (myapp-10m, a-1h); bare tags stay hex-scoped (6+ with a letter, abc1234 yes, 20240115 no). Out: dotted versions (v1.2.3-1h), dangling forms (abc1234-, 10m-), uppercase units (10M).

Spared: latest is never swept by any policy and never counts into keep-N — a repo keeps 10 plus latest.

The sweeper adds its own floor regardless of marks: a TTL row whose promise hasn't elapsed is never wiped by a stale mark.

CLI

kpr serve    # console :9300 + app :8080 + receiver + sweeper loop
kpr status   # high-level posture, one line per section
kpr status <card>  # detail behind a line: store, registry, edge,
                   # clock, activity, observability (--json)
kpr plan     # pending candidates (--json for piping)
kpr plan discard  # drop the whole plan (clear due marks, no dry-run)
kpr plan add <pattern>...     # mark tracked repo:tag by glob, regex: or
                             # exact image (exact names must match; no dry-run)
kpr plan remove <pattern>...  # unmark due rows by glob, regex: or exact image (no dry-run)
kpr store ls  # tracked rows, short columns (--long, --json; ls sentinels)
kpr store inspect <repo:tag>  # one full row (exact spelling)
kpr store rm <repo:tag>...  # drop rows; tag stays, untracked (no dry-run)
kpr store rm --untag <repo:tag>...  # delete the manifest too, row drops on confirm
kpr store unlock   # prove the shared store, set the intent marker
kpr store lock     # drop the intent marker
kpr store adopt [IDENT] [--gen]  # pair the store to the served lineage
kpr reap [policy]  # evaluate one policy (ttl, hash, partial,
                   # untagged, keep-n) or all; marks accumulate until
                   # sweep or plan discard (--no-dry-run to mark, repeat
                   # --exclude to spare keep-N for matching repo:tag)
kpr registry analyze  # six live lines, catalog/store/fs first
                      # (needs a filesystem KPR_REGISTRY_CONFIG),
                      # then revisions, blobs, bytes (--json exact)
kpr registry ls sentinels  # machinery tags as the registry sees
                      # them, with evaluated identity (--json, --long)
kpr sweep    # run one sweep pass in-process, print the summary
kpr gc       # garbage-collect the shared store (dry-run preview by
             # default; --no-dry-run collects, readonly probe first)
kpr env      # resolved configuration

The keeper CLI talks to state directly, so it runs colocated with serve (same network for redis, same volume for files: docker exec kpr kpr …). Detached operation is explicitly deferred.

Garbage collection

Deletes drop the manifest reference only; blob bytes need the stock collector against the shared store:

# 0. Preview anytime (dry-run default, streams the collector):
docker exec kpr kpr gc
# 1. Registry readonly (config file! the env override panics
#    registry:3): storage.maintenance.readonly.enabled: true + restart.
# 2. Collect from the kpr container (shared mounts, same binary):
docker exec kpr kpr gc --no-dry-run                 # refuses unless readonly
docker exec kpr kpr gc --no-dry-run --delete-untagged
# 3. Flip readonly back off + restart.

kpr gc previews by default. An armed run refuses rather than collect blind, when:

  • the binary or config mounts are missing
  • the store root is not a local filesystem
  • the sentinel verdict is inconclusive
  • the registry is serving without online clearance
  • the shared store is unproven
  • another run holds kpr:gc:lock (make gc honors the same key)

A preview prints the same checklist, warns, and proceeds — it deletes nothing either way.

Two things to know before arming:

  • If the registry's cache redis needs a password, pass REGISTRY_REDIS_PASSWORD through (same convention the registry uses). The stock collector kpr shells out to inherits kpr's environment — cacheless or open-cache setups need nothing.
  • Don't run the stock collector alongside. Manual runs take no lock, because the registry sets none.

During a run the collector's output streams through with stage events: sentinel verdicts, collector pid, post-probe. Afterwards gc re-probes the mode. A flip mid-run fails the run unless --accept-mode-flip — loud, never a panic.

Flipping readonly stays with the operator. The command never rewrites registry config.

Configuration

Wiring only (ports, redis addr, registry URL, arming); see docs/CONFIG.md.

Variable Meaning
KPR_REDIS_ADDR redis (default localhost:6379; compose sets redis:6379)
KPR_REDIS_PASSWORD redis password (empty = no auth; compose sets the shared dev default — kpr env shows set/unset only)
KPR_REDIS_DB redis logical DB for kpr rows (default 0; compose sets 4 — DBs 0-2 are taken, 3 is the registry cache)
KPR_STORE state backend, file (default) or redis. Unset means derive: KPR_STORE_DIR alone selects file, KPR_REDIS_ADDR alone selects redis, silence selects file. Must agree with backend vars (see STORES)
KPR_STORE_DIR directory for the file backend (default kpr/, cwd-relative; compose sets it absolute on the shared volume, e.g. <registry-root>/kpr for a self-contained backup)
KPR_REGISTRY_URL registry peer (dev default http://localhost:5000)
KPR_EDGE_ADDR edge proxy listen address inside serve (default :5000 — the registry's published port, moved to the edge)
KPR_EDGE=false run serve without the edge proxy (default-on; a failed RelativeURLs proof also closes it loudly — see docs/EDGE.md)
KPR_CLI_NO_DRY_RUN=true arm one-shot commands (gc collects, reap marks, sweep deletes)
KPR_TIME_METHOD checked-clock transport: local (default), https, ntp (see docs/TIMESTAMPS.md; compose pins https)
KPR_TIME_SERVER time source host (default zeitstempel.dfn.de; air-gapped sites point at their own)

The local compose arms by default (comment the line out to go back to planning). Recreating the kpr container can pause sweeping for up to the 5-minute sweep lock if the old instance died mid-pass — self-heals at lock expiry.

Observability

Optional overlay, off by default; see docs/OBSERVABILITY.md.

make up   # + Quickwit, Jaeger, Prometheus, Grafana (up-minimal for the base stack)

Structured access logs and sweeper activity (sweep pass / sweep row records) land in Quickwit; traces in Jaeger; :9300 links all four. Testing approach and coverage gates: docs/TESTING.md.

Deliberately out

Policy/workflow engine, scheduler, per-repo rule sets, auth, signing, replication, cloud integrations, online registry GC. The reasoning for each is in ARCHITECTURE.

License

GPLv3 (LICENSE). Third-party attributions ship in LICENSES/ and inside the image at /app/licenses/.