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
+117
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
# aptly-push — upload one or more .deb/.dsc/.changes files into an aptly local
|
||||
# repo and (by default) refresh the publish that serves it. Works unchanged
|
||||
# against every security.preset in the Helm chart's matrix: pass credentials
|
||||
# via APTLY_USER/APTLY_PASSWORD, APTLY_TOKEN, or neither for an open repo.
|
||||
#
|
||||
# Usage:
|
||||
# aptly-push --repo stable [--prefix ""] [--distribution stable] \
|
||||
# [--no-publish] [--no-force-replace] [--no-sign] FILE.deb [FILE2.deb ...]
|
||||
#
|
||||
# Signing note: aptly-push runs as an external actor (CI, a developer's
|
||||
# laptop) and generally has no access to the aptly-server pod's GNUPGHOME or
|
||||
# /run/aptly/signing.json — only aptly-init and aptly-reconcile, which run
|
||||
# inside that pod, do. So by default aptly-push sends NO Signing field at all
|
||||
# on the publish-refresh call, letting the server use its own configured
|
||||
# default signer (this is the correct behaviour for a signed repo). Pass
|
||||
# --no-sign (or APTLY_GPG_SIGN=false) only when you know the target repo is
|
||||
# genuinely unsigned (aptly.gpg.enabled: false) — otherwise the update call
|
||||
# will fail with a clear "no GPG key" error rather than silently publishing
|
||||
# unsigned.
|
||||
#
|
||||
# Env (all overridable by the matching flag):
|
||||
# APTLY_URL (required, from lib/common.sh) APTLY_REPO APTLY_PREFIX APTLY_DISTRIBUTION APTLY_GPG_SIGN
|
||||
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
|
||||
|
||||
REPO="${APTLY_REPO:-}"
|
||||
PREFIX="${APTLY_PREFIX:-}"
|
||||
DISTRIBUTION="${APTLY_DISTRIBUTION:-}"
|
||||
DO_PUBLISH=true
|
||||
FORCE_REPLACE=true
|
||||
SIGN="${APTLY_GPG_SIGN:-true}"
|
||||
FILES=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--repo) REPO="$2"; shift 2 ;;
|
||||
--prefix) PREFIX="$2"; shift 2 ;;
|
||||
--distribution) DISTRIBUTION="$2"; shift 2 ;;
|
||||
--no-publish) DO_PUBLISH=false; shift ;;
|
||||
--no-force-replace) FORCE_REPLACE=false; shift ;;
|
||||
--no-sign) SIGN=false; shift ;;
|
||||
--) shift; FILES+=("$@"); break ;;
|
||||
-*) die "unknown flag: $1" ;;
|
||||
*) FILES+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$REPO" ]] || die "--repo (or APTLY_REPO) is required"
|
||||
[[ "${#FILES[@]}" -gt 0 ]] || die "no files given"
|
||||
for f in "${FILES[@]}"; do [[ -f "$f" ]] || die "file not found: $f"; done
|
||||
|
||||
UPLOAD_DIR="push-$(date +%s)-$$"
|
||||
log "uploading ${#FILES[@]} file(s) into upload directory '${UPLOAD_DIR}'"
|
||||
|
||||
cleanup() { api DELETE "/api/files/${UPLOAD_DIR}" >/dev/null 2>&1 || true; }
|
||||
trap cleanup EXIT
|
||||
|
||||
for f in "${FILES[@]}"; do
|
||||
base="$(basename -- "$f")"
|
||||
log " -> ${base}"
|
||||
api POST "/api/files/${UPLOAD_DIR}" -F "file=@${f}" >/dev/null
|
||||
done
|
||||
|
||||
log "including uploaded files into repo '${REPO}'"
|
||||
qs="forceReplace=$([[ "$FORCE_REPLACE" == "true" ]] && echo 1 || echo 0)"
|
||||
resp="$(api POST "/api/repos/${REPO}/file/${UPLOAD_DIR}?${qs}")"
|
||||
|
||||
failed="$(jq -r '(.FailedFiles // []) | length' <<<"$resp")"
|
||||
if [[ "$failed" -gt 0 ]]; then
|
||||
jq -r '.FailedFiles[]' <<<"$resp" | while IFS= read -r ff; do log "FAILED: ${ff}"; done
|
||||
jq -r '(.Report.Warnings // [])[]' <<<"$resp" 2>/dev/null | while IFS= read -r w; do log "warning: ${w}"; done
|
||||
die "${failed} file(s) were rejected by the repo — see above"
|
||||
fi
|
||||
jq -r '(.Report.AddedLines // [])[]' <<<"$resp" 2>/dev/null | while IFS= read -r l; do log "added: ${l}"; done
|
||||
log "included successfully into '${REPO}'"
|
||||
|
||||
if [[ "$DO_PUBLISH" != "true" ]]; then
|
||||
log "skipping publish refresh (--no-publish)"
|
||||
exit 0
|
||||
fi
|
||||
[[ -n "$DISTRIBUTION" ]] || { warn "no --distribution/APTLY_DISTRIBUTION given, skipping publish refresh"; exit 0; }
|
||||
|
||||
escaped_prefix="$(api_prefix "$PREFIX")"
|
||||
log "refreshing publish prefix='${PREFIX:-<root>}' distribution='${DISTRIBUTION}'"
|
||||
|
||||
update_body="{}"
|
||||
[[ "$SIGN" == "false" ]] && update_body='{"Signing":{"Skip":true}}'
|
||||
|
||||
code_body="$(api_status POST "/api/publish/${escaped_prefix}/${DISTRIBUTION}/update" \
|
||||
-H 'Content-Type: application/json' -d "$update_body")"
|
||||
code="$(head -1 <<<"$code_body")"
|
||||
body="$(tail -n +2 <<<"$code_body")"
|
||||
|
||||
if [[ "$code" == "404" ]]; then
|
||||
warn "no publish exists at prefix='${PREFIX:-<root>}' distribution='${DISTRIBUTION}' yet."
|
||||
warn "the package is in the repo but not yet reachable by clients — run aptly-reconcile" \
|
||||
"(or 'helm upgrade') to create the publish target once, then re-run this push."
|
||||
exit 0
|
||||
elif [[ "$code" != 2* ]]; then
|
||||
warn "publish refresh failed (HTTP ${code}): ${body}"
|
||||
warn "the package IS in the repo '${REPO}' — only the publish step failed." \
|
||||
"If this repo is signed, check that the server has a usable signing key" \
|
||||
"(see docs/security.md); if it is meant to be unsigned, re-run with --no-sign."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
task_id="$(jq -r '.ID // empty' <<<"$body")"
|
||||
if ! wait_task "$task_id"; then
|
||||
warn "publish refresh task failed — package is in the repo but not yet published, see task output above"
|
||||
exit 0
|
||||
fi
|
||||
log "published successfully"
|
||||
Reference in New Issue
Block a user