809 lines
29 KiB
Go
809 lines
29 KiB
Go
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"log"
|
||
"math/rand"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"text/template"
|
||
"time"
|
||
)
|
||
|
||
// isAngieEstabOnPort проверяет есть ли активные ESTAB соединения Angie на данном порту
|
||
func isAngieEstabOnPort(port int) bool {
|
||
cmd := exec.Command("ss", "-tnap")
|
||
output, err := cmd.Output()
|
||
if err != nil {
|
||
return false
|
||
}
|
||
portStr := fmt.Sprintf(":%d", port)
|
||
for _, line := range strings.Split(string(output), "\n") {
|
||
fields := strings.Fields(line)
|
||
if len(fields) < 4 {
|
||
continue
|
||
}
|
||
if fields[0] != "ESTAB" {
|
||
continue
|
||
}
|
||
localAddr := fields[3]
|
||
if strings.HasSuffix(localAddr, portStr) && strings.Contains(line, "angie") {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// isPortBusyByAnyProcess проверяет занят ли порт процессом который мешает биндингу.
|
||
// Проверяет только LISTEN сокеты и docker-proxy ESTAB — соединения Angie не блокируют биндинг Docker.
|
||
func isPortBusyByAnyProcess(port int) bool {
|
||
cmd := exec.Command("ss", "-tnap")
|
||
output, err := cmd.Output()
|
||
if err != nil {
|
||
return false
|
||
}
|
||
portStr := fmt.Sprintf(":%d", port)
|
||
for _, line := range strings.Split(string(output), "\n") {
|
||
fields := strings.Fields(line)
|
||
if len(fields) < 4 {
|
||
continue
|
||
}
|
||
localAddr := fields[3] // Local address:port
|
||
if !strings.HasSuffix(localAddr, portStr) {
|
||
continue
|
||
}
|
||
state := fields[0]
|
||
// LISTEN — порт занят, нельзя биндить
|
||
if state == "LISTEN" {
|
||
return true
|
||
}
|
||
// ESTAB от docker-proxy — старый контейнер ещё не остановился
|
||
if state == "ESTAB" && strings.Contains(line, "docker-proxy") {
|
||
return true
|
||
}
|
||
// TIME-WAIT — Docker может биндить поверх TIME-WAIT через SO_REUSEADDR
|
||
// поэтому не блокируем
|
||
}
|
||
return false
|
||
}
|
||
|
||
func recreateContainer(containerFullName, composeFile, containerDir, clientTitle string, resources []ResourceData, resourcePortMap map[int][]PortMapping, containerNum int) bool {
|
||
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 false
|
||
}
|
||
}
|
||
}
|
||
|
||
// Reload Angie после остановки контейнера чтобы закрыть keepalive ESTAB соединения
|
||
log.Printf(" → Reload Angie для закрытия keepalive соединений к остановленному контейнеру...")
|
||
reloadAngie()
|
||
|
||
// Ждём пока Angie закроет ESTAB соединения к портам старого контейнера
|
||
log.Printf(" → Ожидаем закрытия ESTAB соединений Angie на портах %v...", portsToFree)
|
||
for attempt := 0; attempt < 30; attempt++ {
|
||
angieEstab := false
|
||
for _, port := range portsToFree {
|
||
if isAngieEstabOnPort(port) {
|
||
angieEstab = true
|
||
break
|
||
}
|
||
}
|
||
if !angieEstab {
|
||
log.Printf(" ✓ Все ESTAB соединения Angie закрыты (попытка %d)", attempt+1)
|
||
break
|
||
}
|
||
if attempt%5 == 0 && attempt > 0 {
|
||
log.Printf(" → Angie ещё держит соединения (попытка %d/30)...", attempt+1)
|
||
}
|
||
time.Sleep(1 * time.Second)
|
||
}
|
||
time.Sleep(1 * time.Second)
|
||
|
||
if len(portsToFree) > 0 {
|
||
log.Printf(" → Ожидаем освобождения %d портов...", len(portsToFree))
|
||
allPortsFree := false
|
||
for attempt := 0; attempt < 180; attempt++ {
|
||
portsBusy := 0
|
||
busyPortsList := make([]int, 0)
|
||
for _, port := range portsToFree {
|
||
if isPortBusyByAnyProcess(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)
|
||
}
|
||
time.Sleep(1 * time.Second)
|
||
}
|
||
if !allPortsFree {
|
||
log.Printf(" ⚠ Порты не освободились за 180 секунд!")
|
||
log.Printf(" → Проверьте вручную: sudo lsof -i :%d", portsToFree[0])
|
||
return false
|
||
}
|
||
}
|
||
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 false
|
||
}
|
||
time.Sleep(5 * time.Second)
|
||
_, isRunning := checkContainerStatus(containerFullName)
|
||
if !isRunning {
|
||
log.Printf(" ⚠ Контейнер %s не запустился. Попробуйте вручную: cd %s && docker-compose up -d", containerFullName, containerDir)
|
||
return false
|
||
}
|
||
log.Printf(" ✓ Контейнер %s пересоздан с новыми портами", containerFullName)
|
||
return true
|
||
}
|
||
|
||
func startContainer(containerFullName, composeFile, containerDir string) bool {
|
||
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 false
|
||
}
|
||
log.Printf(" ✓ Контейнер %s запущен", containerFullName)
|
||
return true
|
||
}
|
||
|
||
// getContainerStartedAt возвращает время запуска контейнера
|
||
func getContainerStartedAt(containerName string) (time.Time, error) {
|
||
cmd := exec.Command("docker", "inspect", "--format", "{{.State.StartedAt}}", containerName)
|
||
output, err := cmd.Output()
|
||
if err != nil {
|
||
return time.Time{}, fmt.Errorf("ошибка получения времени запуска: %w", err)
|
||
}
|
||
startedAt, err := time.Parse(time.RFC3339Nano, strings.TrimSpace(string(output)))
|
||
if err != nil {
|
||
return time.Time{}, fmt.Errorf("ошибка парсинга времени: %w", err)
|
||
}
|
||
return startedAt, nil
|
||
}
|
||
|
||
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, isMigration bool) error {
|
||
markerFile := fmt.Sprintf("/tmp/ptaf-angie-drain-%s", clientTitle)
|
||
content := fmt.Sprintf("%d", time.Now().Unix())
|
||
if isMigration {
|
||
content += ":migration"
|
||
}
|
||
return os.WriteFile(markerFile, []byte(content), 0644)
|
||
}
|
||
|
||
func getAngieConfigDrainInfo(clientTitle string) (timestamp int64, isMigration bool, found bool) {
|
||
markerFile := fmt.Sprintf("/tmp/ptaf-angie-drain-%s", clientTitle)
|
||
data, err := os.ReadFile(markerFile)
|
||
if err != nil {
|
||
return 0, false, false
|
||
}
|
||
parts := strings.SplitN(strings.TrimSpace(string(data)), ":", 2)
|
||
ts, err := strconv.ParseInt(parts[0], 10, 64)
|
||
if err != nil {
|
||
return 0, false, false
|
||
}
|
||
migration := len(parts) == 2 && parts[1] == "migration"
|
||
return ts, migration, 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, debug bool, ptafFallbackCode 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)
|
||
|
||
// Настройки логирования зависят от режима debug
|
||
ptafLogLevel := "INFO"
|
||
debugEnv := "false"
|
||
errorLogLevel := "notice"
|
||
if debug {
|
||
ptafLogLevel = "DEBUG"
|
||
debugEnv = "true"
|
||
errorLogLevel = "debug"
|
||
}
|
||
|
||
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=%s;
|
||
env DEBUG=%s;
|
||
env HOSTNAME=%s;
|
||
error_log stderr %s;
|
||
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",'
|
||
'"request_id":"$http_x_request_id",'
|
||
'"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 %s;
|
||
proxy_read_timeout 120s;
|
||
proxy_send_timeout 120s;
|
||
send_timeout 60s;
|
||
keepalive_timeout 80s;
|
||
keepalive_requests 1000;
|
||
proxy_buffer_size 64k;
|
||
proxy_buffers 8 64k;
|
||
proxy_busy_buffers_size 128k;
|
||
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, ptafLogLevel, debugEnv, hostname, errorLogLevel, config.WorkerConnections, clientTitle, ptafFallbackCode, 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, containerFullName string, resources []ResourceData, portMappings []PortMapping) (bool, error) {
|
||
type ComposeData struct {
|
||
ClientTitle string
|
||
ContainerNum int
|
||
ContainerFullName string
|
||
DockerImage string
|
||
PTAFConfig string
|
||
WorkerProcesses int
|
||
WorkerConnections int
|
||
FluentBitEnabled bool
|
||
FluentBitPort int
|
||
HasMTLS bool
|
||
Debug bool
|
||
ShmSize int
|
||
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 })
|
||
|
||
dockerImage := config.DockerImage
|
||
if clientInfo.DockerImage.Valid && clientInfo.DockerImage.String != "" {
|
||
dockerImage = clientInfo.DockerImage.String
|
||
}
|
||
|
||
shmSize := clientInfo.ShmSize
|
||
if shmSize <= 0 {
|
||
shmSize = 1
|
||
}
|
||
|
||
data := ComposeData{
|
||
ClientTitle: clientTitle,
|
||
ContainerNum: containerNum,
|
||
ContainerFullName: containerFullName,
|
||
DockerImage: dockerImage,
|
||
PTAFConfig: "",
|
||
WorkerProcesses: config.WorkerProcesses,
|
||
WorkerConnections: config.WorkerConnections,
|
||
FluentBitEnabled: false,
|
||
HasMTLS: hasMTLS,
|
||
Debug: clientInfo.Debug,
|
||
ShmSize: shmSize,
|
||
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: '{{.ShmSize}}gb'
|
||
container_name: {{.ContainerFullName}}
|
||
hostname: {{.ContainerFullName}}
|
||
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}}
|
||
{{- if .Debug}}
|
||
- PTAF_DEBUG=True
|
||
- ERROR_LOG_LEVEL=debug
|
||
{{- end}}
|
||
sysctls:
|
||
kernel.sem: "32000 1024000000 500 32000"
|
||
ulimits:
|
||
nofile:
|
||
soft: 1048576
|
||
hard: 1048576
|
||
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())
|
||
}
|