magos/containers_part2.go
2026-04-24 17:44:32 +03:00

809 lines
29 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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())
}