#!/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 # 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 "HTTPSTATUSBODY". 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 }