This is an automated email from the ASF dual-hosted git repository.

mgubaidullin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-karavan.git

commit 2a85ed899cf7859e7f6239121360a5f2f5357554
Author: Marat Gubaidullin <[email protected]>
AuthorDate: Fri Sep 25 15:15:45 2026 -0400

    Fixes for Karavan 4.22.1
---
 .../org/apache/camel/karavan/KaravanEvents.java    |  3 ++
 .../camel/karavan/api/AbstractApiResource.java     | 28 +++++++++++
 .../camel/karavan/api/InfrastructureResource.java  | 10 ++--
 .../camel/karavan/api/ProjectFileResource.java     |  4 ++
 .../apache/camel/karavan/cache/KaravanCache.java   |  8 +++
 .../karavan/kubernetes/KubernetesService.java      | 57 +++++++++++++++++-----
 .../kubernetes/KubernetesStatusService.java        |  3 +-
 .../apache/camel/karavan/service/AuthService.java  |  3 +-
 .../karavan/service/CamelComponentService.java     |  3 ++
 .../apache/camel/karavan/service/CodeService.java  | 19 +++++++-
 .../camel/karavan/service/ComplexityService.java   |  8 ++-
 .../camel/karavan/service/ProjectService.java      |  1 +
 12 files changed, 126 insertions(+), 21 deletions(-)

diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/KaravanEvents.java 
b/karavan-app/src/main/java/org/apache/camel/karavan/KaravanEvents.java
index aac4a7b1..b33c8229 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/KaravanEvents.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/KaravanEvents.java
@@ -52,6 +52,7 @@ public class KaravanEvents {
     public static final String SERVICE_UPDATED = "SERVICE_UPDATED";
     public static final String SERVICE_DELETED = "SERVICE_DELETED";
 
+
     public static final String ON_USER_ACTIVITY = "ON_USER_ACTIVITY";
     public static final String ON_PROJECT_ACTIVITY = "ON_PROJECT_ACTIVITY";
     public static final String ON_CONTAINER_ACTIVITY = "ON_CONTAINER_ACTIVITY";
@@ -60,4 +61,6 @@ public class KaravanEvents {
     public static final String PERSIST_ACCESS = "PERSIST_ACCESS";
     public static final String PERSIST_SESSION = "PERSIST_SESSION";
 
+    public static final String FILE_CHANGED = "FILE_CHANGED";
+
 }
diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/api/AbstractApiResource.java
 
b/karavan-app/src/main/java/org/apache/camel/karavan/api/AbstractApiResource.java
index 6f024921..6b019db7 100644
--- 
a/karavan-app/src/main/java/org/apache/camel/karavan/api/AbstractApiResource.java
+++ 
b/karavan-app/src/main/java/org/apache/camel/karavan/api/AbstractApiResource.java
@@ -78,4 +78,32 @@ public class AbstractApiResource {
                 .put("username", username)
                 .put("roles", roles);
     }
+
+    /**
+     * An error body that says what went wrong, not only what was being 
attempted.
+     *
+     * <p>"Could not deploy the gateway configuration" tells an operator 
nothing they did not already know,
+     * and the reason - a rejected apply, an unreachable API server, a 
conflict - is then only in a log line
+     * they may not be able to read. The cause is appended so the UI can show 
it.
+     *
+     * <p>The deepest cause is used because the outer frame is usually the 
least specific: a client library
+     * wraps "conflict with kubectl-client-side-apply on .data.apisix.yaml" in 
its own generic failure.
+     */
+    protected static String withCause(String message, Throwable throwable) {
+        var cause = deepestMessage(throwable);
+        return cause == null ? message : message + ": " + cause;
+    }
+
+    private static String deepestMessage(Throwable throwable) {
+        String message = null;
+        for (var current = throwable; current != null; current = 
current.getCause()) {
+            if (current.getMessage() != null && 
!current.getMessage().isBlank()) {
+                message = current.getMessage();
+            }
+            if (current.getCause() == current) {
+                break;
+            }
+        }
+        return message;
+    }
 }
\ No newline at end of file
diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/api/InfrastructureResource.java
 
b/karavan-app/src/main/java/org/apache/camel/karavan/api/InfrastructureResource.java
index 05f184d4..1f15da97 100644
--- 
a/karavan-app/src/main/java/org/apache/camel/karavan/api/InfrastructureResource.java
+++ 
b/karavan-app/src/main/java/org/apache/camel/karavan/api/InfrastructureResource.java
@@ -23,7 +23,10 @@ import jakarta.ws.rs.*;
 import jakarta.ws.rs.core.MediaType;
 import jakarta.ws.rs.core.Response;
 import org.apache.camel.karavan.KaravanConstants;
-import org.apache.camel.karavan.cache.*;
+import org.apache.camel.karavan.cache.DeploymentStatus;
+import org.apache.camel.karavan.cache.KaravanCache;
+import org.apache.camel.karavan.cache.ProjectFile;
+import org.apache.camel.karavan.cache.ServiceStatus;
 import org.apache.camel.karavan.kubernetes.KubernetesService;
 import org.apache.camel.karavan.model.PodEvent;
 import org.apache.camel.karavan.service.ConfigService;
@@ -32,12 +35,10 @@ import org.jboss.logging.Logger;
 
 import java.util.Comparator;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 import java.util.stream.Collectors;
 
 import static 
org.apache.camel.karavan.KaravanConstants.KUBERNETES_YAML_FILENAME;
-import static org.apache.camel.karavan.KaravanConstants.LABEL_TYPE;
 import static org.apache.camel.karavan.KaravanEvents.CMD_RESTART_INFORMERS;
 
 @Path("/ui/infrastructure")
@@ -104,9 +105,8 @@ public class InfrastructureResource {
         if (resources == null) {
             return Response.status(Response.Status.NOT_FOUND).entity("Resource 
file " + KUBERNETES_YAML_FILENAME + " not found").build();
         }
-        kubernetesService.startDeployment(resources.getCode(), 
Map.of(LABEL_TYPE, ContainerType.packaged.name()));
         try {
-            kubernetesService.startDeployment(resources.getCode(), 
Map.of(LABEL_TYPE, ContainerType.packaged.name()));
+            kubernetesService.startDeployment(resources.getCode());
         } catch (IllegalArgumentException e) {
             LOGGER.error("Rejected " + name + " of project " + projectId + ": 
" + e.getMessage());
             return 
Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java
 
b/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java
index 561c7a00..cc0842c8 100644
--- 
a/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java
+++ 
b/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java
@@ -59,6 +59,10 @@ public class ProjectFileResource {
         }
     }
 
+    /**
+     * Files of a project matching a glob such as {@code *-task-template.md}, 
so a client after a handful of small
+     * files does not have to download every file of the project to filter 
them itself.
+     */
     @GET
     @Authenticated
     @Produces(MediaType.APPLICATION_JSON)
diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/cache/KaravanCache.java 
b/karavan-app/src/main/java/org/apache/camel/karavan/cache/KaravanCache.java
index f4914a55..bd4a1b04 100644
--- a/karavan-app/src/main/java/org/apache/camel/karavan/cache/KaravanCache.java
+++ b/karavan-app/src/main/java/org/apache/camel/karavan/cache/KaravanCache.java
@@ -93,6 +93,12 @@ public class KaravanCache {
         return query(folders, f -> true, ProjectFolder::copy);
     }
 
+    public List<ProjectFolder> getIntegrationProjects() {
+        return query(folders, f -> true, ProjectFolder::copy)
+                .stream().filter(p -> Objects.equals(p.type, 
ProjectFolder.Type.integration))
+                .collect(Collectors.toList());
+    }
+
     public void saveProject(ProjectFolder projectFolder, boolean persist) {
         var key = GroupedKey.create(projectFolder.getProjectId(), DEV, 
projectFolder.getProjectId());
         if (projectFolder.lastUpdate == 0) {
@@ -185,6 +191,7 @@ public class KaravanCache {
         }
         if (persist) {
             eventBus.publish(PERSIST_PROJECT, new CacheEvent(key, SAVE, file));
+            eventBus.publish(FILE_CHANGED, new CacheEvent(key, SAVE, file));
         }
     }
 
@@ -200,6 +207,7 @@ public class KaravanCache {
         var key = GroupedKey.create(projectId, DEV, filename);
         files.remove(key);
         eventBus.publish(PERSIST_PROJECT, new CacheEvent(key, DELETE, null));
+        eventBus.publish(FILE_CHANGED, new CacheEvent(key, SAVE, filename));
     }
 
     // --- Committed Files ---
diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/kubernetes/KubernetesService.java
 
b/karavan-app/src/main/java/org/apache/camel/karavan/kubernetes/KubernetesService.java
index c00840af..db944551 100644
--- 
a/karavan-app/src/main/java/org/apache/camel/karavan/kubernetes/KubernetesService.java
+++ 
b/karavan-app/src/main/java/org/apache/camel/karavan/kubernetes/KubernetesService.java
@@ -255,7 +255,7 @@ public class KubernetesService {
         }
     }
 
-    public void startDeployment(String resources, Map<String, String> labels) {
+    public void startDeployment(String resources) {
         KubernetesList list;
         try {
             list = Serialization.unmarshal(resources, KubernetesList.class);
@@ -271,16 +271,16 @@ public class KubernetesService {
         list.getItems().forEach(item -> validateDeploymentResource(item, 
allowedKinds));
         try (KubernetesClient client = kubernetesClient()) {
             list.getItems().forEach(item -> {
-                if (labels != null) {
-                    putLabels(item.getMetadata(), labels);
-                    if (item instanceof Deployment deployment && 
deployment.getSpec() != null && deployment.getSpec().getTemplate() != null) {
-                        var template = deployment.getSpec().getTemplate();
-                        if (template.getMetadata() == null) {
-                            template.setMetadata(new ObjectMeta());
-                        }
-                        putLabels(template.getMetadata(), labels);
-                    }
-                }
+//                if (labels != null) {
+//                    putLabels(item.getMetadata(), labels);
+//                    if (item instanceof Deployment deployment && 
deployment.getSpec() != null && deployment.getSpec().getTemplate() != null) {
+//                        var template = deployment.getSpec().getTemplate();
+//                        if (template.getMetadata() == null) {
+//                            template.setMetadata(new ObjectMeta());
+//                        }
+//                        putLabels(template.getMetadata(), labels);
+//                    }
+//                }
                 item.getMetadata().setNamespace(getNamespace());
                 
client.resource(item).inNamespace(getNamespace()).serverSideApply();
             });
@@ -360,6 +360,7 @@ public class KubernetesService {
             LOGGER.info("Delete deployment: " + name + " in the namespace: " + 
getNamespace());
             
client.apps().deployments().inNamespace(getNamespace()).withName(name).delete();
             
client.services().inNamespace(getNamespace()).withName(name).delete();
+            
client.configMaps().inNamespace(getNamespace()).withName(name).delete();
         } catch (Exception ex) {
             LOGGER.error(ex.getMessage());
         }
@@ -599,16 +600,26 @@ public class KubernetesService {
     }
 
     public void createConfigMap(String name, Map<String, String> data, 
Map<String, String> labels) {
+        createConfigMap(name, data, labels, Map.of());
+    }
+
+    public void createConfigMap(String name, Map<String, String> data, 
Map<String, String> labels,
+                                Map<String, String> annotations) {
         try (KubernetesClient client = kubernetesClient()) {
             ConfigMap configMap = new ConfigMapBuilder()
                     .withNewMetadata()
                     .withName(name)
                     .withNamespace(getNamespace())
                     .withLabels(labels)
+                    .withAnnotations(annotations)
                     .endMetadata()
                     .withData(data)
                     .build();
-            client.resource(configMap).serverSideApply();
+            // forceConflicts: these ConfigMaps are seeded by a manifest 
applied with kubectl, which records
+            // itself as the field manager of the keys it wrote. A later 
server-side apply from here is then
+            // a 409 FieldManagerConflict, and the operator sees a deploy that 
simply fails. Writing these
+            // keys is exactly what this method is for, so it takes ownership 
rather than asking.
+            client.resource(configMap).forceConflicts().serverSideApply();
         }
     }
 
@@ -784,6 +795,28 @@ public class KubernetesService {
         return result;
     }
 
+    /**
+     * ConfigMaps of the namespace carrying the label, whatever its value - 
the selector is applied server side so
+     * the API server does the filtering. Callers that care about the value 
read it from the returned metadata.
+     */
+    public List<ConfigMap> getConfigMapsByLabel(String labelKey) {
+        try (KubernetesClient client = kubernetesClient()) {
+            return 
client.configMaps().inNamespace(getNamespace()).withLabel(labelKey).list().getItems();
+        } catch (Exception e) {
+            LOGGER.error("getConfigMapsByLabel " + labelKey + ": " + 
e.getMessage());
+            return List.of();
+        }
+    }
+
+    public Map<String, ConfigMap> getProjectConfigMaps() {
+        Map<String, ConfigMap> result = new HashMap<>();
+        for (var configMap: getConfigMapsByLabel(LABEL_PROJECT_ID)) {
+            var name = configMap.getMetadata().getName();
+            result.put(name, configMap);
+        }
+        return result;
+    }
+
     public void deleteConfigMap(String configMapName) {
         ConfigMap configMap = getConfigMap(configMapName);
         if (configMap != null) {
diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/kubernetes/KubernetesStatusService.java
 
b/karavan-app/src/main/java/org/apache/camel/karavan/kubernetes/KubernetesStatusService.java
index 4137bf58..7d46e20a 100644
--- 
a/karavan-app/src/main/java/org/apache/camel/karavan/kubernetes/KubernetesStatusService.java
+++ 
b/karavan-app/src/main/java/org/apache/camel/karavan/kubernetes/KubernetesStatusService.java
@@ -44,6 +44,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 
+
 @Default
 @Readiness
 @ApplicationScoped
@@ -120,7 +121,7 @@ public class KubernetesStatusService implements HealthCheck 
{
                 return HealthCheckResponse.named("Kubernetes").up().build();
             }
         }
-        return HealthCheckResponse.named("Kubernetes").down().build();
+        return HealthCheckResponse.named("Kubernetes").up().build();
     }
 
     public void stopInformers() {
diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/service/AuthService.java 
b/karavan-app/src/main/java/org/apache/camel/karavan/service/AuthService.java
index a175697b..3b04ae1b 100644
--- 
a/karavan-app/src/main/java/org/apache/camel/karavan/service/AuthService.java
+++ 
b/karavan-app/src/main/java/org/apache/camel/karavan/service/AuthService.java
@@ -30,6 +30,7 @@ public class AuthService {
     public static final String ROLE_ADMIN = "platform-admin";
     public static final String ROLE_DEVELOPER = "platform-developer";
     public static final String ROLE_USER = "platform-user";
+    public static final String ROLE_OPERATOR = "platform-operator";
     public static final String ROLE_SERVICE_ACCOUNT = 
"platform-service-account";
 
     public static final String USER_ADMIN = "admin";
@@ -37,7 +38,7 @@ public class AuthService {
     public static final String DEFAULT_EMAIL_SUFFIX = "@platform.platform";
 
     public static List<String> getAllRoles(){
-        return List.of(ROLE_ADMIN, ROLE_DEVELOPER, ROLE_USER);
+        return List.of(ROLE_ADMIN, ROLE_DEVELOPER, ROLE_USER, ROLE_OPERATOR);
     }
 
     private static final int COST = 12;
diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/service/CamelComponentService.java
 
b/karavan-app/src/main/java/org/apache/camel/karavan/service/CamelComponentService.java
index 66a66022..7cc6f5cc 100644
--- 
a/karavan-app/src/main/java/org/apache/camel/karavan/service/CamelComponentService.java
+++ 
b/karavan-app/src/main/java/org/apache/camel/karavan/service/CamelComponentService.java
@@ -5,12 +5,15 @@ import io.vertx.core.json.JsonObject;
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
 import org.apache.camel.karavan.util.CamelComponentMetadata;
+import org.jboss.logging.Logger;
 
 import java.util.*;
 
 @ApplicationScoped
 public class CamelComponentService {
 
+    private static final Logger LOGGER = 
Logger.getLogger(CamelComponentService.class.getName());
+
     @Inject
     CodeService codeService;
 
diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/service/CodeService.java 
b/karavan-app/src/main/java/org/apache/camel/karavan/service/CodeService.java
index 8330fe83..e91297f1 100644
--- 
a/karavan-app/src/main/java/org/apache/camel/karavan/service/CodeService.java
+++ 
b/karavan-app/src/main/java/org/apache/camel/karavan/service/CodeService.java
@@ -107,7 +107,7 @@ public class CodeService {
         }
     }
 
-    private ProjectFile getApplicationProperties(String projectId) {
+    public ProjectFile getApplicationProperties(String projectId) {
         return karavanCache.getProjectFile(projectId, 
APPLICATION_PROPERTIES_FILENAME);
     }
 
@@ -224,6 +224,23 @@ public class CodeService {
         return null;
     }
 
+    public String getEnvSpecificConfigurationText(String fileName) {
+        return getConfigurationText(envFileName(fileName));
+    }
+
+    /** Every environment except {@code dev} works on {@code <env>.}-prefixed 
files, inputs and outputs alike. */
+    public String envFileName(String fileName) {
+        return envFileName(fileName, environment);
+    }
+
+    /**
+     * The same, for an environment other than this instance's. Dev renders 
the artifacts every environment
+     * deploys from, so it has to name files for environments it is not 
running in.
+     */
+    public static String envFileName(String fileName, String env) {
+        return Objects.equals(env, KaravanConstants.DEV) ? fileName : env + 
"." + fileName;
+    }
+
     public String getTemplateText(String fileName) {
         try {
             List<ProjectFile> files = 
karavanCache.getProjectFiles(ProjectFolder.Type.templates.name());
diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/service/ComplexityService.java
 
b/karavan-app/src/main/java/org/apache/camel/karavan/service/ComplexityService.java
index dac56666..0b844c95 100644
--- 
a/karavan-app/src/main/java/org/apache/camel/karavan/service/ComplexityService.java
+++ 
b/karavan-app/src/main/java/org/apache/camel/karavan/service/ComplexityService.java
@@ -37,9 +37,10 @@ public class ComplexityService {
         return karavanCache.getFolders().stream()
                 .filter(p -> Objects.equals(p.getType(), 
ProjectFolder.Type.integration)
                         || Objects.equals(p.getType(), 
ProjectFolder.Type.templates)
+                        || Objects.equals(p.getType(), ProjectFolder.Type.sdx)
                         || Objects.equals(p.getType(), 
ProjectFolder.Type.kamelets)
                         || Objects.equals(p.getType(), 
ProjectFolder.Type.documentation)
-                        )
+                        || Objects.equals(p.getType(), 
ProjectFolder.Type.contracts))
                 .map(this::getProjectComplexity).toList();
     }
 
@@ -74,8 +75,13 @@ public class ComplexityService {
                         routes1.forEach(r -> 
r.getComponentsExt().forEach(complexityFile::addComponentExt));
                         routes1.forEach(r -> 
r.getComponentsInt().forEach(complexityFile::addComponentInt));
                         routes1.forEach(r -> 
r.getKamelets().forEach(complexityFile::addKamelet));
+                        if (file.getName().equals("mcp-route.camel.yaml")) {
+                            complexityProject.setMcp(true);
+                        }
                         routes.addAll(routes1);
 
+                    } else if (file.getName().equals("AgentConfig.java")) {
+                        complexityProject.setAgents(true);
                     } else if 
(file.getName().equals(APPLICATION_PROPERTIES_FILENAME)) {
                         complexityFile.setType(ComplexityFile.Type.properties);
                         
complexityProject.setDependencies(getDependencies(file.getCode()));
diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
 
b/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
index 8f3ba700..6ce600d7 100644
--- 
a/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
+++ 
b/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java
@@ -367,6 +367,7 @@ public class ProjectService {
             List<ProjectFile> files = 
karavanCache.getProjectFilesByName(PROJECT_COMPOSE_FILENAME).stream()
                     .filter(f -> !Objects.equals(f.getProjectId(), 
ProjectFolder.Type.templates.name()))
                     .filter(f -> !Objects.equals(f.getProjectId(), 
ProjectFolder.Type.kamelets.name()))
+                    .filter(f -> !Objects.equals(f.getProjectId(), 
ProjectFolder.Type.contracts.name()))
                     .filter(f -> !Objects.equals(f.getProjectId(), 
ProjectFolder.Type.configuration.name()))
                     .toList();
             if (!files.isEmpty()) {

Reply via email to