Refactor Docker setup to integrate Web UI and streamline configuration
- Updated Dockerfile to build and embed the SvelteKit Web UI directly into the gateway image, eliminating the need for a separate web service. - Modified .dockerignore to exclude unnecessary directories related to the web service. - Adjusted config.compose.yaml to remove CORS settings for the web service, as the UI now shares the same origin as the API. - Enhanced README.md to reflect the new single-port architecture for accessing both the Web UI and API. - Removed the standalone web Dockerfile and updated related documentation for local development and build processes.
This commit is contained in:
@@ -7,3 +7,6 @@ docs
|
|||||||
.gitea
|
.gitea
|
||||||
docker-compose*.yml
|
docker-compose*.yml
|
||||||
deploy
|
deploy
|
||||||
|
web/node_modules
|
||||||
|
web/.svelte-kit
|
||||||
|
web/build
|
||||||
|
|||||||
+16
-5
@@ -1,24 +1,35 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
# --- Web UI (SvelteKit static) — тот же origin, что и API (порт шлюза)
|
||||||
|
FROM node:22-bookworm AS webui
|
||||||
|
WORKDIR /web
|
||||||
|
COPY web/package.json web/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY web/ ./
|
||||||
|
# Пустой URL: fetch идёт на тот же хост/порт, что и панель
|
||||||
|
ENV PUBLIC_TELEMT_GATEWAY_URL=
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
FROM golang:1.22-bookworm AS build
|
FROM golang:1.22-bookworm AS build
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY go.mod ./
|
COPY go.mod ./
|
||||||
COPY cmd/ ./cmd/
|
COPY cmd/ ./cmd/
|
||||||
COPY internal/ ./internal/
|
COPY internal/ ./internal/
|
||||||
|
COPY --from=webui /web/build/ ./internal/webui/static/
|
||||||
RUN go mod tidy && go mod download
|
RUN go mod tidy && go mod download
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/gateway ./cmd/gateway
|
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/gateway ./cmd/gateway
|
||||||
|
|
||||||
# Alpine: non-root + wget for HEALTHCHECK (distroless has no shell/wget).
|
# Alpine: non-root + wget for HEALTHCHECK (distroless has no shell/wget).
|
||||||
FROM alpine:3.19
|
FROM alpine:3.19
|
||||||
RUN apk add --no-cache ca-certificates wget \
|
RUN apk add --no-cache ca-certificates wget \
|
||||||
&& addgroup -S gateway -g 65532 \
|
&& addgroup -S gateway -g 65532 \
|
||||||
&& adduser -S -u 65532 -G gateway gateway \
|
&& adduser -S -u 65532 -G gateway gateway \
|
||||||
&& mkdir -p /var/lib/telemt-gateway \
|
&& mkdir -p /var/lib/telemt-gateway \
|
||||||
&& chown gateway:gateway /var/lib/telemt-gateway
|
&& chown gateway:gateway /var/lib/telemt-gateway
|
||||||
COPY --from=build /out/gateway /gateway
|
COPY --from=build /out/gateway /gateway
|
||||||
USER gateway:gateway
|
USER gateway:gateway
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
ENV CONFIG_PATH=/etc/telemt-gateway/config.yaml
|
ENV CONFIG_PATH=/etc/telemt-gateway/config.yaml
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
CMD wget -q -O- http://127.0.0.1:8080/health >/dev/null || exit 1
|
CMD wget -q -O- http://127.0.0.1:8080/health >/dev/null || exit 1
|
||||||
ENTRYPOINT ["/gateway"]
|
ENTRYPOINT ["/gateway"]
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# telemt-api
|
# telemt-api
|
||||||
|
|
||||||
HTTP‑шлюз на Go для [Telemt Control API](docs/API.md): один порт, **белый список IP (CIDR)**, маршруты вида `/api/{alias}/…` → `{base_url}/v1/…`, агрегация нескольких инстансов — [`/api/agg/…`](docs/AGGREGATE.md), метрики Prometheus на `/metrics`.
|
HTTP‑шлюз на Go для [Telemt Control API](docs/API.md): один порт, **белый список IP (CIDR)**, маршруты вида `/api/{alias}/…` → `{base_url}/v1/…`, агрегация нескольких инстансов — [`/api/agg/…`](docs/AGGREGATE.md), метрики Prometheus на `/metrics`. **Web UI** (SvelteKit) встроен в тот же процесс/образ: статика на `/`, API на `/api/…` и `/health`.
|
||||||
|
|
||||||
## Быстрый старт (Linux)
|
## Быстрый старт (Linux)
|
||||||
|
|
||||||
Предполагается установлены Docker и Docker Compose v2.
|
Предполагается установлены Docker и Docker Compose v2.
|
||||||
|
|
||||||
**Рекомендуется** брать уже собранный образ из Container Registry Gitea (после каждого push в репозиторий CI обновляет теги, в том числе `latest`):
|
**Рекомендуется** брать уже собранный образ из Container Registry Gitea (после каждого push CI обновляет теги, в том числе `latest`). В актуальном образе вместе с шлюзом уже **встроена панель** на `http://<хост>:8080/`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# при необходимости (закрытый registry): логин Gitea + PAT с read:package
|
# при необходимости (закрытый registry): логин Gitea + PAT с read:package
|
||||||
@@ -30,25 +30,25 @@ docker run -d --name telemt-gateway \
|
|||||||
|
|
||||||
curl -sS http://127.0.0.1:8080/health
|
curl -sS http://127.0.0.1:8080/health
|
||||||
curl -sS http://127.0.0.1:8080/api/main_srv/health
|
curl -sS http://127.0.0.1:8080/api/main_srv/health
|
||||||
|
# панель в браузере: http://127.0.0.1:8080/
|
||||||
```
|
```
|
||||||
|
|
||||||
Обновление образа: `docker pull git.shts.su/denozord/telemt-api:latest` и пересоздайте контейнер (`docker rm -f telemt-gateway` и снова `docker run …`).
|
Обновление образа: `docker pull git.shts.su/denozord/telemt-api:latest` и пересоздайте контейнер (`docker rm -f telemt-gateway` и снова `docker run …`).
|
||||||
|
|
||||||
### Compose
|
### Compose
|
||||||
|
|
||||||
Тот же образ шлюза подтягивается из registry (без локальной сборки). Сервис **web** (панель в браузере) собирается из каталога `web/` при первом запуске:
|
Один сервис **gateway** — в образе уже есть и API, и статика панели (см. [Dockerfile](Dockerfile)).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone <url-репозитория> && cd telemt-api
|
git clone <url-репозитория> && cd telemt-api
|
||||||
docker compose pull
|
docker compose pull
|
||||||
docker compose up -d --build
|
docker compose up -d
|
||||||
docker compose logs -f gateway
|
docker compose logs -f gateway
|
||||||
```
|
```
|
||||||
|
|
||||||
- Шлюз: `http://127.0.0.1:8080` (проверка: `curl -sS http://127.0.0.1:8080/health`).
|
- Всё на **одном порту**: `http://127.0.0.1:8080/` — Web UI, `http://127.0.0.1:8080/api/…` — шлюз, `http://127.0.0.1:8080/health` — проверка живости.
|
||||||
- Web UI: `http://127.0.0.1:4173` — в [config.compose.yaml](config.compose.yaml) заданы `cors_allowed_origins` под этот origin; `PUBLIC_TELEMT_GATEWAY_URL` при сборке образа UI указывает на шлюз на хосте (`http://127.0.0.1:8080`).
|
|
||||||
|
|
||||||
Подробнее по панели: [web/README.md](web/README.md).
|
Подробнее по фронту и dev-режиму: [web/README.md](web/README.md). Полный сценарий Docker (CLI, compose, сборка, CI): [docs/GATEWAY_RUN.md](docs/GATEWAY_RUN.md).
|
||||||
|
|
||||||
### Web UI (локально без Docker)
|
### Web UI (локально без Docker)
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ cors_allowed_origins:
|
|||||||
|
|
||||||
### Локальная сборка образа
|
### Локальная сборка образа
|
||||||
|
|
||||||
Если нужен образ из исходников на этой машине: `docker build -t telemt-api-gateway:local .` и в `docker run` укажите тег `telemt-api-gateway:local`. Подробнее — [docs/GATEWAY_RUN.md](docs/GATEWAY_RUN.md).
|
`docker build -t telemt-api-gateway:local .` — в образ попадают Web UI и бинарь шлюза (см. [Dockerfile](Dockerfile)). В `docker run` укажите этот тег. Подробнее — [docs/GATEWAY_RUN.md](docs/GATEWAY_RUN.md).
|
||||||
|
|
||||||
## Документация
|
## Документация
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ cors_allowed_origins:
|
|||||||
| **[docs/API.md](docs/API.md)** | Контракт Telemt Control API (`/v1/…`) |
|
| **[docs/API.md](docs/API.md)** | Контракт Telemt Control API (`/v1/…`) |
|
||||||
| **[docs/AGGREGATE.md](docs/AGGREGATE.md)** | Агрегирующие эндпоинты шлюза (`/api/agg/…`), CORS, кэш |
|
| **[docs/AGGREGATE.md](docs/AGGREGATE.md)** | Агрегирующие эндпоинты шлюза (`/api/agg/…`), CORS, кэш |
|
||||||
| **[docs/AGGREGATE_OPENAPI.yaml](docs/AGGREGATE_OPENAPI.yaml)** | OpenAPI 3 черновик для `/api/agg/*` (генерация типов для UI) |
|
| **[docs/AGGREGATE_OPENAPI.yaml](docs/AGGREGATE_OPENAPI.yaml)** | OpenAPI 3 черновик для `/api/agg/*` (генерация типов для UI) |
|
||||||
| **[web/README.md](web/README.md)** | Web UI (SvelteKit + shadcn-svelte): установка, `PUBLIC_TELEMT_GATEWAY_URL`, Docker |
|
| **[web/README.md](web/README.md)** | Web UI (SvelteKit): разработка с Vite, `PUBLIC_TELEMT_GATEWAY_URL`, встраивание в образ шлюза |
|
||||||
| **[docs/GEOIP.md](docs/GEOIP.md)** | GeoLite2 City (страна/город) и опционально ASN (номер AS, организация) для IP в `unique-ips` |
|
| **[docs/GEOIP.md](docs/GEOIP.md)** | GeoLite2 City (страна/город) и опционально ASN (номер AS, организация) для IP в `unique-ips` |
|
||||||
|
|
||||||
## Сборка и тесты без Docker
|
## Сборка и тесты без Docker
|
||||||
|
|||||||
+1
-4
@@ -4,10 +4,7 @@ listen: ":8080"
|
|||||||
allow_all: true
|
allow_all: true
|
||||||
whitelist_cidrs: []
|
whitelist_cidrs: []
|
||||||
trusted_proxies: []
|
trusted_proxies: []
|
||||||
# UI в Docker (сервис web): браузер на другом origin — нужен CORS
|
# UI отдаётся с того же порта, что и API (образ шлюза) — отдельный CORS для панели не нужен.
|
||||||
cors_allowed_origins:
|
|
||||||
- "http://127.0.0.1:4173"
|
|
||||||
- "http://localhost:4173"
|
|
||||||
servers:
|
servers:
|
||||||
- alias: main_srv
|
- alias: main_srv
|
||||||
base_url: http://host.docker.internal:9091
|
base_url: http://host.docker.internal:9091
|
||||||
|
|||||||
+5
-1
@@ -2,6 +2,9 @@
|
|||||||
#
|
#
|
||||||
# Semantics: client calls GET /api/{alias}/health
|
# Semantics: client calls GET /api/{alias}/health
|
||||||
# forwarded to GET {base_url}/v1/health
|
# forwarded to GET {base_url}/v1/health
|
||||||
|
#
|
||||||
|
# Web UI (SvelteKit) в образе Docker встроен в тот же процесс: GET / — панель,
|
||||||
|
# /api/… — API. CORS для панели на том же origin не нужен.
|
||||||
|
|
||||||
listen: ":8080"
|
listen: ":8080"
|
||||||
|
|
||||||
@@ -23,7 +26,8 @@ whitelist_cidrs:
|
|||||||
# - "10.0.0.0/8"
|
# - "10.0.0.0/8"
|
||||||
trusted_proxies: []
|
trusted_proxies: []
|
||||||
|
|
||||||
# SPA на другом origin (preflight OPTIONS + Access-Control-Allow-Origin):
|
# CORS только если фронт на другом origin (например Vite :5173 при разработке).
|
||||||
|
# В production за nginx на одном хосте с шлюзом обычно не требуется.
|
||||||
# cors_allowed_origins:
|
# cors_allowed_origins:
|
||||||
# - "http://localhost:5173"
|
# - "http://localhost:5173"
|
||||||
# # - "*"
|
# # - "*"
|
||||||
|
|||||||
+1
-10
@@ -1,16 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
web:
|
|
||||||
build:
|
|
||||||
context: ./web
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
args:
|
|
||||||
# URL шлюза в браузере (встраивается при сборке)
|
|
||||||
PUBLIC_TELEMT_GATEWAY_URL: http://127.0.0.1:8080
|
|
||||||
ports:
|
|
||||||
- "4173:4173"
|
|
||||||
gateway:
|
gateway:
|
||||||
image: git.shts.su/denozord/telemt-api:latest
|
image: git.shts.su/denozord/telemt-api:latest
|
||||||
# Локальная сборка вместо pull: укажите build: . и image: telemt-api-gateway:local
|
# Локальная сборка с Web UI внутри образа: build: . и image: telemt-api-gateway:local
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
+12
-6
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
Шлюз — это один HTTP‑вход для нескольких экземпляров [Telemt Control API](API.md):
|
Шлюз — это один HTTP‑вход для нескольких экземпляров [Telemt Control API](API.md):
|
||||||
|
|
||||||
|
- **Web UI** (в образе Docker): статика панели на **`GET /`** (и клиентские маршруты SPA), агрегаты и прокси на **`/api/…`**. Тот же порт, что и у API (например `8080`). Исходники UI — каталог [web/](../web/README.md), сборка встроена в [Dockerfile](../Dockerfile) (стадия Node + `embed` в Go).
|
||||||
- **Белый список IP** (CIDR): кто может обращаться к шлюзу (кроме `GET /health`, см. ниже).
|
- **Белый список IP** (CIDR): кто может обращаться к шлюзу (кроме `GET /health`, см. ниже).
|
||||||
- **Маршрутизация по alias**: клиент вызывает `GET /api/{alias}/health`, шлюз проксирует на `{base_url}/v1/health` у соответствующего сервера.
|
- **Маршрутизация по alias**: клиент вызывает `GET /api/{alias}/health`, шлюз проксирует на `{base_url}/v1/health` у соответствующего сервера.
|
||||||
- **Метрики Prometheus**: `GET /metrics` (под тем же правилом whitelist, что и API).
|
- **Метрики Prometheus**: `GET /metrics` (под тем же правилом whitelist, что и API).
|
||||||
@@ -28,7 +29,8 @@
|
|||||||
## Требования
|
## Требования
|
||||||
|
|
||||||
- Установленные [Docker](https://docs.docker.com/get-docker/) и при необходимости [Docker Compose](https://docs.docker.com/compose/) v2.
|
- Установленные [Docker](https://docs.docker.com/get-docker/) и при необходимости [Docker Compose](https://docs.docker.com/compose/) v2.
|
||||||
- Для локальной сборки из исходников: [Go 1.22+](https://go.dev/dl/) (опционально, если не используете только готовый образ из registry).
|
- Для локальной сборки **Docker-образа** из репозитория: Docker сам подтянет [Node](https://nodejs.org/) на стадии сборки фронта и [Go 1.22+](https://go.dev/dl/) на стадии компиляции (см. [Dockerfile](../Dockerfile)).
|
||||||
|
- Для `go test ./...` без Docker на машине нужен только Go 1.22+.
|
||||||
|
|
||||||
## Минимальная конфигурация
|
## Минимальная конфигурация
|
||||||
|
|
||||||
@@ -106,7 +108,7 @@ docker login git.shts.su
|
|||||||
docker build -t telemt-api-gateway:local .
|
docker build -t telemt-api-gateway:local .
|
||||||
```
|
```
|
||||||
|
|
||||||
В командах `docker run` ниже вместо имени из registry подставьте `telemt-api-gateway:local`.
|
Сборка **многостадийная**: сначала `npm ci` + `npm run build` в каталоге `web/` (в бандл вшивается пустой `PUBLIC_TELEMT_GATEWAY_URL`, запросы API с того же origin), затем компиляция Go со встраиванием `web/build` через `embed`. В командах `docker run` ниже вместо имени из registry подставьте `telemt-api-gateway:local`.
|
||||||
|
|
||||||
## Запуск через Docker CLI
|
## Запуск через Docker CLI
|
||||||
|
|
||||||
@@ -125,9 +127,12 @@ docker run -d --name telemt-gateway \
|
|||||||
```bash
|
```bash
|
||||||
curl -sS -i http://127.0.0.1:8080/health
|
curl -sS -i http://127.0.0.1:8080/health
|
||||||
curl -sS -i http://127.0.0.1:8080/api/main_srv/health
|
curl -sS -i http://127.0.0.1:8080/api/main_srv/health
|
||||||
|
curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/
|
||||||
```
|
```
|
||||||
|
|
||||||
Второй запрос проксируется на `{base_url}/v1/health` для alias `main_srv`.
|
Второй запрос проксируется на `{base_url}/v1/health` для alias `main_srv`. Третий — HTML панели (в образах, собранных с Web UI; ожидайте `200`).
|
||||||
|
|
||||||
|
Панель в браузере: `http://127.0.0.1:8080/` (при `allow_all: false` ваш IP должен быть в `whitelist_cidrs`, иначе для `/` будет `403`, как и для API).
|
||||||
|
|
||||||
Остановка и удаление:
|
Остановка и удаление:
|
||||||
|
|
||||||
@@ -138,7 +143,7 @@ docker rm telemt-gateway
|
|||||||
|
|
||||||
## Запуск через Docker Compose
|
## Запуск через Docker Compose
|
||||||
|
|
||||||
В репозитории есть [docker-compose.yml](../docker-compose.yml) и пример [config.compose.yaml](../config.compose.yaml) с `allow_all: true` и `base_url: http://host.docker.internal:9091` (Telemt на хосте; на Docker Desktop для Linux это обычно работает из коробки). Образ по умолчанию **скачивается** из registry, локальная сборка не требуется.
|
В репозитории есть [docker-compose.yml](../docker-compose.yml) (один сервис **gateway**) и пример [config.compose.yaml](../config.compose.yaml) с `allow_all: true` и `base_url: http://host.docker.internal:9091` (Telemt на хосте; на Docker Desktop для Linux это обычно работает из коробки). Образ по умолчанию **скачивается** из registry, локальная сборка не требуется. UI доступен на том же порту, что и шлюз: `http://127.0.0.1:8080/`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose pull
|
docker compose pull
|
||||||
@@ -159,6 +164,7 @@ docker compose down
|
|||||||
| Сценарий | Ожидание |
|
| Сценарий | Ожидание |
|
||||||
|----------|----------|
|
|----------|----------|
|
||||||
| `GET /health` | `200`, JSON `{"status":"ok"}` |
|
| `GET /health` | `200`, JSON `{"status":"ok"}` |
|
||||||
|
| `GET /` (образ с Web UI) | `200`, HTML панели |
|
||||||
| Разрешённый IP, корректный alias | ответ бэкенда (например `200` для `/v1/health`) |
|
| Разрешённый IP, корректный alias | ответ бэкенда (например `200` для `/v1/health`) |
|
||||||
| IP не в whitelist | `403`, JSON с `code: forbidden` |
|
| IP не в whitelist | `403`, JSON с `code: forbidden` |
|
||||||
| Неизвестный alias | `404`, JSON с `code: not_found` |
|
| Неизвестный alias | `404`, JSON с `code: not_found` |
|
||||||
@@ -167,9 +173,9 @@ docker compose down
|
|||||||
|
|
||||||
## Обновление и CI/CD
|
## Обновление и CI/CD
|
||||||
|
|
||||||
- **Образ**: подтяните свежий тег (`docker pull git.shts.su/denozord/telemt-api:latest` или `docker compose pull`), пересоздайте контейнер (`docker compose up -d` или новый `docker run` с тем же volume конфига). Локальная пересборка нужна только если вы меняете Dockerfile/код и не пользуетесь CI.
|
- **Образ**: подтяните свежий тег (`docker pull git.shts.su/denozord/telemt-api:latest` или `docker compose pull`), пересоздайте контейнер (`docker compose up -d` или новый `docker run` с тем же volume конфига). Локальная пересборка нужна только если вы меняете Dockerfile/код и не пользуетесь CI. Образы, собранные **до** добавления стадии `web/` в Dockerfile, могут отдавать на `/` только заглушку — нужен образ из актуального CI или локальный `docker build`.
|
||||||
- **Конфиг**: отредактируйте файл на хосте и перезапустите контейнер (шлюз не перечитывает конфиг на лету).
|
- **Конфиг**: отредактируйте файл на хосте и перезапустите контейнер (шлюз не перечитывает конфиг на лету).
|
||||||
- **Gitea Actions**: workflow [.gitea/workflows/docker.yaml](../.gitea/workflows/docker.yaml) сначала выполняет `go mod tidy && go test ./...`, затем собирает образ через Buildx и пушит в Container Registry Gitea.
|
- **Gitea Actions**: workflow [.gitea/workflows/docker.yaml](../.gitea/workflows/docker.yaml) сначала выполняет `go mod tidy && go test ./...`, затем собирает образ через Buildx по [Dockerfile](../Dockerfile) (стадии Node для `web/` и Go) и пушит в Container Registry Gitea.
|
||||||
|
|
||||||
- В репозитории должен быть secret **`ACTIONS_PAT`** — personal access token пользователя с правом **`write:package`** (и при необходимости `read:package`), как для обычного `docker login` к registry.
|
- В репозитории должен быть secret **`ACTIONS_PAT`** — personal access token пользователя с правом **`write:package`** (и при необходимости `read:package`), как для обычного `docker login` к registry.
|
||||||
- Логин в registry: пользователь **`gitea.actor`** (кто запустил workflow), пароль — этот PAT.
|
- Логин в registry: пользователь **`gitea.actor`** (кто запустил workflow), пароль — этот PAT.
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/telemt/telemt-api/internal/config"
|
"github.com/telemt/telemt-api/internal/config"
|
||||||
"github.com/telemt/telemt-api/internal/geoip"
|
"github.com/telemt/telemt-api/internal/geoip"
|
||||||
"github.com/telemt/telemt-api/internal/proxy"
|
"github.com/telemt/telemt-api/internal/proxy"
|
||||||
|
"github.com/telemt/telemt-api/internal/webui"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Gateway serves health, metrics, and proxied API routes.
|
// Gateway serves health, metrics, and proxied API routes.
|
||||||
@@ -29,6 +30,7 @@ type Gateway struct {
|
|||||||
transport *http.Transport
|
transport *http.Transport
|
||||||
promHandler http.Handler
|
promHandler http.Handler
|
||||||
corsAllowed []string
|
corsAllowed []string
|
||||||
|
webUI http.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGateway builds handlers and reverse proxies from parsed config.
|
// NewGateway builds handlers and reverse proxies from parsed config.
|
||||||
@@ -75,6 +77,7 @@ func NewGateway(p *config.Parsed, log *slog.Logger, geo *geoip.Service) (*Gatewa
|
|||||||
}
|
}
|
||||||
g.corsAllowed = append([]string(nil), p.Config.CorsAllowedOrigins...)
|
g.corsAllowed = append([]string(nil), p.Config.CorsAllowedOrigins...)
|
||||||
g.agg = aggregate.NewHandler(p, &http.Client{Transport: t}, geo, aggCacheTTL)
|
g.agg = aggregate.NewHandler(p, &http.Client{Transport: t}, geo, aggCacheTTL)
|
||||||
|
g.webUI = webui.Handler()
|
||||||
return g, nil
|
return g, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +234,7 @@ func (g *Gateway) serve(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(r.URL.Path, prefix) {
|
if !strings.HasPrefix(r.URL.Path, prefix) {
|
||||||
http.NotFound(w, r)
|
g.webUI.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
trim := strings.TrimPrefix(r.URL.Path, prefix)
|
trim := strings.TrimPrefix(r.URL.Path, prefix)
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package webui
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed all:static
|
||||||
|
var static embed.FS
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package webui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler отдаёт статику SvelteKit (embed) и index.html для клиентских маршрутов SPA.
|
||||||
|
func Handler() http.Handler {
|
||||||
|
root, err := fs.Sub(static, "static")
|
||||||
|
if err != nil {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
name := strings.TrimPrefix(path.Clean(r.URL.Path), "/")
|
||||||
|
if name == "." || name == "" {
|
||||||
|
name = "index.html"
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := fs.ReadFile(root, name)
|
||||||
|
if err != nil {
|
||||||
|
if path.Ext(name) != "" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, err = fs.ReadFile(root, "index.html")
|
||||||
|
if err != nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name = "index.html"
|
||||||
|
}
|
||||||
|
|
||||||
|
ct := mime.TypeByExtension(path.Ext(name))
|
||||||
|
if ct == "" {
|
||||||
|
ct = "application/octet-stream"
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(ct, "text/") && !strings.Contains(ct, "charset") {
|
||||||
|
ct = ct + "; charset=utf-8"
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", ct)
|
||||||
|
if name == "index.html" {
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method == http.MethodHead {
|
||||||
|
w.Header().Set("Content-Length", strconv.Itoa(len(b)))
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write(b)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package webui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandler_index(t *testing.T) {
|
||||||
|
h := Handler()
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||||
|
if rr.Code != http.StatusOK {
|
||||||
|
t.Fatalf("GET /: %d", rr.Code)
|
||||||
|
}
|
||||||
|
if got := rr.Header().Get("Content-Type"); got == "" {
|
||||||
|
t.Fatal("missing Content-Type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_spaFallback(t *testing.T) {
|
||||||
|
h := Handler()
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/servers/foo", nil))
|
||||||
|
if rr.Code != http.StatusOK {
|
||||||
|
t.Fatalf("GET /servers/foo: %d", rr.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Telemt Panel</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>
|
||||||
|
Заглушка: UI не собран. Соберите фронт (<code>cd web && npm run build</code>) перед сборкой образа шлюза, либо
|
||||||
|
используйте Dockerfile в корне репозитория.
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+3
-2
@@ -1,2 +1,3 @@
|
|||||||
# Базовый URL шлюза telemt-api (без завершающего слэша)
|
# Пусто = запросы к API на том же origin, что и панель (образ Docker со встроенным UI).
|
||||||
PUBLIC_TELEMT_GATEWAY_URL=http://127.0.0.1:8080
|
# Для dev (Vite :5173, шлюз :8080): http://127.0.0.1:8080
|
||||||
|
PUBLIC_TELEMT_GATEWAY_URL=
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
# Статическая сборка SvelteKit и раздача через serve
|
|
||||||
FROM node:22-alpine AS build
|
|
||||||
WORKDIR /app
|
|
||||||
ARG PUBLIC_TELEMT_GATEWAY_URL=http://127.0.0.1:8080
|
|
||||||
ENV PUBLIC_TELEMT_GATEWAY_URL=$PUBLIC_TELEMT_GATEWAY_URL
|
|
||||||
COPY package.json package-lock.json ./
|
|
||||||
RUN npm ci
|
|
||||||
COPY . .
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
FROM node:22-alpine
|
|
||||||
RUN npm install -g serve@14
|
|
||||||
WORKDIR /srv
|
|
||||||
COPY --from=build /app/build ./build
|
|
||||||
EXPOSE 4173
|
|
||||||
CMD ["serve", "-s", "build", "-l", "4173"]
|
|
||||||
+13
-21
@@ -1,16 +1,17 @@
|
|||||||
# Telemt Panel (Web UI)
|
# Telemt Panel (Web UI)
|
||||||
|
|
||||||
SvelteKit + shadcn-svelte: обзор флота (`/api/agg/*`), пользователи и IP, панель по каждой ноде через прокси шлюза (`/api/{alias}/…`).
|
SvelteKit + shadcn-svelte. В **production** статика собирается и **встраивается в образ шлюза** ([Dockerfile](../Dockerfile) в корне репозитория): панель и API на **одном порту** (например `http://127.0.0.1:8080/` — UI, `/api/…` — шлюз).
|
||||||
|
|
||||||
## Переменные окружения
|
## Переменная `PUBLIC_TELEMT_GATEWAY_URL`
|
||||||
|
|
||||||
| Переменная | Описание |
|
| Значение | Когда |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `PUBLIC_TELEMT_GATEWAY_URL` | Базовый URL шлюза **telemt-api** без завершающего `/` (например `http://127.0.0.1:8080`). Встраивается в клиент при сборке. |
|
| *(пусто)* | Тот же хост и порт, что у страницы (Docker-образ шлюза, `npm run build` в Dockerfile с `ENV PUBLIC_TELEMT_GATEWAY_URL=`) |
|
||||||
|
| `http://127.0.0.1:8080` | Локальная разработка: Vite на `:5173`, шлюз на `:8080` |
|
||||||
|
|
||||||
Скопируйте [.env.example](.env.example) в `.env` и при необходимости измените URL.
|
Скопируйте [.env.example](.env.example) в `.env` и при необходимости задайте URL.
|
||||||
|
|
||||||
## Разработка
|
## Разработка (Vite отдельно от Go)
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cd web
|
cd web
|
||||||
@@ -18,39 +19,30 @@ npm install
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Откройте в браузере адрес Vite (по умолчанию `http://localhost:5173`). В конфиге шлюза укажите CORS, например:
|
Откройте `http://localhost:5173`. В конфиге шлюза включите [CORS](../docs/AGGREGATE.md) для этого origin:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
cors_allowed_origins:
|
cors_allowed_origins:
|
||||||
- "http://localhost:5173"
|
- "http://localhost:5173"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Сборка
|
## Сборка только фронта
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
npm run build
|
npm run build
|
||||||
npm run preview # проверка статики
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Типы из OpenAPI агрегатов
|
Артефакт — каталог `build/`, при сборке **Docker-образа шлюза** он копируется в `internal/webui/static/` и попадает в бинарник через `embed`.
|
||||||
|
|
||||||
После изменения [../docs/AGGREGATE_OPENAPI.yaml](../docs/AGGREGATE_OPENAPI.yaml):
|
## Типы из OpenAPI агрегатов
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
npm run gen:api
|
npm run gen:api
|
||||||
```
|
```
|
||||||
|
|
||||||
## Docker
|
Источник: [../docs/AGGREGATE_OPENAPI.yaml](../docs/AGGREGATE_OPENAPI.yaml).
|
||||||
|
|
||||||
Сборка образа из каталога `web/`:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
docker build -t telemt-web:local --build-arg PUBLIC_TELEMT_GATEWAY_URL=http://127.0.0.1:8080 .
|
|
||||||
```
|
|
||||||
|
|
||||||
В [docker-compose.yml](../docker-compose.yml) сервис `web` отдаёт UI на порту **4173**. URL шлюза в образе задаётся **build-arg** (см. compose). Браузер на хосте обращается к шлюзу по `http://127.0.0.1:8080` — при другом адресе пересоберите образ с нужным `PUBLIC_TELEMT_GATEWAY_URL`.
|
|
||||||
|
|
||||||
## Ограничения
|
## Ограничения
|
||||||
|
|
||||||
- Секреты upstream к Telemt задаются на шлюзе (`authorization_env`), не в браузере.
|
- Секреты upstream к Telemt задаются на шлюзе (`authorization_env`), не в браузере.
|
||||||
- Mutating CORS: шлюз должен разрешать `POST`, `PATCH`, `DELETE` в preflight (в текущей версии репозитория заголовок `Access-Control-Allow-Methods` это учитывает).
|
- Для мутаций из другого origin шлюз отдаёт нужные заголовки CORS (в т.ч. методы `POST`, `PATCH`, `DELETE`).
|
||||||
|
|||||||
Reference in New Issue
Block a user