Retire direct Quay deployment after Argo adoption
Validate, publish and promote Quay chart / chart (push) Successful in 45s
Validate, publish and promote Quay chart / chart (push) Successful in 45s
This commit is contained in:
1 parent
53d2639080
commit
fd919cdb54
9 files changed
+35
-750
No files matched your search
@@ -1,94 +0,0 @@
|
||||
name: Validate and deploy Quay
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
KUBECTL_VERSION: v1.36.3
|
||||
KUBERNETES_API: https://host.containers.internal:6443
|
||||
KUBERNETES_TLS_SERVER_NAME: 192.168.0.2
|
||||
NAMESPACE: default
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Validate manifest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Validate scripts and Kubernetes resources
|
||||
run: bash test.sh
|
||||
|
||||
deploy:
|
||||
name: Downtime deployment
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
needs: validate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install kubectl
|
||||
run: |
|
||||
curl --fail --silent --show-error --location \
|
||||
--output "${RUNNER_TEMP}/kubectl" \
|
||||
"https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
|
||||
chmod 0700 "${RUNNER_TEMP}/kubectl"
|
||||
|
||||
- name: Configure Kubernetes access
|
||||
env:
|
||||
KUBE_CONFIG_BASE64: ${{ secrets.KUBE_CONFIG_BASE64 }}
|
||||
run: |
|
||||
test -n "${KUBE_CONFIG_BASE64}"
|
||||
printf '%s' "${KUBE_CONFIG_BASE64}" | base64 --decode > "${RUNNER_TEMP}/kubeconfig"
|
||||
chmod 0600 "${RUNNER_TEMP}/kubeconfig"
|
||||
export KUBECONFIG="${RUNNER_TEMP}/kubeconfig"
|
||||
"${RUNNER_TEMP}/kubectl" config set-cluster cluster \
|
||||
--server="${KUBERNETES_API}" \
|
||||
--tls-server-name="${KUBERNETES_TLS_SERVER_NAME}"
|
||||
|
||||
- name: Validate desired state on the server
|
||||
run: |
|
||||
export KUBECONFIG="${RUNNER_TEMP}/kubeconfig"
|
||||
"${RUNNER_TEMP}/kubectl" apply \
|
||||
--dry-run=server \
|
||||
--validate=false \
|
||||
--filename quay.yaml
|
||||
|
||||
- name: Apply and verify
|
||||
run: |
|
||||
export KUBECONFIG="${RUNNER_TEMP}/kubeconfig"
|
||||
"${RUNNER_TEMP}/kubectl" apply \
|
||||
--validate=false \
|
||||
--filename quay.yaml
|
||||
|
||||
for deployment in quay quay-repomirror; do
|
||||
for attempt in {1..180}; do
|
||||
IFS='|' read -r generation observed desired updated ready available unavailable image <<< "$(
|
||||
"${RUNNER_TEMP}/kubectl" --namespace "${NAMESPACE}" \
|
||||
get "deployment/${deployment}" \
|
||||
--output=jsonpath='{.metadata.generation}|{.status.observedGeneration}|{.spec.replicas}|{.status.updatedReplicas}|{.status.readyReplicas}|{.status.availableReplicas}|{.status.unavailableReplicas}|{.spec.template.spec.containers[0].image}'
|
||||
)"
|
||||
|
||||
echo "${deployment} ${attempt}/180: generation ${observed}/${generation}, replicas ${updated}/${desired} updated, ${ready}/${desired} ready, ${available}/${desired} available, image ${image}"
|
||||
if [[ "${observed}" == "${generation}" \
|
||||
&& "${updated}" == "${desired}" \
|
||||
&& "${ready}" == "${desired}" \
|
||||
&& "${available}" == "${desired}" \
|
||||
&& ( -z "${unavailable}" || "${unavailable}" == "0" ) ]]; then
|
||||
break
|
||||
fi
|
||||
|
||||
if [[ "${attempt}" == 180 ]]; then
|
||||
echo "${deployment} rollout did not complete within fifteen minutes" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
done
|
||||
|
||||
curl --fail --silent --show-error \
|
||||
--max-time 15 \
|
||||
https://registry.brunner.ninja/health/endtoend >/dev/null
|
||||
@@ -1,139 +1,28 @@
|
||||
# Quay on Kubernetes
|
||||
# Project Quay Helm chart
|
||||
|
||||
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 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
|
||||
|
||||
Quay itself has no persistent volume. Durable state remains in existing
|
||||
external services:
|
||||
|
||||
- metadata, users, permissions, and tags: PostgreSQL database `quay` through
|
||||
`postgresql.default.svc.cluster.local`;
|
||||
- image layers and artifacts: Rook Ceph RGW bucket `quay`;
|
||||
- events and build logs: the existing Valkey service in `default`.
|
||||
|
||||
Applying `quay.yaml` does not recreate or delete the PostgreSQL database or RGW
|
||||
bucket. The `CephObjectStoreUser` declaration is kept separately in
|
||||
`ceph-object-user.yaml` and is not managed by CI.
|
||||
|
||||
## Configuration Secret
|
||||
|
||||
The historical manifest stored the complete private Quay configuration in a
|
||||
ConfigMap. The Deployment now mounts `default/secret/quay-config` instead.
|
||||
|
||||
On its first run, `install.sh` copies the existing ConfigMap directly into that
|
||||
Secret without printing the configuration. After the new pods are healthy, the
|
||||
script prints the command that can be used to remove the legacy plaintext
|
||||
ConfigMap. It does not delete it automatically.
|
||||
|
||||
For future configuration changes, copy `config.yaml.example` to the ignored
|
||||
`config.yaml`, fill every placeholder, and run `./install.sh`. Preserve the
|
||||
existing `SECRET_KEY` and `DATABASE_SECRET_KEY`; they are persistent Quay
|
||||
cryptographic keys and should not be regenerated during normal updates.
|
||||
|
||||
`DEFAULT_UI: angular` keeps the legacy logged-out landing page, including its
|
||||
public repository browsing experience. Anonymous access remains controlled by
|
||||
`FEATURE_ANONYMOUS_ACCESS: true`. The Angular UI is deprecated upstream, so a
|
||||
future Quay release may require returning to the React UI.
|
||||
|
||||
An exported Kubernetes Secret YAML is not a safe public backup because its
|
||||
values are merely base64 encoded. Keep an encrypted off-cluster copy of the
|
||||
actual configuration in addition to PostgreSQL and RGW backups.
|
||||
|
||||
## Initial installation
|
||||
|
||||
With an administrator kubeconfig active, run:
|
||||
This repository maintains a [portable Quay chart](chart/README.md) with the
|
||||
official Quay image, registry and mirror Deployments, Service, Ingress, probes,
|
||||
and optional PostgreSQL and Valkey subcharts. It is published to the public
|
||||
[brunner.ninja Helm repository](https://brunner.ninja/charts):
|
||||
|
||||
```sh
|
||||
./install.sh
|
||||
helm repo add brunner.ninja https://brunner.ninja/charts
|
||||
helm repo update brunner.ninja
|
||||
```
|
||||
|
||||
This creates the configuration Secret when necessary, applies the existing
|
||||
Rook object user and all Quay resources, and waits for both Deployments.
|
||||
For a cluster installation, first provision an S3-compatible bucket and a
|
||||
private Quay `config.yaml` Secret as described in the chart documentation.
|
||||
Set the chart values for your database, cache, hostname, and storage, then run
|
||||
`QUAY_VALUES_FILE=values.yaml ./install.sh`. The script performs a waiting Helm
|
||||
upgrade. The chart does not create or delete S3 buckets or their contents.
|
||||
|
||||
The registry Deployment has startup, readiness, and liveness probes. Both
|
||||
Deployments use `Recreate`, accepting downtime and ensuring old and new Quay
|
||||
versions do not access PostgreSQL concurrently.
|
||||
The [homelab Quay Argo CD application](https://code.brunner.ninja/feedc0de/homelab-gitops/src/branch/main/apps/quay)
|
||||
holds only this cluster's values and the Rook object user. On this cluster,
|
||||
Argo CD is the normal deployment writer; do not run `install.sh` against it.
|
||||
The old direct-deploy workflow, kubeconfig, and duplicate manifests were
|
||||
retired after adoption. PostgreSQL and Ceph data were preserved.
|
||||
|
||||
## Automatic tag updates
|
||||
|
||||
To update Quay, change both identical `image:` fields in `quay.yaml`, for
|
||||
example from a 3.8 image to the selected 3.9 image, then commit and push:
|
||||
|
||||
```sh
|
||||
git add quay.yaml
|
||||
git commit -m "Update Quay to 3.9.x"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
The workflow validates every push and pull request. A successful push to
|
||||
`main` performs a server-side dry run, applies the complete manifest, waits for
|
||||
both Deployments, and checks the public `/health/endtoend` endpoint.
|
||||
|
||||
Kubernetes handles pod replacement through each Deployment's native `Recreate`
|
||||
strategy. If the pod template did not change, applying the file does not restart
|
||||
the pods. When an image reference or another pod-template field changes,
|
||||
Kubernetes stops that Deployment's old pod before starting its replacement.
|
||||
|
||||
If the new registry cannot migrate or become healthy, CI fails and leaves the
|
||||
registry stopped or unhealthy for investigation. It does not automatically
|
||||
start the old image because a completed database migration might not be
|
||||
backwards compatible.
|
||||
|
||||
Quay requires supported intermediate database migrations. Reach a distant
|
||||
target using successive supported-version commits rather than skipping from
|
||||
3.8 directly to 3.18. Kubernetes actions require no manual intervention, but a
|
||||
Git commit cannot make an unsupported database-version jump safe.
|
||||
|
||||
Applying an unchanged pod template does not restart the registry, so a
|
||||
README-only commit still verifies the live deployment without causing Quay's
|
||||
`Recreate` downtime.
|
||||
|
||||
## Gitea Actions bootstrap
|
||||
|
||||
CI requires one repository Actions secret:
|
||||
|
||||
- `KUBE_CONFIG_BASE64`: restricted Quay deployment kubeconfig.
|
||||
|
||||
After `./install.sh` has created all application resources, run once:
|
||||
|
||||
```sh
|
||||
./create-ci-kubeconfig.sh
|
||||
```
|
||||
|
||||
For remote cluster access:
|
||||
|
||||
```sh
|
||||
KUBECTL_SSH_HOST=arschrock ./create-ci-kubeconfig.sh
|
||||
```
|
||||
|
||||
Store its final base64 line as the Gitea Actions secret
|
||||
`KUBE_CONFIG_BASE64`. After that bootstrap, tag updates are Git-only.
|
||||
|
||||
The deployment identity can update only the existing Deployments `quay` and
|
||||
`quay-repomirror`, Service `quay`, and Ingress `quay` in `default`. It cannot
|
||||
read or modify Secrets, PostgreSQL, Valkey, Ceph resources, or the RGW bucket.
|
||||
|
||||
The workflow matches the existing brunner.ninja runner: Kubernetes API URL
|
||||
`https://host.containers.internal:6443`, with TLS server name `192.168.0.2`.
|
||||
|
||||
## Local validation
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
./test.sh
|
||||
```
|
||||
|
||||
The test checks shell syntax, requires both Deployments to use one identical
|
||||
non-`latest` image reference, and validates the Kubernetes manifests with
|
||||
kubeconform.
|
||||
Chart releases use Quay's application-version Git tag as the base and append
|
||||
the commit count and hash for chart-only changes. Main-branch CI tests,
|
||||
publishes, and promotes the exact chart version to the homelab GitOps repo.
|
||||
Run `./test.sh` locally to lint and render the chart.
|
||||
@@ -1,9 +0,0 @@
|
||||
# This is a cluster prerequisite, not part of automated Quay deployments.
|
||||
# Reapplying it does not recreate or delete the existing RGW bucket.
|
||||
apiVersion: ceph.rook.io/v1
|
||||
kind: CephObjectStoreUser
|
||||
metadata:
|
||||
name: quay
|
||||
namespace: rook-ceph
|
||||
spec:
|
||||
store: my-store
|
||||
@@ -1,69 +0,0 @@
|
||||
# Bootstrap once with an administrator identity. CI can update only the
|
||||
# existing public Quay resources and cannot access Kubernetes Secrets.
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: quay-deployer
|
||||
namespace: default
|
||||
automountServiceAccountToken: false
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: quay-deployer
|
||||
namespace: default
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- services
|
||||
resourceNames:
|
||||
- quay
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- deployments
|
||||
resourceNames:
|
||||
- quay
|
||||
- quay-repomirror
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingresses
|
||||
resourceNames:
|
||||
- quay
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: quay-deployer
|
||||
namespace: default
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: quay-deployer
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: quay-deployer
|
||||
namespace: default
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: quay-deployer-token
|
||||
namespace: default
|
||||
annotations:
|
||||
kubernetes.io/service-account.name: quay-deployer
|
||||
type: kubernetes.io/service-account-token
|
||||
@@ -1,122 +0,0 @@
|
||||
# Copy this file to config.yaml and replace every REPLACE_WITH_* value.
|
||||
# config.yaml is ignored by Git and is installed as the quay/quay-config Secret.
|
||||
ALLOW_PULLS_WITHOUT_STRICT_LOGGING: true
|
||||
ALLOWED_OCI_ARTIFACT_TYPES:
|
||||
application/vnd.oci.image.config.v1+json:
|
||||
- application/vnd.oci.image.layer.v1.tar+zstd
|
||||
application/vnd.sylabs.sif.config.v1+json:
|
||||
- application/vnd.sylabs.sif.layer.v1+tar
|
||||
AUTHENTICATION_TYPE: Database
|
||||
AVATAR_KIND: local
|
||||
BUILDLOGS_REDIS:
|
||||
host: valkey-primary.default.svc.cluster.local
|
||||
password: REPLACE_WITH_VALKEY_PASSWORD
|
||||
port: 6379
|
||||
CONTACT_INFO:
|
||||
- https://brunner.ninja
|
||||
DATABASE_SECRET_KEY: REPLACE_WITH_PERSISTENT_RANDOM_DATABASE_SECRET_KEY
|
||||
DB_URI: postgresql://quay:REPLACE_WITH_QUAY_DATABASE_PASSWORD@postgresql.default.svc.cluster.local/quay
|
||||
DEFAULT_UI: angular
|
||||
DEFAULT_TAG_EXPIRATION: 2w
|
||||
DISTRIBUTED_STORAGE_CONFIG:
|
||||
default:
|
||||
- RadosGWStorage
|
||||
- access_key: REPLACE_WITH_ROOK_RGW_ACCESS_KEY
|
||||
bucket_name: quay
|
||||
hostname: rook-ceph-rgw-my-store.rook-ceph.svc.cluster.local
|
||||
is_secure: false
|
||||
secret_key: REPLACE_WITH_ROOK_RGW_SECRET_KEY
|
||||
storage_path: /
|
||||
DISTRIBUTED_STORAGE_DEFAULT_LOCATIONS: []
|
||||
DISTRIBUTED_STORAGE_PREFERENCE:
|
||||
- default
|
||||
EXTERNAL_TLS_TERMINATION: true
|
||||
FEATURE_ACI_CONVERSION: false
|
||||
FEATURE_ACTION_LOG_ROTATION: false
|
||||
FEATURE_ANONYMOUS_ACCESS: true
|
||||
FEATURE_APP_REGISTRY: true
|
||||
FEATURE_APP_SPECIFIC_TOKENS: true
|
||||
FEATURE_BITBUCKET_BUILD: false
|
||||
FEATURE_BLACKLISTED_EMAILS: false
|
||||
FEATURE_BUILD_SUPPORT: true
|
||||
FEATURE_CHANGE_TAG_EXPIRATION: true
|
||||
FEATURE_DIRECT_LOGIN: true
|
||||
FEATURE_EXTENDED_REPOSITORY_NAMES: true
|
||||
FEATURE_FIPS: false
|
||||
FEATURE_GITHUB_BUILD: false
|
||||
FEATURE_GITHUB_LOGIN: false
|
||||
FEATURE_GITLAB_BUILD: false
|
||||
FEATURE_GOOGLE_LOGIN: false
|
||||
FEATURE_INVITE_ONLY_USER_CREATION: false
|
||||
FEATURE_MAILING: true
|
||||
FEATURE_NONSUPERUSER_TEAM_SYNCING_SETUP: false
|
||||
FEATURE_PARTIAL_USER_AUTOCOMPLETE: true
|
||||
FEATURE_PROXY_STORAGE: true
|
||||
FEATURE_REPO_MIRROR: true
|
||||
FEATURE_REQUIRE_TEAM_INVITE: true
|
||||
FEATURE_RESTRICTED_V1_PUSH: true
|
||||
FEATURE_SECURITY_NOTIFICATIONS: false
|
||||
FEATURE_SECURITY_SCANNER: false
|
||||
FEATURE_STORAGE_REPLICATION: false
|
||||
FEATURE_TEAM_SYNCING: false
|
||||
FEATURE_USER_CREATION: true
|
||||
FEATURE_USER_LAST_ACCESSED: true
|
||||
FEATURE_USER_LOG_ACCESS: false
|
||||
FEATURE_USER_METADATA: false
|
||||
FEATURE_USER_RENAME: false
|
||||
FEATURE_USERNAME_CONFIRMATION: true
|
||||
FRESH_LOGIN_TIMEOUT: 10m
|
||||
GITHUB_LOGIN_CONFIG: {}
|
||||
GITHUB_TRIGGER_CONFIG: {}
|
||||
GITLAB_TRIGGER_KIND: {}
|
||||
LDAP_ALLOW_INSECURE_FALLBACK: false
|
||||
LDAP_EMAIL_ATTR: mail
|
||||
LDAP_UID_ATTR: uid
|
||||
LDAP_URI: ldap://localhost
|
||||
LOG_ARCHIVE_LOCATION: default
|
||||
LOGS_MODEL: database
|
||||
LOGS_MODEL_CONFIG: {}
|
||||
MAIL_DEFAULT_SENDER: brunner.ninja Registry <registry@brunner.ninja>
|
||||
MAIL_PORT: 587
|
||||
MAIL_SERVER: smtp-relay.gmail.com
|
||||
MAIL_USE_AUTH: false
|
||||
MAIL_USE_TLS: false
|
||||
PREFERRED_URL_SCHEME: https
|
||||
REGISTRY_LOGIN_CONFIG:
|
||||
CLIENT_ID: REPLACE_WITH_OIDC_CLIENT_ID
|
||||
CLIENT_SECRET: REPLACE_WITH_OIDC_CLIENT_SECRET
|
||||
LOGIN_SCOPES:
|
||||
- openid
|
||||
- email
|
||||
OIDC_SERVER: https://auth.brunner.ninja/application/o/registry/
|
||||
SERVICE_ICON: https://s3.brunner.ninja/authentik/media/public/auth-favicon.png
|
||||
SERVICE_NAME: brunner.ninja Auth
|
||||
VERIFIED_EMAIL_CLAIM_NAME: email
|
||||
OIDC_DISABLE_USER_ENDPOINT: true
|
||||
REGISTRY_TITLE: brunner.ninja Registry
|
||||
REGISTRY_TITLE_SHORT: brunner.ninja Registry
|
||||
REPO_MIRROR_INTERVAL: 30
|
||||
REPO_MIRROR_TLS_VERIFY: true
|
||||
SEARCH_MAX_RESULT_PAGE_COUNT: 10
|
||||
SEARCH_RESULTS_PER_PAGE: 10
|
||||
SECRET_KEY: REPLACE_WITH_PERSISTENT_RANDOM_SECRET_KEY
|
||||
SECURITY_SCANNER_INDEXING_INTERVAL: 30
|
||||
SERVER_HOSTNAME: registry.brunner.ninja
|
||||
SETUP_COMPLETE: true
|
||||
SUPER_USERS:
|
||||
- REPLACE_WITH_QUAY_SUPERUSER
|
||||
TAG_EXPIRATION_OPTIONS:
|
||||
- 0s
|
||||
- 1d
|
||||
- 1w
|
||||
- 2w
|
||||
- 4w
|
||||
TEAM_RESYNC_STALE_TIME: 30m
|
||||
TESTING: false
|
||||
USE_CDN: false
|
||||
USER_EVENTS_REDIS:
|
||||
host: valkey-primary.default.svc.cluster.local
|
||||
password: REPLACE_WITH_VALKEY_PASSWORD
|
||||
port: 6379
|
||||
USER_RECOVERY_TOKEN_LIFETIME: 30m
|
||||
USERFILES_LOCATION: default
|
||||
@@ -1,66 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
namespace=${NAMESPACE:-default}
|
||||
service_account=${SERVICE_ACCOUNT:-quay-deployer}
|
||||
secret=${TOKEN_SECRET:-quay-deployer-token}
|
||||
ssh_host=${KUBECTL_SSH_HOST:-}
|
||||
ssh_config=${SSH_CONFIG:-}
|
||||
|
||||
cluster_kubectl() {
|
||||
if [[ -n "${ssh_host}" ]]; then
|
||||
if [[ -n "${ssh_config}" ]]; then
|
||||
ssh -F "${ssh_config}" "${ssh_host}" kubectl "$@"
|
||||
else
|
||||
ssh "${ssh_host}" kubectl "$@"
|
||||
fi
|
||||
else
|
||||
kubectl "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ -n "${ssh_host}" ]]; then
|
||||
cluster_kubectl apply --filename=- < "${project_dir}/ci-deployer.yaml" >&2
|
||||
else
|
||||
cluster_kubectl apply --filename "${project_dir}/ci-deployer.yaml" >&2
|
||||
fi
|
||||
|
||||
for attempt in {1..30}; do
|
||||
if cluster_kubectl --namespace "${namespace}" get secret "${secret}" \
|
||||
--output=jsonpath='{.data.token}' 2>/dev/null | grep -q .; then
|
||||
break
|
||||
fi
|
||||
if [[ "${attempt}" == 30 ]]; then
|
||||
echo "Timed out waiting for the service-account token" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
workdir=$(mktemp --directory)
|
||||
trap 'rm -rf "${workdir}"' EXIT
|
||||
|
||||
server=$(cluster_kubectl config view --minify --output=jsonpath='{.clusters[0].cluster.server}')
|
||||
cluster_kubectl --namespace "${namespace}" get secret "${secret}" \
|
||||
--output=jsonpath='{.data.ca\.crt}' | base64 --decode > "${workdir}/ca.crt"
|
||||
token=$(cluster_kubectl --namespace "${namespace}" get secret "${secret}" \
|
||||
--output=jsonpath='{.data.token}' | base64 --decode)
|
||||
|
||||
export KUBECONFIG="${workdir}/config"
|
||||
kubectl config set-cluster cluster \
|
||||
--server="${server}" \
|
||||
--certificate-authority="${workdir}/ca.crt" \
|
||||
--embed-certs=true >/dev/null
|
||||
kubectl config set-credentials "${service_account}" --token="${token}" >/dev/null
|
||||
kubectl config set-context quay \
|
||||
--cluster=cluster \
|
||||
--user="${service_account}" \
|
||||
--namespace="${namespace}" >/dev/null
|
||||
kubectl config use-context quay >/dev/null
|
||||
|
||||
echo "Store the following single line as the Gitea Actions secret KUBE_CONFIG_BASE64." >&2
|
||||
echo "Treat it as a password; do not commit it." >&2
|
||||
base64 --wrap=0 "${KUBECONFIG}"
|
||||
printf '\n'
|
||||
+9
-68
@@ -1,75 +1,16 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
namespace=default
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
config_file=${QUAY_CONFIG_FILE:-$script_dir/config.yaml}
|
||||
config_updated=false
|
||||
namespace=${QUAY_NAMESPACE:-default}
|
||||
values_file=${QUAY_VALUES_FILE:-values.yaml}
|
||||
|
||||
for command in kubectl grep mktemp; do
|
||||
if ! command -v "$command" >/dev/null 2>&1; then
|
||||
echo "Required command not found: $command" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
install_config_secret() {
|
||||
source_file=$1
|
||||
|
||||
if grep -q 'REPLACE_WITH_' "$source_file"; then
|
||||
echo "Refusing to install a config containing REPLACE_WITH_ placeholders: $source_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
kubectl create secret generic quay-config \
|
||||
--namespace "$namespace" \
|
||||
--from-file="config.yaml=$source_file" \
|
||||
--dry-run=client \
|
||||
--output=yaml | kubectl apply --filename=-
|
||||
config_updated=true
|
||||
}
|
||||
|
||||
if [ -f "$config_file" ]; then
|
||||
install_config_secret "$config_file"
|
||||
echo "Updated $namespace/secret/quay-config from the private config file."
|
||||
elif kubectl get secret quay-config --namespace "$namespace" >/dev/null 2>&1; then
|
||||
echo "Keeping the existing $namespace/secret/quay-config."
|
||||
elif kubectl get configmap quay-config --namespace "$namespace" >/dev/null 2>&1; then
|
||||
temp_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$temp_dir"' EXIT HUP INT TERM
|
||||
umask 077
|
||||
kubectl get configmap quay-config \
|
||||
--namespace "$namespace" \
|
||||
--output=go-template='{{index .data "config.yaml"}}' > "$temp_dir/config.yaml"
|
||||
install_config_secret "$temp_dir/config.yaml"
|
||||
echo "Copied the legacy ConfigMap into $namespace/secret/quay-config without printing it."
|
||||
else
|
||||
echo "No private config file, Secret, or legacy ConfigMap was found." >&2
|
||||
if [ ! -f "$values_file" ]; then
|
||||
echo "Values file not found: $values_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
kubectl apply --filename "$script_dir/ceph-object-user.yaml"
|
||||
kubectl apply --dry-run=server --filename "$script_dir/quay.yaml"
|
||||
kubectl apply --filename "$script_dir/quay.yaml"
|
||||
|
||||
# The Secret is mounted with subPath, so an explicit Recreate rollout is needed
|
||||
# after a configuration update.
|
||||
if [ "$config_updated" = true ]; then
|
||||
kubectl rollout restart deployment/quay deployment/quay-repomirror \
|
||||
--namespace "$namespace"
|
||||
fi
|
||||
|
||||
kubectl rollout status deployment/quay \
|
||||
--namespace "$namespace" \
|
||||
--timeout=15m
|
||||
kubectl rollout status deployment/quay-repomirror \
|
||||
--namespace "$namespace" \
|
||||
--timeout=15m
|
||||
|
||||
echo
|
||||
echo "Quay is available at https://registry.brunner.ninja."
|
||||
if kubectl get configmap quay-config --namespace "$namespace" >/dev/null 2>&1; then
|
||||
echo "After verifying the new pods, remove the legacy plaintext ConfigMap with:"
|
||||
echo "kubectl delete configmap quay-config --namespace $namespace"
|
||||
fi
|
||||
helm repo add brunner.ninja https://brunner.ninja/charts
|
||||
helm repo update brunner.ninja
|
||||
helm upgrade --install quay brunner.ninja/quay \
|
||||
--namespace "$namespace" --create-namespace \
|
||||
--values "$values_file" --wait --timeout 15m
|
||||
@@ -1,146 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: quay
|
||||
namespace: default
|
||||
labels:
|
||||
app: quay
|
||||
app.kubernetes.io/name: quay
|
||||
app.kubernetes.io/component: registry
|
||||
spec:
|
||||
replicas: 1
|
||||
progressDeadlineSeconds: 900
|
||||
strategy:
|
||||
# Version changes deliberately accept downtime so two Quay versions never
|
||||
# access the database concurrently.
|
||||
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
|
||||
# Quay 3.8.1 is the currently running database-compatible version.
|
||||
# Upgrade one supported minor version at a time; do not jump to 3.18.
|
||||
image: quay.io/projectquay/quay:3.18.0
|
||||
env:
|
||||
- name: DEBUGLOG
|
||||
value: "true"
|
||||
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
|
||||
volumeMounts:
|
||||
- name: quay-config
|
||||
mountPath: /conf/stack/config.yaml
|
||||
subPath: config.yaml
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: quay-config
|
||||
secret:
|
||||
secretName: quay-config
|
||||
defaultMode: 0440
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: quay-repomirror
|
||||
namespace: default
|
||||
labels:
|
||||
app: quay-repomirror
|
||||
app.kubernetes.io/name: quay
|
||||
app.kubernetes.io/component: repository-mirror
|
||||
spec:
|
||||
replicas: 1
|
||||
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: quay.io/projectquay/quay:3.18.0
|
||||
args:
|
||||
- repomirror
|
||||
env:
|
||||
- name: DEBUGLOG
|
||||
value: "true"
|
||||
volumeMounts:
|
||||
- name: quay-config
|
||||
mountPath: /conf/stack/config.yaml
|
||||
subPath: config.yaml
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: quay-config
|
||||
secret:
|
||||
secretName: quay-config
|
||||
defaultMode: 0440
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: quay
|
||||
namespace: default
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
protocol: TCP
|
||||
targetPort: 8080
|
||||
selector:
|
||||
app: quay
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: quay
|
||||
namespace: default
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: registry.brunner.ninja
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: quay
|
||||
port:
|
||||
name: http
|
||||
@@ -1,44 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
|
||||
sh -n "${project_dir}/install.sh"
|
||||
bash -n "${project_dir}/create-ci-kubeconfig.sh"
|
||||
bash -n "${project_dir}/test.sh"
|
||||
|
||||
mapfile -t quay_images < <(
|
||||
sed -n 's|^[[:space:]]*image: \(quay.io/projectquay/quay[^[:space:]]*\)$|\1|p' \
|
||||
"${project_dir}/quay.yaml" | sort --unique
|
||||
)
|
||||
if [[ "${#quay_images[@]}" != 1 ]]; then
|
||||
echo "Both Quay Deployments must use the same single image reference." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${quay_images[0]}" == *:latest ]]; then
|
||||
echo "The mutable latest tag is not allowed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
validate_file() {
|
||||
local manifest=$1
|
||||
shift
|
||||
|
||||
if command -v kubeconform >/dev/null 2>&1; then
|
||||
kubeconform -strict -summary "$@" "${manifest}"
|
||||
elif command -v docker >/dev/null 2>&1; then
|
||||
docker run --rm --interactive \
|
||||
ghcr.io/yannh/kubeconform:v0.7.0 \
|
||||
-strict -summary "$@" < "${manifest}"
|
||||
else
|
||||
echo "Required command not found: install kubeconform or Docker" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
validate_file "${project_dir}/quay.yaml"
|
||||
validate_file "${project_dir}/ceph-object-user.yaml" -ignore-missing-schemas
|
||||
validate_file "${project_dir}/ci-deployer.yaml"
|
||||
|
||||
echo "Quay scripts and Kubernetes resources are valid for ${quay_images[0]}."
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
sh -n "$script_dir/install.sh"
|
||||
bash "$script_dir/chart/test.sh"
|
||||
Reference in new issue
Block a user