fix(traefik): make TLS cert resolver configurable, omit when unset
Previously `TraefikLabelBuilder` hardcoded `tls.certresolver=default` on every router. That assumes a resolver literally named `default` exists in the Traefik static config — true for ACME-backed installs, false for dev/local installs that use a file-based TLS store. Traefik logs "Router uses a nonexistent certificate resolver" for the bogus resolver on every managed app, and any future attempt to define a differently- named real resolver would silently skip these routers. Server-wide setting via `CAMELEER_SERVER_RUNTIME_CERTRESOLVER` (empty by default) flows through `ConfigMerger.GlobalRuntimeDefaults.certResolver` into `ResolvedContainerConfig.certResolver`. When blank the `tls.certresolver` label is omitted entirely; `tls=true` is still emitted so Traefik serves the default TLS-store cert. When set, the label is emitted with the configured resolver name. Not per-app/per-env configurable: there is one Traefik per server instance and one resolver config; app-level override would only let users break their own routers. TDD: TraefikLabelBuilderTest gains 3 cases (resolver set, null, blank). Full unit suite 211/0/0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,9 @@ public class DeploymentExecutor {
|
||||
@Value("${cameleer.server.runtime.serverurl:}")
|
||||
private String globalServerUrl;
|
||||
|
||||
@Value("${cameleer.server.runtime.certresolver:}")
|
||||
private String globalCertResolver;
|
||||
|
||||
@Value("${cameleer.server.runtime.jardockervolume:}")
|
||||
private String jarDockerVolume;
|
||||
|
||||
@@ -131,7 +134,8 @@ public class DeploymentExecutor {
|
||||
globalCpuShares,
|
||||
globalRoutingMode,
|
||||
globalRoutingDomain,
|
||||
globalServerUrl.isBlank() ? "http://cameleer-server:8081" : globalServerUrl
|
||||
globalServerUrl.isBlank() ? "http://cameleer-server:8081" : globalServerUrl,
|
||||
globalCertResolver.isBlank() ? null : globalCertResolver
|
||||
);
|
||||
ResolvedContainerConfig config = ConfigMerger.resolve(
|
||||
globalDefaults, env.defaultContainerConfig(), app.containerConfig());
|
||||
@@ -606,6 +610,9 @@ public class DeploymentExecutor {
|
||||
map.put("customArgs", config.customArgs());
|
||||
map.put("extraNetworks", config.extraNetworks());
|
||||
map.put("externalRouting", config.externalRouting());
|
||||
if (config.certResolver() != null) {
|
||||
map.put("certResolver", config.certResolver());
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,10 @@ public final class TraefikLabelBuilder {
|
||||
|
||||
if (config.sslOffloading()) {
|
||||
labels.put("traefik.http.routers." + svc + ".tls", "true");
|
||||
labels.put("traefik.http.routers." + svc + ".tls.certresolver", "default");
|
||||
if (config.certResolver() != null && !config.certResolver().isBlank()) {
|
||||
labels.put("traefik.http.routers." + svc + ".tls.certresolver",
|
||||
config.certResolver());
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
|
||||
@@ -55,6 +55,7 @@ cameleer:
|
||||
routingmode: ${CAMELEER_SERVER_RUNTIME_ROUTINGMODE:path}
|
||||
routingdomain: ${CAMELEER_SERVER_RUNTIME_ROUTINGDOMAIN:localhost}
|
||||
serverurl: ${CAMELEER_SERVER_RUNTIME_SERVERURL:}
|
||||
certresolver: ${CAMELEER_SERVER_RUNTIME_CERTRESOLVER:}
|
||||
jardockervolume: ${CAMELEER_SERVER_RUNTIME_JARDOCKERVOLUME:}
|
||||
indexer:
|
||||
debouncems: ${CAMELEER_SERVER_INDEXER_DEBOUNCEMS:2000}
|
||||
|
||||
@@ -10,7 +10,7 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class TraefikLabelBuilderTest {
|
||||
|
||||
private static ResolvedContainerConfig config(boolean externalRouting) {
|
||||
private static ResolvedContainerConfig config(boolean externalRouting, String certResolver) {
|
||||
return new ResolvedContainerConfig(
|
||||
512, null, 500, null,
|
||||
8080, List.of(), Map.of(),
|
||||
@@ -19,14 +19,15 @@ class TraefikLabelBuilderTest {
|
||||
1, "blue-green",
|
||||
true, true,
|
||||
"spring-boot", "", List.of(),
|
||||
externalRouting
|
||||
externalRouting,
|
||||
certResolver
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void build_emitsTraefikLabelsWhenExternalRoutingEnabled() {
|
||||
Map<String, String> labels = TraefikLabelBuilder.build(
|
||||
"myapp", "dev", "acme", config(true), 0, "abcdef01");
|
||||
"myapp", "dev", "acme", config(true, null), 0, "abcdef01");
|
||||
|
||||
assertEquals("true", labels.get("traefik.enable"));
|
||||
assertEquals("8080", labels.get("traefik.http.services.dev-myapp.loadbalancer.server.port"));
|
||||
@@ -36,7 +37,7 @@ class TraefikLabelBuilderTest {
|
||||
@Test
|
||||
void build_omitsAllTraefikLabelsWhenExternalRoutingDisabled() {
|
||||
Map<String, String> labels = TraefikLabelBuilder.build(
|
||||
"myapp", "dev", "acme", config(false), 0, "abcdef01");
|
||||
"myapp", "dev", "acme", config(false, null), 0, "abcdef01");
|
||||
|
||||
long traefikLabelCount = labels.keySet().stream()
|
||||
.filter(k -> k.startsWith("traefik."))
|
||||
@@ -47,7 +48,7 @@ class TraefikLabelBuilderTest {
|
||||
@Test
|
||||
void build_preservesIdentityLabelsWhenExternalRoutingDisabled() {
|
||||
Map<String, String> labels = TraefikLabelBuilder.build(
|
||||
"myapp", "dev", "acme", config(false), 2, "abcdef01");
|
||||
"myapp", "dev", "acme", config(false, null), 2, "abcdef01");
|
||||
|
||||
assertEquals("cameleer-server", labels.get("managed-by"));
|
||||
assertEquals("acme", labels.get("cameleer.tenant"));
|
||||
@@ -57,4 +58,33 @@ class TraefikLabelBuilderTest {
|
||||
assertEquals("abcdef01", labels.get("cameleer.generation"));
|
||||
assertEquals("dev-myapp-2-abcdef01", labels.get("cameleer.instance-id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void build_emitsCertResolverLabelWhenConfigured() {
|
||||
Map<String, String> labels = TraefikLabelBuilder.build(
|
||||
"myapp", "dev", "acme", config(true, "letsencrypt"), 0, "abcdef01");
|
||||
|
||||
assertEquals("true", labels.get("traefik.http.routers.dev-myapp.tls"));
|
||||
assertEquals("letsencrypt", labels.get("traefik.http.routers.dev-myapp.tls.certresolver"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void build_omitsCertResolverLabelWhenNull() {
|
||||
Map<String, String> labels = TraefikLabelBuilder.build(
|
||||
"myapp", "dev", "acme", config(true, null), 0, "abcdef01");
|
||||
|
||||
assertEquals("true", labels.get("traefik.http.routers.dev-myapp.tls"),
|
||||
"sslOffloading=true should still mark the router TLS-enabled");
|
||||
assertNull(labels.get("traefik.http.routers.dev-myapp.tls.certresolver"),
|
||||
"cert resolver label must be omitted when none is configured");
|
||||
}
|
||||
|
||||
@Test
|
||||
void build_omitsCertResolverLabelWhenBlank() {
|
||||
Map<String, String> labels = TraefikLabelBuilder.build(
|
||||
"myapp", "dev", "acme", config(true, " "), 0, "abcdef01");
|
||||
|
||||
assertNull(labels.get("traefik.http.routers.dev-myapp.tls.certresolver"),
|
||||
"whitespace-only cert resolver must be treated as unset");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user