package main import ( "fmt" "log" "os" "os/exec" "path/filepath" "strings" "time" ) func setupContainers(config Config, clientTitle string, clientInfo *ClientInfo, resources []ResourceData, resourcePortMap map[int][]PortMapping, hostname string) 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) containerFullName := buildContainerFullName(clientTitle, containerNum, hostname) log.Printf(" Обработка контейнера: %s", containerName) confDDir := filepath.Join("/home/install/conf/ptaf-nginx", clientTitle, fmt.Sprintf("ptaf-agent%03d", containerNum), "conf.d") dirs := []string{ containerDir, confDDir, } 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) } } // conf.d должна быть доступна пользователю ptaf внутри контейнера if err := os.Chmod(confDDir, 0777); err != nil { return fmt.Errorf("ошибка установки прав для conf.d %s: %w", confDDir, err) } 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, clientInfo.Debug, getPtafFallbackCode(*clientInfo)) 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") // Удаляем конфиги ресурсов которых больше нет в БД activeConfFiles := make(map[string]bool) for _, res := range resources { activeConfFiles[fmt.Sprintf("%s_%d.conf", clientTitle, res.L7ResourceID)] = true } if entries, err := os.ReadDir(confDPath); err == nil { for _, entry := range entries { if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".conf") { if !activeConfFiles[entry.Name()] { stalePath := filepath.Join(confDPath, entry.Name()) if err := os.Remove(stalePath); err == nil { log.Printf(" → Удалён устаревший конфиг ресурса: %s", entry.Name()) containerChanges[containerNum] = true } else { log.Printf(" ⚠ Ошибка удаления устаревшего конфига %s: %v", entry.Name(), err) } } } } } 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, containerFullName, 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 } // Сохраняем порты только если compose не изменился (нет пересоздания) // Если compose изменился — сохраним после успешного запуска контейнера if !composeChanges[containerNum] { 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 := buildContainerFullName(clientTitle, containerNum, hostname) containerExists, containerRunning := checkContainerStatus(containerFullName) // Собираем порты контейнера для сохранения после успешного запуска containerPorts := []PortMapping{} for _, res := range resources { ports, ok := resourcePortMap[res.L7ResourceID] if !ok || containerNum-1 >= len(ports) { continue } containerPorts = append(containerPorts, ports[containerNum-1]) } if containerExists { if composeChanges[containerNum] { success := recreateContainer(containerFullName, composeFile, containerDir, clientTitle, resources, resourcePortMap, containerNum) if success { if err := saveContainerPorts(containerDir, containerNum, resources, containerPorts); err != nil { log.Printf(" ⚠ Ошибка сохранения портов: %v", err) } } continue } if !containerRunning { startContainer(containerFullName, composeFile, containerDir) } else { // Проверяем порты только если контейнер запущен недавно (менее 5 минут) // Это позволяет исправить ситуацию когда контейнер поднялся без портов // но не трогает давно работающие контейнеры startedAt, err := getContainerStartedAt(containerFullName) if err == nil && time.Since(startedAt) < 5*time.Minute { portsOK := true containerPorts := []PortMapping{} for _, res := range resources { ports, ok := resourcePortMap[res.L7ResourceID] if !ok || containerNum-1 >= len(ports) { continue } containerPorts = append(containerPorts, ports[containerNum-1]) } for _, pm := range containerPorts { for _, dockerPort := range pm.HTTPPorts { if !checkPortListening("127.0.0.1", dockerPort) { log.Printf(" ⚠ Контейнер %s запущен недавно но порт %d не слушает — пересоздаю", containerFullName, dockerPort) portsOK = false break } } if !portsOK { break } for _, dockerPort := range pm.HTTPSPorts { if !checkPortListening("127.0.0.1", dockerPort) { log.Printf(" ⚠ Контейнер %s запущен недавно но порт %d не слушает — пересоздаю", containerFullName, dockerPort) portsOK = false break } } if !portsOK { break } } if !portsOK { recreateContainer(containerFullName, composeFile, containerDir, clientTitle, resources, resourcePortMap, containerNum) continue } } } 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) // Собираем порты которые должен занять контейнер portsToCheck := []int{} for _, res := range resources { ports, ok := resourcePortMap[res.L7ResourceID] if !ok || containerNum-1 >= len(ports) { continue } pm := ports[containerNum-1] for _, dockerPort := range pm.HTTPPorts { portsToCheck = append(portsToCheck, dockerPort) } for _, dockerPort := range pm.HTTPSPorts { portsToCheck = append(portsToCheck, dockerPort) } } // Ждём освобождения портов перед запуском if len(portsToCheck) > 0 { portsReady := false for attempt := 0; attempt < 30; attempt++ { busy := false for _, port := range portsToCheck { if isPortBusyByAnyProcess(port) { if attempt == 0 { log.Printf(" → Порт %d занят, ожидаем освобождения...", port) } busy = true break } } if !busy { portsReady = true if attempt > 0 { log.Printf(" ✓ Порты освободились (попытка %d)", attempt+1) } break } time.Sleep(1 * time.Second) } if !portsReady { log.Printf(" ⚠ Порты не освободились за 30 секунд, пробуем запустить всё равно") } } const maxRetries = 3 started := false for attempt := 1; attempt <= maxRetries; attempt++ { 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(" ⚠ Попытка %d/%d: порт занят, жду 3 секунды и повторяю...", attempt, maxRetries) time.Sleep(3 * time.Second) continue } time.Sleep(5 * time.Second) _, isRunning := checkContainerStatus(containerFullName) if !isRunning { log.Printf(" ⚠ Попытка %d/%d: контейнер %s не запустился", attempt, maxRetries, containerFullName) if attempt < maxRetries { time.Sleep(3 * time.Second) } continue } log.Printf(" ✓ Контейнер %s успешно запущен (попытка %d)", containerName, attempt) started = true break } if !started { log.Printf(" ⚠ Контейнер %s не запустился после %d попыток. Попробуйте вручную: cd %s && docker-compose up -d", containerFullName, maxRetries, containerDir) continue } if err := saveContainerPorts(containerDir, containerNum, resources, containerPorts); err != nil { log.Printf(" ⚠ Ошибка сохранения портов: %v", err) } } } log.Printf(" Проверка лишних контейнеров для %s", clientTitle) processDrainingContainers(clientTitle, clientInfo.ContainersCount) return nil }