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

davsclaus pushed a commit to branch CAMEL-22280-infra-ps-container
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 636cc8ebac7bd26742343494e4e7894edfbe6870
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Nov 12 19:39:17 2026 +0100

    CAMEL-22280: camel-jbang - camel infra ps shows the container of a running 
service
    
    camel infra run keeps the name and short id of the container a service
    runs in with its service data, and camel infra ps shows it in a
    CONTAINER column, for docker exec, logs and inspect. The IMPLEMENTATION
    column no longer cuts its last few characters.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../ROOT/pages/camel-jbang-dev-services.adoc       | 14 ++--
 .../core/commands/infra/InfraBaseCommand.java      | 31 +++++++-
 .../dsl/jbang/core/commands/infra/InfraRun.java    | 40 ++++++++++
 .../dsl/jbang/core/commands/infra/InfraPsTest.java | 22 ++++++
 .../core/commands/infra/InfraRunContainerTest.java | 85 ++++++++++++++++++++++
 5 files changed, 184 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..a02088686c3b 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
@@ -283,6 +283,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 +403,45 @@ 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 (Exception e) {
+            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;
+        }
+    }
+}

Reply via email to