Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1f57e19
feat(consul): add kubernetes auth method as a second way to get an AC…
TaurMorchant Aug 26, 2026
33850c7
feat(consul): read the login mode and its inputs in all four entry po…
TaurMorchant Aug 26, 2026
b1cd52f
chore: todos @cf_ignore
TaurMorchant Aug 27, 2026
f8f5dd1
refactor(consul): apply review notes on the kubernetes login @cf_ignore
TaurMorchant Aug 27, 2026
7247024
refactor(consul): name the login types after what they provide
TaurMorchant Aug 27, 2026
19fea9b
chore: refactoring + todos @cf_ignore
TaurMorchant Aug 27, 2026
3fc1abf
docs(consul): document the login types
TaurMorchant Aug 27, 2026
cd5a844
feat(consul): retry the kubernetes way while on the m2m fallback
TaurMorchant Aug 27, 2026
fce487a
fix(consul): log which auth method issued the ACL token on the spring…
TaurMorchant Aug 28, 2026
787d9a9
chore: minor refactoring
TaurMorchant Aug 28, 2026
99c95a4
fix(consul): keep the login way across phases and relogin before expiry
TaurMorchant Aug 28, 2026
30fcc93
chore: minor TODOs @cf_ignore
TaurMorchant Aug 31, 2026
5bad07e
fix(consul): keep the relogin schedule alive when the task throws an …
TaurMorchant Aug 31, 2026
3bb59be
feat(consul): default to the applications-k8s-m2m auth method
TaurMorchant Aug 31, 2026
26586cd
fix(consul): declare failsafe instead of taking it transitively
TaurMorchant Aug 31, 2026
650c745
fix(consul): end the ConfigData phase the same way whatever the login…
TaurMorchant Aug 31, 2026
9a0da1f
refactor(consul): remove the login code that existed twice
TaurMorchant Aug 31, 2026
bf807dd
docs(consul): correct the login documentation
TaurMorchant Aug 31, 2026
b04499b
chore: minor refactoring
TaurMorchant Sep 1, 2026
5c904c3
docs(consul): keep the kubernetes way independent of the auth method …
TaurMorchant Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions core-quarkus-extensions/config-sources/consul-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,50 @@ If no M2M auth needed(for localdev, tests, etc.) it can be disabled by setting p
quarkus.consul-source-config.m2m.enabled=false
```

#### Login properties

The Consul ACL token is exchanged through `POST /v1/acl/login`. Two ways to obtain it are supported: `kubernetes` sends
the projected service account token of the pod, `m2m` sends an M2M token. The four properties below are read at runtime
when the `TokenStorage` bean is built, so the way can be switched without rebuilding the application.

In `kubernetes-with-m2m-fallback` mode the `kubernetes` way is tried first. If it fails, the pod falls back to the
`m2m` way and logs the reason, the Consul response code, and a truncated response body in a single `INFO` record. That
record marks the decision, so it appears once rather than on every retry; each login attempt logs an `INFO` record of
its own, naming the auth method it went to. In `kubernetes` mode there is no probe and no fallback. In `m2m` mode the
auth method name and the audience are not used at all.

`cloud.microservice.namespace` stays required in every mode, including `kubernetes`, where the namespace never reaches
Consul: the producer reads it whatever the mode says.

The fallback is temporary. Once `fallback-recheck-interval` has passed, the next scheduled relogin tries the
`kubernetes` way again, and the first success switches the pod over for good. Going back to `m2m` never happens. The
recheck rides on the scheduled relogin, so it needs `MaxTokenTTL` on the auth method: without it the token never
expires, nothing is scheduled, and nothing is rechecked.

`fallback-recheck-interval` therefore sets the lower bound on how often the pod retries, not the actual period. The
relogin runs at 80% of `MaxTokenTTL`, and the recheck waits for the first relogin past the interval, so with a
`MaxTokenTTL` of 24 hours a pod retries about every 19 hours whatever the interval says. Plan the migration of a fleet
against `MaxTokenTTL`, and lower it on the auth method if the pods have to move over sooner.

An unknown value of `quarkus.consul-source-config.login.mode` fails the startup, and so does a login failure the
retries do not fix: the `TokenStorage` bean cannot be produced without a token. With
`quarkus.consul-source-config.m2m.enabled=false` none of the four properties are read.

The default is the auth method the platform registers. Set `quarkus.consul-source-config.login.auth-method` only if
your Consul names it differently.

The `kubernetes` way reads the token from `/var/run/secrets/tokens/<audience>/token`. To run it outside a cluster, point
the token directory elsewhere with `com.netcracker.cloud.security.kubernetes.tokens.dir`.

#### Configuration properties
| Property name | Description | Default value |
|----------------------------------------------|---------------------------------------------------------------|-----------------------------------------------------------|
| quarkus.consul-source-config.enabled | Enable configuration approach with Consul (bool) | true |
| quarkus.consul-source-config.agent.url | Consul agent URL | |
| quarkus.consul-source-config.properties-root | List of properties roots | config/$namespace/application, config/$namespace/$appName |
| quarkus.consul-source-config.wait-time | Maximum Value for Consul blocking queries wait time (seconds) | 570 |
| quarkus.consul-source-config.m2m.enabled | Enable the Consul ACL token exchange (bool, build time) | true |
| quarkus.consul-source-config.login.mode | Way to obtain the ACL token: kubernetes-with-m2m-fallback, kubernetes or m2m | kubernetes-with-m2m-fallback |
| quarkus.consul-source-config.login.auth-method | Consul auth method name, used by the kubernetes way | applications-k8s-m2m |
| quarkus.consul-source-config.login.audience | Projected token audience, used by the kubernetes way | netcracker |
| quarkus.consul-source-config.login.fallback-recheck-interval | How often the fallback retries the kubernetes way | PT5H |
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.netcracker.cloud.quarkus.consul.client;

import com.netcracker.cloud.consul.provider.common.ConsulLoginMode;
import com.netcracker.cloud.consul.provider.common.OkHttpTokenStorageFactory;
import com.netcracker.cloud.consul.provider.common.TokenStorage;
import com.netcracker.cloud.consul.provider.common.TokenStorageFactory;
Expand All @@ -17,11 +18,18 @@

import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.util.Optional;

@Singleton
public class ConsulClientConfiguration {

public static final String PROP_LOGIN_MODE = "quarkus.consul-source-config.login.mode";
public static final String PROP_LOGIN_AUTH_METHOD = "quarkus.consul-source-config.login.auth-method";
public static final String PROP_LOGIN_AUDIENCE = "quarkus.consul-source-config.login.audience";
public static final String PROP_LOGIN_FALLBACK_RECHECK_INTERVAL =
"quarkus.consul-source-config.login.fallback-recheck-interval";

private static final Logger log = LoggerFactory.getLogger(ConsulClientConfiguration.class);

@Produces
Expand Down Expand Up @@ -68,11 +76,20 @@ public TokenStorageFactory tokenStorageFactory() {
@UnlessBuildProperty(name = "quarkus.consul-source-config.m2m.enabled", stringValue = "false", enableIfMissing = true)
public TokenStorage tokenStorage(TokenStorageFactory tokenStorageFactory,
@ConfigProperty(name = "cloud.microservice.namespace") String namespace,
@ConfigProperty(name = "quarkus.consul-source-config.agent.url") String agentUrl) {
@ConfigProperty(name = "quarkus.consul-source-config.agent.url") String agentUrl,
@ConfigProperty(name = PROP_LOGIN_MODE) Optional<ConsulLoginMode> mode,
@ConfigProperty(name = PROP_LOGIN_AUTH_METHOD) Optional<String> authMethod,
@ConfigProperty(name = PROP_LOGIN_AUDIENCE) Optional<String> audience,
@ConfigProperty(name = PROP_LOGIN_FALLBACK_RECHECK_INTERVAL)
Optional<Duration> fallbackRecheckInterval) {
return tokenStorageFactory.create(new TokenStorageFactory.CreateOptions.Builder()
.consulUrl(agentUrl)
.namespace(namespace)
.m2mSupplier(() -> M2MManager.getInstance().getToken().getTokenValue())
.mode(mode.orElse(null))
.authMethod(authMethod.orElse(null))
.audience(audience.orElse(null))
.fallbackRecheckInterval(fallbackRecheckInterval.orElse(null))
.build());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.netcracker.cloud.quarkus.consul.client;

import com.netcracker.cloud.consul.provider.common.ConsulLoginMode;
import com.netcracker.cloud.consul.provider.common.TokenStorage;
import com.netcracker.cloud.consul.provider.common.TokenStorageFactory;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.TestProfile;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

import java.time.Duration;
import java.util.Map;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@QuarkusTest
@TestProfile(ConsulClientConfigurationKubernetesModeTest.Profile.class)
class ConsulClientConfigurationKubernetesModeTest {

public static class Profile implements QuarkusTestProfile {

@Override
public Map<String, String> getConfigOverrides() {
return Map.of(
"cloud.microservice.name", "test-app",
"cloud.microservice.namespace", "test-namespace",
"quarkus.consul-source-config.enabled", "false",
"quarkus.consul-source-config.agent.url", "http://localhost:8500",
ConsulClientConfiguration.PROP_LOGIN_MODE, "kubernetes",
ConsulClientConfiguration.PROP_LOGIN_AUTH_METHOD, "core-k8s",
ConsulClientConfiguration.PROP_LOGIN_AUDIENCE, "dbaas",
ConsulClientConfiguration.PROP_LOGIN_FALLBACK_RECHECK_INTERVAL, "PT30M"
);
}
}

@InjectMock
TokenStorageFactory tokenStorageFactory;

@Inject
TokenStorage tokenStorage;

@Test
void loginPropertiesReachCreateOptionsAtRuntime() {
when(tokenStorageFactory.create(any())).thenReturn(new ConsulClientConfigurationTest.NoopTokenStorage());

tokenStorage.get();

ArgumentCaptor<TokenStorageFactory.CreateOptions> options =
ArgumentCaptor.forClass(TokenStorageFactory.CreateOptions.class);
verify(tokenStorageFactory).create(options.capture());
Assertions.assertEquals(ConsulLoginMode.KUBERNETES, options.getValue().getMode());
Assertions.assertEquals("core-k8s", options.getValue().getAuthMethod());
Assertions.assertEquals("dbaas", options.getValue().getAudience());
Assertions.assertEquals(Duration.ofMinutes(30), options.getValue().getFallbackRecheckInterval());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.netcracker.cloud.quarkus.consul.client;

import com.netcracker.cloud.consul.provider.common.TokenStorage;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.TestProfile;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.Map;

@QuarkusTest
@TestProfile(ConsulClientConfigurationM2MDisabledTest.Profile.class)
class ConsulClientConfigurationM2MDisabledTest {

public static class Profile implements QuarkusTestProfile {

@Override
public Map<String, String> getConfigOverrides() {
return Map.of(
"cloud.microservice.name", "test-app",
"cloud.microservice.namespace", "test-namespace",
"quarkus.consul-source-config.enabled", "false",
"quarkus.consul-source-config.agent.url", "http://localhost:8500",
"quarkus.consul-source-config.m2m.enabled", "false",
ConsulClientConfiguration.PROP_LOGIN_MODE, "cloud-foundry"
);
}
}

@Inject
TokenStorage tokenStorage;

@Test
void disabledM2MKeepsTheStubAndReadsNoLoginProperties() {
Assertions.assertEquals("", tokenStorage.get());
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
package com.netcracker.cloud.quarkus.consul.client;

import com.netcracker.cloud.consul.provider.common.ConsulLoginMode;
import com.netcracker.cloud.consul.provider.common.TokenStorage;
import com.netcracker.cloud.consul.provider.common.TokenStorageFactory;
import com.netcracker.cloud.security.core.utils.k8s.AudienceName;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.TestProfile;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.wildfly.common.Assert;

import java.util.Map;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@QuarkusTest
@TestProfile(ConsulClientConfigurationTest.Profile.class)
Expand Down Expand Up @@ -44,4 +49,31 @@ void test() {
Assert.assertNotNull(tokenStorage);
verify(tokenStorageFactory, never()).create(any());
}

@Test
void defaultsAreTakenWhenNoLoginPropertyIsSet() {
when(tokenStorageFactory.create(any())).thenReturn(new NoopTokenStorage());

tokenStorage.get();

ArgumentCaptor<TokenStorageFactory.CreateOptions> options =
ArgumentCaptor.forClass(TokenStorageFactory.CreateOptions.class);
verify(tokenStorageFactory).create(options.capture());
Assertions.assertEquals(ConsulLoginMode.KUBERNETES_WITH_M2M_FALLBACK, options.getValue().getMode());
Assertions.assertEquals(TokenStorageFactory.CreateOptions.DEFAULT_AUTH_METHOD, options.getValue().getAuthMethod());
Assertions.assertEquals(AudienceName.NETCRACKER, options.getValue().getAudience());
}

static class NoopTokenStorage implements TokenStorage {

@Override
public String get() {
return "";
}

@Override
public void update(String token) {
// nothing
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.netcracker.cloud.quarkus.consul.client;

import com.netcracker.cloud.consul.provider.common.ConsulLoginMode;
import io.smallrye.config.SmallRyeConfig;
import io.smallrye.config.SmallRyeConfigBuilder;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.time.Duration;
import java.util.Map;

class ConsulLoginModeConfigTest {

private static SmallRyeConfig configWithMode(String mode) {
return new SmallRyeConfigBuilder()
.withDefaultValues(Map.of(ConsulClientConfiguration.PROP_LOGIN_MODE, mode))
.build();
}

private static ConsulLoginMode read(String mode) {
return configWithMode(mode).getValue(ConsulClientConfiguration.PROP_LOGIN_MODE, ConsulLoginMode.class);
}

@Test
void everyModeIsReadFromItsPropertyValue() {
Assertions.assertEquals(ConsulLoginMode.KUBERNETES, read("kubernetes"));
Assertions.assertEquals(ConsulLoginMode.M2M, read("m2m"));
Assertions.assertEquals(ConsulLoginMode.KUBERNETES_WITH_M2M_FALLBACK, read("kubernetes-with-m2m-fallback"));
}

@Test
void unknownModeBreaksTheStart() {
SmallRyeConfig config = configWithMode("cloud-foundry");

Assertions.assertThrows(IllegalArgumentException.class,
() -> config.getValue(ConsulClientConfiguration.PROP_LOGIN_MODE, ConsulLoginMode.class));
}

@Test
void theFallbackRecheckIntervalIsReadFromItsPropertyValue() {
SmallRyeConfig config = new SmallRyeConfigBuilder()
.withDefaultValues(Map.of(ConsulClientConfiguration.PROP_LOGIN_FALLBACK_RECHECK_INTERVAL, "PT30M"))
.build();

Assertions.assertEquals(Duration.ofMinutes(30),
config.getValue(ConsulClientConfiguration.PROP_LOGIN_FALLBACK_RECHECK_INTERVAL, Duration.class));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import java.util.List;
import java.util.Optional;

class RetrableConsulClientTest {
class RetryableConsulClientTest {

ConsulSourceConfig consulDefaultSourceConfig;

Expand All @@ -28,7 +28,7 @@

@Override
public AgentConfig agent() {
return () -> Optional.empty();
return Optional::empty;
}

@Override
Expand All @@ -47,8 +47,8 @@
void continuesAfterFirstError() {
Response<List<GetValue>> expectedResponse = new Response<>(Collections.emptyList(),
0L, true, 0L);
ConsulClient client = Mockito.mock(ConsulClient.class);

Check warning on line 50 in core-quarkus-extensions/config-sources/consul-client/runtime/src/test/java/com/netcracker/cloud/quarkus/consul/client/RetryableConsulClientTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a static import for "mock".

See more on https://sonarcloud.io/project/issues?id=Netcracker_qubership-core-java-libs&issues=AaBcrASNV12QESVy3wVw&open=AaBcrASNV12QESVy3wVw&pullRequest=192
Mockito.when(client.getKVValues(Mockito.anyString(), Mockito.anyString(), Mockito.any(QueryParams.class)))

Check warning on line 51 in core-quarkus-extensions/config-sources/consul-client/runtime/src/test/java/com/netcracker/cloud/quarkus/consul/client/RetryableConsulClientTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a static import for "when".

See more on https://sonarcloud.io/project/issues?id=Netcracker_qubership-core-java-libs&issues=AaBcrASNV12QESVy3wVx&open=AaBcrASNV12QESVy3wVx&pullRequest=192
.thenThrow(new RuntimeException("consul is not available"))
.thenReturn(expectedResponse);

Expand All @@ -59,8 +59,8 @@

@Test
void returnsNullOnRetryAttemptsExceeded() {
ConsulClient client = Mockito.mock(ConsulClient.class);

Check warning on line 62 in core-quarkus-extensions/config-sources/consul-client/runtime/src/test/java/com/netcracker/cloud/quarkus/consul/client/RetryableConsulClientTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a static import for "mock".

See more on https://sonarcloud.io/project/issues?id=Netcracker_qubership-core-java-libs&issues=AaBcrASNV12QESVy3wVy&open=AaBcrASNV12QESVy3wVy&pullRequest=192
Mockito.when(client.getKVValues(Mockito.anyString(), Mockito.anyString(), Mockito.any(QueryParams.class)))

Check warning on line 63 in core-quarkus-extensions/config-sources/consul-client/runtime/src/test/java/com/netcracker/cloud/quarkus/consul/client/RetryableConsulClientTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a static import for "when".

See more on https://sonarcloud.io/project/issues?id=Netcracker_qubership-core-java-libs&issues=AaBcrASNV12QESVy3wVz&open=AaBcrASNV12QESVy3wVz&pullRequest=192
.thenThrow(new TransportException(new RuntimeException("consul is not available1")));

RetryableConsulClient retryableClient = new RetryableConsulClient(client, new TokenStorageStub());
Expand All @@ -74,7 +74,7 @@
@Test
void oneTryAfterConsideredBad() {
ConsulClient client = Mockito.mock(ConsulClient.class);
Mockito.when(client.getKVValues(Mockito.anyString(), Mockito.anyString(), Mockito.any(QueryParams.class)))

Check warning on line 77 in core-quarkus-extensions/config-sources/consul-client/runtime/src/test/java/com/netcracker/cloud/quarkus/consul/client/RetryableConsulClientTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a static import for "when".

See more on https://sonarcloud.io/project/issues?id=Netcracker_qubership-core-java-libs&issues=AaBcrASNV12QESVy3wV1&open=AaBcrASNV12QESVy3wV1&pullRequest=192
.thenThrow(new RuntimeException("consul is not available1"));

RetryableConsulClient retryableClient = new RetryableConsulClient(client, new TokenStorageStub());
Expand All @@ -84,6 +84,6 @@
Response<List<GetValue>> res = retryableClient.getKVValues("config/namespace/application", new QueryParams(consulDefaultSourceConfig.waitTime(), 0L)); // single client call
Assertions.assertNotNull(res);
Assertions.assertNull(res.getValue());
Mockito.verify(client, Mockito.times(6)).getKVValues(Mockito.anyString(), Mockito.anyString(), Mockito.any(QueryParams.class));

Check warning on line 87 in core-quarkus-extensions/config-sources/consul-client/runtime/src/test/java/com/netcracker/cloud/quarkus/consul/client/RetryableConsulClientTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a static import for "verify".

See more on https://sonarcloud.io/project/issues?id=Netcracker_qubership-core-java-libs&issues=AaBcrASNV12QESVy3wV2&open=AaBcrASNV12QESVy3wV2&pullRequest=192

Check warning on line 87 in core-quarkus-extensions/config-sources/consul-client/runtime/src/test/java/com/netcracker/cloud/quarkus/consul/client/RetryableConsulClientTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a static import for "times".

See more on https://sonarcloud.io/project/issues?id=Netcracker_qubership-core-java-libs&issues=AaBcrASNV12QESVy3wV3&open=AaBcrASNV12QESVy3wV3&pullRequest=192
}
}
Loading
Loading