This is an automated email from the ASF dual-hosted git repository.
oscerd 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 18f53efde6c7 CAMEL-25436: camel-test-infra-spiffe - fix the SPIRE
readiness race and rootless engines (#27936)
18f53efde6c7 is described below
commit 18f53efde6c706c2ad04a818ea75deed1bbfeb20
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Oct 16 11:14:16 2026 +0200
CAMEL-25436: camel-test-infra-spiffe - fix the SPIRE readiness race and
rootless engines (#27936)
The SPIRE infra handed the stack to the tests as soon as the agent's
Workload API socket existed. The SPIRE server only passes a new
registration entry on to its agents when it next rebuilds its entry cache
(every 5 seconds by default) and the agent picks it up on its next sync,
so for several seconds every fetch was answered PERMISSION_DENIED "no
identity issued". SpiffeMutualTlsIT's X509Source retries through that
window, SpiffeWorkloadApiIT's one-shot fetches do not, so the ITs failed
on the CI nodes where failsafe ran SpiffeWorkloadApiIT first. The infra
now waits for the agent to log the creation of the workload X509-SVID,
which it does once the entry is in its cache.
The entry's unix:uid selector used the host uid of the test JVM, but the
agent's unix workload attestor sees that uid through the user namespace
of the agent container, so under rootless Podman or Docker (where the
engine owner is uid 0) it never matched. The entry is now created once
the agent runs, for the uid translated through the agent's uid_map.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
---
.../spiffe/integration/SpiffeMutualTlsIT.java | 4 +-
.../spiffe/integration/SpiffeWorkloadApiIT.java | 4 +-
test-infra/camel-test-infra-spiffe/pom.xml | 5 ++
.../services/SpiffeLocalContainerInfraService.java | 81 ++++++++++++++++++++--
.../spiffe/services/SpiffeWorkloadUidTest.java | 73 +++++++++++++++++++
5 files changed, 156 insertions(+), 11 deletions(-)
diff --git
a/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeMutualTlsIT.java
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeMutualTlsIT.java
index acf5b59849d6..2fd11c24197e 100644
---
a/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeMutualTlsIT.java
+++
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeMutualTlsIT.java
@@ -51,8 +51,8 @@ import static
org.assertj.core.api.Assertions.assertThatThrownBy;
* handshake.
* <p>
* Linux only: the SPIRE agent is run in the host PID namespace with the
Workload API socket bind-mounted, which works
- * against a Linux Docker daemon but not Docker Desktop's VM or rootless
Podman, so the test is skipped off Linux rather
- * than left to fail on a developer machine.
+ * against a Linux Docker or Podman engine (rootful or rootless) but not
Docker Desktop's VM, so the test is skipped off
+ * Linux rather than left to fail on a developer machine.
*/
@EnabledOnOs(OS.LINUX)
class SpiffeMutualTlsIT extends CamelTestSupport {
diff --git
a/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeWorkloadApiIT.java
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeWorkloadApiIT.java
index 14ba7a5fc5c4..c8e0eb6e61bd 100644
---
a/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeWorkloadApiIT.java
+++
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeWorkloadApiIT.java
@@ -37,8 +37,8 @@ import static org.assertj.core.api.Assertions.assertThat;
* validating a JWT-SVID (accepting the configured audience, rejecting
another).
* <p>
* Linux only: the SPIRE agent is run in the host PID namespace with the
Workload API socket bind-mounted, which works
- * against a Linux Docker daemon but not Docker Desktop's VM or rootless
Podman, so the test is skipped off Linux rather
- * than left to fail on a developer machine.
+ * against a Linux Docker or Podman engine (rootful or rootless) but not
Docker Desktop's VM, so the test is skipped off
+ * Linux rather than left to fail on a developer machine.
*/
@EnabledOnOs(OS.LINUX)
class SpiffeWorkloadApiIT extends CamelTestSupport {
diff --git a/test-infra/camel-test-infra-spiffe/pom.xml
b/test-infra/camel-test-infra-spiffe/pom.xml
index 43df0fed7a57..0c7c28a24b5b 100644
--- a/test-infra/camel-test-infra-spiffe/pom.xml
+++ b/test-infra/camel-test-infra-spiffe/pom.xml
@@ -36,6 +36,11 @@
<artifactId>camel-test-infra-common</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<profiles>
diff --git
a/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeLocalContainerInfraService.java
b/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeLocalContainerInfraService.java
index 20aaecb545af..1e277f2ee554 100644
---
a/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeLocalContainerInfraService.java
+++
b/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeLocalContainerInfraService.java
@@ -20,6 +20,8 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
+import java.util.List;
+import java.util.regex.Pattern;
import com.sun.security.auth.module.UnixSystem;
import org.apache.camel.spi.annotations.InfraService;
@@ -30,6 +32,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.Container;
+import org.testcontainers.containers.ContainerLaunchException;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.SelinuxContext;
@@ -44,12 +47,16 @@ import org.testcontainers.utility.MountableFile;
* is ~108 bytes, so a socket under a deep temp tree would fail to connect
from the host). The agent runs in the host
* PID namespace because the {@code unix} workload attestor resolves the
calling process through {@code /proc}, which a
* container cannot see otherwise; a single registration entry is created for
{@code unix:uid:<the test process uid>} so
- * the test JVM is issued {@code spiffe://<trustDomain>/workload}.
+ * the test JVM is issued {@code spiffe://<trustDomain>/workload}. That uid is
the one the agent sees, translated
+ * through the user namespace of the agent container, so rootless Podman and
rootless Docker work as well.
* <p>
- * This assumes a Linux Docker host: the host PID namespace, the Unix-socket
bind mount and the {@code unix:uid}
+ * The service is only handed over once the agent holds that entry. The SPIRE
server passes a new entry on to its agents
+ * when it next rebuilds its entry cache (every 5 seconds by default), and the
agent picks it up on its next sync; until
+ * then the Workload API is up and healthy but answers {@code
PERMISSION_DENIED: no identity issued}.
+ * <p>
+ * This assumes a Linux container host: the host PID namespace, the
Unix-socket bind mount and the {@code unix:uid}
* selector all rely on the test JVM and the containers sharing one Linux
kernel. It is not expected to work on Docker
- * Desktop (macOS/Windows, where the daemon runs in a separate VM) or on
rootless Podman (uid remapping) - which is also
- * why the component gates these integration tests off non-amd64 CI. It
targets a Linux CI/development Docker.
+ * Desktop (macOS/Windows, where the daemon runs in a separate VM).
*/
@InfraService(service = SpiffeInfraService.class,
description = "SPIFFE/SPIRE server and agent exposing the
Workload API",
@@ -124,7 +131,6 @@ public class SpiffeLocalContainerInfraService implements
SpiffeInfraService, Con
waitForServerHealthy();
String joinToken = generateJoinToken();
- createWorkloadEntry();
// the agent needs the host PID namespace so the unix workload
attestor can resolve the host test process, and
// the socket directory bind-mounted (SELinux-shared) so the host can
reach the Workload API socket it creates
@@ -139,6 +145,11 @@ public class SpiffeLocalContainerInfraService implements
SpiffeInfraService, Con
agent.start();
waitForSocket();
+ // the entry is only created now that the agent runs, as its selector
needs the uid the agent sees, which
+ // depends on the user namespace of the agent container
+ createWorkloadEntry(workloadUid());
+ waitForWorkloadEntry();
+
socketPath = "unix://" + hostSocketDir.resolve(SOCKET_FILE);
registerProperties();
LOG.info("SPIRE Workload API available at {}", socketPath);
@@ -191,12 +202,68 @@ public class SpiffeLocalContainerInfraService implements
SpiffeInfraService, Con
throw new RuntimeException("Could not parse a join token from: " +
result.getStdout());
}
- private void createWorkloadEntry() {
- long uid = new UnixSystem().getUid();
+ private void createWorkloadEntry(long uid) {
+ LOG.info("Registering {} for the selector unix:uid:{}",
WORKLOAD_SPIFFE_ID, uid);
exec(server, SERVER_BIN, "entry", "create", "-parentID",
AGENT_SPIFFE_ID, "-spiffeID", WORKLOAD_SPIFFE_ID,
"-selector", "unix:uid:" + uid);
}
+ /**
+ * Returns the uid of this JVM as the agent's unix workload attestor sees
it. The attestor reads the uid from
+ * {@code /proc/<pid>/status} inside the agent container's user namespace,
so the kernel translates it through the
+ * {@code uid_map} of that namespace (see {@link #uidInNamespace(long,
List, long)}).
+ */
+ private long workloadUid() {
+ long uid = new UnixSystem().getUid();
+ Path uidMap = Path.of("/proc",
String.valueOf(agent.getContainerInfo().getState().getPidLong()), "uid_map");
+ try {
+ long overflowUid =
Long.parseLong(Files.readString(Path.of("/proc/sys/kernel/overflowuid")).trim());
+ return uidInNamespace(uid, Files.readAllLines(uidMap),
overflowUid);
+ } catch (IOException | NumberFormatException e) {
+ // no mapping to read (for example the engine is not on this
host): use the uid as is, as a rootful engine does
+ LOG.warn("Could not read the user namespace mapping of the SPIRE
agent from {}, using uid {} unchanged", uidMap,
+ uid, e);
+ return uid;
+ }
+ }
+
+ /**
+ * Translates a uid of this host into a user namespace, given the {@code
uid_map} of the namespace as read from this
+ * host. Each line maps the {@code <count>} uids starting at {@code
<outside>} onto the uids starting at
+ * {@code <inside>}, and a uid outside every range is seen as the kernel
overflow uid. So the uid is unchanged under
+ * a rootful engine, becomes 0 for the engine owner under rootless Podman
or Docker, and becomes the overflow uid
+ * when the namespace does not map it.
+ */
+ static long uidInNamespace(long uid, List<String> uidMap, long
overflowUid) {
+ for (String line : uidMap) {
+ // <inside> <outside> <count>
+ String[] range = line.trim().split("\\s+");
+ long inside = Long.parseLong(range[0]);
+ long outside = Long.parseLong(range[1]);
+ long count = Long.parseLong(range[2]);
+ if (uid >= outside && uid - outside < count) {
+ return inside + uid - outside;
+ }
+ }
+ return overflowUid;
+ }
+
+ private void waitForWorkloadEntry() {
+ // the agent logs the creation of the workload X509-SVID once the
entry is in its cache, which is when a fetch
+ // for this workload stops being answered with PERMISSION_DENIED "no
identity issued"
+ try {
+ Wait.forLogMessage(".*Creating X509-SVID.*" +
Pattern.quote(WORKLOAD_SPIFFE_ID) + ".*", 1)
+ .withStartupTimeout(Duration.ofSeconds(60))
+ .waitUntilReady(agent);
+ } catch (ContainerLaunchException e) {
+ throw new RuntimeException(
+ "The SPIRE agent did not receive the registration entry
for " + WORKLOAD_SPIFFE_ID
+ + " in time, agent log:\n"
+ + agent.getLogs(),
+ e);
+ }
+ }
+
private void waitForSocket() {
Path socket = hostSocketDir.resolve(SOCKET_FILE);
long deadline = System.currentTimeMillis() +
Duration.ofSeconds(60).toMillis();
diff --git
a/test-infra/camel-test-infra-spiffe/src/test/java/org/apache/camel/test/infra/spiffe/services/SpiffeWorkloadUidTest.java
b/test-infra/camel-test-infra-spiffe/src/test/java/org/apache/camel/test/infra/spiffe/services/SpiffeWorkloadUidTest.java
new file mode 100644
index 000000000000..7ca897bb7e79
--- /dev/null
+++
b/test-infra/camel-test-infra-spiffe/src/test/java/org/apache/camel/test/infra/spiffe/services/SpiffeWorkloadUidTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.test.infra.spiffe.services;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import static
org.apache.camel.test.infra.spiffe.services.SpiffeLocalContainerInfraService.uidInNamespace;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The uid the SPIRE agent's unix workload attestor sees for the test JVM,
given the {@code uid_map} of the agent
+ * container as each engine sets it up (the lines are as the kernel prints
them).
+ */
+class SpiffeWorkloadUidTest {
+
+ private static final long OVERFLOW_UID = 65534;
+
+ @Test
+ void aRootfulEngineLeavesTheUidUnchanged() {
+ List<String> uidMap = List.of(" 0 0 4294967295");
+
+ assertThat(uidInNamespace(1000, uidMap, OVERFLOW_UID)).isEqualTo(1000);
+ assertThat(uidInNamespace(0, uidMap, OVERFLOW_UID)).isZero();
+ }
+
+ @Test
+ void aRootlessEngineMapsItsOwnerToRoot() {
+ // rootless Podman and rootless Docker map the engine owner to 0 and
its subordinate uids from 1 upwards
+ List<String> uidMap = List.of(
+ " 0 1000 1",
+ " 1 524288 65536");
+
+ assertThat(uidInNamespace(1000, uidMap, OVERFLOW_UID)).isZero();
+ assertThat(uidInNamespace(524288, uidMap, OVERFLOW_UID)).isEqualTo(1);
+ }
+
+ @Test
+ void aKeepIdNamespaceKeepsTheOwnerUid() {
+ // Podman --userns=keep-id maps the engine owner onto its own uid
+ List<String> uidMap = List.of(
+ " 0 524288 1000",
+ " 1000 1000 1",
+ " 1001 525288 64536");
+
+ assertThat(uidInNamespace(1000, uidMap, OVERFLOW_UID)).isEqualTo(1000);
+ }
+
+ @Test
+ void anUnmappedUidIsSeenAsTheOverflowUid() {
+ List<String> uidMap = List.of(
+ " 0 1000 1",
+ " 1 524288 65536");
+
+ assertThat(uidInNamespace(1001, uidMap,
OVERFLOW_UID)).isEqualTo(OVERFLOW_UID);
+ assertThat(uidInNamespace(524288 + 65536, uidMap,
OVERFLOW_UID)).isEqualTo(OVERFLOW_UID);
+ }
+}