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
264 lines
10 KiB
Smarty
264 lines
10 KiB
Smarty
{{/*
|
|
Standard name/label helpers, bookstack-chart style.
|
|
*/}}
|
|
{{- define "aptly.name" -}}
|
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
|
{{- end -}}
|
|
|
|
{{- define "aptly.fullname" -}}
|
|
{{- if .Values.fullnameOverride -}}
|
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
|
{{- else -}}
|
|
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
|
{{- if contains $name .Release.Name -}}
|
|
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
|
{{- else -}}
|
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
|
|
{{- define "aptly.chart" -}}
|
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
|
{{- end -}}
|
|
|
|
{{- define "aptly.labels" -}}
|
|
helm.sh/chart: {{ include "aptly.chart" . }}
|
|
{{ include "aptly.selectorLabels" . }}
|
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
|
{{- end -}}
|
|
|
|
{{- define "aptly.selectorLabels" -}}
|
|
app.kubernetes.io/name: {{ include "aptly.name" . }}
|
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Image helpers — global.imageRegistry prefixes the repository when set,
|
|
matching the bookstack-chart convention.
|
|
*/}}
|
|
{{- define "aptly.image" -}}
|
|
{{- $registry := .Values.global.imageRegistry -}}
|
|
{{- $repo := .Values.image.repository -}}
|
|
{{- $tag := .Values.image.tag | default .Chart.AppVersion -}}
|
|
{{- if $registry -}}
|
|
{{- printf "%s/%s:%s" $registry $repo $tag -}}
|
|
{{- else -}}
|
|
{{- printf "%s:%s" $repo $tag -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
|
|
{{- define "aptly.nginxImage" -}}
|
|
{{- $registry := .Values.global.imageRegistry -}}
|
|
{{- $repo := .Values.nginx.image.repository -}}
|
|
{{- $tag := .Values.nginx.image.tag -}}
|
|
{{- if $registry -}}
|
|
{{- printf "%s/%s:%s" $registry $repo $tag -}}
|
|
{{- else -}}
|
|
{{- printf "%s:%s" $repo $tag -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Shared by templates/ingress.yaml and templates/httproute.yaml: proxy.enabled=false
|
|
means aptly's unauthenticated write API sits directly behind whatever routes to
|
|
it, on a single listener with no path-based auth split possible — refuse to wire
|
|
that up to an Ingress OR a Gateway API HTTPRoute unless security.preset=open
|
|
confirms it's intended.
|
|
*/}}
|
|
{{- define "aptly.exposureGuard" -}}
|
|
{{- if and (not .Values.proxy.enabled) (or .Values.ingress.enabled .Values.gateway.enabled) (ne .Values.security.preset "open") -}}
|
|
{{- fail "proxy.enabled=false publishes aptly's unauthenticated write API through the Ingress/HTTPRoute (no path-based auth split is possible on a single listener). Set security.preset=open to confirm this is intended, or keep proxy.enabled=true." -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
|
|
{{- define "aptly.imagePullSecrets" -}}
|
|
{{- $secrets := concat (.Values.global.imagePullSecrets | default list) (.Values.image.pullSecrets | default list) -}}
|
|
{{- if $secrets }}
|
|
imagePullSecrets:
|
|
{{- range $secrets }}
|
|
- name: {{ if kindIs "map" . }}{{ .name }}{{ else }}{{ . }}{{ end }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Whether aptly serves the published tree itself (no nginx proxy in front).
|
|
Only sane in combination with security.preset=open — enforced in
|
|
templates/statefulset.yaml (the aptly config) and templates/ingress.yaml
|
|
(the hard `fail` guard).
|
|
*/}}
|
|
{{- define "aptly.serveInApiMode" -}}
|
|
{{- if .Values.proxy.enabled -}}false{{- else -}}true{{- end -}}
|
|
{{- end -}}
|
|
|
|
{{- define "aptly.apiListen" -}}
|
|
{{- if .Values.proxy.enabled -}}127.0.0.1:8080{{- else -}}0.0.0.0:8080{{- end -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Resolve security.preset + explicit read/write overrides into a plain dict
|
|
{r, ra, w, wa} (read-enabled, read-auth, write-enabled, write-auth). Both
|
|
nginx.conf.tpl and templates/ingress.yaml key off this so preset expansion
|
|
lives in exactly one place.
|
|
*/}}
|
|
{{- define "aptly.security" -}}
|
|
{{- $p := .Values.security.preset -}}
|
|
{{- $presets := dict
|
|
"open" (dict "r" true "ra" false "w" true "wa" false)
|
|
"publicRead" (dict "r" true "ra" false "w" true "wa" true)
|
|
"authenticated" (dict "r" true "ra" true "w" true "wa" true)
|
|
"readOnly" (dict "r" true "ra" false "w" false "wa" false)
|
|
-}}
|
|
{{- $base := index $presets $p -}}
|
|
{{- if not $base -}}
|
|
{{- fail (printf "security.preset: unknown value %q (must be one of open, publicRead, authenticated, readOnly)" $p) -}}
|
|
{{- end -}}
|
|
{{- $d := deepCopy $base -}}
|
|
{{- if kindIs "bool" .Values.security.read.enabled }}{{- $_ := set $d "r" .Values.security.read.enabled -}}{{- end -}}
|
|
{{- if kindIs "bool" .Values.security.read.requireAuth }}{{- $_ := set $d "ra" .Values.security.read.requireAuth -}}{{- end -}}
|
|
{{- if kindIs "bool" .Values.security.write.enabled }}{{- $_ := set $d "w" .Values.security.write.enabled -}}{{- end -}}
|
|
{{- if kindIs "bool" .Values.security.write.requireAuth }}{{- $_ := set $d "wa" .Values.security.write.requireAuth -}}{{- end -}}
|
|
{{- $d | toJson -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Render the aptly config (YAML) from the curated values.aptly.* keys, then
|
|
deep-merge aptly.configOverrides on top so every current/future aptly config
|
|
key stays reachable without a chart change. Curated keys are OMITTED when
|
|
unset, so a default install matches aptly's own upstream defaults exactly
|
|
(see utils/config.go in aptly-dev/aptly for the canonical defaults).
|
|
*/}}
|
|
{{- define "aptly.config" -}}
|
|
{{- $v := .Values.aptly -}}
|
|
{{- $c := dict "root_dir" "/var/lib/aptly" -}}
|
|
{{- with $v.logLevel }}{{- $_ := set $c "log_level" . -}}{{- end -}}
|
|
{{- with $v.logFormat }}{{- $_ := set $c "log_format" . -}}{{- end -}}
|
|
{{- if $v.architectures }}{{- $_ := set $c "architectures" $v.architectures -}}{{- end -}}
|
|
{{- with $v.download.concurrency }}{{- $_ := set $c "download_concurrency" . -}}{{- end -}}
|
|
{{- with $v.download.limit }}{{- $_ := set $c "download_limit" . -}}{{- end -}}
|
|
{{- with $v.download.retries }}{{- $_ := set $c "download_retries" . -}}{{- end -}}
|
|
{{- if kindIs "bool" $v.download.sourcePackages }}{{- $_ := set $c "download_sourcepackages" $v.download.sourcePackages -}}{{- end -}}
|
|
{{- $_ := set $c "gpg_provider" ($v.gpg.provider | default "gpg") -}}
|
|
{{- $_ := set $c "gpg_disable_sign" (not $v.gpg.enabled) -}}
|
|
{{- $_ := set $c "gpg_disable_verify" (not $v.gpg.verify) -}}
|
|
{{- if kindIs "bool" $v.publishing.skipContents }}{{- $_ := set $c "skip_contents_publishing" $v.publishing.skipContents -}}{{- end -}}
|
|
{{- if kindIs "bool" $v.publishing.skipBz2 }}{{- $_ := set $c "skip_bz2_publishing" $v.publishing.skipBz2 -}}{{- end -}}
|
|
{{- $_ := set $c "enable_metrics_endpoint" ($v.metrics.enabled | default false) -}}
|
|
{{- $_ := set $c "enable_swagger_endpoint" ($v.swagger.enabled | default false) -}}
|
|
{{- $_ := set $c "serve_in_api_mode" (eq (include "aptly.serveInApiMode" .) "true") -}}
|
|
{{- $_ := set $c "filesystem_publish_endpoints" (dict $.Values.proxy.publishEndpointName (dict "root_dir" "/var/lib/aptly/public" "link_method" "hardlink")) -}}
|
|
{{- $merged := mergeOverwrite $c (deepCopy ($v.configOverrides | default dict)) -}}
|
|
{{- toYaml $merged -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Render nginx's server{} block (mounted at /etc/nginx/conf.d/default.conf,
|
|
which the base image's own nginx.conf already includes from inside its own
|
|
http{} block — this template must therefore emit ONLY a server{} block, see
|
|
compose/config/nginx.*.conf for the same constraint hit empirically).
|
|
*/}}
|
|
{{- define "aptly.nginxConf" -}}
|
|
{{- $sec := include "aptly.security" . | fromJson -}}
|
|
{{- $p := .Values.proxy -}}
|
|
server {
|
|
listen 8080;
|
|
server_name _;
|
|
client_max_body_size {{ $p.maxUploadSize }};
|
|
absolute_redirect off;
|
|
{{- range .Values.security.trustedProxies }}
|
|
set_real_ip_from {{ . }};
|
|
{{- end }}
|
|
{{- if .Values.security.trustedProxies }}
|
|
real_ip_header X-Forwarded-For;
|
|
real_ip_recursive on;
|
|
{{- end }}
|
|
|
|
location = /healthz { access_log off; return 200 "ok\n"; }
|
|
location = /api/ready { access_log off; proxy_pass http://127.0.0.1:8080; }
|
|
location = /api/healthy { access_log off; proxy_pass http://127.0.0.1:8080; }
|
|
|
|
{{- if $sec.w }}
|
|
location /api/ {
|
|
{{- if .Values.security.write.allowCIDRs }}
|
|
{{- if and .Values.security.write.allowCIDRs (not .Values.security.trustedProxies) }}
|
|
# WARNING: write.allowCIDRs is set without security.trustedProxies. Behind
|
|
# an Ingress controller, $remote_addr is the CONTROLLER's pod IP, not the
|
|
# real client — this will match every client on earth. Set
|
|
# trustedProxies to the controller's CIDR, or use networkPolicy instead.
|
|
{{- end }}
|
|
{{- range .Values.security.write.allowCIDRs }}
|
|
allow {{ . }};
|
|
{{- end }}
|
|
deny all;
|
|
satisfy {{ if $sec.wa }}any{{ else }}all{{ end }};
|
|
{{- end }}
|
|
{{- if $sec.wa }}
|
|
auth_basic "aptly";
|
|
auth_basic_user_file /run/aptly/htpasswd;
|
|
{{- else }}
|
|
auth_basic off;
|
|
{{- end }}
|
|
proxy_pass http://127.0.0.1:8080;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_request_buffering off;
|
|
proxy_read_timeout {{ .Values.proxy.readTimeout }};
|
|
proxy_send_timeout {{ .Values.proxy.readTimeout }};
|
|
}
|
|
{{- else }}
|
|
location /api/ { return 404; }
|
|
{{- end }}
|
|
|
|
location = /signing-key.asc {
|
|
alias /run/aptly/pub/signing-key.asc;
|
|
default_type text/plain;
|
|
}
|
|
|
|
{{- if $sec.r }}
|
|
location / {
|
|
{{- if $sec.ra }}
|
|
auth_basic "aptly";
|
|
auth_basic_user_file /run/aptly/htpasswd;
|
|
{{- end }}
|
|
root /var/lib/aptly/public;
|
|
autoindex on;
|
|
autoindex_exact_size off;
|
|
|
|
location ~* /(InRelease|Release|Release\.gpg|Packages(\.[a-z0-9]+)?|Sources(\.[a-z0-9]+)?)$ {
|
|
{{- if $sec.ra }}
|
|
auth_basic "aptly";
|
|
auth_basic_user_file /run/aptly/htpasswd;
|
|
{{- end }}
|
|
root /var/lib/aptly/public;
|
|
add_header Cache-Control "no-cache" always;
|
|
}
|
|
}
|
|
{{- if .Values.proxy.compatPaths }}
|
|
location /repos/{{ .Values.proxy.publishEndpointName }}/ {
|
|
{{- if $sec.ra }}
|
|
auth_basic "aptly";
|
|
auth_basic_user_file /run/aptly/htpasswd;
|
|
{{- end }}
|
|
alias /var/lib/aptly/public/;
|
|
autoindex on;
|
|
}
|
|
{{- end }}
|
|
{{- else }}
|
|
location / { return 404; }
|
|
{{- end }}
|
|
}
|
|
{{- if .Values.metrics.service.enabled }}
|
|
|
|
# Separate, unauthenticated listener so scraping never needs the write-path
|
|
# credentials and a ServiceMonitor never needs a basicAuth secret.
|
|
server {
|
|
listen 9090;
|
|
server_name _;
|
|
location = /api/metrics { proxy_pass http://127.0.0.1:8080; }
|
|
location / { return 404; }
|
|
}
|
|
{{- end }}
|
|
{{- end -}}
|