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