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()) {
