From b2dbb4ad98974122874e2692d0431cdaf48e582c Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 19 Aug 2026 12:39:33 +0700 Subject: [PATCH] feat(health-checks): enhance health check configuration and UI components - Introduced a new HealthProviderToggle component to manage health check providers (local/cloudflare) in the UI. - Updated health check configuration to include additional parameters such as retries and consecutive success/failure counts. - Improved the service edit and group edit sheets to support the new health check provider options. - Enhanced documentation to clarify the use of ACTIONS_PAT and GITEA_TOKEN for wiki updates. This commit improves the health check management experience and expands the configuration options for better service monitoring. --- .gitea/README.md | 4 +- .gitea/workflows/cd.yaml | 25 ++-- .../components/health-check-config-fields.tsx | 138 ++++++++++++++++-- .../web/src/components/service-edit-sheet.tsx | 5 + .../components/service-group-edit-sheet.tsx | 2 + .../_auth/services/$serviceId/health.tsx | 25 +--- docs/releasing.md | 2 +- 7 files changed, 154 insertions(+), 47 deletions(-) diff --git a/.gitea/README.md b/.gitea/README.md index 77fcf3a..30c0fb0 100644 --- a/.gitea/README.md +++ b/.gitea/README.md @@ -35,13 +35,13 @@ Runner: `ubuntu-latest`, Docker для **docker-check** (PR) и **publish** (CD) Повтор упавшего **publish** (тег уже есть, bake нет): detect берёт `v*` на `HEAD` и всё равно пушит образы. Подробнее: [docs/releasing.md](../docs/releasing.md#перезапуск-упавшего-job-publish). -Job **update-wiki** идёт **параллельно** publish (не блокирует образы): при diff `docs/Home.md` копирует файл в wiki-репозиторий. Clone/push идут с `Authorization: Basic oauth2:` — после clone git вырезает токен из `origin`, без header Gitea отвечает `Repository not found` (часто на внутреннем `GITEA_INSTANCE_URL` раннера). Секрет: **`GITEA_TOKEN`**, fallback **`ACTIONS_PAT`**. +Job **update-wiki** идёт **параллельно** publish (не блокирует образы): при diff `docs/Home.md` копирует файл в wiki-репозиторий. Clone/push идут на публичный **`https://git.shx.one`** (не внутренний `gitea.server_url` / `192.168.x.x:3000`): Gitea `ROOT_URL` совпадает с Host, иначе `git-receive-pack` wiki отвечает `Repository not found`. Токен в URL `https://oauth2:@…/*.wiki.git` — Gitea на неаутентифицированный wiki push даёт **404, не 401**, поэтому `http.extraHeader` / ASKPASS не срабатывают. Секрет: **`ACTIONS_PAT`**, fallback **`GITEA_TOKEN`**. ### Секреты **`ACTIONS_PAT`**: push tags, releases, Container Registry. Для git tag fallback: `gitea.token`. Push OCI — **только PAT** (у job token Gitea нет права packages). -**`GITEA_TOKEN`**: clone/push wiki. +**`GITEA_TOKEN`**: опциональный wiki-only PAT (fallback, если нет `ACTIONS_PAT`). ### Теги образов diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index a9a7331..8a81566 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -40,21 +40,26 @@ jobs: - name: Update and push Wiki content if: steps.check_changes.outputs.changed == 'true' env: - WIKI_TOKEN: ${{ secrets.GITEA_TOKEN || secrets.ACTIONS_PAT }} - SERVER_URL: ${{ gitea.server_url }} + # ACTIONS_PAT уже пишет git (tags/releases). GITEA_TOKEN — опциональный + # wiki-only PAT; если он задан без write, Gitea отвечает 404, не 403. + WIKI_TOKEN: ${{ secrets.ACTIONS_PAT || secrets.GITEA_TOKEN }} + # Не gitea.server_url: на runner это внутренний http://192.168.x.x:3000, + # а ROOT_URL = git.shx.one — git-receive-pack wiki тогда даёт 404. + GITEA_PUBLIC_URL: https://git.shx.one REPO: ${{ gitea.repository }} run: | set -euo pipefail if [ -z "${WIKI_TOKEN:-}" ]; then - echo "GITEA_TOKEN / ACTIONS_PAT is empty — cannot push wiki" + echo "ACTIONS_PAT / GITEA_TOKEN is empty — cannot push wiki" exit 1 fi - # runner GITEA_INSTANCE_URL часто внутренний (http://192.168.x.x:3000). - # git после clone вырезает userinfo из origin → push без токена даёт 404 - # «Repository not found». Authorization header переживает insteadOf/sanitize. - WIKI_URL="${SERVER_URL}/${REPO}.wiki.git" - AUTH_HEADER="Authorization: Basic $(printf '%s' "oauth2:${WIKI_TOKEN}" | base64 | tr -d '\n')" - git -c http.extraHeader="${AUTH_HEADER}" clone "${WIKI_URL}" cfdm.wiki + PUBLIC_URL="${GITEA_PUBLIC_URL%/}" + TOKEN_ENC="$(python3 -c 'import urllib.parse,os; print(urllib.parse.quote(os.environ["WIKI_TOKEN"], safe=""))')" + WIKI_URL="${PUBLIC_URL}/${REPO}.wiki.git" + # Gitea на неаутентифицированный wiki push отвечает 404, не 401 — + # extraHeader/ASKPASS не помогают: токен должен быть в URL с первого запроса. + AUTH_INSTEAD="url.https://oauth2:${TOKEN_ENC}@${PUBLIC_URL#https://}/.insteadOf=${PUBLIC_URL}/" + GIT_TERMINAL_PROMPT=0 git -c "${AUTH_INSTEAD}" clone "${WIKI_URL}" cfdm.wiki cp docs/Home.md cfdm.wiki/Home.md cd cfdm.wiki git config user.name "Gitea Actions" @@ -65,7 +70,7 @@ jobs: exit 0 fi git commit -m "docs: Update Wiki from main repository" - git -c http.extraHeader="${AUTH_HEADER}" push origin HEAD + GIT_TERMINAL_PROMPT=0 git -c "${AUTH_INSTEAD}" push origin HEAD publish: needs: [quality] diff --git a/apps/web/src/components/health-check-config-fields.tsx b/apps/web/src/components/health-check-config-fields.tsx index f9696b4..4a0b86e 100644 --- a/apps/web/src/components/health-check-config-fields.tsx +++ b/apps/web/src/components/health-check-config-fields.tsx @@ -17,11 +17,15 @@ import { SelectValue, } from '@cfdm/ui/components/select' import { Switch } from '@cfdm/ui/components/switch' +import { Button } from '@cfdm/ui/components/button' +import { ButtonGroup } from '@cfdm/ui/components/button-group' import { FieldGroup } from '@cfdm/ui/components/field' +import { Alert, AlertDescription, AlertTitle } from '@/components/reui/alert' import { cn } from '@cfdm/ui/lib/utils' export type LbMode = 'round_robin' | 'failover' | 'weighted' export type HealthCheckType = 'tcp' | 'http' +export type HealthProvider = 'local' | 'cloudflare' export interface HealthCheckConfig { enabled: boolean @@ -32,7 +36,11 @@ export interface HealthCheckConfig { interval_sec: number timeout_ms: number verify_tls: boolean - provider?: 'local' | 'cloudflare' + provider: HealthProvider + method?: string | null + retries?: number + consecutive_fails?: number + consecutive_successes?: number } export interface LbAndHealthConfig extends HealthCheckConfig { @@ -50,6 +58,47 @@ const healthCheckTypes = [ { value: 'http', label: 'HTTP' }, ] as const +const cloudflareTypes = [ + { value: 'tcp', label: 'TCP' }, + { value: 'http', label: 'HTTP' }, +] as const + +export function HealthProviderToggle({ + value, + onChange, + id, +}: { + value: HealthProvider + onChange: (next: HealthProvider) => void + id?: string +}) { + const provider = value || 'local' + return ( + + + + + ) +} + interface HealthCheckConfigFieldsProps { value: LbAndHealthConfig onChange: (next: LbAndHealthConfig) => void @@ -120,6 +169,7 @@ export function HealthCheckConfigFields({ className={rowClass} > - patch({ provider: (v ?? 'local') as 'local' | 'cloudflare' }) + onChange={(provider) => + patch({ + provider, + enabled: provider === 'cloudflare' ? true : value.enabled, + }) } - > - - - - - Local - Cloudflare - - + /> + {value.provider === 'cloudflare' ? ( + + Cloudflare Health Checks + + Поля соответствуют официальному API зоны. Если план не позволяет Health + Checks, API вернёт ошибку — останется Local. Workers не используются. + + + ) : null} patch({ method: v ?? 'GET' })} + > + + + + + GET + HEAD + + + + ) : null} ) : null} diff --git a/apps/web/src/components/service-edit-sheet.tsx b/apps/web/src/components/service-edit-sheet.tsx index 71f45ad..928174f 100644 --- a/apps/web/src/components/service-edit-sheet.tsx +++ b/apps/web/src/components/service-edit-sheet.tsx @@ -57,6 +57,7 @@ interface BindingHealthConfig { interval_sec: number timeout_ms: number verify_tls: boolean + provider: 'local' | 'cloudflare' } export interface ServiceBindingDraft { @@ -79,6 +80,7 @@ const defaultHealth: BindingHealthConfig = { interval_sec: 30, timeout_ms: 3000, verify_tls: false, + provider: 'local', } interface ServiceEditSheetProps { @@ -112,6 +114,7 @@ function toBindingDrafts(service: ServiceView): ServiceBindingDraft[] { interval_sec: binding.health_check_interval_sec, timeout_ms: binding.health_check_timeout_ms, verify_tls: binding.health_check_verify_tls ?? false, + provider: 'local', }, target_ip_weights: binding.target_ip_weights ?? {}, target_ip_priorities: binding.target_ip_priorities ?? {}, @@ -328,6 +331,7 @@ export function ServiceEditSheet({ interval_sec: next.interval_sec, timeout_ms: next.timeout_ms, verify_tls: next.verify_tls, + provider: next.provider ?? 'local', }, } : item, @@ -650,6 +654,7 @@ export function ServiceEditSheet({ interval_sec: binding.health.interval_sec, timeout_ms: binding.health.timeout_ms, verify_tls: binding.health.verify_tls, + provider: binding.health.provider ?? 'local', }} onChange={(next) => handleBindingHealthChange(index, next)} lbModeLabel="Режим балансировки" diff --git a/apps/web/src/components/service-group-edit-sheet.tsx b/apps/web/src/components/service-group-edit-sheet.tsx index facfd65..09222b0 100644 --- a/apps/web/src/components/service-group-edit-sheet.tsx +++ b/apps/web/src/components/service-group-edit-sheet.tsx @@ -54,6 +54,7 @@ const defaultLbHealth: LbAndHealthConfig = { interval_sec: 30, timeout_ms: 3000, verify_tls: false, + provider: 'local', } export function ServiceGroupEditSheet({ @@ -93,6 +94,7 @@ export function ServiceGroupEditSheet({ interval_sec: group.health_check_interval_sec, timeout_ms: group.health_check_timeout_ms, verify_tls: group.health_check_verify_tls, + provider: 'local', }) } else { form.reset({ name: '', type: 'custom', domain: null }) diff --git a/apps/web/src/routes/_auth/services/$serviceId/health.tsx b/apps/web/src/routes/_auth/services/$serviceId/health.tsx index 8d3517a..5cb7fdf 100644 --- a/apps/web/src/routes/_auth/services/$serviceId/health.tsx +++ b/apps/web/src/routes/_auth/services/$serviceId/health.tsx @@ -10,14 +10,8 @@ import { FormFieldSimple } from '@/components/form-field' import { LoadingButton } from '@/components/loading-button' import { Button } from '@cfdm/ui/components/button' import { Input } from '@cfdm/ui/components/input' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@cfdm/ui/components/select' import { Alert, AlertDescription, AlertTitle } from '@/components/reui/alert' +import { HealthProviderToggle } from '@/components/health-check-config-fields' import { createOriginHealthCheck, listOriginHealthChecks, @@ -125,20 +119,11 @@ export function ServiceHealthPage() { - + onChange={(next) => form.setValue('provider', next)} + /> {provider === 'cloudflare' ? ( diff --git a/docs/releasing.md b/docs/releasing.md index 1ee6ee8..503efde 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -51,7 +51,7 @@ Workflows: [.gitea/workflows/ci.yaml](../.gitea/workflows/ci.yaml), [.gitea/work Fallback для **git tag**: `gitea.token`, если PAT недоступен. Push образов в Container Registry — **только `ACTIONS_PAT`** (у job token Gitea нет права packages). -Wiki: секрет **`GITEA_TOKEN`** (fallback `ACTIONS_PAT`) для clone/push `*.wiki.git`. Push идёт с HTTP `Authorization`, потому что git после clone вырезает токен из remote URL. +Wiki: секрет **`ACTIONS_PAT`** (fallback `GITEA_TOKEN`) для clone/push `*.wiki.git` на `https://git.shx.one` (не внутренний `GITEA_INSTANCE_URL` раннера). Токен передаётся в URL (`oauth2:`): Gitea на неаутентифицированный wiki push отвечает 404, а не 401. ## Источник правды для версии