Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ body:
attributes:
label: Версии
description: Panel, Node Agent, HAProxy и ОС ноды.
placeholder: "Panel 2.0.1, Agent 2.0.1, HAProxy 3.4, Ubuntu 24.04"
placeholder: "Panel 2.1.0, Agent 2.1.0, HAProxy 3.4, Ubuntu 24.04"
validations:
required: true
- type: textarea
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 2.1.0 — Настройки HAProxy ноды и редактор конфига

Panel и Node Agent — 2.1.0, HAProxy renderer v21, миграция 000055 (`node_config_state.last_error_detail`). Заметки к релизу: [docs/releases/2.1.0.md](docs/releases/2.1.0.md). Идея и первая версия — @fesevu (#15).

- **Настройки HAProxy ноды** — `maxconn`, `nbthread`, `timeout connect/client/server` в диалоге настроек ноды под «Логи соединений HAProxy» (`nodes.metadata.haproxy_settings`, пусто = по умолчанию). Применяются ко всем маршрутам ноды через reload без разрыва соединений.
- **Редактор конфигурации** `/nodes/:id/haproxy` («Конфигурация HAProxy» на странице ноды): Monaco (локально, отдельный chunk), подсветка HAProxy, автодополнение, подсказки на русском, предупреждения об опасных местах, пометка имён `nf_be_*`/`nf_srv_*`. «Применить на ноду» сохраняет и применяет с diff против применённого конфига; история, загрузка из маршрутов, возврат к сборке из UI. Пока активен ручной конфиг, изменения маршрутов возвращают 409 `advanced_config_active`.
- **Проверка конфига** — линтер в браузере и `POST /api/v1/nodes/{id}/config-lint` (статический, ключевые слова сверены с HAProxy 2.8/3.0/3.2). Сохранение с ошибками — 422 `config_lint_failed`, `"force": true` сохраняет всё равно. Ручная ревизия хранит `renderer=manual`, `listener_tcp_ports`, `lint`; в конце добавляется перевод строки.
- **Agent:** при неудачном `haproxy -c` отправляет строки `[ALERT]`/`[WARNING]` (≤1500 байт, без строк с паролями); Panel отдаёт их в `config-state.last_error_detail`, редактор показывает со ссылками на строки.
- **Исправлено:** ручная ревизия при политике файрвола `apply` ломала каждый heartbeat Agent (500 `desired firewall listener plan is incomplete`) — план берёт порты из `listener_tcp_ports`, старые ручные ревизии без них сохраняют текущие порты.
- Переключатель логов соединений HAProxy в диалоге «Добавить ноду».

## 2.0.1 — Логи HAProxy на ноде и исправления редактора

Panel и Node Agent — 2.0.1, HAProxy renderer v21, миграций нет. Заметки к релизу: [docs/releases/2.0.1.md](docs/releases/2.0.1.md).
Expand Down
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<a href="#быстрая-установка">Установка</a> ·
<a href="#обновление-с-10x">Обновление</a> ·
<a href="#скриншоты">Скриншоты</a> ·
<a href="docs/releases/2.0.1.md">Что нового в 2.0.1</a> ·
<a href="docs/releases/2.1.0.md">Что нового в 2.1.0</a> ·
<a href="../../issues">Issues</a> ·
<a href="#поддержать-проект">Поддержка</a>
</p>
Expand Down Expand Up @@ -58,10 +58,10 @@ curl -fsSL https://raw.githubusercontent.com/NodeFlow-dev/nodeflow/main/install.
2. предложит режим доступа: Caddy cookie-gate (ссылка активации ставит защищённую cookie) или без него — токен администратора Panel нужен в любом случае;
3. установит Docker и **Caddy** — Caddy сам получит сертификат Let's Encrypt и будет проксировать HTTPS на Panel, которая слушает только `127.0.0.1:8080`;
4. скачает install kit последнего релиза, проверит его по SHA-256, который GitHub публикует для каждого asset релиза, возьмёт из него `compose.release.yaml`, сгенерирует `.env`, CA, сертификат mTLS и ключ подписи обновлений в `/opt/nodeflow`;
5. выполнит `docker compose pull && docker compose up -d` с образом `ghcr.io/nodeflow-dev/nodeflow-panel:2.0.1` — на сервере ничего не собирается, миграции БД применяет сам образ;
5. выполнит `docker compose pull && docker compose up -d` с образом `ghcr.io/nodeflow-dev/nodeflow-panel:2.1.0` — на сервере ничего не собирается, миграции БД применяет сам образ;
6. опубликует подписанные релизы Node Agent (amd64 и arm64) и сохранит реквизиты входа в `~/nodeflow-credentials.txt` (права `0600`).

Файрвол установщик не меняет. Для автоматизации без вопросов передайте переменные `NODEFLOW_DOMAIN`, `NODEFLOW_AUTH_MODE=cookie|none` и при необходимости `NODEFLOW_VERSION=2.0.1` (по умолчанию — последний релиз):
Файрвол установщик не меняет. Для автоматизации без вопросов передайте переменные `NODEFLOW_DOMAIN`, `NODEFLOW_AUTH_MODE=cookie|none` и при необходимости `NODEFLOW_VERSION=2.1.0` (по умолчанию — последний релиз):

```bash
curl -fsSL https://raw.githubusercontent.com/NodeFlow-dev/nodeflow/main/install.sh \
Expand Down Expand Up @@ -90,7 +90,7 @@ sudo NODE_AGENT_TOKEN=<токен> bash install-node.sh
Команды выполняются от root (`sudo -i`): каталог `/opt/nodeflow` закрыт для остальных пользователей.

```bash
v=2.0.1
v=2.1.0
kit=NodeFlow-Panel-$v-Agent-$v-install-kit
curl -fsSLO "https://github.com/NodeFlow-dev/nodeflow/releases/download/v$v/$kit.tar.gz"
# сверьте с digest asset'а на странице релиза или в API:
Expand Down Expand Up @@ -119,7 +119,7 @@ curl -fsSL https://raw.githubusercontent.com/NodeFlow-dev/nodeflow/main/install.

Найдя `/opt/nodeflow/.env`, он работает как апгрейдер: делает `pg_dump` и архив конфигурации в `/var/backups/nodeflow/`, сохраняет `.env`, `tls/`, `pki/` и Caddy-сниппет `/etc/caddy/conf.d/nodeflow-panel.caddy`, переводит установку со сборки из исходников на образ ghcr.io, запускает миграции и проверяет, что Panel отвечает новой версией. При ошибке возвращаются прежние `compose.yaml` и `.env`; дамп БД остаётся (миграции автоматически не откатываются).

Затем в **Настройки → Node Agent** обновите ноды до 2.0.1 — updater заменит Agent атомарно и откатит его при неудачном запуске. Самообновление не меняет systemd-юнит, поэтому на нодах, установленных версиями 1.0.x, один раз добавьте права на настройку pipe-буферов ядра (без этого splice остаётся на 256 КиБ) — или переустановите Agent из меню ноды:
Затем в **Настройки → Node Agent** обновите ноды до 2.1.0 — updater заменит Agent атомарно и откатит его при неудачном запуске. Самообновление не меняет systemd-юнит, поэтому на нодах, установленных версиями 1.0.x, один раз добавьте права на настройку pipe-буферов ядра (без этого splice остаётся на 256 КиБ) — или переустановите Agent из меню ноды:

```bash
sudo install -d /etc/systemd/system/nodeflow-node-agent.service.d
Expand Down Expand Up @@ -165,8 +165,9 @@ go test ./...

## Документация

- Заметки к релизам [2.0.1](docs/releases/2.0.1.md) и [2.0.0](docs/releases/2.0.0.md), [CHANGELOG](CHANGELOG.md)
- Заметки к релизам [2.1.0](docs/releases/2.1.0.md), [2.0.1](docs/releases/2.0.1.md) и [2.0.0](docs/releases/2.0.0.md), [CHANGELOG](CHANGELOG.md)
- [API Panel](docs/api-contract.md)
- [Настройки HAProxy ноды и редактор конфига](docs/features/haproxy-configuration.md)
- [Архитектура](docs/architecture.md)
- [Первичная установка ноды](docs/bootstrap-flow.md)
- [Самообновление Agent](docs/node-self-update.md)
Expand Down
2 changes: 1 addition & 1 deletion cmd/node-agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"github.com/nodeflow/nodeflow/internal/agent"
)

var version = "2.0.1"
var version = "2.1.0"

func main() {
showVersion := flag.Bool("version", false, "print Node Agent version and exit")
Expand Down
2 changes: 1 addition & 1 deletion cmd/panel-api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (

// panelVersion is the published Panel release version. Release builds may
// replace it with -ldflags "-X main.panelVersion=...".
var panelVersion = "2.0.1"
var panelVersion = "2.1.0"

func main() {
if len(os.Args) > 1 {
Expand Down
4 changes: 2 additions & 2 deletions compose.release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ services:
<<: *hardening

migrate:
image: ghcr.io/nodeflow-dev/nodeflow-panel:${NODEFLOW_VERSION:-2.0.1}
image: ghcr.io/nodeflow-dev/nodeflow-panel:${NODEFLOW_VERSION:-2.1.0}
restart: "no"
command: ["migrate"]
depends_on:
Expand Down Expand Up @@ -79,7 +79,7 @@ services:
<<: *hardening

panel-api:
image: ghcr.io/nodeflow-dev/nodeflow-panel:${NODEFLOW_VERSION:-2.0.1}
image: ghcr.io/nodeflow-dev/nodeflow-panel:${NODEFLOW_VERSION:-2.1.0}
restart: unless-stopped
depends_on:
migrate:
Expand Down
116 changes: 111 additions & 5 deletions docs/api-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ once and atomically bound to the verified leaf before subsequent use.
| `POST` | `/api/v1/nodes/{node_id}/render-config` | Deterministically preview a complete HAProxy config from enabled routes. |
| `GET` | `/api/v1/dns/resolve?host={hostname}` | Resolve a DNS hostname from the Panel host so the route editor can offer a `preferred_ip`; see "DNS resolve helper". |
| `POST` | `/api/v1/nodes/{node_id}/enrollment-tokens` | Issue a bearer token; plaintext is returned once, hash is stored. |
| `GET, POST` | `/api/v1/nodes/{node_id}/config-revisions` | List/create immutable, monotonically numbered HAProxy configurations. |
| `GET, POST` | `/api/v1/nodes/{node_id}/config-revisions` | List/create immutable, monotonically numbered HAProxy configurations (`advanced_editor` revisions are linted; see "Advanced editor"). |
| `POST` | `/api/v1/nodes/{node_id}/config-lint` | Static, read-only lint of an advanced-editor draft. |
| `POST` | `/api/v1/nodes/{node_id}/config-revisions/from-routes` | Render enabled routes and create an immutable revision without assigning it. |
| `GET` | `/api/v1/nodes/{node_id}/config-revisions/{revision}` | Read one immutable configuration revision. |
| `GET` | `/api/v1/nodes/{node_id}/config-state` | Read desired/actual revision and convergence state. |
| `GET` | `/api/v1/nodes/{node_id}/config-state` | Read desired/actual revision and convergence state (`last_error_detail`: HAProxy output of the last failed `haproxy -c`, Agent ≥ 2.1.0). |
| `POST` | `/api/v1/nodes/{node_id}/generated-config` | Atomically resume generated configuration from current routes and node settings. |
| `PUT` | `/api/v1/nodes/{node_id}/desired-revision` | Assign any existing revision as desired state. |
| `GET, POST` | `/api/v1/agent-releases` | List or stream/upload and sign an immutable Agent release. |
| `DELETE` | `/api/v1/agent-releases/{release_id}` | Delete only an unused release; installed, assigned and rollback-required releases return `409 release_in_use`. |
Expand Down Expand Up @@ -672,9 +674,11 @@ support.

`POST /api/v1/nodes/{node_id}/render-config` has no request body. It returns the
complete config, SHA-256, renderer version, counts, warnings and stable runtime
names. Disabled routes are excluded. An empty enabled route set returns
`422 no_enabled_routes`; a defensively detected inconsistent stored route set
returns `422 invalid_route_set`.
names. Disabled routes are excluded. An empty enabled route set returns a
base configuration without listeners, suitable for the Advanced editor. A
defensively detected inconsistent stored route set returns `422 invalid_route_set`.
Node-wide `global` and `defaults` settings and manual revision behavior are
described in [HAProxy configuration](features/haproxy-configuration.md).

The current renderer rejects wildcard/specific bind overlaps, caps enabled routes at
1024 and emits one ACL per SNI route, with up to 32 SNI values per `acl` line
Expand Down Expand Up @@ -748,6 +752,101 @@ The created immutable revision metadata includes `source`, `renderer`, counts,
`route_fingerprints: {route_uuid: desired_spec_sha256}`. This endpoint never changes
`desired_revision`; use `PUT .../desired-revision` as a separate explicit step.

### Advanced editor: lint and manual revisions

```http
POST /api/v1/nodes/{node_id}/config-lint
Content-Type: application/json

{"config":"global\n daemon\n..."}
```

Admin-authenticated, read-only static check (nothing stored, not audited). The
Panel has no `haproxy` binary: this is a structural best-effort check, the
Agent's `haproxy -c` before apply stays the final gate. `config` is limited to
524288 bytes (`400 validation_error`); unknown node → `404 not_found`.
Response `200`:

```json
{
"valid": false,
"issues": [
{"line": 12, "column": 5, "end_column": 16, "severity": "error",
"code": "undefined_backend", "message": "Backend «x» не определён"}
],
"listener_tcp_ports": [443, 8443],
"sections": [{"type": "frontend", "name": "fe", "line": 10}]
}
```

`valid` is `true` when there are no `error` issues. `issues` is sorted by
line/column (1-based; `end_column` exclusive), at most 500 plus a final
`too_many_issues` warning; messages are short Russian text. `listener_tcp_ports`
are sorted unique TCP ports from `bind` lines of `frontend`/`listen` sections
(ranges expanded; Unix/abns/UDP/QUIC binds ignored). `sections` lists every
section header in order (`name` is `""` for `global`/`defaults`).

Error codes: `outside_section`, `unknown_section`, `missing_name`,
`invalid_name`, `duplicate_proxy`, `duplicate_section`, `undefined_backend`
(skipped for dynamic `%[...]`/`{...}` names), `server_missing_name`,
`server_missing_address`, `duplicate_server`, `invalid_address`,
`invalid_port`, `bind_missing_address`, `bind_not_allowed`,
`server_not_allowed`, `use_backend_not_allowed`, `missing_backend`,
`invalid_timeout` (value must match `^[0-9]+(us|ms|s|m|h|d)?$`),
`timeout_missing_kind`, `timeout_missing_value`, `cond_unbalanced`,
`cond_missing_expression`, `unterminated_quote`, `too_many_words`,
`too_many_ports`, `invalid_server_template_count`, `server_template_incomplete`,
`missing_address`, `nul_byte`, `invalid_utf8`, `config_empty`,
`config_too_large`. Warning codes: `unknown_directive`, `unknown_timeout`,
`server_port_missing`, `default_backend_ignored`, `runtime_name_missing`
(a `nf_be_*` backend or `nf_srv_*` server that the generated configuration of
the node's current routes defines is missing — traffic statistics and quotas
depend on these names), `too_many_issues`. Any config produced by the renderer
lints with zero issues.

`POST /api/v1/nodes/{node_id}/config-revisions` with
`metadata.source:"advanced_editor"` runs the same lint server-side:

- with lint errors and without `"force":true` →
`422 {"error":{"code":"config_lint_failed","message":"..."},"issues":[...]}`
(issues as above); nothing is stored;
- otherwise the revision is stored (`201`) with `metadata.renderer:"manual"`,
`metadata.lint:{"errors":n,"warnings":n}` and
`metadata.listener_tcp_ports` (the linted bind ports; omitted only when bind
lines expand to more than 1024 ports);
- a first line `# Generated by NodeFlow. Do not edit.` is replaced with
`# Edited manually in NodeFlow advanced editor.` before storing.

`force` is accepted only for `advanced_editor` revisions (otherwise
`400 validation_error`); other revisions keep the previous behavior (no lint).
A forced revision with lint errors may still be assigned via
`PUT .../desired-revision`.

While a manual (`advanced_editor`) revision is assigned, route mutations that
would publish a configuration return `409 advanced_config_active`;
`POST /api/v1/nodes/{node_id}/generated-config` resumes generated
configuration. The firewall listener plan of a manual revision uses its
`metadata.listener_tcp_ports`; legacy manual revisions without it keep the
currently open ports (nothing is pruned).

When the Agent's `haproxy -c` fails, its config report carries
`details.error_detail` — the `[ALERT]`/`[WARNING]` lines (temp path replaced by
`haproxy.cfg`, ANSI stripped, lines with `password`/`secret` dropped, at most
1500 bytes). Panel stores it in `node_config_state.last_error_detail`
(migration 000055) and returns it as `last_error_detail` in
`GET .../config-state`; `null` for older Agents, which report only
`validation_failed`.

### Node HAProxy settings

`POST`/`PUT /api/v1/nodes` accept `metadata.haproxy_settings`:
`{"maxconn":int,"nbthread":int,"timeout_connect":"5s","timeout_client":"15m","timeout_server":"15m"}`,
every field optional (absent = default). Omitting the key keeps the stored
value, `{}` resets. Unknown fields, non-integral or out-of-range numbers and
malformed durations → `400`. A change republishes the node's generated
configuration in the same transaction (soft reload); an assigned manual
revision is not changed.

## Current MVP: Agent API (default `127.0.0.1:4200`)

All endpoints except health require `Authorization: Bearer $NODE_AGENT_TOKEN`.
Expand Down Expand Up @@ -822,6 +921,13 @@ new node starts in Panel mode `apply` (existing nodes are unchanged). After a
route revision becomes actual, its HAProxy TCP listener ports are therefore
opened automatically with exact `nodeflow`-tagged UFW rules; disabling
or deleting the last route on a port removes only that tagged stale rule.
Advanced-editor revisions (`metadata.source:"advanced_editor"`) with
`metadata.listener_tcp_ports` are complete listener plans too (ports come from
the Panel linter, see "Advanced editor: lint and manual revisions"). A legacy
manual revision without `listener_tcp_ports` never fails the heartbeat: while
it is desired, `firewall_assignment.mode` degrades to `observe` (no
transition, current rules kept, nothing pruned) and the config is still
delivered.

The Agent treats Panel's integer revision as decimal string `"42"` in its local revision marker. The marker is published only after a successful HAProxy reload, and idempotency requires both the marker and managed-config SHA-256 to match. Configurations are limited to 524288 bytes; the ten newest known-good backups are retained. Assignment checksum mismatch, validation and activation failures produce bounded reports with stable error codes only; raw HAProxy/systemd output is never reported.

Expand Down
Loading
Loading