magos/ports.go
2026-04-09 15:41:54 +03:00

273 lines
9.7 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 (
"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)
// Используем -tuanl чтобы видеть все состояния: LISTEN, ESTAB, TIME-WAIT и др.
cmd := exec.Command("ss", "-tuanl")
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)
// Формат ss: Netid State Recv-Q Send-Q Local-Address:Port Peer-Address:Port
if len(fields) < 5 {
continue
}
// Local address в 5м поле (индекс 4)
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
}