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; + } + } +}
