Fix ports for docker

This commit is contained in:
Magnus Root 2026-04-09 15:41:54 +03:00
parent 4a82783b84
commit ba322e084d
3 changed files with 218 additions and 20 deletions

View file

@ -179,6 +179,48 @@ func setupContainers(config Config, clientTitle string, clientInfo *ClientInfo,
} }
if !containerRunning { if !containerRunning {
startContainer(containerFullName, composeFile, containerDir) 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] { if containerChanges[containerNum] {
log.Printf(" Конфигурация изменилась, проверяем и перезагружаем nginx в %s", containerFullName) log.Printf(" Конфигурация изменилась, проверяем и перезагружаем nginx в %s", containerFullName)
@ -196,17 +238,80 @@ func setupContainers(config Config, clientTitle string, clientInfo *ClientInfo,
} }
} else { } else {
log.Printf(" Запуск нового контейнера %s", containerName) 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 := exec.Command("docker-compose", "-f", composeFile, "up", "-d")
cmd.Dir = containerDir cmd.Dir = containerDir
output, _ := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
log.Printf(" → Вывод docker-compose: %s", string(output)) 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) time.Sleep(5 * time.Second)
_, isRunning := checkContainerStatus(containerFullName) _, isRunning := checkContainerStatus(containerFullName)
if !isRunning { if !isRunning {
log.Printf(" ⚠ Контейнер %s не запустился. Попробуйте вручную: cd %s && docker-compose up -d", containerFullName, containerDir) 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 continue
} }
log.Printf(" ✓ Контейнер %s успешно запущен", containerName)
} }
} }

View file

@ -14,6 +14,63 @@ import (
"time" "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) { func recreateContainer(containerFullName, composeFile, containerDir, clientTitle string, resources []ResourceData, resourcePortMap map[int][]PortMapping, containerNum int) {
log.Printf(" docker-compose.yml изменился, пересоздаём контейнер %s...", containerFullName) log.Printf(" docker-compose.yml изменился, пересоздаём контейнер %s...", containerFullName)
portsToFree := make([]int, 0) portsToFree := make([]int, 0)
@ -58,20 +115,39 @@ func recreateContainer(containerFullName, composeFile, containerDir, clientTitle
} }
} }
// 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 { if len(portsToFree) > 0 {
log.Printf(" → Ожидаем освобождения %d портов...", len(portsToFree)) log.Printf(" → Ожидаем освобождения %d портов...", len(portsToFree))
allPortsFree := false allPortsFree := false
for attempt := 0; attempt < 180; attempt++ { // Увеличено с 60 до 180 секунд for attempt := 0; attempt < 180; attempt++ {
usedPorts, err := getUsedPorts()
if err != nil {
log.Printf(" ⚠ Ошибка проверки портов: %v", err)
time.Sleep(1 * time.Second)
continue
}
portsBusy := 0 portsBusy := 0
busyPortsList := make([]int, 0) busyPortsList := make([]int, 0)
for _, port := range portsToFree { for _, port := range portsToFree {
if usedPorts[port] { if isPortBusyByAnyProcess(port) {
portsBusy++ portsBusy++
busyPortsList = append(busyPortsList, port) busyPortsList = append(busyPortsList, port)
} }
@ -82,12 +158,12 @@ func recreateContainer(containerFullName, composeFile, containerDir, clientTitle
break break
} }
if attempt%5 == 0 && attempt > 0 { if attempt%5 == 0 && attempt > 0 {
log.Printf(" → Ещё заняты %d портов: %v (попытка %d/180)", portsBusy, busyPortsList, attempt+1) // Обновлено с 60 на 180 log.Printf(" → Ещё заняты %d портов: %v (попытка %d/180)", portsBusy, busyPortsList, attempt+1)
} }
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
} }
if !allPortsFree { if !allPortsFree {
log.Printf(" ⚠ Порты не освободились за 180 секунд!") // Обновлено с 60 на 180 log.Printf(" ⚠ Порты не освободились за 180 секунд!")
log.Printf(" → Проверьте вручную: sudo lsof -i :%d", portsToFree[0]) log.Printf(" → Проверьте вручную: sudo lsof -i :%d", portsToFree[0])
return return
} }
@ -129,6 +205,20 @@ func startContainer(containerFullName, composeFile, containerDir string) {
log.Printf(" ✓ Контейнер %s запущен", containerFullName) log.Printf(" ✓ Контейнер %s запущен", containerFullName)
} }
// 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) { func checkContainerStatus(containerName string) (exists bool, running bool) {
cmd := exec.Command("docker", "ps", "-a", "--filter", fmt.Sprintf("name=^%s$", containerName), "--format", "{{.Names}}") cmd := exec.Command("docker", "ps", "-a", "--filter", fmt.Sprintf("name=^%s$", containerName), "--format", "{{.Names}}")
output, err := cmd.Output() output, err := cmd.Output()

View file

@ -114,7 +114,8 @@ func (pa *PortAllocator) allocatePortsForResources(resourcesData []ResourceData,
func getUsedPorts() (map[int]bool, error) { func getUsedPorts() (map[int]bool, error) {
usedPorts := make(map[int]bool) usedPorts := make(map[int]bool)
cmd := exec.Command("ss", "-tuln") // Используем -tuanl чтобы видеть все состояния: LISTEN, ESTAB, TIME-WAIT и др.
cmd := exec.Command("ss", "-tuanl")
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
return usedPorts, fmt.Errorf("ошибка выполнения ss: %w", err) return usedPorts, fmt.Errorf("ошибка выполнения ss: %w", err)
@ -123,10 +124,12 @@ func getUsedPorts() (map[int]bool, error) {
lines := strings.Split(string(output), "\n") lines := strings.Split(string(output), "\n")
for _, line := range lines { for _, line := range lines {
fields := strings.Fields(line) fields := strings.Fields(line)
// Формат ss: Netid State Recv-Q Send-Q Local-Address:Port Peer-Address:Port
if len(fields) < 5 { if len(fields) < 5 {
continue continue
} }
// Local address в 5м поле (индекс 4)
localAddr := fields[4] localAddr := fields[4]
parts := strings.Split(localAddr, ":") parts := strings.Split(localAddr, ":")
if len(parts) < 2 { if len(parts) < 2 {