diff --git a/jambotron-ui/src/app/modules/user-module/project-details.component/project-details.component.html b/jambotron-ui/src/app/modules/user-module/project-details.component/project-details.component.html index 12b97d6..3f7e1cf 100644 --- a/jambotron-ui/src/app/modules/user-module/project-details.component/project-details.component.html +++ b/jambotron-ui/src/app/modules/user-module/project-details.component/project-details.component.html @@ -61,15 +61,15 @@
Маршрутизация сети: - +
open_in_new {{ domain }} - порт {{ getContainerPort(container.name) }} - + порт {{ getContainerPort(container) }} +
diff --git a/jambotron-ui/src/app/modules/user-module/project-details.component/project-details.component.ts b/jambotron-ui/src/app/modules/user-module/project-details.component/project-details.component.ts index e0880b1..d81d0cc 100644 --- a/jambotron-ui/src/app/modules/user-module/project-details.component/project-details.component.ts +++ b/jambotron-ui/src/app/modules/user-module/project-details.component/project-details.component.ts @@ -264,64 +264,81 @@ export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChec * Безопасно извлекает имя сервиса (например, 'web') из имени Docker-контейнера. * Проверяет префиксы как по ID, так и по Name проекта. */ - getServiceName(containerName: string): string | null { - if (!containerName || !this.project) return null; + getServiceName(container: any): string | null { + if (!container) return null; - const prefixId = `jambotron-${this.project.id}-`; - const prefixName = `jambotron-${this.project.name}-`; - - let servicePart = ''; - - if (containerName.startsWith(prefixId)) { - servicePart = containerName.substring(prefixId.length); - } else if (containerName.startsWith(prefixName)) { - servicePart = containerName.substring(prefixName.length); - } else { - // Резервный разбор по токенам (jambotron-проект-сервис) - const parts = containerName.split('-'); - if (parts.length >= 3 && parts[0] === 'jambotron') { - servicePart = parts.slice(2).join('-'); - } else { - return null; - } + // 1. Идеальный путь созидания: берем стабильный ID из меток Docker + if (container.labels && container.labels['jambotron.service.id']) { + return container.labels['jambotron.service.id']; } - // Отрезаем индекс реплики Docker Compose (например, "-1") - if (servicePart.endsWith('-1')) { - servicePart = servicePart.substring(0, servicePart.length - 2); + // 2. Если меток нет, переходим к парсингу имени контейнера + const containerName = container.name || ''; + + // Проверяем базовый маркер нашей системы + if (!containerName.startsWith('jambotron-')) { + console.warn(`[Jambotron] Контейнер не принадлежит системе Jambotron: ${containerName}`); + return null; } - return servicePart; + + // Отрезаем индекс реплики (-1, -2) и префикс "jambotron-" + let cleanName = containerName.replace(/-\d+$/, '').replace(/^jambotron-/, ''); + + // 3. Проверяем массив известных имен сервисов из YAML + const validServices: string[] = this.config?.serviceNames || []; + if (validServices.length > 0) { + const match = validServices.find((service: string) => cleanName.endsWith(service) || cleanName.includes(service)); + if (match) return match; + } + + // 4. Финальный пуленепробиваемый фоллбэк для старых контейнеров (вырезаем имя проекта) + // Для "test-web" -> cleanName = "test-web" -> parts = ['test', 'web'] -> возвращает "web" + const parts = cleanName.split('-'); + if (parts.length >= 2) { + return parts.slice(1).join('-'); + } + + // Только если вообще ничего не получилось извлечь — выводим реальное предупреждение + if (!cleanName) { + console.warn(`[Jambotron] Не удалось определить имя сервиса для: ${containerName}`); + return null; + } + + return cleanName; } /** * Возвращает текущий сформированный домен для вывода в UI */ - getContainerDomain(containerName: string): string | null { - const serviceName = this.getServiceName(containerName); - if (!serviceName || !this.config?.serviceConfigs) return null; + getContainerDomain(container: any): string | null { + const serviceId = this.getServiceName(container); + if (!serviceId || !this.config?.serviceConfigs) return null; - const serviceConfig = this.config.serviceConfigs[serviceName]; + // Ищем настройки в мапе по системному ID (например, srv-0) + const serviceConfig = this.config.serviceConfigs[serviceId]; return serviceConfig && serviceConfig.subdomain ? `${serviceConfig.subdomain}.jambotron.com` : null; } /** * Возвращает порт сервиса из конфигурации MongoDB */ - getContainerPort(containerName: string): number { - const serviceName = this.getServiceName(containerName); - if (!serviceName || !this.config?.serviceConfigs) return 80; + getContainerPort(container: any): number { + const serviceId = this.getServiceName(container); + if (!serviceId || !this.config?.serviceConfigs) return 80; - const serviceConfig = this.config.serviceConfigs[serviceName]; + // Ищем настройки в мапе по системному ID (например, srv-0) + const serviceConfig = this.config.serviceConfigs[serviceId]; return serviceConfig && serviceConfig.port ? serviceConfig.port : 80; } /** - * Активация режима редактирования. Теперь полностью безопасна. + * Активация режима редактирования. */ startEditDomain(container: any) { - const serviceName = this.getServiceName(container.name); + // ПЕРЕДАЕМ КОНТЕЙНЕР ЦЕЛИКОМ, а не container.name + const serviceName = this.getServiceName(container); if (!serviceName) { - console.warn(`[Jambotron] Не удалось определить имя сервиса для: ${container.name}`); + console.warn(`[Jambotron] Не удалось определить имя сервиса для:`, container); return; } @@ -338,7 +355,8 @@ export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChec * Сохранение изменённого поддомена и порта в MongoDB базы данных */ saveDomainConfig(container: any) { - const serviceName = this.getServiceName(container.name); + // ПЕРЕДАЕМ КОНТЕЙНЕР ЦЕЛИКОМ, а не container.name + const serviceName = this.getServiceName(container); if (!serviceName || !this.project?.id || !this.config) return; if (!this.config.serviceConfigs) { @@ -360,7 +378,6 @@ export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChec // Отправка монолитного конфига на бэкенд this.projectService.updateProjectConfig(this.project.id, this.config).subscribe({ next: () => { - // Бэкенд возвращает void (200 OK). Нам НЕ нужно затирать config! this.editingDomains[container.id] = false; console.log(`[Caddy] Маршрут для сервиса ${serviceName} успешно сохранён.`); }, diff --git a/src/main/java/com/jambotronGroup/jambotron/model/ProjectConfig.java b/src/main/java/com/jambotronGroup/jambotron/model/ProjectConfig.java index 2499381..2390856 100644 --- a/src/main/java/com/jambotronGroup/jambotron/model/ProjectConfig.java +++ b/src/main/java/com/jambotronGroup/jambotron/model/ProjectConfig.java @@ -32,8 +32,10 @@ public class ProjectConfig { @Data @NoArgsConstructor @AllArgsConstructor + // Класс настроек (ServiceSettings.java) public static class ServiceSettings { - private String subdomain; // Уникальный поддомен (например, lioshik-app-api) - private Integer port; // Внутренний порт контейнера (8080, 80 и т.д.) + private String yamlServiceName; // "web-test" или "logger-test" + private String subdomain; // "test-web" + private Integer port; // 80 } } \ No newline at end of file diff --git a/src/main/java/com/jambotronGroup/jambotron/projects/ProjectService.java b/src/main/java/com/jambotronGroup/jambotron/projects/ProjectService.java index a211a95..3522fd7 100644 --- a/src/main/java/com/jambotronGroup/jambotron/projects/ProjectService.java +++ b/src/main/java/com/jambotronGroup/jambotron/projects/ProjectService.java @@ -95,17 +95,132 @@ public class ProjectService { @Transactional public void updateProjectConfig(UUID projectId, ProjectConfig newConfig) { - ProjectConfig existing = configRepository.findByProjectId(String.valueOf(projectId)) - .orElseThrow(() -> new RuntimeException("Configuration not found")); + // 1. Поиск существующего проекта в Postgres + Project project = projectRepository.findById(projectId) + .orElseThrow(() -> new IllegalArgumentException("Проект не найден с ID: " + projectId)); - // Валидация поддоменов перед сохранением - validateSubdomains(newConfig); + // Находим или инициализируем корневую конфигурацию в MongoDB (ключ — String) + ProjectConfig existing = configRepository.findByProjectId(projectId.toString()) + .orElseGet(() -> { + ProjectConfig c = new ProjectConfig(); + c.setProjectId(projectId.toString()); + c.setServiceConfigs(new HashMap<>()); + return c; + }); - existing.setRawComposeContent(newConfig.getRawComposeContent()); - existing.setServiceConfigs(newConfig.getServiceConfigs()); - existing.setServiceNames(extractServiceNames(newConfig.getRawComposeContent())); - existing.setVersion(existing.getVersion() + 1); + // ПЕРВОПРИЧИНА ИСПРАВЛЕНА: Берем исходный YAML текст из прилетевшего конфига (newConfig) + String rawYaml = newConfig.getRawComposeContent(); + + // 2. Парсинг актуальных имен сервисов из docker-compose.yml + List actualYamlNames = new ArrayList<>(); + if (rawYaml != null && !rawYaml.isBlank()) { + try { + Yaml yaml = new Yaml(); + Map obj = yaml.load(rawYaml); + if (obj != null && obj.get("services") instanceof Map) { + Map servicesMap = (Map) obj.get("services"); + for (Object key : servicesMap.keySet()) { + actualYamlNames.add(String.valueOf(key)); + } + } + } catch (Exception e) { + _logger.error("Ошибка парсинга docker-compose.yml для извлечения имен сервисов в проекте {}", projectId, e); + } + } + + // 3. Умное сопоставление (маппинг) Service ID + Map incomingConfigs = newConfig.getServiceConfigs(); + Map existingConfigs = existing.getServiceConfigs(); + + if (incomingConfigs == null) incomingConfigs = new HashMap<>(); + if (existingConfigs == null) existingConfigs = new HashMap<>(); + + Map alignedConfigs = new HashMap<>(); + + // Итерируемся по реальным сервисам из файла конфигурации + for (int i = 0; i < actualYamlNames.size(); i++) { + String yamlName = actualYamlNames.get(i); + final int currentIndex = i; + + // Определяем стабильный системный ID (например, srv-0) на основе старой базы + String targetServiceId = existingConfigs.entrySet().stream() + .filter(e -> yamlName.equals(e.getValue().getYamlServiceName())) + .map(Map.Entry::getKey) + .findFirst() + .orElse(null); + + if (targetServiceId == null) { + // Если в базе еще нет, проверяем, вдруг фронт привязал к srv-X + targetServiceId = incomingConfigs.entrySet().stream() + .filter(e -> yamlName.equals(e.getValue().getYamlServiceName())) + .map(Map.Entry::getKey) + .findFirst() + .orElse("srv-" + currentIndex); + } + + // ИЩЕМ ИСТОЧНИК ДАННЫХ (Где лежат введенные пользователем порт и поддомен) + ProjectConfig.ServiceSettings userUiSettings = null; + + // Стратегия А: Ищем по стабильному ID (srv-0) + ProjectConfig.ServiceSettings settingsBySubId = incomingConfigs.get(targetServiceId); + if (settingsBySubId != null && (settingsBySubId.getSubdomain() != null || settingsBySubId.getPort() != null)) { + userUiSettings = settingsBySubId; + } + + // Стратегия Б: Ищем по полному YAML-имени как по ключу мапы (например, "web-test") + if (userUiSettings == null) { + ProjectConfig.ServiceSettings settingsByFullName = incomingConfigs.get(yamlName); + if (settingsByFullName != null && (settingsByFullName.getSubdomain() != null || settingsByFullName.getPort() != null)) { + userUiSettings = settingsByFullName; + } + } + + // Стратегия В (Решение для image_bb4570): Ищем частичное совпадение ключей мапы (фронт прислал ключ "web" для сервиса "web-test") + if (userUiSettings == null) { + userUiSettings = incomingConfigs.entrySet().stream() + .filter(entry -> yamlName.startsWith(entry.getKey()) || entry.getKey().startsWith(yamlName)) + .map(Map.Entry::getValue) + .filter(s -> s.getSubdomain() != null || s.getPort() != null) + .findFirst() + .orElse(null); + } + + // Стратегия Г: Ищем по внутреннему полю yamlServiceName, если оно заполнено + if (userUiSettings == null) { + userUiSettings = incomingConfigs.values().stream() + .filter(s -> yamlName.equals(s.getYamlServiceName())) + .findFirst() + .orElse(null); + } + + // Создаем или извлекаем целевой объект настроек для базы + ProjectConfig.ServiceSettings finalSettings = existingConfigs.get(targetServiceId); + if (finalSettings == null) { + finalSettings = new ProjectConfig.ServiceSettings(); + } + + // НАКАТЫВАЕМ ДАННЫЕ: Если нашли донора из UI, забираем его конфигурацию порта и домена + if (userUiSettings != null) { + finalSettings.setSubdomain(userUiSettings.getSubdomain()); + finalSettings.setPort(userUiSettings.getPort()); + } + + // Жестко гарантируем железную привязку к реальному имени контейнера + finalSettings.setYamlServiceName(yamlName); + alignedConfigs.put(targetServiceId, finalSettings); + } + + // Сохраняем выровненную конфигурацию и метаданные в документ Mongo + existing.setServiceConfigs(alignedConfigs); + existing.setRawComposeContent(rawYaml); + existing.setServiceNames(actualYamlNames); + + // Валидируем поддомены перед записью в репозиторий + validateSubdomains(existing); + + // 4. Сохранение обновленного монолита в MongoDB configRepository.save(existing); + _logger.info("[Jambotron] Конфигурация шлюза для проекта {} успешно синхронизирована.", projectId); } private void validateSubdomains(ProjectConfig config) { @@ -202,21 +317,39 @@ public class ProjectService { } // Перебираем конфигурации сервисов, сохраненные в MongoDB - config.getServiceConfigs().forEach((serviceName, serviceSetting) -> { - if (services.containsKey(serviceName) && serviceSetting.getSubdomain() != null && !serviceSetting.getSubdomain().isEmpty()) { - Map service = (Map) services.get(serviceName); + config.getServiceConfigs().forEach((serviceKey, serviceSetting) -> { + + // Умный поиск: ищем ключ в docker-compose.yml + String actualComposeKey = null; + + // 1. Проверяем, может serviceKey — это прямое имя из YAML (web-test) + if (services.containsKey(serviceKey)) { + actualComposeKey = serviceKey; + } + // 2. Или может это сохраненный yamlServiceName внутри настроек? + else if (serviceSetting.getYamlServiceName() != null && services.containsKey(serviceSetting.getYamlServiceName())) { + actualComposeKey = serviceSetting.getYamlServiceName(); + } + // 3. Резервный вариант: если прилетело урезанное "web", ищем частичное совпадение в YAML ("web-test" стартует с "web") + else { + actualComposeKey = services.keySet().stream() + .filter(k -> k.startsWith(serviceKey) || serviceKey.startsWith(k)) + .findFirst() + .orElse(null); + } + + // Если нашли точку соприкосновения и задан поддомен — инжектим метки Caddy + if (actualComposeKey != null && serviceSetting.getSubdomain() != null && !serviceSetting.getSubdomain().isEmpty()) { + Map service = (Map) services.get(actualComposeKey); - // Получаем существующий блок labels или создаем новый, если его нет Map labels = (Map) service.get("labels"); if (labels == null) { labels = new HashMap<>(); } - String domain = serviceSetting.getSubdomain(); - // Если внутренний порт не задан, по умолчанию шлем на 80 + String domain = serviceSetting.getSubdomain() + ".jambotron.com"; String internalPort = serviceSetting.getPort() != null ? serviceSetting.getPort().toString() : "80"; - // Прописываем директивы созидания для caddy-docker-proxy labels.put("caddy", domain); labels.put("caddy.reverse_proxy", "{{upstreams " + internalPort + "}}"); @@ -224,7 +357,6 @@ public class ProjectService { } }); - // Настраиваем красивый вывод структуры YAML (BLOCK style) DumperOptions options = new DumperOptions(); options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); Yaml dumper = new Yaml(options); @@ -233,7 +365,6 @@ public class ProjectService { } catch (Exception e) { _logger.error("Ошибка при модификации YAML структуры для Caddy", e); - // Если упал парсинг, возвращаем исходный текст, чтобы не сломать деплой целиком return rawComposeContent; } }