Public Access
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
114 lines
4.4 KiB
Bash
Executable File
114 lines
4.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Shared helpers for aptly-init, aptly-reconcile, aptly-mirror-refresh, aptly-push.
|
|
# Sourced, not executed: no shebang execution, no `set` calls here (callers set their own).
|
|
|
|
log() { printf '[%s] %s\n' "$(basename "${0:-common.sh}")" "$*" >&2; }
|
|
die() { log "ERROR: $*"; exit 1; }
|
|
warn() { log "WARN: $*"; }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# aptly REST API client
|
|
#
|
|
# Auth resolution order (first match wins):
|
|
# APTLY_TOKEN -> Authorization: Bearer <token>
|
|
# APTLY_USER + APTLY_PASSWORD -> HTTP Basic
|
|
# neither -> no Authorization header (open / read-only repo)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
: "${APTLY_URL:?APTLY_URL must be set, e.g. http://aptly:8080 or https://apt.example.com}"
|
|
APTLY_URL="${APTLY_URL%/}"
|
|
APTLY_CURL_RETRIES="${APTLY_CURL_RETRIES:-5}"
|
|
APTLY_CURL_RETRY_DELAY="${APTLY_CURL_RETRY_DELAY:-3}"
|
|
|
|
_auth_args=()
|
|
if [[ -n "${APTLY_TOKEN:-}" ]]; then
|
|
_auth_args=(-H "Authorization: Bearer ${APTLY_TOKEN}")
|
|
elif [[ -n "${APTLY_USER:-}" && -n "${APTLY_PASSWORD:-}" ]]; then
|
|
_auth_args=(-u "${APTLY_USER}:${APTLY_PASSWORD}")
|
|
fi
|
|
|
|
# api METHOD PATH [curl-data-args...]
|
|
# PATH must start with /api/... Prints the response body on stdout.
|
|
# Non-2xx is a hard failure (die) — callers that need to inspect the status
|
|
# themselves should use api_status instead.
|
|
#
|
|
# --path-as-is is not optional: aptly's root-publish-prefix convention is the
|
|
# literal path segment ".", and curl's URL parser applies RFC 3986 dot-segment
|
|
# removal by default — silently rewriting ".../publish/." to ".../publish/"
|
|
# and ".../publish/./stable" to ".../publish/stable" before the request is
|
|
# even sent, which then 404s or hits the wrong (prefix-less) route.
|
|
api() {
|
|
local method="$1" path="$2"; shift 2
|
|
local out
|
|
# Deliberately no --retry-all-errors: that would also retry a deterministic
|
|
# 401 (wrong credentials) or 404 several times before giving up, turning an
|
|
# instant "wrong password" into a ~15s hang. Plain --retry already covers
|
|
# the transient cases that matter (connection errors, 429, 5xx).
|
|
out="$(curl -fsS --path-as-is --retry "$APTLY_CURL_RETRIES" --retry-delay "$APTLY_CURL_RETRY_DELAY" \
|
|
-X "$method" "${_auth_args[@]}" "${APTLY_URL}${path}" "$@")" \
|
|
|| die "API call failed: ${method} ${path}"
|
|
printf '%s' "$out"
|
|
}
|
|
|
|
# api_status METHOD PATH [curl-data-args...]
|
|
# Prints "HTTPSTATUS<newline>BODY". Never dies on non-2xx — caller decides.
|
|
api_status() {
|
|
local method="$1" path="$2"; shift 2
|
|
curl -sS --path-as-is -o /tmp/api_status.body -w '%{http_code}' -X "$method" \
|
|
"${_auth_args[@]}" "${APTLY_URL}${path}" "$@" > /tmp/api_status.code \
|
|
|| die "API call errored (connection): ${method} ${path}"
|
|
printf '%s\n' "$(cat /tmp/api_status.code)"
|
|
cat /tmp/api_status.body
|
|
}
|
|
|
|
# api_ready: block until /api/ready answers 200, or die after timeout.
|
|
api_ready() {
|
|
local timeout="${APTLY_WAIT_TIMEOUT:-600}" waited=0
|
|
log "waiting for ${APTLY_URL}/api/ready (timeout ${timeout}s)"
|
|
until curl -fsS -o /dev/null "${APTLY_URL}/api/ready"; do
|
|
(( waited >= timeout )) && die "aptly not ready after ${timeout}s"
|
|
sleep 3
|
|
waited=$(( waited + 3 ))
|
|
done
|
|
log "aptly is ready (waited ${waited}s)"
|
|
}
|
|
|
|
# wait_task TASK_ID: poll /api/tasks/:id until State is 2 (succeeded) or 3 (failed).
|
|
# State enum per aptly's task package: 0 init, 1 running, 2 succeeded, 3 failed.
|
|
wait_task() {
|
|
local id="$1" timeout="${APTLY_TASK_TIMEOUT:-3600}" waited=0 state
|
|
[[ -z "$id" || "$id" == "null" ]] && return 0
|
|
while true; do
|
|
state="$(api GET "/api/tasks/${id}" | jq -r '.State')"
|
|
case "$state" in
|
|
2) return 0 ;;
|
|
3)
|
|
log "task ${id} failed, output:"
|
|
api GET "/api/tasks/${id}/output" >&2 || true
|
|
return 1
|
|
;;
|
|
esac
|
|
(( waited >= timeout )) && die "task ${id} did not finish within ${timeout}s (last state: ${state})"
|
|
sleep 2
|
|
waited=$(( waited + 2 ))
|
|
done
|
|
}
|
|
|
|
# api_prefix PREFIX: map a human-readable publish prefix to aptly's URL-escaped form.
|
|
# "" -> "." "debian/stable" -> "debian_stable"
|
|
# See aptly's api/publish.go: ParsePrefix / EscapePrefix semantics.
|
|
api_prefix() {
|
|
local p="$1"
|
|
if [[ -z "$p" ]]; then
|
|
printf '.'
|
|
else
|
|
printf '%s' "${p//\//_}"
|
|
fi
|
|
}
|
|
|
|
require_cmd() {
|
|
for c in "$@"; do
|
|
command -v "$c" >/dev/null 2>&1 || die "required command not found: $c"
|
|
done
|
|
}
|