Added new docs and sid blocks

This commit is contained in:
Magnus Root 2026-06-03 16:07:28 +03:00
parent b1d0436452
commit 80571bf47f
20 changed files with 280 additions and 24 deletions

0
.gitignore vendored Normal file → Executable file
View file

46
ARCHITECTURE.md Normal file → Executable file
View file

@ -117,6 +117,8 @@ waf-host-02 | instance-c
- sp_antiddos # Использовать антиддос сети в set_real_ip (дефолт: true) - sp_antiddos # Использовать антиддос сети в set_real_ip (дефолт: true)
- real_ip_networks # Кастомные сети для set_real_ip если sp_antiddos = false (inet[]) - real_ip_networks # Кастомные сети для set_real_ip если sp_antiddos = false (inet[])
- worker_processes # Количество worker процессов nginx внутри контейнера (дефолт: 4) - worker_processes # Количество worker процессов nginx внутри контейнера (дефолт: 4)
- one_container # Все SID в один контейнер (дефолт: true). При false — блочный режим
- sid_block # Блоки SID для разбивки по контейнерам: {sid1,sid2}{sid3} (TEXT)
``` ```
**Важно:** `waf_instance` определяет на каком хосте должен работать клиент **Важно:** `waf_instance` определяет на каком хосте должен работать клиент
@ -260,24 +262,46 @@ main() → initConfig() → connectToDB()
4. fetchPTAFResourcesData — получение данных ресурсов из API/кеша 4. fetchPTAFResourcesData — получение данных ресурсов из API/кеша
5. resolvePortsForClient — загрузка/выделение портов 5. one_container = false И sid_block задан?
├─ Да → processPTAFClientBlocks (блочный режим, см. ниже)
└─ Нет → стандартный режим:
6. resolvePortsForClient — загрузка/выделение портов
├─ .ports.json существует → переиспользуем порты ├─ .ports.json существует → переиспользуем порты
├─ Scale up → сохраняем старые порты, добавляем новые ├─ Scale up → сохраняем старые порты, добавляем новые
└─ Нет файла → выделяем все заново └─ Нет файла → выделяем все заново
6. generateAngieConfigsWithoutReload — генерация Angie конфига 7. generateAngieConfigsWithoutReload — генерация Angie конфига
7. setupContainers — генерация конфигов и управление контейнерами 8. setupContainers — генерация конфигов и управление контейнерами
├─ Запущен недавно, порты не слушают → пересоздаём ├─ Запущен недавно, порты не слушают → пересоздаём
├─ Compose изменился → пересоздаём ├─ Compose изменился → пересоздаём
├─ Конфиг изменился → nginx -s reload ├─ Конфиг изменился → nginx -s reload
└─ Новый → запускаем (с ожиданием освобождения портов) └─ Новый → запускаем (с ожиданием освобождения портов)
8. Проверка портов → reload Angie если нужно 9. Проверка портов → reload Angie если нужно
9. processDrainingContainers — drain лишних контейнеров 10. processDrainingContainers — drain лишних контейнеров
``` ```
### 4. Блочный режим (one_container = false)
Используется когда разные группы SID должны обслуживаться разными наборами контейнеров.
```
sid_block = '{10111,10231}{10112}{10113}'
Блок 1 → SID 10111, 10231 → контейнеры {clientTitle}_block1_AZ3_hn01_a001..a{N}
Блок 2 → SID 10112 → контейнеры {clientTitle}_block2_AZ3_hn01_a001..a{N}
Блок 3 → SID 10113 → контейнеры {clientTitle}_block3_AZ3_hn01_a001..a{N}
```
- `containers_count` применяется к каждому блоку независимо
- Каждый блок получает свои Angie конфиги и nginx конфиги только со своими SID
- SID не указанные ни в одном блоке не обрабатываются
- Имя контейнера: `{clientTitle}_block{N}_{AZx}_{hn/n}{NN}_a{001}`
- Каждый блок обрабатывается через стандартный `resolvePortsForClient``generateAngieConfigsWithoutReload``setupContainers`
### 4. Обработка SW-клиента ### 4. Обработка SW-клиента
``` ```
@ -786,7 +810,17 @@ UPDATE client_info SET shm_size = 8 WHERE client_title = 'CLIENT001';
UPDATE apps_settings SET balancing_method = 'least_conn' WHERE l7resourceid = 12345; UPDATE apps_settings SET balancing_method = 'least_conn' WHERE l7resourceid = 12345;
``` ```
### Настроить ptaf_fallback ### Включить блочный режим (разбивка SID по контейнерам)
```sql
-- Три блока SID, каждый в своём наборе контейнеров
UPDATE client_info
SET one_container = false,
sid_block = '{10111,10231}{10112}{10113}'
WHERE client_title = 'CLIENT001';
```
Формат `sid_block`: `{sid1,sid2}{sid3}{sid4,sid5,sid6}` — каждая пара `{}` это один блок.
```sql ```sql
-- Пропускать трафик если WAF недоступен -- Пропускать трафик если WAF недоступен

43
DATABASE_SCHEMA.md Normal file → Executable file
View file

@ -102,6 +102,8 @@ WHERE hostname = $1 AND instance = $2;
| `sp_antiddos` | BOOLEAN | Использовать антиддос сети в set_real_ip | `true` | | `sp_antiddos` | BOOLEAN | Использовать антиддос сети в set_real_ip | `true` |
| `real_ip_networks` | INET[] | Кастомные сети для set_real_ip (если sp_antiddos=false) | `{10.0.0.0/8}` | | `real_ip_networks` | INET[] | Кастомные сети для set_real_ip (если sp_antiddos=false) | `{10.0.0.0/8}` |
| `worker_processes` | INTEGER | Количество worker процессов nginx | `4` | | `worker_processes` | INTEGER | Количество worker процессов nginx | `4` |
| `one_container` | BOOLEAN | Все SID в один контейнер | `true` |
| `sid_block` | TEXT | Блоки SID для разбивки: `{sid1,sid2}{sid3}` | `NULL` |
### Используемые запросы ### Используемые запросы
@ -121,7 +123,9 @@ SELECT
ptaf_fallback_code, ptaf_fallback_code,
sp_antiddos, sp_antiddos,
real_ip_networks, real_ip_networks,
worker_processes worker_processes,
one_container,
sid_block
FROM client_info FROM client_info
WHERE waf_instance = $1; WHERE waf_instance = $1;
``` ```
@ -144,7 +148,9 @@ SELECT
ptaf_fallback_code, ptaf_fallback_code,
sp_antiddos, sp_antiddos,
real_ip_networks, real_ip_networks,
worker_processes worker_processes,
one_container,
sid_block
FROM client_info FROM client_info
WHERE client_title = $1; WHERE client_title = $1;
``` ```
@ -177,6 +183,12 @@ ALTER TABLE client_info
ALTER TABLE client_info ALTER TABLE client_info
ADD COLUMN IF NOT EXISTS worker_processes INTEGER DEFAULT 4; ADD COLUMN IF NOT EXISTS worker_processes INTEGER DEFAULT 4;
ALTER TABLE client_info
ADD COLUMN IF NOT EXISTS one_container BOOLEAN DEFAULT TRUE;
ALTER TABLE client_info
ADD COLUMN IF NOT EXISTS sid_block TEXT DEFAULT NULL;
``` ```
### Ключевые поля ### Ключевые поля
@ -220,7 +232,29 @@ UPDATE client_info SET ptaf_fallback_code = '503' WHERE client_title = 'CLIENT00
UPDATE client_info SET worker_processes = 8 WHERE client_title = 'CLIENT001'; UPDATE client_info SET worker_processes = 8 WHERE client_title = 'CLIENT001';
``` ```
#### sp_antiddos / real_ip_networks #### one_container / sid_block
`one_container = true` (дефолт) — все SID клиента обслуживаются одним набором контейнеров. Стандартный режим.
`one_container = false` + заполненный `sid_block` — блочный режим. Каждый блок `{}` в `sid_block` создаёт отдельный набор контейнеров только со своими SID.
**Формат `sid_block`:** `{sid1,sid2}{sid3}{sid4,sid5}` — каждая пара фигурных скобок это один блок.
```sql
-- Три блока: первый с двумя SID, второй и третий с одним
UPDATE client_info
SET one_container = false,
sid_block = '{10111,10231}{10112}{10113}'
WHERE client_title = 'CLIENT001';
```
**Именование контейнеров в блочном режиме:**
- Блок 1 → `{clientTitle}_block1_AZ3_hn01_a001`
- Блок 2 → `{clientTitle}_block2_AZ3_hn01_a001`
**containers_count** применяется к каждому блоку — если `containers_count=2` и 3 блока → 6 контейнеров суммарно.
**SID не указанные ни в одном блоке не обрабатываются.**
`sp_antiddos = true` (дефолт) — в Angie используются антиддос сети из таблицы `ips` для `set_real_ip_from`. `sp_antiddos = true` (дефолт) — в Angie используются антиддос сети из таблицы `ips` для `set_real_ip_from`.
@ -597,6 +631,9 @@ SELECT client_title FROM client_info WHERE debug = true;
-- Клиенты с индивидуальным образом -- Клиенты с индивидуальным образом
SELECT client_title, docker_image FROM client_info WHERE docker_image IS NOT NULL; SELECT client_title, docker_image FROM client_info WHERE docker_image IS NOT NULL;
-- Клиенты в блочном режиме
SELECT client_title, sid_block FROM client_info WHERE one_container = false;
-- Клиенты с нестандартным количеством worker процессов -- Клиенты с нестандартным количеством worker процессов
SELECT client_title, worker_processes FROM client_info WHERE worker_processes != 4; SELECT client_title, worker_processes FROM client_info WHERE worker_processes != 4;

0
LICENSE Normal file → Executable file
View file

0
README.md Normal file → Executable file
View file

0
api.go Normal file → Executable file
View file

0
config.go Normal file → Executable file
View file

0
containers_part1.go Normal file → Executable file
View file

0
containers_part2.go Normal file → Executable file
View file

8
db.go Normal file → Executable file
View file

@ -99,7 +99,7 @@ func getClientInfoByInstance(db *sql.DB, instance string) ([]ClientInfo, error)
query := ` query := `
SELECT containers_count, ptaf_config, client_title, fluent_bit_port, waf_instance, SELECT containers_count, ptaf_config, client_title, fluent_bit_port, waf_instance,
docker_image, docker_image_download, debug, shm_size, ptaf_fallback_code, docker_image, docker_image_download, debug, shm_size, ptaf_fallback_code,
sp_antiddos, real_ip_networks, worker_processes sp_antiddos, real_ip_networks, worker_processes, one_container, sid_block
FROM client_info FROM client_info
WHERE waf_instance = $1 WHERE waf_instance = $1
` `
@ -131,6 +131,8 @@ func getClientInfoByInstance(db *sql.DB, instance string) ([]ClientInfo, error)
&ci.SpAntiddos, &ci.SpAntiddos,
pq.Array(&ci.RealIPNetworks), pq.Array(&ci.RealIPNetworks),
&ci.WorkerProcesses, &ci.WorkerProcesses,
&ci.OneContainer,
&ci.SidBlock,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("ошибка сканирования строки: %w", err) return nil, fmt.Errorf("ошибка сканирования строки: %w", err)
@ -217,7 +219,7 @@ func getClientInfoByClientTitle(db *sql.DB, clientTitle string) (*ClientInfo, er
query := ` query := `
SELECT containers_count, ptaf_config, client_title, fluent_bit_port, waf_instance, SELECT containers_count, ptaf_config, client_title, fluent_bit_port, waf_instance,
docker_image, docker_image_download, debug, shm_size, ptaf_fallback_code, docker_image, docker_image_download, debug, shm_size, ptaf_fallback_code,
sp_antiddos, real_ip_networks, worker_processes sp_antiddos, real_ip_networks, worker_processes, one_container, sid_block
FROM client_info FROM client_info
WHERE client_title = $1 WHERE client_title = $1
` `
@ -238,6 +240,8 @@ func getClientInfoByClientTitle(db *sql.DB, clientTitle string) (*ClientInfo, er
&clientInfo.SpAntiddos, &clientInfo.SpAntiddos,
pq.Array(&clientInfo.RealIPNetworks), pq.Array(&clientInfo.RealIPNetworks),
&clientInfo.WorkerProcesses, &clientInfo.WorkerProcesses,
&clientInfo.OneContainer,
&clientInfo.SidBlock,
) )
if err != nil { if err != nil {
if strings.Contains(err.Error(), "does not exist") { if strings.Contains(err.Error(), "does not exist") {

0
drain.go Normal file → Executable file
View file

0
go.mod Normal file → Executable file
View file

0
go.sum Normal file → Executable file
View file

22
main.go Normal file → Executable file
View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"database/sql" "database/sql"
"fmt" "fmt"
"io" "io"
@ -26,6 +27,22 @@ func updateLockStatus(status string) {
lockStatusFile.WriteString(fmt.Sprintf("%d\n%s\n", os.Getpid(), status)) lockStatusFile.WriteString(fmt.Sprintf("%d\n%s\n", os.Getpid(), status))
} }
// checkDockerAvailable проверяет доступность Docker daemon с таймаутом
func checkDockerAvailable() error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "docker", "info", "--format", "{{.ServerVersion}}")
output, err := cmd.Output()
if ctx.Err() == context.DeadlineExceeded {
return fmt.Errorf("таймаут подключения к Docker daemon (10 сек) — возможно Docker не запущен")
}
if err != nil {
return fmt.Errorf("ошибка подключения к Docker daemon: %w", err)
}
log.Printf("✓ Docker daemon доступен, версия: %s", strings.TrimSpace(string(output)))
return nil
}
func main() { func main() {
// ════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════
// Lock-файл для предотвращения параллельного запуска // Lock-файл для предотвращения параллельного запуска
@ -110,6 +127,11 @@ func main() {
log.Fatalf("Ошибка загрузки конфигурации: %v", err) log.Fatalf("Ошибка загрузки конфигурации: %v", err)
} }
// Проверяем доступность Docker daemon
if err := checkDockerAvailable(); err != nil {
log.Fatalf("❌ Docker daemon недоступен: %v", err)
}
instanceNames, err := getInstancesByHostname(db, hostname) instanceNames, err := getInstancesByHostname(db, hostname)
if err != nil { if err != nil {
log.Fatalf("Ошибка получения instances для hostname %s: %v", hostname, err) log.Fatalf("Ошибка получения instances для hostname %s: %v", hostname, err)

0
nginx.go Normal file → Executable file
View file

0
ports.go Normal file → Executable file
View file

117
ptaf_processor.go Normal file → Executable file
View file

@ -81,6 +81,11 @@ func processPTAFClient(
return false return false
} }
// Если one_container = false — разбиваем SID по блокам и обрабатываем каждый блок отдельно
if !clientInfo.OneContainer && clientInfo.SidBlock.Valid && clientInfo.SidBlock.String != "" {
return processPTAFClientBlocks(db, config, hostname, instanceName, clientInfo, resourcesData, portAllocator)
}
// Выделение портов // Выделение портов
resourcePortMap, err := resolvePortsForClient(clientInfo, resourcesData, portAllocator) resourcePortMap, err := resolvePortsForClient(clientInfo, resourcesData, portAllocator)
if err != nil { if err != nil {
@ -289,3 +294,115 @@ func checkPortListeningPTAF(host string, port int) bool {
conn.Close() conn.Close()
return true return true
} }
// processPTAFClientBlocks обрабатывает клиента с разбивкой SID по блокам контейнеров
// Используется когда one_container = false
func processPTAFClientBlocks(
db *sql.DB,
config Config,
hostname, instanceName string,
clientInfo ClientInfo,
resourcesData []ResourceData,
portAllocator *PortAllocator,
) bool {
blocks, err := parseSidBlocks(clientInfo.SidBlock.String)
if err != nil {
log.Printf("⚠ Ошибка парсинга sid_block для %s: %v", clientInfo.ClientTitle, err)
return false
}
if len(blocks) == 0 {
log.Printf("⚠ sid_block пустой для %s", clientInfo.ClientTitle)
return false
}
log.Printf(" → Режим блоков: %d блоков SID", len(blocks))
// Строим map SID → ResourceData для быстрого поиска
sidToResource := make(map[int]ResourceData)
for _, res := range resourcesData {
sidToResource[res.L7ResourceID] = res
}
success := true
for blockIdx, sidList := range blocks {
blockNum := blockIdx + 1
blockName := fmt.Sprintf("block%d", blockNum)
log.Printf(" → Обработка блока %d: SID %v", blockNum, sidList)
// Собираем ресурсы для этого блока
var blockResources []ResourceData
for _, sid := range sidList {
if res, ok := sidToResource[sid]; ok {
blockResources = append(blockResources, res)
} else {
log.Printf(" ⚠ SID %d не найден в данных ресурсов", sid)
}
}
if len(blockResources) == 0 {
log.Printf(" ⚠ Нет данных для блока %d, пропускаем", blockNum)
continue
}
// Создаём clientInfo для этого блока с модифицированным именем
blockClientInfo := clientInfo
blockClientInfo.ClientTitle = fmt.Sprintf("%s_%s", clientInfo.ClientTitle, blockName)
// Выделяем порты для блока
blockPortMap, err := resolvePortsForClient(blockClientInfo, blockResources, portAllocator)
if err != nil {
log.Printf(" ⚠ Ошибка выделения портов для блока %d: %v", blockNum, err)
success = false
continue
}
// Генерируем конфиги Angie для блока
angieChanged, err := generateAngieConfigsWithoutReload(config, blockClientInfo.ClientTitle, blockClientInfo, blockResources, blockPortMap, blockClientInfo.Debug)
if err != nil {
log.Printf(" ⚠ Ошибка генерации конфигов Angie для блока %d: %v", blockNum, err)
success = false
continue
}
// Настраиваем контейнеры для блока
err = setupContainers(config, blockClientInfo.ClientTitle, &blockClientInfo, blockResources, blockPortMap, hostname)
if err != nil {
log.Printf(" ⚠ Ошибка настройки контейнеров для блока %d: %v", blockNum, err)
success = false
continue
}
// Reload Angie если нужно
if angieChanged {
allPortsReady := true
for _, res := range blockResources {
containerPorts, ok := blockPortMap[res.L7ResourceID]
if !ok {
continue
}
for _, ports := range containerPorts {
for _, dockerPort := range ports.HTTPSPorts {
if !checkPortListening("127.0.0.1", dockerPort) {
allPortsReady = false
break
}
}
if !allPortsReady {
break
}
}
if !allPortsReady {
break
}
}
if allPortsReady {
reloadAngie()
} else {
log.Printf(" ⚠ Не все порты готовы для блока %d, пропускаем reload Angie", blockNum)
}
}
log.Printf(" ✓ Блок %d обработан успешно", blockNum)
}
return success
}

0
sw_processor.go Normal file → Executable file
View file

2
types.go Normal file → Executable file
View file

@ -56,6 +56,8 @@ type ClientInfo struct {
SpAntiddos bool // Использовать антиддос сети для set_real_ip (дефолт true) SpAntiddos bool // Использовать антиддос сети для set_real_ip (дефолт true)
RealIPNetworks []string // Кастомные сети для set_real_ip если SpAntiddos = false RealIPNetworks []string // Кастомные сети для set_real_ip если SpAntiddos = false
WorkerProcesses int // Количество worker процессов nginx (дефолт 4) WorkerProcesses int // Количество worker процессов nginx (дефолт 4)
OneContainer bool // Все SID в один контейнер (дефолт true)
SidBlock sql.NullString // Блоки SID для разбивки по контейнерам: {sid1,sid2}{sid3}
} }
// Структуры для API ответов // Структуры для API ответов

40
utils.go Normal file → Executable file
View file

@ -384,3 +384,43 @@ func buildContainerFullName(clientTitle string, containerNum int, hostname strin
// prefix = "AZ3_hn01", итог: "PSB_CFA_AZ3_hn01_a001" // prefix = "AZ3_hn01", итог: "PSB_CFA_AZ3_hn01_a001"
return fmt.Sprintf("%s_%s_a%03d", clientTitle, prefix, containerNum) return fmt.Sprintf("%s_%s_a%03d", clientTitle, prefix, containerNum)
} }
// parseSidBlocks парсит строку вида {10111,10231}{10112} в срезы блоков SID
// Возвращает [][]int где каждый элемент — блок SID для одного набора контейнеров
func parseSidBlocks(sidBlock string) ([][]int, error) {
sidBlock = strings.TrimSpace(sidBlock)
if sidBlock == "" {
return nil, fmt.Errorf("пустая строка sid_block")
}
var blocks [][]int
i := 0
for i < len(sidBlock) {
if sidBlock[i] != '{' {
return nil, fmt.Errorf("ожидался '{' на позиции %d", i)
}
end := strings.Index(sidBlock[i:], "}")
if end < 0 {
return nil, fmt.Errorf("не найдена закрывающая '}' начиная с позиции %d", i)
}
inner := sidBlock[i+1 : i+end]
parts := strings.Split(inner, ",")
var block []int
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "" {
continue
}
var sid int
if _, err := fmt.Sscanf(p, "%d", &sid); err != nil {
return nil, fmt.Errorf("невалидный SID '%s': %w", p, err)
}
block = append(block, sid)
}
if len(block) > 0 {
blocks = append(blocks, block)
}
i += end + 1
}
return blocks, nil
}