Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a5d23b560 | ||
|
|
fb46fb48b5 | ||
|
|
4c7c53e0f2 | ||
|
|
ba934265ac | ||
|
|
c7155808d9 | ||
|
|
fc4d64bdb2 | ||
|
|
a6f7fc6030 | ||
|
|
11de1d1468 | ||
|
|
64962d1a63 | ||
|
|
b08a0a927d | ||
|
|
8203ba1b0b | ||
|
|
48033b5495 | ||
|
|
73d2af73e5 | ||
|
|
64253e005e | ||
|
|
69accd1752 | ||
|
|
0cf4b08a95 | ||
|
|
86df5d9048 | ||
|
|
d7441bbbc2 | ||
|
|
2be089e048 | ||
|
|
83b2e68371 | ||
|
|
597f64cbb0 |
No files matched your search
+38
-30
@@ -1,42 +1,50 @@
|
||||
# EDU ownership handoff
|
||||
|
||||
## Current status
|
||||
## Status
|
||||
|
||||
EDU PR #1 merged at 2026-10-07 08:04:30 UTC. Main release `5094952464ce315130839303985fd04d721bc1f2` passed CI run 1585 and deploy run 1586. The workstation checkout `/srv/edu-master` is at that SHA. The release changed the application image digests:
|
||||
The EDU ownership handoff is complete. The homelab repository no longer owns
|
||||
EDU workloads, images, routes, alerts, or deployment selection. The EDU
|
||||
repository is the only deployment owner: [forust/edu-master](https://git.forust.xyz/forust/edu-master).
|
||||
|
||||
- Session keeper: `sha256:1e59473bd40fe4c22622017d808a8927a68788275fe073dc23d718c44b2fd5dd`
|
||||
- Webinar checker: `sha256:987d9bf0770272766523ea5b94c7f3f849175d737551d46591ae55e058cf9f12`
|
||||
Homelab PRs #99 and #105 are merged. PR #105 removed the EDU subtree and its
|
||||
build, deploy, rollback, verification, route-probe, and registry references.
|
||||
It also added the serial image build matrix for the homelab services. This
|
||||
handoff record is the only remaining EDU-specific file in homelab Git.
|
||||
|
||||
The live workloads remain healthy in context `Default`, namespace `edu-master`. Both health and live probes return 200. Redis AUTH passes, session TTL is 1178 seconds, the delivery backlog is zero, all nine EDU alert rules are healthy, and the scrape target is UP. The unauthorized-pod Redis check passed. The Redis PVC UID and Secret UID and values, including the Fernet key, match their pre-release state.
|
||||
The dedicated workstation checkout is `/srv/edu-master`, at release
|
||||
`4f2b2a0e37dc11ac2c75441a15076c178e219d37`. It contains `k8s/active`; root
|
||||
`active` is absent. The old untracked `/srv/homelab/edu_master` checkout was
|
||||
moved outside the homelab repository to
|
||||
`/srv/edu-master-legacy-archive-20261007/edu_master`. Its private files remain
|
||||
mode `0600` inside an archive directory with mode `0700`. The homelab deploy
|
||||
checkout has no EDU marker or tracked EDU application/deployment files.
|
||||
`AUTODEPLOY=false` remains in place for homelab deployment.
|
||||
|
||||
The homelab EDU active marker was present after the EDU deployment. It was moved to the private snapshot as `homelab-k8s-active.marker` while holding `/tmp/homelab-apply.lock`. The homelab checkout at `/srv/homelab` is at `5f9354b` and has the tracked marker deletion. Its deploy preflight blocks a dirty checkout until this removal is reconciled. Preserve private ignored configuration when syncing that checkout.
|
||||
## Release evidence
|
||||
|
||||
The remaining homelab change is PR #105, branch `feat/edu-handoff-matrix`, based on `codex/ci-visible-checks`. Its eight protected checks passed. Renovate runs 1587 and 1588 passed. Image publishing was skipped for the PR. The EDU runtime changes are in PR #3 from `fix/handoff-runtime` to `main`; CI run 1589 is in progress. Those runtime changes have not been released.
|
||||
EDU PR #4 merged after its review and CI checks. Main-push CI run 1652 passed
|
||||
all validation and both image builds. Deploy run 1653 passed for the exact main
|
||||
SHA above.
|
||||
|
||||
## Approval gate and next steps
|
||||
The workstation rollout completed for both Deployments. The deployment
|
||||
verified `/health` and `/live` with HTTP 200, Redis AUTH, session TTL of 1058
|
||||
seconds, a delivery backlog of zero, and all nine EDU vmalert rules with
|
||||
matching expressions and healthy evaluation.
|
||||
|
||||
PR #99 must merge before PR #105 can target `main`. A merge attempt for PR #99 returned HTTP 405 because it needs one approval; the protected branch has `required_approvals=1` and whitelist approval is enabled. This approval gate prevents the remaining transfer steps.
|
||||
The images now run by digest:
|
||||
|
||||
After the required approval:
|
||||
- Session keeper: `sha256:998dea51aa3015fd9cabefb0f53b030157a650c3bef72e02fe84f17d5762613d`
|
||||
- Webinar checker: `sha256:92f3c1fa2bb7f9b4680a9fc76a5b33dfbea8ef3dd9c6490ebc45876fd4c54461`
|
||||
|
||||
1. Merge PR #99.
|
||||
2. Retarget PR #105 to `main`. Complete CI and review, then approve and merge it.
|
||||
3. Under the homelab apply lock, sync `/srv/homelab` to the merged removal. Preserve private ignored configuration and keep the active marker removed. Confirm the deploy preflight is clean.
|
||||
4. Merge the EDU runtime PR after its CI and review pass. The main-push CI run must complete successfully before its exact SHA can deploy.
|
||||
5. Verify the new release SHA, image digests, workload health, Redis AUTH and TTL, backlog, PVC and Secret identity, and monitoring. Record the results in the EDU PR.
|
||||
Redis StatefulSet was unchanged. PVC `redis-data-pvc` remains bound to PV
|
||||
`pvc-a4f2a79a-363a-4c12-ae91-92cdfc2a0d2e` with capacity 1 GiB. The existing
|
||||
runtime Secret and Fernet key were preserved during the handoff. Notification
|
||||
delivery was verified before closeout, as confirmed by the operator. The
|
||||
deployment did not record downtime.
|
||||
|
||||
`AUTODEPLOY=false` is explicitly configured. The EDU repository path and port secrets are confirmed, and `EDU_KUBE_CONTEXT=Default` is configured as a repository variable. Keep deployment and registry credentials outside Git. Never run both homelab and EDU deployment paths at the same time.
|
||||
|
||||
## Change summary
|
||||
|
||||
The homelab PR removes the EDU subtree, deployment and image selection, rollback and verification cases, route probes, Renovate references, and external-image exceptions. It adds a serial dynamic matrix for the three homelab images. Each job builds an image or reuses a matching immutable digest. The final job checks all image results and publishes full-SHA tags and the existing release artifact only after they pass. PRs do not publish images. The protected check names from PR #99 are preserved. PR #100's service-metrics work is independent of this handoff.
|
||||
|
||||
The EDU runtime PR adds the Playwright service manifest, reconciles Redis storage and Secret reload annotations, and adds pre-apply Redis backup and identity checks. It verifies application endpoints, Redis AUTH, session TTL, metrics, and all nine vmalert rules. Rollback checks workload and application health and reports when manual recovery is needed. Its deployment guard rejects an unexpected or dirty checkout and refuses deployment while either legacy homelab EDU marker exists.
|
||||
|
||||
## Rollback and limits
|
||||
|
||||
The private snapshot is `/home/forust/.local/state/edu-master-deploy/handoff-20261007T080838Z` on the workstation. It contains the pre-handoff Redis RDB and recovery data. RDB checksum verification confirmed twelve keys. Keep the snapshot outside Git. For an EDU release failure, restore the saved Kubernetes resources and inspect application health. The rollback does not automatically restore the Redis RDB; restore old Redis data only when recovery requires it.
|
||||
|
||||
For an ownership rollback, stop EDU deployment triggers first, restore the reviewed homelab source and marker, then reapply recorded immutable image digests. Verify both workload and application health. Never delete or recreate the Redis PVC.
|
||||
|
||||
The initial EDU release and the homelab marker move are complete. PR #99 approval and merge, PR #105 retarget and merge, homelab checkout reconciliation, EDU runtime PR merge, and release of those runtime changes remain pending. Synthetic Telegram delivery and Alertmanager-to-Telegram notification were not tested.
|
||||
The release rollback snapshot is
|
||||
`/home/forust/.local/state/edu-master-deploy/20261007T180541Z-4f2b2a0e37dc11ac2c75441a15076c178e219d37`.
|
||||
The handoff data snapshot remains at
|
||||
`/home/forust/.local/state/edu-master-deploy/handoff-20261007T080838Z`.
|
||||
Both snapshots are outside Git. Do not restore old Redis data unless recovery
|
||||
requires it. Never delete or recreate the Redis PVC.
|
||||
@@ -7,4 +7,5 @@ self-hosted-runner:
|
||||
labels:
|
||||
- arch
|
||||
- homelab
|
||||
- homelab-pr
|
||||
- prod
|
||||
+35
-6
@@ -1,17 +1,20 @@
|
||||
# Homelab CI/CD
|
||||
|
||||
The native Gitea runner runs on **vps**; production runs on **workstation**.
|
||||
Compose, workflow, shell, Python, formatting, YAML, Dockerfile and Kubernetes
|
||||
checks appear as separate jobs. Jobs run on `homelab:host`, one at a time; the
|
||||
build waits for every check to pass. CI and deploy runs also show a summary with
|
||||
The native Gitea runners run on **vps**; production runs on **workstation**.
|
||||
Main-branch checks and image builds use `homelab:host`. Pull request and
|
||||
non-main checks use `homelab-pr:host` under a separate account without Docker
|
||||
access. The `homelab-pr` runner is registered at User scope for `forust`, so
|
||||
any repository under that account can schedule jobs that request this label.
|
||||
Each runner accepts one job at a time; the build waits for every check to pass.
|
||||
CI and deploy runs also show a summary with
|
||||
the release SHA, image build or reuse results, deploy mode, selected services,
|
||||
and image digests. Failed runs keep a summary of completed image builds, stage
|
||||
results, apply results, and recorded Kubernetes recovery. The final deploy
|
||||
summary is in the smoke job; earlier jobs show the state observed at that time.
|
||||
Apply success is separate from health and recovery. Update the installed
|
||||
workstation controller with `setup-workstation.sh` when no deploy is running.
|
||||
No job images or Kubernetes credentials
|
||||
are needed on the VPS. Builds use one pinned BuildKit helper container. CI and deploy are separate workflows.
|
||||
No job images or Kubernetes credentials are needed on the VPS. Builds use one
|
||||
pinned BuildKit helper container. CI and deploy are separate workflows.
|
||||
|
||||
## Runner installation
|
||||
|
||||
@@ -37,6 +40,32 @@ pushes directly to the registry, and caps retained local cache at 1 GiB with a
|
||||
2 GiB free-space target. This is not a hard limit on peak build disk usage.
|
||||
Nothing runs `docker system prune`, removes unrelated images, or deletes volumes.
|
||||
|
||||
### Pull request runner
|
||||
|
||||
Install the unprivileged host runner on the VPS:
|
||||
|
||||
```sh
|
||||
sudo bash .gitea/runner/setup-pr-runner.sh
|
||||
```
|
||||
|
||||
Get a registration token from the user Actions runner settings. Run the
|
||||
installer in a terminal. It asks for the token without echoing it, registers the
|
||||
runner as `homelab-pr` with label `homelab-pr:host`, then enables the service.
|
||||
The work directory is `/var/lib/gitea-pr-runner`. Confirm that Gitea lists the
|
||||
runner as User scope before merging the workflow change. An unmatched label can
|
||||
fall back to the default job image.
|
||||
|
||||
Renovate PR validation uses `pull_request_target`, which reads the workflow from
|
||||
the base branch. It checks out the PR head only after runner selection and runs
|
||||
that code on `homelab-pr`. Keep this workflow read-only and do not add secrets.
|
||||
|
||||
The PR runner has a separate home and tool cache. Do not add it to the `docker`
|
||||
group or give it access to `/var/run/docker.sock`. It runs repository code from
|
||||
pull requests, so keep its registration and permissions separate from the
|
||||
trusted `homelab` runner. This separates users and host permissions, but both
|
||||
runners still share the VPS kernel and network. Use a disposable VM if PRs from
|
||||
untrusted external authors must be fully isolated.
|
||||
|
||||
## Workstation setup
|
||||
|
||||
As the existing SSH deploy user on workstation:
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
runner:
|
||||
file: /var/lib/gitea-pr-runner/.runner
|
||||
capacity: 1
|
||||
timeout: 5h
|
||||
labels:
|
||||
- homelab-pr:host
|
||||
cache:
|
||||
enabled: false
|
||||
@@ -0,0 +1,27 @@
|
||||
[Unit]
|
||||
Description=Gitea Actions untrusted pull request runner
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
User=gitea-pr-runner
|
||||
Group=gitea-pr-runner
|
||||
WorkingDirectory=/var/lib/gitea-pr-runner
|
||||
Environment=HOME=/var/lib/gitea-pr-runner
|
||||
Environment=PATH=/var/lib/gitea-pr-runner/.cache/homelab-ci/bin:/usr/local/bin:/usr/bin:/bin
|
||||
ExecStart=/usr/local/bin/gitea-runner daemon --config /etc/gitea-pr-runner/config.yaml
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
ProtectSystem=full
|
||||
ProtectHome=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectControlGroups=yes
|
||||
RestrictSUIDSGID=yes
|
||||
LockPersonality=yes
|
||||
UMask=0077
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install a native runner for untrusted PR jobs without Docker access.
|
||||
set -euo pipefail
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
[ "$(id -u)" -eq 0 ] || { echo 'Run with sudo on the runner host' >&2; exit 1; }
|
||||
for tool in cp cut date getent id install runuser systemctl useradd; do
|
||||
command -v "$tool" >/dev/null || { echo "Install missing prerequisite: $tool" >&2; exit 1; }
|
||||
done
|
||||
command -v /usr/local/bin/gitea-runner >/dev/null || {
|
||||
echo 'Install gitea-runner 3.0.2 at /usr/local/bin/gitea-runner first' >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
id gitea-pr-runner >/dev/null 2>&1 || \
|
||||
useradd --system --create-home --home-dir /var/lib/gitea-pr-runner --shell /usr/bin/bash gitea-pr-runner
|
||||
runner_home="$(getent passwd gitea-pr-runner | cut -d: -f6)"
|
||||
[ "$runner_home" = /var/lib/gitea-pr-runner ] || {
|
||||
echo 'Unexpected PR runner home; inspect the existing service first' >&2
|
||||
exit 1
|
||||
}
|
||||
case " $(id -nG gitea-pr-runner) " in
|
||||
*' docker '*)
|
||||
echo 'The PR runner account must not belong to the docker group' >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
install -d -m 0755 /etc/gitea-pr-runner
|
||||
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
for existing in /etc/gitea-pr-runner/config.yaml /etc/systemd/system/gitea-pr-runner.service; do
|
||||
[ ! -f "$existing" ] || cp -p "$existing" "$existing.before-$stamp"
|
||||
done
|
||||
install -m 0644 "$here/pr-config.yaml" /etc/gitea-pr-runner/config.yaml
|
||||
install -m 0644 "$here/pr-runner.service" /etc/systemd/system/gitea-pr-runner.service
|
||||
|
||||
if [ ! -f /var/lib/gitea-pr-runner/.runner ]; then
|
||||
read -r -s -p 'Enter the Gitea repository runner registration token: ' runner_token
|
||||
printf '\n'
|
||||
[ -n "$runner_token" ] || { echo 'Runner token is required' >&2; exit 1; }
|
||||
export GITEA_RUNNER_REGISTRATION_TOKEN="$runner_token"
|
||||
unset runner_token
|
||||
runuser --preserve-environment -u gitea-pr-runner -- \
|
||||
/usr/local/bin/gitea-runner register \
|
||||
--config /etc/gitea-pr-runner/config.yaml \
|
||||
--instance https://gitea.forust.xyz \
|
||||
--name homelab-pr \
|
||||
--labels homelab-pr:host \
|
||||
--no-interactive
|
||||
unset GITEA_RUNNER_REGISTRATION_TOKEN
|
||||
fi
|
||||
chmod 0600 /var/lib/gitea-pr-runner/.runner
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now gitea-pr-runner.service
|
||||
systemctl restart gitea-pr-runner.service
|
||||
echo "PR runner ready. Configuration backups: *.before-$stamp"
|
||||
+13
-13
@@ -14,7 +14,7 @@ concurrency:
|
||||
jobs:
|
||||
compose:
|
||||
name: Compose
|
||||
runs-on: homelab
|
||||
runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
fi
|
||||
workflows:
|
||||
name: Workflows
|
||||
runs-on: homelab
|
||||
runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
fi
|
||||
shell:
|
||||
name: Shell
|
||||
runs-on: homelab
|
||||
runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
fi
|
||||
formatting:
|
||||
name: Formatting
|
||||
runs-on: homelab
|
||||
runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -194,7 +194,7 @@ jobs:
|
||||
fi
|
||||
python:
|
||||
name: Python and tests
|
||||
runs-on: homelab
|
||||
runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -232,7 +232,7 @@ jobs:
|
||||
fi
|
||||
yaml:
|
||||
name: YAML
|
||||
runs-on: homelab
|
||||
runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -280,7 +280,7 @@ jobs:
|
||||
fi
|
||||
dockerfiles:
|
||||
name: Dockerfiles
|
||||
runs-on: homelab
|
||||
runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -326,7 +326,7 @@ jobs:
|
||||
fi
|
||||
kubernetes:
|
||||
name: Kubernetes
|
||||
runs-on: homelab
|
||||
runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -395,7 +395,7 @@ jobs:
|
||||
run: python3 .gitea/workflows/release.py prepare --output build-plan.json
|
||||
- name: Store the image plan
|
||||
id: artifact
|
||||
uses: actions/upload-artifact@c6a366c94c3e0affe28c06c8df20a878f24da3cf
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: build-plan
|
||||
path: build-plan.json
|
||||
@@ -435,7 +435,7 @@ jobs:
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
||||
- name: Download the checked image plan
|
||||
id: inputs
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: build-plan
|
||||
- name: Build or reuse this image
|
||||
@@ -447,7 +447,7 @@ jobs:
|
||||
run: python3 .gitea/workflows/release.py image --image "$IMAGE_NAME" --output image.json
|
||||
- name: Store the image result
|
||||
id: artifact
|
||||
uses: actions/upload-artifact@c6a366c94c3e0affe28c06c8df20a878f24da3cf
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: image-${{ matrix.name }}
|
||||
path: image.json
|
||||
@@ -482,7 +482,7 @@ jobs:
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
||||
- name: Download all image results
|
||||
id: inputs
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
path: artifacts
|
||||
- name: Pin SHA tags and write the complete release
|
||||
@@ -495,7 +495,7 @@ jobs:
|
||||
--plan artifacts/build-plan/build-plan.json
|
||||
- name: Store commit release
|
||||
id: artifact
|
||||
uses: actions/upload-artifact@c6a366c94c3e0affe28c06c8df20a878f24da3cf
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: release-${{ github.sha }}
|
||||
path: release.json
|
||||
|
||||
@@ -45,12 +45,17 @@ def prepare(source_file):
|
||||
before = json.loads(json.dumps(config))
|
||||
for service, settings in config['services'].items():
|
||||
reference = settings.get('image')
|
||||
nextcloud_aio_master = project == 'nextcloud' and service == 'nextcloud-aio-mastercontainer'
|
||||
if not reference or settings.get('build'):
|
||||
raise ValueError(f'{project}/{service}: Compose deploy requires a published image')
|
||||
image_repo = reference.split('@')[0].rsplit('/', 1)
|
||||
image_repo[-1] = image_repo[-1].split(':')[0]
|
||||
image_repo = '/'.join(image_repo)
|
||||
if image_repo in release['images']:
|
||||
# Nextcloud AIO validates the mastercontainer image reference and rejects
|
||||
# a digest. Keep its configured tag so AIO can start and manage its stack.
|
||||
if nextcloud_aio_master:
|
||||
pinned = reference
|
||||
elif image_repo in release['images']:
|
||||
pinned = image_repo + '@' + release['images'][image_repo]
|
||||
elif os.environ.get('REFRESH_IMAGES') != 'true' and reference in locks:
|
||||
pinned = locks[reference]
|
||||
@@ -75,7 +80,12 @@ def prepare(source_file):
|
||||
actual.add(next((d for d in digests or [] if d.split('@')[0] == image_repo), image_id))
|
||||
if len(actual) > 1:
|
||||
raise ValueError(f'{project}/{service}: mixed running images, cannot capture one recovery config')
|
||||
before['services'][service]['image'] = next(iter(actual)) if actual else reference
|
||||
# AIO also rejects a digest in its recovery config. Preserve its tag in
|
||||
# both deploy and recovery files.
|
||||
if nextcloud_aio_master:
|
||||
before['services'][service]['image'] = reference
|
||||
else:
|
||||
before['services'][service]['image'] = next(iter(actual)) if actual else reference
|
||||
for name, data in (('compose', config), ('compose-before', before)):
|
||||
folder = directory / name
|
||||
folder.mkdir(mode=0o700, exist_ok=True)
|
||||
|
||||
@@ -141,7 +141,8 @@ def make_plan(directory):
|
||||
planner = load_module('deploy_plan', source / '.gitea/workflows/deploy-plan.py')
|
||||
request = json.loads((directory / 'request.json').read_text())
|
||||
previous = json.loads((STATE / 'last-success.json').read_text()) if (STATE / 'last-success.json').exists() else None
|
||||
helm = json.loads(command('helm', 'list', '--all', '-A', '-o', 'json'))
|
||||
# Helm 4 lists every release status by default and removed the --all flag.
|
||||
helm = json.loads(command('helm', 'list', '-A', '-o', 'json'))
|
||||
plan = planner.make_plan(source, CONFIG_REPO, request['release'], previous, request['mode'], helm)
|
||||
if request['refresh_images']:
|
||||
plan['selected']['compose'] = plan['active']['compose']
|
||||
|
||||
@@ -180,7 +180,8 @@ save_snapshot() {
|
||||
| select(any(.metadata.ownerReferences[]?; .uid == $w.metadata.uid))
|
||||
| select($w.kind != "StatefulSet" or .metadata.name == $w.status.currentRevision) | .revision] | max // 0) end)
|
||||
}]' "$dir/workloads.json" >"$dir/revisions.json" || return 1
|
||||
releases="$(helm list --all -A -o json)" || return 1
|
||||
# Helm 4 lists every release status by default and removed the --all flag.
|
||||
releases="$(helm list -A -o json)" || return 1
|
||||
for entry in "${HELM_RELEASES[@]}"; do
|
||||
IFS='|' read -r release _ namespace _ _ _ <<<"$entry"
|
||||
if ! jq -e --arg r "$release" --arg n "$namespace" \
|
||||
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
apply:
|
||||
needs: [gate]
|
||||
runs-on: homelab
|
||||
timeout-minutes: 100
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- name: Checkout checked commit
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
name: renovate-ci
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
# Read the workflow from the trusted base branch. PR code runs only on the
|
||||
# unprivileged runner selected below.
|
||||
pull_request_target:
|
||||
paths:
|
||||
- "renovate/**"
|
||||
- ".gitea/workflows/renovate-ci.yaml"
|
||||
@@ -26,37 +28,47 @@ permissions:
|
||||
|
||||
jobs:
|
||||
validate-renovate:
|
||||
runs-on: homelab
|
||||
runs-on: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
# renovate/k8s/cronjob.yaml is the single source of truth for the image tag,
|
||||
# so the same version that runs in the cluster is the one validated here.
|
||||
- name: Resolve the deployed Renovate image
|
||||
# renovate/k8s/cronjob.yaml is the single source of truth for the version.
|
||||
- name: Resolve the deployed Renovate version
|
||||
id: image
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
image="$(sed -n 's|.*image:[[:space:]]*\(renovate/renovate:[^[:space:]]*\).*|\1|p' \
|
||||
renovate/k8s/cronjob.yaml | head -1)"
|
||||
if [ -z "$image" ]; then
|
||||
echo "::error::no renovate/renovate image found in renovate/k8s/cronjob.yaml"
|
||||
if [[ ! "$image" =~ ^renovate/renovate:([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
||||
echo "::error::expected a pinned renovate/renovate semantic version in renovate/k8s/cronjob.yaml"
|
||||
exit 1
|
||||
fi
|
||||
echo "using $image"
|
||||
echo "image=$image" >> "$GITHUB_OUTPUT"
|
||||
version="${BASH_REMATCH[1]}"
|
||||
echo "using Renovate $version"
|
||||
printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate Renovate repository config
|
||||
- name: Prepare pinned validation tools
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker run --rm \
|
||||
-v "$PWD/renovate:/opt/renovate:ro" \
|
||||
-e RENOVATE_CONFIG_FILE=/opt/renovate/renovate.json \
|
||||
"${{ steps.image.outputs.image }}" \
|
||||
renovate-config-validator /opt/renovate/renovate.json
|
||||
tools_dir="$(bash .gitea/workflows/install-ci-tools.sh kubeconform node)"
|
||||
echo "$tools_dir" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Validate Renovate repository config
|
||||
shell: bash
|
||||
env:
|
||||
RENOVATE_VERSION: ${{ steps.image.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm_cache="$(mktemp -d "${RUNNER_TEMP:-/tmp}/renovate-npm-cache.XXXXXXXX")"
|
||||
trap 'rm -rf "$npm_cache"' EXIT
|
||||
NPM_CONFIG_CACHE="$npm_cache" RENOVATE_CONFIG_FILE="$PWD/renovate/renovate.json" \
|
||||
npm exec --yes --package="renovate@${RENOVATE_VERSION}" -- renovate-config-validator
|
||||
|
||||
# The CronJob cannot read the repository, so renovate/k8s/configmap.yaml
|
||||
# carries an inlined copy of the config. Fail if it no longer matches.
|
||||
@@ -70,8 +82,6 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tools_dir="$(bash .gitea/workflows/install-ci-tools.sh kubeconform)"
|
||||
export PATH="$tools_dir:$PATH"
|
||||
kubeconform \
|
||||
-strict \
|
||||
-ignore-missing-schemas \
|
||||
|
||||
@@ -32,11 +32,14 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
run-renovate:
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: homelab
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
with:
|
||||
ref: refs/heads/main
|
||||
|
||||
# renovate/k8s/cronjob.yaml is the single source of truth for the image tag.
|
||||
# Reading it here means this workflow validates and runs the exact version
|
||||
@@ -48,21 +51,23 @@ jobs:
|
||||
set -euo pipefail
|
||||
image="$(sed -n 's|.*image:[[:space:]]*\(renovate/renovate:[^[:space:]]*\).*|\1|p' \
|
||||
renovate/k8s/cronjob.yaml | head -1)"
|
||||
if [ -z "$image" ]; then
|
||||
echo "::error::no renovate/renovate image found in renovate/k8s/cronjob.yaml"
|
||||
if [[ ! "$image" =~ ^renovate/renovate:[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "::error::expected a pinned renovate/renovate semantic version in renovate/k8s/cronjob.yaml"
|
||||
exit 1
|
||||
fi
|
||||
echo "using $image"
|
||||
echo "image=$image" >> "$GITHUB_OUTPUT"
|
||||
printf 'image=%s\n' "$image" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate Renovate config
|
||||
shell: bash
|
||||
env:
|
||||
RENOVATE_IMAGE: ${{ steps.image.outputs.image }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker run --rm \
|
||||
-v "$PWD/renovate/renovate.json:/opt/renovate/renovate.json:ro" \
|
||||
-e RENOVATE_CONFIG_FILE=/opt/renovate/renovate.json \
|
||||
"${{ steps.image.outputs.image }}" \
|
||||
"$RENOVATE_IMAGE" \
|
||||
renovate-config-validator
|
||||
|
||||
- name: Run Renovate
|
||||
@@ -73,6 +78,7 @@ jobs:
|
||||
RENOVATE_REPOSITORIES: ${{ inputs.repositories }}
|
||||
RENOVATE_DRY_RUN: ${{ inputs.dry_run && 'full' || '' }}
|
||||
LOG_LEVEL: ${{ inputs.log_level }}
|
||||
RENOVATE_IMAGE: ${{ steps.image.outputs.image }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -89,4 +95,4 @@ jobs:
|
||||
-e RENOVATE_CONFIG_FILE=/opt/renovate/renovate.json \
|
||||
-e RENOVATE_BASE_DIR=/tmp/renovate \
|
||||
-e LOG_LEVEL="${LOG_LEVEL:-info}" \
|
||||
"${{ steps.image.outputs.image }}"
|
||||
"$RENOVATE_IMAGE"
|
||||
@@ -20,6 +20,8 @@ data:
|
||||
|
||||
GITEA__mailer__ENABLED: "false"
|
||||
|
||||
GITEA__metrics__ENABLED: "true"
|
||||
|
||||
# No code/issue search needed: bleve reindexes the whole issue index on
|
||||
# every pod restart (cron.rebuild_issue_indexer RUN_AT_START) and hammers
|
||||
# the rotational disk for an hour. "db" serves issue search from postgres.
|
||||
|
||||
@@ -3,6 +3,8 @@ kind: Service
|
||||
metadata:
|
||||
name: gitea-service
|
||||
namespace: gitea
|
||||
labels:
|
||||
app: gitea
|
||||
spec:
|
||||
selector:
|
||||
app: gitea
|
||||
|
||||
@@ -7,7 +7,8 @@ spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`gitea.forust.xyz`) || Host(`git.forust.xyz`)
|
||||
# Metrics are scraped directly through the cluster Service.
|
||||
- match: (Host(`gitea.forust.xyz`) || Host(`git.forust.xyz`)) && !PathPrefix(`/metrics`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: gitea-service
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: gitea
|
||||
namespace: gitea
|
||||
labels:
|
||||
release: prometheus-stack
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: gitea
|
||||
endpoints:
|
||||
- port: http
|
||||
path: /metrics
|
||||
interval: 30s
|
||||
scrapeTimeout: 10s
|
||||
@@ -7,7 +7,7 @@
|
||||
# # Dev server_url
|
||||
# server_url: https://hs.dev_internal_domain.internal
|
||||
listen_addr: 0.0.0.0:8080
|
||||
metrics_listen_addr: 127.0.0.1:9090
|
||||
metrics_listen_addr: 0.0.0.0:9090
|
||||
grpc_listen_addr: 127.0.0.1:50443
|
||||
grpc_allow_insecure: false
|
||||
noise:
|
||||
|
||||
@@ -3,6 +3,8 @@ kind: Service
|
||||
metadata:
|
||||
name: headscale-server-external
|
||||
namespace: headscale
|
||||
labels:
|
||||
app: headscale
|
||||
spec:
|
||||
ports:
|
||||
- port: 8080
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: operator.victoriametrics.com/v1beta1
|
||||
kind: VMServiceScrape
|
||||
metadata:
|
||||
name: headscale
|
||||
namespace: headscale
|
||||
labels:
|
||||
release: prometheus-stack
|
||||
spec:
|
||||
# The external Service has a manually managed EndpointSlice, not Endpoints.
|
||||
discoveryRole: endpointslice
|
||||
selector:
|
||||
matchLabels:
|
||||
app: headscale
|
||||
endpoints:
|
||||
- port: metrics
|
||||
path: /metrics
|
||||
interval: 30s
|
||||
scrapeTimeout: 10s
|
||||
@@ -6,6 +6,10 @@ metadata:
|
||||
data:
|
||||
TZ: "Europe/Bratislava"
|
||||
|
||||
IMMICH_TELEMETRY_INCLUDE: "all"
|
||||
IMMICH_API_METRICS_PORT: "8081"
|
||||
IMMICH_MICROSERVICES_METRICS_PORT: "8082"
|
||||
|
||||
# The database in this namespace, not the shared one in the database
|
||||
# namespace: v3 needs VectorChord, and only the dedicated image carries it.
|
||||
DB_HOSTNAME: "immich-postgres"
|
||||
|
||||
@@ -3,6 +3,8 @@ kind: Service
|
||||
metadata:
|
||||
name: immich-service
|
||||
namespace: immich
|
||||
labels:
|
||||
app: immich
|
||||
spec:
|
||||
selector:
|
||||
app: immich
|
||||
@@ -10,6 +12,12 @@ spec:
|
||||
- name: http
|
||||
port: 2283
|
||||
targetPort: 2283
|
||||
- name: api-metrics
|
||||
port: 8081
|
||||
targetPort: api-metrics
|
||||
- name: worker-metrics
|
||||
port: 8082
|
||||
targetPort: worker-metrics
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
@@ -41,6 +49,10 @@ spec:
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 2283
|
||||
- name: api-metrics
|
||||
containerPort: 8081
|
||||
- name: worker-metrics
|
||||
containerPort: 8082
|
||||
volumeMounts:
|
||||
- name: immich-data
|
||||
mountPath: /data
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: immich
|
||||
namespace: immich
|
||||
labels:
|
||||
release: prometheus-stack
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: immich
|
||||
endpoints:
|
||||
- port: api-metrics
|
||||
path: /metrics
|
||||
interval: 30s
|
||||
scrapeTimeout: 10s
|
||||
- port: worker-metrics
|
||||
path: /metrics
|
||||
interval: 30s
|
||||
scrapeTimeout: 10s
|
||||
@@ -3,6 +3,8 @@ kind: Service
|
||||
metadata:
|
||||
name: netbird-server-service
|
||||
namespace: netbird
|
||||
labels:
|
||||
app: netbird-server
|
||||
spec:
|
||||
selector:
|
||||
app: netbird-server
|
||||
@@ -11,6 +13,10 @@ spec:
|
||||
name: http
|
||||
targetPort: 80
|
||||
protocol: TCP
|
||||
- port: 9090
|
||||
name: metrics
|
||||
targetPort: metrics
|
||||
protocol: TCP
|
||||
- port: 3478
|
||||
name: stun
|
||||
targetPort: 3478
|
||||
@@ -59,6 +65,9 @@ spec:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
protocol: TCP
|
||||
- containerPort: 9090
|
||||
name: metrics
|
||||
protocol: TCP
|
||||
- containerPort: 3478
|
||||
name: stun
|
||||
protocol: UDP
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: netbird-server
|
||||
namespace: netbird
|
||||
labels:
|
||||
release: prometheus-stack
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: netbird-server
|
||||
endpoints:
|
||||
- port: metrics
|
||||
path: /metrics
|
||||
interval: 30s
|
||||
scrapeTimeout: 10s
|
||||
@@ -0,0 +1,51 @@
|
||||
# Paperless-ngx
|
||||
|
||||
Paperless-ngx runs in the `paperless` namespace. It uses the shared PostgreSQL
|
||||
service in the `database` namespace and Valkey for its task queue. The document
|
||||
library, exports, and consume folder are stored on the `local-path-retain`
|
||||
volume. The PVC size is fixed at 50 GiB because this storage class does not
|
||||
support volume expansion.
|
||||
|
||||
The local route is `https://papers.workstation.internal`; the public route is
|
||||
`https://papers.forust.xyz`. Both use TLS. Paperless keeps its own login and
|
||||
password authentication. OCR is configured for Russian and English documents.
|
||||
Keep `papers.forust.xyz` in the existing Cloudflare DDNS `DOMAINS` setting so
|
||||
the public record follows the workstation address.
|
||||
|
||||
## Prepare the secret
|
||||
|
||||
Create `k8s/secrets.yaml` on the workstation from
|
||||
`k8s/secrets.yaml.example`. Set a unique random `PAPERLESS_SECRET_KEY`, a long
|
||||
`PAPERLESS_ADMIN_PASSWORD`, and `PAPERLESS_DB_PASSWORD`.
|
||||
|
||||
Add the same `PAPERLESS_DB_PASSWORD` value to the local
|
||||
`postgres/k8s/secrets.yaml` file. Keep both secret files out of Git. The
|
||||
database bootstrap Job creates the `paperless` role and database from the
|
||||
shared PostgreSQL secret. The job runs in the `database` namespace and needs
|
||||
that namespace's existing `postgres-shared-secrets` Secret.
|
||||
|
||||
For example, generate a key with:
|
||||
|
||||
```sh
|
||||
python3 -c 'import secrets; print(secrets.token_urlsafe(64))'
|
||||
```
|
||||
|
||||
Then apply the secret before enabling the service:
|
||||
|
||||
```sh
|
||||
kubectl apply -f paperless/k8s/namespace.yaml
|
||||
kubectl apply -f postgres/k8s/secrets.yaml
|
||||
kubectl apply -f paperless/k8s/secrets.yaml
|
||||
```
|
||||
|
||||
The normal deploy workflow applies the remaining manifests when
|
||||
`paperless/k8s/active` is present. Verify the rollout and ingress after deploy:
|
||||
|
||||
```sh
|
||||
kubectl -n paperless rollout status deployment/paperless
|
||||
kubectl -n paperless get pods,pvc,services
|
||||
```
|
||||
|
||||
Back up the `paperless-data` PVC and the shared PostgreSQL database. The PVC
|
||||
contains the originals, archived PDFs, and export/consume folders. Valkey has
|
||||
no persistent volume; queued tasks are recreated after a restart.
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: paperless-prod-tls
|
||||
namespace: paperless
|
||||
spec:
|
||||
secretName: paperless-prod-tls
|
||||
dnsNames:
|
||||
- papers.forust.xyz
|
||||
issuerRef:
|
||||
name: letsencrypt-prod
|
||||
kind: ClusterIssuer
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: internal-wildcard-tls
|
||||
namespace: paperless
|
||||
spec:
|
||||
secretName: internal-wildcard-tls
|
||||
dnsNames:
|
||||
- "*.workstation.internal"
|
||||
- "*.gigaforust.internal"
|
||||
- workstation.internal
|
||||
- gigaforust.internal
|
||||
issuerRef:
|
||||
name: internal-ca
|
||||
kind: ClusterIssuer
|
||||
@@ -0,0 +1,58 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: paperless-database-init
|
||||
namespace: database
|
||||
spec:
|
||||
backoffLimit: 5
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: paperless-database-init
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
containers:
|
||||
- name: create-database
|
||||
image: postgres:17.11-alpine
|
||||
command:
|
||||
- /bin/sh
|
||||
- -ec
|
||||
- |
|
||||
PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql \
|
||||
--host postgres \
|
||||
--username postgres \
|
||||
--dbname postgres \
|
||||
--set ON_ERROR_STOP=1 \
|
||||
--set paperless_password="$PAPERLESS_DB_PASSWORD" <<'SQL'
|
||||
SELECT format(
|
||||
'CREATE ROLE paperless LOGIN PASSWORD %L',
|
||||
:'paperless_password'
|
||||
)
|
||||
WHERE NOT EXISTS (
|
||||
SELECT FROM pg_roles WHERE rolname = 'paperless'
|
||||
)
|
||||
\gexec
|
||||
SELECT format('CREATE DATABASE paperless OWNER paperless')
|
||||
WHERE NOT EXISTS (
|
||||
SELECT FROM pg_database WHERE datname = 'paperless'
|
||||
)
|
||||
\gexec
|
||||
SQL
|
||||
env:
|
||||
- name: POSTGRES_ADMIN_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-shared-secrets
|
||||
key: POSTGRES_ADMIN_PASSWORD
|
||||
- name: PAPERLESS_DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-shared-secrets
|
||||
key: PAPERLESS_DB_PASSWORD
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
@@ -0,0 +1,33 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: paperless-prod
|
||||
namespace: paperless
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`papers.forust.xyz`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: paperless
|
||||
port: 8000
|
||||
tls:
|
||||
secretName: paperless-prod-tls
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: paperless-local
|
||||
namespace: paperless
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`papers.workstation.internal`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: paperless
|
||||
port: 8000
|
||||
tls:
|
||||
secretName: internal-wildcard-tls
|
||||
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: paperless
|
||||
@@ -0,0 +1,39 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: paperless-ingress
|
||||
namespace: paperless
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: paperless
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: traefik
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8000
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: paperless-valkey-ingress
|
||||
namespace: paperless
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: paperless-valkey
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: paperless
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 6379
|
||||
@@ -0,0 +1,210 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: paperless
|
||||
namespace: paperless
|
||||
labels:
|
||||
app.kubernetes.io/name: paperless
|
||||
spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: paperless
|
||||
ports:
|
||||
- name: http
|
||||
port: 8000
|
||||
targetPort: http
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: paperless
|
||||
namespace: paperless
|
||||
labels:
|
||||
app.kubernetes.io/name: paperless
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: paperless
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: paperless
|
||||
spec:
|
||||
enableServiceLinks: false
|
||||
containers:
|
||||
- name: paperless
|
||||
image: ghcr.io/paperless-ngx/paperless-ngx:3.2.1
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8000
|
||||
env:
|
||||
- name: PAPERLESS_URL
|
||||
value: https://papers.forust.xyz
|
||||
- name: PAPERLESS_ALLOWED_HOSTS
|
||||
value: papers.forust.xyz,papers.workstation.internal
|
||||
- name: PAPERLESS_CSRF_TRUSTED_ORIGINS
|
||||
value: https://papers.forust.xyz,https://papers.workstation.internal
|
||||
- name: PAPERLESS_TIME_ZONE
|
||||
value: Europe/Bratislava
|
||||
- name: PAPERLESS_REDIS
|
||||
value: redis://paperless-valkey:6379
|
||||
- name: PAPERLESS_DBENGINE
|
||||
value: postgresql
|
||||
- name: PAPERLESS_DBHOST
|
||||
value: postgres.database.svc.cluster.local
|
||||
- name: PAPERLESS_DBNAME
|
||||
value: paperless
|
||||
- name: PAPERLESS_DBUSER
|
||||
value: paperless
|
||||
- name: PAPERLESS_DBPASS
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: paperless-secrets
|
||||
key: PAPERLESS_DB_PASSWORD
|
||||
- name: PAPERLESS_OCR_LANGUAGE
|
||||
value: rus+eng
|
||||
- name: PAPERLESS_OCR_LANGUAGES
|
||||
value: rus
|
||||
- name: PAPERLESS_TASK_WORKERS
|
||||
value: "1"
|
||||
- name: PAPERLESS_ADMIN_USER
|
||||
value: admin
|
||||
- name: PAPERLESS_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: paperless-secrets
|
||||
key: PAPERLESS_SECRET_KEY
|
||||
- name: PAPERLESS_ADMIN_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: paperless-secrets
|
||||
key: PAPERLESS_ADMIN_PASSWORD
|
||||
volumeMounts:
|
||||
- name: documents
|
||||
mountPath: /usr/src/paperless/data
|
||||
subPath: data
|
||||
- name: documents
|
||||
mountPath: /usr/src/paperless/media
|
||||
subPath: media
|
||||
- name: documents
|
||||
mountPath: /usr/src/paperless/export
|
||||
subPath: export
|
||||
- name: documents
|
||||
mountPath: /usr/src/paperless/consume
|
||||
subPath: consume
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
httpHeaders:
|
||||
- name: Host
|
||||
value: papers.workstation.internal
|
||||
failureThreshold: 60
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
httpHeaders:
|
||||
- name: Host
|
||||
value: papers.workstation.internal
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
httpHeaders:
|
||||
- name: Host
|
||||
value: papers.workstation.internal
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: 2Gi
|
||||
volumes:
|
||||
- name: documents
|
||||
persistentVolumeClaim:
|
||||
claimName: paperless-data
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: paperless-data
|
||||
namespace: paperless
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: local-path-retain
|
||||
resources:
|
||||
requests:
|
||||
storage: 50Gi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: paperless-valkey
|
||||
namespace: paperless
|
||||
labels:
|
||||
app.kubernetes.io/name: paperless-valkey
|
||||
spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: paperless-valkey
|
||||
ports:
|
||||
- name: redis
|
||||
port: 6379
|
||||
targetPort: redis
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: paperless-valkey
|
||||
namespace: paperless
|
||||
labels:
|
||||
app.kubernetes.io/name: paperless-valkey
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: paperless-valkey
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: paperless-valkey
|
||||
spec:
|
||||
containers:
|
||||
- name: valkey
|
||||
image: valkey/valkey:9.0.3-alpine
|
||||
args:
|
||||
- valkey-server
|
||||
- --save
|
||||
- ""
|
||||
- --appendonly
|
||||
- "no"
|
||||
ports:
|
||||
- name: redis
|
||||
containerPort: 6379
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["valkey-cli", "ping"]
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["valkey-cli", "ping"]
|
||||
periodSeconds: 30
|
||||
resources:
|
||||
requests:
|
||||
cpu: 25m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: paperless-secrets
|
||||
namespace: paperless
|
||||
type: Opaque
|
||||
stringData:
|
||||
PAPERLESS_SECRET_KEY: "<GENERATE_WITH_python3_-c_import_secrets;_print(secrets.token_urlsafe(64))>"
|
||||
PAPERLESS_ADMIN_PASSWORD: "<SET_A_LONG_UNIQUE_PASSWORD>"
|
||||
PAPERLESS_DB_PASSWORD: "<SET_THE_SAME_VALUE_AS_database_PAPERLESS_DB_PASSWORD>"
|
||||
@@ -4,4 +4,5 @@ GITEA_DB_PASSWORD=
|
||||
NETBOX_DB_PASSWORD=
|
||||
NETRONOME_DB_PASSWORD=
|
||||
PENPOT_DB_PASSWORD=
|
||||
PAPERLESS_DB_PASSWORD=
|
||||
STATUSPAGE_DB_PASSWORD=
|
||||
+12
-11
@@ -1,20 +1,21 @@
|
||||
# Shared PostgreSQL
|
||||
|
||||
This directory contains the shared PostgreSQL 17 deployment for Authentik,
|
||||
Gitea, NetBox, Netronome, and Statuspage. It creates one database and one login role
|
||||
per service. Per-service standalone databases were removed after the
|
||||
migration (Sep 2026); Penpot stays on its own compose PostgreSQL (archived,
|
||||
not part of the shared instance).
|
||||
Gitea, NetBox, Netronome, Paperless-ngx, and Statuspage. It creates one database
|
||||
and one login role per service. Per-service standalone databases were removed
|
||||
after the migration (Sep 2026); Penpot stays on its own compose PostgreSQL
|
||||
(archived, not part of the shared instance).
|
||||
|
||||
## Compatibility baseline
|
||||
|
||||
| Service | Current application | Shared PostgreSQL 17 |
|
||||
| ---------- | ------------------- | -------------------------------------- |
|
||||
| Authentik | 2025.10.x | Supported (Authentik requires 14+) |
|
||||
| Gitea | 1.27.3 | Supported (Gitea requires 12+) |
|
||||
| NetBox | 4.7.x | Supported (NetBox 4.x requires 13+) |
|
||||
| Netronome | 0.14.0 | Supported (upstream's example uses 17) |
|
||||
| Statuspage | custom | Supported |
|
||||
| Service | Current application | Shared PostgreSQL 17 |
|
||||
| ------------- | ------------------- | -------------------------------------- |
|
||||
| Authentik | 2025.10.x | Supported (Authentik requires 14+) |
|
||||
| Gitea | 1.27.3 | Supported (Gitea requires 12+) |
|
||||
| NetBox | 4.7.x | Supported (NetBox 4.x requires 13+) |
|
||||
| Netronome | 0.14.0 | Supported (upstream's example uses 17) |
|
||||
| Paperless-ngx | 3.2.1 | Supported |
|
||||
| Statuspage | custom | Supported |
|
||||
|
||||
A major-version change must use a logical dump/restore; changing only the
|
||||
image tag while keeping a data directory is not supported.
|
||||
|
||||
@@ -26,6 +26,18 @@ spec:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: penpot
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: paperless
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: paperless
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: database
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: paperless-database-init
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: statuspage
|
||||
|
||||
@@ -126,6 +126,7 @@ data:
|
||||
: "${NETBOX_DB_PASSWORD:?NETBOX_DB_PASSWORD is required}"
|
||||
: "${NETRONOME_DB_PASSWORD:?NETRONOME_DB_PASSWORD is required}"
|
||||
: "${PENPOT_DB_PASSWORD:?PENPOT_DB_PASSWORD is required}"
|
||||
: "${PAPERLESS_DB_PASSWORD:?PAPERLESS_DB_PASSWORD is required}"
|
||||
: "${STATUSPAGE_DB_PASSWORD:?STATUSPAGE_DB_PASSWORD is required}"
|
||||
|
||||
create_role_and_database() {
|
||||
@@ -147,4 +148,5 @@ data:
|
||||
create_role_and_database netbox netbox "$NETBOX_DB_PASSWORD"
|
||||
create_role_and_database netronome netronome "$NETRONOME_DB_PASSWORD"
|
||||
create_role_and_database penpot penpot "$PENPOT_DB_PASSWORD"
|
||||
create_role_and_database paperless paperless "$PAPERLESS_DB_PASSWORD"
|
||||
create_role_and_database statuspage statuspage "$STATUSPAGE_DB_PASSWORD"
|
||||
@@ -11,4 +11,5 @@ stringData:
|
||||
NETBOX_DB_PASSWORD: ""
|
||||
NETRONOME_DB_PASSWORD: ""
|
||||
PENPOT_DB_PASSWORD: ""
|
||||
PAPERLESS_DB_PASSWORD: ""
|
||||
STATUSPAGE_DB_PASSWORD: ""
|
||||
@@ -15,3 +15,29 @@ The VictoriaMetrics Operator chart and its CRDs are installed before the
|
||||
Kubernetes manifests by the normal deploy workflow. On a cluster where the
|
||||
operator CRDs are not installed yet, CI skips the server-side dry-run of the
|
||||
`VMAgent` resource; the deploy installs the chart before applying that resource.
|
||||
|
||||
## Application metrics
|
||||
|
||||
The application ServiceMonitors use a 30s interval and a 10s timeout:
|
||||
|
||||
- Headscale: the external Service points to the Compose host on port 19090.
|
||||
A VMServiceScrape uses EndpointSlice discovery for this manually managed target.
|
||||
The Compose configuration must bind metrics to `0.0.0.0:9090`.
|
||||
- NetBird: the combined server exports `/metrics` on port 9090. The existing
|
||||
`server.metricsPort` setting enables the listener.
|
||||
- Gitea: `GITEA__metrics__ENABLED` enables `/metrics` on the HTTP port. The public
|
||||
ingress excludes this path. The monitor uses the internal Service directly.
|
||||
- Immich: `IMMICH_TELEMETRY_INCLUDE=all` enables API and worker metrics on ports
|
||||
8081 and 8082. The monitor scrapes both ports on each server replica.
|
||||
|
||||
Deploy through the existing CI and deploy workflow. Gitea and Immich reload their
|
||||
ConfigMap changes through Reloader. Check the VMAgent targets after deployment
|
||||
and query `up{scraper="victoria",namespace=~"netbird|gitea|immich|headscale"}` in
|
||||
VictoriaMetrics. All targets should report 1.
|
||||
|
||||
For rollback, revert the application metrics changes, run CI, and deploy the
|
||||
revert. Remove the three application ServiceMonitors and the Headscale VMServiceScrape explicitly: the deployment
|
||||
workflow applies manifests and does not prune removed resources.
|
||||
|
||||
For Headscale rollback, remove its VMServiceScrape and Service label, restore the
|
||||
previous Compose metrics bind address, and restart only the Headscale service.
|
||||
Whitespace-only changes.
Whitespace-only changes.
Reference in new issue
Block a user