v0.5.10-beta - update-systeem met kanalen en een-klik bijwerken
Some checks failed
Deploy server-up (dev) / deploy (push) Failing after 4s
Some checks failed
Deploy server-up (dev) / deploy (push) Failing after 4s
- Update-kanalen stable/beta in plaats van het vinkje "pre-releases meenemen".
Stable ziet alleen releases, beta ook pre-releases; een release telt hoger
dan zijn eigen beta (0.5.10-beta1 < 0.5.10). UPDATE_INCLUDE_PRERELEASE
migreert automatisch naar het beta-kanaal.
- Nieuw core/selfupdate.py: image uit de registry ophalen, SU_TAG wegschrijven
en de eigen container laten hercreeren door een korte helper-container die op
het nieuwe image draait (bevat de docker- en compose-CLI al). Een container
kan zichzelf niet hercreeren, vandaar de helper.
- Deploy-map wordt uitgelezen uit de compose-labels van de eigen container.
Ontbreken die, draait de container niet vanaf een registry-image of is de
docker-socket er niet, dan meldt de UI waarom bijwerken niet kan.
- Vorige tag wordt onthouden; terugrolknop in de instellingen.
- Update-check een uur gecached, met geforceerde check via de knop; eerder deed
elke paginalading een netwerkverzoek. Laatst-gecontroleerd zichtbaar.
- Registry-inloggegevens instelbaar; het token komt net als de git-tokens nooit
terug via de API en leeg laten betekent ongewijzigd.
- docker-compose.yml gebruikt ${SU_IMAGE:-server-up}:${SU_TAG:-latest}; zonder
SU_IMAGE blijft lokaal bouwen werken zoals voorheen.
- deploy-prod.yml bouwt en pusht het image in dezelfde job wanneer SU_IMAGE
ingesteld is; build.yml is nu alleen handmatig. Bewust geen aparte
build-workflow op dezelfde tag: bij een runner met een job tegelijk zou de
deploy wachten op een build die zelf nog in de wachtrij staat.
- docs/updates.md: werking, complete Forgejo-instelling (registry, tokens,
variables, secrets, runners), release-procedure voor stable en beta,
terugrollen en probleemoplossing.
- Tests uitgebreid naar 148 (kanaallogica, cache, image-afleiding incl.
registry-poortnummers, tag-validatie, .env-schrijven, tokenlek).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
This commit is contained in:
parent
8b29553a71
commit
8ff5f4b1d3
14 changed files with 1297 additions and 65 deletions
|
|
@ -1,52 +1,89 @@
|
||||||
# Forgejo Actions — bouw + push image NAAR DE REGISTRY (optioneel, handmatig)
|
# Forgejo Actions — bouw het image en push het naar de container-registry.
|
||||||
#
|
#
|
||||||
# UITGESCHAKELD voor automatische runs. Deze server bouwt zelf via
|
# Dit image is wat de "Nu bijwerken"-knop in Server Up ophaalt. Draait bij elke
|
||||||
# deploy-prod.yml / deploy.yml (rsync + `docker compose build`), dus een
|
# v*-tag, zodat elke release (en pre-release) een image krijgt met dezelfde tag
|
||||||
# registry-push is niet nodig. Stond dit op `push: tags: v*`, dan bleef de job
|
# als de release zelf: v0.5.10 → server-up:0.5.10.
|
||||||
# hangen omdat hij een runner met label `docker` zoekt die niet bestaat —
|
|
||||||
# dat veroorzaakte de eindeloos "building" run in de Actions-tab.
|
|
||||||
#
|
#
|
||||||
# Wil je tóch naar de Forgejo container-registry publiceren: start deze workflow
|
# Vereisten (Repository → Settings):
|
||||||
# handmatig via de Actions-tab (Run workflow) en zorg dat er een runner met
|
# Variables → REGISTRY = git.example.com (host van je Forgejo)
|
||||||
# label `docker` bestaat (of pas `runs-on` aan naar je eigen runner-label).
|
# OWNER = bes-r (gebruiker/organisatie)
|
||||||
|
# Secrets → PACKAGE_TOKEN = <token met write:package>
|
||||||
#
|
#
|
||||||
# Vereisten (alleen voor handmatige runs):
|
# Zie docs/updates.md voor de complete instelling.
|
||||||
# Settings → Variables → REGISTRY=git.example.com, OWNER=bes-r
|
|
||||||
# Settings → Secrets → PACKAGE_TOKEN=<token met write:package scope>
|
|
||||||
|
|
||||||
name: Build and push image (handmatig)
|
name: Build en push image (handmatig)
|
||||||
|
|
||||||
|
# Alleen handmatig. Bij een v*-tag bouwt en pusht deploy-prod.yml het image al
|
||||||
|
# in dezelfde job — zo kan er geen wachtrij ontstaan waarin de deploy op een
|
||||||
|
# build wacht die zelf nog in de rij staat.
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: "Versie om te bouwen (bv. 0.5.10). Leeg = VERSION-bestand"
|
||||||
|
required: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: docker
|
runs-on: [self-hosted, prod]
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Extract versie uit tag
|
- name: Versie en tags bepalen
|
||||||
id: tag
|
id: meta
|
||||||
run: echo "v=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Login op Forgejo registry
|
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
VERSION="${{ inputs.tag }}"
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
VERSION="$(tr -d '[:space:]' < VERSION)"
|
||||||
|
fi
|
||||||
|
IMAGE="${{ vars.REGISTRY }}/${{ vars.OWNER }}/server-up"
|
||||||
|
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
|
||||||
|
echo "IMAGE=${IMAGE}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
# Pre-release (tag met een streepje, bv. v0.5.10-beta1) krijgt géén
|
||||||
|
# 'latest'-tag: die hoort bij het stable-kanaal.
|
||||||
|
if [[ "$VERSION" == *-* ]]; then
|
||||||
|
echo "EXTRA_TAG=beta" >> "$GITHUB_ENV"
|
||||||
|
else
|
||||||
|
echo "EXTRA_TAG=latest" >> "$GITHUB_ENV"
|
||||||
|
fi
|
||||||
|
echo "Bouwen: ${IMAGE}:${VERSION}"
|
||||||
|
|
||||||
|
- name: Tests draaien
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
python3 -m venv /tmp/su-build-venv
|
||||||
|
/tmp/su-build-venv/bin/pip install -q -r server-up/requirements.txt pytest
|
||||||
|
/tmp/su-build-venv/bin/python -m pytest tests -q
|
||||||
|
|
||||||
|
- name: Inloggen op de registry
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
echo "${{ secrets.PACKAGE_TOKEN }}" \
|
echo "${{ secrets.PACKAGE_TOKEN }}" \
|
||||||
| docker login ${{ vars.REGISTRY }} \
|
| docker login "${{ vars.REGISTRY }}" \
|
||||||
-u ${{ vars.OWNER }} \
|
-u "${{ vars.OWNER }}" --password-stdin
|
||||||
--password-stdin
|
|
||||||
|
|
||||||
- name: Bouw image met versie ingebakken
|
- name: Bouwen
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
docker build \
|
docker build \
|
||||||
--build-arg SU_VERSION=${{ steps.tag.outputs.v }} \
|
--pull \
|
||||||
-t ${{ vars.REGISTRY }}/${{ vars.OWNER }}/server-up:${{ steps.tag.outputs.v }} \
|
--build-arg "SU_VERSION=${VERSION}" \
|
||||||
-t ${{ vars.REGISTRY }}/${{ vars.OWNER }}/server-up:latest \
|
-t "${IMAGE}:${VERSION}" \
|
||||||
|
-t "${IMAGE}:${EXTRA_TAG}" \
|
||||||
.
|
.
|
||||||
|
|
||||||
- name: Push image
|
- name: Pushen
|
||||||
run: |
|
run: |
|
||||||
docker push ${{ vars.REGISTRY }}/${{ vars.OWNER }}/server-up:${{ steps.tag.outputs.v }}
|
set -euo pipefail
|
||||||
docker push ${{ vars.REGISTRY }}/${{ vars.OWNER }}/server-up:latest
|
docker push "${IMAGE}:${VERSION}"
|
||||||
|
docker push "${IMAGE}:${EXTRA_TAG}"
|
||||||
|
echo "Gepusht: ${IMAGE}:${VERSION} en ${IMAGE}:${EXTRA_TAG}"
|
||||||
|
|
||||||
|
- name: Uitloggen
|
||||||
|
if: always()
|
||||||
|
run: docker logout "${{ vars.REGISTRY }}" || true
|
||||||
|
|
|
||||||
|
|
@ -60,13 +60,41 @@ jobs:
|
||||||
working-directory: ${{ env.DEPLOY_DIR }}
|
working-directory: ${{ env.DEPLOY_DIR }}
|
||||||
run: echo "${VERSION}" > VERSION
|
run: echo "${VERSION}" > VERSION
|
||||||
|
|
||||||
- name: Build image
|
- name: Bouwen (en pushen naar de registry)
|
||||||
working-directory: ${{ env.DEPLOY_DIR }}
|
working-directory: ${{ env.DEPLOY_DIR }}
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# SU_TAG vastleggen zodat compose en de "Nu bijwerken"-knop in de app
|
||||||
|
# naar hetzelfde image wijzen (zie docs/updates.md).
|
||||||
|
touch .env
|
||||||
|
sed -i '/^SU_TAG=/d' .env
|
||||||
|
echo "SU_TAG=${VERSION}" >> .env
|
||||||
|
|
||||||
|
# Draait deze server vanaf de registry (SU_IMAGE in .env), dan bouwen
|
||||||
|
# we hier én pushen we het resultaat, zodat de in-app updateknop het
|
||||||
|
# image kan ophalen. Bouwen en pushen zitten bewust in dezelfde job:
|
||||||
|
# een aparte build-workflow zou met deze deploy om de runner strijden.
|
||||||
|
SU_IMAGE="$(grep -E '^SU_IMAGE=' .env | cut -d= -f2- || true)"
|
||||||
docker compose build --pull
|
docker compose build --pull
|
||||||
# docker compose bouwt als server-up:latest; voeg versie- en channel-tags toe
|
|
||||||
docker tag server-up:latest "server-up:${VERSION}"
|
if [ -n "$SU_IMAGE" ]; then
|
||||||
docker tag server-up:latest "server-up:${CHANNEL}"
|
echo "Registry-modus: ${SU_IMAGE}:${VERSION} pushen"
|
||||||
|
echo "${{ secrets.PACKAGE_TOKEN }}" \
|
||||||
|
| docker login "${{ vars.REGISTRY }}" -u "${{ vars.OWNER }}" --password-stdin
|
||||||
|
docker tag "${SU_IMAGE}:${VERSION}" "${SU_IMAGE}:${CHANNEL}" || true
|
||||||
|
docker push "${SU_IMAGE}:${VERSION}"
|
||||||
|
# Pre-releases krijgen geen 'latest': dat hoort bij stable.
|
||||||
|
case "$VERSION" in
|
||||||
|
*-*) docker tag "${SU_IMAGE}:${VERSION}" "${SU_IMAGE}:beta"
|
||||||
|
docker push "${SU_IMAGE}:beta" ;;
|
||||||
|
*) docker tag "${SU_IMAGE}:${VERSION}" "${SU_IMAGE}:latest"
|
||||||
|
docker push "${SU_IMAGE}:latest" ;;
|
||||||
|
esac
|
||||||
|
docker logout "${{ vars.REGISTRY }}" || true
|
||||||
|
else
|
||||||
|
echo "Lokale bouwmodus (geen SU_IMAGE ingesteld)"
|
||||||
|
docker tag "server-up:${VERSION}" "server-up:${CHANNEL}" || true
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
working-directory: ${{ env.DEPLOY_DIR }}
|
working-directory: ${{ env.DEPLOY_DIR }}
|
||||||
|
|
|
||||||
|
|
@ -63,10 +63,14 @@ jobs:
|
||||||
- name: Build image
|
- name: Build image
|
||||||
working-directory: ${{ env.DEPLOY_DIR }}
|
working-directory: ${{ env.DEPLOY_DIR }}
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# De dev-server bouwt altijd zelf vanaf de dev-branch. SU_TAG vastleggen
|
||||||
|
# zodat compose exact het zojuist gebouwde image start.
|
||||||
|
touch .env
|
||||||
|
sed -i '/^SU_TAG=/d' .env
|
||||||
|
echo "SU_TAG=${VERSION}" >> .env
|
||||||
docker compose build --pull
|
docker compose build --pull
|
||||||
# docker compose bouwt als server-up:latest; voeg versie- en channel-tags toe
|
docker tag "server-up:${VERSION}" "server-up:${CHANNEL}" || true
|
||||||
docker tag server-up:latest "server-up:${VERSION}"
|
|
||||||
docker tag server-up:latest "server-up:${CHANNEL}"
|
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
working-directory: ${{ env.DEPLOY_DIR }}
|
working-directory: ${{ env.DEPLOY_DIR }}
|
||||||
|
|
|
||||||
53
CHANGELOG.md
53
CHANGELOG.md
|
|
@ -1,3 +1,56 @@
|
||||||
|
# v0.5.10-beta — Update-systeem met kanalen en één-klik bijwerken
|
||||||
|
|
||||||
|
## 🔄 Kanalen
|
||||||
|
|
||||||
|
Server Up kent nu twee update-kanalen in plaats van een vinkje "pre-releases
|
||||||
|
meenemen":
|
||||||
|
|
||||||
|
- **stable** — alleen echte releases (`v0.5.10`)
|
||||||
|
- **beta** — ook pre-releases (`v0.5.10-beta1`), die `release.yml` automatisch
|
||||||
|
als zodanig markeert bij een tag met een streepje
|
||||||
|
|
||||||
|
Een release telt hoger dan zijn eigen beta (`0.5.10-beta1` < `0.5.10`), dus wie
|
||||||
|
op beta zit krijgt de definitieve versie alsnog aangeboden. De oude instelling
|
||||||
|
`UPDATE_INCLUDE_PRERELEASE` migreert automatisch naar het beta-kanaal.
|
||||||
|
|
||||||
|
## ⬇️ Bijwerken vanuit de interface
|
||||||
|
|
||||||
|
Nieuw: **Nu bijwerken** haalt het image uit je Forgejo container-registry en
|
||||||
|
vervangt de eigen container.
|
||||||
|
|
||||||
|
- Omdat een container zichzelf niet kan hercreëren, doet Server Up alleen het
|
||||||
|
voorwerk (image ophalen, `SU_TAG` wegschrijven) en laat het de hercreatie over
|
||||||
|
aan een korte helper-container die op het nieuwe image draait — dat bevat de
|
||||||
|
docker- en compose-CLI al.
|
||||||
|
- De deploy-map wordt uitgelezen uit de compose-labels van de eigen container,
|
||||||
|
niet geraden. Ontbreken die labels, draait de container niet vanaf een
|
||||||
|
registry-image, of is de docker-socket er niet, dan meldt de interface
|
||||||
|
precies waaróm bijwerken niet kan in plaats van iets te proberen.
|
||||||
|
- De vorige tag wordt onthouden, met een **terugrolknop** in de instellingen.
|
||||||
|
- Voortgang van de `docker pull` loopt via het bestaande job-logvenster; daarna
|
||||||
|
pollt de browser `/healthz` tot de nieuwe versie leeft.
|
||||||
|
|
||||||
|
## 🔧 Overig
|
||||||
|
|
||||||
|
- Update-check wordt een uur gecached; **Controleren** forceert een verse check.
|
||||||
|
Eerder deed elke paginalading een netwerkverzoek.
|
||||||
|
- Laatst-gecontroleerd-tijdstip zichtbaar in de interface.
|
||||||
|
- Registry-inloggegevens instelbaar voor een privé registry. Het token wordt net
|
||||||
|
als de git-tokens nooit teruggegeven door de API (alleen een `has_`-vlag) en
|
||||||
|
leeg laten betekent "ongewijzigd".
|
||||||
|
- `docker-compose.yml` gebruikt `${SU_IMAGE:-server-up}:${SU_TAG:-latest}`, zodat
|
||||||
|
image en tag los instelbaar zijn. Zonder `SU_IMAGE` blijft alles werken zoals
|
||||||
|
voorheen (lokaal bouwen).
|
||||||
|
- `deploy-prod.yml` bouwt én pusht het image in dezelfde job wanneer `SU_IMAGE`
|
||||||
|
ingesteld is. Bewust niet als aparte workflow: bij één runner zou de deploy
|
||||||
|
wachten op een build die zelf nog in de wachtrij staat.
|
||||||
|
- `build.yml` is nu alleen handmatig, voor het herbouwen van een specifieke tag.
|
||||||
|
- `docs/updates.md` toegevoegd: hoe het werkt, de complete Forgejo-instelling
|
||||||
|
(registry, tokens, variables, secrets, runners), de release-procedure voor
|
||||||
|
zowel stable als beta, terugrollen en een probleemoplostabel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# v0.5.00-beta — Authenticatie, beveiliging en eigen IP-adressen
|
# v0.5.00-beta — Authenticatie, beveiliging en eigen IP-adressen
|
||||||
|
|
||||||
> **Let op bij het bijwerken.** Server Up heeft nu een login. Open na het
|
> **Let op bij het bijwerken.** Server Up heeft nu een login. Open na het
|
||||||
|
|
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.5.00-beta
|
0.5.10-beta
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,11 @@ services:
|
||||||
# Bakt de versie in het image. De deploy-workflow zet SU_VERSION
|
# Bakt de versie in het image. De deploy-workflow zet SU_VERSION
|
||||||
# (git-tag of main-<sha>); zonder waarde valt het terug op 'dev'.
|
# (git-tag of main-<sha>); zonder waarde valt het terug op 'dev'.
|
||||||
SU_VERSION: ${SU_VERSION:-dev}
|
SU_VERSION: ${SU_VERSION:-dev}
|
||||||
image: server-up:latest
|
# Image + tag staan los, zodat bijwerken vanuit de interface werkt: die
|
||||||
|
# schrijft SU_TAG in .env en laat compose de container hercreëren.
|
||||||
|
# Wil je bijwerken via de interface, zet SU_IMAGE dan op je registry:
|
||||||
|
# SU_IMAGE=git.example.com/bes-r/server-up
|
||||||
|
image: ${SU_IMAGE:-server-up}:${SU_TAG:-latest}
|
||||||
container_name: server-up
|
container_name: server-up
|
||||||
hostname: server-up
|
hostname: server-up
|
||||||
user: "0:0"
|
user: "0:0"
|
||||||
|
|
|
||||||
278
docs/updates.md
Normal file
278
docs/updates.md
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
# Updates en Forgejo-instelling
|
||||||
|
|
||||||
|
Server Up controleert zelf of er een nieuwere versie is en kan zichzelf
|
||||||
|
bijwerken met één klik. Dit document beschrijft hoe dat werkt en hoe je Forgejo
|
||||||
|
er precies voor inricht.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hoe het werkt
|
||||||
|
|
||||||
|
Er zijn twee losse onderdelen:
|
||||||
|
|
||||||
|
| Onderdeel | Wat het doet | Waar het vandaan komt |
|
||||||
|
|-----------|--------------|------------------------|
|
||||||
|
| **Detectie** | "Er is een v0.5.10" | Forgejo **Releases-API** |
|
||||||
|
| **Uitvoeren** | Het nieuwe image ophalen en de container vervangen | Forgejo **container-registry** |
|
||||||
|
|
||||||
|
Je hebt ze allebei nodig voor de updateknop. Alleen detectie werkt ook prima —
|
||||||
|
dan zie je een melding en werk je bij met een git-tag, zoals voorheen.
|
||||||
|
|
||||||
|
### Kanalen
|
||||||
|
|
||||||
|
| Kanaal | Ziet | Voorbeeld |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| `stable` | Alleen echte releases | `v0.5.10` |
|
||||||
|
| `beta` | Ook pre-releases | `v0.5.10-beta1` én `v0.5.10` |
|
||||||
|
|
||||||
|
Een tag met een streepje erin (`v0.5.10-beta1`) wordt door `release.yml`
|
||||||
|
automatisch als pre-release gemarkeerd. Semver bepaalt de volgorde, en een
|
||||||
|
release telt hoger dan zijn eigen beta: `0.5.10-beta1` < `0.5.10`. Zit je op
|
||||||
|
beta en verschijnt de definitieve `v0.5.10`, dan krijg je die dus als update
|
||||||
|
aangeboden.
|
||||||
|
|
||||||
|
### Wat er gebeurt als je op "Nu bijwerken" klikt
|
||||||
|
|
||||||
|
```
|
||||||
|
1. docker pull git.example.com/bes-r/server-up:0.5.10
|
||||||
|
2. SU_TAG=0.5.10 wegschrijven in het .env van de deploy-map
|
||||||
|
3. helper-container starten: sleep 5 && docker compose up -d server-up
|
||||||
|
4. Server Up wordt vervangen; de browser pollt /healthz tot de nieuwe versie leeft
|
||||||
|
```
|
||||||
|
|
||||||
|
Stap 3 is nodig omdat een container zichzelf niet kan hercreëren: het commando
|
||||||
|
zou halverwege zijn eigen proces afbreken. De helper draait op het zojuist
|
||||||
|
gehaalde image — dat bevat de docker- en compose-CLI al — en ruimt zichzelf op.
|
||||||
|
|
||||||
|
De deploy-map wordt niet geraden maar uitgelezen uit de compose-labels op de
|
||||||
|
eigen container (`com.docker.compose.project.working_dir`). Draait Server Up niet
|
||||||
|
via compose, dan meldt de interface dat bijwerken niet kan in plaats van iets te
|
||||||
|
proberen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deel 1 — Forgejo inrichten
|
||||||
|
|
||||||
|
### 1.1 Container-registry aanzetten
|
||||||
|
|
||||||
|
De registry zit in Forgejo ingebouwd, maar staat niet altijd aan. Controleer in
|
||||||
|
`app.ini` op je Forgejo-server:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[packages]
|
||||||
|
ENABLED = true
|
||||||
|
```
|
||||||
|
|
||||||
|
Herstart Forgejo na een wijziging. Ga daarna naar je profiel → **Packages**; als
|
||||||
|
die pagina bestaat, werkt de registry.
|
||||||
|
|
||||||
|
> **Draai je Forgejo op een poort (bv. `:3000`) zonder TLS?** Dan moet Docker die
|
||||||
|
> registry als "insecure" kennen. Zet op elke server die images ophaalt in
|
||||||
|
> `/etc/docker/daemon.json`:
|
||||||
|
>
|
||||||
|
> ```json
|
||||||
|
> { "insecure-registries": ["10.0.20.22:3000"] }
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> en `systemctl restart docker`. Met HTTPS en een geldig certificaat is dit niet
|
||||||
|
> nodig — dat heeft de voorkeur.
|
||||||
|
|
||||||
|
### 1.2 Token aanmaken voor de registry
|
||||||
|
|
||||||
|
**Instellingen → Applicaties → Nieuw token genereren**
|
||||||
|
|
||||||
|
| Veld | Waarde |
|
||||||
|
|------|--------|
|
||||||
|
| Naam | `server-up-packages` |
|
||||||
|
| Rechten | `package: Read and Write` |
|
||||||
|
|
||||||
|
Kopieer het token meteen — je ziet het maar één keer. Je hebt het twee keer
|
||||||
|
nodig: als repository-secret (voor de build) en in Server Up zelf (voor het
|
||||||
|
ophalen).
|
||||||
|
|
||||||
|
Wil je scheiden: maak een tweede token met alleen `package: Read` voor de
|
||||||
|
servers die enkel ophalen. Dat is netter, maar niet verplicht.
|
||||||
|
|
||||||
|
### 1.3 Repository-variabelen en -secrets
|
||||||
|
|
||||||
|
Ga naar de repository → **Settings → Actions**.
|
||||||
|
|
||||||
|
**Variables** (niet geheim, zichtbaar in logs):
|
||||||
|
|
||||||
|
| Naam | Waarde | Toelichting |
|
||||||
|
|------|--------|-------------|
|
||||||
|
| `REGISTRY` | `10.0.20.22:3000` | Host van je Forgejo, zonder `http://` |
|
||||||
|
| `OWNER` | `bes-r` | Je gebruikersnaam of organisatie |
|
||||||
|
|
||||||
|
**Secrets** (versleuteld, gemaskeerd in logs):
|
||||||
|
|
||||||
|
| Naam | Waarde |
|
||||||
|
|------|--------|
|
||||||
|
| `PACKAGE_TOKEN` | het token uit 1.2 |
|
||||||
|
| `RELEASE_TOKEN` | *(optioneel)* token met `repository: Read and Write`, als het automatische `GITHUB_TOKEN` niet volstaat voor `release.yml` |
|
||||||
|
|
||||||
|
### 1.4 Runners controleren
|
||||||
|
|
||||||
|
De workflows verwachten deze labels:
|
||||||
|
|
||||||
|
| Workflow | Draait op | Trigger |
|
||||||
|
|----------|-----------|---------|
|
||||||
|
| `deploy.yml` | `[self-hosted, dev]` | push naar `dev` |
|
||||||
|
| `deploy-prod.yml` | `[self-hosted, prod]` | push naar `main` + `v*`-tags |
|
||||||
|
| `release.yml` | `[self-hosted, prod]` | `v*`-tags |
|
||||||
|
| `build.yml` | `[self-hosted, prod]` | alleen handmatig |
|
||||||
|
|
||||||
|
Controleer je labels met:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# op de server waar act_runner draait
|
||||||
|
grep -A3 'labels:' /etc/act_runner/config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Staat er iets anders, pas dan `runs-on:` in de workflows aan óf hernoem de
|
||||||
|
labels van de runner. De runner heeft toegang tot de docker-socket nodig.
|
||||||
|
|
||||||
|
> **Let op de volgorde.** `deploy-prod.yml` bouwt én pusht het image in dezelfde
|
||||||
|
> job. Dat is bewust: een aparte build-workflow zou bij één runner met de deploy
|
||||||
|
> om dezelfde plek in de wachtrij strijden, waarbij de deploy wacht op een build
|
||||||
|
> die zelf nog in de rij staat. `build.yml` is er alleen voor handmatig
|
||||||
|
> herbouwen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deel 2 — De server instellen
|
||||||
|
|
||||||
|
Op de server waar Server Up draait, in de deploy-map (`/opt/server-up`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
SU_IMAGE=10.0.20.22:3000/bes-r/server-up
|
||||||
|
SU_TAG=0.5.10
|
||||||
|
BIND=127.0.0.1
|
||||||
|
PORT=5000
|
||||||
|
```
|
||||||
|
|
||||||
|
`SU_IMAGE` is het schakelaartje: staat het er, dan draait deze server vanaf de
|
||||||
|
registry en werkt de updateknop. Staat het er niet, dan bouwt de server lokaal
|
||||||
|
zoals voorheen en meldt de interface netjes waarom bijwerken niet kan.
|
||||||
|
|
||||||
|
Eerste keer overstappen van lokaal bouwen naar de registry:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/server-up
|
||||||
|
docker login 10.0.20.22:3000 -u bes-r # token uit 1.2 als wachtwoord
|
||||||
|
docker pull 10.0.20.22:3000/bes-r/server-up:0.5.10
|
||||||
|
docker compose up -d
|
||||||
|
docker compose ps # controleer de nieuwe image-naam
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deel 3 — Server Up instellen
|
||||||
|
|
||||||
|
**Instellingen → Updates**:
|
||||||
|
|
||||||
|
| Veld | Waarde |
|
||||||
|
|------|--------|
|
||||||
|
| Kanaal | `Stable` of `Beta` |
|
||||||
|
| Releases-API URL | `http://10.0.20.22:3000/api/v1/repos/bes-r/server-up/releases` |
|
||||||
|
| Image | `10.0.20.22:3000/bes-r/server-up` |
|
||||||
|
| Registry-gebruiker | `bes-r` *(alleen bij een privé repo)* |
|
||||||
|
| Registry-token | het token uit 1.2 *(idem)* |
|
||||||
|
|
||||||
|
Het registry-token wordt opgeslagen in `config.json` (0600) en komt nooit terug
|
||||||
|
via de API — je ziet alleen of het ingesteld is. Leeg laten bij het opslaan
|
||||||
|
betekent "niet wijzigen".
|
||||||
|
|
||||||
|
Klik op **Controleren**. Staat er een versie, dan werkt de detectie.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Een versie uitbrengen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Versienummer bijwerken
|
||||||
|
echo "0.5.10" > VERSION
|
||||||
|
|
||||||
|
# 2. Changelog-sectie schrijven (release.yml haalt de notes hieruit)
|
||||||
|
$EDITOR CHANGELOG.md # begin met: # v0.5.10 — Korte titel
|
||||||
|
|
||||||
|
# 3. Vastleggen en taggen
|
||||||
|
git add VERSION CHANGELOG.md
|
||||||
|
git commit -m "v0.5.10 - korte omschrijving"
|
||||||
|
git push origin dev
|
||||||
|
|
||||||
|
# 4. Via een pull request naar main, dan pas taggen
|
||||||
|
git checkout main && git pull
|
||||||
|
git tag v0.5.10
|
||||||
|
git push origin v0.5.10
|
||||||
|
```
|
||||||
|
|
||||||
|
Die tag zet drie dingen in gang:
|
||||||
|
|
||||||
|
1. **`release.yml`** maakt de Forgejo-release met notes uit `CHANGELOG.md` plus
|
||||||
|
een commitoverzicht sinds de vorige tag.
|
||||||
|
2. **`deploy-prod.yml`** draait de tests, bouwt het image, pusht het als
|
||||||
|
`:0.5.10` én `:latest`, en herstart de prod-server.
|
||||||
|
3. Elke Server Up-installatie ziet bij de volgende check de nieuwe versie.
|
||||||
|
|
||||||
|
### Een beta uitbrengen
|
||||||
|
|
||||||
|
Precies hetzelfde, met een streepje in de tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "0.5.10-beta1" > VERSION
|
||||||
|
git commit -am "v0.5.10-beta1"
|
||||||
|
git tag v0.5.10-beta1
|
||||||
|
git push origin v0.5.10-beta1
|
||||||
|
```
|
||||||
|
|
||||||
|
`release.yml` markeert dit automatisch als pre-release, en het image krijgt de
|
||||||
|
tags `:0.5.10-beta1` en `:beta` (géén `:latest`). Alleen installaties op het
|
||||||
|
beta-kanaal zien hem.
|
||||||
|
|
||||||
|
Volgens de projectafspraken toont de dev-branch altijd `beta` in het
|
||||||
|
versienummer; die tags horen dus bij wat je vanuit dev uitbrengt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Terugrollen
|
||||||
|
|
||||||
|
Server Up onthoudt bij elke update de vorige tag. Onder **Instellingen →
|
||||||
|
Updates** verschijnt dan een knop "Terug naar deze versie", die precies hetzelfde
|
||||||
|
doet met het oude image.
|
||||||
|
|
||||||
|
Start de nieuwe versie helemaal niet — dan is er ook geen interface om op te
|
||||||
|
klikken — dan doe je het met de hand:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/server-up
|
||||||
|
sed -i 's/^SU_TAG=.*/SU_TAG=0.5.00/' .env
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problemen oplossen
|
||||||
|
|
||||||
|
| Symptoom | Oorzaak en oplossing |
|
||||||
|
|----------|----------------------|
|
||||||
|
| Knop staat uit, "komt niet uit een registry" | `SU_IMAGE` ontbreekt in `.env`, of de container draait nog op een lokaal gebouwd image. Zie deel 2. |
|
||||||
|
| Knop staat uit, "geen compose-labels" | De container is met `docker run` gestart in plaats van met compose. Start hem via `docker compose up -d`. |
|
||||||
|
| Knop staat uit, "container niet te vinden" | De docker-socket is niet gemount, of de containernaam wijkt af. Zet `SU_CONTAINER` op de juiste naam. |
|
||||||
|
| `docker pull` faalt met "unauthorized" | Registry-gebruiker/token niet ingesteld in Server Up, of het token mist `package: Read`. |
|
||||||
|
| `docker pull` faalt met "http: server gave HTTP response to HTTPS client" | Insecure registry niet geconfigureerd; zie 1.1. |
|
||||||
|
| "Geen releases gevonden in kanaal 'stable'" | Er zijn alleen pre-releases. Zet het kanaal op Beta of breng een release zonder streepje uit. |
|
||||||
|
| Update-check geeft een netwerkfout | De Releases-URL klopt niet, of Server Up kan Forgejo niet bereiken. Test met `curl` vanuit de container. |
|
||||||
|
| Interface komt na bijwerken niet terug | Nieuwe image start niet. Kijk met `docker compose logs server-up` en rol handmatig terug (zie boven). |
|
||||||
|
| Nieuwe versie verschijnt niet | De check cachet een uur. Klik op **Controleren** om te forceren. |
|
||||||
|
|
||||||
|
### Handmatig controleren of de registry werkt
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Is het image gepusht?
|
||||||
|
curl -s -u bes-r:<token> \
|
||||||
|
http://10.0.20.22:3000/v2/bes-r/server-up/tags/list
|
||||||
|
|
||||||
|
# Geeft de Releases-API iets terug?
|
||||||
|
curl -s http://10.0.20.22:3000/api/v1/repos/bes-r/server-up/releases \
|
||||||
|
| python3 -m json.tool | head -30
|
||||||
|
```
|
||||||
|
|
@ -15,7 +15,7 @@ from flask import Flask, Response, jsonify, render_template, request, session
|
||||||
import core as cfg
|
import core as cfg
|
||||||
from core import audit, jobs, i18n
|
from core import audit, jobs, i18n
|
||||||
from core import auth, docker, git, boilerplates, updater
|
from core import auth, docker, git, boilerplates, updater
|
||||||
from core import compose_transform, networks
|
from core import compose_transform, networks, selfupdate
|
||||||
from core.modules import Module, CORE, discover
|
from core.modules import Module, CORE, discover
|
||||||
|
|
||||||
app = Flask(__name__, static_folder="static", template_folder="templates")
|
app = Flask(__name__, static_folder="static", template_folder="templates")
|
||||||
|
|
@ -336,7 +336,81 @@ def api_version():
|
||||||
|
|
||||||
@app.route("/api/update")
|
@app.route("/api/update")
|
||||||
def api_update():
|
def api_update():
|
||||||
return jsonify(updater.check(VERSION))
|
"""Status van de update-check, aangevuld met of bijwerken mogelijk is."""
|
||||||
|
force = request.args.get("force") in ("1", "true", "yes")
|
||||||
|
res = updater.check(VERSION, force=force)
|
||||||
|
st = selfupdate.status()
|
||||||
|
res.update({
|
||||||
|
"can_apply": st["can_apply"],
|
||||||
|
"apply_reason": st["reason"],
|
||||||
|
"image": st["image"],
|
||||||
|
"previous_tag": st["previous_tag"],
|
||||||
|
"channels": list(updater.CHANNELS),
|
||||||
|
})
|
||||||
|
return jsonify(res)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/update/apply", methods=["POST"])
|
||||||
|
def api_update_apply():
|
||||||
|
"""Werk Server Up bij naar een tag uit de releases-API.
|
||||||
|
|
||||||
|
Draait als job zodat de UI de voortgang van de `docker pull` kan volgen.
|
||||||
|
Kort na afloop verdwijnt deze container; de front-end pollt daarna /healthz.
|
||||||
|
"""
|
||||||
|
d = request.json or {}
|
||||||
|
tag = (d.get("tag") or "").strip()
|
||||||
|
if not tag:
|
||||||
|
info = updater.check(VERSION)
|
||||||
|
tag = info.get("latest_tag") or ""
|
||||||
|
if not tag:
|
||||||
|
return jsonify(ok=False, msg="Geen versie opgegeven en geen release gevonden"), 400
|
||||||
|
st = selfupdate.status()
|
||||||
|
if not st["can_apply"]:
|
||||||
|
return jsonify(ok=False, msg=st["reason"]), 400
|
||||||
|
|
||||||
|
jid, q = jobs.create(f"update:{tag}")
|
||||||
|
gebruiker = auth.current_user() or ""
|
||||||
|
|
||||||
|
def _run(jid):
|
||||||
|
qq = jobs.get_queue(jid)
|
||||||
|
try:
|
||||||
|
jobs.log(qq, "section", f"Bijwerken naar {tag}")
|
||||||
|
ok, msg = selfupdate.apply(tag, log_fn=lambda m: jobs.log(qq, "dim", m))
|
||||||
|
jobs.log(qq, "ok" if ok else "error", msg)
|
||||||
|
audit.log("update", "apply", "ok" if ok else "error", ref=tag,
|
||||||
|
detail=gebruiker)
|
||||||
|
jobs.finish(jid, "done" if ok else "error")
|
||||||
|
except Exception as e:
|
||||||
|
jobs.log(qq, "error", str(e))
|
||||||
|
jobs.finish(jid, "error")
|
||||||
|
jobs.done(qq)
|
||||||
|
|
||||||
|
jobs.run(_run, jid)
|
||||||
|
updater.invalidate()
|
||||||
|
return jsonify(ok=True, job_id=jid, tag=tag)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/update/rollback", methods=["POST"])
|
||||||
|
def api_update_rollback():
|
||||||
|
st = selfupdate.status()
|
||||||
|
if not st["can_apply"]:
|
||||||
|
return jsonify(ok=False, msg=st["reason"]), 400
|
||||||
|
if not st.get("previous_tag"):
|
||||||
|
return jsonify(ok=False, msg="Geen vorige versie bekend"), 400
|
||||||
|
|
||||||
|
jid, q = jobs.create("update:rollback")
|
||||||
|
|
||||||
|
def _run(jid):
|
||||||
|
qq = jobs.get_queue(jid)
|
||||||
|
ok, msg = selfupdate.rollback(log_fn=lambda m: jobs.log(qq, "dim", m))
|
||||||
|
jobs.log(qq, "ok" if ok else "error", msg)
|
||||||
|
audit.log("update", "rollback", "ok" if ok else "error",
|
||||||
|
ref=st.get("previous_tag", ""), ip=request.remote_addr)
|
||||||
|
jobs.finish(jid, "done" if ok else "error")
|
||||||
|
jobs.done(qq)
|
||||||
|
|
||||||
|
jobs.run(_run, jid)
|
||||||
|
return jsonify(ok=True, job_id=jid, tag=st["previous_tag"])
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/docker/images")
|
@app.route("/api/docker/images")
|
||||||
|
|
@ -1476,6 +1550,14 @@ def api_settings_put():
|
||||||
blocked = _SETTINGS_HIDE | cfg.SECRET_KEYS
|
blocked = _SETTINGS_HIDE | cfg.SECRET_KEYS
|
||||||
updates = {k: v for k, v in d.items() if k not in blocked}
|
updates = {k: v for k, v in d.items() if k not in blocked}
|
||||||
current = cfg.load()
|
current = cfg.load()
|
||||||
|
# Write-only credentials: leeg laten staan betekent "niet wijzigen", want de
|
||||||
|
# UI kent de huidige waarde niet (zie cfg.redact).
|
||||||
|
for key in cfg.WRITE_ONLY_KEYS:
|
||||||
|
updates.pop(f"has_{key.lower()}", None)
|
||||||
|
if key in updates and not str(updates[key] or "").strip():
|
||||||
|
updates.pop(key)
|
||||||
|
if (updates.get("UPDATE_CHANNEL") or "stable") not in updater.CHANNELS:
|
||||||
|
return jsonify(ok=False, msg="Onbekend update-kanaal"), 400
|
||||||
for key in ("APP_REPOS", "MODULE_REPOS"):
|
for key in ("APP_REPOS", "MODULE_REPOS"):
|
||||||
if key not in updates:
|
if key not in updates:
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,18 @@ DEFAULTS = {
|
||||||
# Leeg = uitgeschakeld. Voorbeeld (Forgejo):
|
# Leeg = uitgeschakeld. Voorbeeld (Forgejo):
|
||||||
# http://10.0.20.22:3000/api/v1/repos/bes-r/server-up/releases
|
# http://10.0.20.22:3000/api/v1/repos/bes-r/server-up/releases
|
||||||
"UPDATE_API_URL": os.environ.get("SU_UPDATE_API", ""),
|
"UPDATE_API_URL": os.environ.get("SU_UPDATE_API", ""),
|
||||||
"UPDATE_INCLUDE_PRERELEASE": False,
|
"UPDATE_INCLUDE_PRERELEASE": False, # verouderd, zie UPDATE_CHANNEL
|
||||||
|
# Kanaal: "stable" (alleen releases) of "beta" (ook pre-releases).
|
||||||
|
"UPDATE_CHANNEL": os.environ.get("SU_UPDATE_CHANNEL", "stable"),
|
||||||
|
# Image waarvandaan bijgewerkt wordt, zonder tag. Leeg = afleiden uit de
|
||||||
|
# draaiende container. Bv. git.example.com/bes-r/server-up
|
||||||
|
"UPDATE_IMAGE": os.environ.get("SU_UPDATE_IMAGE", ""),
|
||||||
|
# Alleen nodig bij een privé registry.
|
||||||
|
"UPDATE_REGISTRY_USER": "",
|
||||||
|
"UPDATE_REGISTRY_TOKEN": "",
|
||||||
|
# Administratie van de laatste update (voor terugrollen).
|
||||||
|
"UPDATE_PREVIOUS_TAG": "",
|
||||||
|
"UPDATE_LAST_APPLIED": {},
|
||||||
# Authenticatie — beheerd via core.auth; hier alleen als placeholder zodat
|
# Authenticatie — beheerd via core.auth; hier alleen als placeholder zodat
|
||||||
# load() de sleutel kent. Wordt nooit via /api/settings teruggegeven.
|
# load() de sleutel kent. Wordt nooit via /api/settings teruggegeven.
|
||||||
"AUTH": {},
|
"AUTH": {},
|
||||||
|
|
@ -47,9 +58,14 @@ DEFAULTS = {
|
||||||
"NETWORKS": [],
|
"NETWORKS": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Sleutels die nooit naar de client mogen: geheimen of interne administratie.
|
# Sleutels die de client noch mag lezen noch mag schrijven; AUTH bevat
|
||||||
|
# wachtwoordhashes en loopt uitsluitend via /api/auth/*.
|
||||||
SECRET_KEYS = frozenset({"AUTH"})
|
SECRET_KEYS = frozenset({"AUTH"})
|
||||||
|
|
||||||
|
# Credentials die je wél mag instellen maar nooit terugkrijgt. Bij het opslaan
|
||||||
|
# betekent een lege waarde "ongewijzigd" — net als bij de git-tokens.
|
||||||
|
WRITE_ONLY_KEYS = frozenset({"UPDATE_REGISTRY_TOKEN"})
|
||||||
|
|
||||||
_path: Path = Path(os.environ.get("SU_CONFIG", "/data/config.json"))
|
_path: Path = Path(os.environ.get("SU_CONFIG", "/data/config.json"))
|
||||||
_lock = threading.RLock()
|
_lock = threading.RLock()
|
||||||
|
|
||||||
|
|
@ -118,6 +134,10 @@ def redact(config: dict) -> dict:
|
||||||
dát er een token is zonder de waarde prijs te geven.
|
dát er een token is zonder de waarde prijs te geven.
|
||||||
"""
|
"""
|
||||||
out = {k: v for k, v in config.items() if k not in SECRET_KEYS}
|
out = {k: v for k, v in config.items() if k not in SECRET_KEYS}
|
||||||
|
for key in WRITE_ONLY_KEYS:
|
||||||
|
if key in out:
|
||||||
|
out[f"has_{key.lower()}"] = bool(str(out[key] or "").strip())
|
||||||
|
out[key] = ""
|
||||||
for key in ("APP_REPOS", "MODULE_REPOS"):
|
for key in ("APP_REPOS", "MODULE_REPOS"):
|
||||||
repos = out.get(key)
|
repos = out.get(key)
|
||||||
if not isinstance(repos, list):
|
if not isinstance(repos, list):
|
||||||
|
|
|
||||||
268
server-up/core/selfupdate.py
Normal file
268
server-up/core/selfupdate.py
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
"""Zelf-update: Server Up vervangt zijn eigen container door een nieuwer image.
|
||||||
|
|
||||||
|
Het lastige aan jezelf bijwerken is dat het proces dat `docker compose up -d`
|
||||||
|
uitvoert, halverwege door datzelfde commando wordt gestopt. Daarom doet Server
|
||||||
|
Up alleen het voorwerk (image ophalen, tag wegschrijven) en laat het de
|
||||||
|
daadwerkelijke hercreatie over aan een korte **helper-container**:
|
||||||
|
|
||||||
|
1. `docker pull <registry>/<repo>:<tag>` ← nieuw image binnenhalen
|
||||||
|
2. `SU_TAG=<tag>` in het .env van de deploy-map schrijven
|
||||||
|
3. helper starten: sleep 5 && docker compose up -d server-up
|
||||||
|
4. helper stopt zichzelf (`--rm`)
|
||||||
|
|
||||||
|
De helper draait op het zojuist gepulde image: dat bevat de docker- en
|
||||||
|
compose-CLI al (zie Dockerfile) en is per definitie aanwezig, dus er is geen
|
||||||
|
extra image nodig.
|
||||||
|
|
||||||
|
De deploy-map wordt niet geraden maar uitgelezen uit de compose-labels die
|
||||||
|
Docker op de eigen container heeft gezet.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shlex
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import core as cfg
|
||||||
|
from core import docker
|
||||||
|
|
||||||
|
# Labels die `docker compose` op elke container zet.
|
||||||
|
_LBL_PROJECT = "com.docker.compose.project"
|
||||||
|
_LBL_WORKDIR = "com.docker.compose.project.working_dir"
|
||||||
|
_LBL_CONFIGS = "com.docker.compose.project.config_files"
|
||||||
|
_LBL_SERVICE = "com.docker.compose.service"
|
||||||
|
|
||||||
|
_TAG_RE = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$")
|
||||||
|
_IMAGE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]*(:[0-9]{1,5})?(/[A-Za-z0-9._/-]+)*$")
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ── Zelfinspectie ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def container_name() -> str:
|
||||||
|
return os.environ.get("HOSTNAME") or os.environ.get("SU_CONTAINER", "server-up")
|
||||||
|
|
||||||
|
|
||||||
|
def inspect_self() -> dict:
|
||||||
|
"""Gegevens over de eigen container: image, compose-project en deploy-map."""
|
||||||
|
r = docker._run([docker._bin(), "inspect", container_name()], timeout=15)
|
||||||
|
if r.returncode != 0:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
data = json.loads(r.stdout)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
if not data:
|
||||||
|
return {}
|
||||||
|
c = data[0]
|
||||||
|
labels = (c.get("Config") or {}).get("Labels") or {}
|
||||||
|
return {
|
||||||
|
"name": (c.get("Name") or "").lstrip("/"),
|
||||||
|
"image": (c.get("Config") or {}).get("Image", ""),
|
||||||
|
"image_id": c.get("Image", ""),
|
||||||
|
"project": labels.get(_LBL_PROJECT, ""),
|
||||||
|
"working_dir": labels.get(_LBL_WORKDIR, ""),
|
||||||
|
"config_files": labels.get(_LBL_CONFIGS, ""),
|
||||||
|
"service": labels.get(_LBL_SERVICE, "") or "server-up",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def image_repo(info: dict | None = None) -> str:
|
||||||
|
"""Het image zonder tag, bv. `git.example.com/bes-r/server-up`.
|
||||||
|
|
||||||
|
Voorkeur: expliciet ingesteld via UPDATE_IMAGE. Anders afgeleid uit het
|
||||||
|
image waarmee deze container draait.
|
||||||
|
"""
|
||||||
|
expliciet = (cfg.load().get("UPDATE_IMAGE") or "").strip()
|
||||||
|
if expliciet:
|
||||||
|
return expliciet.rsplit(":", 1)[0] if _has_tag(expliciet) else expliciet
|
||||||
|
img = (info or inspect_self()).get("image", "")
|
||||||
|
if not img:
|
||||||
|
return ""
|
||||||
|
return img.rsplit(":", 1)[0] if _has_tag(img) else img
|
||||||
|
|
||||||
|
|
||||||
|
def _has_tag(image: str) -> bool:
|
||||||
|
"""`host:5000/repo` heeft geen tag, `repo:1.2` wel — kijk ná de laatste `/`."""
|
||||||
|
laatste = image.rsplit("/", 1)[-1]
|
||||||
|
return ":" in laatste
|
||||||
|
|
||||||
|
|
||||||
|
def status() -> dict:
|
||||||
|
"""Kan deze installatie zichzelf bijwerken, en zo niet: waarom niet?"""
|
||||||
|
info = inspect_self()
|
||||||
|
repo = image_repo(info)
|
||||||
|
res = {"can_apply": False, "reason": "", "image": repo,
|
||||||
|
"current_image": info.get("image", ""),
|
||||||
|
"working_dir": info.get("working_dir", ""),
|
||||||
|
"service": info.get("service", ""),
|
||||||
|
"previous_tag": (cfg.load().get("UPDATE_PREVIOUS_TAG") or "")}
|
||||||
|
if not info:
|
||||||
|
res["reason"] = ("De eigen container is niet te vinden via de "
|
||||||
|
"Docker-socket. Draait Server Up wel in Docker?")
|
||||||
|
return res
|
||||||
|
if not info.get("working_dir"):
|
||||||
|
res["reason"] = ("Deze container is niet met docker compose gestart "
|
||||||
|
"(geen compose-labels), dus bijwerken vanuit de "
|
||||||
|
"interface kan niet.")
|
||||||
|
return res
|
||||||
|
if not repo:
|
||||||
|
res["reason"] = "Geen image bekend om bij te werken."
|
||||||
|
return res
|
||||||
|
if "/" not in repo:
|
||||||
|
res["reason"] = (f"Het image '{repo}' komt niet uit een registry. Stel "
|
||||||
|
"UPDATE_IMAGE in op bv. git.example.com/bes-r/server-up "
|
||||||
|
"en draai vanaf een registry-image.")
|
||||||
|
return res
|
||||||
|
res["can_apply"] = True
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
# ── Registry ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _login(log_fn=None) -> tuple[bool, str]:
|
||||||
|
"""Optionele registry-login voor een privé Forgejo-registry."""
|
||||||
|
c = cfg.load()
|
||||||
|
user = (c.get("UPDATE_REGISTRY_USER") or "").strip()
|
||||||
|
token = (c.get("UPDATE_REGISTRY_TOKEN") or "").strip()
|
||||||
|
if not user or not token:
|
||||||
|
return True, "" # publieke registry of al ingelogd
|
||||||
|
registry = image_repo().split("/")[0]
|
||||||
|
if log_fn:
|
||||||
|
log_fn(f"Inloggen op {registry} als {user}…")
|
||||||
|
r = docker._run([docker._bin(), "login", registry, "-u", user,
|
||||||
|
"--password-stdin"], input=token, timeout=30)
|
||||||
|
if r.returncode != 0:
|
||||||
|
# Het token mag nooit in een logregel belanden.
|
||||||
|
melding = (r.stderr or r.stdout or "").replace(token, "***").strip()
|
||||||
|
return False, f"Registry-login mislukt: {melding[:200]}"
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
|
def pull(tag: str, log_fn=None) -> tuple[bool, str]:
|
||||||
|
"""Haal `<repo>:<tag>` binnen. Retourneert (ok, image of foutmelding)."""
|
||||||
|
if not _TAG_RE.match(tag or ""):
|
||||||
|
return False, f"Ongeldige tag: {tag!r}"
|
||||||
|
repo = image_repo()
|
||||||
|
if not repo or not _IMAGE_RE.match(repo):
|
||||||
|
return False, f"Ongeldig image: {repo!r}"
|
||||||
|
ok, err = _login(log_fn)
|
||||||
|
if not ok:
|
||||||
|
return False, err
|
||||||
|
image = f"{repo}:{tag}"
|
||||||
|
if log_fn:
|
||||||
|
log_fn(f"Ophalen {image}…")
|
||||||
|
rc = docker._stream([docker._bin(), "pull", image], log_fn=log_fn)
|
||||||
|
if rc != 0:
|
||||||
|
return False, (f"Ophalen van {image} mislukt. Bestaat die tag in de "
|
||||||
|
"registry, en heeft deze server er toegang toe?")
|
||||||
|
return True, image
|
||||||
|
|
||||||
|
|
||||||
|
# ── Toepassen ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _write_tag(working_dir: str, tag: str, log_fn=None):
|
||||||
|
"""Zet SU_TAG in het .env van de deploy-map, zodat compose het nieuwe image
|
||||||
|
gebruikt en de wijziging een herstart overleeft."""
|
||||||
|
env_file = Path(working_dir) / ".env"
|
||||||
|
regels: list[str] = []
|
||||||
|
if env_file.exists():
|
||||||
|
regels = env_file.read_text(encoding="utf-8").splitlines()
|
||||||
|
nieuw, gezet = [], False
|
||||||
|
for r in regels:
|
||||||
|
if r.strip().startswith("SU_TAG="):
|
||||||
|
nieuw.append(f"SU_TAG={tag}")
|
||||||
|
gezet = True
|
||||||
|
else:
|
||||||
|
nieuw.append(r)
|
||||||
|
if not gezet:
|
||||||
|
nieuw.append(f"SU_TAG={tag}")
|
||||||
|
env_file.write_text("\n".join(nieuw) + "\n", encoding="utf-8")
|
||||||
|
if log_fn:
|
||||||
|
log_fn(f"SU_TAG={tag} vastgelegd in {env_file}")
|
||||||
|
|
||||||
|
|
||||||
|
def apply(tag: str, log_fn=None) -> tuple[bool, str]:
|
||||||
|
"""Werk Server Up bij naar `tag`.
|
||||||
|
|
||||||
|
Bij succes keert deze functie terug terwijl de helper al gestart is; enkele
|
||||||
|
seconden later verdwijnt dit proces omdat de container hercreëerd wordt.
|
||||||
|
"""
|
||||||
|
st = status()
|
||||||
|
if not st["can_apply"]:
|
||||||
|
return False, st["reason"]
|
||||||
|
|
||||||
|
info = inspect_self()
|
||||||
|
werkmap = info["working_dir"]
|
||||||
|
service = info["service"]
|
||||||
|
|
||||||
|
ok, image_of_fout = pull(tag, log_fn)
|
||||||
|
if not ok:
|
||||||
|
return False, image_of_fout
|
||||||
|
image = image_of_fout
|
||||||
|
|
||||||
|
# Onthoud waar we vandaan komen, zodat terugrollen mogelijk blijft als het
|
||||||
|
# nieuwe image niet start.
|
||||||
|
huidig = info.get("image", "")
|
||||||
|
vorige_tag = huidig.rsplit(":", 1)[-1] if _has_tag(huidig) else "latest"
|
||||||
|
cfg.patch({"UPDATE_PREVIOUS_TAG": vorige_tag,
|
||||||
|
"UPDATE_LAST_APPLIED": {"tag": tag, "ts": time.time(),
|
||||||
|
"from": vorige_tag}})
|
||||||
|
|
||||||
|
try:
|
||||||
|
_write_tag(werkmap, tag, log_fn)
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"Kan {werkmap}/.env niet schrijven: {e}"
|
||||||
|
|
||||||
|
if log_fn:
|
||||||
|
log_fn(f"Vorige versie: {vorige_tag} (terugrollen kan vanuit Instellingen)")
|
||||||
|
log_fn("Helper starten die de container hercreëert…")
|
||||||
|
|
||||||
|
ok, melding = _spawn_helper(image, werkmap, service, log_fn)
|
||||||
|
if not ok:
|
||||||
|
return False, melding
|
||||||
|
return True, (f"Bijwerken naar {tag} gestart. Server Up herstart nu; "
|
||||||
|
"de interface komt over enkele seconden terug.")
|
||||||
|
|
||||||
|
|
||||||
|
def _spawn_helper(image: str, working_dir: str, service: str,
|
||||||
|
log_fn=None) -> tuple[bool, str]:
|
||||||
|
"""Start een korte container die `docker compose up -d <service>` uitvoert.
|
||||||
|
|
||||||
|
Draait op hetzelfde (nieuwe) image, dat de docker- en compose-CLI al bevat.
|
||||||
|
`sleep` geeft dit proces de tijd om zijn HTTP-antwoord af te maken voordat
|
||||||
|
het onder zijn eigen voeten wordt weggehaald.
|
||||||
|
"""
|
||||||
|
script = (f"sleep 5; "
|
||||||
|
f"docker compose up -d --remove-orphans {shlex.quote(service)}")
|
||||||
|
cmd = [docker._bin(), "run", "--rm", "--detach",
|
||||||
|
"--name", "server-up-updater",
|
||||||
|
"-v", "/var/run/docker.sock:/var/run/docker.sock",
|
||||||
|
"-v", f"{working_dir}:{working_dir}",
|
||||||
|
"-w", working_dir,
|
||||||
|
"--entrypoint", "sh",
|
||||||
|
image, "-c", script]
|
||||||
|
# Een blijven hangen helper van een vorige poging zou de naam bezet houden.
|
||||||
|
docker._run([docker._bin(), "rm", "-f", "server-up-updater"], timeout=15)
|
||||||
|
r = docker._run(cmd, timeout=30)
|
||||||
|
if r.returncode != 0:
|
||||||
|
return False, f"Helper starten mislukt: {(r.stderr or r.stdout).strip()[:300]}"
|
||||||
|
if log_fn:
|
||||||
|
log_fn("Helper gestart — tot zo.")
|
||||||
|
return True, "ok"
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(log_fn=None) -> tuple[bool, str]:
|
||||||
|
"""Ga terug naar de tag die vóór de laatste update draaide."""
|
||||||
|
vorige = (cfg.load().get("UPDATE_PREVIOUS_TAG") or "").strip()
|
||||||
|
if not vorige:
|
||||||
|
return False, "Geen vorige versie bekend om naar terug te rollen."
|
||||||
|
if log_fn:
|
||||||
|
log_fn(f"Terugrollen naar {vorige}…")
|
||||||
|
return apply(vorige, log_fn)
|
||||||
|
|
@ -1,13 +1,37 @@
|
||||||
"""Update-check tegen een Forgejo/GitHub Releases-API (stdlib only)."""
|
"""Update-check tegen een Forgejo/GitHub Releases-API (stdlib only).
|
||||||
|
|
||||||
|
Twee kanalen:
|
||||||
|
|
||||||
|
* **stable** — alleen echte releases (`v0.5.10`)
|
||||||
|
* **beta** — ook pre-releases (`v0.5.10-beta1`), zoals `release.yml` die
|
||||||
|
markeert voor tags met een streepje erin
|
||||||
|
|
||||||
|
Een beta-gebruiker ziet dus zowel betas als stabiele releases; wie op stable zit
|
||||||
|
ziet alleen releases. Schakel je van beta terug naar stable terwijl je een beta
|
||||||
|
draait, dan wordt de eerstvolgende stabiele release aangeboden zodra die hoger
|
||||||
|
is (`0.5.10-beta1` < `0.5.10`).
|
||||||
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
import core as cfg
|
import core as cfg
|
||||||
|
|
||||||
_SEMVER = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)(?:[-+.](.+))?$")
|
_SEMVER = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)(?:[-+.](.+))?$")
|
||||||
|
|
||||||
|
CHANNELS = ("stable", "beta")
|
||||||
|
|
||||||
|
# Resultaat van de laatste check, zodat niet elke paginalading een
|
||||||
|
# netwerkverzoek doet. De UI kan met force=True alsnog verversen.
|
||||||
|
_CACHE: dict = {}
|
||||||
|
_CACHE_TS: float = 0.0
|
||||||
|
_CACHE_TTL = 3600
|
||||||
|
_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
def _pre_key(pre: str):
|
def _pre_key(pre: str):
|
||||||
parts = [p for p in re.split(r"[.+-]", pre) if p] if pre else []
|
parts = [p for p in re.split(r"[.+-]", pre) if p] if pre else []
|
||||||
|
|
@ -26,30 +50,65 @@ def _parse(v: str):
|
||||||
1 if not pre else 0, _pre_key(pre))
|
1 if not pre else 0, _pre_key(pre))
|
||||||
|
|
||||||
|
|
||||||
def check(current: str) -> dict:
|
def channel() -> str:
|
||||||
|
"""Het ingestelde kanaal, met migratie vanaf de oude boolean."""
|
||||||
|
c = cfg.load()
|
||||||
|
ch = (c.get("UPDATE_CHANNEL") or "").strip().lower()
|
||||||
|
if ch in CHANNELS:
|
||||||
|
return ch
|
||||||
|
# Oude instelling: UPDATE_INCLUDE_PRERELEASE=true betekende 'beta'.
|
||||||
|
if c.get("UPDATE_INCLUDE_PRERELEASE"):
|
||||||
|
return "beta"
|
||||||
|
return "stable"
|
||||||
|
|
||||||
|
|
||||||
|
def is_prerelease(version: str) -> bool:
|
||||||
|
t = _parse(version)
|
||||||
|
return bool(t and t[3] == 0)
|
||||||
|
|
||||||
|
|
||||||
|
def _releases(url: str) -> list[dict]:
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url, headers={"Accept": "application/json", "User-Agent": "server-up"})
|
||||||
|
with urllib.request.urlopen(req, timeout=8) as r:
|
||||||
|
data = json.loads(r.read().decode("utf-8"))
|
||||||
|
return data if isinstance(data, list) else [data]
|
||||||
|
|
||||||
|
|
||||||
|
def check(current: str, force: bool = False) -> dict:
|
||||||
|
"""Zoek de nieuwste release in het ingestelde kanaal.
|
||||||
|
|
||||||
|
Het resultaat wordt een uur gecached; `force=True` omzeilt de cache.
|
||||||
|
"""
|
||||||
|
global _CACHE, _CACHE_TS
|
||||||
|
with _LOCK:
|
||||||
|
if not force and _CACHE and (time.time() - _CACHE_TS) < _CACHE_TTL:
|
||||||
|
if _CACHE.get("current") == current:
|
||||||
|
return dict(_CACHE)
|
||||||
|
|
||||||
c = cfg.load()
|
c = cfg.load()
|
||||||
url = (c.get("UPDATE_API_URL") or "").strip()
|
url = (c.get("UPDATE_API_URL") or "").strip()
|
||||||
include_pre = bool(c.get("UPDATE_INCLUDE_PRERELEASE"))
|
ch = channel()
|
||||||
res = {"enabled": bool(url), "current": current, "latest": None,
|
res = {"enabled": bool(url), "current": current, "channel": ch,
|
||||||
"update_available": False, "url": "", "notes": "",
|
"current_is_prerelease": is_prerelease(current),
|
||||||
"prerelease": False, "error": ""}
|
"latest": None, "latest_tag": "", "update_available": False,
|
||||||
|
"url": "", "notes": "", "prerelease": False, "error": "",
|
||||||
|
"checked_at": time.time()}
|
||||||
if not url:
|
if not url:
|
||||||
return res
|
return res
|
||||||
|
|
||||||
try:
|
try:
|
||||||
req = urllib.request.Request(
|
rels = _releases(url)
|
||||||
url, headers={"Accept": "application/json", "User-Agent": "server-up"})
|
|
||||||
with urllib.request.urlopen(req, timeout=8) as r:
|
|
||||||
data = json.loads(r.read().decode("utf-8"))
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
res["error"] = f"{type(e).__name__}: {e}"
|
res["error"] = f"{type(e).__name__}: {e}"
|
||||||
return res
|
return res
|
||||||
|
|
||||||
rels = data if isinstance(data, list) else [data]
|
|
||||||
best, best_t = None, None
|
best, best_t = None, None
|
||||||
for rel in rels:
|
for rel in rels:
|
||||||
if not isinstance(rel, dict) or rel.get("draft"):
|
if not isinstance(rel, dict) or rel.get("draft"):
|
||||||
continue
|
continue
|
||||||
if rel.get("prerelease") and not include_pre:
|
# Op stable tellen pre-releases niet mee; op beta wél.
|
||||||
|
if rel.get("prerelease") and ch != "beta":
|
||||||
continue
|
continue
|
||||||
t = _parse(rel.get("tag_name") or rel.get("name") or "")
|
t = _parse(rel.get("tag_name") or rel.get("name") or "")
|
||||||
if not t:
|
if not t:
|
||||||
|
|
@ -57,13 +116,28 @@ def check(current: str) -> dict:
|
||||||
if best_t is None or t > best_t:
|
if best_t is None or t > best_t:
|
||||||
best, best_t = rel, t
|
best, best_t = rel, t
|
||||||
if not best:
|
if not best:
|
||||||
|
res["error"] = f"Geen releases gevonden in kanaal '{ch}'"
|
||||||
return res
|
return res
|
||||||
|
|
||||||
res["latest"] = (best.get("tag_name") or best.get("name") or "").lstrip("v")
|
tag = (best.get("tag_name") or best.get("name") or "")
|
||||||
|
res["latest_tag"] = tag
|
||||||
|
res["latest"] = tag.lstrip("v")
|
||||||
res["url"] = best.get("html_url") or best.get("url") or ""
|
res["url"] = best.get("html_url") or best.get("url") or ""
|
||||||
res["notes"] = best.get("body") or ""
|
res["notes"] = best.get("body") or ""
|
||||||
res["prerelease"] = bool(best.get("prerelease"))
|
res["prerelease"] = bool(best.get("prerelease"))
|
||||||
cur_t = _parse(current)
|
cur_t = _parse(current)
|
||||||
if cur_t and best_t and best_t > cur_t:
|
if cur_t and best_t and best_t > cur_t:
|
||||||
res["update_available"] = True
|
res["update_available"] = True
|
||||||
|
|
||||||
|
with _LOCK:
|
||||||
|
_CACHE = dict(res)
|
||||||
|
_CACHE_TS = time.time()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate():
|
||||||
|
"""Gooi de cache weg (na een instellingswijziging of na een update)."""
|
||||||
|
global _CACHE, _CACHE_TS
|
||||||
|
with _LOCK:
|
||||||
|
_CACHE = {}
|
||||||
|
_CACHE_TS = 0.0
|
||||||
|
|
|
||||||
|
|
@ -550,20 +550,74 @@ tailwind.config = {
|
||||||
<pre class="p-3.5 text-xs whitespace-pre-wrap break-words text-t2 dark:text-t2-dark max-h-64 overflow-auto" x-text="update.notes"></pre>
|
<pre class="p-3.5 text-xs whitespace-pre-wrap break-words text-t2 dark:text-t2-dark max-h-64 overflow-auto" x-text="update.notes"></pre>
|
||||||
</details>
|
</details>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="update.update_available">
|
<div class="flex flex-wrap gap-2" x-show="update.update_available">
|
||||||
<a :href="update.url" target="_blank" rel="noopener" class="btn btn-primary w-max"><span class="mdi mdi-open-in-new"></span> <span x-text="t('view_release')">Bekijk release</span></a>
|
<button class="btn btn-primary" @click="applyUpdate()" :disabled="!update.can_apply || updating">
|
||||||
</template>
|
<span class="mdi" :class="updating ? 'mdi-loading mdi-spin' : 'mdi-download'"></span>
|
||||||
|
<span x-text="updating ? 'Bezig…' : ('Nu bijwerken naar v'+update.latest)"></span>
|
||||||
|
</button>
|
||||||
|
<a :href="update.url" target="_blank" rel="noopener" class="btn"><span class="mdi mdi-open-in-new"></span> <span x-text="t('view_release')">Bekijk release</span></a>
|
||||||
|
</div>
|
||||||
|
<!-- Waarom de knop niet bruikbaar is (geen registry-image, geen
|
||||||
|
compose-labels, …) — anders staat er alleen een dode knop. -->
|
||||||
|
<p x-show="update.update_available && !update.can_apply"
|
||||||
|
class="text-xs text-wr flex items-start gap-1.5">
|
||||||
|
<span class="mdi mdi-alert-outline shrink-0"></span>
|
||||||
|
<span><span x-text="update.apply_reason"></span>
|
||||||
|
Bijwerken kan wel via een git-tag; zie <span class="font-mono">docs/updates.md</span>.</span>
|
||||||
|
</p>
|
||||||
<p x-show="update.error" class="text-xs text-er" x-text="update.error"></p>
|
<p x-show="update.error" class="text-xs text-er" x-text="update.error"></p>
|
||||||
|
<p x-show="update.checked_at" class="text-[11px] text-dm dark:text-dm-dark">
|
||||||
|
Laatst gecontroleerd: <span x-text="fmtTime(update.checked_at)"></span>
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="pt-2 border-t border-bd dark:border-bd-dark space-y-3">
|
<div class="pt-2 border-t border-bd dark:border-bd-dark space-y-3">
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Kanaal</label>
|
||||||
|
<div class="inline-flex rounded-xl bg-s2 dark:bg-s2-dark p-1 gap-1">
|
||||||
|
<button class="seg" :class="updChannel()==='stable' ? 'seg-on' : ''"
|
||||||
|
@click="setChannel('stable')"><span class="mdi mdi-check-decagram"></span> Stable</button>
|
||||||
|
<button class="seg" :class="updChannel()==='beta' ? 'seg-on' : ''"
|
||||||
|
@click="setChannel('beta')"><span class="mdi mdi-flask-outline"></span> Beta</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-[11px] text-dm dark:text-dm-dark mt-1">
|
||||||
|
<span x-show="updChannel()==='stable'">Alleen stabiele releases.</span>
|
||||||
|
<span x-show="updChannel()==='beta'">Ook pre-releases (bv. v0.5.10-beta1). Handig om mee te testen, minder voorspelbaar.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="form-label" x-text="t('update_api_url')">Releases-API URL</label>
|
<label class="form-label" x-text="t('update_api_url')">Releases-API URL</label>
|
||||||
<input class="form-input font-mono" x-model="settings.UPDATE_API_URL" @change="autoSave(); setTimeout(()=>loadUpdate(), 600)" placeholder="http://git.example.com/api/v1/repos/bes-r/server-up/releases">
|
<input class="form-input font-mono" x-model="settings.UPDATE_API_URL" @change="autoSave(); setTimeout(()=>loadUpdate(true), 600)" placeholder="http://git.example.com/api/v1/repos/bes-r/server-up/releases">
|
||||||
<p class="text-[11px] text-dm dark:text-dm-dark mt-1" x-text="t('update_api_hint')">Forgejo/GitHub Releases-API. Leeg = uit.</p>
|
<p class="text-[11px] text-dm dark:text-dm-dark mt-1" x-text="t('update_api_hint')">Forgejo/GitHub Releases-API. Leeg = uit.</p>
|
||||||
</div>
|
</div>
|
||||||
<label class="inline-flex items-center gap-2.5 cursor-pointer">
|
<div>
|
||||||
<input type="checkbox" :checked="!!settings.UPDATE_INCLUDE_PRERELEASE" @change="settings.UPDATE_INCLUDE_PRERELEASE=$event.target.checked; autoSave(); setTimeout(()=>loadUpdate(), 600)" class="w-5 h-5 rounded border-bd dark:border-bd-dark text-ac">
|
<label class="form-label">Image (voor bijwerken vanuit de interface)</label>
|
||||||
<span class="text-sm text-tx dark:text-tx-dark" x-text="t('include_prerelease')">Pre-releases (beta) meenemen</span>
|
<input class="form-input font-mono" x-model="settings.UPDATE_IMAGE" @change="autoSave(); setTimeout(()=>loadUpdate(true), 600)" placeholder="git.example.com/bes-r/server-up">
|
||||||
</label>
|
<p class="text-[11px] text-dm dark:text-dm-dark mt-1">
|
||||||
|
Zonder tag. Leeg = afleiden uit de draaiende container.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid sm:grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Registry-gebruiker (optioneel)</label>
|
||||||
|
<input class="form-input" x-model="settings.UPDATE_REGISTRY_USER" @change="autoSave()" placeholder="bes-r">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Registry-token</label>
|
||||||
|
<input type="password" class="form-input" x-model="settings.UPDATE_REGISTRY_TOKEN" @change="autoSave()"
|
||||||
|
:placeholder="settings.has_update_registry_token ? '•••••••• (ingesteld)' : 'alleen bij een privé registry'">
|
||||||
|
<p class="text-[11px] text-dm dark:text-dm-dark mt-1">Leeg laten = ongewijzigd.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div x-show="update.previous_tag" class="pt-2 border-t border-bd dark:border-bd-dark">
|
||||||
|
<label class="form-label">Terugrollen</label>
|
||||||
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
<span class="text-xs text-t2 dark:text-t2-dark">Vorige versie:
|
||||||
|
<span class="font-mono text-tx dark:text-tx-dark" x-text="update.previous_tag"></span></span>
|
||||||
|
<button class="btn-sm btn-danger" @click="rollbackUpdate()" :disabled="updating">
|
||||||
|
<span class="mdi mdi-undo-variant"></span> Terug naar deze versie
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1043,7 +1097,10 @@ function app() {
|
||||||
settings: {},
|
settings: {},
|
||||||
languages: [],
|
languages: [],
|
||||||
modules: [],
|
modules: [],
|
||||||
update: { enabled:false, update_available:false, current:'', latest:'', url:'', notes:'', prerelease:false },
|
update: { enabled:false, update_available:false, current:'', latest:'', latest_tag:'',
|
||||||
|
url:'', notes:'', prerelease:false, channel:'stable', can_apply:false,
|
||||||
|
apply_reason:'', previous_tag:'', checked_at:0 },
|
||||||
|
updating: false,
|
||||||
T: {},
|
T: {},
|
||||||
|
|
||||||
job: { id: null, lines: [], status: 'idle', timer: null },
|
job: { id: null, lines: [], status: 'idle', timer: null },
|
||||||
|
|
@ -1535,7 +1592,63 @@ function app() {
|
||||||
|
|
||||||
async loadModules() { const d = await this.rpc('/api/modules'); this.modules = Array.isArray(d) ? d : []; },
|
async loadModules() { const d = await this.rpc('/api/modules'); this.modules = Array.isArray(d) ? d : []; },
|
||||||
|
|
||||||
async loadUpdate() { const d = await this.rpc('/api/update'); if (d && typeof d === 'object') this.update = d; },
|
async loadUpdate(force) {
|
||||||
|
const d = await this.rpc('/api/update' + (force ? '?force=1' : ''));
|
||||||
|
if (d && typeof d === 'object') this.update = d;
|
||||||
|
},
|
||||||
|
updChannel() { return this.settings.UPDATE_CHANNEL || this.update.channel || 'stable'; },
|
||||||
|
setChannel(ch) {
|
||||||
|
this.settings.UPDATE_CHANNEL = ch;
|
||||||
|
this.autoSave();
|
||||||
|
setTimeout(() => this.loadUpdate(true), 600);
|
||||||
|
},
|
||||||
|
fmtTime(ts) {
|
||||||
|
if (!ts) return '';
|
||||||
|
return new Date(ts * 1000).toLocaleString();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Bijwerken: de container vervangt zichzelf, dus na het startsein is deze
|
||||||
|
// pagina zijn server kwijt. We pollen /healthz tot de nieuwe versie leeft.
|
||||||
|
async applyUpdate() {
|
||||||
|
const naar = this.update.latest_tag || this.update.latest;
|
||||||
|
if (!confirm('Server Up bijwerken naar ' + naar + '?\n\n'
|
||||||
|
+ 'De container wordt vervangen en is enkele seconden niet bereikbaar.')) return;
|
||||||
|
this.updating = true;
|
||||||
|
const d = await this.rpc('/api/update/apply', { tag: naar });
|
||||||
|
if (!d.job_id) { this.updating = false; this.toast(d.msg || 'Fout', 'er'); return; }
|
||||||
|
this.termOpen = true;
|
||||||
|
this.pollJob(d.job_id, () => this.waitForRestart());
|
||||||
|
},
|
||||||
|
async rollbackUpdate() {
|
||||||
|
if (!confirm('Terugrollen naar ' + this.update.previous_tag + '?')) return;
|
||||||
|
this.updating = true;
|
||||||
|
const d = await this.rpc('/api/update/rollback', {});
|
||||||
|
if (!d.job_id) { this.updating = false; this.toast(d.msg || 'Fout', 'er'); return; }
|
||||||
|
this.termOpen = true;
|
||||||
|
this.pollJob(d.job_id, () => this.waitForRestart());
|
||||||
|
},
|
||||||
|
async waitForRestart() {
|
||||||
|
this.toast('Server Up herstart…', 'in');
|
||||||
|
// Even wachten tot de oude container écht weg is, anders zien we hem nog.
|
||||||
|
await new Promise(r => setTimeout(r, 8000));
|
||||||
|
for (let i = 0; i < 60; i++) {
|
||||||
|
try {
|
||||||
|
const r = await fetch('/healthz', { cache: 'no-store' });
|
||||||
|
if (r.ok) {
|
||||||
|
const d = await r.json();
|
||||||
|
if (d.version && d.version !== this.update.current) {
|
||||||
|
this.updating = false;
|
||||||
|
this.toast('Bijgewerkt naar v' + d.version, 'ok');
|
||||||
|
setTimeout(() => location.reload(), 1200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { /* container is nog aan het herstarten */ }
|
||||||
|
await new Promise(r => setTimeout(r, 2000));
|
||||||
|
}
|
||||||
|
this.updating = false;
|
||||||
|
this.toast('Herstart duurt lang — ververs de pagina zelf', 'wr');
|
||||||
|
},
|
||||||
|
|
||||||
async checkWizard() {
|
async checkWizard() {
|
||||||
const d = await this.rpc('/api/wizard');
|
const d = await this.rpc('/api/wizard');
|
||||||
|
|
|
||||||
156
tests/test_selfupdate.py
Normal file
156
tests/test_selfupdate.py
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
"""Zelf-update: image-afleiding, veiligheidscontroles en de tag-administratie."""
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "server-up"))
|
||||||
|
|
||||||
|
from conftest import login
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def su(env):
|
||||||
|
from core import selfupdate
|
||||||
|
return selfupdate
|
||||||
|
|
||||||
|
|
||||||
|
def _fake_inspect(su, monkeypatch, **overrides):
|
||||||
|
info = {"name": "server-up", "image": "git.example.com/bes-r/server-up:0.5.00",
|
||||||
|
"image_id": "sha256:abc", "project": "server-up",
|
||||||
|
"working_dir": "/opt/server-up",
|
||||||
|
"config_files": "/opt/server-up/docker-compose.yml",
|
||||||
|
"service": "server-up"}
|
||||||
|
info.update(overrides)
|
||||||
|
monkeypatch.setattr(su, "inspect_self", lambda: info)
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("image,verwacht", [
|
||||||
|
("git.example.com/bes-r/server-up:0.5.00", "git.example.com/bes-r/server-up"),
|
||||||
|
("git.example.com/bes-r/server-up", "git.example.com/bes-r/server-up"),
|
||||||
|
("server-up:latest", "server-up"),
|
||||||
|
# Poortnummer in de registryhost mag niet als tag gelezen worden.
|
||||||
|
("git.example.com:3000/bes-r/server-up", "git.example.com:3000/bes-r/server-up"),
|
||||||
|
("git.example.com:3000/bes-r/server-up:1.2.3", "git.example.com:3000/bes-r/server-up"),
|
||||||
|
])
|
||||||
|
def test_image_repo_afleiding(su, monkeypatch, image, verwacht):
|
||||||
|
_fake_inspect(su, monkeypatch, image=image)
|
||||||
|
assert su.image_repo() == verwacht
|
||||||
|
|
||||||
|
|
||||||
|
def test_expliciet_ingesteld_image_wint(su, env, monkeypatch):
|
||||||
|
_fake_inspect(su, monkeypatch)
|
||||||
|
env["core"].patch({"UPDATE_IMAGE": "registry.intern/team/server-up"})
|
||||||
|
assert su.image_repo() == "registry.intern/team/server-up"
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_ok_bij_registry_image(su, monkeypatch):
|
||||||
|
_fake_inspect(su, monkeypatch)
|
||||||
|
st = su.status()
|
||||||
|
assert st["can_apply"] is True
|
||||||
|
assert st["image"] == "git.example.com/bes-r/server-up"
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_weigert_lokaal_gebouwd_image(su, monkeypatch):
|
||||||
|
_fake_inspect(su, monkeypatch, image="server-up:latest")
|
||||||
|
st = su.status()
|
||||||
|
assert st["can_apply"] is False
|
||||||
|
assert "registry" in st["reason"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_weigert_zonder_compose_labels(su, monkeypatch):
|
||||||
|
_fake_inspect(su, monkeypatch, working_dir="")
|
||||||
|
st = su.status()
|
||||||
|
assert st["can_apply"] is False
|
||||||
|
assert "compose" in st["reason"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_zonder_container(su, monkeypatch):
|
||||||
|
monkeypatch.setattr(su, "inspect_self", lambda: {})
|
||||||
|
st = su.status()
|
||||||
|
assert st["can_apply"] is False
|
||||||
|
assert "niet te vinden" in st["reason"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("tag", [
|
||||||
|
"../../etc", "0.5.0; rm -rf /", "-x", "", "a" * 200, "tag met spatie",
|
||||||
|
])
|
||||||
|
def test_pull_weigert_ongeldige_tag(su, monkeypatch, tag):
|
||||||
|
_fake_inspect(su, monkeypatch)
|
||||||
|
ok, msg = su.pull(tag)
|
||||||
|
assert ok is False
|
||||||
|
assert "Ongeldige tag" in msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_tag_maakt_env_aan(su, tmp_path):
|
||||||
|
su._write_tag(str(tmp_path), "0.5.10")
|
||||||
|
assert (tmp_path / ".env").read_text().strip() == "SU_TAG=0.5.10"
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_tag_vervangt_bestaande_regel(su, tmp_path):
|
||||||
|
(tmp_path / ".env").write_text("BIND=0.0.0.0\nSU_TAG=0.5.00\nPORT=5000\n")
|
||||||
|
su._write_tag(str(tmp_path), "0.5.10")
|
||||||
|
regels = (tmp_path / ".env").read_text().splitlines()
|
||||||
|
assert regels == ["BIND=0.0.0.0", "SU_TAG=0.5.10", "PORT=5000"]
|
||||||
|
assert sum(1 for r in regels if r.startswith("SU_TAG=")) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_tag_behoudt_andere_instellingen(su, tmp_path):
|
||||||
|
(tmp_path / ".env").write_text("SU_IMAGE=git.example.com/bes-r/server-up\n")
|
||||||
|
su._write_tag(str(tmp_path), "0.5.10")
|
||||||
|
inhoud = (tmp_path / ".env").read_text()
|
||||||
|
assert "SU_IMAGE=git.example.com/bes-r/server-up" in inhoud
|
||||||
|
assert "SU_TAG=0.5.10" in inhoud
|
||||||
|
|
||||||
|
|
||||||
|
def test_rollback_zonder_vorige_versie(su):
|
||||||
|
ok, msg = su.rollback()
|
||||||
|
assert ok is False
|
||||||
|
assert "Geen vorige versie" in msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_endpoint_weigert_zonder_registry(client, monkeypatch):
|
||||||
|
from core import selfupdate
|
||||||
|
csrf = login(client)
|
||||||
|
monkeypatch.setattr(selfupdate, "inspect_self",
|
||||||
|
lambda: {"image": "server-up:latest", "working_dir": "/opt/x",
|
||||||
|
"service": "server-up"})
|
||||||
|
r = client.post("/api/update/apply", json={"tag": "0.5.10"},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
assert r.status_code == 400
|
||||||
|
assert "registry" in r.get_json()["msg"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_rollback_endpoint_weigert_zonder_vorige_tag(client, monkeypatch):
|
||||||
|
from core import selfupdate
|
||||||
|
csrf = login(client)
|
||||||
|
monkeypatch.setattr(selfupdate, "inspect_self",
|
||||||
|
lambda: {"image": "git.example.com/bes-r/server-up:0.5.00",
|
||||||
|
"working_dir": "/opt/x", "service": "server-up"})
|
||||||
|
r = client.post("/api/update/rollback", headers={"X-CSRF-Token": csrf})
|
||||||
|
assert r.status_code == 400
|
||||||
|
assert "Geen vorige versie" in r.get_json()["msg"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_token_lekt_niet_via_settings(client, env):
|
||||||
|
csrf = login(client)
|
||||||
|
client.put("/api/settings",
|
||||||
|
json={"UPDATE_REGISTRY_USER": "bes-r",
|
||||||
|
"UPDATE_REGISTRY_TOKEN": "geheim-package-token"},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
|
||||||
|
body = client.get("/api/settings").get_data(as_text=True)
|
||||||
|
assert "geheim-package-token" not in body
|
||||||
|
assert "has_update_registry_token" in body
|
||||||
|
# Wel echt opgeslagen:
|
||||||
|
assert env["core"].load()["UPDATE_REGISTRY_TOKEN"] == "geheim-package-token"
|
||||||
|
|
||||||
|
|
||||||
|
def test_leeg_registry_token_wist_bestaande_niet(client, env):
|
||||||
|
csrf = login(client)
|
||||||
|
client.put("/api/settings", json={"UPDATE_REGISTRY_TOKEN": "blijf-staan"},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
client.put("/api/settings", json={"UPDATE_REGISTRY_TOKEN": ""},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
assert env["core"].load()["UPDATE_REGISTRY_TOKEN"] == "blijf-staan"
|
||||||
115
tests/test_update_channels.py
Normal file
115
tests/test_update_channels.py
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
"""Update-kanalen: stable ziet alleen releases, beta ook pre-releases."""
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "server-up"))
|
||||||
|
|
||||||
|
RELEASES = [
|
||||||
|
{"tag_name": "v0.5.00", "prerelease": False, "html_url": "u/0.5.00", "body": "stabiel"},
|
||||||
|
{"tag_name": "v0.5.10-beta1", "prerelease": True, "html_url": "u/beta1", "body": "beta"},
|
||||||
|
{"tag_name": "v0.4.60", "prerelease": False, "html_url": "u/0.4.60", "body": "oud"},
|
||||||
|
{"tag_name": "v9.9.9", "prerelease": False, "draft": True, "html_url": "u/draft"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def upd(env, monkeypatch):
|
||||||
|
from core import updater
|
||||||
|
updater.invalidate()
|
||||||
|
monkeypatch.setattr(updater, "_releases", lambda url: RELEASES)
|
||||||
|
env["core"].patch({"UPDATE_API_URL": "http://forgejo.test/api/releases"})
|
||||||
|
return updater
|
||||||
|
|
||||||
|
|
||||||
|
def test_stable_negeert_prereleases(upd):
|
||||||
|
res = upd.check("0.4.60", force=True)
|
||||||
|
assert res["channel"] == "stable"
|
||||||
|
assert res["latest"] == "0.5.00"
|
||||||
|
assert res["update_available"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_beta_ziet_de_prerelease(upd, env):
|
||||||
|
env["core"].patch({"UPDATE_CHANNEL": "beta"})
|
||||||
|
res = upd.check("0.5.00", force=True)
|
||||||
|
assert res["channel"] == "beta"
|
||||||
|
assert res["latest"] == "0.5.10-beta1"
|
||||||
|
assert res["update_available"] is True
|
||||||
|
assert res["prerelease"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_stable_biedt_geen_update_vanaf_nieuwere_beta(upd, env):
|
||||||
|
"""Draai je 0.5.10-beta1 op stable, dan is 0.5.00 géén update."""
|
||||||
|
env["core"].patch({"UPDATE_CHANNEL": "stable"})
|
||||||
|
res = upd.check("0.5.10-beta1", force=True)
|
||||||
|
assert res["latest"] == "0.5.00"
|
||||||
|
assert res["update_available"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_stabiele_release_wint_van_gelijke_beta(upd, env):
|
||||||
|
"""v0.5.10 hoort hoger te zijn dan v0.5.10-beta1."""
|
||||||
|
env["core"].patch({"UPDATE_CHANNEL": "beta"})
|
||||||
|
import core.updater as u
|
||||||
|
assert u._parse("0.5.10") > u._parse("0.5.10-beta1")
|
||||||
|
|
||||||
|
|
||||||
|
def test_draft_wordt_overgeslagen(upd):
|
||||||
|
assert upd.check("0.1.0", force=True)["latest"] != "9.9.9"
|
||||||
|
|
||||||
|
|
||||||
|
def test_oude_bool_migreert_naar_beta(upd, env):
|
||||||
|
env["core"].patch({"UPDATE_INCLUDE_PRERELEASE": True})
|
||||||
|
env["core"].patch({"UPDATE_CHANNEL": ""})
|
||||||
|
assert upd.channel() == "beta"
|
||||||
|
|
||||||
|
|
||||||
|
def test_expliciet_kanaal_wint_van_oude_bool(upd, env):
|
||||||
|
env["core"].patch({"UPDATE_INCLUDE_PRERELEASE": True, "UPDATE_CHANNEL": "stable"})
|
||||||
|
assert upd.channel() == "stable"
|
||||||
|
|
||||||
|
|
||||||
|
def test_cache_voorkomt_herhaalde_netwerkverzoeken(upd, monkeypatch):
|
||||||
|
tellers = {"n": 0}
|
||||||
|
|
||||||
|
def _tel(url):
|
||||||
|
tellers["n"] += 1
|
||||||
|
return RELEASES
|
||||||
|
|
||||||
|
monkeypatch.setattr(upd, "_releases", _tel)
|
||||||
|
upd.check("0.4.60", force=True)
|
||||||
|
upd.check("0.4.60")
|
||||||
|
upd.check("0.4.60")
|
||||||
|
assert tellers["n"] == 1
|
||||||
|
upd.check("0.4.60", force=True)
|
||||||
|
assert tellers["n"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_zonder_url_uitgeschakeld(upd, env):
|
||||||
|
env["core"].patch({"UPDATE_API_URL": ""})
|
||||||
|
res = upd.check("0.4.60", force=True)
|
||||||
|
assert res["enabled"] is False
|
||||||
|
assert res["update_available"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_netwerkfout_wordt_gemeld(upd, monkeypatch):
|
||||||
|
def _stuk(url):
|
||||||
|
raise TimeoutError("te traag")
|
||||||
|
monkeypatch.setattr(upd, "_releases", _stuk)
|
||||||
|
res = upd.check("0.4.60", force=True)
|
||||||
|
assert "TimeoutError" in res["error"]
|
||||||
|
assert res["update_available"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_prerelease():
|
||||||
|
from core import updater
|
||||||
|
assert updater.is_prerelease("0.5.00-beta") is True
|
||||||
|
assert updater.is_prerelease("0.5.00") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_ongeldig_kanaal_wordt_geweigerd(client):
|
||||||
|
from conftest import login
|
||||||
|
csrf = login(client)
|
||||||
|
r = client.put("/api/settings", json={"UPDATE_CHANNEL": "onzin"},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
assert r.status_code == 400
|
||||||
Loading…
Reference in a new issue