Fix ports for docker
This commit is contained in:
parent
4a82783b84
commit
ba322e084d
3 changed files with 218 additions and 20 deletions
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
5
ports.go
5
ports.go
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue