Hub stage 4: its own image and pipeline
docker/Dockerfile.hub builds the console and ships it with the hub sources on node:22-alpine, without node_modules, as a non-root user with a healthcheck. .gitea/workflows/hub.yaml checks and pushes git.shx.one/denozord/technitium-hub (latest from main, sha-*, X.Y.Z and X.Y from hub-v tags), independent of the console pipeline, which now ignores hub-only changes. Compose example for hub.shnt.top and hub/README.md, with the read-only permissions each server's token needs. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
48b5d9fb15
commit
3f917c6bd2
5 files changed
+230
No files matched your search
@@ -4,6 +4,13 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
tags: ['v*']
|
tags: ['v*']
|
||||||
|
# The hub has its own pipeline (hub.yaml): a change that only touches it does
|
||||||
|
# not rebuild the console.
|
||||||
|
paths-ignore:
|
||||||
|
- 'hub/**'
|
||||||
|
- 'docker/Dockerfile.hub'
|
||||||
|
- '.gitea/workflows/hub.yaml'
|
||||||
|
- 'docs/compose.hub.example.yaml'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# The hub has its own pipeline and its own image (git.shx.one/denozord/technitium-hub),
|
||||||
|
# independent of ci.yaml. A change that only touches the console's docs does not run it.
|
||||||
|
name: Hub
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
tags: ['hub-v*']
|
||||||
|
paths:
|
||||||
|
- 'hub/**'
|
||||||
|
- 'src/**'
|
||||||
|
- 'public/**'
|
||||||
|
- 'index.html'
|
||||||
|
- 'package.json'
|
||||||
|
- 'package-lock.json'
|
||||||
|
- 'vite.config.ts'
|
||||||
|
- 'docker/Dockerfile.hub'
|
||||||
|
- '.gitea/workflows/hub.yaml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: hub-${{ gitea.workflow }}-${{ gitea.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run typecheck
|
||||||
|
- run: npx vitest run hub
|
||||||
|
|
||||||
|
image:
|
||||||
|
needs: check
|
||||||
|
if: gitea.ref_name == 'main' || startsWith(gitea.ref, 'refs/tags/hub-v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.shx.one
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.ACTION_PAT || secrets.ACTIONS_PAT }}
|
||||||
|
|
||||||
|
# The hub is not named after the repository: its own image. Registry names are lowercase.
|
||||||
|
- name: Resolve image name
|
||||||
|
id: image
|
||||||
|
run: |
|
||||||
|
owner="$(dirname '${{ gitea.repository }}' | tr '[:upper:]' '[:lower:]')"
|
||||||
|
echo "name=git.shx.one/${owner}/technitium-hub" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# A hub-v1.2.3 tag gives 1.2.3 and 1.2 (the prefix is stripped); main gives latest.
|
||||||
|
- id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ steps.image.outputs.name }}
|
||||||
|
tags: |
|
||||||
|
type=match,pattern=hub-v(\d+\.\d+\.\d+),group=1
|
||||||
|
type=match,pattern=hub-v(\d+\.\d+)\.\d+,group=1
|
||||||
|
type=raw,value=latest,enable=${{ gitea.ref_name == 'main' }}
|
||||||
|
type=sha,prefix=sha-
|
||||||
|
|
||||||
|
- uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/Dockerfile.hub
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=registry,ref=${{ steps.image.outputs.name }}:buildcache
|
||||||
|
cache-to: type=registry,ref=${{ steps.image.outputs.name }}:buildcache,mode=max
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# The hub image built by Gitea CI (git.shx.one): one small container that serves the
|
||||||
|
# console and a read-only, Technitium-compatible API over several real servers.
|
||||||
|
# See hub/README.md. Self-contained: it does not use the console image.
|
||||||
|
#
|
||||||
|
# docker run --rm -p 8080:8080 -e HUB_SERVERS=dns=https://dns.example.net \
|
||||||
|
# -e DNS_TOKEN=... -e HUB_USER=admin -e HUB_PASSWORD=... \
|
||||||
|
# git.shx.one/denozord/technitium-hub:latest
|
||||||
|
FROM node:22-alpine AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
# What the hub runs: its sources without the tests, and a package.json that makes the
|
||||||
|
# .ts files ES modules. It imports only node: built-ins, so there is no node_modules.
|
||||||
|
RUN mkdir -p /out/hub \
|
||||||
|
&& cp hub/*.ts /out/hub/ \
|
||||||
|
&& rm /out/hub/*.test.ts \
|
||||||
|
&& echo '{"type":"module"}' > /out/package.json
|
||||||
|
|
||||||
|
FROM node:22-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /out/ ./
|
||||||
|
COPY --from=build /src/dist ./dist
|
||||||
|
ENV HUB_STATIC=/app/dist HUB_PORT=8080 NODE_ENV=production
|
||||||
|
USER node
|
||||||
|
EXPOSE 8080
|
||||||
|
# No curl in alpine: node asks the hub's public endpoint itself.
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD ["node", "-e", "fetch('http://127.0.0.1:'+(process.env.HUB_PORT||8080)+'/api/status').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
|
||||||
|
CMD ["node", "--experimental-strip-types", "--no-warnings", "/app/hub/server.ts"]
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
# Хаб: одна консоль со статистикой нескольких серверов Technitium (см. hub/README.md).
|
||||||
|
# Подключается к уже работающему Traefik из compose.shnt.example.yaml (сеть dns-net).
|
||||||
|
#
|
||||||
|
# На КАЖДОМ сервере Technitium, один раз:
|
||||||
|
# 1. Administration > Groups: группа hub-readers, ей право только View на Dashboard, Logs,
|
||||||
|
# Apps (список приложений логирования для Query Logs) и Settings (состояние блокировки).
|
||||||
|
# 2. Administration > Users: пользователь hub-reader в этой группе.
|
||||||
|
# 3. Войти под ним и создать токен: Administration > Sessions > Create API Token.
|
||||||
|
# Токены кладутся в .env рядом с этим файлом (не в git):
|
||||||
|
# DNS_TOKEN=...
|
||||||
|
# NDNS_TOKEN=...
|
||||||
|
# HUB_USER=... # логин в сам хаб
|
||||||
|
# HUB_PASSWORD=... # длинный пароль: у хаба нет ограничения числа попыток входа
|
||||||
|
#
|
||||||
|
# Обновление: docker compose pull technitium-hub && docker compose up -d technitium-hub
|
||||||
|
|
||||||
|
services:
|
||||||
|
technitium-hub:
|
||||||
|
image: git.shx.one/denozord/technitium-hub:latest
|
||||||
|
container_name: technitium-hub
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dns-net
|
||||||
|
environment:
|
||||||
|
# id=адрес, по порядку; токен каждого берётся из <ID>_TOKEN
|
||||||
|
- HUB_SERVERS=dns=https://dns.shnt.top,ndns=https://ndns.shnt.top
|
||||||
|
- DNS_TOKEN=${DNS_TOKEN}
|
||||||
|
- NDNS_TOKEN=${NDNS_TOKEN}
|
||||||
|
- HUB_USER=${HUB_USER}
|
||||||
|
- HUB_PASSWORD=${HUB_PASSWORD}
|
||||||
|
- HUB_DOMAIN=hub.shnt.top
|
||||||
|
- TZ=Asia/Novosibirsk
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=dns-net"
|
||||||
|
|
||||||
|
- "traefik.http.routers.hub.rule=Host(`hub.shnt.top`)"
|
||||||
|
- "traefik.http.routers.hub.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.hub.tls.certresolver=myresolver"
|
||||||
|
- "traefik.http.routers.hub.service=hub-svc"
|
||||||
|
- "traefik.http.services.hub-svc.loadbalancer.server.port=8080"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dns-net:
|
||||||
|
external: true
|
||||||
|
name: dns-net
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Technitium hub
|
||||||
|
|
||||||
|
A small, dependency-free Node program that looks like ONE Technitium DNS Server to the
|
||||||
|
console and answers from several real ones. It serves the console's static build and a
|
||||||
|
read-only, Technitium-compatible `/api/*`:
|
||||||
|
|
||||||
|
- `node=<id>` forwards the call to that server, with that server's token.
|
||||||
|
- No `node` (or `node=cluster`) asks every server in parallel and merges the answers
|
||||||
|
(statistics are summed, top lists and query logs are merged). If a server does not
|
||||||
|
answer, the others are shown and the response carries `hubWarnings`.
|
||||||
|
- Anything that would change data is refused with `403`. The hub has its own sign-in.
|
||||||
|
|
||||||
|
The servers themselves are not changed: each only needs a read-only API token. Create it
|
||||||
|
for a user whose group has View, and nothing else, on Dashboard, Logs, Apps (the logging
|
||||||
|
apps Query Logs reads from) and Settings (the blocking state).
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Variable | Default | Meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| `HUB_SERVERS` | required | Ordered `id=url` list, e.g. `dns=https://dns.example.net,ndns=https://ndns.example.net`. The first server is shown as the Primary node. |
|
||||||
|
| `<ID>_TOKEN` | required | API token of each server; the id is uppercased with non-alphanumerics as `_` (`dns` gives `DNS_TOKEN`, `my-dns` gives `MY_DNS_TOKEN`). |
|
||||||
|
| `HUB_USER`, `HUB_PASSWORD` | required | The hub's own sign-in. It refuses to start without them. |
|
||||||
|
| `HUB_PORT` | `8080` | Listening port. |
|
||||||
|
| `HUB_CACHE_SECONDS` | `15` | How long successful answers of the servers are cached. |
|
||||||
|
| `HUB_TIMEOUT_MS` | `8000` | Per-request timeout towards a server. |
|
||||||
|
| `HUB_DOMAIN` | `hub` | Shown as the server's domain in the console. |
|
||||||
|
| `HUB_STATIC` | `./dist` | The console's build to serve. |
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
Public: `status`, `user/login`. After signing in: `user/session/get`, `user/logout`,
|
||||||
|
`user/checkForUpdate` (always "no update") and these reads:
|
||||||
|
|
||||||
|
`dashboard/stats/get`, `dashboard/stats/getTop`, `dashboard/metrics/json`, `apps/list`,
|
||||||
|
`logs/query`, `logs/list`, `settings/get`, `admin/cluster/state` (synthesized).
|
||||||
|
|
||||||
|
Everything else answers `403 The hub is read-only.`
|
||||||
|
|
||||||
|
## Run locally
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
node dev/mock-api.mjs --port 5380 &
|
||||||
|
node dev/mock-api.mjs --port 5381 --scale 2 &
|
||||||
|
HUB_SERVERS=a=http://127.0.0.1:5380,b=http://127.0.0.1:5381 A_TOKEN=x B_TOKEN=y \
|
||||||
|
HUB_USER=admin HUB_PASSWORD=admin HUB_PORT=5390 npm run hub
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open http://localhost:5390. Tests: `npx vitest run hub`.
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker run --rm -p 8080:8080 \
|
||||||
|
-e HUB_SERVERS=dns=https://dns.example.net,ndns=https://ndns.example.net \
|
||||||
|
-e DNS_TOKEN=... -e NDNS_TOKEN=... -e HUB_USER=admin -e HUB_PASSWORD=... \
|
||||||
|
git.shx.one/denozord/technitium-hub:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
The image is built by `docker/Dockerfile.hub` and published by `.gitea/workflows/hub.yaml`
|
||||||
|
(`latest` from `main`, `sha-<7>`, and `X.Y.Z` / `X.Y` from a `hub-vX.Y.Z` tag). A compose
|
||||||
|
example with Traefik is in `docs/compose.hub.example.yaml`.
|
||||||
Reference in new issue
Block a user