270 lines
9.5 KiB
Go
270 lines
9.5 KiB
Go
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
|
||
}
|