Files
aptly-containerized/rootfs/usr/local/bin/lib/common.sh
T
f.weber 103ad311b7
CI / lint (push) Failing after 24s
CI / smoke-test (push) Failing after 2m4s
Release image / release (push) Successful in 23m18s
Release chart / release (push) Successful in 7s
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
2026-08-12 12:21:08 +02:00

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
}