This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 345c136dfb6d CAMEL-22280: camel-jbang - camel infra ps shows the
container of a running service (#29747)
345c136dfb6d is described below
commit 345c136dfb6d1df1099dadaa239aff5ef5df6924
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Nov 13 08:24:06 2026 +0100
CAMEL-22280: camel-jbang - camel infra ps shows the container of a running
service (#29747)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../ROOT/pages/camel-jbang-dev-services.adoc | 14 ++--
.../core/commands/infra/InfraBaseCommand.java | 31 +++++++-
.../dsl/jbang/core/commands/infra/InfraRun.java | 50 +++++++++++++
.../dsl/jbang/core/commands/infra/InfraPsTest.java | 22 ++++++
.../core/commands/infra/InfraRunContainerTest.java | 85 ++++++++++++++++++++++
5 files changed, 194 insertions(+), 8 deletions(-)
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-dev-services.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-dev-services.adoc
index 616e9d307953..3f20676abe48 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-dev-services.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-dev-services.adoc
@@ -114,19 +114,23 @@ camel infra restart kafka --background
[source,bash]
----
$ camel infra ps
- PID ALIAS IMPLEMENTATION UI DESCRIPTION
SERVICE_DATA
- 45211 arangodb ArangoDB is a multi-model database for
high-pe... {"getServiceAddress":"http:/...
+ PID ALIAS IMPLEMENTATION UI DESCRIPTION
CONTAINER SERVICE_DATA
+ 45211 arangodb ArangoDB is a multi-model database for
hig… camel-arangodb {"containerId":"3f2a9c1b7d4e",…
----
+The CONTAINER column is the Docker container the service runs in, so you can
use it with `docker exec`,
+`docker logs` or `docker inspect`. A service that runs embedded, in the
process itself, has none. The
+container name and its short id are in the connection details too
(`containerName` and `containerId`).
+
There is one row per running instance, so starting the same service twice
lists both processes, each
with its own PID and connection details:
[source,bash]
----
$ camel infra ps
- PID ALIAS IMPLEMENTATION UI DESCRIPTION
SERVICE_DATA
- 45244 ftp FTP is a standard communication protocol
... {"port":21,"host":"localhost"...
- 45262 ftp FTP is a standard communication protocol
... {"port":2121,"host":"localhos...
+ PID ALIAS IMPLEMENTATION UI DESCRIPTION
CONTAINER SERVICE_DATA
+ 45244 ftp FTP is a standard communication protocol …
{"port":21,"host":"localhost"…
+ 45262 ftp FTP is a standard communication protocol …
{"port":2121,"host":"localhos…
----
Pass a service name to list only its instances, and `--json` to get the full
connection details of each
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraBaseCommand.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraBaseCommand.java
index dd13fddd7e7b..a15ac0b7b524 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraBaseCommand.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraBaseCommand.java
@@ -338,18 +338,25 @@ public abstract class InfraBaseCommand extends
CamelCommand {
int pidWidth = showPidColumn()
? CamelTableColumns.measure("PID", Integer.MAX_VALUE,
rows, r -> r.pid) : 0;
int implWidth = CamelTableColumns.measure("IMPLEMENTATION", 35,
rows, Row::aliasImplementation);
- int overhead =
TerminalWidthHelper.noBorderOverhead(showPidColumn() ? 6 : 5);
+ // the container is shown for the running services only (infra ps)
+ int containerWidth = showPidColumn()
+ ? CamelTableColumns.measure("CONTAINER", 30, rows, r ->
containerOf(r.serviceData())) : 0;
+ int overhead =
TerminalWidthHelper.noBorderOverhead(showPidColumn() ? 7 : 5);
int descWidth = CamelTableColumns.lastColumnWidth(
- tw, overhead, pidWidth, aliasWidth, implWidth, uiWidth,
serviceDataWidth);
+ tw, overhead, pidWidth, aliasWidth, implWidth, uiWidth,
containerWidth, serviceDataWidth);
printer().println(AsciiTable.getTable(AsciiTable.NO_BORDERS, rows,
Arrays.asList(
new
Column().header("PID").visible(showPidColumn()).headerAlign(HorizontalAlign.CENTER).with(r
-> r.pid),
new
Column().header("ALIAS").minWidth(aliasWidth).dataAlign(HorizontalAlign.LEFT)
.with(Row::alias),
- new Column().header("IMPLEMENTATION").maxWidth(implWidth,
OverflowBehaviour.ELLIPSIS_RIGHT)
+ // the max width of a column includes its padding, the
measured width does not
+ new Column().header("IMPLEMENTATION").maxWidth(implWidth +
2, OverflowBehaviour.ELLIPSIS_RIGHT)
.dataAlign(HorizontalAlign.LEFT).with(Row::aliasImplementation),
new
Column().header("UI").maxWidth(uiWidth).dataAlign(HorizontalAlign.CENTER)
.with(r -> r.uiSupported() ? "x" : ""),
CamelTableColumns.lastText("DESCRIPTION",
descWidth).with(Row::description),
+ new Column().header("CONTAINER").visible(showPidColumn())
+ .maxWidth(containerWidth + 2,
OverflowBehaviour.ELLIPSIS_RIGHT)
+ .dataAlign(HorizontalAlign.LEFT).with(r ->
containerOf(r.serviceData())),
new
Column().header("SERVICE_DATA").maxWidth(serviceDataWidth,
OverflowBehaviour.ELLIPSIS_RIGHT)
.dataAlign(HorizontalAlign.LEFT).with(Row::serviceData))));
}
@@ -373,6 +380,24 @@ public abstract class InfraBaseCommand extends
CamelCommand {
}
}
+ /**
+ * The container a running service is in, as {@code infra run} wrote it to
its service data: the container name, or
+ * the short container id when it has no name. Returns {@code null} for a
service that does not run in a container.
+ */
+ static String containerOf(String serviceData) {
+ if (parseServiceData(serviceData) instanceof Map<?, ?> data) {
+ Object name = data.get("containerName");
+ if (name != null) {
+ return name.toString();
+ }
+ Object id = data.get("containerId");
+ if (id != null) {
+ return id.toString();
+ }
+ }
+ return null;
+ }
+
public String getLogFileName(String service, String pid) {
return String.format("infra-%s-%s.log", service, pid);
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRun.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRun.java
index 913b3100ea01..93cea35d2c89 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRun.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRun.java
@@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.infra;
import java.io.File;
import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.file.Files;
@@ -47,6 +48,8 @@ import
org.apache.camel.main.download.MavenDependencyDownloader;
import org.apache.camel.tooling.maven.MavenArtifact;
import org.apache.camel.util.json.JsonObject;
import org.apache.logging.log4j.core.config.Configurator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import static
org.apache.camel.dsl.jbang.core.commands.RunHelper.addCamelCLICommand;
@@ -58,6 +61,8 @@ import static
org.apache.camel.dsl.jbang.core.commands.RunHelper.addCamelCLIComm
" camel infra run ollama --property
ollama.model=qwen2.5:0.5b" })
public class InfraRun extends InfraBaseCommand {
+ private static final Logger LOG = LoggerFactory.getLogger(InfraRun.class);
+
@Arguments(required = true, description = "Service name")
private List<String> serviceName;
@@ -283,6 +288,7 @@ public class InfraRun extends InfraBaseCommand {
if (testInfraService.description() != null &&
!testInfraService.description().isEmpty()) {
properties.put("description", testInfraService.description());
}
+ addContainerDetails(actualService, properties);
String jsonProperties =
jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(properties);
printer().println(jsonProperties);
@@ -402,6 +408,50 @@ public class InfraRun extends InfraBaseCommand {
});
}
+ /**
+ * Adds the name and id of the container a service runs in, so that {@code
docker exec}, {@code docker logs} and
+ * {@code camel infra ps} can tell which container it is. A service that
does not run in a container, such as an
+ * embedded broker, has none.
+ *
+ * @param service the started service
+ * @param properties the service data to add the container details to
+ */
+ static void addContainerDetails(Object service, Map<String, Object>
properties) {
+ Object container;
+ try {
+ container =
service.getClass().getMethod("getContainer").invoke(service);
+ } catch (Exception e) {
+ // not a container service
+ return;
+ }
+ if (container == null) {
+ return;
+ }
+ String name = (String) invokeQuietly(container, "getContainerName");
+ if (name != null && !name.isBlank()) {
+ // Docker answers the name with a leading slash
+ properties.put("containerName", name.startsWith("/") ?
name.substring(1) : name);
+ }
+ String id = (String) invokeQuietly(container, "getContainerId");
+ if (id != null && !id.isBlank()) {
+ // the short id, as docker ps shows it
+ properties.put("containerId", id.length() > 12 ? id.substring(0,
12) : id);
+ }
+ }
+
+ private static Object invokeQuietly(Object target, String methodName) {
+ try {
+ return target.getClass().getMethod(methodName).invoke(target);
+ } catch (InvocationTargetException e) {
+ // the service has the method, but it failed, such as when docker
cannot be reached
+ LOG.debug("{}#{} failed", target.getClass().getSimpleName(),
methodName, e.getCause());
+ return null;
+ } catch (Exception e) {
+ // not a container service
+ return null;
+ }
+ }
+
private static void clearInfraProperties() {
System.clearProperty("camel.infra.port");
System.clearProperty("camel.infra.fixedPort");
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraPsTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraPsTest.java
index 4c197ba980d5..0893a7f05c15 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraPsTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraPsTest.java
@@ -158,6 +158,28 @@ class InfraPsTest extends InfraCommandTestSupport {
assertFalse(Files.exists(logFile("ftp", 5678)), "the dead instance's
log file should be pruned");
}
+ @Test
+ void shouldShowTheContainerOfARunningService() throws Exception {
+ // CAMEL-22280: the container infra run started, so docker exec and
docker logs know which one it is
+ writePidFile("kafka", 1234,
"{\"containerId\":\"0a1b2c3d4e5f\",\"containerName\":\"camel-kafka\"}");
+ // without a name (an older infra run) the short id is shown
+ writePidFile("nats", 5678, "{\"containerId\":\"ba9876543210\"}");
+ // an embedded service runs in no container
+ writePidFile("ftp", 4321, "{\"port\":21}");
+
+ int exit = command().doCall();
+
+ assertEquals(0, exit);
+ String out = printer.getOutput();
+ assertTrue(out.lines().findFirst().orElse("").contains("CONTAINER"),
"the header should name the column, was: " + out);
+ String kafka = out.lines().filter(l ->
l.contains("1234")).findFirst().orElse("");
+ assertTrue(kafka.contains("camel-kafka"), "kafka should show its
container name, was: " + out);
+ // and its implementations in full
+ assertTrue(kafka.contains("strimzi"), "kafka should show all its
implementations, was: " + out);
+ String nats = out.lines().filter(l ->
l.contains("5678")).findFirst().orElse("");
+ assertTrue(nats.contains("ba9876543210"), "nats should show its
container id, was: " + out);
+ }
+
@Test
void shouldTreatTheCurrentProcessAsAliveByDefault() throws Exception {
// exercises the real ProcessHandle based aliveCheck rather than a
stub: this JVM is certainly running
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRunContainerTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRunContainerTest.java
new file mode 100644
index 000000000000..bcc2c32d485a
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRunContainerTest.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.infra;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
+
+/**
+ * {@code camel infra run} keeps the container a service runs in with its
service data (CAMEL-22280).
+ */
+class InfraRunContainerTest {
+
+ @Test
+ void theNameAndShortIdOfTheContainer() {
+ Map<String, Object> properties = new TreeMap<>();
+
+ InfraRun.addContainerDetails(
+ new FakeContainerService(
+ new FakeContainer(
+ "/camel-kafka",
+
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")),
+ properties);
+
+ assertThat(properties).containsOnly(entry("containerName",
"camel-kafka"), entry("containerId", "0123456789ab"));
+ }
+
+ @Test
+ void aServiceThatRunsInNoContainer() {
+ Map<String, Object> properties = new TreeMap<>();
+
+ InfraRun.addContainerDetails(new Object(), properties);
+ InfraRun.addContainerDetails(new FakeContainerService(null),
properties);
+
+ assertThat(properties).isEmpty();
+ }
+
+ static class FakeContainerService {
+ private final FakeContainer container;
+
+ FakeContainerService(FakeContainer container) {
+ this.container = container;
+ }
+
+ public FakeContainer getContainer() {
+ return container;
+ }
+ }
+
+ static class FakeContainer {
+ private final String name;
+ private final String id;
+
+ FakeContainer(String name, String id) {
+ this.name = name;
+ this.id = id;
+ }
+
+ public String getContainerName() {
+ return name;
+ }
+
+ public String getContainerId() {
+ return id;
+ }
+ }
+}