Public Access
Initial implementation: aptly container image, Compose stacks, Helm chart, and Gitea Actions pipelines
Provides a self-contained, containerized aptly (Debian repo manager) stack with independently releasable image and Helm chart versions. - images/: aptly-server (aptly built from source, cross-compiled) and aptly-deb-builder (nfpm + dpkg-buildpackage) container images - rootfs/: shared aptly-init/aptly-reconcile/aptly-push/aptly-pack scripts consumed identically by Compose and the Helm chart, driven by one declarative state.yaml contract - compose/: test (ephemeral, open) and production docker-compose stacks with an nginx read/auth sidecar - charts/aptly/: aptly-native Helm chart covering every security posture from fully open to authenticated read+write, Ingress and Gateway API support (usable in parallel for migration scenarios), metrics, and declarative repo/mirror/publish reconciliation via a Helm hook - .gitea/workflows/: CI (lint, template, kubeconform, E2E smoke test) plus separately tagged image (image/v*) and chart (chart/v*) releases, weekly rebuilds, and a preflight workflow validating the runner's Docker/Helm-OCI capabilities - pubkeys/: RSA chart-signing key for Helm --sign / Artifact Hub's signKey annotation (Helm can't verify Ed25519 keys) - docs/, README.md, charts/aptly/README.md: usage, security, and versioning documentation
This commit is contained in:
Executable
+110
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
# aptly-mirror-refresh — periodic content refresh for mirrors declared in
|
||||
# state.yaml: fetch the mirror, snapshot it, switch every publish target that
|
||||
# references it, then prune old snapshots. Intended to be run by an external
|
||||
# scheduler (cron, a Compose --profile, a k8s CronJob you wire up yourself —
|
||||
# this chart does not ship one, see docs/versioning.md "Nicht im Scope").
|
||||
#
|
||||
# Only touches mirrors listed in --mirrors (or all mirrors in state.yaml if
|
||||
# omitted). Safe to run concurrently with aptly-reconcile against the same
|
||||
# instance (both go through the REST API); NOT safe to run two instances of
|
||||
# this script concurrently against the same mirror, since both would try to
|
||||
# update+snapshot it at once — use your scheduler's concurrency policy
|
||||
# (Gitea/k8s CronJob: concurrencyPolicy=Forbid) to prevent that.
|
||||
#
|
||||
# Env:
|
||||
# APTLY_STATE_FILE path to state.yaml (required)
|
||||
# APTLY_MIRRORS comma-separated mirror names to refresh (default: all)
|
||||
# APTLY_KEEP_SNAPSHOTS how many historical snapshots to retain per mirror (default: 5)
|
||||
# APTLY_SNAPSHOT_PREFIX snapshot name template prefix (default: mirror name)
|
||||
# plus lib/common.sh's APTLY_URL / auth vars
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=rootfs/usr/local/bin/lib/common.sh
|
||||
source "${SCRIPT_DIR}/lib/common.sh"
|
||||
require_cmd curl jq yq date
|
||||
|
||||
: "${APTLY_STATE_FILE:?APTLY_STATE_FILE must point at a state.yaml}"
|
||||
KEEP="${APTLY_KEEP_SNAPSHOTS:-5}"
|
||||
STATE_JSON="$(yq -o=json '.' "$APTLY_STATE_FILE")"
|
||||
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
|
||||
wanted_mirrors=()
|
||||
if [[ -n "${APTLY_MIRRORS:-}" ]]; then
|
||||
IFS=',' read -ra wanted_mirrors <<< "$APTLY_MIRRORS"
|
||||
fi
|
||||
|
||||
wants() {
|
||||
local name="$1"
|
||||
[[ "${#wanted_mirrors[@]}" -eq 0 ]] && return 0
|
||||
for m in "${wanted_mirrors[@]}"; do [[ "$m" == "$name" ]] && return 0; done
|
||||
return 1
|
||||
}
|
||||
|
||||
api_ready
|
||||
failures=0
|
||||
|
||||
mirror_count="$(jq '(.mirrors // []) | length' <<<"$STATE_JSON")"
|
||||
for ((i=0; i<mirror_count; i++)); do
|
||||
m="$(jq ".mirrors[$i]" <<<"$STATE_JSON")"
|
||||
name="$(jq -r '.name' <<<"$m")"
|
||||
wants "$name" || continue
|
||||
|
||||
log "updating mirror '${name}'"
|
||||
resp="$(api PUT "/api/mirrors/${name}" -H 'Content-Type: application/json' -d '{"ForceUpdate": true}')" \
|
||||
|| { warn "mirror update failed for ${name}"; failures=$((failures+1)); continue; }
|
||||
task_id="$(jq -r '.ID // empty' <<<"$resp")"
|
||||
if ! wait_task "$task_id"; then
|
||||
warn "mirror update task failed for ${name}, skipping snapshot/publish"
|
||||
failures=$((failures+1))
|
||||
continue
|
||||
fi
|
||||
|
||||
snap_name="${name}-${STAMP}"
|
||||
log "snapshotting '${name}' -> '${snap_name}'"
|
||||
resp="$(api POST "/api/mirrors/${name}/snapshots" -H 'Content-Type: application/json' \
|
||||
-d "$(jq -n --arg n "$snap_name" '{Name: $n}')")" \
|
||||
|| { warn "snapshot failed for ${name}"; failures=$((failures+1)); continue; }
|
||||
|
||||
# Switch every publish target in state.yaml whose sources reference this mirror's snapshots.
|
||||
publish_count="$(jq '(.publish // []) | length' <<<"$STATE_JSON")"
|
||||
for ((j=0; j<publish_count; j++)); do
|
||||
p="$(jq ".publish[$j]" <<<"$STATE_JSON")"
|
||||
kind="$(jq -r '.sourceKind // "local"' <<<"$p")"
|
||||
[[ "$kind" == "snapshot" ]] || continue
|
||||
refs_mirror="$(jq --arg m "$name" '[.sources[] | select(.name | startswith($m + "-"))] | length > 0' <<<"$p")"
|
||||
[[ "$refs_mirror" == "true" ]] || continue
|
||||
|
||||
prefix="$(jq -r '.prefix // ""' <<<"$p")"
|
||||
dist="$(jq -r '.distribution' <<<"$p")"
|
||||
escaped_prefix="$(api_prefix "$prefix")"
|
||||
component="$(jq -r '.sources[0].component // "main"' <<<"$p")"
|
||||
|
||||
log "switching publish prefix='${prefix}' distribution='${dist}' -> snapshot '${snap_name}'"
|
||||
resp="$(api PUT "/api/publish/${escaped_prefix}/${dist}" -H 'Content-Type: application/json' \
|
||||
-d "$(jq -n --arg n "$snap_name" --arg c "$component" '{Snapshots: [{Name: $n, Component: $c}]}')")" \
|
||||
|| { warn "publish switch failed for ${prefix}/${dist}"; failures=$((failures+1)); continue; }
|
||||
task_id="$(jq -r '.ID // empty' <<<"$resp")"
|
||||
wait_task "$task_id" || { warn "publish switch task failed for ${prefix}/${dist}"; failures=$((failures+1)); }
|
||||
done
|
||||
|
||||
# Prune old snapshots for this mirror beyond APTLY_KEEP_SNAPSHOTS, oldest first.
|
||||
all_snaps="$(api GET "/api/snapshots" | jq -r --arg m "$name" \
|
||||
'[.[] | select(.Name | startswith($m + "-"))] | sort_by(.CreatedAt) | .[].Name')"
|
||||
total="$(wc -l <<<"$all_snaps" | tr -d ' ')"
|
||||
to_prune=$(( total > KEEP ? total - KEEP : 0 ))
|
||||
if [[ "$to_prune" -gt 0 ]]; then
|
||||
log "pruning ${to_prune} old snapshot(s) for '${name}' (keeping ${KEEP})"
|
||||
head -n "$to_prune" <<<"$all_snaps" | while IFS= read -r s; do
|
||||
[[ -z "$s" ]] && continue
|
||||
api DELETE "/api/snapshots/${s}" >/dev/null 2>&1 \
|
||||
|| warn "could not drop snapshot ${s} (likely still referenced by a publish — will retry next run)"
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$failures" -gt 0 ]]; then
|
||||
die "${failures} mirror(s) failed to refresh — see warnings above"
|
||||
fi
|
||||
log "mirror refresh complete"
|
||||
Reference in New Issue
Block a user