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 @@
Маршрутизация сети:
-
+
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;
}
}