Publish reusable Quay chart with optional PostgreSQL and Valkey
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
name: Validate, publish and promote Quay chart
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
jobs:
|
||||
chart:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: v4.3.0
|
||||
- name: Validate chart
|
||||
run: |
|
||||
python3 -m venv "${RUNNER_TEMP}/chart-test"
|
||||
"${RUNNER_TEMP}/chart-test/bin/pip" install PyYAML
|
||||
helm dependency build chart/quay
|
||||
PATH="${RUNNER_TEMP}/chart-test/bin:${PATH}" bash chart/test.sh
|
||||
- name: Select immutable chart version
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
run: printf 'CHART_VERSION=%s\n' "$(./scripts/chart-version.sh)" >> "${GITHUB_ENV}"
|
||||
- name: Publish chart
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
env:
|
||||
PACKAGE_USERNAME: ${{ secrets.PACKAGE_USERNAME }}
|
||||
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -n "${PACKAGE_USERNAME}" && test -n "${PACKAGE_TOKEN}"
|
||||
mkdir -p .chart-packages
|
||||
helm package chart/quay --destination .chart-packages --version "${CHART_VERSION}"
|
||||
curl --fail --silent --show-error --user "${PACKAGE_USERNAME}:${PACKAGE_TOKEN}" \
|
||||
--request POST --upload-file ".chart-packages/quay-${CHART_VERSION}.tgz" \
|
||||
https://code.brunner.ninja/api/packages/feedc0de/helm/api/charts
|
||||
- name: Check current main
|
||||
id: current-main
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
env:
|
||||
COMMIT_SHA: ${{ gitea.sha }}
|
||||
run: |
|
||||
git fetch --no-tags origin main
|
||||
if test "$(git rev-parse FETCH_HEAD)" = "${COMMIT_SHA}"; then
|
||||
echo 'promote=true' >> "${GITEA_OUTPUT}"
|
||||
else
|
||||
echo 'promote=false' >> "${GITEA_OUTPUT}"
|
||||
fi
|
||||
- uses: actions/checkout@v4
|
||||
if: steps.current-main.outputs.promote == 'true'
|
||||
with:
|
||||
repository: feedc0de/homelab-gitops
|
||||
ref: main
|
||||
token: ${{ secrets.GITOPS_TOKEN }}
|
||||
path: homelab-gitops
|
||||
- name: Promote chart
|
||||
if: steps.current-main.outputs.promote == 'true'
|
||||
working-directory: homelab-gitops
|
||||
run: |
|
||||
set -euo pipefail
|
||||
./scripts/update-chart-version.sh quay "${CHART_VERSION}"
|
||||
git config user.name feedc0de
|
||||
git config user.email feedc0de@users.noreply.code.brunner.ninja
|
||||
git add bootstrap/quay.yaml
|
||||
if git diff --cached --quiet; then exit 0; fi
|
||||
git commit -m "Deploy Quay chart ${CHART_VERSION}"
|
||||
for attempt in 1 2 3; do
|
||||
git push origin HEAD:main && exit 0
|
||||
test "${attempt}" = 3 && exit 1
|
||||
git fetch origin main
|
||||
git rebase origin/main
|
||||
done
|
||||
@@ -1,3 +1,5 @@
|
||||
config.yaml
|
||||
*.kubeconfig
|
||||
.env
|
||||
chart/quay/charts/
|
||||
.chart-packages/
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
# Quay on Kubernetes
|
||||
|
||||
Plain Kubernetes deployment for `https://registry.brunner.ninja` in the
|
||||
`default` namespace. Quay updates are deliberately simple: change both matching
|
||||
`quay.io/projectquay/quay` image references in `quay.yaml`, commit, and push to
|
||||
`main`. Gitea Actions performs the Kubernetes update.
|
||||
This repository now maintains a [reusable Helm chart](chart/README.md) for
|
||||
Project Quay. It declares Quay's registry and repository mirror, optional
|
||||
PostgreSQL and Valkey subcharts, and the Service and Ingress. The chart uses
|
||||
the official Quay image and is published to `https://brunner.ninja/charts`.
|
||||
|
||||
The deployment uses plain YAML. There is no Helm chart, Operator, Kustomize,
|
||||
Argo CD, or custom container build.
|
||||
The homelab migration is staged in
|
||||
[`homelab-gitops/apps/quay`](https://code.brunner.ninja/feedc0de/homelab-gitops/src/branch/main/apps/quay).
|
||||
Until Argo CD has adopted and verified the live Deployments, the older plain
|
||||
manifest and direct-deploy workflow below remain the rollback path. Do not
|
||||
change `quay.yaml` during the database cutover.
|
||||
|
||||
## Persistent data
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Quay Helm chart
|
||||
|
||||
This chart runs the official Project Quay image and owns the registry
|
||||
Deployment, repository-mirror Deployment, Service, Ingress, and probes. It can
|
||||
also deploy PostgreSQL and Valkey through optional Bitnami subcharts. It does
|
||||
not create a second container image.
|
||||
|
||||
An S3-compatible object store and bucket are required. Provision them outside
|
||||
this chart, then create a private `config.yaml` using
|
||||
[`config.yaml.example`](config.yaml.example). The file includes the database
|
||||
URI, S3 credentials, Redis credentials, and two persistent Quay encryption
|
||||
keys. Keep all of those out of Helm values and public Git. Store the file in a
|
||||
Secret in the release namespace:
|
||||
|
||||
```sh
|
||||
kubectl -n default create secret generic quay-config --from-file=config.yaml
|
||||
```
|
||||
|
||||
Set `quay.configSecretName` to that Secret name. For an existing installation,
|
||||
preserve its `SECRET_KEY`, `DATABASE_SECRET_KEY`, bucket, and database records.
|
||||
The chart never creates or deletes S3 objects or buckets.
|
||||
|
||||
The optional PostgreSQL subchart is enabled with `postgresql.enabled=true`.
|
||||
Use a durable storage class and stable `postgresql.auth.existingSecret`; the
|
||||
`database-password` key must match the password inside Quay's `DB_URI`. The
|
||||
chart also accepts an external PostgreSQL server with the subchart disabled.
|
||||
Likewise, `valkey.enabled=true` installs a cache; keep the Redis hosts and
|
||||
password in `config.yaml` aligned with either that subchart or your external
|
||||
cache. Both dependencies are independently optional.
|
||||
|
||||
Published releases are available from the indexed Helm repository:
|
||||
|
||||
```sh
|
||||
helm repo add brunner.ninja https://brunner.ninja/charts
|
||||
helm repo update brunner.ninja
|
||||
helm upgrade --install quay brunner.ninja/quay --namespace default \
|
||||
--values values.yaml --wait --timeout 15m
|
||||
```
|
||||
|
||||
For local development, run `helm dependency build chart/quay` and
|
||||
`bash chart/test.sh`. Chart package versions derive from the actual Quay Git
|
||||
release tag with `git describe`; `Chart.yaml.appVersion` is the tested upstream
|
||||
image version. Homelab-specific values are in
|
||||
[`homelab-gitops/apps/quay`](https://code.brunner.ninja/feedc0de/homelab-gitops/src/branch/main/apps/quay).
|
||||
@@ -0,0 +1,30 @@
|
||||
# Create a private config.yaml from this template and store it as the
|
||||
# config.yaml key in the Secret selected by quay.configSecretName.
|
||||
SETUP_COMPLETE: true
|
||||
SERVER_HOSTNAME: quay.example.org
|
||||
PREFERRED_URL_SCHEME: https
|
||||
EXTERNAL_TLS_TERMINATION: true
|
||||
AUTHENTICATION_TYPE: Database
|
||||
SECRET_KEY: REPLACE_WITH_STABLE_RANDOM_SECRET_KEY
|
||||
DATABASE_SECRET_KEY: REPLACE_WITH_STABLE_RANDOM_DATABASE_SECRET_KEY
|
||||
DB_URI: postgresql://quay:REPLACE_WITH_DATABASE_PASSWORD@quay-postgresql.default.svc.cluster.local/quay
|
||||
BUILDLOGS_REDIS:
|
||||
host: quay-valkey-primary.default.svc.cluster.local
|
||||
port: 6379
|
||||
password: REPLACE_WITH_VALKEY_PASSWORD
|
||||
USER_EVENTS_REDIS:
|
||||
host: quay-valkey-primary.default.svc.cluster.local
|
||||
port: 6379
|
||||
password: REPLACE_WITH_VALKEY_PASSWORD
|
||||
DISTRIBUTED_STORAGE_CONFIG:
|
||||
default:
|
||||
- RadosGWStorage
|
||||
- access_key: REPLACE_WITH_S3_ACCESS_KEY
|
||||
secret_key: REPLACE_WITH_S3_SECRET_KEY
|
||||
bucket_name: REPLACE_WITH_EXISTING_BUCKET
|
||||
hostname: REPLACE_WITH_S3_HOST
|
||||
is_secure: true
|
||||
storage_path: /
|
||||
DISTRIBUTED_STORAGE_DEFAULT_LOCATIONS: []
|
||||
DISTRIBUTED_STORAGE_PREFERENCE:
|
||||
- default
|
||||
@@ -0,0 +1,9 @@
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
version: 18.12.4
|
||||
- name: valkey
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
version: 6.3.4
|
||||
digest: sha256:ae4c73e405ad893c310aeac3f35de37f7c8cbbd28a7b3089d973a680048990cc
|
||||
generated: "2026-10-20T08:46:49.462960335+02:00"
|
||||
@@ -0,0 +1,15 @@
|
||||
apiVersion: v2
|
||||
name: quay
|
||||
description: Project Quay registry with optional PostgreSQL and Valkey
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: 3.18.0
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 18.12.4
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
condition: postgresql.enabled
|
||||
- name: valkey
|
||||
version: 6.3.4
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
condition: valkey.enabled
|
||||
@@ -0,0 +1,70 @@
|
||||
{{- if .Values.quay.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: quay
|
||||
labels:
|
||||
app: quay
|
||||
app.kubernetes.io/name: quay
|
||||
app.kubernetes.io/component: registry
|
||||
spec:
|
||||
replicas: {{ .Values.quay.replicas }}
|
||||
progressDeadlineSeconds: 900
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: quay
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: quay
|
||||
app.kubernetes.io/name: quay
|
||||
app.kubernetes.io/component: registry
|
||||
spec:
|
||||
containers:
|
||||
- name: quay
|
||||
image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}"
|
||||
env:
|
||||
- name: DEBUGLOG
|
||||
value: {{ .Values.quay.debugLog | quote }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /health/instance
|
||||
port: http
|
||||
failureThreshold: 60
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 5
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health/endtoend
|
||||
port: http
|
||||
failureThreshold: 3
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health/instance
|
||||
port: http
|
||||
failureThreshold: 3
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
{{- with .Values.resources }}
|
||||
resources:
|
||||
{{ toYaml . | indent 12 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: quay-config
|
||||
mountPath: /conf/stack/config.yaml
|
||||
subPath: config.yaml
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: quay-config
|
||||
secret:
|
||||
secretName: {{ .Values.quay.configSecretName | quote }}
|
||||
defaultMode: 0440
|
||||
{{- end }}
|
||||
@@ -0,0 +1,29 @@
|
||||
{{- if and .Values.quay.enabled .Values.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: quay
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{ toYaml . | indent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.ingress.className }}
|
||||
ingressClassName: {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- with .Values.ingress.tls }}
|
||||
tls:
|
||||
{{ toYaml . | indent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: {{ .Values.ingress.host | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: quay
|
||||
port:
|
||||
name: http
|
||||
{{- end }}
|
||||
@@ -0,0 +1,45 @@
|
||||
{{- if and .Values.quay.enabled .Values.repomirror.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: quay-repomirror
|
||||
labels:
|
||||
app: quay-repomirror
|
||||
app.kubernetes.io/name: quay
|
||||
app.kubernetes.io/component: repository-mirror
|
||||
spec:
|
||||
replicas: {{ .Values.repomirror.replicas }}
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: quay-repomirror
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: quay-repomirror
|
||||
app.kubernetes.io/name: quay
|
||||
app.kubernetes.io/component: repository-mirror
|
||||
spec:
|
||||
containers:
|
||||
- name: quay-repomirror
|
||||
image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}"
|
||||
args: [repomirror]
|
||||
env:
|
||||
- name: DEBUGLOG
|
||||
value: {{ .Values.quay.debugLog | quote }}
|
||||
{{- with .Values.repomirrorResources }}
|
||||
resources:
|
||||
{{ toYaml . | indent 12 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: quay-config
|
||||
mountPath: /conf/stack/config.yaml
|
||||
subPath: config.yaml
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: quay-config
|
||||
secret:
|
||||
secretName: {{ .Values.quay.configSecretName | quote }}
|
||||
defaultMode: 0440
|
||||
{{- end }}
|
||||
@@ -0,0 +1,17 @@
|
||||
{{- if .Values.quay.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: quay
|
||||
labels:
|
||||
app.kubernetes.io/name: quay
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
protocol: TCP
|
||||
targetPort: 8080
|
||||
selector:
|
||||
app: quay
|
||||
{{- end }}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"image": {"type": "object", "properties": {"repository": {"type": "string", "minLength": 1}, "tag": {"type": "string"}}},
|
||||
"quay": {"type": "object", "required": ["enabled", "configSecretName"], "properties": {"enabled": {"type": "boolean"}, "replicas": {"type": "integer", "minimum": 0}, "configSecretName": {"type": "string", "minLength": 1}, "debugLog": {"type": "boolean"}}},
|
||||
"repomirror": {"type": "object", "properties": {"enabled": {"type": "boolean"}, "replicas": {"type": "integer", "minimum": 0}}},
|
||||
"service": {"type": "object", "properties": {"port": {"type": "integer", "minimum": 1, "maximum": 65535}}},
|
||||
"ingress": {"type": "object", "properties": {"enabled": {"type": "boolean"}, "host": {"type": "string", "minLength": 1}, "className": {"type": "string"}, "annotations": {"type": "object", "additionalProperties": {"type": "string"}}, "tls": {"type": "array"}}},
|
||||
"resources": {"type": "object"},
|
||||
"repomirrorResources": {"type": "object"}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
image:
|
||||
repository: quay.io/projectquay/quay
|
||||
tag: ""
|
||||
|
||||
quay:
|
||||
enabled: true
|
||||
replicas: 1
|
||||
configSecretName: quay-config
|
||||
debugLog: false
|
||||
|
||||
repomirror:
|
||||
enabled: true
|
||||
replicas: 1
|
||||
|
||||
service:
|
||||
port: 80
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
host: quay.example.org
|
||||
annotations: {}
|
||||
tls: []
|
||||
|
||||
resources: {}
|
||||
repomirrorResources: {}
|
||||
|
||||
postgresql:
|
||||
enabled: false
|
||||
auth:
|
||||
username: quay
|
||||
database: quay
|
||||
|
||||
valkey:
|
||||
enabled: false
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
chart_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/quay" && pwd)
|
||||
helm lint "$chart_dir" --strict
|
||||
|
||||
python3 - "$chart_dir" <<'PY'
|
||||
import subprocess
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
chart = sys.argv[1]
|
||||
|
||||
def render(*args):
|
||||
output = subprocess.check_output(
|
||||
['helm', 'template', 'quay', chart, '--namespace', 'default', *args], text=True
|
||||
)
|
||||
return {(doc['kind'], doc['metadata']['name']): doc for doc in yaml.safe_load_all(output) if doc}
|
||||
|
||||
active = render()
|
||||
assert {('Deployment', 'quay'), ('Deployment', 'quay-repomirror'),
|
||||
('Service', 'quay')} <= active.keys()
|
||||
registry = active['Deployment', 'quay']
|
||||
mirror = active['Deployment', 'quay-repomirror']
|
||||
assert registry['spec']['selector']['matchLabels'] == {'app': 'quay'}
|
||||
assert mirror['spec']['selector']['matchLabels'] == {'app': 'quay-repomirror'}
|
||||
assert registry['spec']['strategy']['type'] == mirror['spec']['strategy']['type'] == 'Recreate'
|
||||
assert registry['spec']['template']['spec']['containers'][0]['image'] == mirror['spec']['template']['spec']['containers'][0]['image']
|
||||
assert registry['spec']['template']['spec']['volumes'][0]['secret']['secretName'] == 'quay-config'
|
||||
assert active['Service', 'quay']['spec']['selector'] == {'app': 'quay'}
|
||||
|
||||
staged = render('--set', 'quay.enabled=false', '--set', 'postgresql.enabled=true')
|
||||
assert ('StatefulSet', 'quay-postgresql') in staged
|
||||
assert not any(name == 'quay' or name == 'quay-repomirror' for kind, name in staged)
|
||||
assert ('Service', 'quay-postgresql') in staged
|
||||
PY
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
description=$(git -C "$project_dir" describe --tags --long --match 'v[0-9]*' --dirty)
|
||||
case "$description" in
|
||||
*-dirty) echo 'Refusing to package a dirty tree.' >&2; exit 1 ;;
|
||||
esac
|
||||
version=${description#v}
|
||||
case "$version" in
|
||||
*-0-g*) version=${version%%-0-g*} ;;
|
||||
esac
|
||||
printf '%s\n' "$version"
|
||||
Reference in New Issue
Block a user