You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by "oscerd (via GitHub)" <gi...@apache.org> on 2026/10/06 08:26:45 UTC
[PR] CAMEL-24733: camel-spiffe - add integration tests against a real SPIRE agent [camel]
oscerd opened a new pull request, #27429:
URL: https://github.com/apache/camel/pull/27429
# CAMEL-24733: camel-spiffe - integration tests against a real SPIRE agent
`camel-spiffe` had no integration tests and no test-infra module — everything was unit-tested against a mocked
`WorkloadApiClient` / `X509Source`. That left `SpiffeSSLContextParameters` (the class that builds the mTLS `SSLContext`
and decides which peers are accepted, the most security-critical code in the component) never exercised against a real
Workload API.
## What this adds
**New module `test-infra/camel-test-infra-spiffe`** — mirrors `camel-test-infra-opa`. It runs a SPIRE **server** plus
**agent** (images `ghcr.io/spiffe/spire-server|spire-agent:1.15.3`) and exposes the Workload API Unix socket to the
test JVM, via `SpiffeServiceFactory.createSingletonService()`.
The orchestration handles the realities of attesting a host process from a containerised agent:
- the agent runs in the **host PID namespace** (`--pid host`) so the `unix` workload attestor can resolve the calling
test process through `/proc`;
- the Workload API socket directory is bind-mounted to a **short** host path (the `AF_UNIX` `sun_path` limit is ~108
bytes, so a socket under a deep temp tree cannot be connected to from the host), SELinux-shared (`:z`);
- a single registration entry is created for `unix:uid:<the test process uid>`, so the JVM is issued
`spiffe://example.org/workload`;
- the SPIRE configs are copied into the (distroless) images, and the agent joins with a single-use join token generated
once the server is healthy.
**Integration tests in `camel-spiffe`** (`src/test/.../integration/`):
- `SpiffeWorkloadApiIT` — fetch an X509-SVID and a JWT-SVID through the component producer; validate a JWT-SVID against
its configured audience, and reject one minted for another audience.
- `SpiffeMutualTlsIT` — complete a real mutual-TLS handshake through `SpiffeSSLContextParameters`, asserting that an
allow-listed peer connects and a non-allow-listed one is refused during the handshake.
## Image/architecture constraint
The SPIRE images are published for `amd64`/`arm64` only, so `camel-spiffe` sets `skipITs.ppc64le` / `skipITs.s390x`
(the same accommodation `camel-opa` made).
## Validation
Run locally against real SPIRE containers: **6 integration tests pass** (plus the existing 41 unit tests). The agent
attested the host JVM, issued the SVID, and all three scenarios — SVID fetch, JWT validation incl. wrong-audience
rejection, and the mTLS allow/deny handshake — passed.
`main` only; test-only plus the new test-infra module.
---
_Claude Code on behalf of oscerd_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-24733: camel-spiffe - add integration tests against a real SPIRE agent [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus commented on code in PR #27429:
URL: https://github.com/apache/camel/pull/27429#discussion_r4193499758
##########
components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeMutualTlsIT.java:
##########
@@ -0,0 +1,135 @@
+/*
+ * 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.component.spiffe.integration;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLSocket;
+
+import org.apache.camel.component.spiffe.SpiffeSSLContextParameters;
+import org.apache.camel.support.jsse.ClientAuthentication;
+import org.apache.camel.support.jsse.SSLContextServerParameters;
+import org.apache.camel.test.infra.spiffe.services.SpiffeService;
+import org.apache.camel.test.infra.spiffe.services.SpiffeServiceFactory;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Completes a real mutual-TLS handshake through {@link SpiffeSSLContextParameters} against SVIDs from a live SPIRE
+ * Workload API, asserting that an allow-listed peer connects and a non-allow-listed one is refused during the
+ * handshake.
+ */
+class SpiffeMutualTlsIT extends CamelTestSupport {
+
+ @RegisterExtension
+ static SpiffeService service = SpiffeServiceFactory.createSingletonService();
+
+ private SpiffeSSLContextParameters serverSsl(String acceptedSpiffeIds) {
+ SpiffeSSLContextParameters ssl = new SpiffeSSLContextParameters();
+ ssl.setSpiffeSocketPath(service.getWorkloadApiSocketPath());
+ ssl.setAcceptedSpiffeIds(acceptedSpiffeIds);
+ SSLContextServerParameters serverParameters = new SSLContextServerParameters();
+ serverParameters.setClientAuthentication(ClientAuthentication.REQUIRE.name());
+ ssl.setServerParameters(serverParameters);
+ return ssl;
+ }
+
+ private SpiffeSSLContextParameters clientSsl() {
+ SpiffeSSLContextParameters ssl = new SpiffeSSLContextParameters();
+ ssl.setSpiffeSocketPath(service.getWorkloadApiSocketPath());
+ ssl.setAcceptedSpiffeIds(service.getWorkloadSpiffeId());
+ return ssl;
+ }
+
+ @Test
+ void allowsAnAllowListedPeer() throws Exception {
+ SSLContext serverContext = serverSsl(service.getWorkloadSpiffeId()).createSSLContext(context);
+ SSLContext clientContext = clientSsl().createSSLContext(context);
+
+ assertThatCode(() -> handshake(serverContext, clientContext)).doesNotThrowAnyException();
+ }
+
+ @Test
+ void refusesANonAllowListedPeer() throws Exception {
+ // the server accepts only an id the client does not have; its SVID is spiffe://example.org/workload
+ SSLContext serverContext = serverSsl("spiffe://example.org/not-allowed").createSSLContext(context);
+ SSLContext clientContext = clientSsl().createSSLContext(context);
+
+ // a rejected mutual-TLS handshake surfaces as an IOException - a TLS alert (SSLHandshakeException) when the peer
+ // sends one, or a connection reset (SocketException) when it just drops the connection; both are acceptable, the
+ // point is that the handshake does not complete
+ assertThatThrownBy(() -> handshake(serverContext, clientContext))
+ .as("a non-allow-listed peer must be refused during the handshake")
+ .isInstanceOf(IOException.class);
Review Comment:
Minor: `IOException` also accepts a `SocketTimeoutException` or a connect failure, so the deny test can pass for the wrong reason. Asserting the server-side `SSLHandshakeException` (ideally with the rejected SPIFFE ID in the message), or at least excluding `SocketTimeoutException`, would make it precise.
##########
test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeLocalContainerInfraService.java:
##########
@@ -0,0 +1,302 @@
+/*
+ * 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.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+
+import com.sun.security.auth.module.UnixSystem;
+import org.apache.camel.spi.annotations.InfraService;
+import org.apache.camel.test.infra.common.LocalPropertyResolver;
+import org.apache.camel.test.infra.common.services.ContainerService;
+import org.apache.camel.test.infra.spiffe.common.SpiffeProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.SelinuxContext;
+import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.MountableFile;
+
+/**
+ * Runs a SPIRE server and agent so a test can fetch SVIDs from a real Workload API.
+ * <p>
+ * The agent's Workload API Unix socket is bind-mounted to a short host path (the {@code AF_UNIX} {@code sun_path} limit
+ * 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}.
+ */
+@InfraService(service = SpiffeInfraService.class,
+ description = "SPIFFE/SPIRE server and agent exposing the Workload API",
+ serviceAlias = { "spiffe" })
+public class SpiffeLocalContainerInfraService implements SpiffeInfraService, ContainerService<GenericContainer<?>> {
+
+ public static final String TRUST_DOMAIN = "example.org";
+ public static final String AGENT_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN + "/agent";
+ public static final String WORKLOAD_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN + "/workload";
+
+ private static final Logger LOG = LoggerFactory.getLogger(SpiffeLocalContainerInfraService.class);
+
+ private static final String SERVER_ALIAS = "spire-server";
+ private static final int SERVER_PORT = 8081;
+ private static final String SERVER_BIN = "/opt/spire/bin/spire-server";
+ private static final String SERVER_CONF = "/opt/spire/conf/server/server.conf";
+ private static final String AGENT_CONF = "/opt/spire/conf/agent/agent.conf";
+ private static final String SOCKET_CONTAINER_DIR = "/tmp/spire-agent/public";
+ private static final String SOCKET_FILE = "api.sock";
+ private static final String SERVER_CONF_RESOURCE = "org/apache/camel/test/infra/spiffe/services/spire-server.conf";
+ private static final String AGENT_CONF_RESOURCE = "org/apache/camel/test/infra/spiffe/services/spire-agent.conf";
+
+ private final String serverImage;
+ private final String agentImage;
+
+ private Network network;
+ private GenericContainer<?> server;
+ private GenericContainer<?> agent;
+ private Path hostSocketDir;
+ private String socketPath;
+
+ public SpiffeLocalContainerInfraService() {
+ this(LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_SERVER_CONTAINER),
+ LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_AGENT_CONTAINER));
+ }
+
+ public SpiffeLocalContainerInfraService(String serverImage, String agentImage) {
+ this.serverImage = serverImage;
+ this.agentImage = agentImage;
+ }
+
+ @Override
+ public void initialize() {
+ try {
+ doInitialize();
+ } catch (RuntimeException e) {
+ // a partial start would leak the server container, the network and the temp socket directory, and a
+ // TestServiceUtil.tryInitialize() retry would overwrite the fields and orphan them; tear down whatever
+ // came up before propagating
+ cleanup();
+ throw e;
+ }
+ }
+
+ private void doInitialize() {
+ createHostSocketDir();
+ network = Network.newNetwork();
+
+ // the image entrypoint is "spire-server run", so only -config is passed; the config is copied in (no bind mount
+ // needed for it) and port 8081 is exposed purely so the listening-port wait can tell when the API is up
+ server = new GenericContainer<>(serverImage)
+ .withNetwork(network)
+ .withNetworkAliases(SERVER_ALIAS)
+ .withCopyFileToContainer(MountableFile.forClasspathResource(SERVER_CONF_RESOURCE), SERVER_CONF)
+ .withExposedPorts(SERVER_PORT)
+ .withCommand("-config", SERVER_CONF)
+ .waitingFor(Wait.forListeningPort());
+ LOG.info("Starting the SPIRE server container");
+ server.start();
+ 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
+ agent = new GenericContainer<>(agentImage)
+ .withNetwork(network)
+ .withCopyFileToContainer(MountableFile.forClasspathResource(AGENT_CONF_RESOURCE), AGENT_CONF)
+ .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withPidMode("host"))
Review Comment:
Host PID mode, the unix socket in the bind-mounted directory and the uid selector all assume a Linux Docker host; see the review summary for macOS/Windows Docker Desktop and rootless Podman.
##########
components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeMutualTlsIT.java:
##########
@@ -0,0 +1,135 @@
+/*
+ * 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.component.spiffe.integration;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLSocket;
+
+import org.apache.camel.component.spiffe.SpiffeSSLContextParameters;
+import org.apache.camel.support.jsse.ClientAuthentication;
+import org.apache.camel.support.jsse.SSLContextServerParameters;
+import org.apache.camel.test.infra.spiffe.services.SpiffeService;
+import org.apache.camel.test.infra.spiffe.services.SpiffeServiceFactory;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Completes a real mutual-TLS handshake through {@link SpiffeSSLContextParameters} against SVIDs from a live SPIRE
+ * Workload API, asserting that an allow-listed peer connects and a non-allow-listed one is refused during the
+ * handshake.
+ */
+class SpiffeMutualTlsIT extends CamelTestSupport {
+
+ @RegisterExtension
+ static SpiffeService service = SpiffeServiceFactory.createSingletonService();
+
+ private SpiffeSSLContextParameters serverSsl(String acceptedSpiffeIds) {
+ SpiffeSSLContextParameters ssl = new SpiffeSSLContextParameters();
+ ssl.setSpiffeSocketPath(service.getWorkloadApiSocketPath());
+ ssl.setAcceptedSpiffeIds(acceptedSpiffeIds);
+ SSLContextServerParameters serverParameters = new SSLContextServerParameters();
+ serverParameters.setClientAuthentication(ClientAuthentication.REQUIRE.name());
+ ssl.setServerParameters(serverParameters);
+ return ssl;
+ }
+
+ private SpiffeSSLContextParameters clientSsl() {
+ SpiffeSSLContextParameters ssl = new SpiffeSSLContextParameters();
+ ssl.setSpiffeSocketPath(service.getWorkloadApiSocketPath());
+ ssl.setAcceptedSpiffeIds(service.getWorkloadSpiffeId());
+ return ssl;
+ }
+
+ @Test
+ void allowsAnAllowListedPeer() throws Exception {
+ SSLContext serverContext = serverSsl(service.getWorkloadSpiffeId()).createSSLContext(context);
+ SSLContext clientContext = clientSsl().createSSLContext(context);
+
+ assertThatCode(() -> handshake(serverContext, clientContext)).doesNotThrowAnyException();
+ }
+
+ @Test
+ void refusesANonAllowListedPeer() throws Exception {
+ // the server accepts only an id the client does not have; its SVID is spiffe://example.org/workload
+ SSLContext serverContext = serverSsl("spiffe://example.org/not-allowed").createSSLContext(context);
+ SSLContext clientContext = clientSsl().createSSLContext(context);
+
+ // a rejected mutual-TLS handshake surfaces as an IOException - a TLS alert (SSLHandshakeException) when the peer
+ // sends one, or a connection reset (SocketException) when it just drops the connection; both are acceptable, the
+ // point is that the handshake does not complete
+ assertThatThrownBy(() -> handshake(serverContext, clientContext))
+ .as("a non-allow-listed peer must be refused during the handshake")
+ .isInstanceOf(IOException.class);
+ }
+
+ /**
+ * Drives a mutual-TLS handshake over a loopback socket: the server requires a client certificate and both sides
+ * present their SVID. Returns normally when the handshake and a one-byte exchange complete, and throws when either
+ * side rejects the peer.
+ */
+ private void handshake(SSLContext serverContext, SSLContext clientContext) throws Exception {
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ try (SSLServerSocket serverSocket = (SSLServerSocket) serverContext.getServerSocketFactory()
+ .createServerSocket(0, 1, InetAddress.getLoopbackAddress())) {
+ serverSocket.setNeedClientAuth(true);
Review Comment:
This forces client authentication directly on the socket, bypassing the `clientAuthentication=REQUIRE` configured on the `SSLContextServerParameters`. With this line the deny test would still pass if the SSL context decorator dropped the client authentication again (the CAMEL-24571 regression CAMEL-24733 asks to cover). Suggest removing it, so client auth only comes from `SpiffeSSLContextParameters`.
##########
test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeLocalContainerInfraService.java:
##########
@@ -0,0 +1,302 @@
+/*
+ * 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.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+
+import com.sun.security.auth.module.UnixSystem;
+import org.apache.camel.spi.annotations.InfraService;
+import org.apache.camel.test.infra.common.LocalPropertyResolver;
+import org.apache.camel.test.infra.common.services.ContainerService;
+import org.apache.camel.test.infra.spiffe.common.SpiffeProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.SelinuxContext;
+import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.MountableFile;
+
+/**
+ * Runs a SPIRE server and agent so a test can fetch SVIDs from a real Workload API.
+ * <p>
+ * The agent's Workload API Unix socket is bind-mounted to a short host path (the {@code AF_UNIX} {@code sun_path} limit
+ * 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}.
+ */
+@InfraService(service = SpiffeInfraService.class,
+ description = "SPIFFE/SPIRE server and agent exposing the Workload API",
+ serviceAlias = { "spiffe" })
+public class SpiffeLocalContainerInfraService implements SpiffeInfraService, ContainerService<GenericContainer<?>> {
+
+ public static final String TRUST_DOMAIN = "example.org";
+ public static final String AGENT_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN + "/agent";
+ public static final String WORKLOAD_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN + "/workload";
+
+ private static final Logger LOG = LoggerFactory.getLogger(SpiffeLocalContainerInfraService.class);
+
+ private static final String SERVER_ALIAS = "spire-server";
+ private static final int SERVER_PORT = 8081;
+ private static final String SERVER_BIN = "/opt/spire/bin/spire-server";
+ private static final String SERVER_CONF = "/opt/spire/conf/server/server.conf";
+ private static final String AGENT_CONF = "/opt/spire/conf/agent/agent.conf";
+ private static final String SOCKET_CONTAINER_DIR = "/tmp/spire-agent/public";
+ private static final String SOCKET_FILE = "api.sock";
+ private static final String SERVER_CONF_RESOURCE = "org/apache/camel/test/infra/spiffe/services/spire-server.conf";
+ private static final String AGENT_CONF_RESOURCE = "org/apache/camel/test/infra/spiffe/services/spire-agent.conf";
+
+ private final String serverImage;
+ private final String agentImage;
+
+ private Network network;
+ private GenericContainer<?> server;
+ private GenericContainer<?> agent;
+ private Path hostSocketDir;
+ private String socketPath;
+
+ public SpiffeLocalContainerInfraService() {
+ this(LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_SERVER_CONTAINER),
+ LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_AGENT_CONTAINER));
+ }
+
+ public SpiffeLocalContainerInfraService(String serverImage, String agentImage) {
+ this.serverImage = serverImage;
+ this.agentImage = agentImage;
+ }
+
+ @Override
+ public void initialize() {
+ try {
+ doInitialize();
+ } catch (RuntimeException e) {
+ // a partial start would leak the server container, the network and the temp socket directory, and a
+ // TestServiceUtil.tryInitialize() retry would overwrite the fields and orphan them; tear down whatever
+ // came up before propagating
+ cleanup();
+ throw e;
+ }
+ }
+
+ private void doInitialize() {
+ createHostSocketDir();
+ network = Network.newNetwork();
+
+ // the image entrypoint is "spire-server run", so only -config is passed; the config is copied in (no bind mount
+ // needed for it) and port 8081 is exposed purely so the listening-port wait can tell when the API is up
+ server = new GenericContainer<>(serverImage)
+ .withNetwork(network)
+ .withNetworkAliases(SERVER_ALIAS)
+ .withCopyFileToContainer(MountableFile.forClasspathResource(SERVER_CONF_RESOURCE), SERVER_CONF)
+ .withExposedPorts(SERVER_PORT)
+ .withCommand("-config", SERVER_CONF)
+ .waitingFor(Wait.forListeningPort());
+ LOG.info("Starting the SPIRE server container");
+ server.start();
+ 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
+ agent = new GenericContainer<>(agentImage)
+ .withNetwork(network)
+ .withCopyFileToContainer(MountableFile.forClasspathResource(AGENT_CONF_RESOURCE), AGENT_CONF)
+ .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withPidMode("host"))
+ .withStartupCheckStrategy(new IsRunningStartupCheckStrategy())
+ .withCommand("-config", AGENT_CONF, "-joinToken", joinToken);
+ agent.addFileSystemBind(hostSocketDir.toString(), SOCKET_CONTAINER_DIR, BindMode.READ_WRITE, SelinuxContext.SHARED);
+ LOG.info("Starting the SPIRE agent container");
+ agent.start();
+ waitForSocket();
+
+ socketPath = "unix://" + hostSocketDir.resolve(SOCKET_FILE);
+ registerProperties();
+ LOG.info("SPIRE Workload API available at {}", socketPath);
+ }
+
+ private void createHostSocketDir() {
+ try {
+ // keep the socket path short (AF_UNIX sun_path is limited to ~108 bytes); /tmp keeps it well within that
+ hostSocketDir = Files.createTempDirectory(Path.of("/tmp"), "spiffe");
+ hostSocketDir.toFile().setReadable(true, false);
+ hostSocketDir.toFile().setWritable(true, false);
Review Comment:
Nit: a short comment that the directory must be world-writable because the agent runs as a non-root uid would help, as this is a world-writable directory under `/tmp`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-24733: camel-spiffe - add integration tests against a real SPIRE agent [camel]
Posted by "gnodet-bot (via GitHub)" <gi...@apache.org>.
gnodet-bot commented on code in PR #27429:
URL: https://github.com/apache/camel/pull/27429#discussion_r4193403306
##########
components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeWorkloadApiIT.java:
##########
@@ -0,0 +1,108 @@
+/*
+ * 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.component.spiffe.integration;
+
+import io.spiffe.exception.JwtSvidException;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.spiffe.SpiffeConstants;
+import org.apache.camel.test.infra.spiffe.services.SpiffeService;
+import org.apache.camel.test.infra.spiffe.services.SpiffeServiceFactory;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Exercises the camel-spiffe producer against a real SPIRE Workload API: fetching an X509-SVID and a JWT-SVID, and
+ * validating a JWT-SVID (accepting the configured audience, rejecting another).
+ */
+class SpiffeWorkloadApiIT extends CamelTestSupport {
+
+ @RegisterExtension
+ static SpiffeService service = SpiffeServiceFactory.createSingletonService();
+
+ private static String socket() {
+ return "RAW(" + service.getWorkloadApiSocketPath() + ")";
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:x509")
+ .toF("spiffe:x509?operation=fetchX509Svid&spiffeSocketPath=%s", socket());
+ from("direct:jwt")
+ .toF("spiffe:jwt?operation=fetchJwtSvid&audience=my-audience&spiffeSocketPath=%s", socket());
+ from("direct:validateCorrectAudience")
+ .toF("spiffe:valOk?operation=validateJwtSvid&audience=my-audience&spiffeSocketPath=%s", socket());
+ from("direct:validateWrongAudience")
+ .toF("spiffe:valWrong?operation=validateJwtSvid&audience=other-audience&spiffeSocketPath=%s",
+ socket());
+ }
+ };
+ }
+
+ @Test
+ void fetchesAnX509SvidForThisWorkload() {
+ Exchange out = template.request("direct:x509", e -> {
+ });
+
+ assertThat(out.getException()).isNull();
+ assertThat(out.getMessage().getHeader(SpiffeConstants.SPIFFE_ID))
+ .isEqualTo(service.getWorkloadSpiffeId());
+ // the default x509Response is the certificate chain, which must carry at least the leaf
+ assertThat(out.getMessage().getBody()).isNotNull();
Review Comment:
💡 **Still not addressed from previous review.** `isNotNull()` only proves the body field was set — it doesn't verify the response is actually a certificate chain. A slightly stronger check:
```suggestion
assertThat(out.getMessage().getBody(String.class)).as("X509-SVID response").isNotEmpty();
```
Or, if the body is an `X509Certificate[]`, assert `hasSize` ≥ 1.
##########
test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeLocalContainerInfraService.java:
##########
@@ -0,0 +1,270 @@
+/*
+ * 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.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+
+import com.sun.security.auth.module.UnixSystem;
+import org.apache.camel.spi.annotations.InfraService;
+import org.apache.camel.test.infra.common.LocalPropertyResolver;
+import org.apache.camel.test.infra.common.services.ContainerService;
+import org.apache.camel.test.infra.spiffe.common.SpiffeProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.SelinuxContext;
+import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.MountableFile;
+
+/**
+ * Runs a SPIRE server and agent so a test can fetch SVIDs from a real Workload API.
+ * <p>
+ * The agent's Workload API Unix socket is bind-mounted to a short host path (the {@code AF_UNIX} {@code sun_path} limit
+ * 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}.
+ */
+@InfraService(service = SpiffeInfraService.class,
+ description = "SPIFFE/SPIRE server and agent exposing the Workload API",
+ serviceAlias = { "spiffe" })
+public class SpiffeLocalContainerInfraService implements SpiffeInfraService, ContainerService<GenericContainer<?>> {
+
+ public static final String TRUST_DOMAIN = "example.org";
+ public static final String AGENT_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN + "/agent";
+ public static final String WORKLOAD_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN + "/workload";
+
+ private static final Logger LOG = LoggerFactory.getLogger(SpiffeLocalContainerInfraService.class);
+
+ private static final String SERVER_ALIAS = "spire-server";
+ private static final int SERVER_PORT = 8081;
+ private static final String SERVER_BIN = "/opt/spire/bin/spire-server";
+ private static final String SERVER_CONF = "/opt/spire/conf/server/server.conf";
+ private static final String AGENT_CONF = "/opt/spire/conf/agent/agent.conf";
+ private static final String SOCKET_CONTAINER_DIR = "/tmp/spire-agent/public";
+ private static final String SOCKET_FILE = "api.sock";
+ private static final String SERVER_CONF_RESOURCE = "org/apache/camel/test/infra/spiffe/services/spire-server.conf";
+ private static final String AGENT_CONF_RESOURCE = "org/apache/camel/test/infra/spiffe/services/spire-agent.conf";
+
+ private final String serverImage;
+ private final String agentImage;
+
+ private Network network;
+ private GenericContainer<?> server;
+ private GenericContainer<?> agent;
+ private Path hostSocketDir;
+ private String socketPath;
+
+ public SpiffeLocalContainerInfraService() {
+ this(LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_SERVER_CONTAINER),
+ LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_AGENT_CONTAINER));
+ }
+
+ public SpiffeLocalContainerInfraService(String serverImage, String agentImage) {
+ this.serverImage = serverImage;
+ this.agentImage = agentImage;
+ }
+
+ @Override
+ public void initialize() {
+ createHostSocketDir();
+ network = Network.newNetwork();
+
+ // the image entrypoint is "spire-server run", so only -config is passed; the config is copied in (no bind mount
+ // needed for it) and port 8081 is exposed purely so the listening-port wait can tell when the API is up
+ server = new GenericContainer<>(serverImage)
+ .withNetwork(network)
+ .withNetworkAliases(SERVER_ALIAS)
+ .withCopyFileToContainer(MountableFile.forClasspathResource(SERVER_CONF_RESOURCE), SERVER_CONF)
+ .withExposedPorts(SERVER_PORT)
+ .withCommand("-config", SERVER_CONF)
+ .waitingFor(Wait.forListeningPort());
+ LOG.info("Starting the SPIRE server container");
+ server.start();
+ 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
+ agent = new GenericContainer<>(agentImage)
+ .withNetwork(network)
+ .withCopyFileToContainer(MountableFile.forClasspathResource(AGENT_CONF_RESOURCE), AGENT_CONF)
+ .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withPidMode("host"))
+ .withStartupCheckStrategy(new IsRunningStartupCheckStrategy())
+ .withCommand("-config", AGENT_CONF, "-joinToken", joinToken);
+ agent.addFileSystemBind(hostSocketDir.toString(), SOCKET_CONTAINER_DIR, BindMode.READ_WRITE, SelinuxContext.SHARED);
+ LOG.info("Starting the SPIRE agent container");
+ agent.start();
+ waitForSocket();
+
+ socketPath = "unix://" + hostSocketDir.resolve(SOCKET_FILE);
+ registerProperties();
+ LOG.info("SPIRE Workload API available at {}", socketPath);
Review Comment:
⚠️ **Still not addressed from previous review.** If `generateJoinToken()`, `createWorkloadEntry()`, or `agent.start()` throws after the server container is already started, the server, the Docker `Network`, and the `hostSocketDir` are left allocated. `TestServiceUtil.tryInitialize()` will call `initialize()` again, overwriting the field references and orphaning the previous resources.
Wrap the body after `createHostSocketDir()` in a try/catch:
```suggestion
public void initialize() {
createHostSocketDir();
try {
network = Network.newNetwork();
// the image entrypoint is "spire-server run", so only -config is passed; the config is copied in (no bind mount
// needed for it) and port 8081 is exposed purely so the listening-port wait can tell when the API is up
server = new GenericContainer<>(serverImage)
.withNetwork(network)
.withNetworkAliases(SERVER_ALIAS)
.withCopyFileToContainer(MountableFile.forClasspathResource(SERVER_CONF_RESOURCE), SERVER_CONF)
.withExposedPorts(SERVER_PORT)
.withCommand("-config", SERVER_CONF)
.waitingFor(Wait.forListeningPort());
LOG.info("Starting the SPIRE server container");
server.start();
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
agent = new GenericContainer<>(agentImage)
.withNetwork(network)
.withCopyFileToContainer(MountableFile.forClasspathResource(AGENT_CONF_RESOURCE), AGENT_CONF)
.withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withPidMode("host"))
.withStartupCheckStrategy(new IsRunningStartupCheckStrategy())
.withCommand("-config", AGENT_CONF, "-joinToken", joinToken);
agent.addFileSystemBind(hostSocketDir.toString(), SOCKET_CONTAINER_DIR, BindMode.READ_WRITE, SelinuxContext.SHARED);
LOG.info("Starting the SPIRE agent container");
agent.start();
waitForSocket();
socketPath = "unix://" + hostSocketDir.resolve(SOCKET_FILE);
registerProperties();
LOG.info("SPIRE Workload API available at {}", socketPath);
} catch (RuntimeException e) {
shutdown();
throw e;
}
}
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-24733: camel-spiffe - add integration tests against a real SPIRE agent [camel]
Posted by "oscerd (via GitHub)" <gi...@apache.org>.
oscerd commented on code in PR #27429:
URL: https://github.com/apache/camel/pull/27429#discussion_r4193550309
##########
components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeMutualTlsIT.java:
##########
@@ -0,0 +1,135 @@
+/*
+ * 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.component.spiffe.integration;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLSocket;
+
+import org.apache.camel.component.spiffe.SpiffeSSLContextParameters;
+import org.apache.camel.support.jsse.ClientAuthentication;
+import org.apache.camel.support.jsse.SSLContextServerParameters;
+import org.apache.camel.test.infra.spiffe.services.SpiffeService;
+import org.apache.camel.test.infra.spiffe.services.SpiffeServiceFactory;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Completes a real mutual-TLS handshake through {@link SpiffeSSLContextParameters} against SVIDs from a live SPIRE
+ * Workload API, asserting that an allow-listed peer connects and a non-allow-listed one is refused during the
+ * handshake.
+ */
+class SpiffeMutualTlsIT extends CamelTestSupport {
+
+ @RegisterExtension
+ static SpiffeService service = SpiffeServiceFactory.createSingletonService();
+
+ private SpiffeSSLContextParameters serverSsl(String acceptedSpiffeIds) {
+ SpiffeSSLContextParameters ssl = new SpiffeSSLContextParameters();
+ ssl.setSpiffeSocketPath(service.getWorkloadApiSocketPath());
+ ssl.setAcceptedSpiffeIds(acceptedSpiffeIds);
+ SSLContextServerParameters serverParameters = new SSLContextServerParameters();
+ serverParameters.setClientAuthentication(ClientAuthentication.REQUIRE.name());
+ ssl.setServerParameters(serverParameters);
+ return ssl;
+ }
+
+ private SpiffeSSLContextParameters clientSsl() {
+ SpiffeSSLContextParameters ssl = new SpiffeSSLContextParameters();
+ ssl.setSpiffeSocketPath(service.getWorkloadApiSocketPath());
+ ssl.setAcceptedSpiffeIds(service.getWorkloadSpiffeId());
+ return ssl;
+ }
+
+ @Test
+ void allowsAnAllowListedPeer() throws Exception {
+ SSLContext serverContext = serverSsl(service.getWorkloadSpiffeId()).createSSLContext(context);
+ SSLContext clientContext = clientSsl().createSSLContext(context);
+
+ assertThatCode(() -> handshake(serverContext, clientContext)).doesNotThrowAnyException();
+ }
+
+ @Test
+ void refusesANonAllowListedPeer() throws Exception {
+ // the server accepts only an id the client does not have; its SVID is spiffe://example.org/workload
+ SSLContext serverContext = serverSsl("spiffe://example.org/not-allowed").createSSLContext(context);
+ SSLContext clientContext = clientSsl().createSSLContext(context);
+
+ // a rejected mutual-TLS handshake surfaces as an IOException - a TLS alert (SSLHandshakeException) when the peer
+ // sends one, or a connection reset (SocketException) when it just drops the connection; both are acceptable, the
+ // point is that the handshake does not complete
+ assertThatThrownBy(() -> handshake(serverContext, clientContext))
+ .as("a non-allow-listed peer must be refused during the handshake")
+ .isInstanceOf(IOException.class);
+ }
+
+ /**
+ * Drives a mutual-TLS handshake over a loopback socket: the server requires a client certificate and both sides
+ * present their SVID. Returns normally when the handshake and a one-byte exchange complete, and throws when either
+ * side rejects the peer.
+ */
+ private void handshake(SSLContext serverContext, SSLContext clientContext) throws Exception {
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ try (SSLServerSocket serverSocket = (SSLServerSocket) serverContext.getServerSocketFactory()
+ .createServerSocket(0, 1, InetAddress.getLoopbackAddress())) {
+ serverSocket.setNeedClientAuth(true);
Review Comment:
Great catch — this was the hole that mattered. Removed in 2abb54f: the test no longer calls `setNeedClientAuth(true)` on the socket, so requiring the client certificate comes solely from `serverParameters.clientAuthentication=REQUIRE` applied by the SpiffeSSLContextParameters decorator. I re-ran it and the deny test still refuses the non-allow-listed peer, which confirms the decorator preserves client authentication — so the test now genuinely covers the CAMEL-24571 regression (if the decorator dropped client auth, this test would fail).
_Claude Code on behalf of oscerd_
##########
components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeMutualTlsIT.java:
##########
@@ -0,0 +1,135 @@
+/*
+ * 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.component.spiffe.integration;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLSocket;
+
+import org.apache.camel.component.spiffe.SpiffeSSLContextParameters;
+import org.apache.camel.support.jsse.ClientAuthentication;
+import org.apache.camel.support.jsse.SSLContextServerParameters;
+import org.apache.camel.test.infra.spiffe.services.SpiffeService;
+import org.apache.camel.test.infra.spiffe.services.SpiffeServiceFactory;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Completes a real mutual-TLS handshake through {@link SpiffeSSLContextParameters} against SVIDs from a live SPIRE
+ * Workload API, asserting that an allow-listed peer connects and a non-allow-listed one is refused during the
+ * handshake.
+ */
+class SpiffeMutualTlsIT extends CamelTestSupport {
+
+ @RegisterExtension
+ static SpiffeService service = SpiffeServiceFactory.createSingletonService();
+
+ private SpiffeSSLContextParameters serverSsl(String acceptedSpiffeIds) {
+ SpiffeSSLContextParameters ssl = new SpiffeSSLContextParameters();
+ ssl.setSpiffeSocketPath(service.getWorkloadApiSocketPath());
+ ssl.setAcceptedSpiffeIds(acceptedSpiffeIds);
+ SSLContextServerParameters serverParameters = new SSLContextServerParameters();
+ serverParameters.setClientAuthentication(ClientAuthentication.REQUIRE.name());
+ ssl.setServerParameters(serverParameters);
+ return ssl;
+ }
+
+ private SpiffeSSLContextParameters clientSsl() {
+ SpiffeSSLContextParameters ssl = new SpiffeSSLContextParameters();
+ ssl.setSpiffeSocketPath(service.getWorkloadApiSocketPath());
+ ssl.setAcceptedSpiffeIds(service.getWorkloadSpiffeId());
+ return ssl;
+ }
+
+ @Test
+ void allowsAnAllowListedPeer() throws Exception {
+ SSLContext serverContext = serverSsl(service.getWorkloadSpiffeId()).createSSLContext(context);
+ SSLContext clientContext = clientSsl().createSSLContext(context);
+
+ assertThatCode(() -> handshake(serverContext, clientContext)).doesNotThrowAnyException();
+ }
+
+ @Test
+ void refusesANonAllowListedPeer() throws Exception {
+ // the server accepts only an id the client does not have; its SVID is spiffe://example.org/workload
+ SSLContext serverContext = serverSsl("spiffe://example.org/not-allowed").createSSLContext(context);
+ SSLContext clientContext = clientSsl().createSSLContext(context);
+
+ // a rejected mutual-TLS handshake surfaces as an IOException - a TLS alert (SSLHandshakeException) when the peer
+ // sends one, or a connection reset (SocketException) when it just drops the connection; both are acceptable, the
+ // point is that the handshake does not complete
+ assertThatThrownBy(() -> handshake(serverContext, clientContext))
+ .as("a non-allow-listed peer must be refused during the handshake")
+ .isInstanceOf(IOException.class);
Review Comment:
Tightened in 2abb54f: the deny assertion now requires `isInstanceOfAny(SSLException.class, SocketException.class)` and explicitly excludes `SocketTimeoutException` and `ConnectException`. I kept `SocketException` alongside `SSLException` because CI (GitHub Actions) surfaced the refusal as a connection-reset `SocketException` rather than an `SSLHandshakeException` on the first run, so requiring SSLHandshakeException specifically would be flaky; excluding timeouts and connect failures keeps it from passing for the wrong reason.
_Claude Code on behalf of oscerd_
##########
test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeLocalContainerInfraService.java:
##########
@@ -0,0 +1,302 @@
+/*
+ * 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.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+
+import com.sun.security.auth.module.UnixSystem;
+import org.apache.camel.spi.annotations.InfraService;
+import org.apache.camel.test.infra.common.LocalPropertyResolver;
+import org.apache.camel.test.infra.common.services.ContainerService;
+import org.apache.camel.test.infra.spiffe.common.SpiffeProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.SelinuxContext;
+import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.MountableFile;
+
+/**
+ * Runs a SPIRE server and agent so a test can fetch SVIDs from a real Workload API.
+ * <p>
+ * The agent's Workload API Unix socket is bind-mounted to a short host path (the {@code AF_UNIX} {@code sun_path} limit
+ * 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}.
+ */
+@InfraService(service = SpiffeInfraService.class,
+ description = "SPIFFE/SPIRE server and agent exposing the Workload API",
+ serviceAlias = { "spiffe" })
+public class SpiffeLocalContainerInfraService implements SpiffeInfraService, ContainerService<GenericContainer<?>> {
+
+ public static final String TRUST_DOMAIN = "example.org";
+ public static final String AGENT_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN + "/agent";
+ public static final String WORKLOAD_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN + "/workload";
+
+ private static final Logger LOG = LoggerFactory.getLogger(SpiffeLocalContainerInfraService.class);
+
+ private static final String SERVER_ALIAS = "spire-server";
+ private static final int SERVER_PORT = 8081;
+ private static final String SERVER_BIN = "/opt/spire/bin/spire-server";
+ private static final String SERVER_CONF = "/opt/spire/conf/server/server.conf";
+ private static final String AGENT_CONF = "/opt/spire/conf/agent/agent.conf";
+ private static final String SOCKET_CONTAINER_DIR = "/tmp/spire-agent/public";
+ private static final String SOCKET_FILE = "api.sock";
+ private static final String SERVER_CONF_RESOURCE = "org/apache/camel/test/infra/spiffe/services/spire-server.conf";
+ private static final String AGENT_CONF_RESOURCE = "org/apache/camel/test/infra/spiffe/services/spire-agent.conf";
+
+ private final String serverImage;
+ private final String agentImage;
+
+ private Network network;
+ private GenericContainer<?> server;
+ private GenericContainer<?> agent;
+ private Path hostSocketDir;
+ private String socketPath;
+
+ public SpiffeLocalContainerInfraService() {
+ this(LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_SERVER_CONTAINER),
+ LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_AGENT_CONTAINER));
+ }
+
+ public SpiffeLocalContainerInfraService(String serverImage, String agentImage) {
+ this.serverImage = serverImage;
+ this.agentImage = agentImage;
+ }
+
+ @Override
+ public void initialize() {
+ try {
+ doInitialize();
+ } catch (RuntimeException e) {
+ // a partial start would leak the server container, the network and the temp socket directory, and a
+ // TestServiceUtil.tryInitialize() retry would overwrite the fields and orphan them; tear down whatever
+ // came up before propagating
+ cleanup();
+ throw e;
+ }
+ }
+
+ private void doInitialize() {
+ createHostSocketDir();
+ network = Network.newNetwork();
+
+ // the image entrypoint is "spire-server run", so only -config is passed; the config is copied in (no bind mount
+ // needed for it) and port 8081 is exposed purely so the listening-port wait can tell when the API is up
+ server = new GenericContainer<>(serverImage)
+ .withNetwork(network)
+ .withNetworkAliases(SERVER_ALIAS)
+ .withCopyFileToContainer(MountableFile.forClasspathResource(SERVER_CONF_RESOURCE), SERVER_CONF)
+ .withExposedPorts(SERVER_PORT)
+ .withCommand("-config", SERVER_CONF)
+ .waitingFor(Wait.forListeningPort());
+ LOG.info("Starting the SPIRE server container");
+ server.start();
+ 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
+ agent = new GenericContainer<>(agentImage)
+ .withNetwork(network)
+ .withCopyFileToContainer(MountableFile.forClasspathResource(AGENT_CONF_RESOURCE), AGENT_CONF)
+ .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withPidMode("host"))
Review Comment:
Documented in 2abb54f — a class-javadoc note that the host PID namespace, the Unix-socket bind mount and the `unix:uid` selector assume a Linux Docker host (not Docker Desktop's separate VM, nor rootless Podman's uid remapping), which is also why the component gates these ITs off non-amd64 CI. Making them portable to those runtimes is out of scope for this ticket; they target the Linux CI/development Docker.
_Claude Code on behalf of oscerd_
##########
test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeLocalContainerInfraService.java:
##########
@@ -0,0 +1,302 @@
+/*
+ * 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.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+
+import com.sun.security.auth.module.UnixSystem;
+import org.apache.camel.spi.annotations.InfraService;
+import org.apache.camel.test.infra.common.LocalPropertyResolver;
+import org.apache.camel.test.infra.common.services.ContainerService;
+import org.apache.camel.test.infra.spiffe.common.SpiffeProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.SelinuxContext;
+import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.MountableFile;
+
+/**
+ * Runs a SPIRE server and agent so a test can fetch SVIDs from a real Workload API.
+ * <p>
+ * The agent's Workload API Unix socket is bind-mounted to a short host path (the {@code AF_UNIX} {@code sun_path} limit
+ * 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}.
+ */
+@InfraService(service = SpiffeInfraService.class,
+ description = "SPIFFE/SPIRE server and agent exposing the Workload API",
+ serviceAlias = { "spiffe" })
+public class SpiffeLocalContainerInfraService implements SpiffeInfraService, ContainerService<GenericContainer<?>> {
+
+ public static final String TRUST_DOMAIN = "example.org";
+ public static final String AGENT_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN + "/agent";
+ public static final String WORKLOAD_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN + "/workload";
+
+ private static final Logger LOG = LoggerFactory.getLogger(SpiffeLocalContainerInfraService.class);
+
+ private static final String SERVER_ALIAS = "spire-server";
+ private static final int SERVER_PORT = 8081;
+ private static final String SERVER_BIN = "/opt/spire/bin/spire-server";
+ private static final String SERVER_CONF = "/opt/spire/conf/server/server.conf";
+ private static final String AGENT_CONF = "/opt/spire/conf/agent/agent.conf";
+ private static final String SOCKET_CONTAINER_DIR = "/tmp/spire-agent/public";
+ private static final String SOCKET_FILE = "api.sock";
+ private static final String SERVER_CONF_RESOURCE = "org/apache/camel/test/infra/spiffe/services/spire-server.conf";
+ private static final String AGENT_CONF_RESOURCE = "org/apache/camel/test/infra/spiffe/services/spire-agent.conf";
+
+ private final String serverImage;
+ private final String agentImage;
+
+ private Network network;
+ private GenericContainer<?> server;
+ private GenericContainer<?> agent;
+ private Path hostSocketDir;
+ private String socketPath;
+
+ public SpiffeLocalContainerInfraService() {
+ this(LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_SERVER_CONTAINER),
+ LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_AGENT_CONTAINER));
+ }
+
+ public SpiffeLocalContainerInfraService(String serverImage, String agentImage) {
+ this.serverImage = serverImage;
+ this.agentImage = agentImage;
+ }
+
+ @Override
+ public void initialize() {
+ try {
+ doInitialize();
+ } catch (RuntimeException e) {
+ // a partial start would leak the server container, the network and the temp socket directory, and a
+ // TestServiceUtil.tryInitialize() retry would overwrite the fields and orphan them; tear down whatever
+ // came up before propagating
+ cleanup();
+ throw e;
+ }
+ }
+
+ private void doInitialize() {
+ createHostSocketDir();
+ network = Network.newNetwork();
+
+ // the image entrypoint is "spire-server run", so only -config is passed; the config is copied in (no bind mount
+ // needed for it) and port 8081 is exposed purely so the listening-port wait can tell when the API is up
+ server = new GenericContainer<>(serverImage)
+ .withNetwork(network)
+ .withNetworkAliases(SERVER_ALIAS)
+ .withCopyFileToContainer(MountableFile.forClasspathResource(SERVER_CONF_RESOURCE), SERVER_CONF)
+ .withExposedPorts(SERVER_PORT)
+ .withCommand("-config", SERVER_CONF)
+ .waitingFor(Wait.forListeningPort());
+ LOG.info("Starting the SPIRE server container");
+ server.start();
+ 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
+ agent = new GenericContainer<>(agentImage)
+ .withNetwork(network)
+ .withCopyFileToContainer(MountableFile.forClasspathResource(AGENT_CONF_RESOURCE), AGENT_CONF)
+ .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withPidMode("host"))
+ .withStartupCheckStrategy(new IsRunningStartupCheckStrategy())
+ .withCommand("-config", AGENT_CONF, "-joinToken", joinToken);
+ agent.addFileSystemBind(hostSocketDir.toString(), SOCKET_CONTAINER_DIR, BindMode.READ_WRITE, SelinuxContext.SHARED);
+ LOG.info("Starting the SPIRE agent container");
+ agent.start();
+ waitForSocket();
+
+ socketPath = "unix://" + hostSocketDir.resolve(SOCKET_FILE);
+ registerProperties();
+ LOG.info("SPIRE Workload API available at {}", socketPath);
+ }
+
+ private void createHostSocketDir() {
+ try {
+ // keep the socket path short (AF_UNIX sun_path is limited to ~108 bytes); /tmp keeps it well within that
+ hostSocketDir = Files.createTempDirectory(Path.of("/tmp"), "spiffe");
+ hostSocketDir.toFile().setReadable(true, false);
+ hostSocketDir.toFile().setWritable(true, false);
Review Comment:
Added in 2abb54f — a comment at the directory creation explaining it is made world-accessible so both sides of the bind mount can use the socket: the agent container (running as root, which creates the socket) and the host test process may run under different uids, so an owner-only directory would stop one of them traversing it to create or connect to the socket.
_Claude Code on behalf of oscerd_
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-24733: camel-spiffe - add integration tests against a real SPIRE agent [camel]
Posted by "oscerd (via GitHub)" <gi...@apache.org>.
oscerd commented on code in PR #27429:
URL: https://github.com/apache/camel/pull/27429#discussion_r4193496136
##########
test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeLocalContainerInfraService.java:
##########
@@ -0,0 +1,270 @@
+/*
+ * 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.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+
+import com.sun.security.auth.module.UnixSystem;
+import org.apache.camel.spi.annotations.InfraService;
+import org.apache.camel.test.infra.common.LocalPropertyResolver;
+import org.apache.camel.test.infra.common.services.ContainerService;
+import org.apache.camel.test.infra.spiffe.common.SpiffeProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.SelinuxContext;
+import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.MountableFile;
+
+/**
+ * Runs a SPIRE server and agent so a test can fetch SVIDs from a real Workload API.
+ * <p>
+ * The agent's Workload API Unix socket is bind-mounted to a short host path (the {@code AF_UNIX} {@code sun_path} limit
+ * 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}.
+ */
+@InfraService(service = SpiffeInfraService.class,
+ description = "SPIFFE/SPIRE server and agent exposing the Workload API",
+ serviceAlias = { "spiffe" })
+public class SpiffeLocalContainerInfraService implements SpiffeInfraService, ContainerService<GenericContainer<?>> {
+
+ public static final String TRUST_DOMAIN = "example.org";
+ public static final String AGENT_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN + "/agent";
+ public static final String WORKLOAD_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN + "/workload";
+
+ private static final Logger LOG = LoggerFactory.getLogger(SpiffeLocalContainerInfraService.class);
+
+ private static final String SERVER_ALIAS = "spire-server";
+ private static final int SERVER_PORT = 8081;
+ private static final String SERVER_BIN = "/opt/spire/bin/spire-server";
+ private static final String SERVER_CONF = "/opt/spire/conf/server/server.conf";
+ private static final String AGENT_CONF = "/opt/spire/conf/agent/agent.conf";
+ private static final String SOCKET_CONTAINER_DIR = "/tmp/spire-agent/public";
+ private static final String SOCKET_FILE = "api.sock";
+ private static final String SERVER_CONF_RESOURCE = "org/apache/camel/test/infra/spiffe/services/spire-server.conf";
+ private static final String AGENT_CONF_RESOURCE = "org/apache/camel/test/infra/spiffe/services/spire-agent.conf";
+
+ private final String serverImage;
+ private final String agentImage;
+
+ private Network network;
+ private GenericContainer<?> server;
+ private GenericContainer<?> agent;
+ private Path hostSocketDir;
+ private String socketPath;
+
+ public SpiffeLocalContainerInfraService() {
+ this(LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_SERVER_CONTAINER),
+ LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_AGENT_CONTAINER));
+ }
+
+ public SpiffeLocalContainerInfraService(String serverImage, String agentImage) {
+ this.serverImage = serverImage;
+ this.agentImage = agentImage;
+ }
+
+ @Override
+ public void initialize() {
+ createHostSocketDir();
+ network = Network.newNetwork();
+
+ // the image entrypoint is "spire-server run", so only -config is passed; the config is copied in (no bind mount
+ // needed for it) and port 8081 is exposed purely so the listening-port wait can tell when the API is up
+ server = new GenericContainer<>(serverImage)
+ .withNetwork(network)
+ .withNetworkAliases(SERVER_ALIAS)
+ .withCopyFileToContainer(MountableFile.forClasspathResource(SERVER_CONF_RESOURCE), SERVER_CONF)
+ .withExposedPorts(SERVER_PORT)
+ .withCommand("-config", SERVER_CONF)
+ .waitingFor(Wait.forListeningPort());
+ LOG.info("Starting the SPIRE server container");
+ server.start();
+ 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
+ agent = new GenericContainer<>(agentImage)
+ .withNetwork(network)
+ .withCopyFileToContainer(MountableFile.forClasspathResource(AGENT_CONF_RESOURCE), AGENT_CONF)
+ .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withPidMode("host"))
+ .withStartupCheckStrategy(new IsRunningStartupCheckStrategy())
+ .withCommand("-config", AGENT_CONF, "-joinToken", joinToken);
+ agent.addFileSystemBind(hostSocketDir.toString(), SOCKET_CONTAINER_DIR, BindMode.READ_WRITE, SelinuxContext.SHARED);
+ LOG.info("Starting the SPIRE agent container");
+ agent.start();
+ waitForSocket();
+
+ socketPath = "unix://" + hostSocketDir.resolve(SOCKET_FILE);
+ registerProperties();
+ LOG.info("SPIRE Workload API available at {}", socketPath);
Review Comment:
Fixed in 489f201. `initialize()` now delegates to `doInitialize()` inside a try/catch: on any failure it calls `cleanup()`, which stops the agent and server containers, closes the `Network`, deletes the temp socket directory, and nulls every field — so a partially-started service is fully torn down and a `TestServiceUtil.tryInitialize()` retry starts clean instead of orphaning the previous server/network/socket. Each teardown step is best-effort so one failure does not leak the rest.
_Claude Code on behalf of oscerd_
##########
components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeWorkloadApiIT.java:
##########
@@ -0,0 +1,108 @@
+/*
+ * 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.component.spiffe.integration;
+
+import io.spiffe.exception.JwtSvidException;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.spiffe.SpiffeConstants;
+import org.apache.camel.test.infra.spiffe.services.SpiffeService;
+import org.apache.camel.test.infra.spiffe.services.SpiffeServiceFactory;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Exercises the camel-spiffe producer against a real SPIRE Workload API: fetching an X509-SVID and a JWT-SVID, and
+ * validating a JWT-SVID (accepting the configured audience, rejecting another).
+ */
+class SpiffeWorkloadApiIT extends CamelTestSupport {
+
+ @RegisterExtension
+ static SpiffeService service = SpiffeServiceFactory.createSingletonService();
+
+ private static String socket() {
+ return "RAW(" + service.getWorkloadApiSocketPath() + ")";
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:x509")
+ .toF("spiffe:x509?operation=fetchX509Svid&spiffeSocketPath=%s", socket());
+ from("direct:jwt")
+ .toF("spiffe:jwt?operation=fetchJwtSvid&audience=my-audience&spiffeSocketPath=%s", socket());
+ from("direct:validateCorrectAudience")
+ .toF("spiffe:valOk?operation=validateJwtSvid&audience=my-audience&spiffeSocketPath=%s", socket());
+ from("direct:validateWrongAudience")
+ .toF("spiffe:valWrong?operation=validateJwtSvid&audience=other-audience&spiffeSocketPath=%s",
+ socket());
+ }
+ };
+ }
+
+ @Test
+ void fetchesAnX509SvidForThisWorkload() {
+ Exchange out = template.request("direct:x509", e -> {
+ });
+
+ assertThat(out.getException()).isNull();
+ assertThat(out.getMessage().getHeader(SpiffeConstants.SPIFFE_ID))
+ .isEqualTo(service.getWorkloadSpiffeId());
+ // the default x509Response is the certificate chain, which must carry at least the leaf
+ assertThat(out.getMessage().getBody()).isNotNull();
Review Comment:
Fixed in 489f201. The X509-SVID test now asserts the body — the certificate chain (a `List<X509Certificate>` under the default `x509Response=chain`) — is non-empty: `assertThat(out.getMessage().getBody(List.class)).isNotEmpty()`, so it verifies at least the leaf certificate is present rather than only that the body was set. The identity itself is asserted separately via the `CamelSpiffeId` header.
_Claude Code on behalf of oscerd_
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-24733: camel-spiffe - add integration tests against a real SPIRE agent [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus commented on code in PR #27429:
URL: https://github.com/apache/camel/pull/27429#discussion_r4194245728
##########
test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeLocalContainerInfraService.java:
##########
@@ -0,0 +1,302 @@
+/*
+ * 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.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+
+import com.sun.security.auth.module.UnixSystem;
+import org.apache.camel.spi.annotations.InfraService;
+import org.apache.camel.test.infra.common.LocalPropertyResolver;
+import org.apache.camel.test.infra.common.services.ContainerService;
+import org.apache.camel.test.infra.spiffe.common.SpiffeProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.SelinuxContext;
+import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.MountableFile;
+
+/**
+ * Runs a SPIRE server and agent so a test can fetch SVIDs from a real Workload API.
+ * <p>
+ * The agent's Workload API Unix socket is bind-mounted to a short host path (the {@code AF_UNIX} {@code sun_path} limit
+ * 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}.
+ */
+@InfraService(service = SpiffeInfraService.class,
+ description = "SPIFFE/SPIRE server and agent exposing the Workload API",
+ serviceAlias = { "spiffe" })
+public class SpiffeLocalContainerInfraService implements SpiffeInfraService, ContainerService<GenericContainer<?>> {
+
+ public static final String TRUST_DOMAIN = "example.org";
+ public static final String AGENT_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN + "/agent";
+ public static final String WORKLOAD_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN + "/workload";
+
+ private static final Logger LOG = LoggerFactory.getLogger(SpiffeLocalContainerInfraService.class);
+
+ private static final String SERVER_ALIAS = "spire-server";
+ private static final int SERVER_PORT = 8081;
+ private static final String SERVER_BIN = "/opt/spire/bin/spire-server";
+ private static final String SERVER_CONF = "/opt/spire/conf/server/server.conf";
+ private static final String AGENT_CONF = "/opt/spire/conf/agent/agent.conf";
+ private static final String SOCKET_CONTAINER_DIR = "/tmp/spire-agent/public";
+ private static final String SOCKET_FILE = "api.sock";
+ private static final String SERVER_CONF_RESOURCE = "org/apache/camel/test/infra/spiffe/services/spire-server.conf";
+ private static final String AGENT_CONF_RESOURCE = "org/apache/camel/test/infra/spiffe/services/spire-agent.conf";
+
+ private final String serverImage;
+ private final String agentImage;
+
+ private Network network;
+ private GenericContainer<?> server;
+ private GenericContainer<?> agent;
+ private Path hostSocketDir;
+ private String socketPath;
+
+ public SpiffeLocalContainerInfraService() {
+ this(LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_SERVER_CONTAINER),
+ LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_AGENT_CONTAINER));
+ }
+
+ public SpiffeLocalContainerInfraService(String serverImage, String agentImage) {
+ this.serverImage = serverImage;
+ this.agentImage = agentImage;
+ }
+
+ @Override
+ public void initialize() {
+ try {
+ doInitialize();
+ } catch (RuntimeException e) {
+ // a partial start would leak the server container, the network and the temp socket directory, and a
+ // TestServiceUtil.tryInitialize() retry would overwrite the fields and orphan them; tear down whatever
+ // came up before propagating
+ cleanup();
+ throw e;
+ }
+ }
+
+ private void doInitialize() {
+ createHostSocketDir();
+ network = Network.newNetwork();
+
+ // the image entrypoint is "spire-server run", so only -config is passed; the config is copied in (no bind mount
+ // needed for it) and port 8081 is exposed purely so the listening-port wait can tell when the API is up
+ server = new GenericContainer<>(serverImage)
+ .withNetwork(network)
+ .withNetworkAliases(SERVER_ALIAS)
+ .withCopyFileToContainer(MountableFile.forClasspathResource(SERVER_CONF_RESOURCE), SERVER_CONF)
+ .withExposedPorts(SERVER_PORT)
+ .withCommand("-config", SERVER_CONF)
+ .waitingFor(Wait.forListeningPort());
+ LOG.info("Starting the SPIRE server container");
+ server.start();
+ 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
+ agent = new GenericContainer<>(agentImage)
+ .withNetwork(network)
+ .withCopyFileToContainer(MountableFile.forClasspathResource(AGENT_CONF_RESOURCE), AGENT_CONF)
+ .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withPidMode("host"))
Review Comment:
Thanks for documenting it, and for fixing the client-auth point. To be clear, I was not asking to make the ITs portable, only that they skip where they cannot work: as component ITs run by default, `mvn verify` in camel-spiffe runs them on a macOS developer machine too (the skip only covers ppc64le and s390x, so Apple Silicon and Intel Macs both run them), where they fail rather than skip. An `@EnabledOnOs(OS.LINUX)` on `SpiffeWorkloadApiIT` and `SpiffeMutualTlsIT` (or the same check in the infra service, turning it into a skip) would keep them running on the Linux CI and out of the way elsewhere.
_Claude Code on behalf of davsclaus_
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-24733: camel-spiffe - add integration tests against a real SPIRE agent [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus merged PR #27429:
URL: https://github.com/apache/camel/pull/27429
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-24733: camel-spiffe - add integration tests against a real SPIRE agent [camel]
Posted by "oscerd (via GitHub)" <gi...@apache.org>.
oscerd commented on code in PR #27429:
URL: https://github.com/apache/camel/pull/27429#discussion_r4194307435
##########
test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeLocalContainerInfraService.java:
##########
@@ -0,0 +1,302 @@
+/*
+ * 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.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+
+import com.sun.security.auth.module.UnixSystem;
+import org.apache.camel.spi.annotations.InfraService;
+import org.apache.camel.test.infra.common.LocalPropertyResolver;
+import org.apache.camel.test.infra.common.services.ContainerService;
+import org.apache.camel.test.infra.spiffe.common.SpiffeProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.SelinuxContext;
+import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.MountableFile;
+
+/**
+ * Runs a SPIRE server and agent so a test can fetch SVIDs from a real Workload API.
+ * <p>
+ * The agent's Workload API Unix socket is bind-mounted to a short host path (the {@code AF_UNIX} {@code sun_path} limit
+ * 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}.
+ */
+@InfraService(service = SpiffeInfraService.class,
+ description = "SPIFFE/SPIRE server and agent exposing the Workload API",
+ serviceAlias = { "spiffe" })
+public class SpiffeLocalContainerInfraService implements SpiffeInfraService, ContainerService<GenericContainer<?>> {
+
+ public static final String TRUST_DOMAIN = "example.org";
+ public static final String AGENT_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN + "/agent";
+ public static final String WORKLOAD_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN + "/workload";
+
+ private static final Logger LOG = LoggerFactory.getLogger(SpiffeLocalContainerInfraService.class);
+
+ private static final String SERVER_ALIAS = "spire-server";
+ private static final int SERVER_PORT = 8081;
+ private static final String SERVER_BIN = "/opt/spire/bin/spire-server";
+ private static final String SERVER_CONF = "/opt/spire/conf/server/server.conf";
+ private static final String AGENT_CONF = "/opt/spire/conf/agent/agent.conf";
+ private static final String SOCKET_CONTAINER_DIR = "/tmp/spire-agent/public";
+ private static final String SOCKET_FILE = "api.sock";
+ private static final String SERVER_CONF_RESOURCE = "org/apache/camel/test/infra/spiffe/services/spire-server.conf";
+ private static final String AGENT_CONF_RESOURCE = "org/apache/camel/test/infra/spiffe/services/spire-agent.conf";
+
+ private final String serverImage;
+ private final String agentImage;
+
+ private Network network;
+ private GenericContainer<?> server;
+ private GenericContainer<?> agent;
+ private Path hostSocketDir;
+ private String socketPath;
+
+ public SpiffeLocalContainerInfraService() {
+ this(LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_SERVER_CONTAINER),
+ LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_AGENT_CONTAINER));
+ }
+
+ public SpiffeLocalContainerInfraService(String serverImage, String agentImage) {
+ this.serverImage = serverImage;
+ this.agentImage = agentImage;
+ }
+
+ @Override
+ public void initialize() {
+ try {
+ doInitialize();
+ } catch (RuntimeException e) {
+ // a partial start would leak the server container, the network and the temp socket directory, and a
+ // TestServiceUtil.tryInitialize() retry would overwrite the fields and orphan them; tear down whatever
+ // came up before propagating
+ cleanup();
+ throw e;
+ }
+ }
+
+ private void doInitialize() {
+ createHostSocketDir();
+ network = Network.newNetwork();
+
+ // the image entrypoint is "spire-server run", so only -config is passed; the config is copied in (no bind mount
+ // needed for it) and port 8081 is exposed purely so the listening-port wait can tell when the API is up
+ server = new GenericContainer<>(serverImage)
+ .withNetwork(network)
+ .withNetworkAliases(SERVER_ALIAS)
+ .withCopyFileToContainer(MountableFile.forClasspathResource(SERVER_CONF_RESOURCE), SERVER_CONF)
+ .withExposedPorts(SERVER_PORT)
+ .withCommand("-config", SERVER_CONF)
+ .waitingFor(Wait.forListeningPort());
+ LOG.info("Starting the SPIRE server container");
+ server.start();
+ 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
+ agent = new GenericContainer<>(agentImage)
+ .withNetwork(network)
+ .withCopyFileToContainer(MountableFile.forClasspathResource(AGENT_CONF_RESOURCE), AGENT_CONF)
+ .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withPidMode("host"))
Review Comment:
Good point — enforced in 174a07a. Both ITs are now `@EnabledOnOs(OS.LINUX)`, so `mvn verify` skips them on macOS/Windows (Docker Desktop's VM) and any non-Linux host rather than running and failing; they still run on a Linux Docker host (CI and Linux dev), where the host PID namespace and the bind-mounted Workload API socket work. The `skipITs.ppc64le`/`skipITs.s390x` properties continue to cover the non-amd64 CI architectures. Verified locally on Linux: both ITs still run (6 tests), not skipped.
_Claude Code on behalf of oscerd_
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-24733: camel-spiffe - add integration tests against a real SPIRE agent [camel]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #27429:
URL: https://github.com/apache/camel/pull/27429#issuecomment-6012401182
:star2: Thank you for your contribution to the Apache Camel project! :star2:
:robot: CI automation will test this PR automatically.
:camel: Apache Camel Committers, please review the following items:
* First-time contributors **require MANUAL approval** for the GitHub Actions to run
* You can use the command `/component-test (camel-)component-name1 (camel-)component-name2..` to request a test from the test bot although they are normally detected and executed by CI.
* You can label PRs using `skip-tests` and `test-dependents` to fine-tune the checks executed by this PR.
* Build and test logs are available in the summary page. **Only** [Apache Camel committers](https://camel.apache.org/community/team/#committers) have access to the summary.
:warning: Be careful when sharing logs. Review their contents before sharing them publicly.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-24733: camel-spiffe - add integration tests against a real SPIRE agent [camel]
Posted by "gnodet-bot (via GitHub)" <gi...@apache.org>.
gnodet-bot commented on code in PR #27429:
URL: https://github.com/apache/camel/pull/27429#discussion_r4193198993
##########
test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeLocalContainerInfraService.java:
##########
@@ -0,0 +1,270 @@
+/*
+ * 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.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+
+import com.sun.security.auth.module.UnixSystem;
+import org.apache.camel.spi.annotations.InfraService;
+import org.apache.camel.test.infra.common.LocalPropertyResolver;
+import org.apache.camel.test.infra.common.services.ContainerService;
+import org.apache.camel.test.infra.spiffe.common.SpiffeProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.SelinuxContext;
+import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.MountableFile;
+
+/**
+ * Runs a SPIRE server and agent so a test can fetch SVIDs from a real Workload API.
+ * <p>
+ * The agent's Workload API Unix socket is bind-mounted to a short host path (the {@code AF_UNIX} {@code sun_path} limit
+ * 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}.
+ */
+@InfraService(service = SpiffeInfraService.class,
+ description = "SPIFFE/SPIRE server and agent exposing the Workload API",
+ serviceAlias = { "spiffe" })
+public class SpiffeLocalContainerInfraService implements SpiffeInfraService, ContainerService<GenericContainer<?>> {
+
+ public static final String TRUST_DOMAIN = "example.org";
+ public static final String AGENT_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN + "/agent";
+ public static final String WORKLOAD_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN + "/workload";
+
+ private static final Logger LOG = LoggerFactory.getLogger(SpiffeLocalContainerInfraService.class);
+
+ private static final String SERVER_ALIAS = "spire-server";
+ private static final int SERVER_PORT = 8081;
+ private static final String SERVER_BIN = "/opt/spire/bin/spire-server";
+ private static final String SERVER_CONF = "/opt/spire/conf/server/server.conf";
+ private static final String AGENT_CONF = "/opt/spire/conf/agent/agent.conf";
+ private static final String SOCKET_CONTAINER_DIR = "/tmp/spire-agent/public";
+ private static final String SOCKET_FILE = "api.sock";
+ private static final String SERVER_CONF_RESOURCE = "org/apache/camel/test/infra/spiffe/services/spire-server.conf";
+ private static final String AGENT_CONF_RESOURCE = "org/apache/camel/test/infra/spiffe/services/spire-agent.conf";
+
+ private final String serverImage;
+ private final String agentImage;
+
+ private Network network;
+ private GenericContainer<?> server;
+ private GenericContainer<?> agent;
+ private Path hostSocketDir;
+ private String socketPath;
+
+ public SpiffeLocalContainerInfraService() {
+ this(LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_SERVER_CONTAINER),
+ LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_AGENT_CONTAINER));
+ }
+
+ public SpiffeLocalContainerInfraService(String serverImage, String agentImage) {
+ this.serverImage = serverImage;
+ this.agentImage = agentImage;
+ }
+
+ @Override
+ public void initialize() {
+ createHostSocketDir();
+ network = Network.newNetwork();
+
+ // the image entrypoint is "spire-server run", so only -config is passed; the config is copied in (no bind mount
+ // needed for it) and port 8081 is exposed purely so the listening-port wait can tell when the API is up
+ server = new GenericContainer<>(serverImage)
+ .withNetwork(network)
+ .withNetworkAliases(SERVER_ALIAS)
+ .withCopyFileToContainer(MountableFile.forClasspathResource(SERVER_CONF_RESOURCE), SERVER_CONF)
+ .withExposedPorts(SERVER_PORT)
+ .withCommand("-config", SERVER_CONF)
+ .waitingFor(Wait.forListeningPort());
+ LOG.info("Starting the SPIRE server container");
+ server.start();
+ 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
+ agent = new GenericContainer<>(agentImage)
+ .withNetwork(network)
+ .withCopyFileToContainer(MountableFile.forClasspathResource(AGENT_CONF_RESOURCE), AGENT_CONF)
+ .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withPidMode("host"))
+ .withStartupCheckStrategy(new IsRunningStartupCheckStrategy())
+ .withCommand("-config", AGENT_CONF, "-joinToken", joinToken);
+ agent.addFileSystemBind(hostSocketDir.toString(), SOCKET_CONTAINER_DIR, BindMode.READ_WRITE, SelinuxContext.SHARED);
+ LOG.info("Starting the SPIRE agent container");
+ agent.start();
+ waitForSocket();
+
+ socketPath = "unix://" + hostSocketDir.resolve(SOCKET_FILE);
+ registerProperties();
+ LOG.info("SPIRE Workload API available at {}", socketPath);
Review Comment:
⚠️ **Resource leak on partial initialization failure.** If `generateJoinToken()`, `createWorkloadEntry()`, or `agent.start()` throws, the already-started `server` container, the `Network`, and the temp `hostSocketDir` are left running/allocated. The `TestServiceUtil.tryInitialize()` retry logic will call `initialize()` again, which overwrites the `server`/`network`/`hostSocketDir` fields with new instances — orphaning the previous ones.
Wrap the body of `initialize()` after `createHostSocketDir()` in a try/catch that calls the cleanup method on failure:
```suggestion
public void initialize() {
createHostSocketDir();
try {
network = Network.newNetwork();
// the image entrypoint is "spire-server run", so only -config is passed; the config is copied in (no bind mount
// needed for it) and port 8081 is exposed purely so the listening-port wait can tell when the API is up
server = new GenericContainer<>(serverImage)
.withNetwork(network)
.withNetworkAliases(SERVER_ALIAS)
.withCopyFileToContainer(MountableFile.forClasspathResource(SERVER_CONF_RESOURCE), SERVER_CONF)
.withExposedPorts(SERVER_PORT)
.withCommand("-config", SERVER_CONF)
.waitingFor(Wait.forListeningPort());
LOG.info("Starting the SPIRE server container");
server.start();
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
agent = new GenericContainer<>(agentImage)
.withNetwork(network)
.withCopyFileToContainer(MountableFile.forClasspathResource(AGENT_CONF_RESOURCE), AGENT_CONF)
.withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withPidMode("host"))
.withStartupCheckStrategy(new IsRunningStartupCheckStrategy())
.withCommand("-config", AGENT_CONF, "-joinToken", joinToken);
agent.addFileSystemBind(hostSocketDir.toString(), SOCKET_CONTAINER_DIR, BindMode.READ_WRITE, SelinuxContext.SHARED);
LOG.info("Starting the SPIRE agent container");
agent.start();
waitForSocket();
socketPath = "unix://" + hostSocketDir.resolve(SOCKET_FILE);
registerProperties();
LOG.info("SPIRE Workload API available at {}", socketPath);
} catch (RuntimeException e) {
doShutdown();
throw e;
}
}
```
(where `doShutdown()` is your existing cleanup logic — or just inline the try/catch around the current body and call `this.shutdown()` in the catch).
##########
components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeWorkloadApiIT.java:
##########
@@ -0,0 +1,108 @@
+/*
+ * 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.component.spiffe.integration;
+
+import io.spiffe.exception.JwtSvidException;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.spiffe.SpiffeConstants;
+import org.apache.camel.test.infra.spiffe.services.SpiffeService;
+import org.apache.camel.test.infra.spiffe.services.SpiffeServiceFactory;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Exercises the camel-spiffe producer against a real SPIRE Workload API: fetching an X509-SVID and a JWT-SVID, and
+ * validating a JWT-SVID (accepting the configured audience, rejecting another).
+ */
+class SpiffeWorkloadApiIT extends CamelTestSupport {
+
+ @RegisterExtension
+ static SpiffeService service = SpiffeServiceFactory.createSingletonService();
+
+ private static String socket() {
+ return "RAW(" + service.getWorkloadApiSocketPath() + ")";
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:x509")
+ .toF("spiffe:x509?operation=fetchX509Svid&spiffeSocketPath=%s", socket());
+ from("direct:jwt")
+ .toF("spiffe:jwt?operation=fetchJwtSvid&audience=my-audience&spiffeSocketPath=%s", socket());
+ from("direct:validateCorrectAudience")
+ .toF("spiffe:valOk?operation=validateJwtSvid&audience=my-audience&spiffeSocketPath=%s", socket());
+ from("direct:validateWrongAudience")
+ .toF("spiffe:valWrong?operation=validateJwtSvid&audience=other-audience&spiffeSocketPath=%s",
+ socket());
+ }
+ };
+ }
+
+ @Test
+ void fetchesAnX509SvidForThisWorkload() {
+ Exchange out = template.request("direct:x509", e -> {
+ });
+
+ assertThat(out.getException()).isNull();
+ assertThat(out.getMessage().getHeader(SpiffeConstants.SPIFFE_ID))
+ .isEqualTo(service.getWorkloadSpiffeId());
+ // the default x509Response is the certificate chain, which must carry at least the leaf
+ assertThat(out.getMessage().getBody()).isNotNull();
Review Comment:
💡 **Weak assertion.** `isNotNull()` only proves the body was set — it doesn’t verify the response is actually a certificate chain. A slightly stronger check:
```suggestion
assertThat(out.getMessage().getBody(String.class)).as("X509-SVID response").isNotEmpty();
```
Or, if the body is an `X509Certificate[]`, assert `hasSize` ≥ 1.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-24733: camel-spiffe - add integration tests against a real SPIRE agent [camel]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #27429:
URL: https://github.com/apache/camel/pull/27429#issuecomment-6012614885
<!-- ci-tested-modules -->
:test_tube: **CI tested the following changed modules:**
- `catalog/camel-catalog`
- `components/camel-spiffe`
- `test-infra`
- `test-infra/camel-test-infra-all`
- `test-infra/camel-test-infra-spiffe`
---
<details><summary>:microscope: Scalpel shadow comparison — Scalpel: 11 of 699 tested, 3 compile-only — current: 11 all tested</summary>
[Maveniverse Scalpel](https://github.com/maveniverse/scalpel) detected **11 affected modules** (current approach: 11).
**Skip-tests mode would test 11 modules** (4 direct + 0 downstream), **skip tests for 3** (generated code, meta-modules)
<details><summary>Modules Scalpel would test (11)</summary>
- `camel-jbang-mcp` ← depends on affected reactor module org.apache.camel:camel-yaml-dsl-validator
- `camel-jbang-plugin-mcp` ← depends on affected reactor module org.apache.camel:camel-jbang-core
- `camel-jbang-plugin-route-parser` ← depends on affected reactor module org.apache.camel:camel-route-parser
- `camel-jbang-plugin-tui` ← depends on affected reactor module org.apache.camel:camel-yaml-dsl-validator
- `camel-jbang-plugin-validate` ← depends on affected reactor module org.apache.camel:camel-yaml-dsl-validator
- `camel-launcher-container` ← depends on affected reactor module org.apache.camel:camel-launcher
- `camel-spiffe` ← components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeMutualTlsIT.java, components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeWorkloadApiIT.java, own pom components/camel-spiffe/pom.xml changed
- `camel-test-infra-all` ← test-infra/camel-test-infra-all/src/generated/resources/META-INF/metadata.json, own pom test-infra/camel-test-infra-all/pom.xml changed
- `camel-test-infra-spiffe` ← test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/common/SpiffeProperties.java, test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeInfraService.java, test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeLocalContainerInfraService.java, test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeRemoteInfraService.java, test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeService.java, test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeServiceFactory.java, test-infra/camel-test-infra-spiffe/src/main/resources/org/apache/camel/test/infra/spiffe/services/container.properties, test-infra/camel-test-infra-spiffe/src/main/resources/org/apache/camel/test/infra/spiffe/services/spire-agent.conf, test-i
nfra/camel-test-infra-spiffe/src/main/resources/org/apache/camel/test/infra/spiffe/services/spire-server.conf, own pom test-infra/camel-test-infra-spiffe/pom.xml changed
- `camel-yaml-dsl-validator` ← depends on affected reactor module org.apache.camel:camel-catalog
- `camel-yaml-dsl-validator-maven-plugin` ← depends on affected reactor module org.apache.camel:camel-yaml-dsl-validator
</details>
<details><summary>Modules with tests skipped (3)</summary>
- `camel-itest`
- `camel-yaml-dsl`
- `camel-yaml-dsl-deserializers`
</details>
> :information_source: Shadow mode — Scalpel observes but does not affect test execution. [Learn more](https://github.com/maveniverse/scalpel)
</details>
<details><summary>All tested modules (38 modules, 1m 1s total)</summary>
**Total reactor time:** 1m 1s
| Module | Duration | Status |
| --- | --- | --- |
| Camel :: SPIFFE | 43.9s | FAILURE |
| Camel :: Test Infra :: All test services | 13.4s | SUCCESS |
| Camel :: Test Infra :: SPIFFE | 3.4s | SUCCESS |
| Camel :: All Components Sync point | n/a | |
| Camel :: Assembly | n/a | |
| Camel :: Catalog :: Camel Catalog | n/a | |
| Camel :: Catalog :: Camel Report Maven Plugin | n/a | |
| Camel :: Catalog :: Camel Route Parser | n/a | |
| Camel :: Catalog :: Console | n/a | |
| Camel :: Catalog :: Dummy Component | n/a | |
| Camel :: Catalog :: Maven | n/a | |
| Camel :: Catalog :: Suggest (deprecated) | n/a | |
| Camel :: Component DSL | n/a | |
| Camel :: Coverage | n/a | |
| Camel :: Docs | n/a | |
| Camel :: Endpoint DSL | n/a | |
| Camel :: Endpoint DSL :: Support | n/a | |
| Camel :: Integration Tests | n/a | |
| Camel :: JBang :: Core | n/a | |
| Camel :: JBang :: Integration tests | n/a | |
| Camel :: JBang :: MCP | n/a | |
| Camel :: JBang :: Main | n/a | |
| Camel :: JBang :: Plugin :: Edit | n/a | |
| Camel :: JBang :: Plugin :: Generate | n/a | |
| Camel :: JBang :: Plugin :: Kubernetes | n/a | |
| Camel :: JBang :: Plugin :: MCP | n/a | |
| Camel :: JBang :: Plugin :: Route Parser | n/a | |
| Camel :: JBang :: Plugin :: TUI | n/a | |
| Camel :: JBang :: Plugin :: Testing | n/a | |
| Camel :: JBang :: Plugin :: Validate | n/a | |
| Camel :: Kamelet Main | n/a | |
| Camel :: Launcher | n/a | |
| Camel :: Launcher :: Container | n/a | |
| Camel :: YAML DSL | n/a | |
| Camel :: YAML DSL :: Deserializers | n/a | |
| Camel :: YAML DSL :: Maven Plugins | n/a | |
| Camel :: YAML DSL :: Validator | n/a | |
| Camel :: YAML DSL :: Validator Maven Plugin | n/a | |
**Top 20 slowest modules:**
- `Camel :: SPIFFE` (43.9s)
- `Camel :: Test Infra :: All test services` (13.4s)
- `Camel :: Test Infra :: SPIFFE` (3.4s)
</details>
---
:gear: [View full build and test results](https://github.com/apache/camel/actions/runs/37436148282)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-24733: camel-spiffe - add integration tests against a real SPIRE agent [camel]
Posted by "oscerd (via GitHub)" <gi...@apache.org>.
oscerd commented on code in PR #27429:
URL: https://github.com/apache/camel/pull/27429#discussion_r4193725936
##########
test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeLocalContainerInfraService.java:
##########
@@ -0,0 +1,270 @@
+/*
+ * 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.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+
+import com.sun.security.auth.module.UnixSystem;
+import org.apache.camel.spi.annotations.InfraService;
+import org.apache.camel.test.infra.common.LocalPropertyResolver;
+import org.apache.camel.test.infra.common.services.ContainerService;
+import org.apache.camel.test.infra.spiffe.common.SpiffeProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.SelinuxContext;
+import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.MountableFile;
+
+/**
+ * Runs a SPIRE server and agent so a test can fetch SVIDs from a real Workload API.
+ * <p>
+ * The agent's Workload API Unix socket is bind-mounted to a short host path (the {@code AF_UNIX} {@code sun_path} limit
+ * 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}.
+ */
+@InfraService(service = SpiffeInfraService.class,
+ description = "SPIFFE/SPIRE server and agent exposing the Workload API",
+ serviceAlias = { "spiffe" })
+public class SpiffeLocalContainerInfraService implements SpiffeInfraService, ContainerService<GenericContainer<?>> {
+
+ public static final String TRUST_DOMAIN = "example.org";
+ public static final String AGENT_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN + "/agent";
+ public static final String WORKLOAD_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN + "/workload";
+
+ private static final Logger LOG = LoggerFactory.getLogger(SpiffeLocalContainerInfraService.class);
+
+ private static final String SERVER_ALIAS = "spire-server";
+ private static final int SERVER_PORT = 8081;
+ private static final String SERVER_BIN = "/opt/spire/bin/spire-server";
+ private static final String SERVER_CONF = "/opt/spire/conf/server/server.conf";
+ private static final String AGENT_CONF = "/opt/spire/conf/agent/agent.conf";
+ private static final String SOCKET_CONTAINER_DIR = "/tmp/spire-agent/public";
+ private static final String SOCKET_FILE = "api.sock";
+ private static final String SERVER_CONF_RESOURCE = "org/apache/camel/test/infra/spiffe/services/spire-server.conf";
+ private static final String AGENT_CONF_RESOURCE = "org/apache/camel/test/infra/spiffe/services/spire-agent.conf";
+
+ private final String serverImage;
+ private final String agentImage;
+
+ private Network network;
+ private GenericContainer<?> server;
+ private GenericContainer<?> agent;
+ private Path hostSocketDir;
+ private String socketPath;
+
+ public SpiffeLocalContainerInfraService() {
+ this(LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_SERVER_CONTAINER),
+ LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_AGENT_CONTAINER));
+ }
+
+ public SpiffeLocalContainerInfraService(String serverImage, String agentImage) {
+ this.serverImage = serverImage;
+ this.agentImage = agentImage;
+ }
+
+ @Override
+ public void initialize() {
+ createHostSocketDir();
+ network = Network.newNetwork();
+
+ // the image entrypoint is "spire-server run", so only -config is passed; the config is copied in (no bind mount
+ // needed for it) and port 8081 is exposed purely so the listening-port wait can tell when the API is up
+ server = new GenericContainer<>(serverImage)
+ .withNetwork(network)
+ .withNetworkAliases(SERVER_ALIAS)
+ .withCopyFileToContainer(MountableFile.forClasspathResource(SERVER_CONF_RESOURCE), SERVER_CONF)
+ .withExposedPorts(SERVER_PORT)
+ .withCommand("-config", SERVER_CONF)
+ .waitingFor(Wait.forListeningPort());
+ LOG.info("Starting the SPIRE server container");
+ server.start();
+ 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
+ agent = new GenericContainer<>(agentImage)
+ .withNetwork(network)
+ .withCopyFileToContainer(MountableFile.forClasspathResource(AGENT_CONF_RESOURCE), AGENT_CONF)
+ .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withPidMode("host"))
+ .withStartupCheckStrategy(new IsRunningStartupCheckStrategy())
+ .withCommand("-config", AGENT_CONF, "-joinToken", joinToken);
+ agent.addFileSystemBind(hostSocketDir.toString(), SOCKET_CONTAINER_DIR, BindMode.READ_WRITE, SelinuxContext.SHARED);
+ LOG.info("Starting the SPIRE agent container");
+ agent.start();
+ waitForSocket();
+
+ socketPath = "unix://" + hostSocketDir.resolve(SOCKET_FILE);
+ registerProperties();
+ LOG.info("SPIRE Workload API available at {}", socketPath);
Review Comment:
Fixed in 489f201 (and refined in 2abb54f): `initialize()` now wraps `doInitialize()` and, on any failure, `cleanup()` stops the agent and server containers, closes the `Network`, deletes the temp socket directory, and nulls the fields — so a `tryInitialize()` retry cannot orphan a partial start. (Same as the follow-up thread on this line below.)
_Claude Code on behalf of oscerd_
##########
components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeWorkloadApiIT.java:
##########
@@ -0,0 +1,108 @@
+/*
+ * 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.component.spiffe.integration;
+
+import io.spiffe.exception.JwtSvidException;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.spiffe.SpiffeConstants;
+import org.apache.camel.test.infra.spiffe.services.SpiffeService;
+import org.apache.camel.test.infra.spiffe.services.SpiffeServiceFactory;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Exercises the camel-spiffe producer against a real SPIRE Workload API: fetching an X509-SVID and a JWT-SVID, and
+ * validating a JWT-SVID (accepting the configured audience, rejecting another).
+ */
+class SpiffeWorkloadApiIT extends CamelTestSupport {
+
+ @RegisterExtension
+ static SpiffeService service = SpiffeServiceFactory.createSingletonService();
+
+ private static String socket() {
+ return "RAW(" + service.getWorkloadApiSocketPath() + ")";
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:x509")
+ .toF("spiffe:x509?operation=fetchX509Svid&spiffeSocketPath=%s", socket());
+ from("direct:jwt")
+ .toF("spiffe:jwt?operation=fetchJwtSvid&audience=my-audience&spiffeSocketPath=%s", socket());
+ from("direct:validateCorrectAudience")
+ .toF("spiffe:valOk?operation=validateJwtSvid&audience=my-audience&spiffeSocketPath=%s", socket());
+ from("direct:validateWrongAudience")
+ .toF("spiffe:valWrong?operation=validateJwtSvid&audience=other-audience&spiffeSocketPath=%s",
+ socket());
+ }
+ };
+ }
+
+ @Test
+ void fetchesAnX509SvidForThisWorkload() {
+ Exchange out = template.request("direct:x509", e -> {
+ });
+
+ assertThat(out.getException()).isNull();
+ assertThat(out.getMessage().getHeader(SpiffeConstants.SPIFFE_ID))
+ .isEqualTo(service.getWorkloadSpiffeId());
+ // the default x509Response is the certificate chain, which must carry at least the leaf
+ assertThat(out.getMessage().getBody()).isNotNull();
Review Comment:
Fixed in 489f201: the X509-SVID test now asserts the returned certificate chain body is non-empty — `assertThat(out.getMessage().getBody(List.class)).isNotEmpty()` — rather than merely non-null. (Same as the follow-up thread on this line below.)
_Claude Code on behalf of oscerd_
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org