Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.springframework.cloud.bootstrap.config.BootstrapPropertySource;
import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.cloud.kubernetes.commons.config.MountConfigMapPropertySource;
import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySource;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
Expand Down Expand Up @@ -110,6 +111,10 @@ else if (source instanceof MountConfigMapPropertySource mountConfigMapPropertySo
// we know that the type is correct here
managedSources.add((S) mountConfigMapPropertySource);
}
else if (source instanceof SecretsPropertySource secretsPropertySource) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when we were mounting secrets, reload was not working, since there was no this check in place. Even it I'm fixing it here, it will eventually be dropped, since we will move from mounting to config import.

// we know that the type is correct here
managedSources.add((S) secretsPropertySource);
}
else if (source instanceof BootstrapPropertySource<?> bootstrapPropertySource) {
PropertySource<?> propertySource = bootstrapPropertySource.getDelegate();
LOG.debug(() -> "bootstrap delegate class : " + propertySource.getClass());
Expand All @@ -120,6 +125,10 @@ else if (propertySource instanceof MountConfigMapPropertySource mountConfigMapPr
// we know that the type is correct here
managedSources.add((S) mountConfigMapPropertySource);
}
else if (propertySource instanceof SecretsPropertySource secretsPropertySource) {
// we know that the type is correct here
managedSources.add((S) secretsPropertySource);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@

import org.springframework.cloud.bootstrap.config.BootstrapPropertySource;
import org.springframework.cloud.kubernetes.commons.config.MountConfigMapPropertySource;
import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySource;
import org.springframework.cloud.kubernetes.commons.config.SourceData;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.MapPropertySource;
Expand Down Expand Up @@ -127,8 +129,8 @@ void testFindPropertySources() {
MockEnvironment environment = new MockEnvironment();
MutablePropertySources propertySources = environment.getPropertySources();
propertySources.addFirst(new OneComposite());
propertySources.addFirst(new PlainPropertySource("plain"));
propertySources.addFirst(new OneBootstrap(new EnumerablePropertySource<>("enumerable") {
propertySources.addFirst(new PlainPropertySource<>("plain"));
propertySources.addFirst(new OneBootstrap<>(new EnumerablePropertySource<>("enumerable") {
@Override
public String[] getPropertyNames() {
return new String[0];
Expand All @@ -143,11 +145,35 @@ public Object getProperty(String name) {

List<? extends PropertySource> result = ConfigReloadUtil.findPropertySources(PlainPropertySource.class,
environment);
Assertions.assertEquals(4, result.size());
Assertions.assertEquals(3, result.size());
Assertions.assertEquals("b", result.get(0).getProperty("a"));
Assertions.assertEquals("plain", result.get(1).getProperty(""));
Assertions.assertEquals("from-bootstrap", result.get(2).getProperty(""));
Assertions.assertEquals("from-inner-two-composite", result.get(3).getProperty(""));
Assertions.assertEquals("from-inner-two-composite", result.get(2).getProperty(""));
}

@Test
void testSecretsPropertySource() {
MockEnvironment environment = new MockEnvironment();
MutablePropertySources propertySources = environment.getPropertySources();
propertySources.addFirst(new SecretsPropertySource(new SourceData("secret", Map.of("a", "b"))));

List<? extends PropertySource> result = ConfigReloadUtil.findPropertySources(PlainPropertySource.class,
environment);
assertThat(result.size()).isEqualTo(1);
assertThat(result.get(0).getProperty("a")).isEqualTo("b");
}

@Test
void testBootstrapSecretsPropertySource() {
MockEnvironment environment = new MockEnvironment();
MutablePropertySources propertySources = environment.getPropertySources();
propertySources
.addFirst(new OneBootstrap<>(new SecretsPropertySource(new SourceData("secret", Map.of("a", "b")))));

List<? extends PropertySource> result = ConfigReloadUtil.findPropertySources(PlainPropertySource.class,
environment);
assertThat(result.size()).isEqualTo(1);
assertThat(result.get(0).getProperty("a")).isEqualTo("b");
}

private static final class OneComposite extends CompositePropertySource {
Expand All @@ -171,12 +197,12 @@ private TwoComposite() {

@Override
public Collection<PropertySource<?>> getPropertySources() {
return List.of(new PlainPropertySource("from-inner-two-composite"));
return List.of(new PlainPropertySource<>("from-inner-two-composite"));
}

}

private static final class PlainPropertySource extends PropertySource<String> {
private static final class PlainPropertySource<T> extends PropertySource<T> {

private PlainPropertySource(String name) {
super(name);
Expand All @@ -189,15 +215,18 @@ public Object getProperty(String name) {

}

private static final class OneBootstrap extends BootstrapPropertySource<String> {
private static final class OneBootstrap<T> extends BootstrapPropertySource<T> {

private final EnumerablePropertySource<T> delegate;

private OneBootstrap(EnumerablePropertySource<String> delegate) {
private OneBootstrap(EnumerablePropertySource<T> delegate) {
super(delegate);
this.delegate = delegate;
}

@Override
public PropertySource<String> getDelegate() {
return new PlainPropertySource("from-bootstrap");
public PropertySource<T> getDelegate() {
return delegate;
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright 2013-2025 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.kubernetes.k8s.client.reload.it;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Map;

import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.models.V1Secret;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.testcontainers.k3s.K3sContainer;

import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
import org.springframework.cloud.kubernetes.integration.tests.commons.Phase;
import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util;
import org.springframework.http.HttpMethod;
import org.springframework.web.reactive.function.client.WebClient;

import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.builder;
import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.retrySpec;

/**
* @author wind57
*/
class K8sClientSecretMountBootstrapPollingIT extends K8sClientReloadBase {

private static final String IMAGE_NAME = "spring-cloud-kubernetes-k8s-client-reload";

private static final String NAMESPACE = "default";

private static final K3sContainer K3S = Commons.container();

private static Util util;

private static CoreV1Api coreV1Api;

@BeforeAll
static void beforeAllLocal() throws Exception {
K3S.start();
Commons.validateImage(IMAGE_NAME, K3S);
Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S);

util = new Util(K3S);
coreV1Api = new CoreV1Api();
util.setUp(NAMESPACE);
manifestsSecret(Phase.CREATE, util, NAMESPACE, IMAGE_NAME);
}

@AfterAll
static void afterAll() {
manifestsSecret(Phase.DELETE, util, NAMESPACE, IMAGE_NAME);
}

/**
* <pre>
* - we have bootstrap enabled
* - we will 'locate' property sources from secrets.
* - there are no explicit secrets to search for, but what we will also read,
* is 'spring.cloud.kubernetes.secret.paths', which we have set to
* '/tmp/application.properties'
* in this test. That is populated by the volumeMounts (see mount/deployment-with-secret.yaml)
* - we first assert that we are actually reading the path based source
*
* - we then change the secret content, wait for k8s to pick it up and replace them
* - our polling will then detect that change, and trigger a reload.
* </pre>
*/
@Test
void test() throws Exception {
WebClient webClient = builder().baseUrl("http://localhost:32321/secret").build();
String result = webClient.method(HttpMethod.GET)
.retrieve()
.bodyToMono(String.class)
.retryWhen(retrySpec())
.block();

// we first read the initial value from the configmap
assertThat(result).isEqualTo("initial");

// replace data in secret and wait for k8s to pick it up
// our polling will detect that and restart the app
V1Secret secret = (V1Secret) util.yaml("mount/secret.yaml");
secret.setData(Map.of("from.properties.secret.key", "as-mount-changed".getBytes(StandardCharsets.UTF_8)));
coreV1Api.replaceNamespacedSecret("secret-reload", NAMESPACE, secret, null, null, null, null);

Commons.waitForLogStatement("Detected change in config maps/secrets, reload will be triggered", K3S,
IMAGE_NAME);

await().atMost(Duration.ofSeconds(120))
.pollInterval(Duration.ofSeconds(1))
.until(() -> webClient.method(HttpMethod.GET)
.retrieve()
.bodyToMono(String.class)
.retryWhen(retrySpec())
.block()
.equals("as-mount-changed"));
}

}
Loading