Work version
This commit is contained in:
commit
fe3bf8a309
18 changed files with 7143 additions and 0 deletions
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Binaries
|
||||||
|
magos
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Credentials
|
||||||
|
*.env
|
||||||
|
|
||||||
|
# Output of the go coverage tool
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
vendor/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Config with sensitive data
|
||||||
|
config.yaml
|
||||||
|
config.yml
|
||||||
|
*.local.yaml
|
||||||
|
*.local.yml
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
1232
ARCHITECTURE.md
Normal file
1232
ARCHITECTURE.md
Normal file
File diff suppressed because it is too large
Load diff
912
DATABASE_SCHEMA.md
Normal file
912
DATABASE_SCHEMA.md
Normal file
|
|
@ -0,0 +1,912 @@
|
||||||
|
# Magos - Схема базы данных
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Обзор
|
||||||
|
|
||||||
|
Magos работает с PostgreSQL базой данных `waf_info`, которая содержит конфигурации WAF платформы. Программа использует 5 основных таблиц для получения информации о хостах, клиентах, приложениях и backend серверах.
|
||||||
|
|
||||||
|
### Используемые таблицы
|
||||||
|
|
||||||
|
1. **instances** - информация о хостах/серверах
|
||||||
|
2. **client_info** - данные клиентов и контейнеров
|
||||||
|
3. **apps_settings** - настройки приложений и кастомные директивы
|
||||||
|
4. **origins** - backend серверы (получаются через REST API)
|
||||||
|
5. **aliases** - дополнительные домены (получаются через REST API)
|
||||||
|
6. **ips** - IP адреса для WAF и antibot сетей
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Подключение к БД
|
||||||
|
|
||||||
|
### Параметры подключения
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DB_USER=login
|
||||||
|
DB_PASSWORD=password
|
||||||
|
DB_NAME=waf_info
|
||||||
|
DB_PORT=5432
|
||||||
|
PRIMARY_DB_HOST=10.10.10.8
|
||||||
|
SECONDARY_DB_HOST=10.10.10.5
|
||||||
|
```
|
||||||
|
|
||||||
|
### Failover логика
|
||||||
|
|
||||||
|
1. Попытка подключения к Primary DB (10.10.10.8)
|
||||||
|
2. Если неудача → подключение к Secondary DB (10.10.10.5)
|
||||||
|
3. Если обе недоступны → программа останавливается
|
||||||
|
|
||||||
|
**Функция:** `connectToDB()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Таблица: instances
|
||||||
|
|
||||||
|
### Назначение
|
||||||
|
|
||||||
|
Хранит информацию о хостах/серверах в кластере PTAF.
|
||||||
|
|
||||||
|
### Структура
|
||||||
|
|
||||||
|
| Поле | Тип | Описание | Пример |
|
||||||
|
|------|-----|----------|--------|
|
||||||
|
| `hostname` | TEXT | Hostname сервера | `host01.example.com` |
|
||||||
|
| `instance` | TEXT | Идентификатор инстанса | `waf-prod-01` |
|
||||||
|
|
||||||
|
### Используемые запросы
|
||||||
|
|
||||||
|
#### 1. Получение instance по hostname
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT instance
|
||||||
|
FROM instances
|
||||||
|
WHERE hostname = $1;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Функция:** `getInstanceByHostname(hostname string)`
|
||||||
|
|
||||||
|
**Использование:** При старте программы для определения какой это инстанс.
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
```go
|
||||||
|
hostname := "host01.example.com"
|
||||||
|
instance, err := getInstanceByHostname(db, hostname)
|
||||||
|
// instance = "waf-prod-01"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. Проверка принадлежности хоста к инстансу
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM instances
|
||||||
|
WHERE hostname = $1 AND instance = $2;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Функция:** `checkHostBelongsToInstance(hostname, instance string)`
|
||||||
|
|
||||||
|
**Использование:** Валидация что хост принадлежит данному инстансу.
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
```go
|
||||||
|
hostname := "host01.example.com"
|
||||||
|
instance := "waf-prod-01"
|
||||||
|
err := checkHostBelongsToInstance(db, hostname, instance)
|
||||||
|
// err == nil если хост принадлежит инстансу
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Примеры данных
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO instances (hostname, instance) VALUES
|
||||||
|
('host01.example.com', 'waf-prod-01'),
|
||||||
|
('host02.example.com', 'waf-prod-01'),
|
||||||
|
('host03.example.com', 'waf-prod-02');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Таблица: client_info
|
||||||
|
|
||||||
|
### Назначение
|
||||||
|
|
||||||
|
Хранит информацию о клиентах: количество контейнеров, привязка к инстансу.
|
||||||
|
|
||||||
|
### Структура
|
||||||
|
|
||||||
|
| Поле | Тип | Описание | Пример |
|
||||||
|
|------|-----|----------|--------|
|
||||||
|
| `client_title` | TEXT | Идентификатор клиента | `CLIENT001` |
|
||||||
|
| `containers_count` | INTEGER | Количество контейнеров | `3` |
|
||||||
|
| `ptaf_config` | TEXT | Конфигурация PTAF (JSON) | `{"key": "value"}` |
|
||||||
|
| `fluent_bit_port` | INTEGER | Порт Fluent Bit | `24224` |
|
||||||
|
| `waf_instance` | TEXT | Instance для apps_settings | `waf-prod-01` |
|
||||||
|
|
||||||
|
### Используемые запросы
|
||||||
|
|
||||||
|
#### 1. Получение клиентов по instance
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
containers_count,
|
||||||
|
ptaf_config,
|
||||||
|
client_title,
|
||||||
|
fluent_bit_port,
|
||||||
|
waf_instance
|
||||||
|
FROM client_info
|
||||||
|
WHERE waf_instance = $1;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Функция:** `getClientInfoByInstance(instance string)`
|
||||||
|
|
||||||
|
**Использование:** Получение всех клиентов для данного инстанса.
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
```go
|
||||||
|
instance := "waf-prod-01"
|
||||||
|
clients, err := getClientInfoByInstance(db, instance)
|
||||||
|
// clients = [{ContainersCount: 3, ClientTitle: "CLIENT001", ...}, ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. Получение клиента по client_title
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
containers_count,
|
||||||
|
ptaf_config,
|
||||||
|
client_title,
|
||||||
|
fluent_bit_port,
|
||||||
|
waf_instance
|
||||||
|
FROM client_info
|
||||||
|
WHERE client_title = $1;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Функция:** `getClientInfoByClientTitle(clientTitle string)`
|
||||||
|
|
||||||
|
**Использование:** Получение информации о конкретном клиенте.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Примеры данных
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO client_info
|
||||||
|
(client_title, containers_count, ptaf_config, fluent_bit_port, waf_instance)
|
||||||
|
VALUES
|
||||||
|
('CLIENT001', 3, '{"version": "1.0"}', 24224, 'waf-prod-01'),
|
||||||
|
('CLIENT002', 5, '{"version": "1.0"}', 24225, 'waf-prod-01'),
|
||||||
|
('CLIENT003', 2, '{"version": "1.0"}', 24226, 'waf-prod-02');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Ключевое поле: containers_count
|
||||||
|
|
||||||
|
**Назначение:** Определяет сколько Docker контейнеров нужно создать для клиента.
|
||||||
|
|
||||||
|
**Логика:**
|
||||||
|
- `containers_count = 3` → создать ptaf_client_001, ptaf_client_002, ptaf_client_003
|
||||||
|
- При увеличении → создаются новые контейнеры
|
||||||
|
- При уменьшении → лишние контейнеры помечаются на graceful shutdown (90s)
|
||||||
|
|
||||||
|
**Пример изменения:**
|
||||||
|
```sql
|
||||||
|
-- Увеличить до 5 контейнеров
|
||||||
|
UPDATE client_info
|
||||||
|
SET containers_count = 5
|
||||||
|
WHERE client_title = 'CLIENT001';
|
||||||
|
|
||||||
|
-- Manager автоматически создаст ptaf_client_004 и ptaf_client_005
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Таблица: apps_settings
|
||||||
|
|
||||||
|
### Назначение
|
||||||
|
|
||||||
|
Основная таблица с настройками приложений и кастомными директивами Nginx/Angie.
|
||||||
|
|
||||||
|
### Структура
|
||||||
|
|
||||||
|
| Поле | Тип | Описание | Значение по умолчанию |
|
||||||
|
|------|-----|----------|----------------------|
|
||||||
|
| `l7resourceid` | INTEGER | ID ресурса | Первичный ключ |
|
||||||
|
| `client_title` | TEXT | Клиент (FK) | `CLIENT001` |
|
||||||
|
| `waf_enabled` | BOOLEAN | WAF включен | `true` |
|
||||||
|
| `sni` | INTEGER | SNI режим (0=keepalive on, 1=keepalive off) | `0` |
|
||||||
|
| `custom_input_http_ports` | INTEGER[] | HTTP порты для прослушивания | `{80, 8080}` |
|
||||||
|
| `custom_input_https_ports` | INTEGER[] | HTTPS порты для прослушивания | `{443, 8443}` |
|
||||||
|
| `custom_output_http_ports` | INTEGER[] | HTTP порты к backend | `{80}` |
|
||||||
|
| `custom_output_https_ports` | INTEGER[] | HTTPS порты к backend | `{443}` |
|
||||||
|
| `upstream_angie_custom` | TEXT | Кастомные upstream директивы (Angie) | `NULL` |
|
||||||
|
| `upstream_nginx_custom` | TEXT | Кастомные upstream директивы (Nginx) | `NULL` |
|
||||||
|
| `server_angie_custom` | TEXT | Полная замена server блока (Angie) | `NULL` |
|
||||||
|
| `server_nginx_custom` | TEXT | Полная замена server блока (Nginx) | `NULL` |
|
||||||
|
| `server_directives_angie_custom` | TEXT | Кастомные server директивы (Angie) | `NULL` |
|
||||||
|
| `server_directives_nginx_custom` | TEXT | Кастомные server директивы (Nginx) | `NULL` |
|
||||||
|
| `location_angie_custom` | TEXT | Кастомные location блоки (Angie) | `NULL` |
|
||||||
|
| `location_nginx_custom` | TEXT | Кастомные location блоки (Nginx) | `NULL` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Используемые запросы
|
||||||
|
|
||||||
|
#### Получение настроек по client_title
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
l7resourceid,
|
||||||
|
waf_enabled,
|
||||||
|
custom_input_http_ports,
|
||||||
|
custom_input_https_ports,
|
||||||
|
custom_output_http_ports,
|
||||||
|
custom_output_https_ports,
|
||||||
|
upstream_angie_custom,
|
||||||
|
server_nginx_custom,
|
||||||
|
server_angie_custom,
|
||||||
|
client_title,
|
||||||
|
upstream_nginx_custom,
|
||||||
|
location_angie_custom,
|
||||||
|
location_nginx_custom,
|
||||||
|
server_directives_angie_custom,
|
||||||
|
server_directives_nginx_custom,
|
||||||
|
sni
|
||||||
|
FROM apps_settings
|
||||||
|
WHERE client_title = $1
|
||||||
|
ORDER BY l7resourceid ASC;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Функция:** `getAppsSettingsByClientTitle(clientTitle string)`
|
||||||
|
|
||||||
|
**Использование:** Получение всех настроек приложений для клиента.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Примеры данных
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO apps_settings
|
||||||
|
(l7resourceid, client_title, waf_enabled, sni,
|
||||||
|
custom_input_http_ports, custom_input_https_ports,
|
||||||
|
custom_output_http_ports, custom_output_https_ports)
|
||||||
|
VALUES
|
||||||
|
(12323, 'CLIENT001', true, 0, '{80}', '{443}', '{80}', '{443}'),
|
||||||
|
(12324, 'CLIENT001', true, 0, '{80,8080}', '{443,8443}', '{80}', '{443}'),
|
||||||
|
(15000, 'CLIENT002', true, 0, '{80}', '{443}', '{80}', '{443}');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Кастомные поля (подробно)
|
||||||
|
|
||||||
|
#### upstream_angie_custom / upstream_nginx_custom
|
||||||
|
|
||||||
|
**Режим:** REPLACE (полная замена keepalive директив)
|
||||||
|
|
||||||
|
**NULL значение:**
|
||||||
|
```nginx
|
||||||
|
upstream backend {
|
||||||
|
server 127.0.0.1:21000;
|
||||||
|
|
||||||
|
# Default upstream directives
|
||||||
|
keepalive 60;
|
||||||
|
keepalive_timeout 70s;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**НЕ NULL значение:**
|
||||||
|
```nginx
|
||||||
|
upstream backend {
|
||||||
|
server 127.0.0.1:21000;
|
||||||
|
|
||||||
|
# Custom upstream directives (replace defaults)
|
||||||
|
keepalive 200;
|
||||||
|
keepalive_timeout 90s;
|
||||||
|
least_conn;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример SQL:**
|
||||||
|
```sql
|
||||||
|
-- Изменить балансировку на least_conn
|
||||||
|
UPDATE apps_settings
|
||||||
|
SET upstream_angie_custom = 'keepalive 60;
|
||||||
|
keepalive_timeout 70s;
|
||||||
|
least_conn;'
|
||||||
|
WHERE l7resourceid = 12323;
|
||||||
|
|
||||||
|
-- Вернуть дефолты
|
||||||
|
UPDATE apps_settings
|
||||||
|
SET upstream_angie_custom = NULL
|
||||||
|
WHERE l7resourceid = 12323;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### server_directives_angie_custom / server_directives_nginx_custom
|
||||||
|
|
||||||
|
**Режим:** REPLACE (замена else блока в server)
|
||||||
|
|
||||||
|
**NULL значение:**
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
|
||||||
|
# Default directives
|
||||||
|
keepalive_timeout 80s;
|
||||||
|
send_timeout 60s;
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_read_timeout 120s;
|
||||||
|
proxy_send_timeout 120s;
|
||||||
|
|
||||||
|
large_client_header_buffers 4 128k;
|
||||||
|
proxy_buffers 4 32k;
|
||||||
|
proxy_buffer_size 16k;
|
||||||
|
proxy_busy_buffers_size 32k;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**НЕ NULL значение:**
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
|
||||||
|
# Custom directives (replace defaults)
|
||||||
|
keepalive_timeout 120s;
|
||||||
|
client_max_body_size 100m;
|
||||||
|
# Нужно указать ВСЕ директивы, включая буферы!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример SQL:**
|
||||||
|
```sql
|
||||||
|
-- Увеличить таймауты
|
||||||
|
UPDATE apps_settings
|
||||||
|
SET server_directives_angie_custom = 'keepalive_timeout 120s;
|
||||||
|
send_timeout 90s;
|
||||||
|
proxy_connect_timeout 90s;
|
||||||
|
proxy_read_timeout 180s;
|
||||||
|
proxy_send_timeout 180s;
|
||||||
|
|
||||||
|
large_client_header_buffers 4 128k;
|
||||||
|
proxy_buffers 4 32k;
|
||||||
|
proxy_buffer_size 16k;'
|
||||||
|
WHERE l7resourceid = 12323;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### server_angie_custom / server_nginx_custom
|
||||||
|
|
||||||
|
**Режим:** SERVER_BLOCK (полная замена всего server блока)
|
||||||
|
|
||||||
|
**NULL значение:** Используется стандартный шаблон server блока
|
||||||
|
|
||||||
|
**НЕ NULL значение:** Полная замена server блока
|
||||||
|
|
||||||
|
**Пример SQL:**
|
||||||
|
```sql
|
||||||
|
-- Создать полностью кастомный server блок
|
||||||
|
UPDATE apps_settings
|
||||||
|
SET server_angie_custom = 'server {
|
||||||
|
listen 9999;
|
||||||
|
server_name special.example.com;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 200 "Custom response";
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
WHERE l7resourceid = 12323;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### location_angie_custom / location_nginx_custom
|
||||||
|
|
||||||
|
**Режим:** APPEND (добавление к дефолтному location)
|
||||||
|
|
||||||
|
**NULL значение:** Только стандартный `location / { proxy_pass }`
|
||||||
|
|
||||||
|
**НЕ NULL значение:** Добавляются дополнительные location блоки
|
||||||
|
|
||||||
|
**Пример SQL:**
|
||||||
|
```sql
|
||||||
|
-- Добавить location для API
|
||||||
|
UPDATE apps_settings
|
||||||
|
SET location_nginx_custom = 'location /api {
|
||||||
|
proxy_pass http://api.backend.com;
|
||||||
|
proxy_set_header X-API-Key secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /static {
|
||||||
|
alias /var/www/static;
|
||||||
|
}'
|
||||||
|
WHERE l7resourceid = 12323;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### sni (Server Name Indication)
|
||||||
|
|
||||||
|
**Значения:**
|
||||||
|
- `0` - Keepalive ВКЛЮЧЕН (дефолт)
|
||||||
|
- `1` - Keepalive ОТКЛЮЧЕН
|
||||||
|
|
||||||
|
**Когда sni = 1:**
|
||||||
|
- Директивы `keepalive` и `keepalive_timeout` НЕ добавляются в upstream
|
||||||
|
- Используется для SNI passthrough (прямая передача без терминации SSL)
|
||||||
|
|
||||||
|
**Пример SQL:**
|
||||||
|
```sql
|
||||||
|
-- Отключить keepalive для SNI passthrough
|
||||||
|
UPDATE apps_settings
|
||||||
|
SET sni = 1
|
||||||
|
WHERE l7resourceid = 12323;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### custom_input_*_ports и custom_output_*_ports
|
||||||
|
|
||||||
|
**custom_input_http_ports / custom_input_https_ports:**
|
||||||
|
- Порты на которых Angie и Nginx контейнеры будут слушать
|
||||||
|
- Angie слушает на этих портах (80, 443, 8080, 8443)
|
||||||
|
- Nginx контейнеры слушают на этих же портах
|
||||||
|
|
||||||
|
**custom_output_http_ports / custom_output_https_ports:**
|
||||||
|
- Порты к которым Nginx контейнеры будут проксировать на backend
|
||||||
|
- Обычно {80} для HTTP и {443} для HTTPS
|
||||||
|
- Могут быть нестандартные порты (8080, 8443)
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
```sql
|
||||||
|
-- Приложение слушает на портах 80, 8080 (HTTP) и 443, 8443 (HTTPS)
|
||||||
|
-- Backend работает на порту 443
|
||||||
|
UPDATE apps_settings
|
||||||
|
SET
|
||||||
|
custom_input_http_ports = '{80, 8080}',
|
||||||
|
custom_input_https_ports = '{443, 8443}',
|
||||||
|
custom_output_http_ports = '{80}',
|
||||||
|
custom_output_https_ports = '{443}'
|
||||||
|
WHERE l7resourceid = 12323;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Результат:**
|
||||||
|
```nginx
|
||||||
|
# Angie
|
||||||
|
server {
|
||||||
|
listen 80; # custom_input_http_ports[0]
|
||||||
|
listen 8080; # custom_input_http_ports[1]
|
||||||
|
listen 443 ssl; # custom_input_https_ports[0]
|
||||||
|
listen 8443 ssl; # custom_input_https_ports[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Nginx upstream
|
||||||
|
upstream backend {
|
||||||
|
server 185.22.65.148:443; # custom_output_https_ports[0]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Таблица: ips
|
||||||
|
|
||||||
|
### Назначение
|
||||||
|
|
||||||
|
Хранит IP адреса для WAF и antibot сетей (белые списки).
|
||||||
|
|
||||||
|
### Структура
|
||||||
|
|
||||||
|
| Поле | Тип | Описание |
|
||||||
|
|------|-----|----------|
|
||||||
|
| `antibot_networks` | TEXT[] | IP сети для antibot |
|
||||||
|
| `waf_networks` | TEXT[] | IP сети для WAF |
|
||||||
|
|
||||||
|
### Используемые запросы
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT antibot_networks, waf_networks
|
||||||
|
FROM ips;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Функция:** `loadIPsFromDB()`
|
||||||
|
|
||||||
|
**Использование:** При старте программы для загрузки IP сетей.
|
||||||
|
|
||||||
|
**Пример данных:**
|
||||||
|
```sql
|
||||||
|
INSERT INTO ips (antibot_networks, waf_networks) VALUES
|
||||||
|
(
|
||||||
|
'{172.16.0.0/12, 10.0.0.0/8}',
|
||||||
|
'{172.16.0.0/12, 10.100.0.0/16}'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Использование в конфигах:**
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
# Доверенные IP для получения реального IP клиента
|
||||||
|
set_real_ip_from 172.16.0.0/12;
|
||||||
|
set_real_ip_from 10.100.0.0/16;
|
||||||
|
real_ip_header X-Forwarded-For;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## REST API (origins и aliases)
|
||||||
|
|
||||||
|
### Origins (Backend серверы)
|
||||||
|
|
||||||
|
**Не хранятся в БД**, получаются через REST API.
|
||||||
|
|
||||||
|
**Эндпоинт:** `/api/waf-proxy/get-apps-and-resourses`
|
||||||
|
|
||||||
|
**Функция:** `makeAPIRequest()`, `getResourceDataFromCache()`
|
||||||
|
|
||||||
|
**Пример ответа:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"result": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"ip": "185.22.65.148",
|
||||||
|
"weight": 100,
|
||||||
|
"mode": "active",
|
||||||
|
"comment": "Primary backend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"ip": "185.22.65.149",
|
||||||
|
"weight": 50,
|
||||||
|
"mode": "backup",
|
||||||
|
"comment": "Backup backend"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Использование в конфигах:**
|
||||||
|
```nginx
|
||||||
|
upstream backend {
|
||||||
|
server 185.22.65.148:443 weight=100;
|
||||||
|
server 185.22.65.149:443 weight=50 backup;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Aliases (Дополнительные домены)
|
||||||
|
|
||||||
|
**Не хранятся в БД**, получаются через REST API.
|
||||||
|
|
||||||
|
**Эндпоинт:** `/api/waf-proxy/get-apps-and-resourses`
|
||||||
|
|
||||||
|
**Пример ответа:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"result": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"alias": "www.example.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"alias": "app.example.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Использование в конфигах:**
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name example.com www.example.com app.example.com;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Связи между таблицами
|
||||||
|
|
||||||
|
```
|
||||||
|
instances
|
||||||
|
↓ (hostname → instance)
|
||||||
|
client_info (waf_instance)
|
||||||
|
↓ (client_title)
|
||||||
|
apps_settings (client_title)
|
||||||
|
↓ (l7resourceid через API)
|
||||||
|
Origins (от REST API)
|
||||||
|
Aliases (от REST API)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Поток данных:**
|
||||||
|
|
||||||
|
1. Program запускается на хосте с hostname = `host01.example.com`
|
||||||
|
2. Запрос к `instances` → получаем `instance = "waf-prod-01"`
|
||||||
|
3. Запрос к `client_info` WHERE `waf_instance = "waf-prod-01"` → получаем клиентов
|
||||||
|
4. Для каждого клиента запрос к `apps_settings` WHERE `client_title = "CLIENT001"`
|
||||||
|
5. Для каждого ресурса API запрос → получаем origins и aliases
|
||||||
|
6. Генерируем конфиги на основе всех данных
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Примеры типичных запросов
|
||||||
|
|
||||||
|
### Получить все ресурсы клиента
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
a.l7resourceid,
|
||||||
|
a.custom_input_https_ports,
|
||||||
|
a.upstream_angie_custom,
|
||||||
|
c.containers_count
|
||||||
|
FROM apps_settings a
|
||||||
|
JOIN client_info c ON a.client_title = c.client_title
|
||||||
|
WHERE a.client_title = 'CLIENT001'
|
||||||
|
ORDER BY a.l7resourceid;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Изменить количество контейнеров
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Увеличить до 5
|
||||||
|
UPDATE client_info
|
||||||
|
SET containers_count = 5
|
||||||
|
WHERE client_title = 'CLIENT001';
|
||||||
|
|
||||||
|
-- Проверка
|
||||||
|
SELECT client_title, containers_count
|
||||||
|
FROM client_info
|
||||||
|
WHERE client_title = 'CLIENT001';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Добавить кастомную балансировку
|
||||||
|
|
||||||
|
```sql
|
||||||
|
UPDATE apps_settings
|
||||||
|
SET upstream_nginx_custom = 'keepalive 60;
|
||||||
|
keepalive_timeout 70s;
|
||||||
|
least_conn;'
|
||||||
|
WHERE l7resourceid = 12323;
|
||||||
|
|
||||||
|
-- Проверка
|
||||||
|
SELECT l7resourceid, upstream_nginx_custom
|
||||||
|
FROM apps_settings
|
||||||
|
WHERE l7resourceid = 12323;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Включить SNI passthrough
|
||||||
|
|
||||||
|
```sql
|
||||||
|
UPDATE apps_settings
|
||||||
|
SET sni = 1
|
||||||
|
WHERE l7resourceid = 12323;
|
||||||
|
|
||||||
|
-- Проверка
|
||||||
|
SELECT l7resourceid, sni
|
||||||
|
FROM apps_settings
|
||||||
|
WHERE sni = 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Добавить дополнительные порты
|
||||||
|
|
||||||
|
```sql
|
||||||
|
UPDATE apps_settings
|
||||||
|
SET
|
||||||
|
custom_input_https_ports = '{443, 8443, 9443}',
|
||||||
|
custom_output_https_ports = '{443, 8443, 9443}'
|
||||||
|
WHERE l7resourceid = 12323;
|
||||||
|
|
||||||
|
-- Проверка
|
||||||
|
SELECT l7resourceid, custom_input_https_ports, custom_output_https_ports
|
||||||
|
FROM apps_settings
|
||||||
|
WHERE l7resourceid = 12323;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Индексы (рекомендуемые)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Для быстрого поиска по hostname
|
||||||
|
CREATE INDEX idx_instances_hostname ON instances(hostname);
|
||||||
|
|
||||||
|
-- Для быстрого поиска клиентов по instance
|
||||||
|
CREATE INDEX idx_client_info_waf_instance ON client_info(waf_instance);
|
||||||
|
CREATE INDEX idx_client_info_client_title ON client_info(client_title);
|
||||||
|
|
||||||
|
-- Для быстрого поиска настроек по client_title
|
||||||
|
CREATE INDEX idx_apps_settings_client_title ON apps_settings(client_title);
|
||||||
|
CREATE INDEX idx_apps_settings_l7resourceid ON apps_settings(l7resourceid);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Миграции и изменения схемы
|
||||||
|
|
||||||
|
### Добавление нового кастомного поля
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Добавить новое поле для header манипуляций
|
||||||
|
ALTER TABLE apps_settings
|
||||||
|
ADD COLUMN headers_custom TEXT DEFAULT NULL;
|
||||||
|
|
||||||
|
-- Обновить код для чтения этого поля
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Изменение типа поля
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Изменить тип sni на boolean (если сейчас integer)
|
||||||
|
ALTER TABLE apps_settings
|
||||||
|
ALTER COLUMN sni TYPE BOOLEAN
|
||||||
|
USING (sni::integer = 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. NULL vs пустая строка
|
||||||
|
|
||||||
|
**Правильно:**
|
||||||
|
```sql
|
||||||
|
UPDATE apps_settings
|
||||||
|
SET upstream_angie_custom = NULL -- NULL для дефолтов
|
||||||
|
WHERE l7resourceid = 12323;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Неправильно:**
|
||||||
|
```sql
|
||||||
|
UPDATE apps_settings
|
||||||
|
SET upstream_angie_custom = '' -- Пустая строка может вызвать ошибки
|
||||||
|
WHERE l7resourceid = 12323;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Массивы портов
|
||||||
|
|
||||||
|
**Правильно:**
|
||||||
|
```sql
|
||||||
|
UPDATE apps_settings
|
||||||
|
SET custom_input_https_ports = '{443, 8443}' -- PostgreSQL array
|
||||||
|
WHERE l7resourceid = 12323;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Неправильно:**
|
||||||
|
```sql
|
||||||
|
UPDATE apps_settings
|
||||||
|
SET custom_input_https_ports = '443,8443' -- Строка, не массив
|
||||||
|
WHERE l7resourceid = 12323;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Многострочные кастомные директивы
|
||||||
|
|
||||||
|
**Правильно:**
|
||||||
|
```sql
|
||||||
|
UPDATE apps_settings
|
||||||
|
SET server_directives_angie_custom = 'keepalive_timeout 80s;
|
||||||
|
send_timeout 60s;
|
||||||
|
proxy_buffers 4 32k;'
|
||||||
|
WHERE l7resourceid = 12323;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Также правильно (с экранированием):**
|
||||||
|
```sql
|
||||||
|
UPDATE apps_settings
|
||||||
|
SET server_directives_angie_custom = E'keepalive_timeout 80s;\nsend_timeout 60s;'
|
||||||
|
WHERE l7resourceid = 12323;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Мониторинг БД
|
||||||
|
|
||||||
|
### Проверка соединения
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Проверить активные соединения
|
||||||
|
SELECT
|
||||||
|
datname,
|
||||||
|
usename,
|
||||||
|
client_addr,
|
||||||
|
state,
|
||||||
|
query_start
|
||||||
|
FROM pg_stat_activity
|
||||||
|
WHERE datname = 'waf_info';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Проверка данных
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Количество клиентов на каждом instance
|
||||||
|
SELECT waf_instance, COUNT(*) as clients_count
|
||||||
|
FROM client_info
|
||||||
|
GROUP BY waf_instance;
|
||||||
|
|
||||||
|
-- Количество ресурсов на клиента
|
||||||
|
SELECT client_title, COUNT(*) as resources_count
|
||||||
|
FROM apps_settings
|
||||||
|
GROUP BY client_title;
|
||||||
|
|
||||||
|
-- Клиенты с кастомными upstream директивами
|
||||||
|
SELECT client_title, l7resourceid
|
||||||
|
FROM apps_settings
|
||||||
|
WHERE upstream_angie_custom IS NOT NULL
|
||||||
|
OR upstream_nginx_custom IS NOT NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Резервное копирование
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Дамп всей БД
|
||||||
|
pg_dump -h 10.100.10.8 -U install -d waf_info > waf_info_backup.sql
|
||||||
|
|
||||||
|
# Дамп только структуры
|
||||||
|
pg_dump -h 10.100.10.8 -U install -d waf_info --schema-only > schema.sql
|
||||||
|
|
||||||
|
# Дамп только данных
|
||||||
|
pg_dump -h 10.100.10.8 -U install -d waf_info --data-only > data.sql
|
||||||
|
|
||||||
|
# Восстановление
|
||||||
|
psql -h 10.100.10.8 -U install -d waf_info < waf_info_backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Итого
|
||||||
|
|
||||||
|
### Основные таблицы
|
||||||
|
|
||||||
|
| Таблица | Записей (примерно) | Частота изменений |
|
||||||
|
|---------|-------------------|-------------------|
|
||||||
|
| instances | 10-50 | Редко (при добавлении хостов) |
|
||||||
|
| client_info | 50-500 | Редко (при добавлении клиентов) |
|
||||||
|
| apps_settings | 500-5000 | Часто (изменение настроек) |
|
||||||
|
| ips | 1 | Очень редко |
|
||||||
|
|
||||||
|
### Ключевые связи
|
||||||
|
|
||||||
|
- `instances.instance` → `client_info.waf_instance`
|
||||||
|
- `client_info.client_title` → `apps_settings.client_title`
|
||||||
|
- `apps_settings.l7resourceid` → REST API (origins, aliases)
|
||||||
|
|
||||||
|
### Кастомизация
|
||||||
|
|
||||||
|
Все кастомные поля в `apps_settings`:
|
||||||
|
- 8 полей для кастомных директив
|
||||||
|
- 4 режима: REPLACE (upstream, server_directives), SERVER_BLOCK, APPEND (location)
|
||||||
|
- NULL = дефолты, NOT NULL = кастом
|
||||||
26
README.md
Normal file
26
README.md
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Magos
|
||||||
|
|
||||||
|
Программа для автоматизированного управления конфигурациями WAF.
|
||||||
|
|
||||||
|
## Описание
|
||||||
|
|
||||||
|
Программа выполняет следующие задачи:
|
||||||
|
|
||||||
|
1. Подключается к PostgreSQL БД (primary/secondary)
|
||||||
|
2. Определяет instance текущего хоста по hostname
|
||||||
|
3. Получает биндинги для данного instance из таблицы `apps_settings`
|
||||||
|
4. Группирует биндинги по `client_title`
|
||||||
|
5. Для каждого клиента:
|
||||||
|
- Получает данные из БД или через API ServicePipe (server_name, aliases, origins)
|
||||||
|
- Выделяет свободные порты для контейнеров
|
||||||
|
- Генерирует конфигурационные файлы Angie
|
||||||
|
- Создает директории для контейнеров
|
||||||
|
- Генерирует конфигурационные файлы Nginx для контейнеров
|
||||||
|
- Генерирует docker-compose.yml файлы
|
||||||
|
- Запускает контейнеры через docker-compose
|
||||||
|
|
||||||
|
## Подробности
|
||||||
|
|
||||||
|
Изучить как работает программа можно в файле ARCHITECTURE.md.
|
||||||
|
|
||||||
|
Посмотреть зависимость работы программы от БД в файле DATABASE_SCHEMA.md
|
||||||
364
api.go
Normal file
364
api.go
Normal file
|
|
@ -0,0 +1,364 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// apiClient — переиспользуемый HTTP-клиент для всех API запросов.
|
||||||
|
// http.Client безопасен для конкурентного использования и переиспользует соединения.
|
||||||
|
var apiClient = &http.Client{
|
||||||
|
Timeout: 120 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeAPIRequest выполняет GET запрос к API с авторизацией и повторными попытками
|
||||||
|
func makeAPIRequest(url, token string) ([]byte, error) {
|
||||||
|
maxRetries := 3
|
||||||
|
var lastErr error
|
||||||
|
|
||||||
|
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||||
|
body, err := doAPIRequest(url, token)
|
||||||
|
if err == nil {
|
||||||
|
if attempt > 1 {
|
||||||
|
log.Printf(" ✓ Запрос успешен после %d попыток", attempt)
|
||||||
|
}
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lastErr = err
|
||||||
|
if attempt < maxRetries {
|
||||||
|
waitTime := time.Duration(attempt*attempt) * time.Second // 1s, 4s, 9s
|
||||||
|
log.Printf(" ⚠ Попытка %d/%d не удалась: %v, повтор через %v", attempt, maxRetries, err, waitTime)
|
||||||
|
time.Sleep(waitTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("запрос не удался после %d попыток: %w", maxRetries, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// doAPIRequest выполняет одиночный API запрос (без retry)
|
||||||
|
func doAPIRequest(url, token string) ([]byte, error) {
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ошибка создания запроса: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := apiClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ошибка чтения ответа: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("API вернул код %d, URL: %s, ответ: %s", resp.StatusCode, url, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getResourceDataFromCache пытается получить данные из таблицы sp_info
|
||||||
|
func getResourceDataFromCache(db *sql.DB, config Config, l7ResourceID int) (serverName string, aliases []string, origins []OriginItem, found bool, err error) {
|
||||||
|
query := `
|
||||||
|
SELECT
|
||||||
|
domain_name,
|
||||||
|
aliases,
|
||||||
|
origins,
|
||||||
|
updated_at,
|
||||||
|
EXTRACT(EPOCH FROM (NOW() - updated_at)) / 60 AS age_minutes
|
||||||
|
FROM sp_info
|
||||||
|
WHERE sid = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
var domainName sql.NullString
|
||||||
|
var aliasesJSON sql.NullString
|
||||||
|
var originsJSON sql.NullString
|
||||||
|
var updatedAt sql.NullTime
|
||||||
|
var ageMinutes sql.NullFloat64
|
||||||
|
|
||||||
|
err = db.QueryRow(query, l7ResourceID).Scan(&domainName, &aliasesJSON, &originsJSON, &updatedAt, &ageMinutes)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return "", nil, nil, false, nil // Данных нет в кеше
|
||||||
|
}
|
||||||
|
return "", nil, nil, false, fmt.Errorf("ошибка чтения из sp_info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем свежесть данных (не старее 10 минут)
|
||||||
|
if !ageMinutes.Valid {
|
||||||
|
log.Printf(" ⚠ updated_at не заполнен в кеше, обращаюсь к API")
|
||||||
|
return "", nil, nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
age := ageMinutes.Float64
|
||||||
|
if age < 0 {
|
||||||
|
age = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if age > 10 {
|
||||||
|
log.Printf(" ⚠ Данные в кеше устарели (возраст: %.1f мин), обращаюсь к API", age)
|
||||||
|
return "", nil, nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf(" ✓ Данные из кеша актуальны (возраст: %.1f мин)", age)
|
||||||
|
|
||||||
|
if domainName.Valid {
|
||||||
|
serverName = domainName.String
|
||||||
|
}
|
||||||
|
|
||||||
|
aliases, err = parseAliasesJSON(aliasesJSON)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, nil, false, fmt.Errorf("ошибка парсинга aliases из sp_info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
origins, err = parseOriginsJSON(originsJSON, config.WAFNetworks)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, nil, false, fmt.Errorf("ошибка парсинга origins из sp_info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverName, aliases, origins, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getResourceDataFromManualInfo получает данные из таблицы manual_info (ручное управление)
|
||||||
|
// В отличие от sp_info, не проверяет timestamp - данные всегда актуальны
|
||||||
|
func getResourceDataFromManualInfo(db *sql.DB, config Config, l7ResourceID int) (serverName string, aliases []string, origins []OriginItem, err error) {
|
||||||
|
query := `
|
||||||
|
SELECT
|
||||||
|
domain_name,
|
||||||
|
aliases,
|
||||||
|
origins
|
||||||
|
FROM manual_info
|
||||||
|
WHERE sid = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
var domainName sql.NullString
|
||||||
|
var aliasesJSON sql.NullString
|
||||||
|
var originsJSON sql.NullString
|
||||||
|
|
||||||
|
err = db.QueryRow(query, l7ResourceID).Scan(&domainName, &aliasesJSON, &originsJSON)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return "", nil, nil, fmt.Errorf("данные не найдены в manual_info для sid=%d", l7ResourceID)
|
||||||
|
}
|
||||||
|
return "", nil, nil, fmt.Errorf("ошибка чтения из manual_info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if domainName.Valid {
|
||||||
|
serverName = domainName.String
|
||||||
|
}
|
||||||
|
|
||||||
|
aliases, err = parseAliasesJSON(aliasesJSON)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, nil, fmt.Errorf("ошибка парсинга aliases из manual_info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
origins, err = parseOriginsJSON(originsJSON, config.WAFNetworks)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, nil, fmt.Errorf("ошибка парсинга origins из manual_info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverName, aliases, origins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseAliasesJSON парсит JSON массив алиасов из sql.NullString
|
||||||
|
func parseAliasesJSON(aliasesJSON sql.NullString) ([]string, error) {
|
||||||
|
if !aliasesJSON.Valid || aliasesJSON.String == "" || aliasesJSON.String == "null" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var aliases []string
|
||||||
|
if err := json.Unmarshal([]byte(aliasesJSON.String), &aliases); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return aliases, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseOriginsJSON парсит JSON массив origins из sql.NullString, фильтруя WAF IP
|
||||||
|
func parseOriginsJSON(originsJSON sql.NullString, wafNetworks []string) ([]OriginItem, error) {
|
||||||
|
if !originsJSON.Valid || originsJSON.String == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var originsData []struct {
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Weight int `json:"weight"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(originsJSON.String), &originsData); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var origins []OriginItem
|
||||||
|
for _, o := range originsData {
|
||||||
|
if !isIPInWAFNetworks(o.IP, wafNetworks) {
|
||||||
|
origins = append(origins, OriginItem{
|
||||||
|
IP: o.IP,
|
||||||
|
Weight: o.Weight,
|
||||||
|
Mode: o.Mode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return origins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getServerName получает имя сервера (l7ResourceName) через API
|
||||||
|
func getServerName(config Config, l7ResourceID int) (string, error) {
|
||||||
|
url := fmt.Sprintf("https://api.servicepipe.ru/api/v1/l7/resource/%d/global", l7ResourceID)
|
||||||
|
body, err := makeAPIRequest(url, config.APIToken)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var response ResourceResponse
|
||||||
|
if err := json.Unmarshal(body, &response); err != nil {
|
||||||
|
return "", fmt.Errorf("ошибка парсинга JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Data.Result.L7ResourceName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAliases получает список aliases через API
|
||||||
|
func getAliases(config Config, l7ResourceID int) ([]string, error) {
|
||||||
|
url := fmt.Sprintf("https://api.servicepipe.ru/api/v1/l7/alias/global?limit=1000&l7ResourceId=%d", l7ResourceID)
|
||||||
|
body, err := makeAPIRequest(url, config.APIToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var response AliasResponse
|
||||||
|
if err := json.Unmarshal(body, &response); err != nil {
|
||||||
|
return nil, fmt.Errorf("ошибка парсинга JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var aliases []string
|
||||||
|
for _, item := range response.Data.Result.Items {
|
||||||
|
aliases = append(aliases, item.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
return aliases, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getOrigins получает список origins через API, исключая WAF IP
|
||||||
|
func getOrigins(config Config, l7ResourceID int) ([]OriginItem, error) {
|
||||||
|
url := fmt.Sprintf("https://api.servicepipe.ru/api/v1/l7/origin/global?limit=1000&l7ResourceId=%d", l7ResourceID)
|
||||||
|
body, err := makeAPIRequest(url, config.APIToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var response OriginResponse
|
||||||
|
if err := json.Unmarshal(body, &response); err != nil {
|
||||||
|
return nil, fmt.Errorf("ошибка парсинга JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var origins []OriginItem
|
||||||
|
for _, item := range response.Data.Result.Items {
|
||||||
|
if !isIPInWAFNetworks(item.IP, config.WAFNetworks) {
|
||||||
|
origins = append(origins, OriginItem{
|
||||||
|
IP: item.IP,
|
||||||
|
Weight: item.Weight,
|
||||||
|
Mode: item.Mode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return origins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getInputHTTPPorts возвращает список HTTP INPUT портов из AppsSettings или дефолтные
|
||||||
|
func getInputHTTPPorts(settings AppsSettings) []int {
|
||||||
|
if len(settings.CustomInputHTTPPorts) > 0 {
|
||||||
|
return int64ArrayToIntSlice(settings.CustomInputHTTPPorts)
|
||||||
|
}
|
||||||
|
return []int{80}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getInputHTTPSPorts возвращает список HTTPS INPUT портов из AppsSettings или дефолтные
|
||||||
|
func getInputHTTPSPorts(settings AppsSettings) []int {
|
||||||
|
if len(settings.CustomInputHTTPSPorts) > 0 {
|
||||||
|
return int64ArrayToIntSlice(settings.CustomInputHTTPSPorts)
|
||||||
|
}
|
||||||
|
return []int{443}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getOutputHTTPPorts возвращает список HTTP OUTPUT портов из AppsSettings или дефолтные
|
||||||
|
func getOutputHTTPPorts(settings AppsSettings) []int {
|
||||||
|
if len(settings.CustomOutputHTTPPorts) > 0 {
|
||||||
|
return int64ArrayToIntSlice(settings.CustomOutputHTTPPorts)
|
||||||
|
}
|
||||||
|
return []int{80}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getOutputHTTPSPorts возвращает список HTTPS OUTPUT портов из AppsSettings или дефолтные
|
||||||
|
func getOutputHTTPSPorts(settings AppsSettings) []int {
|
||||||
|
if len(settings.CustomOutputHTTPSPorts) > 0 {
|
||||||
|
return int64ArrayToIntSlice(settings.CustomOutputHTTPSPorts)
|
||||||
|
}
|
||||||
|
return []int{443}
|
||||||
|
}
|
||||||
|
|
||||||
|
// int64ArrayToIntSlice конвертирует pq.Int64Array в []int
|
||||||
|
func int64ArrayToIntSlice(arr []int64) []int {
|
||||||
|
result := make([]int, len(arr))
|
||||||
|
for i, v := range arr {
|
||||||
|
result[i] = int(v)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeCustomUpstreamOrDefault записывает кастомные или дефолтные upstream-директивы
|
||||||
|
func writeCustomUpstreamOrDefault(w stringWriter, customUpstream sql.NullString, sni sql.NullInt64) {
|
||||||
|
hasCustomUpstream := customUpstream.Valid && customUpstream.String != ""
|
||||||
|
|
||||||
|
if hasCustomUpstream {
|
||||||
|
w.WriteString("\n # Custom upstream directives (replace defaults)\n")
|
||||||
|
lines := strings.Split(strings.TrimSpace(customUpstream.String), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed != "" {
|
||||||
|
w.WriteString(" " + trimmed + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Если нет custom - используем дефолтные keepalive директивы (только если sni != 1)
|
||||||
|
if !sni.Valid || sni.Int64 != 1 {
|
||||||
|
w.WriteString("\n # Default upstream directives\n")
|
||||||
|
w.WriteString(" keepalive 60;\n")
|
||||||
|
w.WriteString(" keepalive_timeout 70s;\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stringWriter — интерфейс для записи строк (совпадает с *os.File и *strings.Builder)
|
||||||
|
type stringWriter interface {
|
||||||
|
WriteString(s string) (int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getMaxFails возвращает max_fails из AppsSettings или дефолтное значение 5
|
||||||
|
func getMaxFails(settings AppsSettings) int {
|
||||||
|
if settings.MaxFails.Valid && settings.MaxFails.Int64 >= 0 {
|
||||||
|
return int(settings.MaxFails.Int64)
|
||||||
|
}
|
||||||
|
return 5 // дефолт
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFailTimeout возвращает fail_timeout из AppsSettings или дефолтное значение 15
|
||||||
|
func getFailTimeout(settings AppsSettings) int {
|
||||||
|
if settings.FailTimeout.Valid && settings.FailTimeout.Int64 >= 0 {
|
||||||
|
return int(settings.FailTimeout.Int64)
|
||||||
|
}
|
||||||
|
return 15 // дефолт
|
||||||
|
}
|
||||||
225
config.go
Normal file
225
config.go
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Константы для graceful shutdown
|
||||||
|
const (
|
||||||
|
// Время ожидания перед удалением контейнера при масштабировании вниз (в секундах)
|
||||||
|
// Должно быть больше чем keepalive_timeout в nginx (обычно 60-75 секунд)
|
||||||
|
ContainerDrainTimeout = 90
|
||||||
|
|
||||||
|
// Время ожидания перед удалением контейнера при миграции между хостами (в секундах)
|
||||||
|
// Увеличенное время для безопасного переключения трафика на новый хост
|
||||||
|
MigrationDrainTimeout = 3600 // 1 час
|
||||||
|
|
||||||
|
// Максимальное количество контейнеров на одного клиента
|
||||||
|
MaxContainersPerClient = 20
|
||||||
|
)
|
||||||
|
|
||||||
|
// Константы диапазонов портов для Docker
|
||||||
|
const (
|
||||||
|
HTTPPortStart = 18000 // Начало диапазона HTTP портов
|
||||||
|
HTTPPortEnd = 20999 // Конец диапазона HTTP портов (3000 портов)
|
||||||
|
HTTPSPortStart = 21000 // Начало диапазона HTTPS портов
|
||||||
|
HTTPSPortEnd = 23999 // Конец диапазона HTTPS портов (3000 портов)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Пути к файлам конфигурации
|
||||||
|
const (
|
||||||
|
EnvFilePath = "/etc/magos/magos.env"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Константы конфигурации по умолчанию (используются как fallback если env не загружен)
|
||||||
|
const (
|
||||||
|
DefaultDBName = "waf_info"
|
||||||
|
DefaultDBPort = 5432
|
||||||
|
DefaultPrimaryDBHost = "10.100.10.8"
|
||||||
|
DefaultSecondaryDBHost = "10.100.13.5"
|
||||||
|
DefaultDockerImage = "ptaf-core-nginx-agent:release-4.1.6.409465"
|
||||||
|
DefaultWorkerProcesses = 4
|
||||||
|
DefaultWorkerConnections = 64000
|
||||||
|
)
|
||||||
|
|
||||||
|
// Конфигурация приложения
|
||||||
|
type Config struct {
|
||||||
|
DBUser string
|
||||||
|
DBPassword string
|
||||||
|
DBName string
|
||||||
|
DBPort int
|
||||||
|
PrimaryDBHost string
|
||||||
|
SecondaryDBHost string
|
||||||
|
APIToken string
|
||||||
|
AntibotNetworks []string
|
||||||
|
WAFNetworks []string
|
||||||
|
WorkerProcesses int
|
||||||
|
WorkerConnections int
|
||||||
|
DockerImage string
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadEnvFile загружает переменные окружения из файла /etc/magos/magos.env
|
||||||
|
func loadEnvFile() error {
|
||||||
|
if err := godotenv.Load(EnvFilePath); err != nil {
|
||||||
|
return fmt.Errorf("ошибка загрузки %s: %w", EnvFilePath, err)
|
||||||
|
}
|
||||||
|
log.Printf("✓ Загружен файл конфигурации: %s", EnvFilePath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEnv возвращает значение переменной окружения или дефолтное значение
|
||||||
|
func getEnv(key, defaultValue string) string {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEnvInt возвращает int значение переменной окружения или дефолтное значение
|
||||||
|
func getEnvInt(key string, defaultValue int) int {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
if intVal, err := strconv.Atoi(value); err == nil {
|
||||||
|
return intVal
|
||||||
|
}
|
||||||
|
log.Printf("⚠ Некорректное значение %s=%s, используется дефолт %d", key, value, defaultValue)
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultConfig возвращает конфигурацию из env-файла или дефолтные значения
|
||||||
|
func getDefaultConfig() Config {
|
||||||
|
// Пытаемся загрузить env файл (не критично если не получится)
|
||||||
|
if err := loadEnvFile(); err != nil {
|
||||||
|
log.Printf("⚠ %v", err)
|
||||||
|
log.Printf(" Используются дефолтные значения для DB_USER, DB_PASSWORD, API_TOKEN")
|
||||||
|
}
|
||||||
|
|
||||||
|
return Config{
|
||||||
|
DBUser: getEnv("DB_USER", ""),
|
||||||
|
DBPassword: getEnv("DB_PASSWORD", ""),
|
||||||
|
DBName: getEnv("DB_NAME", DefaultDBName),
|
||||||
|
DBPort: getEnvInt("DB_PORT", DefaultDBPort),
|
||||||
|
PrimaryDBHost: getEnv("PRIMARY_DB_HOST", DefaultPrimaryDBHost),
|
||||||
|
SecondaryDBHost: getEnv("SECONDARY_DB_HOST", DefaultSecondaryDBHost),
|
||||||
|
APIToken: getEnv("API_TOKEN", ""),
|
||||||
|
AntibotNetworks: []string{
|
||||||
|
"185.66.85.0/24",
|
||||||
|
"185.66.86.0/24",
|
||||||
|
"185.35.5.0/24",
|
||||||
|
"185.35.6.0/24",
|
||||||
|
"212.67.26.0/24",
|
||||||
|
},
|
||||||
|
WAFNetworks: []string{
|
||||||
|
"109.238.89.0/24",
|
||||||
|
"89.20.63.0/24",
|
||||||
|
},
|
||||||
|
WorkerProcesses: getEnvInt("WORKER_PROCESSES", DefaultWorkerProcesses),
|
||||||
|
WorkerConnections: getEnvInt("WORKER_CONNECTIONS", DefaultWorkerConnections),
|
||||||
|
DockerImage: getEnv("DOCKER_IMAGE", DefaultDockerImage),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initConfig инициализирует конфигурацию, загружая IP списки из БД
|
||||||
|
func initConfig(db *sql.DB) (Config, error) {
|
||||||
|
// Читаем IP адреса и сети из таблицы ips
|
||||||
|
antibotNetworks, wafNetworks, err := loadIPsFromDB(db)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("⚠ Не удалось загрузить IP настройки из БД: %v", err)
|
||||||
|
log.Printf(" Используются значения по умолчанию")
|
||||||
|
return getDefaultConfig(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загружаем env файл для получения credentials
|
||||||
|
if err := loadEnvFile(); err != nil {
|
||||||
|
log.Printf("⚠ %v", err)
|
||||||
|
log.Printf(" Используются дефолтные значения для DB_USER, DB_PASSWORD, API_TOKEN")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Формируем Config с настройками из env + IP из БД
|
||||||
|
config := Config{
|
||||||
|
DBUser: getEnv("DB_USER", ""),
|
||||||
|
DBPassword: getEnv("DB_PASSWORD", ""),
|
||||||
|
DBName: getEnv("DB_NAME", DefaultDBName),
|
||||||
|
DBPort: getEnvInt("DB_PORT", DefaultDBPort),
|
||||||
|
PrimaryDBHost: getEnv("PRIMARY_DB_HOST", DefaultPrimaryDBHost),
|
||||||
|
SecondaryDBHost: getEnv("SECONDARY_DB_HOST", DefaultSecondaryDBHost),
|
||||||
|
APIToken: getEnv("API_TOKEN", ""),
|
||||||
|
DockerImage: getEnv("DOCKER_IMAGE", DefaultDockerImage),
|
||||||
|
WorkerProcesses: getEnvInt("WORKER_PROCESSES", DefaultWorkerProcesses),
|
||||||
|
WorkerConnections: getEnvInt("WORKER_CONNECTIONS", DefaultWorkerConnections),
|
||||||
|
AntibotNetworks: antibotNetworks,
|
||||||
|
WAFNetworks: wafNetworks,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Валидация критичных параметров
|
||||||
|
if config.DBUser == "" {
|
||||||
|
log.Printf("⚠ DB_USER не установлен в %s", EnvFilePath)
|
||||||
|
}
|
||||||
|
if config.DBPassword == "" {
|
||||||
|
log.Printf("⚠ DB_PASSWORD не установлен в %s", EnvFilePath)
|
||||||
|
}
|
||||||
|
if config.APIToken == "" {
|
||||||
|
log.Printf("⚠ API_TOKEN не установлен в %s", EnvFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("✓ IP конфигурация загружена из таблицы ips")
|
||||||
|
log.Printf(" - Antibot Networks: %d сетей", len(config.AntibotNetworks))
|
||||||
|
log.Printf(" - WAF Networks: %d сетей", len(config.WAFNetworks))
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadIPsFromDB загружает списки IP адресов и сетей из таблицы ips
|
||||||
|
func loadIPsFromDB(db *sql.DB) (antibotNetworks []string, wafNetworks []string, err error) {
|
||||||
|
query := `
|
||||||
|
SELECT antibot_networks, waf_networks
|
||||||
|
FROM ips
|
||||||
|
WHERE id = 1
|
||||||
|
`
|
||||||
|
|
||||||
|
// PostgreSQL массивы читаем через pq.Array
|
||||||
|
var antibotNetworksArray, wafNetworksArray []string
|
||||||
|
|
||||||
|
err = db.QueryRow(query).Scan(
|
||||||
|
pq.Array(&antibotNetworksArray),
|
||||||
|
pq.Array(&wafNetworksArray),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil, fmt.Errorf("записи с id=1 не найдено в таблице ips")
|
||||||
|
}
|
||||||
|
return nil, nil, fmt.Errorf("ошибка чтения из таблицы ips: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return antibotNetworksArray, wafNetworksArray, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isIPInWAFNetworks проверяет, входит ли IP адрес в одну из WAF подсетей
|
||||||
|
func isIPInWAFNetworks(ipStr string, wafNetworks []string) bool {
|
||||||
|
ip := net.ParseIP(ipStr)
|
||||||
|
if ip == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cidr := range wafNetworks {
|
||||||
|
_, ipNet, err := net.ParseCIDR(cidr)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ipNet.Contains(ip) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
194
containers_part1.go
Normal file
194
containers_part1.go
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupContainers(config Config, clientTitle string, clientInfo *ClientInfo, resources []ResourceData, resourcePortMap map[int][]PortMapping) error {
|
||||||
|
log.Printf("Настройка контейнеров для %s", clientTitle)
|
||||||
|
baseDir := filepath.Join("/home/install/ptaf", clientTitle)
|
||||||
|
containerChanges := make(map[int]bool)
|
||||||
|
composeChanges := make(map[int]bool)
|
||||||
|
|
||||||
|
for containerNum := 1; containerNum <= clientInfo.ContainersCount; containerNum++ {
|
||||||
|
containerName := fmt.Sprintf("%s-ptaf-agent%03d", clientTitle, containerNum)
|
||||||
|
containerDir := filepath.Join(baseDir, containerName)
|
||||||
|
log.Printf(" Обработка контейнера: %s", containerName)
|
||||||
|
|
||||||
|
dirs := []string{
|
||||||
|
containerDir,
|
||||||
|
filepath.Join("/home/install/conf/ptaf-nginx", clientTitle, fmt.Sprintf("ptaf-agent%03d", containerNum), "conf.d"),
|
||||||
|
}
|
||||||
|
for _, dir := range dirs {
|
||||||
|
existed := false
|
||||||
|
if _, err := os.Stat(dir); err == nil {
|
||||||
|
existed = true
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("ошибка создания директории %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
if !existed {
|
||||||
|
log.Printf(" ✓ Создана директория: %s", dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logDir := filepath.Join("/var/log/ptaf_nginx", clientTitle, fmt.Sprintf("ptaf-agent%03d", containerNum))
|
||||||
|
logDirExisted := false
|
||||||
|
if _, err := os.Stat(logDir); err == nil {
|
||||||
|
logDirExisted = true
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(logDir, 0777); err != nil {
|
||||||
|
return fmt.Errorf("ошибка создания директории логов %s: %w", logDir, err)
|
||||||
|
}
|
||||||
|
if err := os.Chmod(logDir, 0777); err != nil {
|
||||||
|
return fmt.Errorf("ошибка установки прав для директории логов %s: %w", logDir, err)
|
||||||
|
}
|
||||||
|
if !logDirExisted {
|
||||||
|
log.Printf(" ✓ Создана директория логов: %s (права: 0777)", logDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
nginxConfPath := filepath.Join("/home/install/conf/ptaf-nginx", clientTitle, fmt.Sprintf("ptaf-agent%03d", containerNum), "nginx.conf")
|
||||||
|
existingPodIP := ""
|
||||||
|
if info, err := os.Stat(nginxConfPath); err == nil {
|
||||||
|
if info.IsDir() {
|
||||||
|
log.Printf(" ⚠ nginx.conf является директорией, удаляем...")
|
||||||
|
os.RemoveAll(nginxConfPath)
|
||||||
|
} else {
|
||||||
|
existingPodIP = extractPodIPFromConfig(nginxConfPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ptafConfig := ""
|
||||||
|
if clientInfo.PTAFConfig.Valid {
|
||||||
|
ptafConfig = clientInfo.PTAFConfig.String
|
||||||
|
}
|
||||||
|
|
||||||
|
changed, err := generateNginxConf(nginxConfPath, config, ptafConfig, existingPodIP)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ошибка генерации nginx.conf: %w", err)
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
log.Printf(" ✓ nginx.conf изменен: %s", nginxConfPath)
|
||||||
|
containerChanges[containerNum] = true
|
||||||
|
} else {
|
||||||
|
log.Printf(" ○ nginx.conf не изменился: %s", nginxConfPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
mimeTypesPath := filepath.Join("/home/install/conf/ptaf-nginx", clientTitle, fmt.Sprintf("ptaf-agent%03d", containerNum), "mime.types")
|
||||||
|
if info, err := os.Stat(mimeTypesPath); err == nil {
|
||||||
|
if info.IsDir() {
|
||||||
|
log.Printf(" ⚠ mime.types является директорией, удаляем...")
|
||||||
|
os.RemoveAll(mimeTypesPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changed, err = generateMimeTypes(mimeTypesPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ошибка генерации mime.types: %w", err)
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
log.Printf(" ✓ mime.types изменен: %s", mimeTypesPath)
|
||||||
|
containerChanges[containerNum] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
confDPath := filepath.Join("/home/install/conf/ptaf-nginx", clientTitle, fmt.Sprintf("ptaf-agent%03d", containerNum), "conf.d")
|
||||||
|
containerPorts := []PortMapping{}
|
||||||
|
for _, res := range resources {
|
||||||
|
ports, ok := resourcePortMap[res.L7ResourceID]
|
||||||
|
if !ok || containerNum-1 >= len(ports) {
|
||||||
|
return fmt.Errorf("не найдены порты для l7ResourceID %d контейнер %d", res.L7ResourceID, containerNum)
|
||||||
|
}
|
||||||
|
containerPorts = append(containerPorts, ports[containerNum-1])
|
||||||
|
confFile := filepath.Join(confDPath, fmt.Sprintf("%s_%d.conf", clientTitle, res.L7ResourceID))
|
||||||
|
oldHash := ""
|
||||||
|
if _, err := os.Stat(confFile); err == nil {
|
||||||
|
oldHash, _ = getFileHash(confFile)
|
||||||
|
}
|
||||||
|
err := generateNginxResourceConfig(confFile, config, res, ports[containerNum-1])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ошибка генерации конфига для ресурса: %w", err)
|
||||||
|
}
|
||||||
|
newHash, _ := getFileHash(confFile)
|
||||||
|
if oldHash != "" && oldHash != newHash {
|
||||||
|
log.Printf(" ✓ Конфиг ресурса изменен: %s", confFile)
|
||||||
|
containerChanges[containerNum] = true
|
||||||
|
} else if oldHash == "" {
|
||||||
|
log.Printf(" ✓ Создан конфиг ресурса: %s", confFile)
|
||||||
|
containerChanges[containerNum] = true
|
||||||
|
} else {
|
||||||
|
log.Printf(" ○ Конфиг ресурса не изменился: %s", confFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeFile := filepath.Join(containerDir, "docker-compose.yml")
|
||||||
|
log.Printf(" → DEBUG порты контейнера %d:", containerNum)
|
||||||
|
for idx, pm := range containerPorts {
|
||||||
|
log.Printf(" PortMapping[%d]: HTTP=%v, HTTPS=%v", idx, pm.HTTPPorts, pm.HTTPSPorts)
|
||||||
|
}
|
||||||
|
changed, err = generateDockerCompose(composeFile, config, clientTitle, clientInfo, containerNum, resources, containerPorts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ошибка генерации docker-compose.yml: %w", err)
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
log.Printf(" ✓ docker-compose.yml изменен: %s", composeFile)
|
||||||
|
composeChanges[containerNum] = true
|
||||||
|
}
|
||||||
|
if err := saveContainerPorts(containerDir, containerNum, resources, containerPorts); err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка сохранения портов: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf(" Запуск и обновление контейнеров для %s", clientTitle)
|
||||||
|
for containerNum := 1; containerNum <= clientInfo.ContainersCount; containerNum++ {
|
||||||
|
containerName := fmt.Sprintf("%s-ptaf-agent%03d", clientTitle, containerNum)
|
||||||
|
containerDir := filepath.Join(baseDir, containerName)
|
||||||
|
composeFile := filepath.Join(containerDir, "docker-compose.yml")
|
||||||
|
containerFullName := fmt.Sprintf("ptaf_%s_%03d", clientTitle, containerNum)
|
||||||
|
containerExists, containerRunning := checkContainerStatus(containerFullName)
|
||||||
|
|
||||||
|
if containerExists {
|
||||||
|
if composeChanges[containerNum] {
|
||||||
|
recreateContainer(containerFullName, composeFile, containerDir, clientTitle, resources, resourcePortMap, containerNum)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !containerRunning {
|
||||||
|
startContainer(containerFullName, composeFile, containerDir)
|
||||||
|
}
|
||||||
|
if containerChanges[containerNum] {
|
||||||
|
log.Printf(" Конфигурация изменилась, проверяем и перезагружаем nginx в %s", containerFullName)
|
||||||
|
if err := testNginxConfig(containerFullName); err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка проверки конфигурации в %s: %v", containerFullName, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := reloadNginx(containerFullName); err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка перезагрузки nginx в %s: %v", containerFullName, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf(" ✓ Nginx успешно перезагружен в контейнере %s", containerFullName)
|
||||||
|
} else {
|
||||||
|
log.Printf(" ○ Конфигурация не изменилась, проверка и перезагрузка не требуются для %s", containerFullName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf(" Запуск нового контейнера %s", containerName)
|
||||||
|
cmd := exec.Command("docker-compose", "-f", composeFile, "up", "-d")
|
||||||
|
cmd.Dir = containerDir
|
||||||
|
output, _ := cmd.CombinedOutput()
|
||||||
|
log.Printf(" → Вывод docker-compose: %s", string(output))
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
_, isRunning := checkContainerStatus(containerFullName)
|
||||||
|
if !isRunning {
|
||||||
|
log.Printf(" ⚠ Контейнер %s не запустился. Попробуйте вручную: cd %s && docker-compose up -d", containerFullName, containerDir)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf(" ✓ Контейнер %s успешно запущен", containerName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf(" Проверка лишних контейнеров для %s", clientTitle)
|
||||||
|
processDrainingContainers(clientTitle, clientInfo.ContainersCount)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
676
containers_part2.go
Normal file
676
containers_part2.go
Normal file
|
|
@ -0,0 +1,676 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func recreateContainer(containerFullName, composeFile, containerDir, clientTitle string, resources []ResourceData, resourcePortMap map[int][]PortMapping, containerNum int) {
|
||||||
|
log.Printf(" docker-compose.yml изменился, пересоздаём контейнер %s...", containerFullName)
|
||||||
|
portsToFree := make([]int, 0)
|
||||||
|
for _, res := range resources {
|
||||||
|
if containerPorts, ok := resourcePortMap[res.L7ResourceID]; ok {
|
||||||
|
if containerNum-1 < len(containerPorts) {
|
||||||
|
for _, dockerPort := range containerPorts[containerNum-1].HTTPPorts {
|
||||||
|
portsToFree = append(portsToFree, dockerPort)
|
||||||
|
}
|
||||||
|
for _, dockerPort := range containerPorts[containerNum-1].HTTPSPorts {
|
||||||
|
portsToFree = append(portsToFree, dockerPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf(" → Останавливаем старый контейнер...")
|
||||||
|
cmd := exec.Command("docker-compose", "-f", composeFile, "down")
|
||||||
|
cmd.Dir = containerDir
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка остановки контейнера %s: %v, output: %s", containerFullName, err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf(" → Ожидаем остановки контейнера...")
|
||||||
|
containerStillExists := false
|
||||||
|
for i := 0; i < 30; i++ {
|
||||||
|
exists, _ := checkContainerStatus(containerFullName)
|
||||||
|
if !exists {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
containerStillExists = true
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
if containerStillExists {
|
||||||
|
exists, _ := checkContainerStatus(containerFullName)
|
||||||
|
if exists {
|
||||||
|
log.Printf(" → Контейнер не остановился, принудительное удаление...")
|
||||||
|
if err := forceRemoveContainer(containerFullName); err != nil {
|
||||||
|
log.Printf(" ⚠ Не удалось принудительно удалить контейнер: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(portsToFree) > 0 {
|
||||||
|
log.Printf(" → Ожидаем освобождения %d портов...", len(portsToFree))
|
||||||
|
allPortsFree := false
|
||||||
|
for attempt := 0; attempt < 180; attempt++ { // Увеличено с 60 до 180 секунд
|
||||||
|
usedPorts, err := getUsedPorts()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка проверки портов: %v", err)
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
portsBusy := 0
|
||||||
|
busyPortsList := make([]int, 0)
|
||||||
|
for _, port := range portsToFree {
|
||||||
|
if usedPorts[port] {
|
||||||
|
portsBusy++
|
||||||
|
busyPortsList = append(busyPortsList, port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if portsBusy == 0 {
|
||||||
|
allPortsFree = true
|
||||||
|
log.Printf(" ✓ Все порты освобождены (попытка %d)", attempt+1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if attempt%5 == 0 && attempt > 0 {
|
||||||
|
log.Printf(" → Ещё заняты %d портов: %v (попытка %d/180)", portsBusy, busyPortsList, attempt+1) // Обновлено с 60 на 180
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
if !allPortsFree {
|
||||||
|
log.Printf(" ⚠ Порты не освободились за 180 секунд!") // Обновлено с 60 на 180
|
||||||
|
log.Printf(" → Проверьте вручную: sudo lsof -i :%d", portsToFree[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
log.Printf(" → Запускаем новый контейнер...")
|
||||||
|
cmd = exec.Command("docker-compose", "-f", composeFile, "up", "-d")
|
||||||
|
cmd.Dir = containerDir
|
||||||
|
output, err = cmd.CombinedOutput()
|
||||||
|
log.Printf(" → Вывод docker-compose: %s", string(output))
|
||||||
|
if err != nil && strings.Contains(string(output), "address already in use") {
|
||||||
|
log.Printf(" ⚠ Порт всё ещё занят после ожидания освобождения!")
|
||||||
|
log.Printf(" → Проверьте: docker ps -a | grep %s", clientTitle)
|
||||||
|
log.Printf(" → Найдите процесс: sudo lsof -i | grep LISTEN | grep 127.0.0.1")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
_, isRunning := checkContainerStatus(containerFullName)
|
||||||
|
if !isRunning {
|
||||||
|
log.Printf(" ⚠ Контейнер %s не запустился. Попробуйте вручную: cd %s && docker-compose up -d", containerFullName, containerDir)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf(" ✓ Контейнер %s пересоздан с новыми портами", containerFullName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startContainer(containerFullName, composeFile, containerDir string) {
|
||||||
|
log.Printf(" Контейнер %s остановлен, запускаем...", containerFullName)
|
||||||
|
cmd := exec.Command("docker-compose", "-f", composeFile, "up", "-d")
|
||||||
|
cmd.Dir = containerDir
|
||||||
|
output, _ := cmd.CombinedOutput()
|
||||||
|
log.Printf(" → Вывод docker-compose: %s", string(output))
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
_, isRunning := checkContainerStatus(containerFullName)
|
||||||
|
if !isRunning {
|
||||||
|
log.Printf(" ⚠ Контейнер %s не запустился. Попробуйте вручную: cd %s && docker-compose up -d", containerFullName, containerDir)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf(" ✓ Контейнер %s запущен", containerFullName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkContainerStatus(containerName string) (exists bool, running bool) {
|
||||||
|
cmd := exec.Command("docker", "ps", "-a", "--filter", fmt.Sprintf("name=^%s$", containerName), "--format", "{{.Names}}")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(string(output)) == "" {
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
cmd = exec.Command("docker", "ps", "--filter", fmt.Sprintf("name=^%s$", containerName), "--format", "{{.Names}}")
|
||||||
|
output, err = cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return true, false
|
||||||
|
}
|
||||||
|
running = strings.TrimSpace(string(output)) != ""
|
||||||
|
return true, running
|
||||||
|
}
|
||||||
|
|
||||||
|
func forceRemoveContainer(containerName string) error {
|
||||||
|
cmd := exec.Command("docker", "stop", "-t", "10", containerName)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка остановки контейнера: %v, output: %s", err, string(output))
|
||||||
|
}
|
||||||
|
cmd = exec.Command("docker", "rm", "-f", containerName)
|
||||||
|
output, err = cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ошибка удаления контейнера: %w, output: %s", err, string(output))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNginxConfig(containerName string) error {
|
||||||
|
log.Printf(" Проверка конфигурации nginx в контейнере %s", containerName)
|
||||||
|
cmd := exec.Command("docker", "exec", containerName, "/opt/ptaf/bin/ptaf-nginx", "-t", "-c", "/opt/ptaf/conf/nginx.conf")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("конфигурация невалидна: %w, output: %s", err, string(output))
|
||||||
|
}
|
||||||
|
log.Printf(" Конфигурация валидна: %s", strings.TrimSpace(string(output)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func reloadNginx(containerName string) error {
|
||||||
|
log.Printf(" Перезагрузка nginx в контейнере %s", containerName)
|
||||||
|
cmd := exec.Command("docker", "exec", containerName, "/opt/ptaf/bin/ptaf-nginx", "-s", "reload")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ошибка перезагрузки: %w, output: %s", err, string(output))
|
||||||
|
}
|
||||||
|
log.Printf(" Nginx перезагружен успешно")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func markContainerForDrain(containerName string, isMigration bool) error {
|
||||||
|
timestamp := time.Now().Unix()
|
||||||
|
markerFile := fmt.Sprintf("/tmp/ptaf-drain-%s", containerName)
|
||||||
|
content := fmt.Sprintf("%d,%t", timestamp, isMigration)
|
||||||
|
return os.WriteFile(markerFile, []byte(content), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getContainerDrainInfo(containerName string) (timestamp int64, isMigration bool, found bool) {
|
||||||
|
markerFile := fmt.Sprintf("/tmp/ptaf-drain-%s", containerName)
|
||||||
|
data, err := os.ReadFile(markerFile)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false, false
|
||||||
|
}
|
||||||
|
content := strings.TrimSpace(string(data))
|
||||||
|
parts := strings.Split(content, ",")
|
||||||
|
if len(parts) < 1 {
|
||||||
|
return 0, false, false
|
||||||
|
}
|
||||||
|
ts, err := strconv.ParseInt(parts[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false, false
|
||||||
|
}
|
||||||
|
migration := false
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
migration = parts[1] == "true"
|
||||||
|
}
|
||||||
|
return ts, migration, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func markAngieConfigForDrain(clientTitle string) error {
|
||||||
|
markerFile := fmt.Sprintf("/tmp/ptaf-angie-drain-%s", clientTitle)
|
||||||
|
return os.WriteFile(markerFile, []byte(fmt.Sprintf("%d", time.Now().Unix())), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAngieConfigDrainTimestamp(clientTitle string) (int64, bool) {
|
||||||
|
markerFile := fmt.Sprintf("/tmp/ptaf-angie-drain-%s", clientTitle)
|
||||||
|
data, err := os.ReadFile(markerFile)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
timestamp, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return timestamp, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopAndRemoveContainer(containerName, composeFile, containerDir string) error {
|
||||||
|
log.Printf(" Остановка контейнера %s", containerName)
|
||||||
|
cmd := exec.Command("docker-compose", "-f", composeFile, "down", "-v")
|
||||||
|
cmd.Dir = containerDir
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ошибка остановки контейнера: %w, output: %s", err, string(output))
|
||||||
|
}
|
||||||
|
os.Remove(fmt.Sprintf("/tmp/ptaf-drain-%s", containerName))
|
||||||
|
log.Printf(" ✓ Контейнер %s остановлен и удалён", containerName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeContainerFiles(clientTitle string, containerNum int) error {
|
||||||
|
baseDir := filepath.Join("/home/install/ptaf", clientTitle)
|
||||||
|
containerName := fmt.Sprintf("%s-ptaf-agent%03d", clientTitle, containerNum)
|
||||||
|
for _, dir := range []string{
|
||||||
|
filepath.Join(baseDir, containerName),
|
||||||
|
filepath.Join("/home/install/conf/ptaf-nginx", clientTitle, fmt.Sprintf("ptaf-agent%03d", containerNum)),
|
||||||
|
filepath.Join("/var/log/ptaf_nginx", clientTitle, fmt.Sprintf("ptaf-agent%03d", containerNum)),
|
||||||
|
} {
|
||||||
|
if _, err := os.Stat(dir); err == nil {
|
||||||
|
log.Printf(" Удаление директории: %s", dir)
|
||||||
|
if err := os.RemoveAll(dir); err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка удаления %s: %v", dir, err)
|
||||||
|
} else {
|
||||||
|
log.Printf(" ✓ Удалена директория: %s", dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupClientDirectoryIfEmpty(clientTitle string) error {
|
||||||
|
clientConfDir := fmt.Sprintf("/home/install/conf/ptaf-nginx/%s", clientTitle)
|
||||||
|
if _, err := os.Stat(clientConfDir); os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
entries, err := os.ReadDir(clientConfDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ошибка чтения директории %s: %w", clientConfDir, err)
|
||||||
|
}
|
||||||
|
hasContainers := false
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() && strings.HasPrefix(entry.Name(), "ptaf-agent") {
|
||||||
|
hasContainers = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasContainers {
|
||||||
|
log.Printf(" ✓ У клиента %s не осталось контейнеров, удаляем директории клиента", clientTitle)
|
||||||
|
os.RemoveAll(clientConfDir)
|
||||||
|
ptafDir := fmt.Sprintf("/home/install/ptaf/%s", clientTitle)
|
||||||
|
if _, err := os.Stat(ptafDir); err == nil {
|
||||||
|
os.RemoveAll(ptafDir)
|
||||||
|
}
|
||||||
|
hashFile := fmt.Sprintf("/etc/angie/http.d/angie-ptaf-%s.conf.hash", clientTitle)
|
||||||
|
if _, err := os.Stat(hashFile); err == nil {
|
||||||
|
os.Remove(hashFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func reloadAngie() {
|
||||||
|
log.Printf(" Перезагрузка Angie...")
|
||||||
|
cmd := exec.Command("systemctl", "reload", "angie")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка перезагрузки Angie: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf(" ✓ Angie перезагружен")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateNginxConf генерирует основной nginx.conf для PTAF
|
||||||
|
func generateNginxConf(filePath string, config Config, ptafConfig string, existingPodIP string) (bool, error) {
|
||||||
|
var randomIP string
|
||||||
|
if existingPodIP != "" {
|
||||||
|
randomIP = existingPodIP
|
||||||
|
log.Printf(" → Используется существующий POD_IP: %s", randomIP)
|
||||||
|
} else {
|
||||||
|
randomIP = fmt.Sprintf("135.%d.%d.%d",
|
||||||
|
rand.Intn(254)+1,
|
||||||
|
rand.Intn(254)+1,
|
||||||
|
rand.Intn(254)+1)
|
||||||
|
log.Printf(" → Сгенерирован новый POD_IP: %s", randomIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
pathParts := strings.Split(filePath, "/")
|
||||||
|
clientTitle := ""
|
||||||
|
containerNum := "001"
|
||||||
|
for i, part := range pathParts {
|
||||||
|
if part == "ptaf-nginx" && i+1 < len(pathParts) {
|
||||||
|
clientTitle = pathParts[i+1]
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(part, "ptaf-agent") {
|
||||||
|
containerNum = strings.TrimPrefix(part, "ptaf-agent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hostname := fmt.Sprintf("ptaf-%s-agent%s", clientTitle, containerNum)
|
||||||
|
log.Printf(" → clientTitle=%s, containerNum=%s, hostname=%s", clientTitle, containerNum, hostname)
|
||||||
|
|
||||||
|
content := fmt.Sprintf(`#user ptaf;
|
||||||
|
master_process on;
|
||||||
|
worker_processes %d;
|
||||||
|
worker_rlimit_nofile 1048576;
|
||||||
|
daemon off;
|
||||||
|
load_module /opt/ptaf/lib/ngx_wrapper.so;
|
||||||
|
env POD_IP=%s;
|
||||||
|
env PTAF_LOG_LEVEL=INFO;
|
||||||
|
env DEBUG=false;
|
||||||
|
env HOSTNAME=%s;
|
||||||
|
error_log stderr notice;
|
||||||
|
pid /opt/ptaf/logs/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections %d;
|
||||||
|
accept_mutex off;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /opt/ptaf/conf/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format waf escape=json
|
||||||
|
'{'
|
||||||
|
'"SID":$http_x_sid,'
|
||||||
|
'"server_name":"$server_name",'
|
||||||
|
'"app_name":"$host",'
|
||||||
|
'"proxyed_to":"$upstream_addr",'
|
||||||
|
'"upstream_status":"$upstream_status",'
|
||||||
|
'"upstream_response_length":"$upstream_response_length",'
|
||||||
|
'"upstream_response_time":"$upstream_response_time",'
|
||||||
|
'"upstream_bytes_sent":"$upstream_bytes_sent",'
|
||||||
|
'"server_port":$server_port,'
|
||||||
|
'"server_addr":"$server_addr",'
|
||||||
|
'"server_protocol":"$server_protocol",'
|
||||||
|
'"remote_addr":"$remote_addr",'
|
||||||
|
'"response_status_code":$status,'
|
||||||
|
'"response_body_bytes_sent":$body_bytes_sent,'
|
||||||
|
'"http_x_forwarded_for":"$http_x_forwarded_for",'
|
||||||
|
'"uri":"$uri",'
|
||||||
|
'"http_user_agent":"$http_user_agent",'
|
||||||
|
'"query_string":"$query_string",'
|
||||||
|
'"request_method":"$request_method",'
|
||||||
|
'"request_time":$request_time,'
|
||||||
|
'"@timestamp":"$time_iso8601"'
|
||||||
|
'}';
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
log_format main_upstream '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for" $upstream_response_time';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/%s_access.log waf;
|
||||||
|
|
||||||
|
client_max_body_size 0;
|
||||||
|
sendfile on;
|
||||||
|
gzip on;
|
||||||
|
server_names_hash_bucket_size 512;
|
||||||
|
server_tokens off;
|
||||||
|
ptaf_fallback 503;
|
||||||
|
proxy_read_timeout 120s;
|
||||||
|
proxy_send_timeout 120s;
|
||||||
|
send_timeout 60s;
|
||||||
|
keepalive_timeout 80s;
|
||||||
|
keepalive_requests 1000;
|
||||||
|
proxy_buffer_size 8k;
|
||||||
|
proxy_buffers 8 8k;
|
||||||
|
proxy_busy_buffers_size 16k;
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
resolver_timeout 30s;
|
||||||
|
|
||||||
|
geo $dollar {
|
||||||
|
default "$";
|
||||||
|
}
|
||||||
|
|
||||||
|
map $http_upgrade $proxy_http_connection {
|
||||||
|
default upgrade;
|
||||||
|
'' '';
|
||||||
|
}
|
||||||
|
|
||||||
|
# Map для условного Connection header (используется в server блоках)
|
||||||
|
map $http_upgrade $proxy_connection {
|
||||||
|
default upgrade;
|
||||||
|
'' '';
|
||||||
|
}
|
||||||
|
|
||||||
|
# Map для определения backend host из X-Original-Host заголовка
|
||||||
|
map $http_x_original_host $backend_host {
|
||||||
|
"" $host;
|
||||||
|
default $http_x_original_host;
|
||||||
|
}
|
||||||
|
|
||||||
|
ptaf_config %s;
|
||||||
|
|
||||||
|
include /opt/ptaf/conf/conf.d/*.conf;
|
||||||
|
}
|
||||||
|
`, config.WorkerProcesses, randomIP, hostname, config.WorkerConnections, clientTitle, ptafConfig)
|
||||||
|
|
||||||
|
return writeFileIfChanged(filePath, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateMimeTypes генерирует файл mime.types
|
||||||
|
func generateMimeTypes(filePath string) (bool, error) {
|
||||||
|
content := `types {
|
||||||
|
text/html html htm shtml;
|
||||||
|
text/css css;
|
||||||
|
text/xml xml;
|
||||||
|
image/gif gif;
|
||||||
|
image/jpeg jpeg jpg;
|
||||||
|
application/javascript js;
|
||||||
|
application/atom+xml atom;
|
||||||
|
application/rss+xml rss;
|
||||||
|
|
||||||
|
text/mathml mml;
|
||||||
|
text/plain txt;
|
||||||
|
text/vnd.sun.j2me.app-descriptor jad;
|
||||||
|
text/vnd.wap.wml wml;
|
||||||
|
text/x-component htc;
|
||||||
|
|
||||||
|
image/avif avif;
|
||||||
|
image/png png;
|
||||||
|
image/svg+xml svg svgz;
|
||||||
|
image/tiff tif tiff;
|
||||||
|
image/vnd.wap.wbmp wbmp;
|
||||||
|
image/webp webp;
|
||||||
|
image/x-icon ico;
|
||||||
|
image/x-jng jng;
|
||||||
|
image/x-ms-bmp bmp;
|
||||||
|
|
||||||
|
font/woff woff;
|
||||||
|
font/woff2 woff2;
|
||||||
|
|
||||||
|
application/java-archive jar war ear;
|
||||||
|
application/json json;
|
||||||
|
application/mac-binhex40 hqx;
|
||||||
|
application/msword doc;
|
||||||
|
application/pdf pdf;
|
||||||
|
application/postscript ps eps ai;
|
||||||
|
application/rtf rtf;
|
||||||
|
application/vnd.apple.mpegurl m3u8;
|
||||||
|
application/vnd.google-earth.kml+xml kml;
|
||||||
|
application/vnd.google-earth.kmz kmz;
|
||||||
|
application/vnd.ms-excel xls;
|
||||||
|
application/vnd.ms-fontobject eot;
|
||||||
|
application/vnd.ms-powerpoint ppt;
|
||||||
|
application/vnd.oasis.opendocument.graphics odg;
|
||||||
|
application/vnd.oasis.opendocument.presentation odp;
|
||||||
|
application/vnd.oasis.opendocument.spreadsheet ods;
|
||||||
|
application/vnd.oasis.opendocument.text odt;
|
||||||
|
application/vnd.openxmlformats-officedocument.presentationml.presentation pptx;
|
||||||
|
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx;
|
||||||
|
application/vnd.openxmlformats-officedocument.wordprocessingml.document docx;
|
||||||
|
application/vnd.wap.wmlc wmlc;
|
||||||
|
application/wasm wasm;
|
||||||
|
application/x-7z-compressed 7z;
|
||||||
|
application/x-cocoa cco;
|
||||||
|
application/x-java-archive-diff jardiff;
|
||||||
|
application/x-java-jnlp-file jnlp;
|
||||||
|
application/x-makeself run;
|
||||||
|
application/x-perl pl pm;
|
||||||
|
application/x-pilot prc pdb;
|
||||||
|
application/x-rar-compressed rar;
|
||||||
|
application/x-redhat-package-manager rpm;
|
||||||
|
application/x-sea sea;
|
||||||
|
application/x-shockwave-flash swf;
|
||||||
|
application/x-stuffit sit;
|
||||||
|
application/x-tcl tcl tk;
|
||||||
|
application/x-x509-ca-cert der pem crt;
|
||||||
|
application/x-xpinstall xpi;
|
||||||
|
application/xhtml+xml xhtml;
|
||||||
|
application/xspf+xml xspf;
|
||||||
|
application/zip zip;
|
||||||
|
|
||||||
|
application/octet-stream bin exe dll;
|
||||||
|
application/octet-stream deb;
|
||||||
|
application/octet-stream dmg;
|
||||||
|
application/octet-stream iso img;
|
||||||
|
application/octet-stream msi msp msm;
|
||||||
|
|
||||||
|
audio/midi mid midi kar;
|
||||||
|
audio/mpeg mp3;
|
||||||
|
audio/ogg ogg;
|
||||||
|
audio/x-m4a m4a;
|
||||||
|
audio/x-realaudio ra;
|
||||||
|
|
||||||
|
video/3gpp 3gpp 3gp;
|
||||||
|
video/mp2t ts;
|
||||||
|
video/mp4 mp4;
|
||||||
|
video/mpeg mpeg mpg;
|
||||||
|
video/quicktime mov;
|
||||||
|
video/webm webm;
|
||||||
|
video/x-flv flv;
|
||||||
|
video/x-m4v m4v;
|
||||||
|
video/x-mng mng;
|
||||||
|
video/x-ms-asf asx asf;
|
||||||
|
video/x-ms-wmv wmv;
|
||||||
|
video/x-msvideo avi;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
return writeFileIfChanged(filePath, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateDockerCompose генерирует docker-compose.yml файл
|
||||||
|
func generateDockerCompose(filePath string, config Config, clientTitle string, clientInfo *ClientInfo, containerNum int, resources []ResourceData, portMappings []PortMapping) (bool, error) {
|
||||||
|
type ComposeData struct {
|
||||||
|
ClientTitle string
|
||||||
|
ContainerNum int
|
||||||
|
DockerImage string
|
||||||
|
PTAFConfig string
|
||||||
|
WorkerProcesses int
|
||||||
|
WorkerConnections int
|
||||||
|
FluentBitEnabled bool
|
||||||
|
FluentBitPort int
|
||||||
|
HasMTLS bool
|
||||||
|
SortedHTTPPorts []SortedPortMapping
|
||||||
|
SortedHTTPSPorts []SortedPortMapping
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMTLS := false
|
||||||
|
for _, res := range resources {
|
||||||
|
if res.AppsSettings.LocationNginxCustom.Valid && hasMTLSCertificates(res.AppsSettings.LocationNginxCustom.String) {
|
||||||
|
hasMTLS = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if res.AppsSettings.ServerNginxCustom.Valid && hasMTLSCertificates(res.AppsSettings.ServerNginxCustom.String) {
|
||||||
|
hasMTLS = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var allHTTPPorts []SortedPortMapping
|
||||||
|
var allHTTPSPorts []SortedPortMapping
|
||||||
|
|
||||||
|
for _, pm := range portMappings {
|
||||||
|
var httpKeys []int
|
||||||
|
for customPort := range pm.HTTPPorts {
|
||||||
|
httpKeys = append(httpKeys, customPort)
|
||||||
|
}
|
||||||
|
sort.Ints(httpKeys)
|
||||||
|
for _, customPort := range httpKeys {
|
||||||
|
allHTTPPorts = append(allHTTPPorts, SortedPortMapping{CustomPort: customPort, DockerPort: pm.HTTPPorts[customPort]})
|
||||||
|
}
|
||||||
|
|
||||||
|
var httpsKeys []int
|
||||||
|
for customPort := range pm.HTTPSPorts {
|
||||||
|
httpsKeys = append(httpsKeys, customPort)
|
||||||
|
}
|
||||||
|
sort.Ints(httpsKeys)
|
||||||
|
for _, customPort := range httpsKeys {
|
||||||
|
allHTTPSPorts = append(allHTTPSPorts, SortedPortMapping{CustomPort: customPort, DockerPort: pm.HTTPSPorts[customPort]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(allHTTPPorts, func(i, j int) bool { return allHTTPPorts[i].DockerPort < allHTTPPorts[j].DockerPort })
|
||||||
|
sort.Slice(allHTTPSPorts, func(i, j int) bool { return allHTTPSPorts[i].DockerPort < allHTTPSPorts[j].DockerPort })
|
||||||
|
|
||||||
|
data := ComposeData{
|
||||||
|
ClientTitle: clientTitle,
|
||||||
|
ContainerNum: containerNum,
|
||||||
|
DockerImage: config.DockerImage,
|
||||||
|
PTAFConfig: "",
|
||||||
|
WorkerProcesses: config.WorkerProcesses,
|
||||||
|
WorkerConnections: config.WorkerConnections,
|
||||||
|
FluentBitEnabled: false,
|
||||||
|
HasMTLS: hasMTLS,
|
||||||
|
SortedHTTPPorts: allHTTPPorts,
|
||||||
|
SortedHTTPSPorts: allHTTPSPorts,
|
||||||
|
}
|
||||||
|
|
||||||
|
if clientInfo.PTAFConfig.Valid {
|
||||||
|
data.PTAFConfig = clientInfo.PTAFConfig.String
|
||||||
|
}
|
||||||
|
if clientInfo.FluentBitPort.Valid {
|
||||||
|
data.FluentBitEnabled = true
|
||||||
|
data.FluentBitPort = int(clientInfo.FluentBitPort.Int64)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := `services:
|
||||||
|
ptaf-{{.ClientTitle}}-agent{{printf "%03d" .ContainerNum}}:
|
||||||
|
image: '{{.DockerImage}}'
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: '1gb'
|
||||||
|
container_name: ptaf_{{.ClientTitle}}_{{printf "%03d" .ContainerNum}}
|
||||||
|
hostname: ptaf_{{.ClientTitle}}_{{printf "%03d" .ContainerNum}}
|
||||||
|
environment:
|
||||||
|
- CONNECTION_STRING={{.PTAFConfig}}
|
||||||
|
- WORKER_PROCESSES={{.WorkerProcesses}}
|
||||||
|
- WORKER_CONNECTIONS={{.WorkerConnections}}
|
||||||
|
- INCLUDE_CUSTOM_CONFIG=True
|
||||||
|
- PTAF_CONFIGURATOR_DISABLED=1
|
||||||
|
{{- if .FluentBitEnabled}}
|
||||||
|
- EVENT_SENDER_ENABLED=1
|
||||||
|
- EVENT_SENDER_HOST=172.17.0.1
|
||||||
|
- EVENT_SENDER_PORT={{.FluentBitPort}}
|
||||||
|
- EVENT_SENDER_TIMEOUT=3
|
||||||
|
- EVENT_WEB_UI_URL=https://af.ptcloud.ru/events/attacks/%s/general/
|
||||||
|
{{- end}}
|
||||||
|
sysctls:
|
||||||
|
kernel.sem: "32000 1024000000 500 32000"
|
||||||
|
ports:
|
||||||
|
{{- range .SortedHTTPPorts}}
|
||||||
|
- 127.0.0.1:{{.DockerPort}}:{{.CustomPort}}/tcp
|
||||||
|
{{- end}}
|
||||||
|
{{- range .SortedHTTPSPorts}}
|
||||||
|
- 127.0.0.1:{{.DockerPort}}:{{.CustomPort}}/tcp
|
||||||
|
{{- end}}
|
||||||
|
volumes:
|
||||||
|
- /var/log/ptaf_nginx/{{.ClientTitle}}/ptaf-agent{{printf "%03d" .ContainerNum}}:/var/log/nginx
|
||||||
|
- /home/install/conf/ptaf-nginx/{{.ClientTitle}}/ptaf-agent{{printf "%03d" .ContainerNum}}/conf.d:/opt/ptaf/conf/conf.d
|
||||||
|
- /home/install/conf/ptaf-nginx/{{.ClientTitle}}/ptaf-agent{{printf "%03d" .ContainerNum}}/nginx.conf:/opt/ptaf/conf/nginx.conf
|
||||||
|
- /home/install/conf/ptaf-nginx/{{.ClientTitle}}/ptaf-agent{{printf "%03d" .ContainerNum}}/mime.types:/opt/ptaf/conf/mime.types
|
||||||
|
{{- if .HasMTLS}}
|
||||||
|
- /etc/ssl/mtls:/opt/ptaf/conf/main
|
||||||
|
{{- end}}
|
||||||
|
networks:
|
||||||
|
- ptaf-{{.ClientTitle}}-net
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "100m"
|
||||||
|
max-file: "5"
|
||||||
|
networks:
|
||||||
|
ptaf-{{.ClientTitle}}-net:
|
||||||
|
driver: bridge
|
||||||
|
`
|
||||||
|
|
||||||
|
t, err := template.New("compose").Parse(tmpl)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("ошибка парсинга шаблона: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf strings.Builder
|
||||||
|
if err = t.Execute(&buf, data); err != nil {
|
||||||
|
return false, fmt.Errorf("ошибка генерации файла: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return writeFileIfChanged(filePath, buf.String())
|
||||||
|
}
|
||||||
228
db.go
Normal file
228
db.go
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
// connectToDB подключается к базе данных PostgreSQL
|
||||||
|
// Сначала пытается подключиться к primary, если не удалось - к secondary
|
||||||
|
func connectToDB(config Config) *sql.DB {
|
||||||
|
log.Println("Попытка подключения к первичной БД...")
|
||||||
|
|
||||||
|
connStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
|
||||||
|
config.PrimaryDBHost, config.DBPort, config.DBUser, config.DBPassword, config.DBName)
|
||||||
|
|
||||||
|
db, err := sql.Open("postgres", connStr)
|
||||||
|
if err == nil {
|
||||||
|
err = db.Ping()
|
||||||
|
if err == nil {
|
||||||
|
log.Printf("Успешное подключение к первичной БД: %s", config.PrimaryDBHost)
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Не удалось подключиться к первичной БД: %v", err)
|
||||||
|
log.Println("Попытка подключения к вторичной БД...")
|
||||||
|
|
||||||
|
connStr = fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
|
||||||
|
config.SecondaryDBHost, config.DBPort, config.DBUser, config.DBPassword, config.DBName)
|
||||||
|
|
||||||
|
db, err = sql.Open("postgres", connStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Ошибка открытия соединения с вторичной БД: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Ping()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Ошибка подключения к вторичной БД: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Успешное подключение к вторичной БД: %s", config.SecondaryDBHost)
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
// getInstancesByHostname получает все instances для hostname из таблицы instances
|
||||||
|
// Возвращает массив instances, т.к. один hostname может быть связан с несколькими instance
|
||||||
|
func getInstancesByHostname(db *sql.DB, hostname string) ([]string, error) {
|
||||||
|
query := "SELECT instance FROM instances_new WHERE hostname = $1 ORDER BY instance"
|
||||||
|
|
||||||
|
rows, err := db.Query(query, hostname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ошибка выполнения запроса для hostname %s: %w", hostname, err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var instances []string
|
||||||
|
for rows.Next() {
|
||||||
|
var instance string
|
||||||
|
if err := rows.Scan(&instance); err != nil {
|
||||||
|
return nil, fmt.Errorf("ошибка чтения instance: %w", err)
|
||||||
|
}
|
||||||
|
instances = append(instances, instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("ошибка обработки результатов: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(instances) == 0 {
|
||||||
|
return nil, fmt.Errorf("instances не найдены для hostname %s", hostname)
|
||||||
|
}
|
||||||
|
|
||||||
|
return instances, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkHostBelongsToInstance проверяет принадлежит ли хост к указанному instance
|
||||||
|
func checkHostBelongsToInstance(db *sql.DB, hostname, instance string) (bool, error) {
|
||||||
|
query := `
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM instances_new
|
||||||
|
WHERE hostname = $1 AND instance = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
var count int
|
||||||
|
err := db.QueryRow(query, hostname, instance).Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("ошибка проверки принадлежности хоста: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getClientInfoByInstance получает всех клиентов для заданного waf_instance
|
||||||
|
func getClientInfoByInstance(db *sql.DB, instance string) ([]ClientInfo, error) {
|
||||||
|
query := `
|
||||||
|
SELECT containers_count, ptaf_config, client_title, fluent_bit_port, waf_instance
|
||||||
|
FROM client_info
|
||||||
|
WHERE waf_instance = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := db.Query(query, instance)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "does not exist") {
|
||||||
|
return nil, fmt.Errorf("ошибка структуры таблицы client_info: %w (возможно изменилась схема БД)", err)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("ошибка выполнения запроса: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var clientInfoList []ClientInfo
|
||||||
|
for rows.Next() {
|
||||||
|
var ci ClientInfo
|
||||||
|
var wafInstance sql.NullString
|
||||||
|
err := rows.Scan(
|
||||||
|
&ci.ContainersCount,
|
||||||
|
&ci.PTAFConfig,
|
||||||
|
&ci.ClientTitle,
|
||||||
|
&ci.FluentBitPort,
|
||||||
|
&wafInstance,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ошибка сканирования строки: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if wafInstance.Valid {
|
||||||
|
ci.WAFInstance = wafInstance.String
|
||||||
|
}
|
||||||
|
|
||||||
|
clientInfoList = append(clientInfoList, ci)
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientInfoList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAppsSettingsByClientTitle получает apps_settings для заданного client_title
|
||||||
|
func getAppsSettingsByClientTitle(db *sql.DB, clientTitle string) ([]AppsSettings, error) {
|
||||||
|
query := `
|
||||||
|
SELECT
|
||||||
|
l7resourceid, waf_enabled, waf_vendor,
|
||||||
|
custom_input_http_ports, custom_input_https_ports,
|
||||||
|
custom_output_http_ports, custom_output_https_ports,
|
||||||
|
upstream_angie_custom, server_nginx_custom,
|
||||||
|
server_angie_custom, client_title,
|
||||||
|
upstream_nginx_custom,
|
||||||
|
location_angie_custom, location_nginx_custom,
|
||||||
|
server_directives_angie_custom, server_directives_nginx_custom,
|
||||||
|
sni,
|
||||||
|
mode,
|
||||||
|
ssl_enabled,
|
||||||
|
custom_angie_ssl,
|
||||||
|
max_fails,
|
||||||
|
fail_timeout
|
||||||
|
FROM apps_settings
|
||||||
|
WHERE client_title = $1
|
||||||
|
ORDER BY l7resourceid ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := db.Query(query, clientTitle)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "does not exist") {
|
||||||
|
return nil, fmt.Errorf("ошибка структуры таблицы apps_settings: %w (возможно изменилась схема БД)", err)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("ошибка выполнения запроса: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var appsSettingsList []AppsSettings
|
||||||
|
for rows.Next() {
|
||||||
|
var a AppsSettings
|
||||||
|
err := rows.Scan(
|
||||||
|
&a.L7ResourceID, &a.WAFEnabled, &a.WAFVendor,
|
||||||
|
&a.CustomInputHTTPPorts, &a.CustomInputHTTPSPorts,
|
||||||
|
&a.CustomOutputHTTPPorts, &a.CustomOutputHTTPSPorts,
|
||||||
|
&a.UpstreamAngieCustom, &a.ServerNginxCustom,
|
||||||
|
&a.ServerAngieCustom, &a.ClientTitle,
|
||||||
|
&a.UpstreamNginxCustom,
|
||||||
|
&a.LocationAngieCustom, &a.LocationNginxCustom,
|
||||||
|
&a.ServerDirectivesAngieCustom, &a.ServerDirectivesNginxCustom,
|
||||||
|
&a.SNI,
|
||||||
|
&a.Mode,
|
||||||
|
&a.SSLEnabled,
|
||||||
|
&a.CustomAngieSSL,
|
||||||
|
&a.MaxFails,
|
||||||
|
&a.FailTimeout,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ошибка сканирования строки: %w", err)
|
||||||
|
}
|
||||||
|
appsSettingsList = append(appsSettingsList, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
return appsSettingsList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getClientInfoByClientTitle получает информацию о клиенте из таблицы client_info
|
||||||
|
func getClientInfoByClientTitle(db *sql.DB, clientTitle string) (*ClientInfo, error) {
|
||||||
|
query := `
|
||||||
|
SELECT containers_count, ptaf_config, client_title, fluent_bit_port, waf_instance
|
||||||
|
FROM client_info
|
||||||
|
WHERE client_title = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
var clientInfo ClientInfo
|
||||||
|
var wafInstance sql.NullString
|
||||||
|
err := db.QueryRow(query, clientTitle).Scan(
|
||||||
|
&clientInfo.ContainersCount,
|
||||||
|
&clientInfo.PTAFConfig,
|
||||||
|
&clientInfo.ClientTitle,
|
||||||
|
&clientInfo.FluentBitPort,
|
||||||
|
&wafInstance,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "does not exist") {
|
||||||
|
return nil, fmt.Errorf("ошибка структуры таблицы client_info: %w (возможно изменилась схема БД)", err)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("ошибка получения данных из client_info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if wafInstance.Valid {
|
||||||
|
clientInfo.WAFInstance = wafInstance.String
|
||||||
|
}
|
||||||
|
|
||||||
|
return &clientInfo, nil
|
||||||
|
}
|
||||||
265
drain.go
Normal file
265
drain.go
Normal file
|
|
@ -0,0 +1,265 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = sql.ErrNoRows // ensure import
|
||||||
|
|
||||||
|
func processDrainingAngieConfigs() {
|
||||||
|
currentTime := time.Now().Unix()
|
||||||
|
timeout := int64(MigrationDrainTimeout)
|
||||||
|
hostname, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("⚠ Ошибка получения hostname: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tempConfig := getDefaultConfig()
|
||||||
|
db := connectToDB(tempConfig)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
matches, err := filepath.Glob("/tmp/ptaf-angie-drain-*")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("⚠ Ошибка поиска маркеров Angie: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, markerFile := range matches {
|
||||||
|
parts := strings.Split(filepath.Base(markerFile), "-")
|
||||||
|
if len(parts) < 4 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
clientTitle := strings.Join(parts[3:], "-")
|
||||||
|
drainTimestamp, found := getAngieConfigDrainTimestamp(clientTitle)
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
elapsedTime := currentTime - drainTimestamp
|
||||||
|
remainingTime := timeout - elapsedTime
|
||||||
|
angieConfigPath := fmt.Sprintf("/etc/angie/http.d/angie-ptaf-%s.conf", clientTitle)
|
||||||
|
|
||||||
|
if elapsedTime >= timeout {
|
||||||
|
log.Printf(" Конфиг Angie для %s завершил drain (прошло %d сек), проверяю актуальность...", clientTitle, elapsedTime)
|
||||||
|
clientInfo, err := getClientInfoByClientTitle(db, clientTitle)
|
||||||
|
if err == nil && clientInfo.WAFInstance != "" {
|
||||||
|
belongsNow, checkErr := checkHostBelongsToInstance(db, hostname, clientInfo.WAFInstance)
|
||||||
|
if checkErr == nil && belongsNow {
|
||||||
|
log.Printf(" ⚠ Клиент %s снова принадлежит этому хосту, ОТМЕНЯЮ удаление конфига", clientTitle)
|
||||||
|
os.Remove(markerFile)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf(" → Клиент %s не принадлежит этому хосту, удаляю конфиг", clientTitle)
|
||||||
|
if _, err := os.Stat(angieConfigPath); err == nil {
|
||||||
|
if err := os.Remove(angieConfigPath); err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка удаления конфига Angie: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf(" ✓ Конфиг Angie удалён: %s", angieConfigPath)
|
||||||
|
os.Remove(markerFile)
|
||||||
|
reloadAngie()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf(" Конфиг Angie для %s уже не существует, удаляю маркер", clientTitle)
|
||||||
|
os.Remove(markerFile)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf(" ○ Конфиг Angie для %s в процессе drain [миграция] (осталось %d сек)", clientTitle, remainingTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func processAllDrainingContainersGlobally(db *sql.DB, hostname string) {
|
||||||
|
log.Println("\n--- Глобальная проверка клиентов и drain-контейнеров ---")
|
||||||
|
entries, err := os.ReadDir("/home/install/ptaf")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("⚠ Не удалось прочитать директорию: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
processedClients := 0
|
||||||
|
markedForDrain := 0
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
clientTitle := entry.Name()
|
||||||
|
log.Printf(" → Проверка клиента: %s", clientTitle)
|
||||||
|
|
||||||
|
clientInfo, err := getClientInfoByClientTitle(db, clientTitle)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка получения данных клиента из БД: %v", err)
|
||||||
|
checkAndProcessExistingDrainMarkers(clientTitle, &processedClients)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldBeHere := false
|
||||||
|
skipDrainCheck := false
|
||||||
|
if clientInfo.WAFInstance != "" {
|
||||||
|
belongsToInstance, err := checkHostBelongsToInstance(db, hostname, clientInfo.WAFInstance)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка проверки принадлежности хоста: %v", err)
|
||||||
|
} else {
|
||||||
|
shouldBeHere = belongsToInstance
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf(" ℹ waf_instance не указан (NULL), ресурс может быть на любом хосте")
|
||||||
|
shouldBeHere = true
|
||||||
|
skipDrainCheck = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !shouldBeHere && !skipDrainCheck {
|
||||||
|
log.Printf(" ⚠ Клиент %s не должен быть на этом хосте (waf_instance='%s')", clientTitle, clientInfo.WAFInstance)
|
||||||
|
hasExistingMarkers := false
|
||||||
|
for containerNum := 1; containerNum <= MaxContainersPerClient; containerNum++ {
|
||||||
|
if _, err := os.Stat(fmt.Sprintf("/tmp/ptaf-drain-ptaf_%s_%03d", clientTitle, containerNum)); err == nil {
|
||||||
|
hasExistingMarkers = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasExistingMarkers {
|
||||||
|
log.Printf(" → Помечаю контейнеры для миграции")
|
||||||
|
for containerNum := 1; containerNum <= MaxContainersPerClient; containerNum++ {
|
||||||
|
containerFullName := fmt.Sprintf("ptaf_%s_%03d", clientTitle, containerNum)
|
||||||
|
exists, _ := checkContainerStatus(containerFullName)
|
||||||
|
if exists {
|
||||||
|
if err := markContainerForDrain(containerFullName, true); err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка пометки контейнера %s: %v", containerFullName, err)
|
||||||
|
} else {
|
||||||
|
log.Printf(" ✓ Контейнер %s помечен для drain (миграция)", containerFullName)
|
||||||
|
markedForDrain++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
angieConfigPath := fmt.Sprintf("/etc/angie/http.d/angie-ptaf-%s.conf", clientTitle)
|
||||||
|
if _, err := os.Stat(angieConfigPath); err == nil {
|
||||||
|
if err := markAngieConfigForDrain(clientTitle); err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка пометки Angie конфига: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf(" ✓ Angie конфиг помечен для удаления")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processDrainingContainers(clientTitle, 0)
|
||||||
|
processedClients++
|
||||||
|
} else {
|
||||||
|
log.Printf(" ✓ Клиент должен быть здесь, проверяю устаревшие drain-маркеры")
|
||||||
|
checkAndProcessExistingDrainMarkers(clientTitle, &processedClients)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if markedForDrain > 0 {
|
||||||
|
log.Printf("\n✓ Помечено для миграции: %d контейнеров", markedForDrain)
|
||||||
|
}
|
||||||
|
if processedClients > 0 {
|
||||||
|
log.Printf("✓ Обработано клиентов: %d", processedClients)
|
||||||
|
} else {
|
||||||
|
log.Println(" Нет клиентов требующих обработки")
|
||||||
|
}
|
||||||
|
processDrainingAngieConfigs()
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAndProcessExistingDrainMarkers(clientTitle string, processedCount *int) {
|
||||||
|
hasAnyDrainMarkers := false
|
||||||
|
for containerNum := 1; containerNum <= MaxContainersPerClient; containerNum++ {
|
||||||
|
if _, err := os.Stat(fmt.Sprintf("/tmp/ptaf-drain-ptaf_%s_%03d", clientTitle, containerNum)); err == nil {
|
||||||
|
hasAnyDrainMarkers = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasAnyDrainMarkers {
|
||||||
|
if _, err := os.Stat(fmt.Sprintf("/tmp/ptaf-angie-drain-%s", clientTitle)); err == nil {
|
||||||
|
hasAnyDrainMarkers = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasAnyDrainMarkers {
|
||||||
|
log.Printf(" → Найдены drain-маркеры, обрабатываю")
|
||||||
|
processDrainingContainers(clientTitle, 0)
|
||||||
|
*processedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func processDrainingContainers(clientTitle string, maxContainerNum int) {
|
||||||
|
baseDir := filepath.Join("/home/install/ptaf", clientTitle)
|
||||||
|
currentTime := time.Now().Unix()
|
||||||
|
hostname, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("⚠ Ошибка получения hostname: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tempConfig := getDefaultConfig()
|
||||||
|
db := connectToDB(tempConfig)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
for containerNum := maxContainerNum + 1; containerNum <= MaxContainersPerClient; containerNum++ {
|
||||||
|
containerName := fmt.Sprintf("%s-ptaf-agent%03d", clientTitle, containerNum)
|
||||||
|
containerFullName := fmt.Sprintf("ptaf_%s_%03d", clientTitle, containerNum)
|
||||||
|
containerDir := filepath.Join(baseDir, containerName)
|
||||||
|
composeFile := filepath.Join(containerDir, "docker-compose.yml")
|
||||||
|
exists, _ := checkContainerStatus(containerFullName)
|
||||||
|
if !exists {
|
||||||
|
markerFile := fmt.Sprintf("/tmp/ptaf-drain-%s", containerFullName)
|
||||||
|
if _, err := os.Stat(markerFile); err == nil {
|
||||||
|
log.Printf(" Удаляю файл-маркер для несуществующего контейнера %s", containerFullName)
|
||||||
|
os.Remove(markerFile)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
drainTimestamp, isMigration, isDraining := getContainerDrainInfo(containerFullName)
|
||||||
|
if isDraining {
|
||||||
|
var timeout int64
|
||||||
|
timeoutName := "масштабирование"
|
||||||
|
if isMigration {
|
||||||
|
timeout = int64(MigrationDrainTimeout)
|
||||||
|
timeoutName = "миграция"
|
||||||
|
} else {
|
||||||
|
timeout = int64(ContainerDrainTimeout)
|
||||||
|
}
|
||||||
|
elapsedTime := currentTime - drainTimestamp
|
||||||
|
remainingTime := timeout - elapsedTime
|
||||||
|
if elapsedTime >= timeout {
|
||||||
|
if isMigration {
|
||||||
|
log.Printf(" Контейнер %s завершил drain [миграция] (прошло %d сек), проверяю актуальность...", containerFullName, elapsedTime)
|
||||||
|
clientInfo, err := getClientInfoByClientTitle(db, clientTitle)
|
||||||
|
if err == nil && clientInfo.WAFInstance != "" {
|
||||||
|
belongsNow, checkErr := checkHostBelongsToInstance(db, hostname, clientInfo.WAFInstance)
|
||||||
|
if checkErr == nil && belongsNow {
|
||||||
|
log.Printf(" ⚠ Клиент %s снова принадлежит этому хосту, ОТМЕНЯЮ удаление контейнера %s", clientTitle, containerFullName)
|
||||||
|
os.Remove(fmt.Sprintf("/tmp/ptaf-drain-%s", containerFullName))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf(" → Клиент %s не принадлежит этому хосту, удаляю контейнер %s", clientTitle, containerFullName)
|
||||||
|
} else {
|
||||||
|
log.Printf(" Контейнер %s завершил drain [%s] (прошло %d сек), удаляем", containerFullName, timeoutName, elapsedTime)
|
||||||
|
}
|
||||||
|
if err := stopAndRemoveContainer(containerFullName, composeFile, containerDir); err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка удаления контейнера %s: %v", containerFullName, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := removeContainerFiles(clientTitle, containerNum); err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка удаления файлов контейнера %s: %v", containerFullName, err)
|
||||||
|
} else {
|
||||||
|
if err := cleanupClientDirectoryIfEmpty(clientTitle); err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка очистки директорий клиента: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf(" ○ Контейнер %s в процессе drain [%s] (осталось %d сек)", containerFullName, timeoutName, remainingTime)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf(" Контейнер %s больше не нужен, помечаем для drain", containerFullName)
|
||||||
|
if err := markContainerForDrain(containerFullName, false); err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка пометки контейнера для drain: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf(" ✓ Контейнер %s помечен для drain, удаление через %d сек", containerFullName, ContainerDrainTimeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
go.mod
Normal file
7
go.mod
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
module waf_automata
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
require github.com/lib/pq v1.10.9
|
||||||
|
|
||||||
|
require github.com/joho/godotenv v1.5.1 // indirect
|
||||||
299
main.go
Normal file
299
main.go
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Lock-файл для предотвращения параллельного запуска
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
lockFile := "/var/lock/ptaf-main.lock"
|
||||||
|
|
||||||
|
f, err := os.OpenFile(lockFile, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0600)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsExist(err) {
|
||||||
|
log.Fatal("❌ Программа уже запущена! Lock файл существует: ", lockFile)
|
||||||
|
}
|
||||||
|
log.Fatal("❌ Не удалось создать lock файл: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Записываем PID в lock файл
|
||||||
|
pid := os.Getpid()
|
||||||
|
f.WriteString(fmt.Sprintf("%d\n", pid))
|
||||||
|
|
||||||
|
// Гарантированно удаляем lock при выходе
|
||||||
|
defer func() {
|
||||||
|
f.Close()
|
||||||
|
os.Remove(lockFile)
|
||||||
|
log.Printf("✓ Lock файл удален")
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Printf("✓ Lock файл создан: %s (PID: %d)", lockFile, pid)
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
log.Println("=== Запуск программы управления конфигурациями PTAF ===")
|
||||||
|
|
||||||
|
hostname, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Ошибка получения hostname: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("Hostname текущего хоста: %s", hostname)
|
||||||
|
|
||||||
|
tempConfig := getDefaultConfig()
|
||||||
|
db := connectToDB(tempConfig)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
config, err := initConfig(db)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Ошибка загрузки конфигурации: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceNames, err := getInstancesByHostname(db, hostname)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Ошибка получения instances для hostname %s: %v", hostname, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(instanceNames) == 1 {
|
||||||
|
log.Printf("Instance для хоста: %s", instanceNames[0])
|
||||||
|
} else {
|
||||||
|
log.Printf("Instances для хоста (%d): %s", len(instanceNames), strings.Join(instanceNames, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Глобальная обработка drain-контейнеров и миграций
|
||||||
|
processAllDrainingContainersGlobally(db, hostname)
|
||||||
|
|
||||||
|
// Аллокатор портов (заменяет глобальную переменную)
|
||||||
|
portAllocator := NewPortAllocator()
|
||||||
|
|
||||||
|
totalClients := 0
|
||||||
|
for _, instanceName := range instanceNames {
|
||||||
|
count := processInstance(db, config, hostname, instanceName, portAllocator)
|
||||||
|
totalClients += count
|
||||||
|
}
|
||||||
|
|
||||||
|
if totalClients == 0 {
|
||||||
|
log.Println("\nНет активных клиентов для обработки на текущем хосте")
|
||||||
|
} else {
|
||||||
|
log.Printf("\nВсего обработано клиентов: %d (на %d instance)", totalClients, len(instanceNames))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Финальная проверка drain-конфигов Angie
|
||||||
|
log.Println("\n--- Проверка отложенных удалений Angie конфигов ---")
|
||||||
|
processDrainingAngieConfigs()
|
||||||
|
|
||||||
|
log.Println("\n=== Программа завершена успешно ===")
|
||||||
|
}
|
||||||
|
|
||||||
|
// processInstance обрабатывает один instance и возвращает количество обработанных клиентов
|
||||||
|
func processInstance(db *sql.DB, config Config, hostname, instanceName string, portAllocator *PortAllocator) int {
|
||||||
|
log.Printf("\n=== Обработка instance: %s ===", instanceName)
|
||||||
|
|
||||||
|
clientInfoList, err := getClientInfoByInstance(db, instanceName)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("⚠ Ошибка получения клиентов для instance %s: %v", instanceName, err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(clientInfoList) == 0 {
|
||||||
|
log.Printf("Нет активных клиентов для instance %s", instanceName)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Найдено клиентов для instance %s: %d", instanceName, len(clientInfoList))
|
||||||
|
|
||||||
|
for _, clientInfo := range clientInfoList {
|
||||||
|
processClient(db, config, hostname, instanceName, clientInfo, portAllocator)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("\n=== Instance %s обработан ===", instanceName)
|
||||||
|
return len(clientInfoList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// processClient обрабатывает одного клиента. Возвращает true если клиент был обработан.
|
||||||
|
// Роутер по waf_vendor: направляет клиента в PTAF или SW процессор
|
||||||
|
func processClient(db *sql.DB, config Config, hostname, instanceName string, clientInfo ClientInfo, portAllocator *PortAllocator) bool {
|
||||||
|
log.Printf("\n--- Обработка клиента: %s (instance: %s) ---", clientInfo.ClientTitle, instanceName)
|
||||||
|
|
||||||
|
appsSettingsList, err := getAppsSettingsByClientTitle(db, clientInfo.ClientTitle)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Ошибка получения apps_settings для %s: %v", clientInfo.ClientTitle, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(appsSettingsList) == 0 {
|
||||||
|
log.Printf("⚠ Нет apps_settings для клиента %s, пропускаем", clientInfo.ClientTitle)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// ТОЧКА ВЕТВЛЕНИЯ ПО WAF VENDOR
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
vendor := determineVendor(appsSettingsList)
|
||||||
|
log.Printf("WAF Vendor: %s", vendor)
|
||||||
|
|
||||||
|
switch vendor {
|
||||||
|
case "ptaf":
|
||||||
|
return processPTAFClient(db, config, hostname, instanceName, clientInfo, appsSettingsList, portAllocator)
|
||||||
|
case "sw":
|
||||||
|
return processSWClient(db, config, hostname, instanceName, clientInfo, appsSettingsList, portAllocator)
|
||||||
|
default:
|
||||||
|
log.Printf("⚠ Неизвестный или не указан waf_vendor для клиента %s, пропускаем", clientInfo.ClientTitle)
|
||||||
|
log.Printf(" Ожидается: 'ptaf' или 'sw' в поле waf_vendor таблицы apps_settings")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// determineVendor определяет vendor из списка apps_settings
|
||||||
|
// Все ресурсы клиента должны иметь одинаковый waf_vendor
|
||||||
|
func determineVendor(appsSettingsList []AppsSettings) string {
|
||||||
|
for _, apps := range appsSettingsList {
|
||||||
|
if apps.WAFVendor.Valid && apps.WAFVendor.String != "" {
|
||||||
|
v := strings.ToLower(strings.TrimSpace(apps.WAFVendor.String))
|
||||||
|
if v == "ptaf" || v == "sw" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// REMOVED: старая логика processClient перенесена в ptaf/ptaf_processor.go
|
||||||
|
// Ниже идут вспомогательные функции которые используются обоими vendor
|
||||||
|
|
||||||
|
// processClient_REMOVED_PLACEHOLDER - вся логика обработки теперь в vendor-specific модулях
|
||||||
|
// Этот блок оставлен для ясности что код был перенесён, не удалён
|
||||||
|
// handleClientMigration обрабатывает миграцию клиента с текущего хоста
|
||||||
|
func handleClientMigration(clientInfo ClientInfo, hostname string) {
|
||||||
|
log.Printf("⚠ Клиент %s должен быть на instance '%s', текущий хост '%s' не принадлежит этому instance",
|
||||||
|
clientInfo.ClientTitle, clientInfo.WAFInstance, hostname)
|
||||||
|
log.Printf(" Инициирую удаление контейнеров с текущего хоста (миграция)")
|
||||||
|
|
||||||
|
for containerNum := 1; containerNum <= MaxContainersPerClient; containerNum++ {
|
||||||
|
containerFullName := fmt.Sprintf("ptaf_%s_%03d", clientInfo.ClientTitle, containerNum)
|
||||||
|
exists, _ := checkContainerStatus(containerFullName)
|
||||||
|
if exists {
|
||||||
|
log.Printf(" → Помечаю контейнер %s для drain (миграция, таймаут %d сек)",
|
||||||
|
containerFullName, MigrationDrainTimeout)
|
||||||
|
if err := markContainerForDrain(containerFullName, true); err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка пометки: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
angieConfigPath := fmt.Sprintf("/etc/angie/http.d/angie-ptaf-%s.conf", clientInfo.ClientTitle)
|
||||||
|
if _, err := os.Stat(angieConfigPath); err == nil {
|
||||||
|
log.Printf(" → Помечаю конфиг Angie для удаления (миграция, таймаут %d сек)", MigrationDrainTimeout)
|
||||||
|
if err := markAngieConfigForDrain(clientInfo.ClientTitle); err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка пометки конфига: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf(" ✓ Конфиг Angie помечен для удаления")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processDrainingContainers(clientInfo.ClientTitle, 0)
|
||||||
|
processDrainingAngieConfigs()
|
||||||
|
|
||||||
|
log.Printf(" Контейнеры и конфиг будут удалены через %d сек после завершения соединений", MigrationDrainTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanStaleDrainMarkers очищает устаревшие drain-маркеры если клиент вернулся на хост
|
||||||
|
func cleanStaleDrainMarkers(clientInfo ClientInfo) {
|
||||||
|
log.Printf(" → Проверка и очистка устаревших маркеров drain для %s", clientInfo.ClientTitle)
|
||||||
|
cleanedContainers := 0
|
||||||
|
cleanedAngieMarker := false
|
||||||
|
|
||||||
|
for containerNum := 1; containerNum <= MaxContainersPerClient; containerNum++ {
|
||||||
|
containerFullName := fmt.Sprintf("ptaf_%s_%03d", clientInfo.ClientTitle, containerNum)
|
||||||
|
markerFile := fmt.Sprintf("/tmp/ptaf-drain-%s", containerFullName)
|
||||||
|
|
||||||
|
if _, err := os.Stat(markerFile); err == nil {
|
||||||
|
_, isMigration, found := getContainerDrainInfo(containerFullName)
|
||||||
|
if found && isMigration {
|
||||||
|
if err := os.Remove(markerFile); err == nil {
|
||||||
|
log.Printf(" ✓ Удалён устаревший маркер drain для контейнера %s (миграция отменена)", containerFullName)
|
||||||
|
cleanedContainers++
|
||||||
|
}
|
||||||
|
} else if found && !isMigration {
|
||||||
|
if containerNum <= clientInfo.ContainersCount {
|
||||||
|
if err := os.Remove(markerFile); err == nil {
|
||||||
|
log.Printf(" ✓ Удалён устаревший маркер drain для контейнера %s (масштабирование отменено)", containerFullName)
|
||||||
|
cleanedContainers++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
angieMarkerFile := fmt.Sprintf("/tmp/ptaf-angie-drain-%s", clientInfo.ClientTitle)
|
||||||
|
if _, err := os.Stat(angieMarkerFile); err == nil {
|
||||||
|
if err := os.Remove(angieMarkerFile); err == nil {
|
||||||
|
log.Printf(" ✓ Удалён устаревший маркер drain для конфига Angie (миграция отменена)")
|
||||||
|
cleanedAngieMarker = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cleanedContainers > 0 || cleanedAngieMarker {
|
||||||
|
log.Printf(" ✓ Очищено устаревших маркеров: контейнеры=%d, конфиг Angie=%v", cleanedContainers, cleanedAngieMarker)
|
||||||
|
} else {
|
||||||
|
log.Printf(" ✓ Устаревших маркеров drain не найдено")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchResourcesData REMOVED: логика перенесена в vendor-specific модули
|
||||||
|
// - PTAF: ptaf/ptaf_processor.go → fetchPTAFResourcesData()
|
||||||
|
// - SW: sw/sw_processor.go → fetchSWResourcesData() (TODO)
|
||||||
|
|
||||||
|
// resolvePortsForClient загружает сохранённые порты или выделяет новые
|
||||||
|
func resolvePortsForClient(clientInfo ClientInfo, resourcesData []ResourceData, portAllocator *PortAllocator) (map[int][]PortMapping, error) {
|
||||||
|
log.Printf("Чтение сохранённых портов для %s", clientInfo.ClientTitle)
|
||||||
|
savedPortMap := make(map[int][]PortMapping)
|
||||||
|
allPortsFound := true
|
||||||
|
|
||||||
|
for containerNum := 1; containerNum <= clientInfo.ContainersCount; containerNum++ {
|
||||||
|
containerName := fmt.Sprintf("%s-ptaf-agent%03d", clientInfo.ClientTitle, containerNum)
|
||||||
|
containerDir := filepath.Join("/home/install/ptaf", clientInfo.ClientTitle, containerName)
|
||||||
|
|
||||||
|
if portMappings, found := loadContainerPorts(containerDir, resourcesData); found {
|
||||||
|
if containerNum == 1 {
|
||||||
|
for _, res := range resourcesData {
|
||||||
|
savedPortMap[res.L7ResourceID] = make([]PortMapping, clientInfo.ContainersCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for idx, res := range resourcesData {
|
||||||
|
if idx < len(portMappings) {
|
||||||
|
savedPortMap[res.L7ResourceID][containerNum-1] = portMappings[idx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf(" → Файл портов для контейнера %d не найден или невалиден", containerNum)
|
||||||
|
allPortsFound = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allPortsFound && len(savedPortMap) > 0 {
|
||||||
|
log.Printf(" → Используются сохранённые порты из .ports.json файлов (%d контейнеров)", clientInfo.ContainersCount)
|
||||||
|
return savedPortMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf(" → Выделение новых портов для %d ресурсов × %d контейнеров", len(resourcesData), clientInfo.ContainersCount)
|
||||||
|
return portAllocator.allocatePortsForResources(resourcesData, clientInfo.ContainersCount, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPortListening проверяет что порт слушает на указанном адресе
|
||||||
|
func checkPortListening(host string, port int) bool {
|
||||||
|
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", host, port), 2*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
833
nginx.go
Normal file
833
nginx.go
Normal file
|
|
@ -0,0 +1,833 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// generateAngieConfigsWithoutReload генерирует конфиги Angie БЕЗ автоматического reload
|
||||||
|
func generateAngieConfigsWithoutReload(config Config, clientTitle string, resources []ResourceData, resourcePortMap map[int][]PortMapping) (bool, error) {
|
||||||
|
log.Printf("Генерация конфигов Angie для %s", clientTitle)
|
||||||
|
|
||||||
|
configDir := "/etc/angie/http.d"
|
||||||
|
configFile := filepath.Join(configDir, fmt.Sprintf("angie-ptaf-%s.conf", clientTitle))
|
||||||
|
hashFile := configFile + ".hash"
|
||||||
|
|
||||||
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
|
return false, fmt.Errorf("ошибка создания директории %s: %w", configDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpFile := configFile + ".tmp"
|
||||||
|
f, err := os.Create(tmpFile)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("ошибка создания временного файла %s: %w", tmpFile, err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// Заголовок
|
||||||
|
f.WriteString("# angie configuration file\n")
|
||||||
|
f.WriteString("# inserts into http location\n\n")
|
||||||
|
|
||||||
|
// Map для WebSocket
|
||||||
|
f.WriteString("# Map для автоматического определения Connection header\n")
|
||||||
|
f.WriteString("# Для WebSocket: Connection: upgrade, для HTTP: Connection: '' (пустое для keepalive)\n")
|
||||||
|
f.WriteString("map $http_upgrade $proxy_connection {\n")
|
||||||
|
f.WriteString(" default upgrade;\n")
|
||||||
|
f.WriteString(" '' '';\n")
|
||||||
|
f.WriteString("}\n\n")
|
||||||
|
|
||||||
|
for _, res := range resources {
|
||||||
|
containerPorts, ok := resourcePortMap[res.L7ResourceID]
|
||||||
|
if !ok {
|
||||||
|
os.Remove(tmpFile)
|
||||||
|
return false, fmt.Errorf("не найдены порты для l7ResourceID %d", res.L7ResourceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := writeAngieResourceConfig(f, config, res, containerPorts)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(tmpFile)
|
||||||
|
return false, fmt.Errorf("ошибка генерации конфига для l7ResourceID %d: %w", res.L7ResourceID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
newHash, err := getFileHash(tmpFile)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(tmpFile)
|
||||||
|
return false, fmt.Errorf("ошибка вычисления SHA256: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oldHashBytes, err := os.ReadFile(hashFile)
|
||||||
|
oldHash := ""
|
||||||
|
if err == nil {
|
||||||
|
oldHash = strings.TrimSpace(string(oldHashBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldHash == newHash {
|
||||||
|
log.Printf(" ℹ Конфигурация Angie не изменилась (SHA256: %s), reload не требуется", newHash[:12])
|
||||||
|
os.Remove(tmpFile)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Rename(tmpFile, configFile); err != nil {
|
||||||
|
os.Remove(tmpFile)
|
||||||
|
return false, fmt.Errorf("ошибка замены конфига: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(hashFile, []byte(newHash), 0644); err != nil {
|
||||||
|
log.Printf(" ⚠ Не удалось сохранить хеш: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldHash == "" {
|
||||||
|
log.Printf(" ✓ Конфиг Angie создан: %s (БЕЗ reload)", configFile)
|
||||||
|
} else {
|
||||||
|
log.Printf(" ✓ Конфиг Angie обновлён: %s (SHA256: %s → %s) (БЕЗ reload)", configFile, oldHash[:12], newHash[:12])
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateAngieConfigs генерирует конфигурационные файлы Angie с автоматическим reload
|
||||||
|
func generateAngieConfigs(config Config, clientTitle string, resources []ResourceData, resourcePortMap map[int][]PortMapping) error {
|
||||||
|
changed, err := generateAngieConfigsWithoutReload(config, clientTitle, resources, resourcePortMap)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
reloadAngie() // Существующая функция из containers_part2.go (без возврата error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeAngieResourceConfig записывает конфигурацию для одного ресурса в Angie
|
||||||
|
func writeAngieResourceConfig(f *os.File, config Config, res ResourceData, containerPorts []PortMapping) error {
|
||||||
|
allDomains := []string{res.ServerName}
|
||||||
|
allDomains = append(allDomains, res.Aliases...)
|
||||||
|
processedDomains := processDomainsWithWWW(allDomains)
|
||||||
|
|
||||||
|
// Парсим кастомные SERVER_BLOCK
|
||||||
|
var customServerBlocks []CustomServerBlock
|
||||||
|
var serverDirectivesWithoutBlocks string
|
||||||
|
|
||||||
|
if res.AppsSettings.ServerAngieCustom.Valid && strings.TrimSpace(res.AppsSettings.ServerAngieCustom.String) != "" {
|
||||||
|
serverDirectivesRaw := strings.TrimSpace(res.AppsSettings.ServerAngieCustom.String)
|
||||||
|
// Для Angie НЕ санитизируем домены в SERVER_BLOCK (принимаем my_site.com как есть)
|
||||||
|
customServerBlocks = parseCustomServerBlocks(serverDirectivesRaw, false)
|
||||||
|
|
||||||
|
if len(customServerBlocks) > 0 {
|
||||||
|
log.Printf(" → Найдено %d кастомных SERVER_BLOCK директив для Angie l7ResourceID %d",
|
||||||
|
len(customServerBlocks), res.L7ResourceID)
|
||||||
|
for _, block := range customServerBlocks {
|
||||||
|
log.Printf(" → SERVER_BLOCK для Angie порта %d: %s + %d alias(es)",
|
||||||
|
block.Port, block.ServerName, len(block.Aliases))
|
||||||
|
}
|
||||||
|
serverDirectivesWithoutBlocks = stripServerBlocks(serverDirectivesRaw)
|
||||||
|
} else {
|
||||||
|
serverDirectivesWithoutBlocks = serverDirectivesRaw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serverBlockDomainsByPort, allServerBlockDomains := collectServerBlockDomains(customServerBlocks)
|
||||||
|
|
||||||
|
if len(allServerBlockDomains) > 0 {
|
||||||
|
log.Printf(" → Домены в SERVER_BLOCK для ЛЮБЫХ портов Angie: %v", getMapKeys(allServerBlockDomains))
|
||||||
|
}
|
||||||
|
|
||||||
|
hasCustomLocation := res.AppsSettings.LocationAngieCustom.Valid && strings.TrimSpace(res.AppsSettings.LocationAngieCustom.String) != ""
|
||||||
|
customLocationStr := ""
|
||||||
|
if hasCustomLocation {
|
||||||
|
customLocationStr = strings.TrimSpace(res.AppsSettings.LocationAngieCustom.String)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpPortsToListen := getInputHTTPPorts(res.AppsSettings)
|
||||||
|
httpsPortsToListen := getInputHTTPSPorts(res.AppsSettings)
|
||||||
|
|
||||||
|
// Upstream блоки для HTTPS
|
||||||
|
for _, customPort := range httpsPortsToListen {
|
||||||
|
upstreamName := fmt.Sprintf("secure%d_%d_%s", res.L7ResourceID, customPort, res.ServerName)
|
||||||
|
f.WriteString(fmt.Sprintf("upstream %s {\n", upstreamName))
|
||||||
|
for _, ports := range containerPorts {
|
||||||
|
if dockerPort, ok := ports.HTTPSPorts[customPort]; ok {
|
||||||
|
f.WriteString(fmt.Sprintf(" server 127.0.0.1:%d weight=50;\n", dockerPort))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeCustomUpstreamOrDefault(f, res.AppsSettings.UpstreamAngieCustom, res.AppsSettings.SNI)
|
||||||
|
f.WriteString("}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upstream блоки для HTTP
|
||||||
|
for _, customPort := range httpPortsToListen {
|
||||||
|
upstreamName := fmt.Sprintf("unsecure%d_%d_%s", res.L7ResourceID, customPort, res.ServerName)
|
||||||
|
f.WriteString(fmt.Sprintf("upstream %s {\n", upstreamName))
|
||||||
|
for _, ports := range containerPorts {
|
||||||
|
if dockerPort, ok := ports.HTTPPorts[customPort]; ok {
|
||||||
|
f.WriteString(fmt.Sprintf(" server 127.0.0.1:%d weight=50;\n", dockerPort))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeCustomUpstreamOrDefault(f, res.AppsSettings.UpstreamAngieCustom, res.AppsSettings.SNI)
|
||||||
|
f.WriteString("}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upstream блоки для кастомных SERVER_BLOCK в Angie.
|
||||||
|
// В nginx upstream для SERVER_BLOCK генерируется отдельным циклом (строки ~577),
|
||||||
|
// здесь реализуем аналогичную логику для Angie.
|
||||||
|
// Логика выбора docker-порта зеркалит resolveOutputPort из nginx:
|
||||||
|
// 1. Если block.Port совпадает с одним из httpsPortsToListen — берём его docker-порт
|
||||||
|
// 2. Иначе фоллбэк на httpsPortsToListen[0] — детерминировано, без итерации по map
|
||||||
|
if len(customServerBlocks) > 0 {
|
||||||
|
log.Printf(" → Генерация upstream'ов для %d кастомных SERVER_BLOCK (Angie) l7ResourceID %d",
|
||||||
|
len(customServerBlocks), res.L7ResourceID)
|
||||||
|
for _, block := range customServerBlocks {
|
||||||
|
var upstreamName string
|
||||||
|
if block.IsHTTPS {
|
||||||
|
upstreamName = fmt.Sprintf("secure%d_%d_%s", res.L7ResourceID, block.Port, block.ServerName)
|
||||||
|
f.WriteString(fmt.Sprintf("upstream %s {\n", upstreamName))
|
||||||
|
// Определяем input-порт для поиска docker-порта: точное совпадение или фоллбэк на [0]
|
||||||
|
resolvedHTTPSPort := block.Port
|
||||||
|
foundHTTPS := false
|
||||||
|
for _, p := range httpsPortsToListen {
|
||||||
|
if p == block.Port {
|
||||||
|
foundHTTPS = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundHTTPS && len(httpsPortsToListen) > 0 {
|
||||||
|
resolvedHTTPSPort = httpsPortsToListen[0]
|
||||||
|
log.Printf(" └─ SERVER_BLOCK порт %d не в custom_input_https_ports, фоллбэк на %d",
|
||||||
|
block.Port, resolvedHTTPSPort)
|
||||||
|
}
|
||||||
|
for _, ports := range containerPorts {
|
||||||
|
if dockerPort, ok := ports.HTTPSPorts[resolvedHTTPSPort]; ok {
|
||||||
|
f.WriteString(fmt.Sprintf(" server 127.0.0.1:%d weight=50;\n", dockerPort))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
upstreamName = fmt.Sprintf("unsecure%d_%d_%s", res.L7ResourceID, block.Port, block.ServerName)
|
||||||
|
f.WriteString(fmt.Sprintf("upstream %s {\n", upstreamName))
|
||||||
|
resolvedHTTPPort := block.Port
|
||||||
|
foundHTTP := false
|
||||||
|
for _, p := range httpPortsToListen {
|
||||||
|
if p == block.Port {
|
||||||
|
foundHTTP = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundHTTP && len(httpPortsToListen) > 0 {
|
||||||
|
resolvedHTTPPort = httpPortsToListen[0]
|
||||||
|
log.Printf(" └─ SERVER_BLOCK порт %d не в custom_input_http_ports, фоллбэк на %d",
|
||||||
|
block.Port, resolvedHTTPPort)
|
||||||
|
}
|
||||||
|
for _, ports := range containerPorts {
|
||||||
|
if dockerPort, ok := ports.HTTPPorts[resolvedHTTPPort]; ok {
|
||||||
|
f.WriteString(fmt.Sprintf(" server 127.0.0.1:%d weight=50;\n", dockerPort))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeCustomUpstreamOrDefault(f, res.AppsSettings.UpstreamAngieCustom, res.AppsSettings.SNI)
|
||||||
|
f.WriteString("}\n\n")
|
||||||
|
log.Printf(" └─ Angie upstream для SERVER_BLOCK: %s", upstreamName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server блоки для HTTP
|
||||||
|
for _, httpPort := range httpPortsToListen {
|
||||||
|
defaultDomains := filterDomainsForPort(httpPort, processedDomains, serverBlockDomainsByPort, allServerBlockDomains)
|
||||||
|
if len(defaultDomains) > 0 {
|
||||||
|
log.Printf(" → Angie HTTP порт %d: default server с %d доменами", httpPort, len(defaultDomains))
|
||||||
|
upstreamName := fmt.Sprintf("unsecure%d_%d_%s", res.L7ResourceID, httpPort, res.ServerName)
|
||||||
|
|
||||||
|
f.WriteString("server {\n")
|
||||||
|
f.WriteString(fmt.Sprintf(" listen %d;\n", httpPort))
|
||||||
|
f.WriteString(fmt.Sprintf(" server_name %s;\n\n", strings.Join(defaultDomains, " ")))
|
||||||
|
f.WriteString(fmt.Sprintf(" access_log /var/log/angie/%d_%s_access.log waf;\n", res.L7ResourceID, res.ServerName))
|
||||||
|
f.WriteString(fmt.Sprintf(" error_log /var/log/angie/%d_%s_error.log error;\n\n", res.L7ResourceID, res.ServerName))
|
||||||
|
|
||||||
|
writeNetworkSettings(f, config)
|
||||||
|
writeStandardServerSettings(f, getCustomServerDirectives(res.AppsSettings.ServerDirectivesAngieCustom))
|
||||||
|
|
||||||
|
if hasCustomLocation {
|
||||||
|
log.Printf(" └─ Добавление location_angie_custom (отдельный блок) для l7ResourceID %d (HTTP, порт %d)", res.L7ResourceID, httpPort)
|
||||||
|
correctUpstream := fmt.Sprintf("http://%s", upstreamName)
|
||||||
|
writeCustomLocationBlock(f, customLocationStr, correctUpstream, res.L7ResourceID, "HTTP", httpPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WriteString(" location / {\n")
|
||||||
|
if res.AppsSettings.ServerAngieCustom.Valid && serverDirectivesWithoutBlocks != "" {
|
||||||
|
log.Printf(" └─ Добавление server_angie_custom для l7ResourceID %d (HTTP location /)", res.L7ResourceID)
|
||||||
|
writeIndentedLines(f, serverDirectivesWithoutBlocks)
|
||||||
|
}
|
||||||
|
f.WriteString(fmt.Sprintf(" proxy_pass http://%s;\n", upstreamName))
|
||||||
|
f.WriteString(" }\n")
|
||||||
|
f.WriteString("}\n\n")
|
||||||
|
} else {
|
||||||
|
log.Printf(" → Angie HTTP порт %d: default server НЕ создан (нет доменов после исключения SERVER_BLOCK)", httpPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server блоки для HTTPS
|
||||||
|
for _, httpsPort := range httpsPortsToListen {
|
||||||
|
defaultDomains := filterDomainsForPort(httpsPort, processedDomains, serverBlockDomainsByPort, allServerBlockDomains)
|
||||||
|
if len(defaultDomains) > 0 {
|
||||||
|
sslEnabled := true
|
||||||
|
if res.AppsSettings.SSLEnabled.Valid {
|
||||||
|
sslEnabled = res.AppsSettings.SSLEnabled.Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
if sslEnabled {
|
||||||
|
log.Printf(" → Angie HTTPS порт %d: default server с %d доменами (SSL включен)", httpsPort, len(defaultDomains))
|
||||||
|
} else {
|
||||||
|
log.Printf(" → Angie HTTPS порт %d: default server с %d доменами (SSL отключен, plain HTTP)", httpsPort, len(defaultDomains))
|
||||||
|
}
|
||||||
|
|
||||||
|
upstreamName := fmt.Sprintf("secure%d_%d_%s", res.L7ResourceID, httpsPort, res.ServerName)
|
||||||
|
|
||||||
|
f.WriteString("server {\n")
|
||||||
|
if sslEnabled {
|
||||||
|
f.WriteString(fmt.Sprintf(" listen %d ssl;\n", httpsPort))
|
||||||
|
} else {
|
||||||
|
f.WriteString(fmt.Sprintf(" listen %d;\n", httpsPort))
|
||||||
|
}
|
||||||
|
f.WriteString(fmt.Sprintf(" server_name %s;\n\n", strings.Join(defaultDomains, " ")))
|
||||||
|
f.WriteString(fmt.Sprintf(" access_log /var/log/angie/%d_%s_access.log waf;\n", res.L7ResourceID, res.ServerName))
|
||||||
|
f.WriteString(fmt.Sprintf(" error_log /var/log/angie/%d_%s_error.log error;\n\n", res.L7ResourceID, res.ServerName))
|
||||||
|
|
||||||
|
if sslEnabled {
|
||||||
|
writeAngieSSLSettings(f, res.AppsSettings.CustomAngieSSL)
|
||||||
|
}
|
||||||
|
|
||||||
|
writeNetworkSettings(f, config)
|
||||||
|
writeStandardServerSettings(f, getCustomServerDirectives(res.AppsSettings.ServerDirectivesAngieCustom))
|
||||||
|
|
||||||
|
if hasCustomLocation {
|
||||||
|
log.Printf(" └─ Добавление location_angie_custom (отдельный блок) для l7ResourceID %d (HTTPS, порт %d)", res.L7ResourceID, httpsPort)
|
||||||
|
correctUpstream := fmt.Sprintf("http://%s", upstreamName)
|
||||||
|
writeCustomLocationBlock(f, customLocationStr, correctUpstream, res.L7ResourceID, "HTTPS", httpsPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WriteString(" location / {\n")
|
||||||
|
if res.AppsSettings.ServerAngieCustom.Valid && serverDirectivesWithoutBlocks != "" {
|
||||||
|
log.Printf(" └─ Добавление server_angie_custom для l7ResourceID %d (HTTPS location /)", res.L7ResourceID)
|
||||||
|
writeIndentedLines(f, serverDirectivesWithoutBlocks)
|
||||||
|
}
|
||||||
|
f.WriteString(fmt.Sprintf(" proxy_pass http://%s;\n", upstreamName))
|
||||||
|
f.WriteString(" }\n")
|
||||||
|
f.WriteString("}\n\n")
|
||||||
|
} else {
|
||||||
|
log.Printf(" → Angie HTTPS порт %d: default server НЕ создан (нет доменов после исключения SERVER_BLOCK)", httpsPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кастомные SERVER_BLOCK
|
||||||
|
if len(customServerBlocks) > 0 {
|
||||||
|
log.Printf(" → Генерация %d кастомных server блоков для Angie l7ResourceID %d",
|
||||||
|
len(customServerBlocks), res.L7ResourceID)
|
||||||
|
for _, block := range customServerBlocks {
|
||||||
|
aliasInfo := ""
|
||||||
|
if len(block.Aliases) > 0 {
|
||||||
|
aliasInfo = fmt.Sprintf(" + %d alias(es)", len(block.Aliases))
|
||||||
|
}
|
||||||
|
log.Printf(" └─ Angie Server блок: порт %d, server_name %s%s",
|
||||||
|
block.Port, block.ServerName, aliasInfo)
|
||||||
|
generateAngieCustomServerBlock(f, config, res, block, containerPorts, customLocationStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeNetworkSettings записывает WAF и Antibot networks для Angie
|
||||||
|
func writeNetworkSettings(f *os.File, config Config) {
|
||||||
|
f.WriteString(" # waf networks\n")
|
||||||
|
for _, net := range config.WAFNetworks {
|
||||||
|
f.WriteString(fmt.Sprintf(" set_real_ip_from %s;\n", net))
|
||||||
|
}
|
||||||
|
f.WriteString(" # antibot networks\n")
|
||||||
|
for _, net := range config.AntibotNetworks {
|
||||||
|
f.WriteString(fmt.Sprintf(" set_real_ip_from %s;\n", net))
|
||||||
|
}
|
||||||
|
f.WriteString("\n real_ip_header X-Forwarded-For;\n")
|
||||||
|
f.WriteString(" real_ip_recursive on;\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeAngieSSLSettings записывает SSL настройки для Angie
|
||||||
|
func writeAngieSSLSettings(f *os.File, customSSL sql.NullString) {
|
||||||
|
if customSSL.Valid && strings.TrimSpace(customSSL.String) != "" {
|
||||||
|
log.Printf(" └─ Использование custom_angie_ssl (кастомные SSL настройки)")
|
||||||
|
lines := strings.Split(strings.TrimSpace(customSSL.String), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed != "" {
|
||||||
|
f.WriteString(" " + trimmed + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.WriteString("\n")
|
||||||
|
} else {
|
||||||
|
f.WriteString(" ssl_certificate /etc/ssl/certs/sp.crt;\n")
|
||||||
|
f.WriteString(" ssl_certificate_key /etc/ssl/private/sp.key;\n")
|
||||||
|
f.WriteString(" ssl_protocols TLSv1.3 TLSv1.2;\n")
|
||||||
|
f.WriteString(" ssl_prefer_server_ciphers on;\n\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeStandardServerSettings записывает стандартные настройки сервера (Angie)
|
||||||
|
func writeStandardServerSettings(f *os.File, customDirectives string) {
|
||||||
|
settings := ` server_tokens off;
|
||||||
|
sendfile on;
|
||||||
|
gzip on;
|
||||||
|
client_max_body_size 0;
|
||||||
|
|
||||||
|
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
js_set $safe_host main.sanitizeHost;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Connection $proxy_connection;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Host $safe_host;
|
||||||
|
proxy_set_header X-Original-Host $host;
|
||||||
|
proxy_pass_header Date;
|
||||||
|
proxy_pass_header Server;
|
||||||
|
`
|
||||||
|
f.WriteString(settings)
|
||||||
|
writeCustomOrDefaultDirectives(f, customDirectives)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeNginxStandardSettings записывает стандартные настройки сервера (Nginx)
|
||||||
|
func writeNginxStandardSettings(f *os.File, customDirectives string) {
|
||||||
|
settings := ` server_tokens off;
|
||||||
|
sendfile on;
|
||||||
|
gzip on;
|
||||||
|
client_max_body_size 0;
|
||||||
|
|
||||||
|
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
#proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Connection $proxy_connection;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Host $backend_host;
|
||||||
|
proxy_pass_header Date;
|
||||||
|
proxy_pass_header Server;
|
||||||
|
`
|
||||||
|
f.WriteString(settings)
|
||||||
|
writeCustomOrDefaultDirectives(f, customDirectives)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeCustomOrDefaultDirectives записывает кастомные или дефолтные директивы таймаутов/буферов
|
||||||
|
func writeCustomOrDefaultDirectives(f *os.File, customDirectives string) {
|
||||||
|
if customDirectives != "" {
|
||||||
|
f.WriteString("\n # Custom server directives (override defaults)\n")
|
||||||
|
lines := strings.Split(strings.TrimSpace(customDirectives), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed != "" {
|
||||||
|
f.WriteString(" " + trimmed + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
f.WriteString("\n # Default timeouts and buffers\n")
|
||||||
|
f.WriteString(" proxy_connect_timeout 60s;\n")
|
||||||
|
f.WriteString(" proxy_read_timeout 120s;\n")
|
||||||
|
f.WriteString(" proxy_send_timeout 120s;\n")
|
||||||
|
f.WriteString(" send_timeout 60s;\n")
|
||||||
|
f.WriteString(" keepalive_timeout 80s;\n")
|
||||||
|
f.WriteString(" keepalive_requests 1000;\n")
|
||||||
|
f.WriteString(" resolver_timeout 30s;\n")
|
||||||
|
f.WriteString(" large_client_header_buffers 4 128k;\n")
|
||||||
|
f.WriteString(" proxy_buffers 4 32k;\n")
|
||||||
|
f.WriteString(" proxy_buffer_size 16k;\n")
|
||||||
|
f.WriteString(" proxy_busy_buffers_size 32k;\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateAngieCustomServerBlock генерирует кастомный server блок для Angie
|
||||||
|
func generateAngieCustomServerBlock(f *os.File, config Config, res ResourceData, block CustomServerBlock, containerPorts []PortMapping, customLocationStr string) {
|
||||||
|
var upstreamName string
|
||||||
|
if block.IsHTTPS {
|
||||||
|
upstreamName = fmt.Sprintf("secure%d_%d_%s", res.L7ResourceID, block.Port, res.ServerName)
|
||||||
|
} else {
|
||||||
|
upstreamName = fmt.Sprintf("unsecure%d_%d_%s", res.L7ResourceID, block.Port, res.ServerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WriteString("server {\n")
|
||||||
|
|
||||||
|
sslEnabled := true
|
||||||
|
if res.AppsSettings.SSLEnabled.Valid {
|
||||||
|
sslEnabled = res.AppsSettings.SSLEnabled.Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
if block.IsHTTPS {
|
||||||
|
if sslEnabled {
|
||||||
|
f.WriteString(fmt.Sprintf(" listen %d ssl;\n", block.Port))
|
||||||
|
writeAngieSSLSettings(f, res.AppsSettings.CustomAngieSSL)
|
||||||
|
} else {
|
||||||
|
f.WriteString(fmt.Sprintf(" listen %d;\n", block.Port))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
f.WriteString(fmt.Sprintf(" listen %d;\n", block.Port))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(block.Aliases) > 0 {
|
||||||
|
f.WriteString(fmt.Sprintf(" server_name %s %s;\n\n",
|
||||||
|
block.ServerName, strings.Join(block.Aliases, " ")))
|
||||||
|
} else {
|
||||||
|
f.WriteString(fmt.Sprintf(" server_name %s;\n\n", block.ServerName))
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WriteString(fmt.Sprintf(" access_log /var/log/angie/%d_%s_port%d_access.log waf;\n",
|
||||||
|
res.L7ResourceID, sanitizeServerName(block.ServerName), block.Port))
|
||||||
|
f.WriteString(fmt.Sprintf(" error_log /var/log/angie/%d_%s_port%d_error.log error;\n\n",
|
||||||
|
res.L7ResourceID, sanitizeServerName(block.ServerName), block.Port))
|
||||||
|
|
||||||
|
writeNetworkSettings(f, config)
|
||||||
|
writeStandardServerSettings(f, getCustomServerDirectives(res.AppsSettings.ServerDirectivesAngieCustom))
|
||||||
|
|
||||||
|
// Кастомные location блоки из location_angie_custom (перед location /)
|
||||||
|
if customLocationStr != "" {
|
||||||
|
correctUpstream := fmt.Sprintf("http://%s", upstreamName)
|
||||||
|
log.Printf(" └─ Добавление location_angie_custom в SERVER_BLOCK порт %d: %s", block.Port, block.ServerName)
|
||||||
|
writeCustomLocationBlock(f, customLocationStr, correctUpstream, res.L7ResourceID, "SERVER_BLOCK", block.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WriteString(" location / {\n")
|
||||||
|
lines := strings.Split(block.Content, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed != "" {
|
||||||
|
f.WriteString(" " + trimmed + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.WriteString(fmt.Sprintf(" proxy_pass http://%s;\n", upstreamName))
|
||||||
|
f.WriteString(" }\n")
|
||||||
|
f.WriteString("}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeUpstreamServers записывает серверы для upstream блока nginx (с origins)
|
||||||
|
func writeUpstreamServers(f *os.File, origins []OriginItem, outputPort int, settings AppsSettings) {
|
||||||
|
allBackup := true
|
||||||
|
for _, origin := range origins {
|
||||||
|
if strings.ToLower(origin.Mode) != "backup" {
|
||||||
|
allBackup = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, origin := range origins {
|
||||||
|
serverLine := fmt.Sprintf(" server %s:%d", origin.IP, outputPort)
|
||||||
|
if origin.Weight > 0 {
|
||||||
|
serverLine += fmt.Sprintf(" weight=%d", origin.Weight)
|
||||||
|
}
|
||||||
|
if strings.ToLower(origin.Mode) == "backup" && !allBackup {
|
||||||
|
serverLine += " backup"
|
||||||
|
}
|
||||||
|
maxFails := getMaxFails(settings)
|
||||||
|
failTimeout := getFailTimeout(settings)
|
||||||
|
serverLine += fmt.Sprintf(" max_fails=%d fail_timeout=%ds", maxFails, failTimeout)
|
||||||
|
serverLine += ";\n"
|
||||||
|
f.WriteString(serverLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateNginxResourceConfig генерирует конфигурацию nginx для ресурса
|
||||||
|
func generateNginxResourceConfig(filepath string, config Config, res ResourceData, ports PortMapping) error {
|
||||||
|
allDomains := []string{res.ServerName}
|
||||||
|
allDomains = append(allDomains, res.Aliases...)
|
||||||
|
allDomains = sanitizeDomainsForNginx(allDomains)
|
||||||
|
processedDomains := processDomainsWithWWW(allDomains)
|
||||||
|
|
||||||
|
serverNameForProcessing := processedDomains[0]
|
||||||
|
aliasesForProcessing := processedDomains[1:]
|
||||||
|
|
||||||
|
f, err := os.Create(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
f.WriteString("# nginx configuration file\n")
|
||||||
|
f.WriteString("# inserts into http location\n\n")
|
||||||
|
|
||||||
|
// Парсим SERVER_BLOCK
|
||||||
|
var customServerBlocks []CustomServerBlock
|
||||||
|
var serverDirectivesWithoutBlocks string
|
||||||
|
|
||||||
|
if res.AppsSettings.ServerNginxCustom.Valid && strings.TrimSpace(res.AppsSettings.ServerNginxCustom.String) != "" {
|
||||||
|
serverDirectivesRaw := strings.TrimSpace(res.AppsSettings.ServerNginxCustom.String)
|
||||||
|
// Для Nginx санитизируем домены в SERVER_BLOCK (ожидаем my-site.com от Angie)
|
||||||
|
customServerBlocks = parseCustomServerBlocks(serverDirectivesRaw, true)
|
||||||
|
|
||||||
|
if len(customServerBlocks) > 0 {
|
||||||
|
log.Printf(" → Найдено %d кастомных SERVER_BLOCK директив для l7ResourceID %d",
|
||||||
|
len(customServerBlocks), res.L7ResourceID)
|
||||||
|
for _, block := range customServerBlocks {
|
||||||
|
log.Printf(" → SERVER_BLOCK для порта %d: %s + %d alias(es)",
|
||||||
|
block.Port, block.ServerName, len(block.Aliases))
|
||||||
|
}
|
||||||
|
serverDirectivesWithoutBlocks = stripServerBlocks(serverDirectivesRaw)
|
||||||
|
} else {
|
||||||
|
serverDirectivesWithoutBlocks = serverDirectivesRaw
|
||||||
|
}
|
||||||
|
serverDirectivesWithoutBlocks = replaceMTLSPaths(serverDirectivesWithoutBlocks)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverBlockDomainsByPort, allServerBlockDomains := collectServerBlockDomains(customServerBlocks)
|
||||||
|
|
||||||
|
if len(allServerBlockDomains) > 0 {
|
||||||
|
log.Printf(" → Домены в SERVER_BLOCK для ЛЮБЫХ портов: %v", getMapKeys(allServerBlockDomains))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция фильтрации для nginx (использует serverName + aliases отдельно)
|
||||||
|
filterNginxDomainsForPort := func(port int) []string {
|
||||||
|
var defaultDomains []string
|
||||||
|
allDoms := append([]string{serverNameForProcessing}, aliasesForProcessing...)
|
||||||
|
for _, domain := range allDoms {
|
||||||
|
if serverBlockDomainsByPort[port][domain] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if port == 80 || port == 443 {
|
||||||
|
defaultDomains = append(defaultDomains, domain)
|
||||||
|
} else {
|
||||||
|
if !allServerBlockDomains[domain] {
|
||||||
|
defaultDomains = append(defaultDomains, domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultDomains
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location directives
|
||||||
|
var locationDirectives string
|
||||||
|
if res.AppsSettings.LocationNginxCustom.Valid && strings.TrimSpace(res.AppsSettings.LocationNginxCustom.String) != "" {
|
||||||
|
locationDirectives = replaceMTLSPaths(strings.TrimSpace(res.AppsSettings.LocationNginxCustom.String))
|
||||||
|
}
|
||||||
|
|
||||||
|
httpsPortsToListen := getInputHTTPSPorts(res.AppsSettings)
|
||||||
|
httpPortsToListen := getInputHTTPPorts(res.AppsSettings)
|
||||||
|
httpsOutputPorts := getOutputHTTPSPorts(res.AppsSettings)
|
||||||
|
httpOutputPorts := getOutputHTTPPorts(res.AppsSettings)
|
||||||
|
|
||||||
|
hasCustomNginxLocation := locationDirectives != ""
|
||||||
|
isFullNginxLocationBlock := hasCustomNginxLocation && strings.Contains(locationDirectives, "location ")
|
||||||
|
|
||||||
|
// Upstream блоки для HTTPS
|
||||||
|
for idx, inputPort := range httpsPortsToListen {
|
||||||
|
outputPort := httpsOutputPorts[0]
|
||||||
|
if idx < len(httpsOutputPorts) {
|
||||||
|
outputPort = httpsOutputPorts[idx]
|
||||||
|
}
|
||||||
|
upstreamName := fmt.Sprintf("secure%d_%d_%s", res.L7ResourceID, inputPort, res.ServerName)
|
||||||
|
f.WriteString(fmt.Sprintf("upstream %s {\n", upstreamName))
|
||||||
|
writeUpstreamServers(f, res.Origins, outputPort, res.AppsSettings)
|
||||||
|
writeCustomUpstreamOrDefault(f, res.AppsSettings.UpstreamNginxCustom, res.AppsSettings.SNI)
|
||||||
|
f.WriteString("}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upstream блоки для HTTP
|
||||||
|
for idx, inputPort := range httpPortsToListen {
|
||||||
|
outputPort := httpOutputPorts[0]
|
||||||
|
if idx < len(httpOutputPorts) {
|
||||||
|
outputPort = httpOutputPorts[idx]
|
||||||
|
}
|
||||||
|
upstreamName := fmt.Sprintf("unsecure%d_%d_%s", res.L7ResourceID, inputPort, res.ServerName)
|
||||||
|
f.WriteString(fmt.Sprintf("upstream %s {\n", upstreamName))
|
||||||
|
writeUpstreamServers(f, res.Origins, outputPort, res.AppsSettings)
|
||||||
|
writeCustomUpstreamOrDefault(f, res.AppsSettings.UpstreamNginxCustom, res.AppsSettings.SNI)
|
||||||
|
f.WriteString("}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upstream для SERVER_BLOCK
|
||||||
|
if len(customServerBlocks) > 0 {
|
||||||
|
log.Printf(" → Генерация upstream'ов для %d кастомных server блоков", len(customServerBlocks))
|
||||||
|
for _, block := range customServerBlocks {
|
||||||
|
outputPort := resolveOutputPort(block, httpsPortsToListen, httpsOutputPorts, httpPortsToListen, httpOutputPorts)
|
||||||
|
prefix := map[bool]string{true: "secure", false: "unsecure"}[block.IsHTTPS]
|
||||||
|
upstreamName := fmt.Sprintf("%s%d_%d_%s", prefix, res.L7ResourceID, block.Port, block.ServerName)
|
||||||
|
f.WriteString(fmt.Sprintf("upstream %s {\n", upstreamName))
|
||||||
|
writeUpstreamServers(f, res.Origins, outputPort, res.AppsSettings)
|
||||||
|
writeCustomUpstreamOrDefault(f, res.AppsSettings.UpstreamNginxCustom, res.AppsSettings.SNI)
|
||||||
|
f.WriteString("}\n\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server блоки HTTP
|
||||||
|
for _, httpPort := range httpPortsToListen {
|
||||||
|
defaultDomains := filterNginxDomainsForPort(httpPort)
|
||||||
|
if len(defaultDomains) > 0 {
|
||||||
|
log.Printf(" → HTTP порт %d: default server с %d доменами", httpPort, len(defaultDomains))
|
||||||
|
upstreamName := fmt.Sprintf("unsecure%d_%d_%s", res.L7ResourceID, httpPort, res.ServerName)
|
||||||
|
|
||||||
|
f.WriteString("server {\n")
|
||||||
|
f.WriteString(fmt.Sprintf(" listen %d;\n", httpPort))
|
||||||
|
f.WriteString(fmt.Sprintf(" server_name %s;\n\n", strings.Join(defaultDomains, " ")))
|
||||||
|
f.WriteString(fmt.Sprintf(" access_log /var/log/nginx/%d_%s_access.log waf;\n", res.L7ResourceID, res.ServerName))
|
||||||
|
f.WriteString(fmt.Sprintf(" error_log /var/log/nginx/%d_%s_error.log error;\n\n", res.L7ResourceID, res.ServerName))
|
||||||
|
f.WriteString(" set_real_ip_from 172.16.0.0/12;\n")
|
||||||
|
f.WriteString(" real_ip_header X-Forwarded-For;\n")
|
||||||
|
f.WriteString(" real_ip_recursive on;\n\n")
|
||||||
|
writeNginxStandardSettings(f, getCustomServerDirectives(res.AppsSettings.ServerDirectivesNginxCustom))
|
||||||
|
|
||||||
|
if hasCustomNginxLocation && isFullNginxLocationBlock {
|
||||||
|
log.Printf(" └─ Добавление location_nginx_custom (отдельный блок) для l7ResourceID %d (HTTP, порт %d)", res.L7ResourceID, httpPort)
|
||||||
|
writeCustomLocationBlock(f, locationDirectives, fmt.Sprintf("http://%s", upstreamName), res.L7ResourceID, "HTTP", httpPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WriteString(" location / {\n")
|
||||||
|
if serverDirectivesWithoutBlocks != "" {
|
||||||
|
writeIndentedLines(f, serverDirectivesWithoutBlocks)
|
||||||
|
}
|
||||||
|
if hasCustomNginxLocation && !isFullNginxLocationBlock {
|
||||||
|
writeIndentedLines(f, locationDirectives)
|
||||||
|
}
|
||||||
|
f.WriteString(fmt.Sprintf(" proxy_pass http://%s;\n", upstreamName))
|
||||||
|
f.WriteString(" }\n")
|
||||||
|
f.WriteString("}\n\n")
|
||||||
|
} else {
|
||||||
|
log.Printf(" → HTTP порт %d: default server НЕ создан (нет доменов после исключения SERVER_BLOCK)", httpPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server блоки HTTPS
|
||||||
|
for _, httpsPort := range httpsPortsToListen {
|
||||||
|
defaultDomains := filterNginxDomainsForPort(httpsPort)
|
||||||
|
if len(defaultDomains) > 0 {
|
||||||
|
log.Printf(" → HTTPS порт %d: default server с %d доменами", httpsPort, len(defaultDomains))
|
||||||
|
upstreamName := fmt.Sprintf("secure%d_%d_%s", res.L7ResourceID, httpsPort, res.ServerName)
|
||||||
|
|
||||||
|
f.WriteString("server {\n")
|
||||||
|
f.WriteString(fmt.Sprintf(" listen %d;\n", httpsPort))
|
||||||
|
f.WriteString(fmt.Sprintf(" server_name %s;\n\n", strings.Join(defaultDomains, " ")))
|
||||||
|
f.WriteString(fmt.Sprintf(" access_log /var/log/nginx/%d_%s_access.log waf;\n", res.L7ResourceID, res.ServerName))
|
||||||
|
f.WriteString(fmt.Sprintf(" error_log /var/log/nginx/%d_%s_error.log error;\n\n", res.L7ResourceID, res.ServerName))
|
||||||
|
f.WriteString(" proxy_ssl_server_name on;\n")
|
||||||
|
f.WriteString(" proxy_ssl_name $host;\n\n")
|
||||||
|
f.WriteString(" set_real_ip_from 172.16.0.0/12;\n")
|
||||||
|
f.WriteString(" real_ip_header X-Forwarded-For;\n")
|
||||||
|
f.WriteString(" real_ip_recursive on;\n\n")
|
||||||
|
writeNginxStandardSettings(f, getCustomServerDirectives(res.AppsSettings.ServerDirectivesNginxCustom))
|
||||||
|
|
||||||
|
if hasCustomNginxLocation && isFullNginxLocationBlock {
|
||||||
|
log.Printf(" └─ Добавление location_nginx_custom (отдельный блок) для l7ResourceID %d (HTTPS, порт %d)", res.L7ResourceID, httpsPort)
|
||||||
|
writeCustomLocationBlock(f, locationDirectives, fmt.Sprintf("https://%s", upstreamName), res.L7ResourceID, "HTTPS", httpsPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WriteString(" location / {\n")
|
||||||
|
if serverDirectivesWithoutBlocks != "" {
|
||||||
|
writeIndentedLines(f, serverDirectivesWithoutBlocks)
|
||||||
|
}
|
||||||
|
if hasCustomNginxLocation && !isFullNginxLocationBlock {
|
||||||
|
writeIndentedLines(f, locationDirectives)
|
||||||
|
}
|
||||||
|
f.WriteString(fmt.Sprintf(" proxy_pass https://%s;\n", upstreamName))
|
||||||
|
f.WriteString(" }\n")
|
||||||
|
f.WriteString("}\n")
|
||||||
|
} else {
|
||||||
|
log.Printf(" → HTTPS порт %d: default server НЕ создан (нет доменов после исключения SERVER_BLOCK)", httpsPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кастомные SERVER_BLOCK
|
||||||
|
if len(customServerBlocks) > 0 {
|
||||||
|
log.Printf(" → Генерация %d кастомных server блоков для l7ResourceID %d",
|
||||||
|
len(customServerBlocks), res.L7ResourceID)
|
||||||
|
for _, block := range customServerBlocks {
|
||||||
|
aliasInfo := ""
|
||||||
|
if len(block.Aliases) > 0 {
|
||||||
|
aliasInfo = fmt.Sprintf(" + %d alias(es)", len(block.Aliases))
|
||||||
|
}
|
||||||
|
log.Printf(" └─ Server блок: порт %d, server_name %s%s", block.Port, block.ServerName, aliasInfo)
|
||||||
|
outputPort := resolveOutputPort(block, httpsPortsToListen, httpsOutputPorts, httpPortsToListen, httpOutputPorts)
|
||||||
|
generateCustomServerBlock(f, res, block, outputPort, locationDirectives)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveOutputPort определяет OUTPUT порт для SERVER_BLOCK
|
||||||
|
func resolveOutputPort(block CustomServerBlock, httpsInput, httpsOutput, httpInput, httpOutput []int) int {
|
||||||
|
if block.IsHTTPS {
|
||||||
|
for idx, inputPort := range httpsInput {
|
||||||
|
if inputPort == block.Port && idx < len(httpsOutput) {
|
||||||
|
return httpsOutput[idx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(httpsOutput) > 0 {
|
||||||
|
return httpsOutput[0]
|
||||||
|
}
|
||||||
|
return 443
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, inputPort := range httpInput {
|
||||||
|
if inputPort == block.Port && idx < len(httpOutput) {
|
||||||
|
return httpOutput[idx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(httpOutput) > 0 {
|
||||||
|
return httpOutput[0]
|
||||||
|
}
|
||||||
|
return 80
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateCustomServerBlock генерирует отдельный server блок для nginx из SERVER_BLOCK
|
||||||
|
func generateCustomServerBlock(f *os.File, res ResourceData, block CustomServerBlock, outputPort int, locationDirectives string) {
|
||||||
|
protocol := "http"
|
||||||
|
if block.IsHTTPS {
|
||||||
|
protocol = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := map[bool]string{true: "secure", false: "unsecure"}[block.IsHTTPS]
|
||||||
|
upstreamName := fmt.Sprintf("%s%d_%d_%s", prefix, res.L7ResourceID, block.Port, block.ServerName)
|
||||||
|
|
||||||
|
f.WriteString("server {\n")
|
||||||
|
f.WriteString(fmt.Sprintf(" listen %d;\n", block.Port))
|
||||||
|
|
||||||
|
if len(block.Aliases) > 0 {
|
||||||
|
f.WriteString(fmt.Sprintf(" server_name %s %s;\n\n",
|
||||||
|
block.ServerName, strings.Join(block.Aliases, " ")))
|
||||||
|
} else {
|
||||||
|
f.WriteString(fmt.Sprintf(" server_name %s;\n\n", block.ServerName))
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WriteString(fmt.Sprintf(" access_log /var/log/nginx/%d_%s_port%d_access.log waf;\n",
|
||||||
|
res.L7ResourceID, sanitizeServerName(block.ServerName), block.Port))
|
||||||
|
f.WriteString(fmt.Sprintf(" error_log /var/log/nginx/%d_%s_port%d_error.log error;\n\n",
|
||||||
|
res.L7ResourceID, sanitizeServerName(block.ServerName), block.Port))
|
||||||
|
|
||||||
|
f.WriteString(" set_real_ip_from 172.16.0.0/12;\n")
|
||||||
|
f.WriteString(" real_ip_header X-Forwarded-For;\n")
|
||||||
|
f.WriteString(" real_ip_recursive on;\n\n")
|
||||||
|
|
||||||
|
writeNginxStandardSettings(f, getCustomServerDirectives(res.AppsSettings.ServerDirectivesNginxCustom))
|
||||||
|
|
||||||
|
// Кастомные location блоки из location_nginx_custom (перед location /)
|
||||||
|
if locationDirectives != "" && strings.Contains(locationDirectives, "location ") {
|
||||||
|
correctUpstream := fmt.Sprintf("%s://%s", protocol, upstreamName)
|
||||||
|
log.Printf(" └─ Добавление location_nginx_custom в SERVER_BLOCK порт %d: %s", block.Port, block.ServerName)
|
||||||
|
writeCustomLocationBlock(f, locationDirectives, correctUpstream, res.L7ResourceID, "SERVER_BLOCK", block.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WriteString(" location / {\n")
|
||||||
|
lines := strings.Split(block.Content, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed != "" {
|
||||||
|
f.WriteString(" " + trimmed + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Если locationDirectives содержит директивы без location-блока — вставляем в location /
|
||||||
|
if locationDirectives != "" && !strings.Contains(locationDirectives, "location ") {
|
||||||
|
writeIndentedLines(f, locationDirectives)
|
||||||
|
}
|
||||||
|
f.WriteString(fmt.Sprintf(" proxy_pass %s://%s;\n", protocol, upstreamName))
|
||||||
|
f.WriteString(" }\n")
|
||||||
|
f.WriteString("}\n\n")
|
||||||
|
}
|
||||||
270
ports.go
Normal file
270
ports.go
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PortAllocator отслеживает порты, выделенные в текущем запуске программы.
|
||||||
|
// Заменяет глобальную переменную allocatedPortsInCurrentRun.
|
||||||
|
type PortAllocator struct {
|
||||||
|
allocated map[int]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPortAllocator создаёт новый аллокатор портов
|
||||||
|
func NewPortAllocator() *PortAllocator {
|
||||||
|
return &PortAllocator{allocated: make(map[int]bool)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// markAsUsed помечает порт как занятый в текущем запуске.
|
||||||
|
// Используется при загрузке сохранённых портов из .ports.json,
|
||||||
|
// чтобы другие клиенты/контейнеры не получили тот же порт.
|
||||||
|
func (pa *PortAllocator) markAsUsed(port int) {
|
||||||
|
pa.allocated[port] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// allocatePortsForResources выделяет порты для всех ресурсов и контейнеров.
|
||||||
|
// excludePorts — порты, которые нужно исключить из проверки занятости (может быть nil).
|
||||||
|
// Возвращает map[l7ResourceID][]PortMapping где каждый элемент массива — порты для одного контейнера.
|
||||||
|
func (pa *PortAllocator) allocatePortsForResources(resourcesData []ResourceData, containersCount int, excludePorts map[int]bool) (map[int][]PortMapping, error) {
|
||||||
|
// Получаем список занятых портов в системе
|
||||||
|
usedPorts, err := getUsedPorts()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ошибка получения занятых портов: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Исключаем порты клиента (если передано) для переиспользования
|
||||||
|
for port := range excludePorts {
|
||||||
|
delete(usedPorts, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Объединяем с портами из текущего запуска
|
||||||
|
for port := range pa.allocated {
|
||||||
|
usedPorts[port] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resourcePortMap := make(map[int][]PortMapping)
|
||||||
|
currentHTTPPort := HTTPPortStart
|
||||||
|
currentHTTPSPort := HTTPSPortStart
|
||||||
|
|
||||||
|
for _, res := range resourcesData {
|
||||||
|
var portMappingsForResource []PortMapping
|
||||||
|
|
||||||
|
httpPortsNeeded := getInputHTTPPorts(res.AppsSettings)
|
||||||
|
httpsPortsNeeded := getInputHTTPSPorts(res.AppsSettings)
|
||||||
|
|
||||||
|
for containerNum := 0; containerNum < containersCount; containerNum++ {
|
||||||
|
portMapping := PortMapping{
|
||||||
|
HTTPPorts: make(map[int]int),
|
||||||
|
HTTPSPorts: make(map[int]int),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выделяем Docker порты для HTTP
|
||||||
|
for _, customPort := range httpPortsNeeded {
|
||||||
|
for usedPorts[currentHTTPPort] && currentHTTPPort <= HTTPPortEnd {
|
||||||
|
currentHTTPPort++
|
||||||
|
}
|
||||||
|
if currentHTTPPort > HTTPPortEnd {
|
||||||
|
return nil, fmt.Errorf("не хватает свободных HTTP портов (диапазон %d-%d исчерпан)", HTTPPortStart, HTTPPortEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
portMapping.HTTPPorts[customPort] = currentHTTPPort
|
||||||
|
usedPorts[currentHTTPPort] = true
|
||||||
|
pa.allocated[currentHTTPPort] = true
|
||||||
|
currentHTTPPort++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выделяем Docker порты для HTTPS
|
||||||
|
for _, customPort := range httpsPortsNeeded {
|
||||||
|
for usedPorts[currentHTTPSPort] && currentHTTPSPort <= HTTPSPortEnd {
|
||||||
|
currentHTTPSPort++
|
||||||
|
}
|
||||||
|
if currentHTTPSPort > HTTPSPortEnd {
|
||||||
|
return nil, fmt.Errorf("не хватает свободных HTTPS портов (диапазон %d-%d исчерпан)", HTTPSPortStart, HTTPSPortEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
portMapping.HTTPSPorts[customPort] = currentHTTPSPort
|
||||||
|
usedPorts[currentHTTPSPort] = true
|
||||||
|
pa.allocated[currentHTTPSPort] = true
|
||||||
|
currentHTTPSPort++
|
||||||
|
}
|
||||||
|
|
||||||
|
portMappingsForResource = append(portMappingsForResource, portMapping)
|
||||||
|
}
|
||||||
|
|
||||||
|
resourcePortMap[res.L7ResourceID] = portMappingsForResource
|
||||||
|
|
||||||
|
// Логирование
|
||||||
|
totalHTTPPorts := len(httpPortsNeeded) * containersCount
|
||||||
|
totalHTTPSPorts := len(httpsPortsNeeded) * containersCount
|
||||||
|
log.Printf(" → Ресурс %d: выделено %d HTTP портов и %d HTTPS портов (%d контейнеров × %d+%d портов)",
|
||||||
|
res.L7ResourceID, totalHTTPPorts, totalHTTPSPorts, containersCount, len(httpPortsNeeded), len(httpsPortsNeeded))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourcePortMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUsedPorts возвращает map занятых портов
|
||||||
|
func getUsedPorts() (map[int]bool, error) {
|
||||||
|
usedPorts := make(map[int]bool)
|
||||||
|
|
||||||
|
cmd := exec.Command("ss", "-tuln")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return usedPorts, fmt.Errorf("ошибка выполнения ss: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(output), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 5 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
localAddr := fields[4]
|
||||||
|
parts := strings.Split(localAddr, ":")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
port := 0
|
||||||
|
fmt.Sscanf(parts[len(parts)-1], "%d", &port)
|
||||||
|
if (port >= HTTPPortStart && port <= HTTPPortEnd) || (port >= HTTPSPortStart && port <= HTTPSPortEnd) {
|
||||||
|
usedPorts[port] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return usedPorts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveContainerPorts сохраняет порты контейнера в файл .ports.json
|
||||||
|
func saveContainerPorts(containerDir string, containerNum int, resources []ResourceData, portMappings []PortMapping) error {
|
||||||
|
portsFile := filepath.Join(containerDir, ".ports.json")
|
||||||
|
|
||||||
|
savedPorts := SavedContainerPorts{
|
||||||
|
ContainerNum: containerNum,
|
||||||
|
Resources: make([]SavedResourcePorts, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, res := range resources {
|
||||||
|
if idx >= len(portMappings) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
savedPorts.Resources = append(savedPorts.Resources, SavedResourcePorts{
|
||||||
|
L7ResourceID: res.L7ResourceID,
|
||||||
|
HTTPPorts: portMappings[idx].HTTPPorts,
|
||||||
|
HTTPSPorts: portMappings[idx].HTTPSPorts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(savedPorts, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ошибка сериализации портов: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(portsFile, data, 0644); err != nil {
|
||||||
|
return fmt.Errorf("ошибка записи файла портов: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadContainerPorts загружает порты контейнера из файла .ports.json
|
||||||
|
func loadContainerPorts(containerDir string, resources []ResourceData) ([]PortMapping, bool) {
|
||||||
|
portsFile := filepath.Join(containerDir, ".ports.json")
|
||||||
|
|
||||||
|
data, err := os.ReadFile(portsFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var savedPorts SavedContainerPorts
|
||||||
|
if err := json.Unmarshal(data, &savedPorts); err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка парсинга %s: %v", portsFile, err)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(savedPorts.Resources) != len(resources) {
|
||||||
|
log.Printf(" ⚠ Количество ресурсов изменилось (было %d, стало %d), перевыделяем порты",
|
||||||
|
len(savedPorts.Resources), len(resources))
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, savedRes := range savedPorts.Resources {
|
||||||
|
if savedRes.L7ResourceID != resources[idx].L7ResourceID {
|
||||||
|
log.Printf(" ⚠ Порядок ресурсов изменился (позиция %d: было %d, стало %d), перевыделяем порты",
|
||||||
|
idx, savedRes.L7ResourceID, resources[idx].L7ResourceID)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка: изменились ли кастомные порты?
|
||||||
|
for idx, savedRes := range savedPorts.Resources {
|
||||||
|
currentRes := resources[idx]
|
||||||
|
|
||||||
|
// Получаем текущие кастомные порты из БД
|
||||||
|
currentHTTPPorts := getInputHTTPPorts(currentRes.AppsSettings)
|
||||||
|
currentHTTPSPorts := getInputHTTPSPorts(currentRes.AppsSettings)
|
||||||
|
|
||||||
|
// Проверяем HTTP порты
|
||||||
|
if !portsMatch(savedRes.HTTPPorts, currentHTTPPorts) {
|
||||||
|
log.Printf(" ⚠ Кастомные HTTP порты изменились для ресурса %d, перевыделяем порты",
|
||||||
|
currentRes.L7ResourceID)
|
||||||
|
log.Printf(" Было: %v, Стало: %v", getMapKeysInt(savedRes.HTTPPorts), currentHTTPPorts)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем HTTPS порты
|
||||||
|
if !portsMatch(savedRes.HTTPSPorts, currentHTTPSPorts) {
|
||||||
|
log.Printf(" ⚠ Кастомные HTTPS порты изменились для ресурса %d, перевыделяем порты",
|
||||||
|
currentRes.L7ResourceID)
|
||||||
|
log.Printf(" Было: %v, Стало: %v", getMapKeysInt(savedRes.HTTPSPorts), currentHTTPSPorts)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
portMappings := make([]PortMapping, len(savedPorts.Resources))
|
||||||
|
for idx, savedRes := range savedPorts.Resources {
|
||||||
|
portMappings[idx] = PortMapping{
|
||||||
|
HTTPPorts: savedRes.HTTPPorts,
|
||||||
|
HTTPSPorts: savedRes.HTTPSPorts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return portMappings, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// portsMatch проверяет совпадают ли кастомные порты в сохраненном маппинге
|
||||||
|
// с текущими кастомными портами из БД
|
||||||
|
func portsMatch(savedMapping map[int]int, currentCustomPorts []int) bool {
|
||||||
|
// Проверяем что количество совпадает
|
||||||
|
if len(savedMapping) != len(currentCustomPorts) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем что все текущие кастомные порты есть в сохраненном маппинге
|
||||||
|
for _, customPort := range currentCustomPorts {
|
||||||
|
if _, exists := savedMapping[customPort]; !exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// getMapKeysInt возвращает ключи из map[int]int в виде отсортированного слайса
|
||||||
|
func getMapKeysInt(m map[int]int) []int {
|
||||||
|
keys := make([]int, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Ints(keys)
|
||||||
|
return keys
|
||||||
|
}
|
||||||
273
ptaf_processor.go
Normal file
273
ptaf_processor.go
Normal file
|
|
@ -0,0 +1,273 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// processPTAFClient обрабатывает клиента для PTAF vendor
|
||||||
|
// Вся логика из processClient() перенесена сюда без изменений
|
||||||
|
func processPTAFClient(
|
||||||
|
db *sql.DB,
|
||||||
|
config Config,
|
||||||
|
hostname, instanceName string,
|
||||||
|
clientInfo ClientInfo,
|
||||||
|
appsSettingsList []AppsSettings,
|
||||||
|
portAllocator *PortAllocator,
|
||||||
|
) bool {
|
||||||
|
log.Printf("Количество контейнеров: %d", clientInfo.ContainersCount)
|
||||||
|
if clientInfo.PTAFConfig.Valid {
|
||||||
|
log.Printf("PTAF Config: установлен (скрыт для безопасности)")
|
||||||
|
} else {
|
||||||
|
log.Printf("PTAF Config: не установлен")
|
||||||
|
}
|
||||||
|
if clientInfo.FluentBitPort.Valid {
|
||||||
|
log.Printf("Fluent Bit Port: %d", clientInfo.FluentBitPort.Int64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем принадлежность хоста к instance клиента
|
||||||
|
belongsToInstance := false
|
||||||
|
if clientInfo.WAFInstance != "" {
|
||||||
|
var err error
|
||||||
|
belongsToInstance, err = checkHostBelongsToInstance(db, hostname, clientInfo.WAFInstance)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("⚠ Ошибка проверки принадлежности хоста для %s: %v", clientInfo.ClientTitle, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если хост НЕ принадлежит instance — инициируем миграцию
|
||||||
|
if clientInfo.WAFInstance != "" && !belongsToInstance {
|
||||||
|
handleClientMigration(clientInfo, hostname)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хост принадлежит instance — обрабатываем
|
||||||
|
if clientInfo.WAFInstance == "" {
|
||||||
|
log.Printf("ℹ Клиент %s не привязан к конкретному instance, обрабатываю", clientInfo.ClientTitle)
|
||||||
|
} else {
|
||||||
|
log.Printf("✓ Клиент %s принадлежит instance '%s', обрабатываю", clientInfo.ClientTitle, clientInfo.WAFInstance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Автоочистка устаревших маркеров drain
|
||||||
|
cleanStaleDrainMarkers(clientInfo)
|
||||||
|
|
||||||
|
// Получение данных ресурсов
|
||||||
|
resourcesData := fetchPTAFResourcesData(db, config, appsSettingsList)
|
||||||
|
if len(resourcesData) == 0 {
|
||||||
|
log.Printf("⚠ Нет ресурсов с данными для клиента %s, пропускаем", clientInfo.ClientTitle)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выделение портов
|
||||||
|
resourcePortMap, err := resolvePortsForClient(clientInfo, resourcesData, portAllocator)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Ошибка выделения портов для клиента %s: %v", clientInfo.ClientTitle, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Изменен порядок операций
|
||||||
|
// БЫЛО: generateAngieConfigs (с reload) → setupContainers
|
||||||
|
// СТАЛО: generateAngieConfigs (БЕЗ reload) → setupContainers → проверка портов → reload Angie
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// 1. Генерация конфигурации Angie БЕЗ автоматического reload
|
||||||
|
angieChanged, err := generateAngieConfigsWithoutReload(config, clientInfo.ClientTitle, resourcesData, resourcePortMap)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Ошибка генерации конфигов Angie для %s: %v", clientInfo.ClientTitle, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Создание директорий и конфигов для контейнеров (пересоздание если нужно)
|
||||||
|
err = setupContainers(config, clientInfo.ClientTitle, &clientInfo, resourcesData, resourcePortMap)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Ошибка настройки контейнеров для %s: %v", clientInfo.ClientTitle, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Reload Angie только ПОСЛЕ того как все контейнеры готовы
|
||||||
|
if angieChanged {
|
||||||
|
log.Printf(" → Конфиг Angie изменился, проверяем готовность контейнеров перед reload")
|
||||||
|
|
||||||
|
// Проверяем что все порты контейнеров слушают
|
||||||
|
allPortsReady := true
|
||||||
|
totalPorts := 0
|
||||||
|
readyPorts := 0
|
||||||
|
|
||||||
|
for _, res := range resourcesData {
|
||||||
|
containerPorts, ok := resourcePortMap[res.L7ResourceID]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ports := range containerPorts {
|
||||||
|
// Проверяем HTTPS порты
|
||||||
|
for customPort, dockerPort := range ports.HTTPSPorts {
|
||||||
|
totalPorts++
|
||||||
|
if checkPortListening("127.0.0.1", dockerPort) {
|
||||||
|
readyPorts++
|
||||||
|
} else {
|
||||||
|
log.Printf(" ⚠ Порт %d (custom %d) для l7ResourceID %d не слушает!",
|
||||||
|
dockerPort, customPort, res.L7ResourceID)
|
||||||
|
allPortsReady = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем HTTP порты
|
||||||
|
for customPort, dockerPort := range ports.HTTPPorts {
|
||||||
|
totalPorts++
|
||||||
|
if checkPortListening("127.0.0.1", dockerPort) {
|
||||||
|
readyPorts++
|
||||||
|
} else {
|
||||||
|
log.Printf(" ⚠ Порт %d (custom %d) для l7ResourceID %d не слушает!",
|
||||||
|
dockerPort, customPort, res.L7ResourceID)
|
||||||
|
allPortsReady = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf(" → Портов готово: %d/%d", readyPorts, totalPorts)
|
||||||
|
|
||||||
|
if !allPortsReady {
|
||||||
|
log.Printf("⚠ Не все порты готовы, пропускаем reload Angie для безопасности")
|
||||||
|
log.Printf(" → Angie продолжит работать со старой конфигурацией")
|
||||||
|
log.Printf(" → При следующем запуске программы будет повторена попытка")
|
||||||
|
} else {
|
||||||
|
log.Printf(" ✓ Все порты слушают, безопасно перезагрузить Angie")
|
||||||
|
reloadAngie() // Функция из containers_part2.go (не возвращает error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf(" ℹ Конфиг Angie не изменился, reload не требуется")
|
||||||
|
}
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
log.Printf("Клиент %s успешно обработан", clientInfo.ClientTitle)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchPTAFResourcesData получает данные для каждого ресурса из кеша или API
|
||||||
|
// Проверяет что waf_vendor = 'ptaf'
|
||||||
|
func fetchPTAFResourcesData(db *sql.DB, config Config, appsSettingsList []AppsSettings) []ResourceData {
|
||||||
|
var resourcesData []ResourceData
|
||||||
|
|
||||||
|
for _, appsSettings := range appsSettingsList {
|
||||||
|
if !appsSettings.WAFEnabled.Valid || !appsSettings.WAFEnabled.Bool {
|
||||||
|
log.Printf("Пропускаем l7ResourceID %d: waf_enabled = false", appsSettings.L7ResourceID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем waf_vendor - обрабатываем только ресурсы PTAF
|
||||||
|
if appsSettings.WAFVendor.Valid && strings.ToLower(strings.TrimSpace(appsSettings.WAFVendor.String)) != "ptaf" {
|
||||||
|
log.Printf("Пропускаем l7ResourceID %d: waf_vendor = '%s' (требуется 'ptaf')", appsSettings.L7ResourceID, appsSettings.WAFVendor.String)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !appsSettings.WAFVendor.Valid || appsSettings.WAFVendor.String == "" {
|
||||||
|
log.Printf("Пропускаем l7ResourceID %d: waf_vendor не указан (требуется 'ptaf')", appsSettings.L7ResourceID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := "auto"
|
||||||
|
if appsSettings.Mode.Valid {
|
||||||
|
mode = strings.ToLower(strings.TrimSpace(appsSettings.Mode.String))
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode != "auto" && mode != "manual" {
|
||||||
|
log.Printf("Пропускаем l7ResourceID %d: mode = '%s' (требуется 'auto' или 'manual')", appsSettings.L7ResourceID, mode)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode == "manual" {
|
||||||
|
log.Printf("l7ResourceID %d: mode = 'manual', waf_vendor = 'ptaf' ✓", appsSettings.L7ResourceID)
|
||||||
|
} else if appsSettings.Mode.Valid {
|
||||||
|
log.Printf("l7ResourceID %d: mode = 'auto', waf_vendor = 'ptaf' ✓", appsSettings.L7ResourceID)
|
||||||
|
} else {
|
||||||
|
log.Printf("l7ResourceID %d: mode = NULL (обрабатываем как 'auto'), waf_vendor = 'ptaf' ✓", appsSettings.L7ResourceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Получение данных для l7ResourceID: %d", appsSettings.L7ResourceID)
|
||||||
|
|
||||||
|
var serverName string
|
||||||
|
var aliases []string
|
||||||
|
var origins []OriginItem
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if mode == "manual" {
|
||||||
|
serverName, aliases, origins, err = getResourceDataFromManualInfo(db, config, appsSettings.L7ResourceID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Ошибка получения данных из manual_info для l7ResourceID %d: %v", appsSettings.L7ResourceID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf(" ✓ Данные получены из manual_info (waf_vendor: ptaf)")
|
||||||
|
} else {
|
||||||
|
var found bool
|
||||||
|
serverName, aliases, origins, found, err = getResourceDataFromCache(db, config, appsSettings.L7ResourceID)
|
||||||
|
|
||||||
|
if found && err == nil {
|
||||||
|
log.Printf(" Server Name: %s", serverName)
|
||||||
|
log.Printf(" Aliases: %v", aliases)
|
||||||
|
log.Printf(" Origins: %d шт.", len(origins))
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка чтения кеша: %v, запрашиваю API", err)
|
||||||
|
} else {
|
||||||
|
log.Printf(" ℹ Данных нет в кеше, запрашиваю API")
|
||||||
|
}
|
||||||
|
|
||||||
|
serverName, err = getServerName(config, appsSettings.L7ResourceID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Ошибка получения server_name для l7ResourceID %d: %v", appsSettings.L7ResourceID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf(" Server Name: %s", serverName)
|
||||||
|
|
||||||
|
aliases, err = getAliases(config, appsSettings.L7ResourceID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Ошибка получения aliases для l7ResourceID %d: %v", appsSettings.L7ResourceID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf(" Aliases: %v", aliases)
|
||||||
|
|
||||||
|
origins, err = getOrigins(config, appsSettings.L7ResourceID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Ошибка получения origins для l7ResourceID %d: %v", appsSettings.L7ResourceID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf(" Origins: %d шт.", len(origins))
|
||||||
|
|
||||||
|
// Сохранение в кеш (опционально, можно закомментировать если функция отсутствует)
|
||||||
|
// if err := saveResourceDataToCache(db, appsSettings.L7ResourceID, serverName, aliases, origins); err != nil {
|
||||||
|
// log.Printf(" ⚠ Ошибка сохранения в кеш: %v", err)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resourcesData = append(resourcesData, ResourceData{
|
||||||
|
L7ResourceID: appsSettings.L7ResourceID,
|
||||||
|
ServerName: serverName,
|
||||||
|
Aliases: aliases,
|
||||||
|
Origins: origins,
|
||||||
|
AppsSettings: appsSettings,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourcesData
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPortListening проверяет что порт слушает на указанном адресе
|
||||||
|
// Функция уже есть в main.go, но для совместимости оставим здесь тоже
|
||||||
|
// (Go позволяет дублирование функций в одном package если они приватные)
|
||||||
|
func checkPortListeningPTAF(host string, port int) bool {
|
||||||
|
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", host, port), 2*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
810
sw_processor.go
Normal file
810
sw_processor.go
Normal file
|
|
@ -0,0 +1,810 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SW константы
|
||||||
|
const (
|
||||||
|
SWNginxBinary = "solidwall-nginx"
|
||||||
|
SWConfigDir = "/etc/solidwall-nginx"
|
||||||
|
SWSiteAvailableDir = "/etc/solidwall-nginx/site-available"
|
||||||
|
SWSiteEnabledDir = "/etc/solidwall-nginx/site-enabled"
|
||||||
|
SWStaticDir = "/etc/solidwall-nginx/static"
|
||||||
|
SWLogDir = "/var/log/solidwall"
|
||||||
|
SWInternalHTTPPort = 8000
|
||||||
|
SWInternalHTTPSPort = 9000
|
||||||
|
SWSSLCertPath = "ssl/cert.pem"
|
||||||
|
SWSSLKeyPath = "ssl/key.pem"
|
||||||
|
)
|
||||||
|
|
||||||
|
// processSWClient обрабатывает клиента для SW vendor
|
||||||
|
func processSWClient(
|
||||||
|
db *sql.DB,
|
||||||
|
config Config,
|
||||||
|
hostname, instanceName string,
|
||||||
|
clientInfo ClientInfo,
|
||||||
|
appsSettingsList []AppsSettings,
|
||||||
|
portAllocator *PortAllocator,
|
||||||
|
) bool {
|
||||||
|
log.Printf("═══════════════════════════════════════════════════════════")
|
||||||
|
log.Printf("SW VENDOR: обработка клиента %s", clientInfo.ClientTitle)
|
||||||
|
log.Printf("═══════════════════════════════════════════════════════════")
|
||||||
|
|
||||||
|
// Проверяем принадлежность хоста к instance клиента (аналогично PTAF)
|
||||||
|
belongsToInstance := false
|
||||||
|
if clientInfo.WAFInstance != "" {
|
||||||
|
var err error
|
||||||
|
belongsToInstance, err = checkHostBelongsToInstance(db, hostname, clientInfo.WAFInstance)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("⚠ Ошибка проверки принадлежности хоста для %s: %v", clientInfo.ClientTitle, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если хост НЕ принадлежит instance — пропускаем (миграция для SW не реализована)
|
||||||
|
if clientInfo.WAFInstance != "" && !belongsToInstance {
|
||||||
|
log.Printf("⚠ Клиент %s должен быть на instance '%s', текущий хост '%s' не принадлежит этому instance",
|
||||||
|
clientInfo.ClientTitle, clientInfo.WAFInstance, hostname)
|
||||||
|
log.Printf(" SW не поддерживает автоматическую миграцию, пропускаем")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хост принадлежит instance — обрабатываем
|
||||||
|
if clientInfo.WAFInstance == "" {
|
||||||
|
log.Printf("ℹ Клиент %s не привязан к конкретному instance, обрабатываю", clientInfo.ClientTitle)
|
||||||
|
} else {
|
||||||
|
log.Printf("✓ Клиент %s принадлежит instance '%s', обрабатываю", clientInfo.ClientTitle, clientInfo.WAFInstance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение данных ресурсов (фильтр по waf_vendor = 'sw')
|
||||||
|
resourcesData := fetchSWResourcesData(db, config, appsSettingsList)
|
||||||
|
if len(resourcesData) == 0 {
|
||||||
|
log.Printf("⚠ Нет ресурсов с данными для клиента %s, пропускаем", clientInfo.ClientTitle)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("✓ Получено ресурсов для SW: %d", len(resourcesData))
|
||||||
|
|
||||||
|
// Создание static файлов если отсутствуют
|
||||||
|
if err := ensureSWStaticFiles(); err != nil {
|
||||||
|
log.Printf("⚠ Ошибка создания static файлов: %v", err)
|
||||||
|
// Не критично, продолжаем
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерация конфигов для каждого ресурса
|
||||||
|
configsChanged := false
|
||||||
|
for _, res := range resourcesData {
|
||||||
|
changed, err := generateSWConfig(config, clientInfo.ClientTitle, res)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("⚠ Ошибка генерации конфига для l7ResourceID %d: %v", res.L7ResourceID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
configsChanged = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload nginx если были изменения
|
||||||
|
if configsChanged {
|
||||||
|
log.Printf("→ Конфиги SW изменились, выполняю проверку и reload")
|
||||||
|
if err := reloadSWNginx(); err != nil {
|
||||||
|
log.Printf("⚠ Ошибка reload SW nginx: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
log.Printf("✓ SW nginx успешно перезагружен")
|
||||||
|
} else {
|
||||||
|
log.Printf("ℹ Конфиги SW не изменились, reload не требуется")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("✓ Клиент %s (SW) успешно обработан", clientInfo.ClientTitle)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchSWResourcesData получает данные для каждого ресурса из кеша или API
|
||||||
|
// Проверяет что waf_vendor = 'sw'
|
||||||
|
func fetchSWResourcesData(db *sql.DB, config Config, appsSettingsList []AppsSettings) []ResourceData {
|
||||||
|
var resourcesData []ResourceData
|
||||||
|
|
||||||
|
for _, appsSettings := range appsSettingsList {
|
||||||
|
if !appsSettings.WAFEnabled.Valid || !appsSettings.WAFEnabled.Bool {
|
||||||
|
log.Printf("Пропускаем l7ResourceID %d: waf_enabled = false", appsSettings.L7ResourceID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем waf_vendor - обрабатываем только ресурсы SW
|
||||||
|
if appsSettings.WAFVendor.Valid && strings.ToLower(strings.TrimSpace(appsSettings.WAFVendor.String)) != "sw" {
|
||||||
|
log.Printf("Пропускаем l7ResourceID %d: waf_vendor = '%s' (требуется 'sw')", appsSettings.L7ResourceID, appsSettings.WAFVendor.String)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !appsSettings.WAFVendor.Valid || appsSettings.WAFVendor.String == "" {
|
||||||
|
log.Printf("Пропускаем l7ResourceID %d: waf_vendor не указан (требуется 'sw')", appsSettings.L7ResourceID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := "auto"
|
||||||
|
if appsSettings.Mode.Valid {
|
||||||
|
mode = strings.ToLower(strings.TrimSpace(appsSettings.Mode.String))
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode != "auto" && mode != "manual" {
|
||||||
|
log.Printf("Пропускаем l7ResourceID %d: mode = '%s' (требуется 'auto' или 'manual')", appsSettings.L7ResourceID, mode)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("l7ResourceID %d: mode = '%s', waf_vendor = 'sw' ✓", appsSettings.L7ResourceID, mode)
|
||||||
|
log.Printf("Получение данных для l7ResourceID: %d", appsSettings.L7ResourceID)
|
||||||
|
|
||||||
|
var serverName string
|
||||||
|
var aliases []string
|
||||||
|
var origins []OriginItem
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if mode == "manual" {
|
||||||
|
serverName, aliases, origins, err = getResourceDataFromManualInfo(db, config, appsSettings.L7ResourceID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Ошибка получения данных из manual_info для l7ResourceID %d: %v", appsSettings.L7ResourceID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf(" ✓ Данные получены из manual_info (waf_vendor: sw)")
|
||||||
|
} else {
|
||||||
|
var found bool
|
||||||
|
serverName, aliases, origins, found, err = getResourceDataFromCache(db, config, appsSettings.L7ResourceID)
|
||||||
|
|
||||||
|
if found && err == nil {
|
||||||
|
log.Printf(" Server Name: %s", serverName)
|
||||||
|
log.Printf(" Aliases: %v", aliases)
|
||||||
|
log.Printf(" Origins: %d шт.", len(origins))
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка чтения кеша: %v, запрашиваю API", err)
|
||||||
|
} else {
|
||||||
|
log.Printf(" ℹ Данных нет в кеше, запрашиваю API")
|
||||||
|
}
|
||||||
|
|
||||||
|
serverName, err = getServerName(config, appsSettings.L7ResourceID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Ошибка получения server_name для l7ResourceID %d: %v", appsSettings.L7ResourceID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf(" Server Name: %s", serverName)
|
||||||
|
|
||||||
|
aliases, err = getAliases(config, appsSettings.L7ResourceID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Ошибка получения aliases для l7ResourceID %d: %v", appsSettings.L7ResourceID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf(" Aliases: %v", aliases)
|
||||||
|
|
||||||
|
origins, err = getOrigins(config, appsSettings.L7ResourceID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Ошибка получения origins для l7ResourceID %d: %v", appsSettings.L7ResourceID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf(" Origins: %d шт.", len(origins))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resourcesData = append(resourcesData, ResourceData{
|
||||||
|
L7ResourceID: appsSettings.L7ResourceID,
|
||||||
|
ServerName: serverName,
|
||||||
|
Aliases: aliases,
|
||||||
|
Origins: origins,
|
||||||
|
AppsSettings: appsSettings,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourcesData
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateSWConfig генерирует nginx конфиг для одного ресурса SW
|
||||||
|
func generateSWConfig(config Config, clientTitle string, res ResourceData) (bool, error) {
|
||||||
|
configFilename := fmt.Sprintf("solidwall-%s-%d.conf", sanitizeFilename(clientTitle), res.L7ResourceID)
|
||||||
|
configPath := filepath.Join(SWSiteAvailableDir, configFilename)
|
||||||
|
enabledPath := filepath.Join(SWSiteEnabledDir, configFilename)
|
||||||
|
|
||||||
|
log.Printf(" → Генерация конфига SW: %s", configFilename)
|
||||||
|
|
||||||
|
// Создаём временный файл
|
||||||
|
tmpPath := configPath + ".tmp"
|
||||||
|
f, err := os.Create(tmpPath)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("создание временного файла: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// Генерация содержимого конфига
|
||||||
|
if err := writeSWConfigContent(f, config, res); err != nil {
|
||||||
|
os.Remove(tmpPath)
|
||||||
|
return false, fmt.Errorf("запись конфига: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
// Проверяем изменился ли конфиг
|
||||||
|
changed := true
|
||||||
|
if oldContent, err := os.ReadFile(configPath); err == nil {
|
||||||
|
if newContent, err := os.ReadFile(tmpPath); err == nil {
|
||||||
|
changed = string(oldContent) != string(newContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Заменяем старый конфиг новым
|
||||||
|
if err := os.Rename(tmpPath, configPath); err != nil {
|
||||||
|
os.Remove(tmpPath)
|
||||||
|
return false, fmt.Errorf("замена конфига: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём симлинк в site-enabled если не существует
|
||||||
|
if _, err := os.Lstat(enabledPath); os.IsNotExist(err) {
|
||||||
|
relPath := fmt.Sprintf("../site-available/%s", configFilename)
|
||||||
|
if err := os.Symlink(relPath, enabledPath); err != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка создания симлинка: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf(" ✓ Создан симлинк в site-enabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
log.Printf(" ✓ Конфиг %s изменён", configFilename)
|
||||||
|
} else {
|
||||||
|
log.Printf(" ℹ Конфиг %s не изменился", configFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeSWConfigContent записывает содержимое SW nginx конфига
|
||||||
|
func writeSWConfigContent(f *os.File, config Config, res ResourceData) error {
|
||||||
|
// Собираем все домены (server_name + aliases с www)
|
||||||
|
allDomains := []string{res.ServerName}
|
||||||
|
allDomains = append(allDomains, res.Aliases...)
|
||||||
|
processedDomains := processDomainsWithWWW(allDomains)
|
||||||
|
domainsStr := strings.Join(processedDomains, " ")
|
||||||
|
|
||||||
|
// Получаем input порты (что слушаем извне)
|
||||||
|
httpInputPorts := getInputHTTPPorts(res.AppsSettings)
|
||||||
|
httpsInputPorts := getInputHTTPSPorts(res.AppsSettings)
|
||||||
|
|
||||||
|
// Получаем output порты (куда проксируем к origin)
|
||||||
|
httpOutputPorts := getOutputHTTPPorts(res.AppsSettings)
|
||||||
|
httpsOutputPorts := getOutputHTTPSPorts(res.AppsSettings)
|
||||||
|
|
||||||
|
// Проверяем наличие SERVER_BLOCK в server_nginx_custom
|
||||||
|
var customServerBlocks []CustomServerBlock
|
||||||
|
var serverDirectivesWithoutBlocks string
|
||||||
|
|
||||||
|
if res.AppsSettings.ServerNginxCustom.Valid && strings.TrimSpace(res.AppsSettings.ServerNginxCustom.String) != "" {
|
||||||
|
serverDirectivesRaw := strings.TrimSpace(res.AppsSettings.ServerNginxCustom.String)
|
||||||
|
// Для SW не санитизируем домены (работаем как есть)
|
||||||
|
customServerBlocks = parseCustomServerBlocks(serverDirectivesRaw, false)
|
||||||
|
|
||||||
|
if len(customServerBlocks) > 0 {
|
||||||
|
log.Printf(" → Найдено %d кастомных SERVER_BLOCK директив для SW", len(customServerBlocks))
|
||||||
|
serverDirectivesWithoutBlocks = stripServerBlocks(serverDirectivesRaw)
|
||||||
|
} else {
|
||||||
|
serverDirectivesWithoutBlocks = serverDirectivesRaw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// location_nginx_custom
|
||||||
|
var locationDirectives string
|
||||||
|
var isFullLocationBlock bool
|
||||||
|
if res.AppsSettings.LocationNginxCustom.Valid && strings.TrimSpace(res.AppsSettings.LocationNginxCustom.String) != "" {
|
||||||
|
locationDirectives = strings.TrimSpace(res.AppsSettings.LocationNginxCustom.String)
|
||||||
|
isFullLocationBlock = strings.Contains(locationDirectives, "location ")
|
||||||
|
log.Printf(" → Обнаружен location_nginx_custom (полный блок: %v)", isFullLocationBlock)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WriteString("# SolidWall WAF configuration\n")
|
||||||
|
f.WriteString(fmt.Sprintf("# Client: %s, Resource ID: %d\n", res.ServerName, res.L7ResourceID))
|
||||||
|
f.WriteString(fmt.Sprintf("# Generated: %s\n\n", time.Now().Format("2006-01-02 15:04:05")))
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// UPSTREAM БЛОКИ
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// HTTPS upstreams (может быть несколько если custom_output_https_ports)
|
||||||
|
for idx, outputPort := range httpsOutputPorts {
|
||||||
|
upstreamName := fmt.Sprintf("secure%d", res.L7ResourceID)
|
||||||
|
if len(httpsOutputPorts) > 1 {
|
||||||
|
upstreamName = fmt.Sprintf("secure%d_%d", res.L7ResourceID, outputPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WriteString(fmt.Sprintf("upstream %s {\n", upstreamName))
|
||||||
|
writeSWUpstreamServers(f, res.Origins, outputPort, res.AppsSettings)
|
||||||
|
|
||||||
|
// Кастомные upstream директивы или дефолтные keepalive
|
||||||
|
if idx == 0 { // Применяем кастом только к первому upstream
|
||||||
|
writeSWCustomUpstreamOrDefault(f, res.AppsSettings)
|
||||||
|
} else {
|
||||||
|
writeSWDefaultUpstreamSettings(f, res.AppsSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WriteString("}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP upstreams
|
||||||
|
for idx, outputPort := range httpOutputPorts {
|
||||||
|
upstreamName := fmt.Sprintf("unsecure%d", res.L7ResourceID)
|
||||||
|
if len(httpOutputPorts) > 1 {
|
||||||
|
upstreamName = fmt.Sprintf("unsecure%d_%d", res.L7ResourceID, outputPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WriteString(fmt.Sprintf("upstream %s {\n", upstreamName))
|
||||||
|
writeSWUpstreamServers(f, res.Origins, outputPort, res.AppsSettings)
|
||||||
|
|
||||||
|
// Кастомные upstream директивы или дефолтные keepalive
|
||||||
|
if idx == 0 {
|
||||||
|
writeSWCustomUpstreamOrDefault(f, res.AppsSettings)
|
||||||
|
} else {
|
||||||
|
writeSWDefaultUpstreamSettings(f, res.AppsSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WriteString("}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// ПУБЛИЧНЫЕ SERVER БЛОКИ (принимают трафик от клиентов)
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// HTTP servers (порты из custom_input_http_ports или дефолт 80)
|
||||||
|
for _, inputPort := range httpInputPorts {
|
||||||
|
writeSWPublicHTTPServer(f, config, res, inputPort, domainsStr, serverDirectivesWithoutBlocks, locationDirectives, isFullLocationBlock)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPS servers (порты из custom_input_https_ports или дефолт 443)
|
||||||
|
for _, inputPort := range httpsInputPorts {
|
||||||
|
writeSWPublicHTTPSServer(f, config, res, inputPort, domainsStr, serverDirectivesWithoutBlocks, locationDirectives, isFullLocationBlock)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// ВНУТРЕННИЕ SERVER БЛОКИ (проксируют на origins)
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// Определяем какой upstream использовать (если несколько output портов)
|
||||||
|
httpUpstreamName := fmt.Sprintf("unsecure%d", res.L7ResourceID)
|
||||||
|
if len(httpOutputPorts) > 1 {
|
||||||
|
httpUpstreamName = fmt.Sprintf("unsecure%d_%d", res.L7ResourceID, httpOutputPorts[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
httpsUpstreamName := fmt.Sprintf("secure%d", res.L7ResourceID)
|
||||||
|
if len(httpsOutputPorts) > 1 {
|
||||||
|
httpsUpstreamName = fmt.Sprintf("secure%d_%d", res.L7ResourceID, httpsOutputPorts[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSWInternalHTTPServer(f, res, domainsStr, httpUpstreamName)
|
||||||
|
writeSWInternalHTTPSServer(f, res, domainsStr, httpsUpstreamName)
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// КАСТОМНЫЕ SERVER_BLOCK (если есть)
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
if len(customServerBlocks) > 0 {
|
||||||
|
log.Printf(" → Генерация %d кастомных SERVER_BLOCK для SW", len(customServerBlocks))
|
||||||
|
for _, block := range customServerBlocks {
|
||||||
|
generateSWCustomServerBlock(f, config, res, block, locationDirectives)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeSWUpstreamServers записывает серверы в upstream блок
|
||||||
|
func writeSWUpstreamServers(f *os.File, origins []OriginItem, port int, settings AppsSettings) {
|
||||||
|
allBackup := true
|
||||||
|
for _, origin := range origins {
|
||||||
|
if strings.ToLower(origin.Mode) != "backup" {
|
||||||
|
allBackup = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, origin := range origins {
|
||||||
|
serverLine := fmt.Sprintf(" server %s:%d", origin.IP, port)
|
||||||
|
if origin.Weight > 0 {
|
||||||
|
serverLine += fmt.Sprintf(" weight=%d", origin.Weight)
|
||||||
|
}
|
||||||
|
maxFails := getMaxFails(settings)
|
||||||
|
failTimeout := getFailTimeout(settings)
|
||||||
|
serverLine += fmt.Sprintf(" max_fails=%d fail_timeout=%ds", maxFails, failTimeout)
|
||||||
|
if strings.ToLower(origin.Mode) == "backup" && !allBackup {
|
||||||
|
serverLine += " backup"
|
||||||
|
}
|
||||||
|
serverLine += ";\n"
|
||||||
|
f.WriteString(serverLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeSWCustomUpstreamOrDefault записывает кастомные или дефолтные upstream директивы
|
||||||
|
func writeSWCustomUpstreamOrDefault(f *os.File, settings AppsSettings) {
|
||||||
|
if settings.UpstreamNginxCustom.Valid && strings.TrimSpace(settings.UpstreamNginxCustom.String) != "" {
|
||||||
|
log.Printf(" └─ Использование upstream_nginx_custom")
|
||||||
|
lines := strings.Split(strings.TrimSpace(settings.UpstreamNginxCustom.String), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed != "" {
|
||||||
|
f.WriteString(" " + trimmed + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
writeSWDefaultUpstreamSettings(f, settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeSWDefaultUpstreamSettings записывает дефолтные upstream настройки
|
||||||
|
func writeSWDefaultUpstreamSettings(f *os.File, settings AppsSettings) {
|
||||||
|
// Проверяем SNI (0 = keepalive включен, 1 = отключен)
|
||||||
|
keepaliveEnabled := true
|
||||||
|
if settings.SNI.Valid && settings.SNI.Int64 == 1 {
|
||||||
|
keepaliveEnabled = false
|
||||||
|
log.Printf(" └─ Keepalive отключен (sni = 1)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if keepaliveEnabled {
|
||||||
|
f.WriteString(" keepalive 60;\n")
|
||||||
|
f.WriteString(" keepalive_timeout 70s;\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeSWPublicHTTPServer пишет публичный HTTP server блок
|
||||||
|
func writeSWPublicHTTPServer(f *os.File, config Config, res ResourceData, port int, domainsStr string, serverDirectives string, locationDirectives string, isFullLocationBlock bool) {
|
||||||
|
f.WriteString("# Proxying HTTP requests to SolidWall WAF\n")
|
||||||
|
f.WriteString("server {\n")
|
||||||
|
f.WriteString(" include static/server.conf;\n")
|
||||||
|
f.WriteString(fmt.Sprintf(" listen %d;\n", port))
|
||||||
|
f.WriteString(fmt.Sprintf(" server_name %s;\n\n", domainsStr))
|
||||||
|
|
||||||
|
writeSWRealIPConfig(f, config)
|
||||||
|
writeSWStandardSettings(f, res.AppsSettings)
|
||||||
|
|
||||||
|
f.WriteString(fmt.Sprintf(" set $BACKEND http://127.0.0.1:%d;\n", SWInternalHTTPPort))
|
||||||
|
|
||||||
|
// Кастомные location блоки (полные) перед location /
|
||||||
|
if isFullLocationBlock {
|
||||||
|
log.Printf(" └─ Добавление location_nginx_custom (отдельный блок) для HTTP порт %d", port)
|
||||||
|
writeIndentedLines(f, locationDirectives)
|
||||||
|
f.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WriteString(" location / {\n")
|
||||||
|
|
||||||
|
// server_nginx_custom директивы внутри location / (если есть и не SERVER_BLOCK)
|
||||||
|
if serverDirectives != "" {
|
||||||
|
log.Printf(" └─ Добавление server_nginx_custom в location / (HTTP порт %d)", port)
|
||||||
|
writeIndentedLines(f, serverDirectives)
|
||||||
|
}
|
||||||
|
|
||||||
|
// location_nginx_custom директивы внутри location / (если это не полный блок)
|
||||||
|
if !isFullLocationBlock && locationDirectives != "" {
|
||||||
|
log.Printf(" └─ Добавление location_nginx_custom (директивы) в location / (HTTP порт %d)", port)
|
||||||
|
writeIndentedLines(f, locationDirectives)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WriteString(" include static/waf.conf;\n")
|
||||||
|
f.WriteString(" }\n")
|
||||||
|
f.WriteString("}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeSWPublicHTTPSServer пишет публичный HTTPS server блок
|
||||||
|
func writeSWPublicHTTPSServer(f *os.File, config Config, res ResourceData, port int, domainsStr string, serverDirectives string, locationDirectives string, isFullLocationBlock bool) {
|
||||||
|
f.WriteString("# Proxying HTTPS requests to SolidWall WAF\n")
|
||||||
|
f.WriteString("server {\n")
|
||||||
|
f.WriteString(" include static/server.conf;\n")
|
||||||
|
f.WriteString(fmt.Sprintf(" listen %d ssl;\n", port))
|
||||||
|
f.WriteString(fmt.Sprintf(" server_name %s;\n\n", domainsStr))
|
||||||
|
|
||||||
|
f.WriteString(fmt.Sprintf(" ssl_certificate %s;\n", SWSSLCertPath))
|
||||||
|
f.WriteString(fmt.Sprintf(" ssl_certificate_key %s;\n", SWSSLKeyPath))
|
||||||
|
f.WriteString(" ssl_protocols TLSv1.3 TLSv1.2;\n")
|
||||||
|
f.WriteString(" ssl_prefer_server_ciphers on;\n\n")
|
||||||
|
|
||||||
|
writeSWRealIPConfig(f, config)
|
||||||
|
writeSWStandardSettings(f, res.AppsSettings)
|
||||||
|
|
||||||
|
f.WriteString(fmt.Sprintf(" set $BACKEND http://127.0.0.1:%d;\n", SWInternalHTTPSPort))
|
||||||
|
|
||||||
|
// Кастомные location блоки (полные) перед location /
|
||||||
|
if isFullLocationBlock {
|
||||||
|
log.Printf(" └─ Добавление location_nginx_custom (отдельный блок) для HTTPS порт %d", port)
|
||||||
|
writeIndentedLines(f, locationDirectives)
|
||||||
|
f.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WriteString(" location / {\n")
|
||||||
|
|
||||||
|
// server_nginx_custom директивы внутри location /
|
||||||
|
if serverDirectives != "" {
|
||||||
|
log.Printf(" └─ Добавление server_nginx_custom в location / (HTTPS порт %d)", port)
|
||||||
|
writeIndentedLines(f, serverDirectives)
|
||||||
|
}
|
||||||
|
|
||||||
|
// location_nginx_custom директивы внутри location / (если это не полный блок)
|
||||||
|
if !isFullLocationBlock && locationDirectives != "" {
|
||||||
|
log.Printf(" └─ Добавление location_nginx_custom (директивы) в location / (HTTPS порт %d)", port)
|
||||||
|
writeIndentedLines(f, locationDirectives)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WriteString(" include static/waf.conf;\n")
|
||||||
|
f.WriteString(" }\n")
|
||||||
|
f.WriteString("}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeSWInternalHTTPServer пишет внутренний HTTP → Origin server блок
|
||||||
|
func writeSWInternalHTTPServer(f *os.File, res ResourceData, domainsStr string, upstreamName string) {
|
||||||
|
f.WriteString("# Proxying HTTP requests to origin\n")
|
||||||
|
f.WriteString("server {\n")
|
||||||
|
f.WriteString(fmt.Sprintf(" listen 127.0.0.1:%d;\n", SWInternalHTTPPort))
|
||||||
|
f.WriteString(fmt.Sprintf(" server_name %s;\n\n", domainsStr))
|
||||||
|
|
||||||
|
f.WriteString(" set_real_ip_from 127.0.0.1;\n")
|
||||||
|
f.WriteString(" real_ip_header X-Forwarded-For;\n")
|
||||||
|
f.WriteString(" real_ip_recursive on;\n\n")
|
||||||
|
|
||||||
|
writeSWStandardSettings(f, res.AppsSettings)
|
||||||
|
|
||||||
|
f.WriteString(" proxy_set_header CDN-Loop \"WAF\";\n\n")
|
||||||
|
f.WriteString(fmt.Sprintf(" error_log %s/%d_error.log error;\n\n", SWLogDir, res.L7ResourceID))
|
||||||
|
|
||||||
|
f.WriteString(" location / {\n")
|
||||||
|
f.WriteString(fmt.Sprintf(" proxy_pass http://%s;\n", upstreamName))
|
||||||
|
f.WriteString(" }\n")
|
||||||
|
f.WriteString("}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeSWInternalHTTPSServer пишет внутренний HTTPS → Origin server блок
|
||||||
|
func writeSWInternalHTTPSServer(f *os.File, res ResourceData, domainsStr string, upstreamName string) {
|
||||||
|
f.WriteString("# Proxying HTTPS requests to origin\n")
|
||||||
|
f.WriteString("server {\n")
|
||||||
|
f.WriteString(fmt.Sprintf(" listen 127.0.0.1:%d;\n", SWInternalHTTPSPort))
|
||||||
|
f.WriteString(fmt.Sprintf(" server_name %s;\n\n", domainsStr))
|
||||||
|
|
||||||
|
f.WriteString(" proxy_ssl_server_name on;\n")
|
||||||
|
f.WriteString(" proxy_ssl_name $host;\n\n")
|
||||||
|
|
||||||
|
f.WriteString(" set_real_ip_from 127.0.0.1;\n")
|
||||||
|
f.WriteString(" real_ip_header X-Forwarded-For;\n")
|
||||||
|
f.WriteString(" real_ip_recursive on;\n\n")
|
||||||
|
|
||||||
|
writeSWStandardSettings(f, res.AppsSettings)
|
||||||
|
|
||||||
|
f.WriteString(" proxy_set_header CDN-Loop \"WAF\";\n\n")
|
||||||
|
f.WriteString(fmt.Sprintf(" error_log %s/%d_error.log error;\n\n", SWLogDir, res.L7ResourceID))
|
||||||
|
|
||||||
|
f.WriteString(" location / {\n")
|
||||||
|
f.WriteString(fmt.Sprintf(" proxy_pass https://%s;\n", upstreamName))
|
||||||
|
f.WriteString(" }\n")
|
||||||
|
f.WriteString("}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateSWCustomServerBlock генерирует кастомный SERVER_BLOCK для SW
|
||||||
|
func generateSWCustomServerBlock(f *os.File, config Config, res ResourceData, block CustomServerBlock, locationDirectives string) {
|
||||||
|
|
||||||
|
aliasInfo := ""
|
||||||
|
if len(block.Aliases) > 0 {
|
||||||
|
aliasInfo = fmt.Sprintf(" + %d alias(es)", len(block.Aliases))
|
||||||
|
}
|
||||||
|
log.Printf(" └─ SW SERVER_BLOCK: порт %d, server_name %s%s", block.Port, block.ServerName, aliasInfo)
|
||||||
|
|
||||||
|
f.WriteString("# Custom SERVER_BLOCK for SW\n")
|
||||||
|
f.WriteString("server {\n")
|
||||||
|
f.WriteString(" include static/server.conf;\n")
|
||||||
|
|
||||||
|
if block.IsHTTPS {
|
||||||
|
f.WriteString(fmt.Sprintf(" listen %d ssl;\n", block.Port))
|
||||||
|
f.WriteString(fmt.Sprintf(" ssl_certificate %s;\n", SWSSLCertPath))
|
||||||
|
f.WriteString(fmt.Sprintf(" ssl_certificate_key %s;\n", SWSSLKeyPath))
|
||||||
|
f.WriteString(" ssl_protocols TLSv1.3 TLSv1.2;\n")
|
||||||
|
f.WriteString(" ssl_prefer_server_ciphers on;\n\n")
|
||||||
|
} else {
|
||||||
|
f.WriteString(fmt.Sprintf(" listen %d;\n", block.Port))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(block.Aliases) > 0 {
|
||||||
|
f.WriteString(fmt.Sprintf(" server_name %s %s;\n\n", block.ServerName, strings.Join(block.Aliases, " ")))
|
||||||
|
} else {
|
||||||
|
f.WriteString(fmt.Sprintf(" server_name %s;\n\n", block.ServerName))
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSWRealIPConfig(f, config)
|
||||||
|
writeSWStandardSettings(f, res.AppsSettings)
|
||||||
|
|
||||||
|
internalPort := SWInternalHTTPPort
|
||||||
|
if block.IsHTTPS {
|
||||||
|
internalPort = SWInternalHTTPSPort
|
||||||
|
}
|
||||||
|
f.WriteString(fmt.Sprintf(" set $BACKEND http://127.0.0.1:%d;\n", internalPort))
|
||||||
|
|
||||||
|
// Кастомные location блоки (если есть и это полный блок)
|
||||||
|
isFullLocationBlock := strings.Contains(locationDirectives, "location ")
|
||||||
|
if isFullLocationBlock && locationDirectives != "" {
|
||||||
|
log.Printf(" └─ Добавление location_nginx_custom в SERVER_BLOCK")
|
||||||
|
writeIndentedLines(f, locationDirectives)
|
||||||
|
f.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WriteString(" location / {\n")
|
||||||
|
|
||||||
|
// Контент из SERVER_BLOCK тега
|
||||||
|
lines := strings.Split(block.Content, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed != "" {
|
||||||
|
f.WriteString(" " + trimmed + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// location_nginx_custom директивы (если это не полный блок)
|
||||||
|
if !isFullLocationBlock && locationDirectives != "" {
|
||||||
|
writeIndentedLines(f, locationDirectives)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WriteString(" include static/waf.conf;\n")
|
||||||
|
f.WriteString(" }\n")
|
||||||
|
f.WriteString("}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeSWRealIPConfig пишет real_ip конфигурацию (antibot networks)
|
||||||
|
func writeSWRealIPConfig(f *os.File, config Config) {
|
||||||
|
f.WriteString(" # antibot networks\n")
|
||||||
|
for _, net := range config.AntibotNetworks {
|
||||||
|
f.WriteString(fmt.Sprintf(" set_real_ip_from %s;\n", net))
|
||||||
|
}
|
||||||
|
f.WriteString(" real_ip_header X-Forwarded-For;\n")
|
||||||
|
f.WriteString(" real_ip_recursive on;\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeSWStandardSettings пишет стандартные настройки nginx
|
||||||
|
func writeSWStandardSettings(f *os.File, settings AppsSettings) {
|
||||||
|
// Проверяем есть ли server_directives_nginx_custom
|
||||||
|
if settings.ServerDirectivesNginxCustom.Valid && strings.TrimSpace(settings.ServerDirectivesNginxCustom.String) != "" {
|
||||||
|
log.Printf(" └─ Использование server_directives_nginx_custom (замена дефолтных настроек)")
|
||||||
|
|
||||||
|
// Пишем только основные директивы (без таймаутов/буферов)
|
||||||
|
f.WriteString(" server_tokens off;\n")
|
||||||
|
f.WriteString(" sendfile on;\n")
|
||||||
|
f.WriteString(" gzip on;\n\n")
|
||||||
|
|
||||||
|
f.WriteString(" proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;\n")
|
||||||
|
f.WriteString(" proxy_http_version 1.1;\n\n")
|
||||||
|
|
||||||
|
f.WriteString(" #proxy_set_header X-Real-IP $remote_addr;\n")
|
||||||
|
f.WriteString(" #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n")
|
||||||
|
f.WriteString(" proxy_set_header Connection \"upgrade\";\n")
|
||||||
|
f.WriteString(" proxy_set_header Upgrade $http_upgrade;\n")
|
||||||
|
f.WriteString(" proxy_set_header Host $host;\n")
|
||||||
|
f.WriteString(" proxy_pass_header Date;\n")
|
||||||
|
f.WriteString(" proxy_pass_header Server;\n\n")
|
||||||
|
|
||||||
|
// Кастомные директивы (заменяют таймауты/буферы)
|
||||||
|
f.WriteString(" # Custom server directives (override defaults)\n")
|
||||||
|
lines := strings.Split(strings.TrimSpace(settings.ServerDirectivesNginxCustom.String), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed != "" {
|
||||||
|
f.WriteString(" " + trimmed + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.WriteString("\n")
|
||||||
|
} else {
|
||||||
|
// Дефолтные настройки
|
||||||
|
f.WriteString(" server_tokens off;\n")
|
||||||
|
f.WriteString(" sendfile on;\n")
|
||||||
|
f.WriteString(" gzip on;\n")
|
||||||
|
f.WriteString(" client_max_body_size 1g;\n")
|
||||||
|
f.WriteString(" large_client_header_buffers 4 128k;\n\n")
|
||||||
|
|
||||||
|
f.WriteString(" proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;\n")
|
||||||
|
f.WriteString(" proxy_http_version 1.1;\n")
|
||||||
|
f.WriteString(" proxy_connect_timeout 3s;\n")
|
||||||
|
f.WriteString(" proxy_busy_buffers_size 32k;\n")
|
||||||
|
f.WriteString(" proxy_buffers 4 32k;\n")
|
||||||
|
f.WriteString(" proxy_buffer_size 16k;\n")
|
||||||
|
f.WriteString(" proxy_read_timeout 60s;\n")
|
||||||
|
f.WriteString(" proxy_send_timeout 60s;\n")
|
||||||
|
f.WriteString(" send_timeout 60s;\n")
|
||||||
|
f.WriteString(" keepalive_timeout 75s;\n")
|
||||||
|
f.WriteString(" keepalive_requests 1000;\n")
|
||||||
|
f.WriteString(" resolver_timeout 30s;\n\n")
|
||||||
|
|
||||||
|
f.WriteString(" #proxy_set_header X-Real-IP $remote_addr;\n")
|
||||||
|
f.WriteString(" #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n")
|
||||||
|
f.WriteString(" proxy_set_header Connection \"upgrade\";\n")
|
||||||
|
f.WriteString(" proxy_set_header Upgrade $http_upgrade;\n")
|
||||||
|
f.WriteString(" proxy_set_header Host $host;\n")
|
||||||
|
f.WriteString(" proxy_pass_header Date;\n")
|
||||||
|
f.WriteString(" proxy_pass_header Server;\n\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureSWStaticFiles создаёт static файлы если отсутствуют
|
||||||
|
func ensureSWStaticFiles() error {
|
||||||
|
if err := os.MkdirAll(SWStaticDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("создание директории %s: %w", SWStaticDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// static/waf.conf
|
||||||
|
wafConfPath := filepath.Join(SWStaticDir, "waf.conf")
|
||||||
|
if _, err := os.Stat(wafConfPath); os.IsNotExist(err) {
|
||||||
|
wafContent := `uninitialized_variable_warn off;
|
||||||
|
rewrite_by_lua_block { require "solidwall.waf.nginx"() }
|
||||||
|
content_by_lua_block { require "solidwall.waf.nginx"() }
|
||||||
|
log_by_lua_block { require "solidwall.waf.nginx"() }
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(wafConfPath, []byte(wafContent), 0644); err != nil {
|
||||||
|
return fmt.Errorf("создание waf.conf: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("✓ Создан файл: %s", wafConfPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// static/server.conf
|
||||||
|
serverConfPath := filepath.Join(SWStaticDir, "server.conf")
|
||||||
|
if _, err := os.Stat(serverConfPath); os.IsNotExist(err) {
|
||||||
|
serverContent := `root conf/templates;
|
||||||
|
set $solidwall_request_id "";
|
||||||
|
set $solidwall_request_decision "";
|
||||||
|
set $solidwall_request_analysis_time "";
|
||||||
|
set $solidwall_response_decision "";
|
||||||
|
set $solidwall_response_analysis_time "";
|
||||||
|
set $solidwall_backend_response_status_code "";
|
||||||
|
set $solidwall_backend_header_time "";
|
||||||
|
set $reason "";
|
||||||
|
location = /__sw {
|
||||||
|
client_max_body_size 8k;
|
||||||
|
client_body_buffer_size 8k;
|
||||||
|
content_by_lua_block { require "solidwall.waf.nginx.external_api"() }
|
||||||
|
}
|
||||||
|
include user/server.conf;
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(serverConfPath, []byte(serverContent), 0644); err != nil {
|
||||||
|
return fmt.Errorf("создание server.conf: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("✓ Создан файл: %s", serverConfPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// reloadSWNginx выполняет проверку и reload nginx
|
||||||
|
func reloadSWNginx() error {
|
||||||
|
// Проверка конфигурации
|
||||||
|
log.Printf(" → Проверка конфигурации SW nginx...")
|
||||||
|
testCmd := exec.Command(SWNginxBinary, "-t")
|
||||||
|
if output, err := testCmd.CombinedOutput(); err != nil {
|
||||||
|
log.Printf(" ✗ Ошибка проверки конфигурации:\n%s", string(output))
|
||||||
|
return fmt.Errorf("nginx -t failed: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf(" ✓ Конфигурация валидна")
|
||||||
|
|
||||||
|
// Reload
|
||||||
|
log.Printf(" → Перезагрузка SW nginx...")
|
||||||
|
reloadCmd := exec.Command(SWNginxBinary, "-s", "reload")
|
||||||
|
if output, err := reloadCmd.CombinedOutput(); err != nil {
|
||||||
|
log.Printf(" ✗ Ошибка reload:\n%s", string(output))
|
||||||
|
return fmt.Errorf("nginx -s reload failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitizeFilename очищает строку для использования в имени файла
|
||||||
|
func sanitizeFilename(s string) string {
|
||||||
|
s = strings.ReplaceAll(s, "/", "_")
|
||||||
|
s = strings.ReplaceAll(s, "\\", "_")
|
||||||
|
s = strings.ReplaceAll(s, ":", "_")
|
||||||
|
s = strings.ReplaceAll(s, "*", "_")
|
||||||
|
s = strings.ReplaceAll(s, "?", "_")
|
||||||
|
s = strings.ReplaceAll(s, "\"", "_")
|
||||||
|
s = strings.ReplaceAll(s, "<", "_")
|
||||||
|
s = strings.ReplaceAll(s, ">", "_")
|
||||||
|
s = strings.ReplaceAll(s, "|", "_")
|
||||||
|
return s
|
||||||
|
}
|
||||||
146
types.go
Normal file
146
types.go
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Структура для таблицы instances
|
||||||
|
type Instance struct {
|
||||||
|
Hostname string
|
||||||
|
Instance string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Структура для таблицы apps_settings (бывшая appsSettings)
|
||||||
|
type AppsSettings struct {
|
||||||
|
L7ResourceID int
|
||||||
|
WAFEnabled sql.NullBool
|
||||||
|
WAFVendor sql.NullString // Vendor WAF: ptaf, sw и т.д.
|
||||||
|
CustomInputHTTPPorts pq.Int64Array // Массив HTTP портов для прослушивания (Angie и контейнер)
|
||||||
|
CustomInputHTTPSPorts pq.Int64Array // Массив HTTPS портов для прослушивания (Angie и контейнер)
|
||||||
|
CustomOutputHTTPPorts pq.Int64Array // Массив HTTP портов для проксирования к origin
|
||||||
|
CustomOutputHTTPSPorts pq.Int64Array // Массив HTTPS портов для проксирования к origin
|
||||||
|
UpstreamAngieCustom sql.NullString
|
||||||
|
ServerNginxCustom sql.NullString
|
||||||
|
ServerAngieCustom sql.NullString
|
||||||
|
ClientTitle string
|
||||||
|
UpstreamNginxCustom sql.NullString
|
||||||
|
LocationAngieCustom sql.NullString
|
||||||
|
LocationNginxCustom sql.NullString
|
||||||
|
ServerDirectivesAngieCustom sql.NullString // Кастомные server директивы для Angie
|
||||||
|
ServerDirectivesNginxCustom sql.NullString // Кастомные server директивы для Nginx
|
||||||
|
SNI sql.NullInt64 // 0 = keepalive включен, 1 = keepalive отключен
|
||||||
|
Mode sql.NullString // Режим работы ресурса: auto = автоматическое управление, manual = ручное, disabled = отключен
|
||||||
|
SSLEnabled sql.NullBool // true = SSL включен (по умолчанию), false = plain HTTP без SSL
|
||||||
|
CustomAngieSSL sql.NullString // Кастомные SSL директивы для Angie - полностью заменяют дефолтные если заполнено
|
||||||
|
MaxFails sql.NullInt64 // Количество неудачных попыток к origin (дефолт 5)
|
||||||
|
FailTimeout sql.NullInt64 // Время в секундах после которого origin снова доступен (дефолт 15)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Структура для таблицы client_info (бывшая nodes)
|
||||||
|
type ClientInfo struct {
|
||||||
|
ContainersCount int
|
||||||
|
PTAFConfig sql.NullString
|
||||||
|
ClientTitle string
|
||||||
|
FluentBitPort sql.NullInt64
|
||||||
|
WAFInstance string // Instance для связи с apps_settings
|
||||||
|
}
|
||||||
|
|
||||||
|
// Структуры для API ответов
|
||||||
|
type OriginResponse struct {
|
||||||
|
Data struct {
|
||||||
|
Result struct {
|
||||||
|
Items []struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Weight int `json:"weight"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
CreatedAt int64 `json:"createdAt"`
|
||||||
|
} `json:"items"`
|
||||||
|
} `json:"result"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AliasResponse struct {
|
||||||
|
Data struct {
|
||||||
|
Result struct {
|
||||||
|
Items []struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
UseCustomSSL int `json:"useCustomSsl"`
|
||||||
|
UseLetsEncrypt int `json:"useLetsencryptSsl"`
|
||||||
|
SSLExpireDate int64 `json:"sslExpireDate"`
|
||||||
|
CreatedAt int64 `json:"createdAt"`
|
||||||
|
L7ResourceID int `json:"l7ResourceId"`
|
||||||
|
} `json:"items"`
|
||||||
|
} `json:"result"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceResponse struct {
|
||||||
|
Data struct {
|
||||||
|
Result struct {
|
||||||
|
L7ResourceID int `json:"l7ResourceId"`
|
||||||
|
L7ResourceName string `json:"l7ResourceName"`
|
||||||
|
CreatedAt int64 `json:"createdAt"`
|
||||||
|
L7ResourceActive int `json:"l7ResourceIsActive"`
|
||||||
|
ModifiedAt int64 `json:"modifiedAt"`
|
||||||
|
ProtectedIP string `json:"protectedIp"`
|
||||||
|
HTTP2Support int `json:"http2Support"`
|
||||||
|
GRPCSupport int `json:"grpcSupport"`
|
||||||
|
WAFMode int `json:"wafMode"`
|
||||||
|
WAFEnabled int `json:"wafEnabled"`
|
||||||
|
} `json:"result"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Структура для хранения данных о ресурсе
|
||||||
|
type ResourceData struct {
|
||||||
|
L7ResourceID int
|
||||||
|
ServerName string
|
||||||
|
Aliases []string
|
||||||
|
Origins []OriginItem
|
||||||
|
AppsSettings AppsSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
type OriginItem struct {
|
||||||
|
IP string
|
||||||
|
Weight int
|
||||||
|
Mode string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Структура для портов
|
||||||
|
type PortMapping struct {
|
||||||
|
HTTPPorts map[int]int // map[customPort]dockerPort - для HTTP портов
|
||||||
|
HTTPSPorts map[int]int // map[customPort]dockerPort - для HTTPS портов
|
||||||
|
}
|
||||||
|
|
||||||
|
// Структура для отсортированного вывода портов в docker-compose
|
||||||
|
type SortedPortMapping struct {
|
||||||
|
CustomPort int
|
||||||
|
DockerPort int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Структура для сохранения портов ресурса в файл
|
||||||
|
type SavedResourcePorts struct {
|
||||||
|
L7ResourceID int `json:"l7resourceid"`
|
||||||
|
HTTPPorts map[int]int `json:"http_ports"` // map[customPort]dockerPort
|
||||||
|
HTTPSPorts map[int]int `json:"https_ports"` // map[customPort]dockerPort
|
||||||
|
}
|
||||||
|
|
||||||
|
// Структура для сохранения всех портов контейнера
|
||||||
|
type SavedContainerPorts struct {
|
||||||
|
ContainerNum int `json:"container_num"`
|
||||||
|
Resources []SavedResourcePorts `json:"resources"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Структура для кастомного server блока
|
||||||
|
type CustomServerBlock struct {
|
||||||
|
Port int
|
||||||
|
ServerName string
|
||||||
|
Aliases []string
|
||||||
|
Content string
|
||||||
|
IsHTTPS bool
|
||||||
|
}
|
||||||
337
utils.go
Normal file
337
utils.go
Normal file
|
|
@ -0,0 +1,337 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/hex"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// writeFileIfChanged записывает файл только если содержимое изменилось
|
||||||
|
func writeFileIfChanged(filepath string, content string) (bool, error) {
|
||||||
|
// Проверяем существует ли файл
|
||||||
|
oldHash := ""
|
||||||
|
if _, err := os.Stat(filepath); err == nil {
|
||||||
|
var hashErr error
|
||||||
|
oldHash, hashErr = getFileHash(filepath)
|
||||||
|
if hashErr != nil {
|
||||||
|
log.Printf(" ⚠ Ошибка чтения старого файла, перезаписываем")
|
||||||
|
oldHash = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newHash := getStringHash(content)
|
||||||
|
|
||||||
|
if oldHash != "" && oldHash == newHash {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldHash != "" {
|
||||||
|
log.Printf(" → Хеш изменился: old=%s... new=%s...", oldHash[:8], newHash[:8])
|
||||||
|
}
|
||||||
|
|
||||||
|
err := os.WriteFile(filepath, []byte(content), 0644)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFileHash вычисляет SHA256 хеш файла
|
||||||
|
func getFileHash(filepath string) (string, error) {
|
||||||
|
file, err := os.Open(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
hash := sha256.New()
|
||||||
|
if _, err := io.Copy(hash, file); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getStringHash вычисляет SHA256 хеш строки
|
||||||
|
func getStringHash(content string) string {
|
||||||
|
hash := sha256.Sum256([]byte(content))
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractPodIPFromConfig извлекает POD_IP из существующего nginx.conf
|
||||||
|
func extractPodIPFromConfig(filepath string) string {
|
||||||
|
data, err := os.ReadFile(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, "env POD_IP=") {
|
||||||
|
parts := strings.Split(line, "=")
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
ip := strings.TrimSpace(strings.TrimSuffix(parts[1], ";"))
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// processDomainsWithWWW обрабатывает список доменов и добавляет www. варианты если нужно
|
||||||
|
func processDomainsWithWWW(domains []string) []string {
|
||||||
|
domainMap := make(map[string]bool)
|
||||||
|
for _, domain := range domains {
|
||||||
|
domainMap[domain] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]string, 0, len(domains)*2)
|
||||||
|
|
||||||
|
for _, domain := range domains {
|
||||||
|
result = append(result, domain)
|
||||||
|
|
||||||
|
if strings.HasPrefix(domain, "*.") || strings.HasPrefix(domain, "www.") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
wwwDomain := "www." + domain
|
||||||
|
if !domainMap[wwwDomain] {
|
||||||
|
result = append(result, wwwDomain)
|
||||||
|
domainMap[wwwDomain] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitizeDomainForNginx санитизирует домен для nginx конфигов
|
||||||
|
// Заменяет _ на - чтобы соответствовать санитизации в Angie
|
||||||
|
func sanitizeDomainForNginx(domain string) string {
|
||||||
|
return strings.ReplaceAll(domain, "_", "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitizeDomainsForNginx санитизирует массив доменов для nginx конфигов
|
||||||
|
func sanitizeDomainsForNginx(domains []string) []string {
|
||||||
|
result := make([]string, len(domains))
|
||||||
|
for i, domain := range domains {
|
||||||
|
result[i] = sanitizeDomainForNginx(domain)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitizeServerName убирает недопустимые символы из server_name для использования в имени файла
|
||||||
|
func sanitizeServerName(serverName string) string {
|
||||||
|
sanitized := strings.ReplaceAll(serverName, ".", "_")
|
||||||
|
sanitized = strings.ReplaceAll(sanitized, "*", "wildcard")
|
||||||
|
return sanitized
|
||||||
|
}
|
||||||
|
|
||||||
|
// replaceMTLSPaths заменяет пути к mTLS сертификатам на путь внутри контейнера
|
||||||
|
func replaceMTLSPaths(content string) string {
|
||||||
|
content = strings.ReplaceAll(content, "/etc/ssl/certs/", "/opt/ptaf/conf/main/")
|
||||||
|
content = strings.ReplaceAll(content, "/etc/ssl/private/", "/opt/ptaf/conf/main/")
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
// getMapKeys возвращает ключи из map в виде слайса (для логирования)
|
||||||
|
func getMapKeys(m map[string]bool) []string {
|
||||||
|
keys := make([]string, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasMTLSCertificates проверяет содержит ли строка директивы mTLS сертификатов
|
||||||
|
func hasMTLSCertificates(content string) bool {
|
||||||
|
return strings.Contains(content, "proxy_ssl_certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCustomServerDirectives извлекает кастомные server директивы из sql.NullString
|
||||||
|
func getCustomServerDirectives(nullStr sql.NullString) string {
|
||||||
|
if nullStr.Valid && strings.TrimSpace(nullStr.String) != "" {
|
||||||
|
return strings.TrimSpace(nullStr.String)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCustomServerBlocks парсит директивы для поиска [SERVER_BLOCK] директив.
|
||||||
|
// sanitize = true для Nginx (санитизирует домены _ → -), false для Angie (оставляет оригинальные домены)
|
||||||
|
// Синтаксис:
|
||||||
|
//
|
||||||
|
// [SERVER_BLOCK:PORT:SERVERNAME]...[/SERVER_BLOCK]
|
||||||
|
// [SERVER_BLOCK:PORT:SERVERNAME:ALIAS1:ALIAS2:...]...[/SERVER_BLOCK]
|
||||||
|
func parseCustomServerBlocks(customDirectives string, sanitize bool) []CustomServerBlock {
|
||||||
|
blocks := []CustomServerBlock{}
|
||||||
|
|
||||||
|
if customDirectives == "" {
|
||||||
|
return blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
re := regexp.MustCompile(`(?s)\[SERVER_BLOCK:(\d+):([^\]]+)\]\s*\n(.*?)\[/SERVER_BLOCK\]`)
|
||||||
|
matches := re.FindAllStringSubmatch(customDirectives, -1)
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
port, _ := strconv.Atoi(match[1])
|
||||||
|
namesRaw := strings.TrimSpace(match[2])
|
||||||
|
content := strings.TrimSpace(match[3])
|
||||||
|
|
||||||
|
names := strings.Split(namesRaw, ":")
|
||||||
|
|
||||||
|
var serverName string
|
||||||
|
if sanitize {
|
||||||
|
serverName = sanitizeDomainForNginx(strings.TrimSpace(names[0]))
|
||||||
|
} else {
|
||||||
|
serverName = strings.TrimSpace(names[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
var aliases []string
|
||||||
|
if len(names) > 1 {
|
||||||
|
for _, alias := range names[1:] {
|
||||||
|
trimmed := strings.TrimSpace(alias)
|
||||||
|
if trimmed != "" {
|
||||||
|
if sanitize {
|
||||||
|
aliases = append(aliases, sanitizeDomainForNginx(trimmed))
|
||||||
|
} else {
|
||||||
|
aliases = append(aliases, trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isHTTPS := port == 443 || port >= 8443
|
||||||
|
|
||||||
|
content = replaceMTLSPaths(content)
|
||||||
|
|
||||||
|
blocks = append(blocks, CustomServerBlock{
|
||||||
|
Port: port,
|
||||||
|
ServerName: serverName,
|
||||||
|
Aliases: aliases,
|
||||||
|
Content: content,
|
||||||
|
IsHTTPS: isHTTPS,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
// stripServerBlocks удаляет SERVER_BLOCK директивы из строки
|
||||||
|
func stripServerBlocks(s string) string {
|
||||||
|
re := regexp.MustCompile(`(?s)\[SERVER_BLOCK:\d+:[^\]]+\].*?\[/SERVER_BLOCK\]\s*`)
|
||||||
|
return strings.TrimSpace(re.ReplaceAllString(s, ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectServerBlockDomains собирает домены из SERVER_BLOCK по портам и возвращает
|
||||||
|
// (byPort map[port]map[domain]bool, allDomains map[domain]bool)
|
||||||
|
func collectServerBlockDomains(blocks []CustomServerBlock) (map[int]map[string]bool, map[string]bool) {
|
||||||
|
byPort := make(map[int]map[string]bool)
|
||||||
|
all := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, block := range blocks {
|
||||||
|
if byPort[block.Port] == nil {
|
||||||
|
byPort[block.Port] = make(map[string]bool)
|
||||||
|
}
|
||||||
|
byPort[block.Port][block.ServerName] = true
|
||||||
|
all[block.ServerName] = true
|
||||||
|
for _, alias := range block.Aliases {
|
||||||
|
byPort[block.Port][alias] = true
|
||||||
|
all[alias] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return byPort, all
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterDomainsForPort фильтрует домены для конкретного порта, исключая те, что в SERVER_BLOCK
|
||||||
|
func filterDomainsForPort(port int, allDomains []string, serverBlockDomainsByPort map[int]map[string]bool, allServerBlockDomains map[string]bool) []string {
|
||||||
|
var result []string
|
||||||
|
|
||||||
|
for _, domain := range allDomains {
|
||||||
|
if serverBlockDomainsByPort[port][domain] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if port == 80 || port == 443 {
|
||||||
|
result = append(result, domain)
|
||||||
|
} else {
|
||||||
|
// Кастомный порт: исключаем ВСЕ домены из SERVER_BLOCK
|
||||||
|
if !allServerBlockDomains[domain] {
|
||||||
|
result = append(result, domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeIndentedLines записывает строки с отступами для nginx location блоков
|
||||||
|
func writeIndentedLines(w stringWriter, content string) {
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.TrimSpace(line) != "" {
|
||||||
|
if strings.HasPrefix(line, " ") {
|
||||||
|
w.WriteString(line + "\n")
|
||||||
|
} else if strings.HasPrefix(line, " ") {
|
||||||
|
w.WriteString(" " + line + "\n")
|
||||||
|
} else {
|
||||||
|
w.WriteString(" " + line + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeCustomLocationBlock записывает кастомный location блок с правильным proxy_pass
|
||||||
|
func writeCustomLocationBlock(w stringWriter, customLocationStr string, correctUpstream string, resID int, protocol string, port int) {
|
||||||
|
hasProxyPass := strings.Contains(customLocationStr, "proxy_pass")
|
||||||
|
lines := strings.Split(customLocationStr, "\n")
|
||||||
|
|
||||||
|
if hasProxyPass {
|
||||||
|
for i, line := range lines {
|
||||||
|
if strings.Contains(line, "proxy_pass") {
|
||||||
|
trimmed := strings.TrimLeft(line, " \t")
|
||||||
|
indent := line[:len(line)-len(trimmed)]
|
||||||
|
lines[i] = indent + "proxy_pass " + correctUpstream + ";"
|
||||||
|
log.Printf(" └─ Заменен proxy_pass в кастомном location: %s", correctUpstream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var locationLines []string
|
||||||
|
inLocationBlock := false
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(trimmed, "location ") {
|
||||||
|
inLocationBlock = true
|
||||||
|
}
|
||||||
|
if trimmed == "}" {
|
||||||
|
if !hasProxyPass && inLocationBlock {
|
||||||
|
locationLines = append(locationLines, " proxy_pass "+correctUpstream+";")
|
||||||
|
log.Printf(" └─ Добавлен proxy_pass в кастомный location: %s", correctUpstream)
|
||||||
|
}
|
||||||
|
inLocationBlock = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") {
|
||||||
|
locationLines = append(locationLines, line)
|
||||||
|
} else {
|
||||||
|
locationLines = append(locationLines, " "+line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range locationLines {
|
||||||
|
w.WriteString(line + "\n")
|
||||||
|
}
|
||||||
|
w.WriteString("\n")
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue