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 }